feat(admin): vollständiges Admin-Panel in der Web-UI (Phase 1)

Ersetzt das rudimentäre Nutzer-Dropdown durch ein vollwertiges
Admin-Panel-Overlay mit 5 Tabs:

Nutzer:      Anlegen (Token einmalig), Umbenennen, Token-Reset,
             Löschen, Erinnerungen verwalten (anzeigen/add/delete)
Gespräche:   Nutzer → Sessions → Transkript (Chat-Bubble-Ansicht)
Notfälle:    Tabelle aller emergency_events mit Zeitstempel/Kategorie
Status:      Gateway-Status, Provider-Config, Laufzeit-Metriken
Metriken:    Nutzungsstatistik pro Nutzer + Gesamtsumme

Backend: 7 neue Admin-Endpunkte in admin.py, 5 neue Store-Methoden.
Design: Tailwind CSS, Tag/Nacht-Modus, responsiv.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Dieter Schlüter 2026-06-19 01:04:20 +02:00
commit b38992cf28
4 changed files with 710 additions and 34 deletions

View file

@ -137,6 +137,21 @@ class Store(ABC):
def log_emergency(self, user_id: str, category: str, snippet: str) -> None:
"""Protokolliert ein erkanntes Notfall-Signal (sensibel!)."""
@abstractmethod
def list_sessions_for_user(self, user_id: str) -> list[dict]: ...
@abstractmethod
def get_messages_for_session(self, session_id: str, limit: int = 200) -> list[dict]: ...
@abstractmethod
def list_emergency_events(self, limit: int = 50) -> list[dict]: ...
@abstractmethod
def get_usage_for_user(self, user_id: str) -> list[dict]: ...
@abstractmethod
def get_all_usage(self) -> list[dict]: ...
class SQLiteStore(Store):
def __init__(self, db_path: str):
@ -484,3 +499,55 @@ class SQLiteStore(Store):
" VALUES (?, ?, ?, ?)",
(user_id, category, snippet, _now()),
)
# ----- Admin-Abfragen ---------------------------------------------------
def list_sessions_for_user(self, user_id: str) -> list[dict]:
with self._connect() as conn:
rows = conn.execute(
"SELECT s.id, s.created_at, s.updated_at,"
" (SELECT COUNT(*) FROM messages m WHERE m.session_id = s.id) AS msg_count"
" FROM sessions s WHERE s.user_id = ?"
" ORDER BY s.updated_at DESC LIMIT 50",
(user_id,),
).fetchall()
return [dict(r) for r in rows]
def get_messages_for_session(self, session_id: str, limit: int = 200) -> list[dict]:
with self._connect() as conn:
rows = conn.execute(
"SELECT role, content, created_at FROM messages"
" WHERE session_id = ? ORDER BY id LIMIT ?",
(session_id, limit),
).fetchall()
return [dict(r) for r in rows]
def list_emergency_events(self, limit: int = 50) -> list[dict]:
with self._connect() as conn:
rows = conn.execute(
"SELECT e.id, e.user_id, e.category, e.snippet, e.created_at,"
" COALESCE(u.display_name, e.user_id) AS display_name"
" FROM emergency_events e LEFT JOIN users u ON u.id = e.user_id"
" ORDER BY e.id DESC LIMIT ?",
(limit,),
).fetchall()
return [dict(r) for r in rows]
def get_usage_for_user(self, user_id: str) -> list[dict]:
with self._connect() as conn:
rows = conn.execute(
"SELECT day, requests, units FROM usage"
" WHERE user_id = ? ORDER BY day DESC LIMIT 30",
(user_id,),
).fetchall()
return [dict(r) for r in rows]
def get_all_usage(self) -> list[dict]:
with self._connect() as conn:
rows = conn.execute(
"SELECT u.user_id, COALESCE(usr.display_name, u.user_id) AS display_name,"
" SUM(u.requests) AS total_requests, SUM(u.units) AS total_units,"
" MAX(u.day) AS last_active"
" FROM usage u LEFT JOIN users usr ON usr.id = u.user_id"
" GROUP BY u.user_id ORDER BY total_requests DESC",
).fetchall()
return [dict(r) for r in rows]