feat(admin): Dropdown „Zentrales KI-Modell" — erprobte Presets ruck-zuck umstellen

Neue Karte in Admin › System › Status: ein Dropdown kuratierter, erprobter
Modelle, das Provider + Modell in einem Klick als Laufzeit-Override umstellt
(wirkt sofort, kein Neustart).

- Presets in config/models.yaml (Label, optional note, set:{key:value} aus
  RUNTIME_SETTABLE, default:true = Config-Standard). Loader app/model_presets.py
  (nur erlaubte LLM-Keys: default_llm_provider, openrouter_llm_model,
  local_llm_model).
- Endpunkte: GET /api/admin/models (Presets + aktuell aktives LLM + active_index),
  POST /api/admin/models/select (Default -> Overrides löschen, sonst Override-Keys
  setzen) — nutzt set_/delete_config_override + invalidate_cache, mit Audit-Log.
- Frontend: Karte zeigt Dropdown + „Aktiv: <provider> · <modell>"; Auswahl wirkt
  sofort und lädt den Status neu. Bump app.js v=52 -> v=53.
- Startliste: Mistral Small 24B (Standard), DeepSeek V4 Flash/Pro, Ollama Gemma 3.

Tests: Laden/Default-aktiv/Select-setzt-Override/Default-löscht/404/Auth.
263 Offline-Tests grün. Live via Headless-Chrome verifiziert (Auswahl ->
Override gesetzt, Provider-Karte spiegelt das Modell).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Dieter Schlüter 2026-06-26 20:02:44 +02:00
commit 18ad442c94
6 changed files with 243 additions and 2 deletions

View file

@ -1570,13 +1570,36 @@ async function loadStatus() {
const container = $("#status-content");
container.innerHTML = '<p class="text-sm text-slate-400">lade …</p>';
try {
const [cfg, met, runtimeCfg] = await Promise.all([
const [cfg, met, runtimeCfg, models] = await Promise.all([
fetch("/api/config").then((r) => r.json()),
fetch("/api/metrics").then((r) => r.json()),
adminFetch("/api/admin/config"),
adminFetch("/api/admin/models"),
]);
const cfgByKey = Object.fromEntries((runtimeCfg || []).map((d) => [d.key, d.effective_value]));
const llmModel = cfgByKey["openrouter_llm_model"] || "—";
// Dropdown „Zentrales KI-Modell" (erprobte Presets, Auswahl wirkt sofort).
const presets = (models && models.presets) || [];
const activeIdx = models ? models.active_index : null;
const cur = (models && models.current) || {};
const curProv = cur.default_llm_provider || "—";
const curModel = curProv === "openrouter"
? (cur.openrouter_llm_model || "—") : (cur.local_llm_model || "—");
const modelOptions = presets.map((p) =>
`<option value="${p.index}"${p.index === activeIdx ? " selected" : ""}>${escHtml(p.label)}</option>`).join("");
const modelCard = presets.length ? `
<div class="bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 p-4">
<h3 class="font-semibold text-sm mb-1">Zentrales KI-Modell</h3>
<p class="text-xs text-slate-400 mb-3">Erprobte Modelle Auswahl wirkt sofort (kein Neustart).</p>
<div class="flex flex-wrap items-center gap-2">
<select id="model-preset" class="text-sm rounded-lg border border-slate-300 dark:border-slate-600 bg-transparent px-3 py-1.5">
${modelOptions}
</select>
<span id="model-preset-msg" class="text-xs text-slate-400"></span>
</div>
<p class="text-xs text-slate-400 mt-2">Aktiv: <span class="font-mono text-slate-600 dark:text-slate-300">${escHtml(curProv)} · ${escHtml(curModel)}</span></p>
</div>` : "";
const route = cfg.default_route || {};
const counters = met.counters || {};
const turnsTotal = counters["turns_total"] || 0;
@ -1602,6 +1625,8 @@ async function loadStatus() {
</dl>
</div>
${modelCard}
<!-- Provider -->
<div class="bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 p-4">
<h3 class="font-semibold text-sm mb-3">Provider (Standard-Route)</h3>
@ -1633,12 +1658,29 @@ async function loadStatus() {
</dl>
</div>
`;
wireModelPicker();
} catch (e) {
console.error("loadStatus error:", e);
container.innerHTML = '<p class="text-sm text-red-500">Fehler beim Laden.</p>';
}
}
// Dropdown „Zentrales KI-Modell": Auswahl wendet ein Preset an (Laufzeit-Override).
function wireModelPicker() {
const sel = $("#model-preset");
const msg = $("#model-preset-msg");
if (!sel) return;
sel.addEventListener("change", async () => {
msg.textContent = "stelle um …";
sel.disabled = true;
const res = await adminFetch("/api/admin/models/select", "POST", { index: +sel.value });
sel.disabled = false;
if (res === null) { msg.textContent = "Fehler beim Umstellen"; return; }
msg.textContent = "✓ " + (res.selected || "umgestellt");
loadStatus(); // Karte + „Aktiv"-Anzeige aktualisieren
});
}
// ════════════════════════════════════════
// TAB: WARTUNG (Konfiguration)
// ════════════════════════════════════════