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;