// overview.js — Dashboard (v3): Klartext-Urteil, Metrik-Kacheln, System-Gesundheit, // geführter Schnellstart, "Dein Stack". Speist sich aus /api/status + /api/system/stream. import { api } from "../core/api.js"; import { $, esc, icon, toast, fmtBytes, confirmModal } from "../core/ui.js"; let S = null; // letzter Status let SYS = null; // letzte System-Metriken const RUNNING = new Set(["running", "ready", "loading", "starting"]); function models() { return S?.models || []; } function activeModel() { return models().find(m => RUNNING.has(m.state)); } // ---- Aktionen (geführt) ---- function go(view) { document.querySelector(`.nav-item[data-view="${view}"]`)?.click(); } async function freeMemory() { const ok = await confirmModal({ title: "Speicher freigeben?", body: "Alle aktuell geladenen Modelle werden aus dem Grafikspeicher geworfen. " + "Beim nächsten Aufruf lädt das jeweilige Modell einfach neu — es geht nichts verloren.", confirmLabel: "Speicher freigeben", }); if (!ok) return; try { await api("/api/unload", { method: "POST" }); toast("Speicher freigegeben."); setTimeout(() => document.dispatchEvent(new Event("mc:refresh")), 600); } catch (e) { toast(e.message, true); } } window.mcOv = { go, freeMemory }; // ---- Hero (menschliches Urteil) ---- function renderHero() { let title = "Verbinde…", sub = ""; if (S) { const total = models().length, act = activeModel(); if (!S.swap_ok) { title = "LLM-Engine offline"; sub = "Der llama-swap-Dienst antwortet gerade nicht."; } else if (SYS && SYS.ram.percent >= 90) { title = "Achtung: Speicher wird knapp."; sub = `Arbeitsspeicher bei ${Math.round(SYS.ram.percent)} % — eventuell Speicher freigeben.`; } else { title = "Alles läuft rund."; sub = `${total} ${total === 1 ? "Modell" : "Modelle"} bereit · ${act ? `„${act.name}" geladen` : "keins geladen"} · keine Warnungen.`; } } $("#ov-hero").innerHTML = `

${esc(title)}

${esc(sub)}
`; } // ---- Metrik-Kacheln ---- function tile(label, valueHtml, sub, vCls = "", sCls = "") { return `
${label}
${valueHtml}
${sub}
`; } function tempWord(t) { return t == null ? "" : t < 55 ? "kühl & gesund" : t < 72 ? "normal" : "läuft heiß"; } function renderTiles() { const act = activeModel(); const swap = S?.swap_ok; let engine = tile("LLM-Engine", swap ? "Online" : "Offline", "llama-swap", swap ? "ok" : "bad"); let model = tile("Aktives Modell", act ? esc(act.name) : "Keins", act ? "im Grafikspeicher" : "nichts geladen"); let temp, mem; if (SYS) { const t = SYS.gpu_temp ?? SYS.cpu.temp; temp = tile("GPU-Temperatur", t != null ? `${Math.round(t)}°` : "–", tempWord(t), "", t != null && t < 72 ? "ok" : ""); const free = SYS.ram.total - SYS.ram.used; mem = tile("Freier Speicher", `${(free / 1024 ** 3).toFixed(0)} GB`, `von ${(SYS.ram.total / 1024 ** 3).toFixed(0)} GB`); } else { temp = tile("GPU-Temperatur", "–", "messe…"); mem = tile("Freier Speicher", "–", "messe…"); } $("#ov-tiles").innerHTML = engine + model + temp + mem; } // ---- System-Gesundheit ---- function meter(label, pct) { const p = Math.max(0, Math.min(100, pct || 0)); const cls = p >= 90 ? "bad" : p >= 75 ? "warn" : ""; return `
${label} ${Math.round(p)} %
`; } function renderHealth() { const head = `

System-Gesundheit

So ausgelastet ist dein Mini-PC gerade.
`; if (!SYS) { $("#ov-health").innerHTML = head + `
Warte auf Messwerte…
`; return; } let gpuPct = 0; const g = SYS.gpu; if (g && (g.vram.total + g.gtt.total) > 0) gpuPct = ((g.vram.used + g.gtt.used) / (g.vram.total + g.gtt.total)) * 100; $("#ov-health").innerHTML = head + meter("Prozessor (CPU)", SYS.cpu.percent) + meter("Arbeitsspeicher (RAM)", SYS.ram.percent) + meter("Grafikspeicher (VRAM)", gpuPct); } // ---- Schnellstart ---- function qa(ic, tone, title, sub, onclick) { return ``; } function renderQuickstart() { $("#ov-quickstart").innerHTML = `

Schnellstart

Die häufigsten Aufgaben — ein Klick.
` + qa("search", "teal", "Modell finden", "Passend zu deiner Hardware", "window.mcOv.go('cookbook')") + qa("refresh", "amber", "Speicher freigeben", "Modelle entladen · fragt vorher nach", "window.mcOv.freeMemory()") + qa("file", "blue", "Logs ansehen", "Live mitlesen, was läuft", "window.mcOv.go('server')"); } // ---- Dein Stack ---- function capTag(caps) { if (!caps) return ""; if (caps.includes("Bild")) return `Bild`; if (caps.includes("Code")) return `Code`; return `Text`; } function stackRow(m) { const on = RUNNING.has(m.state); const dot = m.state === "loading" || m.state === "starting" ? "load" : on ? "on" : ""; const status = on ? (m.state === "loading" ? "lädt…" : `geladen${m.port ? " · Port " + m.port : ""}`) : "bereit"; return `
${esc(m.name)} ${capTag(m.meta?.caps)} ${status}
`; } function renderStack() { const ms = models(); $("#ov-stack").innerHTML = `

Dein Stack

${ms.length ? ms.length + " konfiguriert" : ""}
` + (ms.length ? `
${ms.map(stackRow).join("")}
` : `
Noch keine Modelle
Hol dir unter „Cookbook" ein Modell, das auf deine Hardware passt.
`); } function renderAll() { renderHero(); renderTiles(); renderHealth(); renderQuickstart(); renderStack(); } function mount() { renderQuickstart(); renderAll(); } function onStatus(s) { S = s; renderHero(); renderTiles(); renderStack(); } function onSystem(sys) { SYS = sys; renderHero(); renderTiles(); renderHealth(); } export default { id: "overview", mount, onStatus, onSystem };