From ab1f8d6e33dfc3bd6b923452513570a308079fb3 Mon Sep 17 00:00:00 2001 From: iris Date: Thu, 21 May 2026 22:01:38 +0200 Subject: [PATCH] dashboard: icon fallback on real img load failure, not container-state guess --- docs/web-ui.md | 15 +++++++++------ hive-c0re/assets/app.js | 31 +++++++++++++++++++------------ hive-c0re/assets/dashboard.css | 19 ++++++++++++++----- 3 files changed, 42 insertions(+), 23 deletions(-) diff --git a/docs/web-ui.md b/docs/web-ui.md index a3cb6d7..a3036da 100644 --- a/docs/web-ui.md +++ b/docs/web-ui.md @@ -160,12 +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`). When the -container is stopped or mid-transient (its web server isn't -answering) the icon falls back to the dimmed hyperhive mark -(`/favicon.svg`) instead of an empty box. +`/icon`, an `` 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 `` points straight +at `/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 diff --git a/hive-c0re/assets/app.js b/hive-c0re/assets/app.js index 6b68c16..4230926 100644 --- a/hive-c0re/assets/app.js +++ b/hive-c0re/assets/app.js @@ -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 ) 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 absolutely positioned inside a wrapper div: + // the div is the flex child and sizes itself via aspect-ratio + + // stretch, the 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 `/icon` - // would just fail to an empty box. Fall back to the dimmed + // The icon points straight at the agent's `/icon`. We don't + // guess whether the agent is reachable from the container row — + // we just let the 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' }); diff --git a/hive-c0re/assets/dashboard.css b/hive-c0re/assets/dashboard.css index dc9133e..5b3e8a8 100644 --- a/hive-c0re/assets/dashboard.css +++ b/hive-c0re/assets/dashboard.css @@ -89,17 +89,26 @@ 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; } -/* Stopped / mid-transient container: the dimmed hyperhive mark stands - in for the unreachable agent icon (issue #195). */ +/* 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 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;