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:
parent
296138ac1e
commit
5d022aaf63
7 changed files with 285 additions and 9 deletions
|
|
@ -9,6 +9,7 @@ from pydantic import BaseModel
|
|||
from app.auth import is_admin_user, require_admin, require_admin_or_user
|
||||
from app.config import settings
|
||||
from app.dependencies import get_store
|
||||
from app.runtime_config import RUNTIME_SETTABLE, invalidate_cache, runtime_settings
|
||||
from app.schemas import MemoryCreate, MemoryOut, UserCreate, UserCreated, UserUpdate
|
||||
|
||||
router = APIRouter()
|
||||
|
|
@ -249,6 +250,55 @@ async def delete_pronunciation(lang: str, section: str, key: str):
|
|||
return {"deleted": key}
|
||||
|
||||
|
||||
# ── Laufzeit-Konfiguration ─────────────────────────────────────────────────
|
||||
|
||||
class ConfigValue(BaseModel):
|
||||
value: str
|
||||
|
||||
|
||||
@router.get("/admin/config", dependencies=[Depends(require_admin)])
|
||||
async def get_runtime_config():
|
||||
"""Gibt alle überschreibbaren Einstellungen mit aktuellem Wert zurück."""
|
||||
overrides = get_store().get_config_overrides()
|
||||
result = []
|
||||
for key, (label, type_str, hint) in RUNTIME_SETTABLE.items():
|
||||
base_val = getattr(settings, key, None)
|
||||
effective_val = getattr(runtime_settings, key, base_val)
|
||||
result.append({
|
||||
"key": key,
|
||||
"label": label,
|
||||
"type": type_str,
|
||||
"hint": hint,
|
||||
"base_value": str(base_val) if base_val is not None else "",
|
||||
"override_value": overrides.get(key),
|
||||
"effective_value": str(effective_val) if effective_val is not None else "",
|
||||
"is_overridden": key in overrides,
|
||||
})
|
||||
return result
|
||||
|
||||
|
||||
@router.put("/admin/config/{key}", dependencies=[Depends(require_admin)])
|
||||
async def set_runtime_config(key: str, body: ConfigValue):
|
||||
"""Setzt eine Laufzeit-Einstellung (wirkt sofort, kein Neustart nötig)."""
|
||||
if key not in RUNTIME_SETTABLE:
|
||||
raise HTTPException(status_code=400, detail=f"Nicht überschreibbar: {key!r}")
|
||||
get_store().set_config_override(key, body.value.strip())
|
||||
invalidate_cache()
|
||||
return {"key": key, "value": body.value.strip()}
|
||||
|
||||
|
||||
@router.delete("/admin/config/{key}", dependencies=[Depends(require_admin)])
|
||||
async def delete_runtime_config(key: str):
|
||||
"""Entfernt eine Laufzeit-Einstellung (fällt auf .env-Wert zurück)."""
|
||||
if key not in RUNTIME_SETTABLE:
|
||||
raise HTTPException(status_code=400, detail=f"Nicht überschreibbar: {key!r}")
|
||||
deleted = get_store().delete_config_override(key)
|
||||
invalidate_cache()
|
||||
if not deleted:
|
||||
raise HTTPException(status_code=404, detail=f"Kein Override für {key!r} gesetzt.")
|
||||
return {"deleted": key}
|
||||
|
||||
|
||||
# ── Live-Log (journalctl → WebSocket) ──────────────────────────────────────
|
||||
|
||||
@router.websocket("/admin/log")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue