swarm.js: restore the queue-summary banner on GET /api/jobq/rollup

hyperhive#3033 (jobq rollup endpoint) merged, unblocking hyperhive#3036.

The banner (removed on PR#3031 rather than ship it on an interim
GET /api/jobq/graph client-side derivation) is back, now reading
GET /api/jobq/rollup — hive-jobq-wire::state_rollup's pre-tallied
Vec<StateCount>, not the full graph. running sums the Running and
Finishing entries' roots (Finishing = own work done, subtree still
going); queued reads the Pending entry's roots. roots specifically,
matching the banner's established "N whole operations" meaning, not
the endpoint's parallel nodes count (~7 nodes per rebuild, 1 root).

Re-adds the rebuild_queue_changed SSE subscription dropped alongside
the banner, wired as a payload-less refetch trigger — confirmed with
atlas on the DagView-deletion tracker that this is the intended final
shape (keep the event, drop the payload) rather than deleting it and
falling back to polling.

Verified the running/queued derivation against constructed
StateCount[] fixtures (running+queued mix, Finishing-counts-as-running,
settled states never contribute, multi-state sums) before touching
swarm.js — 6/6 checks passed. npm run build clean, tracker-tag +
comment-block pre-push lints clean. docs/web-ui/dashboard.md updated
to describe the restored banner + the two prior shapes it went
through.
This commit is contained in:
iris 2026-08-03 21:07:53 +02:00
commit 4ec1c61d52
4 changed files with 124 additions and 69 deletions

View file

@ -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;

View file

@ -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
// <hive-jobq-graph>.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'));

View file

@ -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 <hive-jobq-graph>.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) => {