diff --git a/docs/web-ui.md b/docs/web-ui.md index 8f086622..a3036da0 100644 --- a/docs/web-ui.md +++ b/docs/web-ui.md @@ -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 `` 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 e23d222d..42309267 100644 --- a/hive-c0re/assets/app.js +++ b/hive-c0re/assets/app.js @@ -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 ) 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 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) + // + // 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). (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 0e05cc9e..5b3e8a86 100644 --- a/hive-c0re/assets/dashboard.css +++ b/hive-c0re/assets/dashboard.css @@ -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 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;