364939466f
Architektur auf Separation of Concerns umgestellt – ohne Build-Schritt,
ohne neues Framework, ohne DB (KISS bleibt). Endpoint-URLs unveraendert,
daher 1:1-kompatibel zum bisherigen Stand.
Backend (Top-Level-Helfer + ein Router je Bereich):
- app.py auf duennen Einstieg reduziert (FastAPI + include_router + static)
- config/auth/jobengine/llamaswap als getrennte Helfer-Module
- Endpoints in routers/{models,jobs,maintenance}.py
Frontend (native ES-Module statt Single-File):
- index.html = Huelle: Sidebar-Nav, Topbar, Alert-Banner, Hash-Routing
- css/{base,components}.css – Tokens + Komponenten
- js/core/{api,ui,nav}.js + js/panels/{overview,models,maintenance,jobs}.js + main.js
- Panel-Vertrag: { id, mount?(), onStatus?(s), onJobs?(jobs) }
- Optik an docs/mission-control-overview.png angelehnt (Hero, KPI-Kacheln,
Listen, Aktivitaets-Stream, getoente Karten)
Doku: CLAUDE.md + README auf die neue Struktur aktualisiert.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
118 lines
4.3 KiB
JavaScript
118 lines
4.3 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
|
|
|
|
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();
|
|
$("#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("muted", "System-Last", "gauge", "n/a", "bald · Live-Auslastung");
|
|
}
|
|
|
|
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();
|
|
$("#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" : "")}
|
|
${kvRow("Auslastung (RAM/GPU/Disk)", "folgt", "na")}
|
|
</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(); }
|
|
|
|
export default { id: "overview", mount, onStatus, onJobs };
|