v3 Phase B: alle Panels auf das Design-System + Beginner-UX

- cookbook.js: Fit-Ampel (gruen/gelb/rot) + Legende + Klartext-Urteile, sauberes Modal.
- server.js: heikle Aktionen mit confirmModal/promptModal (Klartext-Konsequenz),
  Konsole im neuen Stil, Begriffe uebersetzt.
- models.js: Tabelle re-skinnt (Capability-Tags statt Emoji, --blue raus),
  Entladen mit Bestaetigung, Konfig-Modal vereinheitlicht.
- jobs.js (Aktivitaet): Metrik-Kacheln + Klartext-Verlaeufe.
- guides.js: Kopf + Intro, Integrations-URL aus Browser-Host abgeleitet.
- index.html: Mountpunkte fuer Modelle-/Aktivitaets-Kopf.
- app.py: no-cache-Middleware fuer /static (UI-Aenderungen wirken sofort nach rsync,
  kein Stale-JS mehr).
- base.css: Sidebar bei schmalem Viewport icon-only (Label-Ueberlappung gefixt).

Verifiziert: alle 6 Panels mounten fehlerfrei (0 Konsolenfehler), Fit-Ampel rechnet live.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-06-21 07:05:15 +02:00
parent 52b0a3bff5
commit 1aea0f558e
8 changed files with 482 additions and 751 deletions
+64 -104
View File
@@ -1,142 +1,102 @@
// jobs.js (Aktivität) — System-Metriken & Hintergrund-Jobs
// jobs.js Aktivität (v3): Live-System-Metriken + Hintergrund-Jobs mit Log.
// Exportiert track(id), damit andere Panels einen Job auto-aufklappen.
import { $, esc, icon } from "../core/ui.js";
import { $, esc, fmtBytes } from "../core/ui.js";
const tracked = new Set();
let JOBS = [];
let SYS = null;
export function track(id) {
tracked.add(id);
renderJobs();
}
export function track(id) { tracked.add(id); renderJobs(); }
function statusBadge(state) {
if (state === "done") return '<span class="badge b-run">fertig</span>';
if (state === "failed") return '<span class="badge b-err">fehler</span>';
return '<span class="badge b-load">lädt…</span>';
}
function dotClass(state) {
if (state === "done") return "on";
if (state === "failed") return "";
return "load";
}
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 kvRow(k, v, cls = "") {
return `<div class="kv-row"><span class="kv-k">${k}</span><span class="kv-v ${cls}">${v}</span></div>`;
}
// Chart-Daten (letzte 30 Sekunden, je 0.5s Stream = 60 Punkte)
const hist = { cpu: [], ram: [], gpu: [] };
const MAX_HIST = 60;
function statusBadge(s) { return s === "done" ? '<span class="badge b-run">fertig</span>' : s === "failed" ? '<span class="badge b-err">fehler</span>' : '<span class="badge b-load">läuft…</span>'; }
function dotClass(s) { return s === "done" ? "on" : s === "failed" ? "" : "load"; }
function tile(label, value, sub) {
return `<div class="tile"><div class="t-l">${label}</div><div class="t-v">${value}</div><div class="t-s">${sub}</div></div>`;
}
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 spark(arr, varName) {
return '<div style="display:flex;align-items:flex-end;gap:2px;height:38px;margin-top:10px">' +
arr.map(v => `<div style="width:4px;background:var(${varName});opacity:.55;height:${Math.max(2, v)}%;border-radius:2px"></div>`).join("") + "</div>";
}
function gpuPct() {
const g = SYS?.gpu;
if (g && (g.vram.total + g.gtt.total) > 0) return ((g.vram.used + g.gtt.used) / (g.vram.total + g.gtt.total)) * 100;
return 0;
}
function renderSys() {
if (!SYS) return;
const sysV = `${SYS.cpu.percent.toFixed(0)}<small>% CPU</small>`;
const sysS = `${SYS.ram.percent.toFixed(0)}% RAM, ${SYS.gpu_temp ? SYS.gpu_temp.toFixed(0)+'°C' : SYS.cpu.temp ? SYS.cpu.temp.toFixed(0)+'°C' : ''}`;
hist.cpu.push(SYS.cpu.percent); hist.ram.push(SYS.ram.percent); hist.gpu.push(gpuPct());
for (const k of ["cpu", "ram", "gpu"]) if (hist[k].length > MAX_HIST) hist[k].shift();
// History aktualisieren
hist.cpu.push(SYS.cpu.percent);
hist.ram.push(SYS.ram.percent);
let gpuP = 0;
if (SYS.gpu && SYS.gpu.vram.total) {
gpuP = ((SYS.gpu.vram.used + SYS.gpu.gtt.used) / (SYS.gpu.vram.total + SYS.gpu.gtt.total)) * 100;
}
hist.gpu.push(gpuP);
if (hist.cpu.length > MAX_HIST) hist.cpu.shift();
if (hist.ram.length > MAX_HIST) hist.ram.shift();
if (hist.gpu.length > MAX_HIST) hist.gpu.shift();
const k = $("#act-kpis");
if (k) k.innerHTML =
tile("Prozessor (CPU)", `${Math.round(SYS.cpu.percent)}<small> %</small>`, SYS.cpu.temp != null ? `${Math.round(SYS.cpu.temp)}° CPU-Temp` : "Auslastung") +
tile("Arbeitsspeicher", `${Math.round(SYS.ram.percent)}<small> %</small>`, `${fmtBytes(SYS.ram.used)} / ${fmtBytes(SYS.ram.total)}`) +
tile("Grafikspeicher", `${Math.round(gpuPct())}<small> %</small>`, SYS.gpu_temp != null ? `${Math.round(SYS.gpu_temp)}° GPU-Temp` : "VRAM + GTT");
// Mini-Sparklines generieren
const makeBars = (arr, color) => {
return '<div style="display:flex; align-items:flex-end; gap:2px; height:40px; margin-top:12px;">' +
arr.map(v => `<div style="width:4px; background:var(--${color}); opacity:0.6; height:${Math.max(2, v)}%; border-radius:2px;"></div>`).join("") +
'</div>';
};
$("#act-kpis").innerHTML =
kpi("blue", "CPU Last", "gauge", sysV, sysS) +
kpi("purple", "RAM", "monitor", `${SYS.ram.percent.toFixed(0)}<small>%</small>`, "Arbeitsspeicher") +
kpi("green", "GPU VRAM", "layers", `${gpuP.toFixed(0)}<small>%</small>`, "Grafikspeicher");
const gb = b => (b / 1024 / 1024 / 1024).toFixed(1);
const ramStr = `${gb(SYS.ram.used)} GB / ${gb(SYS.ram.total)} GB`;
const gpuStr = (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 diskStr = `${SYS.disk.percent.toFixed(0)}% belegt`;
$("#act-sys").innerHTML = `
const g = SYS.gpu;
const gpuStr = g && (g.vram.total + g.gtt.total) > 0 ? `${fmtBytes(g.vram.used + g.gtt.used)} / ${fmtBytes(g.vram.total + g.gtt.total)}` : "";
const s = $("#act-sys");
if (s) s.innerHTML = `
<div class="card-h"><h3>System-Metriken (Bosgame)</h3></div>
<div class="kv" style="margin-bottom: 24px;">
${kvRow("Arbeitsspeicher (RAM)", ramStr)}
${kvRow("Grafikspeicher (VRAM+GTT)", gpuStr)}
${kvRow("Speicherplatz (Disk)", diskStr)}
${kvRow("Temperatur (GPU / CPU)", `${SYS.gpu_temp?.toFixed(1) || ''}°C / ${SYS.cpu.temp?.toFixed(1) || ''}°C`)}
<div class="card-sub">Live-Auslastung deines Mini-PCs, alle 0,5 Sekunden.</div>
<div class="kv" style="margin-bottom:6px">
<div class="kv-row"><span class="kv-k">Arbeitsspeicher (RAM)</span><span class="kv-v">${fmtBytes(SYS.ram.used)} / ${fmtBytes(SYS.ram.total)}</span></div>
<div class="kv-row"><span class="kv-k">Grafikspeicher (VRAM + GTT)</span><span class="kv-v">${gpuStr}</span></div>
<div class="kv-row"><span class="kv-k">Speicherplatz (Disk)</span><span class="kv-v">${Math.round(SYS.disk.percent)} % belegt</span></div>
<div class="kv-row"><span class="kv-k">Temperatur (GPU / CPU)</span><span class="kv-v">${SYS.gpu_temp != null ? Math.round(SYS.gpu_temp) + "°" : ""} / ${SYS.cpu.temp != null ? Math.round(SYS.cpu.temp) + "°" : ""}</span></div>
</div>
<div class="grid grid-3">
<div><div class="meta">CPU Historie</div>${makeBars(hist.cpu, "act")}</div>
<div><div class="meta">RAM Historie</div>${makeBars(hist.ram, "purple")}</div>
<div><div class="meta">VRAM Historie</div>${makeBars(hist.gpu, "on")}</div>
</div>
`;
<div class="grid grid-3" style="margin-top:14px">
<div><div class="meta text-xs">CPU-Verlauf</div>${spark(hist.cpu, "--accent")}</div>
<div><div class="meta text-xs">RAM-Verlauf</div>${spark(hist.ram, "--purple")}</div>
<div><div class="meta text-xs">VRAM-Verlauf</div>${spark(hist.gpu, "--on")}</div>
</div>`;
}
function mount() {
$("#v-activity").innerHTML = `
<div class="card-h"><h3>Hintergrund-Aktivitäten</h3><span class="meta" id="job-count"></span></div>
<div id="jobs"></div>
<div id="jobs-empty" class="empty-c">
<div class="e-t">Keine laufenden Jobs.</div>
<div class="e-s">Downloads, Updates &amp; Co. erscheinen hier mit Live-Log.</div>
</div>`;
$("#act-head").innerHTML = `<div class="pagehead"><div>
<h1>Aktivität</h1>
<div class="sub">Live-Auslastung und laufende Aufgaben (Downloads, Updates) mit Protokoll.</div></div></div>`;
$("#v-activity").innerHTML = `
<div class="card-h"><h3>Hintergrund-Aufgaben</h3><span class="meta" id="job-count"></span></div>
<div class="card-sub">Downloads &amp; Updates erscheinen hier mit Live-Protokoll — zum Aufklappen klicken.</div>
<div id="jobs"></div>
<div id="jobs-empty" class="empty-c"><div class="e-t">Gerade nichts los.</div><div class="e-s">Alles ruhig — keine laufenden Aufgaben.</div></div>`;
// Klicks auf Job-Kopf -> auf/zuklappen
$("#v-activity").addEventListener("click", e => {
const h = e.target.closest(".job-h");
if (!h) return;
const h = e.target.closest(".job-h"); if (!h) return;
const id = h.getAttribute("data-id");
tracked.has(id) ? tracked.delete(id) : tracked.add(id);
renderJobs();
});
if (SYS) renderSys();
}
function renderJobs() {
const c = $("#jobs");
if (!c) return;
const c = $("#jobs"); if (!c) return;
$("#jobs-empty").style.display = JOBS.length ? "none" : "flex";
const failed = JOBS.filter(j => j.state === "failed").length;
$("#job-count").textContent = JOBS.length ? (failed ? failed + " Fehler" : JOBS.length + " gesamt") : "";
c.innerHTML = JOBS.map(j => {
const open = tracked.has(j.id);
const log = open ? `<div class="log">${esc((j.log || []).join("\\n"))}</div>` : "";
return `<div class="job">
<div class="job-h" data-id="${esc(j.id)}">
<span class="li-dot ${dotClass(j.state)}"></span>
<span class="mid">${esc(j.label)}</span>${statusBadge(j.state)}
</div>${log}</div>`;
const log = tracked.has(j.id) ? `<div class="log">${esc((j.log || []).join("\n"))}</div>` : "";
return `<div class="job"><div class="job-h" data-id="${esc(j.id)}">
<span class="li-dot ${dotClass(j.state)}"></span><span class="mid">${esc(j.label)}</span>${statusBadge(j.state)}</div>${log}</div>`;
}).join("");
}
function onJobs(jobs) {
JOBS = jobs || [];
renderJobs();
}
function onSystem(sys) {
SYS = sys;
const c = $("#act-sys");
if (c) renderSys();
}
function onJobs(jobs) { JOBS = jobs || []; renderJobs(); }
function onSystem(sys) { SYS = sys; if ($("#act-sys")) renderSys(); }
export default { id: "jobs", mount, onJobs, onSystem };