diff --git a/docs/web-ui/dashboard.md b/docs/web-ui/dashboard.md index 842e9e37..7cb7cb40 100644 --- a/docs/web-ui/dashboard.md +++ b/docs/web-ui/dashboard.md @@ -892,12 +892,22 @@ fetch entirely. **When the container is stopped** (`ContainerView.running = false`), the async `dashboard-state` fetch is skipped entirely (the agent web server is down), so the badge chain is replaced by a single - muted `■ not running` badge, the nav strip is empty, and status - text / rate-limited / ctx badges are suppressed. The agent icon - goes straight to the dimmed `/favicon.svg` fallback instead of - attempting a doomed load from the container's URL. Static fields - — `needs_update`, `deployed_sha`, `pending_reminders`, `parent`, - `config` link — remain visible regardless of run state. + badge, the nav strip is empty, and status text / rate-limited / ctx + badges are suppressed. The agent icon goes straight to the dimmed + `/favicon.svg` fallback instead of attempting a doomed load from the + container's URL. Static fields — `needs_update`, `deployed_sha`, + `pending_reminders`, `parent`, `config` link — remain visible + regardless of run state. + **Which single badge (hyperhive#3139):** `ContainerView.failed` + (systemd `ActiveState=failed` — the unit exhausted its bounded + restarts and gave up on its own) draws a red `✖ gave up` badge; + otherwise a plain muted `■ not running` — a container an operator + stopped deliberately. Both states read `running: false`; `failed` is + the orthogonal fact (a fifth one alongside `paused`/`needs_update`/ + `needs_login`, same "independent flags, no state machine" shape — + see `ContainerView`'s own doc comment) that tells them apart. An + older backend without the field serves `failed: undefined`, which + reads falsy — degrades cleanly to the single `not running` badge. When the container is running, status badges follow — `⊘ rate limited` (red, while the harness is parked after a 429), `needs login`, `needs update` — plus **one `◐ pending-state…` pill per diff --git a/frontend/packages/dashboard/src/swarm.js b/frontend/packages/dashboard/src/swarm.js index 7e70bcbc..8fad62d8 100644 --- a/frontend/packages/dashboard/src/swarm.js +++ b/frontend/packages/dashboard/src/swarm.js @@ -302,6 +302,7 @@ function containerRowFingerprint(c, node, pending, opRunning, selected, askerCount, targetCount, gatewayLinks, hostname) { return JSON.stringify({ running: c.running, + failed: c.failed, paused: c.paused, needs_login: c.needs_login, needs_update: c.needs_update, @@ -495,10 +496,15 @@ function buildContainerLi(c, node, opts) { } // Status / runtime badges. Pending transients always win // (start / stop / restart / rebuild is in progress). Otherwise, - // when the container is stopped, surface a single `■ not running` - // badge. `needs_login` is still c0re-owned (reads auth sentinel - // files on the host). rate_limited / ctx / status_text are - // agent-owned and rendered by the async dashboard-state fetch above. + // when the container is stopped, surface a single badge — `✖ gave + // up` (red) when systemd's restart bound was hit (`c.failed`), else + // the plain muted `■ not running`. Both read `running: false`; + // `failed` is what tells "gave up" apart from "stopped on purpose" + // (an absent `failed` — an older backend — is `undefined`, falsy, + // so this degrades to the old single badge). `needs_login` is still + // c0re-owned (reads auth sentinel files on the host). rate_limited / + // ctx / status_text are agent-owned and rendered by the async + // dashboard-state fetch above. if (pending.length) { // One badge per pill — an agent can carry several transients at // once now (see docs/web-ui.md::Container row), each rendered @@ -507,6 +513,14 @@ function buildContainerLi(c, node, opts) { head.append(el('span', { class: 'pending-state' }, el('span', { class: 'spinner' }, '◐'), ' ', label + '…')); } + } else if (!c.running && c.failed) { + head.append(el('span', + { + class: 'badge badge-fail', + title: 'container gave up — its unit hit systemd\'s bounded restart limit and stopped ' + + 'on its own, not on operator request; start it to bring the harness back up', + }, + '✖ gave up')); } else if (!c.running) { head.append(el('span', { class: 'badge badge-muted', title: 'container is shut down — start it to bring the harness back up' },