diff --git a/docs/web-ui/dashboard.md b/docs/web-ui/dashboard.md index 075401cd..a71ff7af 100644 --- a/docs/web-ui/dashboard.md +++ b/docs/web-ui/dashboard.md @@ -209,12 +209,12 @@ Three sub-tabs: **R3BU1LD QU3U3** (default), **M3T4 1NPUTS**, **BUILD L0GS**. Its own esbuild bundle (`builds.js`); cold-loads `/api/state` and subscribes to `/api/dashboard/stream` for `rebuild_queue_changed`, `meta_inputs_changed`, `meta_update_running`. -The SW4RM tab (`/dashboard.html`) does **not** read this endpoint (or -any job-queue state) at all — mara, on review: "swarm.js should not -need to pull in the jobq to do its job." Its per-agent pending badges -are transient-only, and its queue-summary banner is removed for now -(see Container row, below), pending a dedicated rollup endpoint it -can read directly instead of the full graph. +The SW4RM tab (`/dashboard.html`) does **not** read this endpoint at +all — mara, on review: "swarm.js should not need to pull in the jobq +to do its job." Its per-agent pending badges are transient-only, and +its queue-summary banner reads the much narrower `GET /api/jobq/rollup` +instead (see Container row, below) — a handful of pre-tallied counts, +not the graph. **R3BU1LD QU3U3** — pending, in-flight, and recently-settled container operations: rebuilds, meta-update cascades, and first-spawns. One @@ -915,16 +915,15 @@ fetch entirely. their own label directly via `TransientSet`/`TransientCleared` events carrying no backing node at all. - **Queued (not-yet-started) work shows nothing on the card, and - `swarm.js` reads no job-queue state at all.** An earlier version of - this page had a job-queue-backed fallback badge for the `Pending` - case (`queuedOpsByAgent()`, reading a `GET /api/jobq/graph` fetch); - removed per mara, on review of hyperhive#3028's PR: *"swarm.js - should not need to pull in the jobq to do its job,"* followed by - *"remove the per agent pending stuff - only show what is running."* - The queue-summary banner below was, for the same reason, also pulled - rather than kept on that same client-side graph-fetch — see its own - entry below for why. + **Queued (not-yet-started) work shows nothing on the card.** An + earlier version of this page had a job-queue-backed fallback badge + for the `Pending` case (`queuedOpsByAgent()`, reading a `GET + /api/jobq/graph` fetch); removed per mara, on review of + hyperhive#3028's PR: *"swarm.js should not need to pull in the jobq + to do its job,"* followed by *"remove the per agent pending stuff - + only show what is running."* Per-agent badges never came back — only + the queue-summary banner below did, once a narrow enough endpoint + existed for it to read instead of the full graph. An **active model badge** (`model · `, blue) appears when the container is running and the harness has persisted a model name (`harness/hyperhive-model`). Read by hive-c0re's `ContainerView` @@ -965,23 +964,28 @@ per-agent actions and navigation links. Contents: agent is stale. Banner pulses on each broker SSE event (`pulseBanner` with a 4s grace timer). -**Build-queue summary banner — currently removed, coming back on a -different data source.** Previously a compact amber banner above the -container list (`◐ build queue — N running · M queued — view -queue →`, linking to the BU1LDS page), derived client-side from `GET -/api/jobq/graph` (filter to `parent: null` group roots — each an -independent operation, not a raw node count — bucket by `state`). -Pulled per mara, on review of hyperhive#3028's PR: *"dont replace one -legacy thing with another. then we will have to either wait with -this pr or split it into what can and cannot be done now"* — a -client-side derivation over the generic graph was itself judged a -stopgap not worth landing, the same way the old `DagView` read it -replaced was. Comes back once a dedicated rollup endpoint (status × -count pairs, tracked separately) exists for `swarm.js` to read -directly instead of pulling the whole graph in for a summary. Until -then, the actual running work per agent is still visible on each -card via the transient pills; only the hive-wide at-a-glance count is -missing. +**Build-queue summary banner** — when the job queue has any active +work, a compact amber banner sits above the container list: `◐ build +queue — N running · M queued — view queue →` (the link goes to the +BU1LDS page's R3BU1LD QU3U3). Reads `GET /api/jobq/rollup` +(`hive-jobq-wire::state_rollup`, `jobqRollupState` in `swarm.js`) — +`Vec<{ state, nodes, roots }>`, every lifecycle state present in a +fixed order, zero counts included — rather than the full +`/api/jobq/graph` tree: `running` sums the `Running` and `Finishing` +entries' `roots` (`Finishing` = own work done, subtree still going, +still in flight), `queued` reads the `Pending` entry's `roots`. +`roots` specifically, not `nodes` — the banner has always meant *N +whole operations*, not raw steps (one rebuild is ~7 nodes but 1 +root); `nodes` exists on the same endpoint for a consumer that wants +step-level counts instead, unused here. + +This banner went through two prior shapes before landing here, both +per mara review comments on hyperhive#3028's PR: a client-side +derivation over the full graph (*"swarm.js should not need to pull in +the jobq to do its job"*), then removed entirely rather than keep +that interim fetch (*"dont replace one legacy thing with another"*). +Restored once the dedicated rollup endpoint (hyperhive#3033) existed +for it to read directly instead. ### Themed dialogs diff --git a/frontend/packages/dashboard/src/dashboard.css b/frontend/packages/dashboard/src/dashboard.css index 843b8b42..251652fe 100644 --- a/frontend/packages/dashboard/src/dashboard.css +++ b/frontend/packages/dashboard/src/dashboard.css @@ -458,11 +458,7 @@ hive-agent-menu { /* Build-queue summary banner on the SW4RM tab: one compact line when the rebuild queue has active work, with a link to the full queue on - the C0R3 page. Amber to match the in-progress / "rebuilding" card tint. - Currently unused — swarm.js dropped the banner pending a dedicated - rollup endpoint (see swarm.js's transients-section comment) — kept - here rather than deleted-then-restored, since the markup/classes are - expected to come back unchanged once that endpoint exists. */ + the C0R3 page. Amber to match the in-progress / "rebuilding" card tint. */ .queue-summary { display: flex; align-items: center; diff --git a/frontend/packages/dashboard/src/swarm.js b/frontend/packages/dashboard/src/swarm.js index c86a358c..7e70bcbc 100644 --- a/frontend/packages/dashboard/src/swarm.js +++ b/frontend/packages/dashboard/src/swarm.js @@ -1,7 +1,8 @@ // SW4RM (containers) domain — extracted from tabs.js. // Agent topology, container-row rendering, selection bar, peer-hives block, -// and all live-update apply handlers for container-state and transient ops. -// See docs/web-ui.md::Container row for the rendering contract. +// and all live-update apply handlers for container-state, transient, and +// job-queue-rollup ops. See docs/web-ui.md::Container row for the +// rendering contract. import { $, form, fmtAgeSecs, @@ -50,20 +51,49 @@ const transientsState = new Map(); // tab-gated visibility). const selectionState = new Set(); -// ─── transients ───────────────────────────────────────────────────────────── +// ─── job queue rollup (queue-summary banner only) ────────────────────────── // -// This is now the ONLY job-queue-derived state on this page — mara, on -// review of the DagView migration: "swarm.js should not need to pull in -// the jobq to do its job." An earlier version of this file also fetched -// GET /api/jobq/graph directly for two things now gone: a per-agent -// queued-but-not-running badge (`queuedOpsByAgent()`, removed per "remove -// the per agent pending stuff - only show what is running" — a card's -// pending badges are transients-only now, which already means "what is -// running") and the queue-summary banner (removed per "dont replace one -// legacy thing with another" — a client-side derivation over the generic -// graph was itself judged a stopgap not worth shipping; the banner comes -// back once the dedicated rollup endpoint exists, and reads that -// directly instead of pulling the graph in here at all). +// The ONE piece of job-queue-derived state on this page, and deliberately +// narrow — mara, on review of the DagView migration: "swarm.js should not +// need to pull in the jobq to do its job." An earlier version of this file +// fetched GET /api/jobq/graph (the full node tree) for two things: a +// per-agent queued-but-not-running badge (`queuedOpsByAgent()`, removed +// per "remove the per agent pending stuff - only show what is running" — +// a card's pending badges are transients-only now, which already means +// "what is running") and this banner, which was pulled entirely per "dont +// replace one legacy thing with another" (a client-side tally over the +// generic graph was itself judged a stopgap). Now that the dedicated +// rollup endpoint exists (hive-jobq-wire::state_rollup, served at +// GET /api/jobq/rollup), the banner reads *that* instead — a handful of +// pre-tallied counts, not the graph. +let jobqRollupState = []; + +// Fetches the rollup fresh and re-renders. Called on cold load (see +// tabs.js's refreshState) and whenever `rebuild_queue_changed` fires +// (applyRebuildQueueChanged below) — a payload-less push trigger by +// design, confirmed with atlas on the jobq-deletion tracker: the event +// carries no `queue` field this page reads, same "something changed, +// go refetch" treatment builds.js already gives it for +// .refresh(). +// Best-effort: a failed fetch leaves the previous snapshot in place +// rather than wiping the banner on a network blip. +export async function refreshJobqRollup() { + let counts; + try { + const r = await fetch('/api/jobq/rollup'); + if (!r.ok) return; + counts = await r.json(); + } catch { + return; + } + jobqRollupState = counts; + renderContainersFromState(); +} +export function applyRebuildQueueChanged() { + refreshJobqRollup(); +} + +// ─── transients ───────────────────────────────────────────────────────────── export function syncTransientsFromSnapshot(s) { transientsState.clear(); @@ -583,15 +613,31 @@ export function renderContainers(s) { )); } - // No queue-summary banner for now — the previous version derived it - // client-side from GET /api/jobq/graph, which mara flagged as - // replacing one legacy DagView-shaped hack with another rather than - // landing the real fix ("dont replace one legacy thing with another - // ... split it into what can and cannot be done now"). Comes back once - // the dedicated rollup endpoint exists, reading that directly. Until - // then the queue's actual state is still visible per-agent on each - // card via the transient pills; only the hive-wide at-a-glance summary - // is missing. + // Queue-summary banner: one compact line above the container list when + // the job queue has active work, linking to the full queue on the + // BU1LDS page. Reads GET /api/jobq/rollup's pre-tallied `roots` counts + // (see jobqRollupState above) rather than the full graph — `roots` + // because "N running / M queued" has always meant *operations*, not + // raw steps (one rebuild is ~7 nodes but 1 root); `nodes` exists on + // the same endpoint for a consumer that wants steps instead, unused + // here. `Finishing` counts as running (own work done, subtree still + // going, still in flight) — same treatment `roots` gets nowhere else, + // since the rollup endpoint doesn't collapse the two itself. + const byState = new Map(jobqRollupState.map((c) => [c.state, c])); + const running = (byState.get('Running')?.roots ?? 0) + + (byState.get('Finishing')?.roots ?? 0); + const queued = byState.get('Pending')?.roots ?? 0; + if (running || queued) { + const parts = []; + if (running) parts.push(`${running} running`); + if (queued) parts.push(`${queued} queued`); + root.append(el('div', { class: 'queue-summary' }, + el('span', { class: 'glyph spinner' }, '◐'), ' ', + el('strong', {}, 'build queue'), ' — ', + parts.join(' · '), ' ', + el('a', { class: 'queue-summary-link', href: '/builds.html' }, 'view queue →'), + )); + } if (!containers.length && !transientsState.size) { root.append(el('p', { class: 'empty' }, 'no managed containers')); diff --git a/frontend/packages/dashboard/src/tabs.js b/frontend/packages/dashboard/src/tabs.js index 40dda34e..b0056ca4 100644 --- a/frontend/packages/dashboard/src/tabs.js +++ b/frontend/packages/dashboard/src/tabs.js @@ -42,8 +42,8 @@ import { renderQuestions, activeQuestionCount, } from './call.js'; import { - syncTransientsFromSnapshot, - applyContainerStateChanged, applyContainerRemoved, + refreshJobqRollup, syncTransientsFromSnapshot, + applyRebuildQueueChanged, applyContainerStateChanged, applyContainerRemoved, applyTransientSet, applyTransientCleared, renderContainers, renderContainersFromState, renderSelectionBar, renderPeerHives, @@ -234,6 +234,14 @@ window.marked = marked; // `transientsState` + `containersState`, not from `s.*`). syncTransientsFromSnapshot(s); syncContainersFromSnapshot(s); + // Job-queue rollup feeds only the SW4RM queue-summary banner + // (per-agent card badges are transient-only — see swarm.js). + // Self-fetches GET /api/jobq/rollup — not read off `s` (this + // page's snapshot carries no jobq field) — fire-and-forget: + // renderContainers below runs off whatever jobqRollupState + // already holds, and refreshJobqRollup's own re-render catches + // up once the fetch resolves. + refreshJobqRollup(); renderContainers(s); // Sync the derived approvals + questions stores from the // snapshot, then render. Live `*_added` / `*_resolved` events @@ -304,10 +312,11 @@ window.marked = marked; container_removed: applyContainerRemoved, // tombstones_changed / meta_inputs_changed / meta_update_running are // handled on /core.html now (the SYST3M panels moved there). - // rebuild_queue_changed: this page no longer subscribes to it at - // all (see swarm.js's transients-section comment) — the SW4RM tab - // has nothing left that reads it, unlike /builds.html which still - // does (its own separate subscription). + // rebuild_queue_changed: refreshes the SW4RM queue-summary banner + // (see swarm.js) — a payload-less push trigger, same treatment + // /builds.html gives it for .refresh() (its own + // separate subscription). + rebuild_queue_changed: applyRebuildQueueChanged, schedules_changed: applySchedulesChanged, capabilities_changed: applyCapabilitiesChanged, tool_groups_changed: applyToolGroupsChanged, @@ -329,7 +338,7 @@ window.marked = marked; const es = openStream( '/api/dashboard/stream?kinds=sent,approval_added,approval_resolved,' + 'question_added,question_resolved,transient_set,transient_cleared,' + - 'container_state_changed,container_removed,' + + 'container_state_changed,container_removed,rebuild_queue_changed,' + 'schedules_changed,capabilities_changed,tool_groups_changed', ); es.onmessage = (e) => {