fix(phase-a): Kontext-Cap, Download-State, Recipe-Edit, OS-Badge, Swap-Flash

A1: 32768-Kontext-Cap entfernt (install-recipe, install-model, register) →
    max_ctx_for() liefert nun bis zu 128k auf Strix Halo; behebt "context
    size exceeded" bei externen Tools.
A2: Download-State jetzt im Status-Endpoint sichtbar: Modelle zeigen
    "↓ Download X%" statt "bereit" während Job läuft (Backend + Frontend).
A3: PUT /api/cookbook/user-recipe/{id} + Edit-Button (✎) für eigene Setups.
    Download-Modal setzt Kontext-Input automatisch auf optimal.
A4: /api/updates liefert apt_cache_age_h; Badge zeigt Tooltip + ⚠ wenn >24h.
A5: Swap-Flash: Topbar-Text pulst kurz teal wenn Modell den State wechselt.
A6: LLM-Engine-Update fragt jetzt per confirmModal nach (Konsistenz).
A7: Event-Delegation statt per-render addEventListener in models.js.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-06-22 21:33:03 +02:00
parent 022c42dccb
commit 82d15d82db
10 changed files with 214 additions and 45 deletions
+35 -4
View File
@@ -18,6 +18,7 @@ const panels = [overview, models, server, jobs, cookbook, connect, news, guides]
let lastJobs = [];
let lastSystem = null;
let prevModelStates = {};
// ---- Topbar / Alert aus dem Status ableiten ----
function applyStatus(s) {
@@ -76,26 +77,56 @@ function hideAlert() { $("#alert").style.display = "none"; }
// ---- Toolbar: ausstehende Updates anzeigen ----
const goView = v => document.querySelector(`.nav-item[data-view="${v}"]`)?.click();
function badge(text, cls, view) {
function badge(text, cls, view, title) {
const s = document.createElement("span");
s.className = "upd-badge" + (cls ? " " + cls : "");
s.textContent = text; s.onclick = () => goView(view);
s.textContent = text;
if (title) s.title = title;
s.onclick = () => goView(view);
return s;
}
async function pollUpdates() {
try {
const u = await api("/api/updates");
const el = $("#update-badges"); if (!el) return;
const age = u.apt_cache_age_h;
const stale = age != null && age > 24;
const ageNote = age != null ? ` · apt-Cache: vor ${age}h` : "";
el.innerHTML = "";
el.appendChild(badge(u.os > 0 ? `OS: ${u.os} Updates` : "OS: aktuell", u.os > 0 ? "warn" : "ok", "server"));
el.appendChild(badge(
u.os > 0 ? `OS: ${u.os} Updates` : (stale ? "OS: ?" : "OS: aktuell"),
u.os > 0 ? "warn" : stale ? "warn" : "ok",
"server",
u.os > 0 ? `${u.os} ausstehende Pakete${ageNote}` : (stale ? `apt-Cache veraltet (${age}h) — OS-Update prüfen` : `Keine ausstehenden Pakete${ageNote}`)
));
el.appendChild(badge(u.engine > 0 ? "Engine: Update" : "Engine: aktuell", u.engine > 0 ? "warn" : "ok", "server"));
el.appendChild(badge(u.models > 0 ? `Modelle: ${u.models}` : "Modelle: aktuell", u.models > 0 ? "warn" : "ok", "news"));
} catch { /* still */ }
}
// ---- Swap-Erkennung: kurze visuelle Hervorhebung wenn Modell wechselt ----
function flashSwap() {
const el = $("#top-active-text");
if (el) { el.classList.add("swap-flash"); setTimeout(() => el.classList.remove("swap-flash"), 900); }
}
// ---- Polling ----
async function pollStatus() {
try { applyStatus(await api("/api/status")); }
try {
const s = await api("/api/status");
// Zustandswechsel erkennen → Swap-Flash auslösen
const newStates = {};
for (const m of (s?.models || [])) newStates[m.name] = m.state;
for (const [name, state] of Object.entries(newStates)) {
const prev = prevModelStates[name];
if (prev && prev !== state && (state === "running" || state === "loading")) {
flashSwap();
break;
}
}
prevModelStates = newStates;
applyStatus(s);
}
catch { applyStatus(null); }
}
async function pollJobs() {