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:
+8
-5
@@ -185,10 +185,8 @@ def _pick_gguf(repo: str, quant: str = "Q4_K_M") -> str | None:
|
|||||||
return (pref or nosplit or ggufs)[0]
|
return (pref or nosplit or ggufs)[0]
|
||||||
|
|
||||||
|
|
||||||
@router.get("/upgrades")
|
def compute_upgrades(ram_gb):
|
||||||
def upgrades():
|
"""Liste relevanter Modell-Upgrades für die installierten Modelle (UPGRADES-Map)."""
|
||||||
"""Schlägt für installierte 'alte' Modelle ein aktuelleres vor (UPGRADES-Map)."""
|
|
||||||
ram_gb = psutil.virtual_memory().total / (1024 ** 3)
|
|
||||||
installed = (read_config().get("models") or {})
|
installed = (read_config().get("models") or {})
|
||||||
out, seen = [], set()
|
out, seen = [], set()
|
||||||
for up in UPGRADES:
|
for up in UPGRADES:
|
||||||
@@ -210,7 +208,12 @@ def upgrades():
|
|||||||
"fit": evaluate_fit(up["params_b"], up["quant"], 8192, ram_gb),
|
"fit": evaluate_fit(up["params_b"], up["quant"], 8192, ram_gb),
|
||||||
"optimal_ctx": max_ctx_for(up["params_b"], up["quant"], ram_gb),
|
"optimal_ctx": max_ctx_for(up["params_b"], up["quant"], ram_gb),
|
||||||
})
|
})
|
||||||
return {"upgrades": out}
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/upgrades")
|
||||||
|
def upgrades():
|
||||||
|
return {"upgrades": compute_upgrades(psutil.virtual_memory().total / (1024 ** 3))}
|
||||||
|
|
||||||
|
|
||||||
@router.post("/install-model")
|
@router.post("/install-model")
|
||||||
|
|||||||
@@ -72,6 +72,25 @@ def reboot(req: PwdReq):
|
|||||||
pass
|
pass
|
||||||
return {"ok": True, "note": "Reboot getriggert."}
|
return {"ok": True, "note": "Reboot getriggert."}
|
||||||
|
|
||||||
|
@router.get("/updates")
|
||||||
|
def updates():
|
||||||
|
"""Ausstehende Updates für die Toolbar: OS-Pakete (apt-Cache) + Modell-Upgrades."""
|
||||||
|
import psutil
|
||||||
|
from routers.cookbook import compute_upgrades
|
||||||
|
os_count = 0
|
||||||
|
try:
|
||||||
|
out = subprocess.run(
|
||||||
|
["bash", "-c", "apt list --upgradable 2>/dev/null | grep -c upgradable || true"],
|
||||||
|
capture_output=True, text=True, timeout=10)
|
||||||
|
os_count = int((out.stdout or "0").strip() or 0)
|
||||||
|
except Exception: # noqa: BLE001
|
||||||
|
os_count = 0
|
||||||
|
try:
|
||||||
|
models = len(compute_upgrades(psutil.virtual_memory().total / (1024 ** 3)))
|
||||||
|
except Exception: # noqa: BLE001
|
||||||
|
models = 0
|
||||||
|
return {"os": os_count, "models": models}
|
||||||
|
|
||||||
@router.websocket("/logs/{service}")
|
@router.websocket("/logs/{service}")
|
||||||
async def stream_logs(websocket: WebSocket, service: str):
|
async def stream_logs(websocket: WebSocket, service: str):
|
||||||
await websocket.accept()
|
await websocket.accept()
|
||||||
|
|||||||
@@ -148,6 +148,10 @@ textarea{resize:vertical;min-height:64px;font-family:var(--sans)}
|
|||||||
.info-dot{display:inline-flex;vertical-align:-3px;color:var(--dim);cursor:help;margin-left:5px}
|
.info-dot{display:inline-flex;vertical-align:-3px;color:var(--dim);cursor:help;margin-left:5px}
|
||||||
.info-dot svg{width:14px;height:14px}
|
.info-dot svg{width:14px;height:14px}
|
||||||
.info-dot:hover{color:var(--accent)}
|
.info-dot:hover{color:var(--accent)}
|
||||||
|
.upd-badge{font-size:11.5px;padding:4px 10px;border-radius:999px;cursor:pointer;white-space:nowrap;
|
||||||
|
background:rgba(68,147,224,.14);color:#86b9ff;border:1px solid rgba(68,147,224,.25)}
|
||||||
|
.upd-badge.warn{background:rgba(224,163,46,.14);color:#f0c570;border-color:rgba(224,163,46,.25)}
|
||||||
|
.upd-badge:hover{filter:brightness(1.12)}
|
||||||
.mono-sm{font-family:var(--mono);font-size:11.5px;color:var(--mut)}
|
.mono-sm{font-family:var(--mono);font-size:11.5px;color:var(--mut)}
|
||||||
|
|
||||||
/* ---- Buttons ---- */
|
/* ---- Buttons ---- */
|
||||||
|
|||||||
+5
-1
@@ -33,6 +33,7 @@
|
|||||||
<header class="topbar">
|
<header class="topbar">
|
||||||
<span class="status-pill"><span id="swdot" class="dot"></span><span id="swlabel">verbinde…</span></span>
|
<span class="status-pill"><span id="swdot" class="dot"></span><span id="swlabel">verbinde…</span></span>
|
||||||
<span class="spacer"></span>
|
<span class="spacer"></span>
|
||||||
|
<span id="update-badges" class="flex" style="gap:6px"></span>
|
||||||
<span class="top-stat" id="top-active-text">Kein Modell geladen</span>
|
<span class="top-stat" id="top-active-text">Kein Modell geladen</span>
|
||||||
<span class="sec-chip" id="sec-chip"><span data-ic="shield"></span><span id="sec-chip-tx">Nur im Heimnetz</span></span>
|
<span class="sec-chip" id="sec-chip"><span data-ic="shield"></span><span id="sec-chip-tx">Nur im Heimnetz</span></span>
|
||||||
</header>
|
</header>
|
||||||
@@ -50,7 +51,10 @@
|
|||||||
<div class="card" id="ov-health"></div>
|
<div class="card" id="ov-health"></div>
|
||||||
<div class="card" id="ov-quickstart"></div>
|
<div class="card" id="ov-quickstart"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card" id="ov-stack"></div>
|
<div class="grid grid-2">
|
||||||
|
<div class="card" id="ov-stack"></div>
|
||||||
|
<div class="card" id="ov-news"></div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="view" data-view="activity" hidden>
|
<section class="view" data-view="activity" hidden>
|
||||||
|
|||||||
@@ -74,6 +74,24 @@ function showAlert(html, warn) {
|
|||||||
}
|
}
|
||||||
function hideAlert() { $("#alert").style.display = "none"; }
|
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 ----
|
// ---- Polling ----
|
||||||
async function pollStatus() {
|
async function pollStatus() {
|
||||||
try { applyStatus(await api("/api/status")); }
|
try { applyStatus(await api("/api/status")); }
|
||||||
@@ -141,3 +159,5 @@ connectSystemStream();
|
|||||||
setInterval(tickClock, 1000);
|
setInterval(tickClock, 1000);
|
||||||
setInterval(pollStatus, 3000);
|
setInterval(pollStatus, 3000);
|
||||||
setInterval(pollJobs, 1500);
|
setInterval(pollJobs, 1500);
|
||||||
|
pollUpdates();
|
||||||
|
setInterval(pollUpdates, 60000);
|
||||||
|
|||||||
@@ -118,11 +118,26 @@ function capTag(caps) {
|
|||||||
function stackRow(m) {
|
function stackRow(m) {
|
||||||
const on = RUNNING.has(m.state);
|
const on = RUNNING.has(m.state);
|
||||||
const dot = m.state === "loading" || m.state === "starting" ? "load" : on ? "on" : "";
|
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>
|
return `<div class="li"><span class="li-dot ${dot}"></span>
|
||||||
<span class="li-id" style="flex:1">${esc(m.name)}</span>
|
<div class="li-main">
|
||||||
${capTag(m.meta?.caps)}
|
<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="width:120px;text-align:right">${status}</span></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() {
|
function renderStack() {
|
||||||
const ms = models();
|
const ms = models();
|
||||||
@@ -136,7 +151,7 @@ function renderStack() {
|
|||||||
|
|
||||||
function renderAll() { renderHero(); renderTiles(); renderHealth(); renderQuickstart(); 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 onStatus(s) { S = s; renderHero(); renderTiles(); renderStack(); }
|
||||||
function onSystem(sys) { SYS = sys; renderHero(); renderTiles(); renderHealth(); }
|
function onSystem(sys) { SYS = sys; renderHero(); renderTiles(); renderHealth(); }
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user