diff --git a/docs/web-ui/dashboard.md b/docs/web-ui/dashboard.md index b6c86cec..29ee1cfc 100644 --- a/docs/web-ui/dashboard.md +++ b/docs/web-ui/dashboard.md @@ -74,13 +74,17 @@ agent is stale. Passive / rare-interaction state, reached from the **Core** tile on the H0M3 hub (served at `/core.html`), with the same minimal chrome as `/builds.html`: a `← home` -back-link + a `createTabStrip` sub-tab nav (**K3PT ST4T3** default, -then **C0NT41N3R L04D**). The page is its own esbuild bundle (`core.js`) -that cold-loads `/api/state` and subscribes to `/api/dashboard/stream` for -`tombstones_changed`, `capabilities_changed`, and `tool_groups_changed` -(the latter two re-render the stale-perms sub-section when permission -data changes). (Rebuild queue and meta inputs live on the BU1LDS -page — see below.) +back-link and a single pane, **K3PT ST4T3**. The page is its own esbuild +bundle (`core.js`) that cold-loads `/api/state` and subscribes to +`/api/dashboard/stream` for `tombstones_changed`, `capabilities_changed`, +and `tool_groups_changed` (the latter two re-render the stale-perms +sub-section when permission data changes). (Rebuild queue and meta inputs +live on the BU1LDS page — see below.) + +Per-container CPU, memory and disk load is not shown in the hive UI. The +same figures are exported as OTEL metrics and graphed in Grafana (see +[observability](../scheduler/observability.md)). An agent's CPU and memory +caps are set per agent in the swarm UI. **K3PT ST4T3** — two sub-sections on one pane: @@ -102,32 +106,11 @@ snapshot-storage rework where the problem changes shape. *Stale permission entries*: agents with explicit capability or tool-group JSON entries but no live container — typically renamed or deleted agents whose entries persisted in `capabilities.json` / -`tool-groups.json`. Lazy-loaded on first K3PT ST4T3 tab activation; +`tool-groups.json`. Loaded when the page opens; autorefreshes on `capabilities_changed` + `tool_groups_changed` SSE events. Each ghost agent gets a `✕ clear perms` button (`DELETE /api/permissions/{name}`). -**C0NT41N3R L04D** — live CPU + memory per agent container, read -straight from cgroup v2 on the host (`cpu.stat`, `memory.current`, -`memory.peak`, `memory.max` under -`/sys/fs/cgroup/machine.slice/machine-h\x2d.scope/`). CPU is a -host-normalised percentage (0..100 across all cores) sampled over a -short (~200 ms) two-read interval; memory shows current + peak with a -bar against the `memory.max` quota. Backed by -`GET /api/container-resources` (`container_stats.rs`), which reads the -files read-only (world-readable; no `hive-priv`) and skips agents whose -scope dir is absent (= not running). Pull-only: `core.js` polls every -5 s **only while the C0NT41N3R L04D sub-tab is active** (CPU needs a fresh -sample each refresh), and stops on sub-tab change. Disk size (`disk_bytes`) -rides the same row, but a separate background sampler feeds it every -30 min: `hive-priv` runs one bounded `du -x` as root over the agent's -`state/` + `work/` + container writable rootfs (shared nix store excluded), -so the 5 s poll stays cheap cgroup-only reads. The row carries the last -measured value; the column shows "unknown" (`null`) before the first -measurement and whenever the last one failed, never 0. Network isn't tracked -per container — see `docs/networking/network.md` for the current -per-agent netns model. - ## BU1LDS page (`/builds.html`) The build lifecycle hub — rebuild queue, live build log, meta inputs, @@ -765,17 +748,6 @@ below — some endpoints aren't in it yet. zero-turn dbs); returns swarm totals, a busiest-first per-agent rollup, swarm model mix, and a labelled `est_cost_usd`. Window defaults to `24h`. -- `GET /api/container-resources` — live per-agent-container CPU + - memory from cgroup v2 (C0R3 › C0NT41N3R L04D panel). Returns one - row per running agent (`name`, `cpu_pct`, `mem_current_bytes`, - `mem_peak_bytes`, `mem_max_bytes`, `disk_bytes`); samples CPU over - ~200 ms so the call briefly awaits. Skips non-running agents (no scope - dir). No network field — agents share the host netns. `disk_bytes` is - the last value from a **separate slow sampler** (not this hot path): - every 30 min `hive-priv` runs a bounded `du -sxb` as root over the agent's - `state/` + `work/` + container writable rootfs, `-x` excluding the shared - read-only nix store (`PrivRequest::MeasureAgentDisk`). `null` means - unknown: not measured yet, or the last measurement failed. - `POST /api/cancel-reminder/{id}` — hard-delete a pending reminder. - `POST /api/retry-reminder/{id}` — re-arm a reminder whose delivery failed (clears the failure state so the scheduler retries). diff --git a/frontend/packages/dashboard/src/common.css b/frontend/packages/dashboard/src/common.css index 22b9f871..5d5d57ec 100644 --- a/frontend/packages/dashboard/src/common.css +++ b/frontend/packages/dashboard/src/common.css @@ -619,8 +619,8 @@ hive-side-panel .md td { /* ─── stats table ───────────────────────────────────────────────── Shared right-aligned numeric table. Used by /stats.html (busiest - agents) and the dashboard SYST3M › C0NT41N3R L04D table — moved here - from dashboard.css when ST4TS became its own page. */ + agents) — moved here from dashboard.css when ST4TS became its own + page. */ .hive-stats-table { width: 100%; border-collapse: collapse; diff --git a/frontend/packages/dashboard/src/core.css b/frontend/packages/dashboard/src/core.css index cb44415e..be613a1d 100644 --- a/frontend/packages/dashboard/src/core.css +++ b/frontend/packages/dashboard/src/core.css @@ -1,6 +1,7 @@ /* ─── /core.html — C0R3 page ─────────────────────────────────────── - Kept state (tombstones) and container load. Section rules imported - from system-sections.css (shared with dashboard.css + builds.css). */ + Kept state (tombstones) and stale permission entries. Section rules + imported from system-sections.css (shared with dashboard.css + + builds.css). */ @import "./system-sections.css"; body.core-shell { @@ -8,22 +9,10 @@ body.core-shell { padding: 0; } -/* Fill the header row next to the ← home back-link, same as the - builds sub-tab strip. */ -.core-tabbar { - flex: 1; -} - .core-main { padding: 1.2em 1.5em 2em; } -/* createTabStrip toggles the native `hidden` attribute on inactive - panes; ensure it wins over any inherited display. */ -.core-pane[hidden] { - display: none; -} - /* ─── K3PT ST4T3 container cards ──────────────────────────────────── core.html doesn't load dashboard.css (that's the operator SPA), so the .container-row card styles aren't inherited. Redefine them diff --git a/frontend/packages/dashboard/src/core.html b/frontend/packages/dashboard/src/core.html index 37210ac8..8728f020 100644 --- a/frontend/packages/dashboard/src/core.html +++ b/frontend/packages/dashboard/src/core.html @@ -13,70 +13,31 @@
- -
-

- kept state from previously tombstoned agents — recreating an agent - with the same name reuses it. -

-
-

loading…

-
-
-
- - -
-

- live cpu + memory per agent container, from cgroup v2 on the host. cpu - is % of total host capacity (all cores), sampled over ~200ms each - refresh; polled every 5s while this tab is open. network is omitted on - purpose — agents share the host netns, so there is no per-container - counter (see docs/networking/network.md). -

-
-

loading…

-
-
+ +

+ kept state from previously tombstoned agents — recreating an agent with + the same name reuses it. +

+
+

loading…

+
+
diff --git a/frontend/packages/dashboard/src/core.js b/frontend/packages/dashboard/src/core.js index b4be8b03..dec4a397 100644 --- a/frontend/packages/dashboard/src/core.js +++ b/frontend/packages/dashboard/src/core.js @@ -1,27 +1,20 @@ // C0R3 page entry (/core.html). The host/coordinator surface, its own -// standalone page so the dashboard tab strip stays lean. Two sub-tabs -// via (default = K3PT ST4T3): kept state (tombstones) -// and container load. +// standalone page so the dashboard tab strip stays lean: kept state +// (tombstones) and the stale permission entries. // -// Rebuild queue + meta inputs live at /builds.html (the build lifecycle -// hub), not here. This page is its own esbuild bundle with its own -// runtime: it cold-loads /api/state and subscribes to /dashboard/stream -// (the same broker event channel the dashboard uses), maintaining its -// own copy of the tombstones state. +// This page is its own esbuild bundle with its own runtime: it cold-loads +// /api/state and subscribes to /dashboard/stream (the same broker event +// channel the dashboard uses), maintaining its own copy of the tombstones +// state. import { $, form, openStream, initServerWarnings } from "./common.js"; import { el } from "@hive/shared/dom.js"; import { asyncBtn, bindAsyncForms } from "@hive/shared/forms.js"; -import "@hive/shared/hive-tab-strip.js"; // ─── derived state (own copies; this bundle has its own runtime) ────────── let tombstonesState = []; -// ContainerView list from the last /api/state snapshot — used by renderContainerLoad -// to splice in the configured cpu_quota/memory_max limits alongside live cgroup data. -let containersState = []; function syncFromSnapshot(s) { tombstonesState = (s.tombstones || []).slice(); - containersState = (s.containers || []).slice(); } // ─── kept state (tombstones) ────────────────────────────────────────────── @@ -187,273 +180,6 @@ async function fetchAndRenderStalePerms() { stalePermsLoaded = true; } -// ─── container load (live cgroup poll while the LOAD sub-tab is open) ────── -let containerLoadTimer = null; - -function cloadFmtBytes(n) { - if (!Number.isFinite(n) || n <= 0) return "0"; - const u = ["B", "KiB", "MiB", "GiB", "TiB"]; - let i = 0; - let v = n; - while (v >= 1024 && i < u.length - 1) { - v /= 1024; - i += 1; - } - return v.toFixed(v < 10 && i > 0 ? 1 : 0) + " " + u[i]; -} -function cloadMeter(pct) { - const p = Math.max(0, Math.min(100, pct)); - const cls = p >= 90 ? "hot" : p >= 70 ? "warn" : ""; - const m = document.createElement("span"); - m.className = "cload-meter"; - m.title = p.toFixed(0) + "%"; - const fill = document.createElement("span"); - fill.className = "fill" + (cls ? " " + cls : ""); - fill.style.width = p + "%"; - m.append(fill); - return m; -} - -// Stash the last load rows so re-renders triggered by SSE state updates can -// call renderContainerLoad(lastLoadRows) without waiting for the next poll. -let lastLoadRows = []; - -function renderContainerLoad(rows) { - lastLoadRows = rows; - const root = $("container-load-section"); - if (!root) return; - if (!Array.isArray(rows) || rows.length === 0) { - root.replaceChildren(); - const p = document.createElement("p"); - p.className = "meta"; - p.textContent = "no running agent containers"; - root.append(p); - return; - } - - // Build a name→ContainerView lookup so we can splice in the configured - // cpu_quota / memory_max limits (set in meta/resource-limits.json and - // resolved by the server) next to the live cgroup readings. - const cvByName = new Map(containersState.map((c) => [c.name, c])); - - const table = document.createElement("table"); - table.className = "hive-stats-table"; - // "cpu cap" / "mem cap" show the configured ceilings from ContainerView - // (effective drop-in values, take effect on next start/restart). - // "limit" remains the live cgroup memory ceiling from /api/container-resources. - table.innerHTML = - "agentcpumemory" + - "peaklimitdisk" + - 'cpu cap' + - 'mem cap' + - ""; - const tb = document.createElement("tbody"); - for (const r of rows) { - const cv = cvByName.get(r.name); - const tr = document.createElement("tr"); - const name = document.createElement("td"); - name.textContent = r.name; - tr.append(name); - const cpu = document.createElement("td"); - cpu.className = "num"; - cpu.append( - (Number(r.cpu_pct) || 0).toFixed(1) + "%", - cloadMeter(Number(r.cpu_pct) || 0), - ); - tr.append(cpu); - const mem = document.createElement("td"); - mem.className = "num"; - const memCur = Number(r.mem_current_bytes) || 0; - if (r.mem_max_bytes) { - mem.append( - cloadFmtBytes(memCur), - cloadMeter((100 * memCur) / r.mem_max_bytes), - ); - } else { - mem.textContent = cloadFmtBytes(memCur); - } - tr.append(mem); - const peak = document.createElement("td"); - peak.className = "num"; - peak.textContent = r.mem_peak_bytes - ? cloadFmtBytes(Number(r.mem_peak_bytes)) - : "—"; - tr.append(peak); - const lim = document.createElement("td"); - lim.className = "num"; - lim.textContent = r.mem_max_bytes - ? cloadFmtBytes(Number(r.mem_max_bytes)) - : "∞"; - tr.append(lim); - // Disk: on-disk footprint (state/ + work/ + container writable rootfs, shared - // nix store excluded). Measured out-of-band every 30 min server-side; - // null means unknown (not measured yet, or the measurement failed). - const disk = document.createElement("td"); - disk.className = "num"; - disk.title = - "state/ + work/ + container writable rootfs (shared nix store excluded); measured every 30 min"; - disk.textContent = - r.disk_bytes != null ? cloadFmtBytes(Number(r.disk_bytes)) : "unknown"; - tr.append(disk); - - // Configured CPU / memory caps from ContainerView (resolved effective - // values: per-agent override when set, hive-wide default otherwise). - const cpuCap = document.createElement("td"); - cpuCap.className = "num cload-cap"; - cpuCap.title = "configured ceiling — takes effect on next start"; - cpuCap.textContent = cv?.cpu_quota || "—"; - tr.append(cpuCap); - const memCap = document.createElement("td"); - memCap.className = "num cload-cap"; - memCap.title = "configured ceiling — takes effect on next start"; - memCap.textContent = cv?.memory_max || "—"; - tr.append(memCap); - - // S3T button toggles the inline edit row for this agent. - const actTd = document.createElement("td"); - const setBtn = document.createElement("button"); - setBtn.type = "button"; - setBtn.className = "btn btn-sm cload-set-btn"; - setBtn.textContent = "S3T"; - setBtn.title = "set CPU / memory cap for " + r.name; - tr.append(actTd); - actTd.append(setBtn); - tb.append(tr); - - // Inline edit row (hidden by default, toggled by the S3T button). - const editRow = document.createElement("tr"); - editRow.className = "cload-edit-row"; - editRow.hidden = true; - const editTd = document.createElement("td"); - editTd.colSpan = 9; - editTd.className = "cload-edit-cell"; - - const editForm = document.createElement("form"); - editForm.className = "cload-edit-form"; - editForm.addEventListener("submit", async (e) => { - e.preventDefault(); - const cpuInput = editForm.querySelector(".cload-cpu-input"); - const memInput = editForm.querySelector(".cload-mem-input"); - const errSpan = editForm.querySelector(".cload-edit-err"); - const submitBtn = editForm.querySelector('[type="submit"]'); - errSpan.hidden = true; - submitBtn.disabled = true; - try { - const body = new URLSearchParams({ - cpu_quota: cpuInput.value.trim(), - memory_max: memInput.value.trim(), - }); - const resp = await fetch( - "/api/resource-limits/" + encodeURIComponent(r.name), - { method: "POST", body }, - ); - if (!resp.ok) { - errSpan.textContent = await resp - .text() - .catch(() => "error " + resp.status); - errSpan.hidden = false; - return; - } - // Success: collapse the edit row. The SSE rescan will push updated - // ContainerView data (cpu_quota/memory_max) to containersState, - // triggering a re-render of the cap columns via refreshContainerLoad. - editRow.hidden = true; - setBtn.textContent = "S3T"; - } catch (err) { - errSpan.textContent = String(err); - errSpan.hidden = false; - } finally { - submitBtn.disabled = false; - } - }); - - const cpuLabel = document.createElement("label"); - cpuLabel.className = "cload-edit-label"; - cpuLabel.textContent = "cpu quota"; - const cpuInput = document.createElement("input"); - cpuInput.type = "text"; - cpuInput.className = "cload-cpu-input"; - // Pre-fill with the current *effective* value (not just a placeholder) - // so submitting after editing only the other field round-trips this one - // unchanged. The form always posts both fields, and the server treats - // an empty field as "clear this override" — a placeholder-only hint - // reads as filled to the eye but posts as empty, silently resetting - // whichever field the user didn't touch. - cpuInput.value = cv ? cv.cpu_quota : ""; - cpuInput.placeholder = "e.g. 200%"; - cpuInput.title = - 'systemd CPUQuota= value (e.g. "400%"). empty = use hive default'; - cpuLabel.append(cpuInput); - - const memLabel = document.createElement("label"); - memLabel.className = "cload-edit-label"; - memLabel.textContent = "mem max"; - const memInput = document.createElement("input"); - memInput.type = "text"; - memInput.className = "cload-mem-input"; - // Same reasoning as cpuInput above. - memInput.value = cv ? cv.memory_max : ""; - memInput.placeholder = "e.g. 8G"; - memInput.title = - 'systemd MemoryMax= value (e.g. "8G", "50%", "infinity"). empty = use hive default'; - memLabel.append(memInput); - - const submitBtn = document.createElement("button"); - submitBtn.type = "submit"; - submitBtn.className = "btn btn-restart cload-save-btn"; - submitBtn.textContent = "S4V3"; - - const hintSpan = document.createElement("span"); - hintSpan.className = "meta cload-edit-hint"; - hintSpan.textContent = "↺ restart to apply to a running container"; - - const errSpan = document.createElement("span"); - errSpan.className = "cload-edit-err"; - errSpan.hidden = true; - - editForm.append(cpuLabel, memLabel, submitBtn, hintSpan, errSpan); - editTd.append(editForm); - editRow.append(editTd); - tb.append(editRow); - - setBtn.addEventListener("click", () => { - const open = !editRow.hidden; - editRow.hidden = open; - setBtn.textContent = open ? "S3T" : "✕"; - }); - } - table.append(tb); - root.replaceChildren(table); -} - -async function refreshContainerLoad() { - try { - const resp = await fetch("/api/container-resources"); - if (!resp.ok) throw new Error("http " + resp.status); - renderContainerLoad(await resp.json()); - } catch (e) { - const root = $("container-load-section"); - if (root) { - root.replaceChildren(); - const p = document.createElement("p"); - p.className = "meta"; - p.textContent = "container load fetch failed: " + e; - root.append(p); - } - } -} -function startContainerLoadPolling() { - refreshContainerLoad(); - if (containerLoadTimer) return; - containerLoadTimer = setInterval(refreshContainerLoad, 5000); -} -function stopContainerLoadPolling() { - if (containerLoadTimer) { - clearInterval(containerLoadTimer); - containerLoadTimer = null; - } -} - // ─── render-all (cold load + any full re-render) ────────────────────────── function renderAll() { renderTombstones({ tombstones: tombstonesState }); @@ -465,19 +191,6 @@ const SSE_HANDLERS = { tombstonesState = (ev.tombstones || []).slice(); renderTombstones({ tombstones: tombstonesState }); }, - // When a container's state changes (e.g. after resource-limits update - // triggers rescan_containers_and_emit), update containersState in place - // so the cap columns in the LOAD table reflect the new configured values. - container_state_changed(ev) { - if (!ev.container) return; - const idx = containersState.findIndex((c) => c.name === ev.container.name); - if (idx >= 0) { - containersState[idx] = ev.container; - } else { - containersState.push(ev.container); - } - if (lastLoadRows.length) renderContainerLoad(lastLoadRows); - }, // Refresh the stale-perms sub-section when perm data changes (a ghost // was cleared, or perms were saved for an agent whose name collides). capabilities_changed(_ev) { @@ -511,33 +224,16 @@ async function init() { // to the bare `ok` response page. bindAsyncForms(() => refreshState()); - // Hash-routed sub-tab strip; default K3PT ST4T3. Container-load - // polling runs only while the LOAD sub-tab is open (cpu is a short - // two-sample read each refresh on the server). - document.getElementById("core-tabbar").configure({ - tabs: [ - { id: "kept", label: "K3PT ST4T3" }, - { id: "load", label: "C0NT41N3R L04D" }, - ], - defaultId: "kept", - onShow: (id) => { - if (id === "load") startContainerLoadPolling(); - else stopContainerLoadPolling(); - // Lazy-load stale-perms on first K3PT ST4T3 activation; always - // re-fetch on subsequent visits in case perms changed. - if (id === "kept") fetchAndRenderStalePerms(); - }, - }); - await refreshState(); + fetchAndRenderStalePerms(); // kinds= matches SSE_HANDLERS below verbatim — narrows this from all - // 13 wire kinds down to the 4 this page acts on. This page was one + // 13 wire kinds down to the 3 this page acts on. This page was one // of 4 unfiltered `/api/dashboard/stream` subscribers before this // (subscription discipline, part 1 of the dashboard-event-stream- // split issue). const es = openStream( - "/api/dashboard/stream?kinds=tombstones_changed,container_state_changed," + + "/api/dashboard/stream?kinds=tombstones_changed," + "capabilities_changed,tool_groups_changed", ); if (es) { diff --git a/frontend/packages/dashboard/src/dashboard.css b/frontend/packages/dashboard/src/dashboard.css index a02618b6..897dc467 100644 --- a/frontend/packages/dashboard/src/dashboard.css +++ b/frontend/packages/dashboard/src/dashboard.css @@ -2,7 +2,7 @@ it defines the CSS custom properties, typography, and shared component rules that this file depends on. */ -/* SYST3M panel rules (rebuild queue / meta inputs / container load) +/* SYST3M panel rules (rebuild queue / meta inputs / kept state) live in system-sections.css, shared with /core.html. A few are still referenced from the dashboard (`.rebuild-queue`), so the dashboard pulls them in here. */ @@ -771,6 +771,4 @@ body.dashboard-shell.has-selection { /* ST4TS moved to its own page (`/stats.html`): the window selector / summary chips / bars live in stats.css, and the - shared `.hive-stats-table` moved to common.css (the SYST3M › - C0NT41N3R L04D table in system-sections.css / on /core.html still - uses it). */ + shared `.hive-stats-table` moved to common.css. */ diff --git a/frontend/packages/dashboard/src/dashboard.html b/frontend/packages/dashboard/src/dashboard.html index 3445d881..2af668cc 100644 --- a/frontend/packages/dashboard/src/dashboard.html +++ b/frontend/packages/dashboard/src/dashboard.html @@ -103,7 +103,7 @@