feat: Web-Such-Budget pro Nutzer/Tag

Neue Tabelle web_search_usage + Store-Zaehler (web_search_count /
add_web_search_usage, kaskadiert in delete_user). Pro-Nutzer-Limit
web_search_daily_limit (Pref > global; 0 = unbegrenzt) in quota.py
(web_search_within_budget). ws.py: Gate vor _resolve (Budget aufgebraucht ->
Web-Suche fuer den Turn aus) + Zaehlung nach dem Turn (trace.web_searches,
gezaehlt im Orchestrator je on_citations). Admin: Feld + UI-Eingabe.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Dieter Schlüter 2026-06-30 09:22:06 +02:00
commit 88c8c9df7f
8 changed files with 106 additions and 5 deletions

View file

@ -1226,6 +1226,11 @@ function buildUserCard(u) {
<input type="checkbox" class="ws-enabled" /> Web-Suche für aktuelle Fragen erlauben
<span class="ws-status text-slate-400"></span>
</label>
<div class="mt-1.5 flex items-center gap-2">
<span class="text-xs text-slate-500 dark:text-slate-400">max. Suchen pro Tag:</span>
<input type="number" min="0" class="ws-limit w-20 rounded-lg border border-slate-300 dark:border-slate-600 bg-transparent px-2 py-1 text-xs focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="∞" />
<span class="ws-limit-status text-xs text-slate-400"></span>
</div>
</div>
<!-- Notfall-Profil -->
@ -1331,6 +1336,24 @@ function buildUserCard(u) {
}
});
}
// Web-Such-Tagesbudget (leer/0 = unbegrenzt) — sofort speichern
const wsLimit = card.querySelector(".ws-limit");
const wsLimitStatus = card.querySelector(".ws-limit-status");
if (wsLimit) {
const lim = Number(u.web_search_daily_limit);
wsLimit.value = Number.isFinite(lim) && lim > 0 ? lim : "";
wsLimit.addEventListener("change", async () => {
const n = parseInt(wsLimit.value, 10);
const val = Number.isFinite(n) && n > 0 ? n : 0; // leer/0 = unbegrenzt
if (wsLimitStatus) wsLimitStatus.textContent = "speichere …";
const res = await adminFetch(`/api/admin/users/${uid}/prefs`, "PUT", { web_search_daily_limit: val });
if (wsLimitStatus) {
wsLimitStatus.textContent = res ? "✓" : "Fehler";
setTimeout(() => { wsLimitStatus.textContent = ""; }, 2000);
}
});
}
const profBtn = card.querySelector(".btn-prof");
const profSection = card.querySelector(".prof-section");
const profArrow = card.querySelector(".prof-arrow");