dashboard: icon fallback on real img load failure, not container-state guess

This commit is contained in:
iris 2026-05-21 22:01:38 +02:00
parent 16f614f45d
commit ab1f8d6e33
3 changed files with 42 additions and 23 deletions

View file

@ -578,20 +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 —
// no broken-image glyph, no collapse. (issue #177)
// 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)
//
// When the container is stopped or mid-transient (restarting,
// rebuilding…) its web server isn't answering, so `<url>/icon`
// would just fail to an empty box. Fall back to the dimmed
// 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) instead. (issue #195)
const reachable = c.running && !pending;
const icon = el('div', {
class: 'container-icon' + (reachable ? '' : ' icon-unreachable'),
style: `background-image:url("${reachable ? `${url}icon` : '/favicon.svg'}")`,
// 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' });