From 9abcda280a134195673d2ee9aefbb6bf90b0bf5a Mon Sep 17 00:00:00 2001 From: iris Date: Thu, 21 May 2026 19:19:47 +0200 Subject: [PATCH] dashboard: full-height square agent icon, icon-left card layout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The agent icon was a 26px inline in the card head, hidden via onerror when a stopped container's web server didn't answer — which collapsed the slot and shifted the row. Restructure the live container card as icon-left / body-right: - the icon is a background-image div with aspect-ratio 1 and align-self stretch — full card height, square, and (being a background) it has no intrinsic size, so loading or failing it can never reflow the row; - a failed load (stopped container) falls through to a placeholder fill instead of collapsing; - the three content lines move into a .card-body column. Tombstone rows keep the plain stacked layout (:not(.tombstone)). closes #177 --- docs/web-ui.md | 5 ++++- hive-c0re/assets/app.js | 33 +++++++++++++++++++------------- hive-c0re/assets/dashboard.css | 35 +++++++++++++++++++++++++--------- 3 files changed, 50 insertions(+), 23 deletions(-) diff --git a/docs/web-ui.md b/docs/web-ui.md index 31b6526..69b0c47 100644 --- a/docs/web-ui.md +++ b/docs/web-ui.md @@ -148,7 +148,10 @@ the previous process's socket release resolves itself. ### Container row -Two-line layout (`assets/app.js::renderContainers`): +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`): - 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 0e9ff2c..dc291e0 100644 --- a/hive-c0re/assets/app.js +++ b/hive-c0re/assets/app.js @@ -516,16 +516,22 @@ const pending = transientsState.get(c.name)?.kind || null; const li = el('li', { class: 'container-row' + (pending ? ' pending' : '') }); - // ── line 1: identity ───────────────────────────────────────── + // 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")`, + }); + // Card body: the three stacked content lines, right of the icon. + const body = el('div', { class: 'card-body' }); + + // ── identity ───────────────────────────────────────────────── const head = el('div', { class: 'head' }); head.append( - // /icon always returns an image (the agent's configured SVG or - // the default hyperhive logo). onerror hides it for a stopped - // container whose web server isn't answering. - el('img', { - class: 'container-icon', src: url + 'icon', alt: '', loading: 'lazy', - onerror: "this.style.display='none'", - }), el('a', { class: 'name', href: url, target: '_blank', rel: 'noopener' }, c.name), el('span', { class: c.is_manager ? 'role role-m1nd' : 'role role-ag3nt' }, c.is_manager ? 'm1nd' : 'ag3nt'), @@ -582,9 +588,9 @@ }, `ctx·${k}k`)); } - li.append(head); + body.append(head); - // ── line 2: action buttons ─────────────────────────────────── + // ── action buttons ─────────────────────────────────────────── const actions = el('div', { class: 'actions' }); if (c.running) { actions.append( @@ -623,9 +629,9 @@ { purge: 'on' }, { noRefresh: true }), ); } - li.append(actions); + body.append(actions); - // ── line 3: drill-ins ──────────────────────────────────────── + // ── drill-ins ──────────────────────────────────────────────── const drill = el('div', { class: 'drill-ins' }); // Per-container journald viewer. Opens the side panel and // fetches the last N lines; refresh re-fetches; unit selector @@ -641,8 +647,9 @@ title: 'applied config repo on the hive forge', }, '↳ config repo ↗')); } - li.append(drill); + body.append(drill); + li.append(icon, body); ul.append(li); } root.append(ul); diff --git a/hive-c0re/assets/dashboard.css b/hive-c0re/assets/dashboard.css index acb23c6..0eca735 100644 --- a/hive-c0re/assets/dashboard.css +++ b/hive-c0re/assets/dashboard.css @@ -68,9 +68,9 @@ a:hover { } .role-m1nd { color: var(--pink); border-color: var(--pink); background: rgba(245, 194, 231, 0.08); } .role-ag3nt { color: var(--amber); border-color: var(--amber); background: rgba(250, 179, 135, 0.08); } -/* Container rows: identity + meta on a flowing first line, action - buttons grouped on a second. Pending rows dim everything except - the pending-state indicator. */ +/* Container rows: a full-height square agent icon on the left, the + identity / actions / drill-in lines stacked in the card body on the + right. Pending rows dim everything except the pending indicator. */ .containers { display: flex; flex-direction: column; gap: 0.4em; } .container-row { padding: 0.6em 0.8em; @@ -79,6 +79,29 @@ a:hover { background: rgba(24, 24, 37, 0.55); transition: opacity 200ms ease, border-color 200ms ease; } +/* Live cards get the icon-left / body-right split; tombstone rows keep + the plain stacked block layout. The icon is a background-image div + with no intrinsic size, so its load state can never reflow the row + — and it stretches to the body height, staying square (issue #177). */ +.container-row:not(.tombstone) { + display: flex; + align-items: stretch; + gap: 0.7em; +} +.container-row:not(.tombstone) > .container-icon { + 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; +} +.container-row .card-body { + flex: 1; + min-width: 0; +} .container-row.pending { border-color: var(--amber); background: rgba(250, 179, 135, 0.05); @@ -95,12 +118,6 @@ a:hover { font-size: 1.05em; font-weight: bold; } -.container-row .head .container-icon { - width: 26px; - height: 26px; - border-radius: 4px; - flex-shrink: 0; -} .container-row .head .meta { margin-left: auto; } .container-row .actions { display: flex;