diff options
| author | Danilo M. <danix@danix.xyz> | 2026-11-09 19:17:29 +0100 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-11-09 19:17:29 +0100 |
| commit | 67e64e964a85562300b463378f0e260d6978867f (patch) | |
| tree | cb706f2142823ca436d301d8b7a2af27e88231c1 | |
| parent | 796407233383e7bf4825f671d84fbe71279488a6 (diff) | |
| download | deck-it-67e64e964a85562300b463378f0e260d6978867f.tar.gz deck-it-67e64e964a85562300b463378f0e260d6978867f.zip | |
feat: build VM card shows the real stats dashboard
| -rw-r--r-- | deckit/modules/sbbuilder.py | 18 | ||||
| -rw-r--r-- | static/deck.css | 6 | ||||
| -rw-r--r-- | static/modules/sbbuilder.js | 96 | ||||
| -rw-r--r-- | tests/test_sbbuilder.py | 6 |
4 files changed, 74 insertions, 52 deletions
diff --git a/deckit/modules/sbbuilder.py b/deckit/modules/sbbuilder.py index 41f3a14..13552a1 100644 --- a/deckit/modules/sbbuilder.py +++ b/deckit/modules/sbbuilder.py @@ -1,7 +1,8 @@ -"""sb-builder build progress, read-only over ssh. +"""sb-builder build state, read-only over ssh. -The build VM's `sbb-remote jobs` already returns JSON; this module runs it over -the configured ssh host alias and relays it. Nothing here writes to the VM. +`sbb-remote stats` carries the dashboard: VM load, uptime, memory, disk, package +counts, orphans, slackrepo results and overlay pulls. `jobs` adds the recent +build list. Both run over one ssh connection and arrive as two JSON objects. """ import json @@ -10,7 +11,7 @@ import subprocess from ..module import Module -def _run(cmd, timeout=5): +def _run(cmd, timeout=8): try: return subprocess.run(cmd, capture_output=True, text=True, timeout=timeout).stdout except (OSError, subprocess.TimeoutExpired): @@ -24,13 +25,16 @@ def make(cfg: dict) -> Module: def state(): try: out = run(["ssh", "-o", "BatchMode=yes", "-o", "ConnectTimeout=4", - host, "sbb-remote", "jobs"]) + host, "sbb-remote stats; sbb-remote jobs"]).lstrip() except OSError: return {"offline": True} try: - data = json.loads(out) + decoder = json.JSONDecoder() + stats, end = decoder.raw_decode(out) + jobs, _ = decoder.raw_decode(out[end:].lstrip()) except (ValueError, TypeError): return {"offline": True} - return {"offline": False, "jobs": data.get("jobs", [])} + return {"offline": False, "stats": stats, + "jobs": jobs.get("jobs", []) if isinstance(jobs, dict) else []} return Module(name="sbbuilder", title="Build VM", state=state) diff --git a/static/deck.css b/static/deck.css index 40d6ac8..8b2416c 100644 --- a/static/deck.css +++ b/static/deck.css @@ -131,8 +131,10 @@ body { .music .time { font-size: 12px; color: var(--udt-subtext0); white-space: nowrap; } .music .controls { display: flex; flex-wrap: wrap; gap: 8px; } -.sbb .sbb-summary { margin-bottom: 8px; font-size: 12px; } -.sbb .row.job .kind { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.sbb .sbb-now { margin-bottom: 8px; font-size: 12px; font-weight: 600; } +.sbb .metric { display: flex; justify-content: space-between; gap: 12px; padding: 3px 0; font-size: 13px; } +.sbb .metric .k { color: var(--udt-subtext0); flex: none; } +.sbb .metric .v { text-align: right; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .grid-stack-item .ui-resizable-se { width: 22px; height: 22px; right: 2px; bottom: 2px; opacity: .45; } .grid-stack-item:hover .ui-resizable-se { opacity: 1; } diff --git a/static/modules/sbbuilder.js b/static/modules/sbbuilder.js index f1c574d..02493af 100644 --- a/static/modules/sbbuilder.js +++ b/static/modules/sbbuilder.js @@ -1,58 +1,72 @@ -function chipClass(status) { - if (status === "ok") return "good"; - if (status === "failed" || status === "lost" || status === "killed") return "bad"; - if (status === "running" || status === "busy" || status === "starting") return "warn"; - return "muted"; -} +const gib = (b) => (b == null ? "?" : (b / 1073741824).toFixed(1)); +const pct = (n) => (n == null ? "?" : `${Math.round(n)}%`); +const up = (s) => { + if (s == null) return "?"; + const d = Math.floor(s / 86400); + const h = Math.floor((s % 86400) / 3600); + return d ? `${d}d ${h}h` : `${h}h`; +}; +const ago = (ts) => { + if (!ts) return "—"; + const d = Math.max(0, Math.floor(Date.now() / 1000 - ts)); + if (d < 3600) return `${Math.floor(d / 60)}m`; + if (d < 86400) return `${Math.floor(d / 3600)}h`; + return `${Math.floor(d / 86400)}d`; +}; -function when(ts) { - if (!ts) return ""; - return new Date(ts * 1000).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }); -} +const ACTIVE = ["running", "busy", "starting"]; -function summary(jobs) { - const running = jobs.filter((j) => ["running", "busy", "starting"].includes(j.status)).length; - const failed = jobs.filter((j) => ["failed", "lost", "killed"].includes(j.status)).length; - const parts = []; - if (running) parts.push(`${running} running`); - if (failed) parts.push(`${failed} failed`); - if (!parts.length) parts.push(jobs.length ? `${jobs.length} recent` : "idle"); - return parts.join(" · "); +function metric(label, value) { + const row = document.createElement("div"); + row.className = "metric"; + const k = document.createElement("span"); + k.className = "k"; + k.textContent = label; + const v = document.createElement("span"); + v.className = "v num"; + v.textContent = value; + row.append(k, v); + return row; } export default { title: "Build VM", - interval: 5, + interval: 10, mount(el) { - el.innerHTML = `<div class="sbb"><div class="sbb-summary muted"></div><div class="sbb-list"></div></div>`; + el.innerHTML = `<div class="sbb"></div>`; }, update(state, el) { - const sum = el.querySelector(".sbb-summary"); - const list = el.querySelector(".sbb-list"); + const box = el.querySelector(".sbb"); + box.textContent = ""; if (state.offline) { - sum.textContent = ""; - list.textContent = "build VM unreachable"; + box.textContent = "build VM unreachable"; return; } + const s = state.stats ?? {}; + const p = s.packages ?? {}; + const sr = s.slackrepo ?? {}; const jobs = state.jobs ?? []; - sum.textContent = summary(jobs); - list.textContent = ""; - for (const job of jobs.slice(0, 8)) { - const row = document.createElement("div"); - row.className = "row job"; - const kind = document.createElement("span"); - kind.className = "kind"; - kind.textContent = job.kind || "job"; - const time = document.createElement("span"); - time.className = "muted num"; - time.textContent = when(job.created); - const status = document.createElement("span"); - status.className = chipClass(job.status); - status.textContent = job.status; - row.append(kind, time, status); - list.append(row); - } + + const running = jobs.find((j) => ACTIVE.includes(j.status)); + const now = document.createElement("div"); + now.className = "sbb-now"; + now.textContent = running ? `● ${running.kind || "job"} running` : "idle"; + now.classList.toggle("warn", Boolean(running)); + box.append(now); + + const overlays = Object.entries(p).filter(([k]) => k !== "total" && k !== "shadowed"); + const results = Object.entries(sr.results ?? {}); + box.append( + metric("CPU · load", `${pct(s.cpu)} · ${s.load ? s.load[0].toFixed(2) : "?"}`), + metric("Uptime", up(s.uptime)), + metric("Memory", `${gib((s.mem?.total ?? 0) - (s.mem?.available ?? 0))} / ${gib(s.mem?.total)} GiB`), + metric("Disk free", `${gib(s.disk?.free)} / ${gib(s.disk?.total)} GiB`), + metric("Packages", `${p.total ?? "?"}${overlays.length ? " · " + overlays.map(([k, v]) => `${k} ${v}`).join(" · ") : ""}`), + metric("Shadowed · orphans", `${p.shadowed ?? "?"} · ${s.orphans ?? "?"}`), + metric("Built · results", `${sr.built ?? "?"}${results.length ? " · " + results.map(([k, v]) => `${v} ${k}`).join(" · ") : ""}`), + metric("Overlay pulls", Object.entries(s.sync ?? {}).map(([k, v]) => `${k} ${ago(v)}`).join(" · ") || "—"), + ); }, }; diff --git a/tests/test_sbbuilder.py b/tests/test_sbbuilder.py index 5c5438e..f71e0f9 100644 --- a/tests/test_sbbuilder.py +++ b/tests/test_sbbuilder.py @@ -17,11 +17,13 @@ class Fail(unittest.TestCase): class Ok(unittest.TestCase): - def test_parses_jobs(self): - out = json.dumps({"jobs": [{"id": "20261109-101010-build", "status": "running"}]}) + def test_parses_stats_and_jobs(self): + out = (json.dumps({"cpu": 5, "packages": {"total": 100}}) + + json.dumps({"jobs": [{"id": "20261109-101010-build", "status": "running"}]})) st = sbb.make({"sbbuilder": {"host": "buildsystem"}, "run": lambda cmd, timeout=5: out}).state() self.assertFalse(st["offline"]) + self.assertEqual(st["stats"]["packages"]["total"], 100) self.assertEqual(st["jobs"][0]["status"], "running") |
