feat(org-detail): Widerrufen-Knopf entfernt, Warnschwelle 0 = aus

Zwei Rückmeldungen des Nutzers.

- Der Widerrufen-Knopf in der Lizenz-Tabelle ist raus. Eine neue Lizenz
  widerruft die alte automatisch, und einen Kunden abschalten erledigt
  der prominente Org-Status-Umschalter. Der API-Endpoint /revoke bleibt.
- Die Warnschwelle lässt sich jetzt abschalten, 0 = aus, in allen drei
  Formularen (Budget, Neue Lizenz, Org-Anlage) samt Modell- und
  Endpoint-Validierung. Gegenstück im Monitor ist Commit 17b1886
  (bisher fiel eine 0 dort still auf die Voreinstellung 80 zurück).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Dieser Commit ist enthalten in:
claude-dev
2026-07-25 15:52:40 +00:00
Ursprung db0a8c1d9e
Commit 83af0d6897
5 geänderte Dateien mit 20 neuen und 25 gelöschten Zeilen

Datei anzeigen

@@ -36,7 +36,7 @@ class LicenseParams(BaseModel):
token_budget_usd: Optional[float] = None token_budget_usd: Optional[float] = None
credits_total: Optional[int] = None credits_total: Optional[int] = None
cost_per_credit: Optional[float] = None cost_per_credit: Optional[float] = None
budget_warning_percent: Optional[int] = Field(default=80, ge=1, le=100) budget_warning_percent: Optional[int] = Field(default=80, ge=0, le=100) # 0 = Warnung aus
unlimited_budget: bool = False unlimited_budget: bool = False
# 'monthly' = Kontingent wird zum Monatswechsel neu gefuellt (der Monitor # 'monthly' = Kontingent wird zum Monatswechsel neu gefuellt (der Monitor
# setzt den Verbrauch traege zurueck), 'total' = gilt fuer die Laufzeit. # setzt den Verbrauch traege zurueck), 'total' = gilt fuer die Laufzeit.

Datei anzeigen

@@ -196,8 +196,8 @@ async def update_budget(license_id: int, data: dict, request: Request, admin=Dep
pct = int(data["budget_warning_percent"]) pct = int(data["budget_warning_percent"])
except (TypeError, ValueError): except (TypeError, ValueError):
raise HTTPException(status_code=400, detail="budget_warning_percent muss eine Zahl sein") raise HTTPException(status_code=400, detail="budget_warning_percent muss eine Zahl sein")
if not 1 <= pct <= 100: if not 0 <= pct <= 100:
raise HTTPException(status_code=400, detail="budget_warning_percent muss zwischen 1 und 100 liegen") raise HTTPException(status_code=400, detail="budget_warning_percent muss zwischen 0 und 100 liegen (0 = Warnung aus)")
data["budget_warning_percent"] = pct data["budget_warning_percent"] = pct
if "max_users" in data: if "max_users" in data:
try: try:

Datei anzeigen

@@ -263,8 +263,8 @@
</label> </label>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="editWarnPercent">Warnschwelle (%)</label> <label for="editWarnPercent">Warnschwelle (%, 0 = aus)</label>
<input type="number" id="editWarnPercent" min="1" max="100" placeholder="80"> <input type="number" id="editWarnPercent" min="0" max="100" placeholder="80">
</div> </div>
<details class="adv-section"> <details class="adv-section">
<summary> <summary>
@@ -625,8 +625,8 @@
</label> </label>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="newOrgLicWarnPercent">Warnschwelle (%)</label> <label for="newOrgLicWarnPercent">Warnschwelle (%, 0 = aus)</label>
<input type="number" id="newOrgLicWarnPercent" value="80" min="1" max="100"> <input type="number" id="newOrgLicWarnPercent" value="80" min="0" max="100">
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="newOrgLicCostPerCredit">Kosten pro Credit (USD)</label> <label for="newOrgLicCostPerCredit">Kosten pro Credit (USD)</label>
@@ -724,8 +724,8 @@
</label> </label>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="newLicWarnPercent">Warnschwelle (%)</label> <label for="newLicWarnPercent">Warnschwelle (%, 0 = aus)</label>
<input type="number" id="newLicWarnPercent" value="80" min="1" max="100"> <input type="number" id="newLicWarnPercent" value="80" min="0" max="100">
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="newLicCostPerCredit">Kosten pro Credit (USD)</label> <label for="newLicCostPerCredit">Kosten pro Credit (USD)</label>
@@ -1162,7 +1162,7 @@
</div> </div>
<script src="/static/js/a11y.js?v=20260725a"></script> <script src="/static/js/a11y.js?v=20260725a"></script>
<script src="/static/js/app.js?v=20260725j"></script> <script src="/static/js/app.js?v=20260725k"></script>
<script src="/static/js/sources.js?v=20260522x2"></script> <script src="/static/js/sources.js?v=20260522x2"></script>
<script src="/static/js/x-scraper.js?v=20260522a"></script> <script src="/static/js/x-scraper.js?v=20260522a"></script>
<script src="/static/js/source-health.js?v=20260509l"></script> <script src="/static/js/source-health.js?v=20260509l"></script>

Datei anzeigen

@@ -561,7 +561,6 @@ async function loadOrgLicenses(orgId) {
<td> <td>
${l.status === "active" ? ` ${l.status === "active" ? `
<button class="btn btn-secondary btn-small" onclick="extendLicense(${l.id})">Verlängern</button> <button class="btn btn-secondary btn-small" onclick="extendLicense(${l.id})">Verlängern</button>
<button class="btn btn-danger btn-small" onclick="confirmRevokeLicense(${l.id})">Widerrufen</button>
` : ""} ` : ""}
</td> </td>
</tr> </tr>
@@ -602,20 +601,9 @@ document.addEventListener("DOMContentLoaded", function () {
}); });
}); });
function confirmRevokeLicense(licId) { // Der Widerrufen-Knopf ist bewusst entfernt. Eine neue Lizenz widerruft die
showConfirm( // alte automatisch, und einen Kunden abschalten erledigt der Org-Status-
"Lizenz widerrufen", // Umschalter in der Kopfzeile. Der API-Endpoint /revoke bleibt bestehen.
"Soll die Lizenz wirklich widerrufen werden? Nutzer können dann nur noch lesen.",
async () => {
try {
await API.put(`/api/licenses/${licId}/revoke`);
if (currentOrgId) loadOrgLicenses(currentOrgId);
} catch (err) {
showToast(err.message, "error");
}
}
);
}
// --- Expiring Licenses (global view) --- // --- Expiring Licenses (global view) ---
async function loadExpiringLicenses() { async function loadExpiringLicenses() {

Datei anzeigen

@@ -54,6 +54,13 @@ def test_license_params_credits_defaults():
assert p.budget_warning_percent == 80 assert p.budget_warning_percent == 80
def test_license_params_warnschwelle_null_ist_aus():
p = LicenseParams(license_type="trial", budget_warning_percent=0)
assert p.budget_warning_percent == 0
with pytest.raises(ValidationError):
LicenseParams(license_type="trial", budget_warning_percent=101)
def test_license_params_period_pattern(): def test_license_params_period_pattern():
LicenseParams(license_type="trial", credits_period="monthly") LicenseParams(license_type="trial", credits_period="monthly")
LicenseParams(license_type="trial", credits_period="total") LicenseParams(license_type="trial", credits_period="total")