feat(admin): Phase 3 — Laufzeit-Konfiguration ohne Server-Neustart

Neue Tabelle config_overrides in SQLite; RuntimeSettings-Wrapper liest
überschreibbare Felder mit 30s TTL-Cache aus der DB und fällt auf
.env-Werte zurück. dependencies.py und quota.py nutzen runtime_settings
als Default statt des statischen Settings-Singletons.

16 Felder überschreibbar: STT/LLM/TTS-Provider, LLM-Modelle, Stimmen,
Systemprompt, Temperatur, Tageskontingent, Normalisierung u.a.

Backend: GET/PUT/DELETE /api/admin/config/{key}
Admin-UI: neuer Tab "⚙ Einstellungen" mit Inline-Edit und Reset.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Dieter Schlüter 2026-06-19 01:39:36 +02:00
commit 5d022aaf63
7 changed files with 285 additions and 9 deletions

View file

@ -152,6 +152,15 @@ class Store(ABC):
@abstractmethod
def get_all_usage(self) -> list[dict]: ...
@abstractmethod
def get_config_overrides(self) -> dict[str, str]: ...
@abstractmethod
def set_config_override(self, key: str, value: str) -> None: ...
@abstractmethod
def delete_config_override(self, key: str) -> bool: ...
class SQLiteStore(Store):
def __init__(self, db_path: str):
@ -216,6 +225,11 @@ class SQLiteStore(Store):
snippet TEXT NOT NULL,
created_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS config_overrides (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
updated_at TEXT NOT NULL
);
"""
)
# Migration fuer bestehende DBs: external_id ergaenzen (falls noch nicht da).
@ -551,3 +565,21 @@ class SQLiteStore(Store):
" GROUP BY u.user_id ORDER BY total_requests DESC",
).fetchall()
return [dict(r) for r in rows]
def get_config_overrides(self) -> dict[str, str]:
with self._connect() as conn:
rows = conn.execute("SELECT key, value FROM config_overrides").fetchall()
return {r["key"]: r["value"] for r in rows}
def set_config_override(self, key: str, value: str) -> None:
with self._connect() as conn:
conn.execute(
"INSERT INTO config_overrides(key, value, updated_at) VALUES(?,?,?)"
" ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at",
(key, value, _now()),
)
def delete_config_override(self, key: str) -> bool:
with self._connect() as conn:
cur = conn.execute("DELETE FROM config_overrides WHERE key=?", (key,))
return cur.rowcount > 0