From 88bc07fbbe0f73295ac4feefd7b4b6d20137c1b5 Mon Sep 17 00:00:00 2001 From: iris Date: Mon, 25 May 2026 01:16:10 +0200 Subject: [PATCH] dashboard: surface in-flight rebuild on container card (#398) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The SW4RM tab's container card was reading container state straight from the snapshot — when a rebuild was in flight and the container was momentarily stopped between teardown and bring-up, the card showed "stopped" while the SYST3M tab's rebuild queue showed the operation running. The two surfaces disagreed. Mara: *should show as building in container page as well*. Cross-reference: build a `inFlightOpsByAgent()` map from `rebuildQueueState` (kinds: rebuild / meta_update / destroy; states: queued / running — skip `spawn` since transients already drive that case). When rendering each container row, prefer the operator-initiated transient if set; otherwise fall back to the in-flight queue entry as a synthetic pending kind: `rebuilding` / `meta-updating` / `destroying` (or `… queued` when still waiting on the worker). The existing `pending-state` spinner badge surfaces it visually — no new CSS rule needed. Also wire `applyRebuildQueueChanged` to re-render containers so the badge lights up the moment a rebuild lands in the queue and clears the moment it finishes — no manual refresh. --- frontend/packages/dashboard/src/app.js | 55 ++++++++++++++++++++++++-- 1 file changed, 51 insertions(+), 4 deletions(-) diff --git a/frontend/packages/dashboard/src/app.js b/frontend/packages/dashboard/src/app.js index d2d8d93..88f056e 100644 --- a/frontend/packages/dashboard/src/app.js +++ b/frontend/packages/dashboard/src/app.js @@ -535,6 +535,34 @@ window.marked = marked; function applyRebuildQueueChanged(ev) { rebuildQueueState = (ev.queue || []).slice(); renderRebuildQueueFromState(); + // Container cards surface in-flight rebuild / meta-update ops as + // a "building..." badge (#398) — re-render the SW4RM tab so + // newly-queued / newly-running ops light up the right card, + // and finished ops fall back to the regular state badges. + renderContainersFromState(); + } + // Map from agent name → highest-priority in-flight queue entry + // (`running` beats `queued`). Used by the container row renderer + // to surface "building..." / "meta-updating..." badges on the + // SW4RM tab when an op is still in the rebuild queue but no + // operator-initiated transient is set (#398). + function inFlightOpsByAgent() { + const out = new Map(); + for (const e of rebuildQueueState) { + if (e.state !== 'queued' && e.state !== 'running') continue; + // spawn ops target an agent that doesn't exist yet as a + // container — the transient store already drives the + // pending row for that case. Skip here to avoid double- + // surfacing if the spawn op happens to land in the queue + // while the row exists transiently. + if (e.kind === 'spawn') continue; + const cur = out.get(e.agent); + // Prefer running over queued; otherwise keep the first match. + if (!cur || (cur.state === 'queued' && e.state === 'running')) { + out.set(e.agent, e); + } + } + return out; } function renderRebuildQueueFromState() { renderRebuildQueue({ rebuild_queue: rebuildQueueState }); @@ -729,13 +757,32 @@ window.marked = marked; const hostname = (s && s.hostname) || window.location.hostname; const ul = el('ul', { class: 'containers' }); const tree = buildAgentTree(containers); + // In-flight rebuild / meta-update / destroy ops per agent name. + // Surface them as "building..." style badges on the container + // card when no operator-initiated transient already covers the + // row (#398). Mara: the SW4RM tab showed an agent as stopped + // while SYST3M showed an active rebuild; cross-reference fixes + // that. + const inFlight = inFlightOpsByAgent(); for (const node of tree) { const c = node.container; const url = `http://${hostname}:${c.port}/`; - // Pending state is overlaid from the transient store, not from - // the container row — `ContainerStateChanged` doesn't carry it, - // `TransientSet` / `TransientCleared` do. - const pending = transientsState.get(c.name)?.kind || null; + // Pending state is overlaid from the transient store first + // (operator-initiated spawn/destroy/rebuild — covers the + // create+start window where the container literally isn't up + // yet), then from the rebuild_queue (#398 — covers in-flight + // ops the worker is running even if no transient was set). + // `ContainerStateChanged` doesn't carry either signal. + const transientKind = transientsState.get(c.name)?.kind || null; + const op = !transientKind ? inFlight.get(c.name) : null; + const pending = transientKind + || (op && (op.state === 'running' + ? (op.kind === 'meta_update' ? 'meta-updating' + : op.kind === 'destroy' ? 'destroying' + : 'rebuilding') + : (op.kind === 'meta_update' ? 'meta-update queued' + : op.kind === 'destroy' ? 'destroy queued' + : 'rebuild queued'))); const li = el('li', { class: 'container-row' + (pending ? ' pending' : '') }); // Topology: depth contributes left-padding; the glyph string in // the .tree-prefix span draws the ├─ / └─ joint + continuation