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
A full-height **square agent icon** on the left (the agent's
`/icon`, painted as a background-image div so its load state can
never reflow the row), and the card body on the right with three
stacked lines (`assets/app.js::renderContainers`):
`/icon`, an `<img>` absolutely positioned inside a wrapper div so
its load state can never reflow the row), and the card body on
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
badges — `⊘ rate limited` (red, while the harness is parked

View file

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

View file

@ -89,14 +89,29 @@ a:hover {
gap: 0.7em;
}
.container-row:not(.tombstone) > .container-icon {
position: relative;
overflow: hidden;
flex: none;
align-self: stretch;
aspect-ratio: 1;
border-radius: 6px;
background-color: rgba(17, 17, 27, 0.6);
background-size: contain;
background-position: center;
background-repeat: no-repeat;
}
/* The icon image fills the square wrapper and is taken out of flow
(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 {
flex: 1;