feat(admin): Bulk "alle Nutzerdaten löschen (Konten behalten)" + Nutzerliste alphabetisch

- DELETE /api/admin/user-data: loescht fuer ALLE Nutzer Verlauf, Statistik
  (usage/web_search_usage/cost_usage) und Erinnerungen; Konten bleiben.
  Gefahrenzone-Knopf mit doppelter Bestaetigung, audit-geloggt.
- list_users jetzt ORDER BY LOWER(display_name) -> Nutzerkarten alphabetisch
  (ohne Gross-/Kleinschreibung).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Dieter Schlüter 2026-06-30 14:43:56 +02:00
commit c337ea4309
4 changed files with 46 additions and 1 deletions

View file

@ -156,6 +156,11 @@ class Store(ABC):
def delete_user_memories(self, user_id: str) -> int:
"""Loescht alle Erinnerungen eines Nutzers. Liefert die Zahl geloeschter Erinnerungen."""
@abstractmethod
def wipe_all_user_data(self) -> dict:
"""Loescht fuer ALLE Nutzer Verlauf, Statistik und Erinnerungen; Konten bleiben.
Liefert ein Dict mit Anzahlen je Kategorie."""
@abstractmethod
def reset_token(self, user_id: str) -> tuple[User, str] | None:
"""Generiert einen neuen Token fuer den Nutzer; der alte wird sofort ungueltig.
@ -354,7 +359,7 @@ class SQLiteStore(Store):
def list_users(self) -> list[User]:
with self._connect() as conn:
rows = conn.execute(
"SELECT * FROM users WHERE id != ? ORDER BY created_at",
"SELECT * FROM users WHERE id != ? ORDER BY LOWER(display_name)",
(ANONYMOUS_USER_ID,),
).fetchall()
return [self._row_to_user(row) for row in rows]
@ -607,6 +612,19 @@ class SQLiteStore(Store):
cur = conn.execute("DELETE FROM memories WHERE user_id = ?", (user_id,))
return cur.rowcount
def wipe_all_user_data(self) -> dict:
with self._connect() as conn:
counts = {
"messages": conn.execute("DELETE FROM messages").rowcount,
"sessions": conn.execute("DELETE FROM sessions").rowcount,
"memories": conn.execute("DELETE FROM memories").rowcount,
}
stats = 0
for tbl in ("usage", "web_search_usage", "cost_usage"):
stats += conn.execute(f"DELETE FROM {tbl}").rowcount
counts["stats"] = stats
return counts
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()