Compare commits

...
Author SHA1 Message Date
iris
ab1f8d6e33 dashboard: icon fallback on real img load failure, not container-state guess 2026-05-21 22:01:38 +02:00
iris
16f614f45d dashboard: dimmed default icon for unreachable containers
A stopped or mid-transient (restarting / rebuilding) container's
web server isn't answering, so its <url>/icon background-image
just failed to an empty box on the card.

When the container isn't reachable (not running, or a transient
is in flight) the icon now falls back to the dimmed hyperhive
mark — /favicon.svg, served by the dashboard itself so it's
always loadable — greyscaled + lowered opacity via the
.icon-unreachable class.

closes #195
2026-05-21 21:56:39 +02:00
3 changed files with 48 additions and 15 deletions

View file

@ -160,9 +160,15 @@ the previous process's socket release resolves itself.
### Container row ### Container row
A full-height **square agent icon** on the left (the agent's A full-height **square agent icon** on the left (the agent's
`/icon`, painted as a background-image div so its load state can `/icon`, an `<img>` absolutely positioned inside a wrapper div so
never reflow the row), and the card body on the right with three its load state can never reflow the row), and the card body on
stacked lines (`assets/app.js::renderContainers`): the right with three stacked lines
(`assets/app.js::renderContainers`). The `<img>` points straight
at `<url>/icon`; if it actually fails to load (container stopped
or mid-transient, web server not answering) the `error` handler
falls it back to the dimmed hyperhive mark (`/favicon.svg`)
instead of an empty box — a real load-failure fallback, not a
guess from container state.
- Line 1: agent name (link → new tab), m1nd/ag3nt chip, status - Line 1: agent name (link → new tab), m1nd/ag3nt chip, status
badges — `⊘ rate limited` (red, while the harness is parked badges — `⊘ rate limited` (red, while the harness is parked

View file

@ -578,15 +578,27 @@
const pending = transientsState.get(c.name)?.kind || null; const pending = transientsState.get(c.name)?.kind || null;
const li = el('li', { class: 'container-row' + (pending ? ' pending' : '') }); const li = el('li', { class: 'container-row' + (pending ? ' pending' : '') });
// Full-height square agent icon, left of the card body. A // Full-height square agent icon, left of the card body. The
// background-image on a div (not <img>) contributes no intrinsic // icon is an <img> absolutely positioned inside a wrapper div:
// size, so loading or failing it can't shift the row layout, and // the div is the flex child and sizes itself via aspect-ratio +
// a failed load (stopped container, web server down) just shows // stretch, the <img> is out of flow so its load state — pending,
// the placeholder fill — no broken-image glyph, no collapse. // loaded or broken — can never contribute intrinsic size or
// (issue #177) // reflow the row. (issue #177)
const icon = el('div', { //
class: 'container-icon', // The icon points straight at the agent's `<url>/icon`. We don't
style: `background-image:url("${url}icon")`, // guess whether the agent is reachable from the container row —
// we just let the <img> try, and if it actually fails to load
// (agent stopped, restarting, rebuilding — web server not
// 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 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';
icon.classList.add('icon-unreachable');
iconImg.src = '/favicon.svg';
}); });
// Card body: the three stacked content lines, right of the icon. // Card body: the three stacked content lines, right of the icon.
const body = el('div', { class: 'card-body' }); const body = el('div', { class: 'card-body' });

View file

@ -89,14 +89,29 @@ a:hover {
gap: 0.7em; gap: 0.7em;
} }
.container-row:not(.tombstone) > .container-icon { .container-row:not(.tombstone) > .container-icon {
position: relative;
overflow: hidden;
flex: none; flex: none;
align-self: stretch; align-self: stretch;
aspect-ratio: 1; aspect-ratio: 1;
border-radius: 6px; border-radius: 6px;
background-color: rgba(17, 17, 27, 0.6); background-color: rgba(17, 17, 27, 0.6);
background-size: contain; }
background-position: center; /* The icon image fills the square wrapper and is taken out of flow
background-repeat: no-repeat; (absolute) so its load state pending, loaded, broken can never
contribute intrinsic size or reflow the row. (issue #177) */
.container-row:not(.tombstone) > .container-icon > .container-icon-img {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: contain;
}
/* When the <img> fails to load it falls back to the dimmed hyperhive
mark, standing in for the unreachable agent icon (issues #195, #202). */
.container-row:not(.tombstone) > .container-icon.icon-unreachable {
filter: grayscale(1);
opacity: 0.4;
} }
.container-row .card-body { .container-row .card-body {
flex: 1; flex: 1;