v6 Phase A: Dashboard & Toolbar

- 'Dein Stack' zeigt echtes Modell (Dateiname) hinter coder/scout/vision.
- Top-News-Karte auf der Uebersicht (neben Stack).
- Toolbar-Update-Badges: /api/updates (OS via apt-Cache + Modell-Upgrades),
  klickbar -> Server bzw. News. compute_upgrades aus cookbook wiederverwendet.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-06-21 14:38:18 +02:00
parent b50323b521
commit d7a3253740
6 changed files with 76 additions and 11 deletions
+20
View File
@@ -74,6 +74,24 @@ function showAlert(html, warn) {
}
function hideAlert() { $("#alert").style.display = "none"; }
// ---- Toolbar: ausstehende Updates anzeigen ----
const goView = v => document.querySelector(`.nav-item[data-view="${v}"]`)?.click();
function badge(text, cls, view) {
const s = document.createElement("span");
s.className = "upd-badge" + (cls ? " " + cls : "");
s.textContent = text; s.onclick = () => goView(view);
return s;
}
async function pollUpdates() {
try {
const u = await api("/api/updates");
const el = $("#update-badges"); if (!el) return;
el.innerHTML = "";
if (u.os > 0) el.appendChild(badge(`${u.os} OS-Updates`, "warn", "server"));
if (u.models > 0) el.appendChild(badge(`${u.models} Modell-Upgrade${u.models > 1 ? "s" : ""}`, "", "news"));
} catch { /* still */ }
}
// ---- Polling ----
async function pollStatus() {
try { applyStatus(await api("/api/status")); }
@@ -141,3 +159,5 @@ connectSystemStream();
setInterval(tickClock, 1000);
setInterval(pollStatus, 3000);
setInterval(pollJobs, 1500);
pollUpdates();
setInterval(pollUpdates, 60000);
+20 -5
View File
@@ -118,11 +118,26 @@ function capTag(caps) {
function stackRow(m) {
const on = RUNNING.has(m.state);
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";
const 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>
<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>`;
<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">${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();
@@ -136,7 +151,7 @@ function renderStack() {
function renderAll() { renderHero(); renderTiles(); renderHealth(); renderQuickstart(); renderStack(); }
function mount() { renderQuickstart(); renderAll(); }
function mount() { renderQuickstart(); renderAll(); renderNews(); }
function onStatus(s) { S = s; renderHero(); renderTiles(); renderStack(); }
function onSystem(sys) { SYS = sys; renderHero(); renderTiles(); renderHealth(); }