dashboard: hide live-only status when container is stopped (#432)
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.
This commit is contained in:
parent
8d40499e8d
commit
683e59c757
1 changed files with 54 additions and 28 deletions
|
|
@ -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)` : '';
|
||||
|
|
|
|||
Loading…
Reference in a new issue