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