52b0a3bff5
- base.css/components.css: EINE Akzentfarbe (Teal), Metrik-Kacheln, Fit-Ampel, Modal, Quickstart-Reihen; beschriftete Sidebar; rueckwaertskompatibel (Legacy-Klassen + fehlende Vars --hi/--red/--red-dim definiert). - index.html: beschriftete Navigation, Topbar mit Security-Chip, neue Overview-Mountpunkte. - ui.js: Icon-Set erweitert + confirmModal/promptModal/fmtBytes/fmtPct (Beginner-UX-Helfer). - overview.js: komplett neu (Klartext-Urteil, 4 Kacheln, System-Gesundheit-Balken, gefuehrter Schnellstart, "Dein Stack"). Inline-Styles raus. Verifiziert: lokal 0 Konsolenfehler, Live-Metriken via WS, alle Views unbeschaedigt. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
144 lines
6.7 KiB
JavaScript
144 lines
6.7 KiB
JavaScript
// 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 = `<div class="pagehead"><div>
|
||
<h1>${esc(title)}</h1><div class="sub">${esc(sub)}</div></div></div>`;
|
||
}
|
||
|
||
// ---- Metrik-Kacheln ----
|
||
function tile(label, valueHtml, sub, vCls = "", sCls = "") {
|
||
return `<div class="tile"><div class="t-l">${label}</div>
|
||
<div class="t-v ${vCls}">${valueHtml}</div><div class="t-s ${sCls}">${sub}</div></div>`;
|
||
}
|
||
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)}<small> GB</small>`,
|
||
`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 `<div class="meter"><div class="meter-h"><span class="mk">${label}</span>
|
||
<span class="mv">${Math.round(p)} %</span></div>
|
||
<div class="bar ${cls}"><i style="width:${Math.max(2, p)}%"></i></div></div>`;
|
||
}
|
||
function renderHealth() {
|
||
const head = `<div class="card-h"><h3>System-Gesundheit</h3></div>
|
||
<div class="card-sub">So ausgelastet ist dein Mini-PC gerade.</div>`;
|
||
if (!SYS) { $("#ov-health").innerHTML = head + `<div class="empty">Warte auf Messwerte…</div>`; 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 `<button class="qa" onclick="${onclick}">
|
||
<span class="qa-ic ${tone}">${icon(ic)}</span>
|
||
<span class="qa-main"><span class="qa-t">${title}</span><span class="qa-s">${sub}</span></span>
|
||
<span class="qa-arrow">${icon("chevron")}</span></button>`;
|
||
}
|
||
function renderQuickstart() {
|
||
$("#ov-quickstart").innerHTML =
|
||
`<div class="card-h"><h3>Schnellstart</h3></div>
|
||
<div class="card-sub">Die häufigsten Aufgaben — ein Klick.</div>` +
|
||
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 `<span class="tag img">Bild</span>`;
|
||
if (caps.includes("Code")) return `<span class="tag code">Code</span>`;
|
||
return `<span class="tag text">Text</span>`;
|
||
}
|
||
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 `<div class="li"><span class="li-dot ${dot}"></span>
|
||
<span class="li-id" style="flex:1">${esc(m.name)}</span>
|
||
${capTag(m.meta?.caps)}
|
||
<span class="li-meta" style="width:120px;text-align:right">${status}</span></div>`;
|
||
}
|
||
function renderStack() {
|
||
const ms = models();
|
||
$("#ov-stack").innerHTML =
|
||
`<div class="card-h"><h3>Dein Stack</h3><span class="meta">${ms.length ? ms.length + " konfiguriert" : ""}</span></div>` +
|
||
(ms.length
|
||
? `<div class="list">${ms.map(stackRow).join("")}</div>`
|
||
: `<div class="empty-c"><div class="e-t">Noch keine Modelle</div>
|
||
<div class="e-s">Hol dir unter „Cookbook" ein Modell, das auf deine Hardware passt.</div></div>`);
|
||
}
|
||
|
||
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 };
|