Files
mission-control/static/js/panels/overview.js
T

133 lines
5.1 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-Kopf: Hero + Mini-Stats, KPI-Reihe, Stack-Gesundheit,
// kompakte Modell-Liste ("Session Router"). Speist sich aus /api/status + /api/jobs.
import { $, icon, esc } from "../core/ui.js";
let S = null; // letzter Status
let J = []; // letzte Job-Liste
let SYS = null; // letzte System-Auslastung
const RUNNING = new Set(["running", "ready", "loading", "starting"]);
function counts() {
const models = S?.models || [];
return {
total: models.length,
running: models.filter(m => RUNNING.has(m.state)).length,
jobsRun: J.filter(j => j.state === "running" || j.state === "queued").length,
jobsErr: J.filter(j => j.state === "failed").length,
swap: !!S?.swap_ok,
};
}
function mini(label, val, tone = "") {
const v = tone ? `<b style="${tone === "bad" ? "color:var(--err)" : ""}">${val}</b>` : val;
return `<div class="mini"><div class="l">${label}</div><div class="v">${v}</div></div>`;
}
function renderHero() {
const c = counts();
$("#hero").innerHTML = `<div class="hero">
<div>
<div class="eyebrow">Übersicht</div>
<h1>Mission Control</h1>
<p>Steuerzentrale für deinen lokalen llama-swap-Stack — Modelle, Downloads,
Wartung und Schnelltest an einem Ort.</p>
</div>
<div class="hero-stats">
${mini("Modelle", c.total)}
${mini("Aktiv", c.running, "on")}
${mini("Jobs", c.jobsRun)}
${mini("Fehler", c.jobsErr, c.jobsErr ? "bad" : "")}
</div>
</div>`;
}
function kpi(cls, title, ic, value, sub) {
return `<div class="kpi ${cls}">
<div class="k-h"><span class="k-t">${title}</span><span class="k-ic">${icon(ic)}</span></div>
<div class="k-v">${value}</div>
<div class="k-s">${sub}</div>
</div>`;
}
function renderKpis() {
const c = counts();
const sysV = SYS ? `${SYS.cpu.percent.toFixed(0)}<small>% CPU</small>` : "n/a";
const sysS = SYS ? `${SYS.ram.percent.toFixed(0)}% RAM, ${SYS.gpu_temp ? SYS.gpu_temp.toFixed(0)+'°C' : SYS.cpu.temp ? SYS.cpu.temp.toFixed(0)+'°C' : ''}` : "bald · Live-Auslastung";
$("#kpis").innerHTML =
kpi(c.swap ? "green" : "red", "llama-swap", "swap",
c.swap ? "Online" : "Offline", "Transport-Status") +
kpi("blue", "Modelle", "monitor",
`${c.running}<small>/${c.total}</small>`, "aktiv / gesamt") +
kpi("purple", "Jobs", "layers", c.jobsRun, "laufend") +
kpi(c.jobsErr ? "red" : "muted", "Fehler", "alert", c.jobsErr, "in der Aktivität") +
kpi(SYS ? "blue" : "muted", "System-Last", "gauge", sysV, sysS);
}
function kvRow(k, v, cls = "") {
return `<div class="kv-row"><span class="kv-k">${k}</span><span class="kv-v ${cls}">${v}</span></div>`;
}
function renderHealth() {
const c = counts();
let sysRow = kvRow("Auslastung (RAM/GPU/Disk)", "folgt", "na");
if (SYS) {
const gb = b => (b / 1024 / 1024 / 1024).toFixed(1);
const ram = `${gb(SYS.ram.used)}GB / ${gb(SYS.ram.total)}GB`;
const gpu = (SYS.gpu && SYS.gpu.vram.total) ? `${gb(SYS.gpu.vram.used + SYS.gpu.gtt.used)}GB / ${gb(SYS.gpu.vram.total + SYS.gpu.gtt.total)}GB` : "";
const disk = `${SYS.disk.percent.toFixed(0)}%`;
sysRow = kvRow("Auslastung (RAM/GPU/Disk)", `${ram} | ${gpu} | ${disk}`);
}
$("#health").innerHTML = `
<div class="card-h"><h3>Stack-Gesundheit</h3>
<span class="meta ${c.swap ? "ok" : ""}">${c.swap ? "Connected" : "Offline"}</span></div>
<div class="kv">
${kvRow("llama-swap", c.swap ? "Connected" : "Offline", c.swap ? "ok" : "bad")}
${kvRow("Modelle (gesamt)", c.total)}
${kvRow("Aktiv", c.running, c.running ? "ok" : "")}
${kvRow("Jobs (laufend)", c.jobsRun)}
${kvRow("Fehler", c.jobsErr, c.jobsErr ? "bad" : "")}
${sysRow}
</div>`;
}
function modelRow(m) {
const on = RUNNING.has(m.state);
const dot = m.state === "loading" || m.state === "starting" ? "load" : on ? "on" : "";
const state = on ? (m.state === "loading" ? "lädt…" : "geladen") : "bereit";
return `<div class="li">
<span class="li-dot ${dot}"></span>
<div class="li-main">
<div class="li-id">${esc(m.name)}</div>
<div class="li-sub">TTL ${m.ttl ?? "—"}${typeof m.ttl === "number" ? "s" : ""}</div>
</div>
<div class="li-right">
<div class="li-meta">${m.port ?? "auto"}</div>
<div class="li-time">${state}</div>
</div>
</div>`;
}
function renderModels() {
const models = S?.models || [];
$("#ov-models").innerHTML = `
<div class="card-h"><h3>Modelle</h3><span class="meta">${models.length || ""}</span></div>
${models.length
? `<div class="list">${models.map(modelRow).join("")}</div>`
: `<div class="empty-c"><div class="e-t">Keine Modelle konfiguriert</div>
<div class="e-s">Hol dir unter „Modelle" eins von HuggingFace.</div></div>`}`;
}
function renderAll() { renderHero(); renderKpis(); renderHealth(); renderModels(); }
function mount() { renderAll(); }
function onStatus(s) { S = s; renderAll(); }
function onJobs(jobs) { J = jobs || []; renderHero(); renderKpis(); renderHealth(); }
function onSystem(sys) { SYS = sys; renderKpis(); renderHealth(); }
export default { id: "overview", mount, onStatus, onJobs, onSystem };