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:
parent
3b44410427
commit
9abcda280a
3 changed files with 50 additions and 23 deletions
|
|
@ -148,7 +148,10 @@ the previous process's socket release resolves itself.
|
||||||
|
|
||||||
### Container row
|
### 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
|
- Line 1: agent name (link → new tab), m1nd/ag3nt chip, status
|
||||||
badges — `⊘ rate limited` (red, while the harness is parked
|
badges — `⊘ rate limited` (red, while the harness is parked
|
||||||
|
|
|
||||||
|
|
@ -516,16 +516,22 @@
|
||||||
const pending = transientsState.get(c.name)?.kind || null;
|
const pending = transientsState.get(c.name)?.kind || null;
|
||||||
const li = el('li', { class: 'container-row' + (pending ? ' pending' : '') });
|
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' });
|
const head = el('div', { class: 'head' });
|
||||||
head.append(
|
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('a', { class: 'name', href: url, target: '_blank', rel: 'noopener' }, c.name),
|
||||||
el('span', { class: c.is_manager ? 'role role-m1nd' : 'role role-ag3nt' },
|
el('span', { class: c.is_manager ? 'role role-m1nd' : 'role role-ag3nt' },
|
||||||
c.is_manager ? 'm1nd' : 'ag3nt'),
|
c.is_manager ? 'm1nd' : 'ag3nt'),
|
||||||
|
|
@ -582,9 +588,9 @@
|
||||||
},
|
},
|
||||||
`ctx·${k}k`));
|
`ctx·${k}k`));
|
||||||
}
|
}
|
||||||
li.append(head);
|
body.append(head);
|
||||||
|
|
||||||
// ── line 2: action buttons ───────────────────────────────────
|
// ── action buttons ───────────────────────────────────────────
|
||||||
const actions = el('div', { class: 'actions' });
|
const actions = el('div', { class: 'actions' });
|
||||||
if (c.running) {
|
if (c.running) {
|
||||||
actions.append(
|
actions.append(
|
||||||
|
|
@ -623,9 +629,9 @@
|
||||||
{ purge: 'on' }, { noRefresh: true }),
|
{ purge: 'on' }, { noRefresh: true }),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
li.append(actions);
|
body.append(actions);
|
||||||
|
|
||||||
// ── line 3: drill-ins ────────────────────────────────────────
|
// ── drill-ins ────────────────────────────────────────────────
|
||||||
const drill = el('div', { class: 'drill-ins' });
|
const drill = el('div', { class: 'drill-ins' });
|
||||||
// Per-container journald viewer. Opens the side panel and
|
// Per-container journald viewer. Opens the side panel and
|
||||||
// fetches the last N lines; refresh re-fetches; unit selector
|
// fetches the last N lines; refresh re-fetches; unit selector
|
||||||
|
|
@ -641,8 +647,9 @@
|
||||||
title: 'applied config repo on the hive forge',
|
title: 'applied config repo on the hive forge',
|
||||||
}, '↳ config repo ↗'));
|
}, '↳ config repo ↗'));
|
||||||
}
|
}
|
||||||
li.append(drill);
|
body.append(drill);
|
||||||
|
|
||||||
|
li.append(icon, body);
|
||||||
ul.append(li);
|
ul.append(li);
|
||||||
}
|
}
|
||||||
root.append(ul);
|
root.append(ul);
|
||||||
|
|
|
||||||
|
|
@ -68,9 +68,9 @@ a:hover {
|
||||||
}
|
}
|
||||||
.role-m1nd { color: var(--pink); border-color: var(--pink); background: rgba(245, 194, 231, 0.08); }
|
.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); }
|
.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
|
/* Container rows: a full-height square agent icon on the left, the
|
||||||
buttons grouped on a second. Pending rows dim everything except
|
identity / actions / drill-in lines stacked in the card body on the
|
||||||
the pending-state indicator. */
|
right. Pending rows dim everything except the pending indicator. */
|
||||||
.containers { display: flex; flex-direction: column; gap: 0.4em; }
|
.containers { display: flex; flex-direction: column; gap: 0.4em; }
|
||||||
.container-row {
|
.container-row {
|
||||||
padding: 0.6em 0.8em;
|
padding: 0.6em 0.8em;
|
||||||
|
|
@ -79,6 +79,29 @@ a:hover {
|
||||||
background: rgba(24, 24, 37, 0.55);
|
background: rgba(24, 24, 37, 0.55);
|
||||||
transition: opacity 200ms ease, border-color 200ms ease;
|
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 {
|
.container-row.pending {
|
||||||
border-color: var(--amber);
|
border-color: var(--amber);
|
||||||
background: rgba(250, 179, 135, 0.05);
|
background: rgba(250, 179, 135, 0.05);
|
||||||
|
|
@ -95,12 +118,6 @@ a:hover {
|
||||||
font-size: 1.05em;
|
font-size: 1.05em;
|
||||||
font-weight: bold;
|
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 .head .meta { margin-left: auto; }
|
||||||
.container-row .actions {
|
.container-row .actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue