// 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 = `
`;
}
// ---- 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 };