From 8fb5e42e1b9097762aab96db4c623ef575523bbe Mon Sep 17 00:00:00 2001 From: dschlueter Date: Tue, 30 Jun 2026 14:29:42 +0200 Subject: [PATCH] =?UTF-8?q?feat(admin):=20Kn=C3=B6pfe=20"Statistik=20zur?= =?UTF-8?q?=C3=BCcksetzen"=20+=20"Erinnerungen=20l=C3=B6schen"=20pro=20Nut?= =?UTF-8?q?zer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DELETE /api/admin/users/{id}/stats (usage/web_search_usage/cost_usage) und .../memories (alle Erinnerungen) — require_admin, audit-geloggt; Konto, Verlauf und (jeweils) die andere Datenart bleiben. Store-Methoden delete_user_stats/ delete_user_memories + zwei UI-Buttons je Nutzerkarte. Co-Authored-By: Claude Opus 4.8 --- app/api/admin.py | 23 +++++++++++++++++++++++ app/store.py | 22 ++++++++++++++++++++++ app/web/app.js | 16 ++++++++++++++++ 3 files changed, 61 insertions(+) diff --git a/app/api/admin.py b/app/api/admin.py index 95a47f6..dadea8a 100644 --- a/app/api/admin.py +++ b/app/api/admin.py @@ -143,6 +143,29 @@ async def delete_user_history(user_id: str, request: Request): return {"user_id": user_id, "deleted_messages": deleted} +@router.delete("/admin/users/{user_id}/stats", dependencies=[Depends(require_admin)]) +async def delete_user_stats(user_id: str, request: Request): + """Setzt die Statistik-/Nutzungsdaten (Anfragen, Web-Suchen, Kosten) eines + Nutzers zurueck; Konto, Verlauf und Erinnerungen bleiben.""" + store = get_store() + if store.get_user(user_id) is None: + raise HTTPException(status_code=404, detail=f"Nutzer {user_id!r} nicht gefunden.") + deleted = store.delete_user_stats(user_id) + log_admin_action(request, "user_stats_reset", user_id=user_id) + return {"user_id": user_id, "deleted_rows": deleted} + + +@router.delete("/admin/users/{user_id}/memories", dependencies=[Depends(require_admin)]) +async def delete_user_memories(user_id: str, request: Request): + """Loescht alle Erinnerungen eines Nutzers; Konto, Verlauf und Statistik bleiben.""" + store = get_store() + if store.get_user(user_id) is None: + raise HTTPException(status_code=404, detail=f"Nutzer {user_id!r} nicht gefunden.") + deleted = store.delete_user_memories(user_id) + log_admin_action(request, "user_memories_deleted", user_id=user_id) + return {"user_id": user_id, "deleted_memories": deleted} + + @router.post("/admin/users/{user_id}/token", response_model=UserCreated, dependencies=[Depends(require_admin)]) async def reset_token(user_id: str): """Stellt einen neuen Bearer-Token aus; der alte wird sofort ungueltig. diff --git a/app/store.py b/app/store.py index 8629938..de33432 100644 --- a/app/store.py +++ b/app/store.py @@ -147,6 +147,15 @@ class Store(ABC): """Loescht NUR den Gespraechsverlauf (Sessions + Nachrichten) eines Nutzers; Konto, Erinnerungen und Nutzungsdaten bleiben. Liefert die Zahl geloeschter Nachrichten.""" + @abstractmethod + def delete_user_stats(self, user_id: str) -> int: + """Loescht die Statistik-/Nutzungsdaten (usage, web_search_usage, cost_usage) + eines Nutzers. Konto, Verlauf und Erinnerungen bleiben.""" + + @abstractmethod + def delete_user_memories(self, user_id: str) -> int: + """Loescht alle Erinnerungen eines Nutzers. Liefert die Zahl geloeschter Erinnerungen.""" + @abstractmethod def reset_token(self, user_id: str) -> tuple[User, str] | None: """Generiert einen neuen Token fuer den Nutzer; der alte wird sofort ungueltig. @@ -585,6 +594,19 @@ class SQLiteStore(Store): conn.execute("DELETE FROM sessions WHERE user_id = ?", (user_id,)) return n + def delete_user_stats(self, user_id: str) -> int: + with self._connect() as conn: + n = 0 + for tbl in ("usage", "web_search_usage", "cost_usage"): + cur = conn.execute(f"DELETE FROM {tbl} WHERE user_id = ?", (user_id,)) + n += cur.rowcount + return n + + def delete_user_memories(self, user_id: str) -> int: + with self._connect() as conn: + cur = conn.execute("DELETE FROM memories WHERE user_id = ?", (user_id,)) + return cur.rowcount + def reset_token(self, user_id: str) -> tuple[User, str] | None: with self._connect() as conn: row = conn.execute("SELECT * FROM users WHERE id = ?", (user_id,)).fetchone() diff --git a/app/web/app.js b/app/web/app.js index 9496dc0..f487297 100644 --- a/app/web/app.js +++ b/app/web/app.js @@ -1198,6 +1198,8 @@ function buildUserCard(u) { + + @@ -1490,6 +1492,20 @@ function buildUserCard(u) { if (res) alert(`Verlauf gelöscht (${res.deleted_messages ?? 0} Nachrichten).`); }); + // Statistik zurücksetzen (Anfragen, Web-Suchen, Kosten) + card.querySelector(".btn-stats").addEventListener("click", async () => { + if (!confirm(`Statistik von „${u.display_name}" zurücksetzen?\nAnfragen, Web-Suchen und Kosten werden auf 0 gesetzt; Konto, Verlauf und Erinnerungen bleiben.`)) return; + const res = await adminFetch(`/api/admin/users/${uid}/stats`, "DELETE"); + if (res) alert(`Statistik zurückgesetzt (${res.deleted_rows ?? 0} Zeilen).`); + }); + + // Erinnerungen löschen (alle) + card.querySelector(".btn-mem-clear").addEventListener("click", async () => { + if (!confirm(`Alle Erinnerungen von „${u.display_name}" löschen?\nKonto, Verlauf und Statistik bleiben.`)) return; + const res = await adminFetch(`/api/admin/users/${uid}/memories`, "DELETE"); + if (res) alert(`Erinnerungen gelöscht (${res.deleted_memories ?? 0}).`); + }); + // Löschen card.querySelector(".btn-delete").addEventListener("click", async () => { if (!confirm(`Nutzer „${u.display_name}" und alle Daten unwiderruflich löschen?`)) return;