feat(kontingent): Credits-Stellwerte je Lizenz + Lizenz beim Org-Anlegen

Bedienseite für das Credits-Monatskontingent des Monitors. Das Portal
setzt jetzt alle Stellwerte, der Monitor bleibt Herr der Laufzeitfelder.

- Lizenz-Modal und Budget-Formular um Abrechnungszeitraum (monthly/total),
  Übertrag-Checkbox und Warnschwelle erweitert, Vorbelegungen auf das
  beschlossene Modell (10.000 Credits, monatlich, Übertrag an, 0,20 USD).
- Neue Organisation kann direkt eine Lizenz mit Kontingent anlegen
  (OrgCreate.license, verschachtelt). Org- und Lizenz-Insert laufen in
  EINER Transaktion, schlägt der Lizenz-Teil fehl, entsteht keine
  halbe Organisation. Gemeinsamer Helper insert_license_row statt
  Duplikation der Widerruf- und Audit-Logik.
- PUT /api/token-usage/budget validiert credits_period (monthly|total)
  und budget_warning_percent (1 bis 100), credits_rollover wird wie
  unlimited_budget normalisiert. Laufzeitfelder bleiben bewusst
  außerhalb der Whitelist.
- Overview und Org-Detail rechnen den Rest jetzt gegen verfügbar =
  Kontingent + Übertrag, dieselbe Bezugsgröße wie der Hard-Stop im
  Monitor. Neuer read-only Block Laufzeitwerte im Token-Subtab.
- Startup-Check loggt einen deutlichen Fehler, wenn die licenses-Tabelle
  die Credits-Spalten noch nicht hat (falsche Deploy-Reihenfolge).
- Migration migrations/2026-07-25_credits_period.py (idempotent,
  wortgleich mit der Monitor-Migration) für die Portal-Staging-DB,
  auf Live gilt: Monitor VOR Portal promoten.
- 13 neue Modell-Tests, pytest grün (die zwei bekannten
  shared-Import-Fehlschläge bestehen unverändert seit vorher).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Dieser Commit ist enthalten in:
claude-dev
2026-07-25 13:11:02 +00:00
Ursprung c95903fb3a
Commit e0c39de21f
10 geänderte Dateien mit 566 neuen und 92 gelöschten Zeilen

Datei anzeigen

