dashboard: surface in-flight rebuild on container card (#398)

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.
This commit is contained in:
iris 2026-05-25 01:16:10 +02:00
parent 7743c07380
commit 88bc07fbbe

View file

@ -535,6 +535,34 @@ window.marked = marked;
function applyRebuildQueueChanged(ev) { function applyRebuildQueueChanged(ev) {
rebuildQueueState = (ev.queue || []).slice(); rebuildQueueState = (ev.queue || []).slice();
renderRebuildQueueFromState(); 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() { function renderRebuildQueueFromState() {
renderRebuildQueue({ rebuild_queue: rebuildQueueState }); renderRebuildQueue({ rebuild_queue: rebuildQueueState });
@ -729,13 +757,32 @@ window.marked = marked;
const hostname = (s && s.hostname) || window.location.hostname; const hostname = (s && s.hostname) || window.location.hostname;
const ul = el('ul', { class: 'containers' }); const ul = el('ul', { class: 'containers' });
const tree = buildAgentTree(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) { for (const node of tree) {
const c = node.container; const c = node.container;
const url = `http://${hostname}:${c.port}/`; const url = `http://${hostname}:${c.port}/`;
// Pending state is overlaid from the transient store, not from // Pending state is overlaid from the transient store first
// the container row — `ContainerStateChanged` doesn't carry it, // (operator-initiated spawn/destroy/rebuild — covers the
// `TransientSet` / `TransientCleared` do. // create+start window where the container literally isn't up
const pending = transientsState.get(c.name)?.kind || null; // 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' : '') }); const li = el('li', { class: 'container-row' + (pending ? ' pending' : '') });
// Topology: depth contributes left-padding; the glyph string in // Topology: depth contributes left-padding; the glyph string in
// the .tree-prefix span draws the ├─ / └─ joint + continuation // the .tree-prefix span draws the ├─ / └─ joint + continuation