Files
mission-control/static/js/panels/overview.js
T
Hitonabi 52b0a3bff5 v3 Phase A: Design-System-Fundament + Übersicht neu
- 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>
2026-06-21 06:51:28 +02:00

144 lines
6.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 };