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>
This commit is contained in:
+122
-163
@@ -1,184 +1,143 @@
|
||||
// overview.js — Dashboard: Quick Actions, Modelle & Recent Jobs
|
||||
// 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 } from "../core/ui.js";
|
||||
import { $, esc, icon, toast, fmtBytes, confirmModal } from "../core/ui.js";
|
||||
|
||||
let S = null; // letzter Status
|
||||
let J = []; // letzte Job-Liste
|
||||
let SYS = null;
|
||||
let S = null; // letzter Status
|
||||
let SYS = null; // letzte System-Metriken
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
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() {
|
||||
$("#hero").innerHTML = `<div class="hero">
|
||||
<div>
|
||||
<div class="eyebrow">Dashboard</div>
|
||||
<h1>Mission Control</h1>
|
||||
<p>Steuerzentrale für deinen lokalen llama-swap-Stack. Hier verwaltest du Modelle, Downloads und Server-Wartung.</p>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
async function triggerAction(action) {
|
||||
if (action === "restart_llama") {
|
||||
toast("Neustart ausgelöst...");
|
||||
try {
|
||||
await api("/api/service/llama-swap/restart", { method: "POST" });
|
||||
toast("llama-swap wird neugestartet.");
|
||||
} catch(e) {
|
||||
toast("Fehler: " + e.message, true);
|
||||
}
|
||||
} else if (action === "update_mc") {
|
||||
toast("Update gestartet! Siehe Aktivitäten.");
|
||||
try {
|
||||
await api("/api/update", { method: "POST" });
|
||||
document.querySelector('.nav-item[data-view="activity"]').click();
|
||||
} catch(e) {
|
||||
toast("Fehler: " + e.message, true);
|
||||
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>`;
|
||||
}
|
||||
|
||||
// Global hook für onclick
|
||||
window.triggerAction = triggerAction;
|
||||
|
||||
function renderQuickActions() {
|
||||
let actionsHtml = "";
|
||||
|
||||
// ---- 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 ram_percent = SYS.ram.percent || 0;
|
||||
// Wenn RAM über 90% ist, zeige Warnung.
|
||||
// Wir nehmen 90 für Produktion, aber für den Test könnte es angepasst werden.
|
||||
if (ram_percent >= 90) {
|
||||
actionsHtml += `
|
||||
<div class="card" style="background:var(--red-dim); border:1px solid var(--red); grid-column:1/-1;">
|
||||
<div class="flex justify-between items-center">
|
||||
<div>
|
||||
<h3 style="color:var(--red); margin:0;">⚠️ Arbeitsspeicher kritisch (${ram_percent.toFixed(0)}%)</h3>
|
||||
<p style="color:var(--red); margin-top:4px;">Der RAM/VRAM ist fast voll. Dies kann zu Systeminstabilität führen.</p>
|
||||
</div>
|
||||
<button class="primary warn" onclick="window.triggerAction('restart_llama')">VRAM leeren (Neustart)</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Simulierter Update-Check (Idealerweise vom Backend, hier als permanenter Button wenn man manuell checken will,
|
||||
// oder wir blenden ihn ein wenn ein lokales flag gesetzt ist. Wir zeigen ihn hier als Feature-Highlight)
|
||||
// Da wir aktuell keinen echten Git-Check im Backend haben, zeigen wir einen "Update Prüfen" Button in den QuickActions.
|
||||
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…");
|
||||
}
|
||||
|
||||
// 3 Standard Kacheln (Cookbook, Server-Status, Aktivität/Guides)
|
||||
$("#ov-quick").innerHTML = actionsHtml + `
|
||||
<button class="card-btn" onclick="document.querySelector('.nav-item[data-view=\\'cookbook\\']').click()">
|
||||
<div class="flex justify-between items-center">
|
||||
<h3>Modell finden</h3>
|
||||
<span class="text-act">${icon("book")}</span>
|
||||
</div>
|
||||
<p>Durchsuche HuggingFace nach neuen Modellen im Cookbook.</p>
|
||||
</button>
|
||||
|
||||
<button class="card-btn" onclick="window.triggerAction('update_mc')">
|
||||
<div class="flex justify-between items-center">
|
||||
<h3>Container Updates</h3>
|
||||
<span class="text-act">${icon("download")}</span>
|
||||
</div>
|
||||
<p>Prüfe auf Updates für Mission-Control und Llama.cpp.</p>
|
||||
</button>
|
||||
|
||||
<button class="card-btn" onclick="document.querySelector('.nav-item[data-view=\\'server\\']').click()">
|
||||
<div class="flex justify-between items-center">
|
||||
<h3>Wartung</h3>
|
||||
<span class="text-act">${icon("server")}</span>
|
||||
</div>
|
||||
<p>Server neustarten, VRAM leeren oder OS aktualisieren.</p>
|
||||
</button>
|
||||
`;
|
||||
$("#ov-tiles").innerHTML = engine + model + temp + mem;
|
||||
}
|
||||
|
||||
function modelRow(m) {
|
||||
// ---- 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 || m.state === "bereit") ? "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("");
|
||||
}
|
||||
|
||||
const filename = m.meta?.filename ? `<div class="li-sub" style="font-family:var(--mono); color:var(--mut);">${esc(m.meta.filename)}</div>` : '';
|
||||
|
||||
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>
|
||||
${filename}
|
||||
</div>
|
||||
<div class="li-right">
|
||||
<div class="li-time">${state}</div>
|
||||
</div>
|
||||
</div>`;
|
||||
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 renderModels() {
|
||||
const models = S?.models || [];
|
||||
$("#ov-models").innerHTML = `
|
||||
<div class="card-h"><h3>Aktuelle Modelle im Stack</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 „Cookbook“ eins von HuggingFace.</div></div>`}`;
|
||||
}
|
||||
function renderAll() { renderHero(); renderTiles(); renderHealth(); renderQuickstart(); renderStack(); }
|
||||
|
||||
function renderRecentJobs() {
|
||||
const latest = J.slice(0, 4);
|
||||
|
||||
const statusBadge = (s) => {
|
||||
if (s === "done") return '<span class="badge b-run" style="font-size:10px">fertig</span>';
|
||||
if (s === "failed") return '<span class="badge b-err" style="font-size:10px">fehler</span>';
|
||||
return '<span class="badge b-load" style="font-size:10px">lädt…</span>';
|
||||
};
|
||||
function mount() { renderQuickstart(); renderAll(); }
|
||||
function onStatus(s) { S = s; renderHero(); renderTiles(); renderStack(); }
|
||||
function onSystem(sys) { SYS = sys; renderHero(); renderTiles(); renderHealth(); }
|
||||
|
||||
$("#ov-recent-jobs").innerHTML = `
|
||||
<div class="card-h"><h3>Letzte Aktivitäten</h3><span class="meta" style="cursor:pointer" onclick="document.querySelector('.nav-item[data-view=\\'activity\\']').click()">Alle ansehen →</span></div>
|
||||
${latest.length
|
||||
? `<div class="list">
|
||||
${latest.map(j => `
|
||||
<div class="li">
|
||||
<div class="li-main">
|
||||
<div class="li-id text-sm">${esc(j.label)}</div>
|
||||
</div>
|
||||
<div class="li-right">${statusBadge(j.state)}</div>
|
||||
</div>
|
||||
`).join("")}
|
||||
</div>`
|
||||
: `<div class="empty-c"><div class="e-t">Keine Aktivitäten</div><div class="e-s">Alles läuft ruhig.</div></div>`
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
function renderAll() {
|
||||
renderHero();
|
||||
renderQuickActions();
|
||||
renderModels();
|
||||
renderRecentJobs();
|
||||
}
|
||||
|
||||
function mount() { renderAll(); }
|
||||
function onStatus(s) { S = s; renderModels(); }
|
||||
function onJobs(jobs) { J = jobs || []; renderRecentJobs(); }
|
||||
function onSystem(sys) { SYS = sys; }
|
||||
|
||||
export default { id: "overview", mount, onStatus, onJobs, onSystem };
|
||||
export default { id: "overview", mount, onStatus, onSystem };
|
||||
|
||||
Reference in New Issue
Block a user