143 lines
5.5 KiB
JavaScript
143 lines
5.5 KiB
JavaScript
// 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";
|
||
|
||
let caps = "";
|
||
if (m.meta && m.meta.caps) {
|
||
caps = m.meta.caps.map(c => {
|
||
if (c === "Code") return `<span title="Code" style="color:var(--blue);font-size:0.9em;margin-left:6px">{ }</span>`;
|
||
if (c === "Bild") return `<span title="Vision" style="color:var(--purple);font-size:0.9em;margin-left:6px">👁</span>`;
|
||
return "";
|
||
}).join("");
|
||
}
|
||
|
||
return `<div class="li">
|
||
<span class="li-dot ${dot}"></span>
|
||
<div class="li-main">
|
||
<div class="li-id" style="font-weight:500">${esc(m.name)}${caps}</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 };
|