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
|
|
@ -385,6 +385,7 @@ function switchAdminTab(name) {
|
|||
if (name === "metrics") loadMetrics();
|
||||
if (name === "words") loadWoerterbuch();
|
||||
if (name === "log") { /* User muss manuell verbinden */ }
|
||||
if (name === "settings") loadSettings();
|
||||
}
|
||||
|
||||
document.querySelectorAll(".admin-tab").forEach((btn) =>
|
||||
|
|
@ -968,3 +969,75 @@ function disconnectLog() {
|
|||
$("#log-connect").addEventListener("click", connectLog);
|
||||
$("#log-disconnect").addEventListener("click", disconnectLog);
|
||||
$("#log-clear").addEventListener("click", () => { $("#log-output").innerHTML = ""; });
|
||||
|
||||
// ════════════════════════════════════════
|
||||
// TAB: EINSTELLUNGEN
|
||||
// ════════════════════════════════════════
|
||||
async function loadSettings() {
|
||||
const container = $("#settings-content");
|
||||
container.innerHTML = '<p class="text-sm text-slate-400">lade …</p>';
|
||||
const data = await adminFetch("/api/admin/config");
|
||||
if (!data) { container.innerHTML = '<p class="text-sm text-red-500">Fehler beim Laden.</p>'; return; }
|
||||
container.innerHTML = "";
|
||||
data.forEach((item) => container.appendChild(buildSettingRow(item)));
|
||||
}
|
||||
|
||||
function buildSettingRow(item) {
|
||||
const wrap = document.createElement("div");
|
||||
const overridden = item.is_overridden;
|
||||
wrap.className = "bg-white dark:bg-slate-800 rounded-xl border " +
|
||||
(overridden
|
||||
? "border-amber-300 dark:border-amber-600"
|
||||
: "border-slate-200 dark:border-slate-700") +
|
||||
" p-4";
|
||||
|
||||
const inputId = "cfg-" + item.key;
|
||||
const isLong = item.type === "str" && item.base_value.length > 60;
|
||||
|
||||
wrap.innerHTML = `
|
||||
<div class="flex items-start justify-between gap-3 mb-2 flex-wrap">
|
||||
<div>
|
||||
<label for="${inputId}" class="font-medium text-sm">${escHtml(item.label)}</label>
|
||||
<span class="ml-2 text-xs font-mono text-slate-400">${escHtml(item.key)}</span>
|
||||
${overridden ? '<span class="ml-2 text-xs rounded-full bg-amber-100 dark:bg-amber-900/40 text-amber-700 dark:text-amber-300 px-2 py-0.5">überschrieben</span>' : ""}
|
||||
</div>
|
||||
${overridden ? `<button class="reset-btn text-xs text-red-500 hover:text-red-700 dark:hover:text-red-400 transition-colors whitespace-nowrap">↩ Zurücksetzen</button>` : ""}
|
||||
</div>
|
||||
<div class="flex gap-2 items-end flex-wrap">
|
||||
${isLong
|
||||
? `<textarea id="${inputId}" rows="3"
|
||||
class="flex-1 min-w-0 rounded-lg border border-slate-300 dark:border-slate-600 bg-transparent px-3 py-2 text-xs font-mono focus:outline-none focus:ring-2 focus:ring-blue-500 resize-y"
|
||||
>${escHtml(item.effective_value)}</textarea>`
|
||||
: `<input id="${inputId}" type="text" value="${escHtml(item.effective_value)}"
|
||||
class="flex-1 min-w-0 rounded-lg border border-slate-300 dark:border-slate-600 bg-transparent px-3 py-2 text-xs font-mono focus:outline-none focus:ring-2 focus:ring-blue-500" />`
|
||||
}
|
||||
<button class="save-btn rounded-lg bg-blue-600 hover:bg-blue-700 text-white text-xs px-3 py-2 font-medium transition-colors whitespace-nowrap">Speichern</button>
|
||||
</div>
|
||||
<p class="text-xs text-slate-400 mt-1.5">
|
||||
<span class="font-medium">.env:</span> <span class="font-mono">${escHtml(item.base_value) || "—"}</span>
|
||||
<span class="ml-3 text-slate-300 dark:text-slate-600">·</span>
|
||||
<span class="ml-2">${escHtml(item.hint)}</span>
|
||||
</p>
|
||||
`;
|
||||
|
||||
const inp = wrap.querySelector(`#${inputId}`);
|
||||
|
||||
wrap.querySelector(".save-btn").addEventListener("click", async () => {
|
||||
const val = inp.value ?? inp.textContent ?? "";
|
||||
const res = await adminFetch(`/api/admin/config/${item.key}`, "PUT", { value: val });
|
||||
if (res !== null) loadSettings();
|
||||
});
|
||||
|
||||
const resetBtn = wrap.querySelector(".reset-btn");
|
||||
if (resetBtn) {
|
||||
resetBtn.addEventListener("click", async () => {
|
||||
if (!confirm(`Override für „${item.label}" entfernen?\nDer .env-Wert gilt danach wieder.`)) return;
|
||||
const res = await adminFetch(`/api/admin/config/${item.key}`, "DELETE");
|
||||
if (res !== null) loadSettings();
|
||||
});
|
||||
}
|
||||
|
||||
return wrap;
|
||||
}
|
||||
|
||||
$("#load-settings").addEventListener("click", loadSettings);
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@
|
|||
<button class="admin-tab px-4 py-3 text-sm font-medium whitespace-nowrap border-b-2 border-transparent text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200 transition-colors" data-tab="metrics">📊 Metriken</button>
|
||||
<button class="admin-tab px-4 py-3 text-sm font-medium whitespace-nowrap border-b-2 border-transparent text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200 transition-colors" data-tab="words">🔤 Wörterbuch</button>
|
||||
<button class="admin-tab px-4 py-3 text-sm font-medium whitespace-nowrap border-b-2 border-transparent text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200 transition-colors" data-tab="log">📋 Log</button>
|
||||
<button class="admin-tab px-4 py-3 text-sm font-medium whitespace-nowrap border-b-2 border-transparent text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200 transition-colors" data-tab="settings">⚙ Einstellungen</button>
|
||||
</nav>
|
||||
|
||||
<!-- Tab Content Container -->
|
||||
|
|
@ -164,6 +165,21 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Tab: Einstellungen ── -->
|
||||
<div id="admin-tab-settings" class="hidden h-full overflow-y-auto p-4">
|
||||
<div class="max-w-3xl mx-auto">
|
||||
<div class="flex items-center justify-between mb-2 flex-wrap gap-2">
|
||||
<h3 class="font-semibold text-base">Laufzeit-Einstellungen</h3>
|
||||
<button id="load-settings"
|
||||
class="rounded-lg border border-slate-300 dark:border-slate-600 text-sm px-3 py-1.5 hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors">Aktualisieren</button>
|
||||
</div>
|
||||
<p class="text-xs text-slate-400 mb-4">Änderungen wirken sofort — kein Server-Neustart nötig. „Zurücksetzen" entfernt den Override und der .env-Wert gilt wieder.</p>
|
||||
<div id="settings-content" class="space-y-2">
|
||||
<p class="text-sm text-slate-400">lade …</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div><!-- /Tab Content Container -->
|
||||
</div><!-- /Admin Panel -->
|
||||
|
||||
|
|
@ -202,6 +218,6 @@
|
|||
<div id="status" class="w-full max-w-3xl mx-auto mt-1 min-h-[1rem] text-xs text-slate-500 dark:text-slate-400"></div>
|
||||
</footer>
|
||||
|
||||
<script src="/app.js?v=11"></script>
|
||||
<script src="/app.js?v=12"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue