From 683e59c7572718896175a3e31857e35dea0ab6db Mon Sep 17 00:00:00 2001 From: iris Date: Mon, 25 May 2026 23:00:55 +0200 Subject: [PATCH] dashboard: hide live-only status when container is stopped (#432) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mara: *don't show agent status if container is shut down. agent meta should probably show the info that it is not running as well*. When `c.running` is false the harness isn't there to report state, but the dashboard kept rendering everything that depends on it — alive-badge, rate-limited / needs-login chips, ctx token chip, self-reported status text, the nav-strip fetched from `/api/agent/{name}/links`. All of it goes stale the moment the container shuts down; the operator sees data that hasn't been true for hours. Two changes in `renderContainers`: 1. Replace the live-only badge chain with a single `■ not running` muted badge when stopped. Pending transients still win (a start / stop in flight gets the spinner). Static info — needs-update, `container :port` meta, `deployed:sha`, pending-reminder count — stays visible regardless of running state. 2. Skip the nav-strip fetch + the self-reported status text on stopped containers. The fetch would fail anyway (container web server is down); the status text was last set when the harness was alive and isn't current. Also short-circuit the agent-icon img: skip the doomed `${url}icon` request and go straight to the dimmed `/favicon.svg` fallback. Avoids a noisy failed request in the console + the brief broken-image flash. No CSS changes — reuses the existing `.badge-muted` style. --- frontend/packages/dashboard/src/app.js | 82 +++++++++++++++++--------- 1 file changed, 54 insertions(+), 28 deletions(-) diff --git a/frontend/packages/dashboard/src/app.js b/frontend/packages/dashboard/src/app.js index 24d22d83..96275567 100644 --- a/frontend/packages/dashboard/src/app.js +++ b/frontend/packages/dashboard/src/app.js @@ -499,14 +499,23 @@ window.marked = marked; // answering) the error handler falls it back to the dimmed // hyperhive mark (`/favicon.svg`, served by the dashboard // itself, always reachable). (issues #195, #202) - const iconImg = el('img', { class: 'container-icon-img', src: `${url}icon`, alt: '' }); + const iconImg = el('img', { class: 'container-icon-img', alt: '' }); const icon = el('div', { class: 'container-icon' }, iconImg); - iconImg.addEventListener('error', () => { - if (iconImg.dataset.fallback) return; // guard: don't loop if the favicon itself 404s - iconImg.dataset.fallback = '1'; + if (c.running) { + iconImg.src = `${url}icon`; + iconImg.addEventListener('error', () => { + if (iconImg.dataset.fallback) return; // guard: don't loop if the favicon itself 404s + iconImg.dataset.fallback = '1'; + icon.classList.add('icon-unreachable'); + iconImg.src = '/favicon.svg'; + }); + } else { + // Container stopped (#432) — skip the doomed `${url}icon` fetch + // and go straight to the dimmed hyperhive mark. Avoids a noisy + // failed request in the console + the brief broken-image flash. icon.classList.add('icon-unreachable'); iconImg.src = '/favicon.svg'; - }); + } // Card body: the three stacked content lines, right of the icon. const body = el('div', { class: 'card-body' }); @@ -527,30 +536,43 @@ window.marked = marked; head.append(navStrip); const forgeBase = `http://${hostname}:3000`; const containerBase = `http://${hostname}:${c.port}`; - fetch(`/api/agent/${encodeURIComponent(c.name)}/links`) - .then((r) => (r.ok ? r.json() : [])) - .then((links) => { - if (!Array.isArray(links)) return; - for (const lnk of links) { - const href = lnk.kind === 'forge' ? forgeBase + (lnk.url || '') - : lnk.kind === 'external' ? (lnk.url || '') - : /* container */ containerBase + (lnk.url || ''); - const a = el('a', { - class: 'nav-link', - href, - target: '_blank', - rel: 'noopener', - title: lnk.label || '', - }); - // Plain text — agent-controlled strings stay out of innerHTML. - a.textContent = lnk.icon || lnk.label || ''; - navStrip.append(a); - } - }) - .catch(() => { /* graceful: agent down → no strip */ }); + if (c.running) { + fetch(`/api/agent/${encodeURIComponent(c.name)}/links`) + .then((r) => (r.ok ? r.json() : [])) + .then((links) => { + if (!Array.isArray(links)) return; + for (const lnk of links) { + const href = lnk.kind === 'forge' ? forgeBase + (lnk.url || '') + : lnk.kind === 'external' ? (lnk.url || '') + : /* container */ containerBase + (lnk.url || ''); + const a = el('a', { + class: 'nav-link', + href, + target: '_blank', + rel: 'noopener', + title: lnk.label || '', + }); + // Plain text — agent-controlled strings stay out of innerHTML. + a.textContent = lnk.icon || lnk.label || ''; + navStrip.append(a); + } + }) + .catch(() => { /* graceful: agent down → no strip */ }); + } + // Status / runtime badges (#432). Pending transients always win + // (start / stop / restart / rebuild is in progress). Otherwise: + // when the container is stopped, surface a single `■ stopped` + // badge and skip everything that depends on a live harness + // (alive / rate-limited / needs-login / ctx / status text); + // those badges go stale the moment the harness goes away and + // just confuse the operator if we keep showing them. if (pending) { head.append(el('span', { class: 'pending-state' }, el('span', { class: 'spinner' }, '◐'), ' ', pending + '…')); + } 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' }, + '■ not running')); } else if (c.rate_limited) { head.append(el('span', { class: 'badge badge-rate-limited', title: 'API rate-limited — harness is parked, will retry automatically' }, @@ -581,7 +603,7 @@ window.marked = marked; }, `⏰ ${c.pending_reminders}`)); } - if (c.ctx_tokens != null) { + if (c.running && c.ctx_tokens != null) { const k = Math.round(c.ctx_tokens / 1000); // Thresholds track the model's real context window when the // backend supplies it; otherwise fall back to fixed constants. @@ -602,7 +624,11 @@ window.marked = marked; body.append(head); // ── agent status text ───────────────────────────────────────── - if (c.status_text) { + // Self-reported status (via set_status MCP tool) — only fresh + // while the harness is up. Skip on stopped containers (#432); + // the text is from the last time the harness was running and + // just misleads now. + if (c.running && c.status_text) { const nowUnix = Math.floor(Date.now() / 1000); const ageStr = c.status_set_at != null ? ` (set ${fmtAgeSecs(nowUnix - c.status_set_at)} ago)` : '';