Token-Budget Hard-Stop + Banner bei aufgebrauchtem Budget

- check_license() liefert jetzt unlimited_budget, credits_total, credits_used,
  read_only_reason. Bei nicht-unlimited UND credits_used >= credits_total wird
  status=budget_exceeded, read_only=True gesetzt.
- require_writable_license blockiert mit 403 + X-License-Status-Header je nach Reason.
- /api/auth/me liefert read_only_reason und unlimited_budget; credits_percent_used
  wird nicht mehr auf 100 gekappt (echte Prozente).
- Frontend: Banner-Text dynamisch je nach reason (budget_exceeded/expired/...).
  Refresh-Button bei read_only deaktiviert + Tooltip. Globaler 403-Handler in
  api.js: bei X-License-Status -> Banner + Toast aktualisieren.
Dieser Commit ist enthalten in:
Claude Code
2026-05-02 20:16:25 +00:00
Ursprung 2b1e8c3632
Commit ee83f38edf
6 geänderte Dateien mit 123 neuen und 12 gelöschten Zeilen

Datei anzeigen

@@ -11,7 +11,8 @@ async def check_license(db: aiosqlite.Connection, organization_id: int) -> dict:
"""Prueft den Lizenzstatus einer Organisation.
Returns:
dict mit: valid, status, license_type, max_users, current_users, read_only, message
dict mit: valid, status, license_type, max_users, current_users, read_only,
read_only_reason, message, unlimited_budget, credits_total, credits_used
"""
# Organisation pruefen
cursor = await db.execute(
@@ -20,10 +21,14 @@ async def check_license(db: aiosqlite.Connection, organization_id: int) -> dict:
)
org = await cursor.fetchone()
if not org:
return {"valid": False, "status": "not_found", "read_only": True, "message": "Organisation nicht gefunden"}
return {"valid": False, "status": "not_found", "read_only": True,
"read_only_reason": "not_found",
"message": "Organisation nicht gefunden"}
if not org["is_active"]:
return {"valid": False, "status": "org_disabled", "read_only": True, "message": "Organisation deaktiviert"}
return {"valid": False, "status": "org_disabled", "read_only": True,
"read_only_reason": "org_disabled",
"message": "Organisation deaktiviert"}
# Aktive Lizenz suchen
cursor = await db.execute(
@@ -35,7 +40,15 @@ async def check_license(db: aiosqlite.Connection, organization_id: int) -> dict:
license_row = await cursor.fetchone()
if not license_row:
return {"valid": False, "status": "no_license", "read_only": True, "message": "Keine aktive Lizenz"}
return {"valid": False, "status": "no_license", "read_only": True,
"read_only_reason": "no_license",
"message": "Keine aktive Lizenz"}
# Felder zur weiteren Verwendung extrahieren
lic_dict = dict(license_row)
unlimited_budget = bool(lic_dict.get("unlimited_budget"))
credits_total = lic_dict.get("credits_total")
credits_used = lic_dict.get("credits_used") or 0
# Ablauf pruefen
now = datetime.now(TIMEZONE)
@@ -52,11 +65,21 @@ async def check_license(db: aiosqlite.Connection, organization_id: int) -> dict:
"status": "expired",
"license_type": license_row["license_type"],
"read_only": True,
"read_only_reason": "expired",
"message": "Lizenz abgelaufen",
"unlimited_budget": unlimited_budget,
"credits_total": credits_total,
"credits_used": credits_used,
}
except (ValueError, TypeError):
pass
# Budget-Check (Hard-Stop bei aufgebrauchten Credits, ausser unlimited)
budget_exceeded = False
if not unlimited_budget and credits_total and credits_total > 0:
if credits_used >= credits_total:
budget_exceeded = True
# Nutzerzahl pruefen
cursor = await db.execute(
"SELECT COUNT(*) as cnt FROM users WHERE organization_id = ? AND is_active = 1",
@@ -64,6 +87,21 @@ async def check_license(db: aiosqlite.Connection, organization_id: int) -> dict:
)
current_users = (await cursor.fetchone())["cnt"]
if budget_exceeded:
return {
"valid": True, # Lizenz ist gueltig, aber Budget aufgebraucht -> read-only
"status": "budget_exceeded",
"license_type": license_row["license_type"],
"max_users": license_row["max_users"],
"current_users": current_users,
"read_only": True,
"read_only_reason": "budget_exceeded",
"message": "Token-Budget aufgebraucht",
"unlimited_budget": False,
"credits_total": credits_total,
"credits_used": credits_used,
}
return {
"valid": True,
"status": license_row["status"],
@@ -71,7 +109,11 @@ async def check_license(db: aiosqlite.Connection, organization_id: int) -> dict:
"max_users": license_row["max_users"],
"current_users": current_users,
"read_only": False,
"read_only_reason": None,
"message": "Lizenz aktiv",
"unlimited_budget": unlimited_budget,
"credits_total": credits_total,
"credits_used": credits_used,
}