@@ -267,18 +267,35 @@
</div>
<div class="form-group">
<label for="editCreditsTotal">Credits-Kontingent</label>
<input type="number" id="editCreditsTotal" placeholder="z.B. 600000">
<input type="number" id="editCreditsTotal" placeholder="z.B. 10000">
</div>
<div class="form-group">
<label for="editCreditsPeriod">Abrechnungszeitraum</label>
<select id="editCreditsPeriod" onchange="onEditPeriodChange()">
<option value="monthly">Monatlich (füllt sich zum Monatswechsel neu)</option>
<option value="total">Gesamtlaufzeit</option>
</select>
</div>
<div class="form-group" style="display:flex; align-items:flex-end;">
<label style="display:flex; align-items:center; gap:8px; cursor:pointer; margin:0;">
<input type="checkbox" id="editCreditsRollover">
<span>Übertrag ungenutzter Credits in den Folgemonat</span>
</label>
</div>
<div class="form-group">
<label for="editWarnPercent">Warnschwelle (%)</label>
<input type="number" id="editWarnPercent" min="1" max="100" placeholder="80">
</div>
<div class="form-group">
<label for="editCostPerCredit">Kosten pro Credit (USD)</label>
<input type="number" id="editCostPerCredit" step="0.0001" placeholder="z.B. 0.0033">
<input type="number" id="editCostPerCredit" step="0.01" placeholder="0.20">
</div>
<div class="form-group">
<label for="editBudgetUsd">Budget-Limit (USD)</label>
<input type="number" id="editBudgetUsd" step="0.01" placeholder="z.B. 2000">
</div>
<div class="form-group">
<label for="editCreditsUsed">Verbrauchte Credits</label>
<label for="editCreditsUsed">Verbrauchte Credits (laufende Periode)</label>
<input type="number" id="editCreditsUsed" step="0.01" placeholder="Aktueller Verbrauch">
</div>
<div style="grid-column:1/-1; display:flex; gap:8px; align-items:center;">
@@ -288,6 +305,31 @@
</form>
</div>
</div>
<h3 style="margin-top:24px;">Laufzeitwerte</h3>
<div class="card" style="margin-top:12px;">
<div class="card-body">
<p class="text-secondary" style="margin:0 0 12px; font-size:13px;">
Diese Werte verwaltet der Monitor selbst, sie sind hier nicht änderbar.
Stand ist die letzte Lizenzprüfung des Monitors, nach einem Monatswechsel
können sie kurzzeitig noch die Vorperiode zeigen.
</p>
<div style="display:grid; grid-template-columns:1fr 1fr 1fr; gap:12px;">
<div class="form-group">
<label>Übertrag aus dem Vormonat</label>
<div id="viewCreditsCarried" style="font-size:16px; font-weight:600;">-</div>
</div>
<div class="form-group">
<label>Laufende Periode</label>
<div id="viewCreditsPeriodStart" style="font-size:16px; font-weight:600;">-</div>
</div>
<div class="form-group">
<label>Verfügbar gesamt (Kontingent + Übertrag)</label>
<div id="viewCreditsAvailable" style="font-size:16px; font-weight:600;">-</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
@@ -574,6 +616,63 @@
</select>
<small class="text-secondary">Steuert die Ausgabesprache der KI-Pipeline (Lagebild, Faktencheck, Recherche) und die sichtbarsten UI-Strings im Monitor.</small>
</div>
<div class="form-group" style="background:var(--bg-tertiary,#0f172a); padding:8px 12px; border-radius:6px; border:1px solid var(--border);">
<label style="display:flex;align-items:center;gap:8px;cursor:pointer;margin:0;">
<input type="checkbox" id="newOrgWithLicense" checked onchange="onNewOrgLicenseToggle()">
<span><strong>Direkt eine Lizenz mit Credits-Kontingent anlegen</strong></span>
</label>
</div>
<div id="newOrgLicenseFields">
<div class="form-group">
<label for="newOrgLicType">Lizenztyp</label>
<select id="newOrgLicType" onchange="onNewOrgLicTypeChange()">
<option value="trial" selected>Trial</option>
<option value="annual">Jahreslizenz</option>
<option value="permanent">Permanent</option>
</select>
</div>
<div class="form-group">
<label for="newOrgLicMaxUsers">Maximale Nutzer</label>
<input type="number" id="newOrgLicMaxUsers" value="5" min="1" max="1000">
</div>
<div class="form-group" id="newOrgLicDurationGroup">
<label for="newOrgLicDuration">Laufzeit (Tage)</label>
<input type="number" id="newOrgLicDuration" value="14" min="1" max="3650">
<div class="text-muted mt-8" style="font-size: 12px;">Trial: Standard 14 Tage, Jahreslizenz: Standard 365 Tage</div>
</div>
<div class="form-group" style="background:var(--bg-tertiary,#0f172a); padding:8px 12px; border-radius:6px; border:1px solid var(--border);">
<label style="display:flex;align-items:center;gap:8px;cursor:pointer;margin:0;">
<input type="checkbox" id="newOrgLicUnlimited" onchange="onNewOrgLicUnlimitedToggle()">
<span><strong>Unbegrenztes Budget</strong> &mdash; getrackt, aber kein Hard-Stop</span>
</label>
</div>
<div class="form-group">
<label for="newOrgLicCreditsTotal">Credits-Kontingent</label>
<input type="number" id="newOrgLicCreditsTotal" value="10000" placeholder="z.B. 10000">
</div>
<div class="form-group">
<label for="newOrgLicCreditsPeriod">Abrechnungszeitraum</label>
<select id="newOrgLicCreditsPeriod" onchange="onNewOrgLicPeriodChange()">
<option value="monthly" selected>Monatlich (Kontingent füllt sich zum Monatswechsel neu)</option>
<option value="total">Gesamtlaufzeit</option>
</select>
</div>
<div class="form-group">
<label style="display:flex;align-items:center;gap:8px;cursor:pointer;margin:0;">
<input type="checkbox" id="newOrgLicRollover" checked>
<span>Übertrag ungenutzter Credits in den Folgemonat (gedeckelt auf ein Monatskontingent)</span>
</label>
</div>
<div class="form-group">
<label for="newOrgLicWarnPercent">Warnschwelle (%)</label>
<input type="number" id="newOrgLicWarnPercent" value="80" min="1" max="100">
</div>
<div class="form-group">
<label for="newOrgLicCostPerCredit">Kosten pro Credit (USD)</label>
<input type="number" id="newOrgLicCostPerCredit" step="0.01" value="0.20" placeholder="0.20">
</div>
</div>
<div id="newOrgError" class="error-msg" style="display:none"></div>
</div>
<div class="modal-footer">
@@ -649,11 +748,28 @@
</div>
<div class="form-group">
<label for="newLicCreditsTotal">Credits-Kontingent</label>
<input type="number" id="newLicCreditsTotal" placeholder="z.B. 600000">
<input type="number" id="newLicCreditsTotal" value="10000" placeholder="z.B. 10000">
</div>
<div class="form-group">
<label for="newLicCreditsPeriod">Abrechnungszeitraum</label>
<select id="newLicCreditsPeriod" onchange="onNewLicPeriodChange()">
<option value="monthly" selected>Monatlich (Kontingent füllt sich zum Monatswechsel neu)</option>
<option value="total">Gesamtlaufzeit</option>
</select>
</div>
<div class="form-group">
<label style="display:flex;align-items:center;gap:8px;cursor:pointer;margin:0;">
<input type="checkbox" id="newLicRollover" checked>
<span>Übertrag ungenutzter Credits in den Folgemonat (gedeckelt auf ein Monatskontingent)</span>
</label>
</div>
<div class="form-group">
<label for="newLicWarnPercent">Warnschwelle (%)</label>
<input type="number" id="newLicWarnPercent" value="80" min="1" max="100">
</div>
<div class="form-group">
<label for="newLicCostPerCredit">Kosten pro Credit (USD)</label>
<input type="number" id="newLicCostPerCredit" step="0.0001" placeholder="z.B. 0.0033">
<input type="number" id="newLicCostPerCredit" step="0.01" placeholder="0.20">
</div>
<div class="form-group">
<label for="newLicBudgetUsd">Budget-Limit (USD)</label>
@@ -1038,7 +1154,7 @@
</div>
</div>
<script src="/static/js/app.js?v=20260522a"></script>
<script src="/static/js/app.js?v=20260725a"></script>
<script src="/static/js/sources.js?v=20260522x2"></script>
<script src="/static/js/x-scraper.js?v=20260522a"></script>
<script src="/static/js/source-health.js?v=20260509l"></script>