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

@@ -0,0 +1,57 @@
"""Migration 2026-07-25: Credits-Perioden-Spalten in licenses.
Legt die fünf Spalten des Credits-Monatskontingents an, exakt dieselben
Statements wie die Monitor-Migration (AegisSight-Monitor, src/database.py,
Block "Credits-Periode"). Idempotent, ein zweiter Lauf ist ein No-op.
Hintergrund: Der Monitor legt diese Spalten beim Start selbst an. Die
Portal-Staging-Umgebung nutzt aber eine EIGENE Datenbank, auf der kein
Monitor läuft, deshalb braucht sie dieses Skript. Auf der Live-DB ist es
nur ein Fallback für den Fall, dass das Portal vor dem Monitor deployt wird.
Ausführung:
DB_PATH=/home/claude-dev/AegisSight-Monitor-staging/data/osint.db python3 migrations/2026-07-25_credits_period.py
DB_PATH=/home/claude-dev/osint-data/osint.db python3 migrations/2026-07-25_credits_period.py
"""
import os
import sqlite3
import sys
# Spaltenname -> ALTER-Statement, wortgleich mit der Monitor-Migration
COLUMNS = {
"credits_period": "ALTER TABLE licenses ADD COLUMN credits_period TEXT DEFAULT 'monthly'",
"credits_period_start": "ALTER TABLE licenses ADD COLUMN credits_period_start TEXT",
"credits_rollover": "ALTER TABLE licenses ADD COLUMN credits_rollover INTEGER DEFAULT 0",
"credits_carried": "ALTER TABLE licenses ADD COLUMN credits_carried REAL DEFAULT 0",
"budget_warning_sent": "ALTER TABLE licenses ADD COLUMN budget_warning_sent INTEGER DEFAULT 0",
}
def main(db_path: str) -> int:
if not os.path.exists(db_path):
print(f"FEHLER: DB nicht gefunden: {db_path}", file=sys.stderr)
return 1
conn = sqlite3.connect(db_path, timeout=60)
conn.execute("PRAGMA busy_timeout = 60000")
conn.execute("PRAGMA journal_mode = WAL")
print(f"Migration auf {db_path}")
cols = [c[1] for c in conn.execute("PRAGMA table_info(licenses)")]
for name, stmt in COLUMNS.items():
if name not in cols:
conn.execute(stmt)
print(f" + licenses.{name} Spalte hinzugefügt")
else:
print(f" = licenses.{name} war bereits da")
conn.commit()
conn.close()
print("Migration abgeschlossen.")
return 0
if __name__ == "__main__":
db_path = os.environ.get("DB_PATH", "/home/claude-dev/osint-data/osint.db")
sys.exit(main(db_path))