dashboard: full-height square agent icon, icon-left card layout

The agent icon was a 26px <img> 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
This commit is contained in:
iris 2026-05-21 19:19:47 +02:00
parent 3b44410427
commit 9abcda280a
3 changed files with 50 additions and 23 deletions

View file

@ -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 <img>) 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);