This repository has been archived on 2026-07-22. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
mission-control/static/js/panels/overview.js
T
Hitonabi 82d15d82db fix(phase-a): Kontext-Cap, Download-State, Recipe-Edit, OS-Badge, Swap-Flash
A1: 32768-Kontext-Cap entfernt (install-recipe, install-model, register) →
    max_ctx_for() liefert nun bis zu 128k auf Strix Halo; behebt "context
    size exceeded" bei externen Tools.
A2: Download-State jetzt im Status-Endpoint sichtbar: Modelle zeigen
    "↓ Download X%" statt "bereit" während Job läuft (Backend + Frontend).
A3: PUT /api/cookbook/user-recipe/{id} + Edit-Button (✎) für eigene Setups.
    Download-Modal setzt Kontext-Input automatisch auf optimal.
A4: /api/updates liefert apt_cache_age_h; Badge zeigt Tooltip + ⚠ wenn >24h.
A5: Swap-Flash: Topbar-Text pulst kurz teal wenn Modell den State wechselt.
A6: LLM-Engine-Update fragt jetzt per confirmModal nach (Konsistenz).
A7: Event-Delegation statt per-render addEventListener in models.js.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-22 21:33:03 +02:00

166 lines
8.0 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 downloading = m.state === "downloading";
const on = RUNNING.has(m.state);
const dot = (m.state === "loading" || m.state === "starting" || downloading) ? "load" : on ? "on" : "";
let status;
if (downloading) {
const pct = m.download_progress != null ? ` ${Math.round(m.download_progress)}%` : "";
status = `↓ Download${pct}`;
} else {
status = on ? (m.state === "loading" ? "lädt…" : "geladen") : "bereit";
}
const file = m.meta?.filename ? `<div class="li-sub mono-sm">${esc(m.meta.filename)}</div>` : "";
return `<div class="li"><span class="li-dot ${dot}"></span>
<div class="li-main">
<div class="flex items-center gap-2"><span class="li-id">${esc(m.name)}</span>${capTag(m.meta?.caps)}</div>${file}</div>
<span class="li-meta" style="white-space:nowrap">${esc(status)}</span></div>`;
}
async function renderNews() {
const el = $("#ov-news"); if (!el) return;
const head = `<div class="card-h"><h3>Top-News</h3><span class="meta" style="cursor:pointer" onclick="window.mcOv.go('news')">Alle →</span></div>`;
el.innerHTML = head + `<div class="empty" style="text-align:center;padding:20px">Lade…</div>`;
try {
const items = ((await api("/api/news")).items || []).slice(0, 4);
el.innerHTML = head + (items.length
? `<div class="list">${items.map(n => `<a class="li" href="${esc(n.link)}" target="_blank" rel="noopener" style="text-decoration:none;color:inherit">
<div class="li-main"><div style="font-size:13px;line-height:1.4">${esc(n.title)}</div>
<div class="li-sub">${esc(n.source)}</div></div></a>`).join("")}</div>`
: `<div class="empty">Keine News.</div>`);
} catch { el.innerHTML = head + `<div class="empty">News gerade nicht ladbar.</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(); renderNews(); }
function onStatus(s) { S = s; renderHero(); renderTiles(); renderStack(); }
function onSystem(sys) { SYS = sys; renderHero(); renderTiles(); renderHealth(); }
export default { id: "overview", mount, onStatus, onSystem };