dashboard: split queued vs running rebuild visual (#769)

Container row's pending highlight fired for both queued + running
rebuild-queue entries, so a long queue painted half the SW4RM tab
amber. Mara on #769: don't highlight while queued, show running as
spinner on agent icon.

- tabs.js: derive opRunning (transient OR op.state === 'running')
  separately from pending; add a pending-running row class.
  Operator-initiated transients still count as running so the
  rebuild-now-clicked → backend-picks-it-up window stays visually
  consistent.
- dashboard.css: move the amber border + tint from .pending to
  .pending-running (queued rows now keep their normal look, with
  the badge text still saying "rebuild queued"). Add a 1s linear
  rotating amber arc as .pending-running > .container-icon::after,
  with overflow:visible so the ring sits just outside the icon and
  composes with the mauve selected ring.
- docs/web-ui.md::Container row: describe the queued vs running
  split in prose. Drop the orphaned (#398) cookie while I'm at it
  — small #712 freebie.
This commit is contained in:
iris 2026-05-31 14:06:30 +02:00 committed by mara
commit 272d8af620
3 changed files with 45 additions and 3 deletions

View file

@ -433,8 +433,14 @@ body sits to the right with three stacked lines
entry for this agent is `queued` or `running` but no
operator-initiated transient is set, the card surfaces a
`building…` / `meta-updating…` badge sourced from
`rebuildQueueState` (#398) — so the SW4RM tab shows the same
`rebuildQueueState` — so the SW4RM tab shows the same
rebuild progress visible on the SYST3M tab's R3BU1LD QU3U3.
The row visual splits queued vs running: a **queued** entry shows
only the pending-state pill (no row tint, so a long queue doesn't
paint half the tab amber); a **running** entry keeps the amber
row tint AND draws a **rotating amber ring** around the agent
icon, so it's obvious at a glance which container is actually
moving.
Container name + port, and a `ctx · Nk` chip showing the
agent's last-turn context size (from `ContainerView.ctx_tokens`,
read from the turn-stats sqlite on each `build_all` sweep;

View file

@ -341,11 +341,38 @@ a:hover {
flex: 1;
min-width: 0;
}
.container-row.pending {
/* Pending state splits queued vs running (#769): queued ops show
only the pending-state badge the row sits unhighlighted so a
long queue doesn't paint half the SW4RM tab amber. Running ops
keep the amber row tint AND get a rotating amber ring on the
agent icon so it's obvious which container is actually moving. */
.container-row.pending .actions { opacity: 0.4; pointer-events: none; }
.container-row.pending-running {
border-color: var(--amber);
background: rgba(250, 179, 135, 0.05);
}
.container-row.pending .actions { opacity: 0.4; pointer-events: none; }
/* The spinner is a transparent border ring with two adjacent sides
coloured amber, rotated by a CSS animation looks like an
orbiting arc around the icon. Override the icon's overflow:hidden
so the ring can sit just outside the square; it composes naturally
with the mauve selected-ring (selected + running shows both). */
@keyframes container-icon-spin {
to { transform: rotate(360deg); }
}
.container-row.pending-running > .container-icon {
overflow: visible;
}
.container-row.pending-running > .container-icon::after {
content: '';
position: absolute;
inset: -4px;
border-radius: 10px;
border: 2px solid transparent;
border-top-color: var(--amber);
border-right-color: var(--amber);
animation: container-icon-spin 1s linear infinite;
pointer-events: none;
}
.container-row .head {
display: flex;
align-items: center;

View file

@ -497,10 +497,19 @@ window.marked = marked;
: (op.kind === 'meta_update' ? 'meta-update queued'
: op.kind === 'destroy' ? 'destroy queued'
: 'rebuild queued')));
// Split the visual into queued vs running (#769): queued ops get
// no row highlight — the badge text alone says "wait, queued" —
// while running ops keep the amber row tint + add a rotating
// amber ring on the agent icon. Operator-initiated transients
// always count as running (the op is already in flight even if
// the queue worker hasn't picked it up yet).
const opRunning = transientKind != null
|| (op != null && op.state === 'running');
const selected = selectionState.has(c.name);
const li = el('li', {
class: 'container-row'
+ (pending ? ' pending' : '')
+ (opRunning ? ' pending-running' : '')
+ (selected ? ' selected' : ''),
});
// Topology: depth contributes left-padding; the glyph string in