Watch
0
0
Fork
You've already forked hyperhive
0

Compare commits

...
Author SHA1 Message Date
iris
2e67656de8 docs: drop leftover 'nothing nests' framing from C0NTAINERS blurb
Per mara's review on #4769: don't extend stale info to say it's stale,
just remove it. The sentence only described the absence of the removed
topology tree, giving the operator nothing actionable.
2026-09-28 13:18:12 +02:00
iris
d9beb965a5 docs/web-ui: drop the redundant Topology tree section
C0NTAINERS already states the flat/alphabetical/no-nesting fact;
the section repeated it with zero new operator info. Fix the two
dangling references (README.md reading-path index, swarm.js
comment) rather than leaving them pointing at a removed heading.
2026-09-28 13:18:12 +02:00
iris
ef4e49805c docs/web-ui/dashboard.md: drop stale topology-tree history per mara review
state current fact only, not the old-vs-new narrative
2026-09-28 13:18:12 +02:00
iris
d447974536 docs/web-ui/dashboard.md: fix vale hits in topology-tree rewrite
- avoid 'backend' (Microsoft.Avoid)
- reword 'was removed' passive voice (write-good.Passive)
2026-09-28 13:18:12 +02:00
iris
6b5c8cc51a dashboard: flatten the agent list, drop dead topology-tree machinery (#4638)
The backend dropped the agent hierarchy's parent field, so every
container is a root and buildAgentTree/treePrefixDom could only ever
produce a single-level flat list — the .tree-prefix CSS lane rules
already matched nothing. Replaced with sortedContainerRows, a plain
alphabetical sort, and dropped the now-dead .tree-prefix/.tree-lane/
data-depth CSS and the depth/isLast/ancestorIsLast fields from the row
fingerprint and buildContainerLi's signature. No visible behavior
change - the rendered list was already flat, just via dead machinery.

Docs updated to describe the simpler implementation directly instead
of narrating the removal (kept the heading name since swarm.js still
points a comment at it).
2026-09-28 13:18:12 +02:00
4 changed files with 14 additions and 177 deletions

View file

@ -115,8 +115,7 @@ touching the code rather than using the UI:
[`shape.md`](shape.md) (SSE multiplexing, Worker-death self-heal,
atomic repaint).
- **"What does a container row contain?"** →
[`dashboard.md`](dashboard.md) (Container row, Topology tree,
Selection bar).
[`dashboard.md`](dashboard.md) (Container row, Selection bar).
- **"What endpoints does the dashboard expose?"** →
[`dashboard.md`](dashboard.md) (Dashboard endpoints, Dashboard event
channel).

View file

@ -57,8 +57,7 @@ surfaces, not tab panes.
## SW4RM tab
**C0NTAINERS** — live containers as a flat list, one row per
container, sorted alphabetically by agent name. No row nests under
another, none carries an indent or a tree glyph, and every container the
container, sorted alphabetically by agent name; every container the
rescan reports gets a row. Pulsing red banner at the top
of this section if any two sub-agents hash to the same port
(`port_conflicts` from `/api/state`): the operator must rename
@ -974,14 +973,6 @@ build. (The `hivectl stop --graceful` CLI flag enqueues the same
`Signal`/`Drain` pair, so the dashboard and CLI paths behave
identically.)
### Topology tree
Nothing nests. `swarm.js::buildAgentTree` returns every container at
depth 0 in alphabetical order, and the row builder emits no prefix
column for a depth-0 row — so a container row carries no indent, no
joint and no continuation bar, and the SW4RM list reads as a flat
roster. The `.tree-prefix` lane rules in `dashboard.css` match nothing.
## Selection bar
Bulk actions (`R3ST4RT` / `ST0P` / `ST4RT` / `P4US3` / `R3SUM3` /

View file

@ -200,84 +200,6 @@ body.dashboard-shell {
opacity 200ms ease,
border-color 200ms ease;
}
/* Topology indent ladder. See docs/web-ui/dashboard.md::Topology tree (Indent
+ lane geometry paragraph) for the 1.8em-per-depth-level
rationale + CSS-attr()-not-yet-portable caveat. */
.container-row[data-depth] {
position: relative;
}
.container-row[data-depth="1"] {
margin-left: 1.8em;
}
.container-row[data-depth="2"] {
margin-left: 3.6em;
}
.container-row[data-depth="3"] {
margin-left: 5.4em;
}
.container-row[data-depth="4"] {
margin-left: 7.2em;
}
.container-row[data-depth="5"] {
margin-left: 9em;
}
.container-row[data-depth="6"] {
margin-left: 10.8em;
}
/* Tree prefix lanes — DOM-painted, not text-glyph-painted. */
.container-row .tree-prefix {
position: absolute;
top: 0;
bottom: -0.4em;
display: flex;
flex-direction: row;
pointer-events: none;
user-select: none;
color: var(--purple-dim);
}
.container-row[data-depth="1"] .tree-prefix {
left: -1.8em;
}
.container-row[data-depth="2"] .tree-prefix {
left: -3.6em;
}
.container-row[data-depth="3"] .tree-prefix {
left: -5.4em;
}
.container-row[data-depth="4"] .tree-prefix {
left: -7.2em;
}
.container-row[data-depth="5"] .tree-prefix {
left: -9em;
}
.container-row[data-depth="6"] .tree-prefix {
left: -10.8em;
}
.tree-prefix .tree-lane {
flex: 0 0 1.8em;
position: relative;
}
.tree-prefix .lane-line::before,
.tree-prefix .lane-joint::before {
content: "";
position: absolute;
left: 0.6em;
top: 0;
bottom: 0;
border-left: 1px solid currentColor;
}
.tree-prefix .lane-joint-last::before {
bottom: auto;
height: 3.1em;
}
.tree-prefix .lane-joint::after {
content: "";
position: absolute;
left: 0.6em;
top: 3.1em;
width: 2em;
border-top: 1px solid currentColor;
}
.container-row:not(.tombstone) {
display: flex;
align-items: flex-start;

View file

@ -226,89 +226,26 @@ function derivePortConflicts(containers) {
return out;
}
// ─── topology tree ───────────────────────────────────────────────────────────
// See docs/web-ui/dashboard.md::Topology tree for the rendering contract
// (forest walk, alphabetical sort, orphan + cycle handling).
// ─── container ordering ──────────────────────────────────────────────────────
// Flat alphabetical sort — see docs/web-ui/dashboard.md::C0NTAINERS.
function buildAgentTree(containers) {
function sortedContainerRows(containers) {
// Close any open context menu before replacing the DOM tree — the
// previous dropdown element would otherwise be a stale reference.
closeAllMenus();
const byName = new Map();
for (const c of containers) byName.set(c.name, c);
const children = new Map(); // parent_name -> [child_name, ...]
const roots = [];
for (const c of containers) {
const p = c.parent || null;
if (p == null || !byName.has(p)) {
roots.push(c.name);
} else {
const list = children.get(p) || [];
list.push(c.name);
children.set(p, list);
}
}
roots.sort();
for (const list of children.values()) list.sort();
const out = [];
const visited = new Set();
function visit(name, depth, ancestorIsLast, isLast) {
if (visited.has(name)) return;
visited.add(name);
const c = byName.get(name);
if (!c) return;
out.push({
container: c,
depth,
ancestorIsLast: [...ancestorIsLast],
isLast,
});
const kids = children.get(name) || [];
kids.forEach((kid, i) =>
visit(kid, depth + 1, [...ancestorIsLast, isLast], i === kids.length - 1),
);
}
roots.forEach((name, i) => visit(name, 0, [], i === roots.length - 1));
// Cycle safety: anything not reached lands at root level so no
// agent silently disappears when a config is malformed.
for (const c of containers) {
if (!visited.has(c.name)) visit(c.name, 0, [], true);
}
return out;
}
// Builds the .tree-prefix DOM for a row at the given depth.
// See docs/web-ui/dashboard.md::Topology tree for why this is DOM-painted
// (one positioned <span> per lane) rather than text-glyph-painted.
function treePrefixDom({ depth, ancestorIsLast, isLast }) {
if (depth === 0) return null;
const prefix = el("span", { class: "tree-prefix", "aria-hidden": "true" });
// Ancestor columns (depth 1..depth-1). Skip depth 0 (root has no
// continuation column — top-level rows are separated visually as
// top-level rows already).
for (let d = 1; d < depth; d++) {
const cls = ancestorIsLast[d]
? "tree-lane lane-blank"
: "tree-lane lane-line";
prefix.append(el("span", { class: cls }));
}
const jointCls =
"tree-lane lane-joint " +
(isLast ? "lane-joint-last" : "lane-joint-branch");
prefix.append(el("span", { class: jointCls }));
return prefix;
return [...containers].sort((a, b) => a.name.localeCompare(b.name));
}
// ─── container row ───────────────────────────────────────────────────────────
// Serialise the visible state of a container row into a stable string
// for change-detection. Includes everything that affects what the row
// renders — container fields, derived pending/selection state, tree
// position, and link-base context. The async dashboard-state (nav
// strip, ctx badge, status text) is intentionally excluded: it
// populates in-place and is preserved when a row is reused.
// renders — container fields, derived pending/selection state, and
// link-base context. The async dashboard-state (nav strip, ctx badge,
// status text) is intentionally excluded: it populates in-place and is
// preserved when a row is reused.
function containerRowFingerprint(
c,
node,
pending,
opRunning,
selected,
@ -326,9 +263,6 @@ function containerRowFingerprint(
pending,
opRunning,
selected,
depth: node.depth,
isLast: node.isLast,
ancestorIsLast: node.ancestorIsLast,
gatewayLinks,
hostname,
});
@ -338,7 +272,7 @@ function containerRowFingerprint(
// renderContainers can call this only for rows whose fingerprint
// changed (keyed cache), skipping the build + async dashboard-state
// fetch for stable rows.
function buildContainerLi(c, node, opts) {
function buildContainerLi(c, opts) {
const { pending, opRunning, selected, url, containerBase, forgeBase, s } =
opts;
// A single `pending-running` class covers any active transient
@ -350,13 +284,6 @@ function buildContainerLi(c, node, opts) {
(opRunning ? " pending-running" : "") +
(selected ? " selected" : ""),
});
// Topology: depth contributes left-padding; the glyph string in
// the .tree-prefix span draws the ├─ / └─ joint + continuation
// lines (`│ `) for ancestors whose subtree extends below this
// row. A depth-0 row gets neither — no glyph, no indent.
if (node.depth > 0) li.dataset.depth = String(node.depth);
const prefix = treePrefixDom(node);
if (prefix) li.prepend(prefix);
// Agent icon: 5em square wrapper with an absolutely-positioned
// <img> + fire-and-forget load with /favicon.svg fallback. The
@ -744,13 +671,12 @@ export function renderContainers(s) {
// rather than emit a possibly-wrong one.
const forgeBase = (s && s.forge_public_url) || null;
const ul = existingUl ?? el("ul", { class: "containers" });
const tree = buildAgentTree(containers);
const rows = sortedContainerRows(containers);
// Build the ordered list of <li> elements, reusing cached rows
// whose displayed state hasn't changed.
const orderedLis = [];
for (const node of tree) {
const c = node.container;
for (const c of rows) {
const url = gatewayLinks
? `/agent/${encodeURIComponent(c.name)}/`
: `http://${hostname}:${c.port}/`;
@ -774,7 +700,6 @@ export function renderContainers(s) {
const fp = containerRowFingerprint(
c,
node,
pending,
opRunning,
selected,
@ -790,7 +715,7 @@ export function renderContainers(s) {
// intact from the previous build, avoiding a redundant fetch.
li = cached.el;
} else {
li = buildContainerLi(c, node, {
li = buildContainerLi(c, {
pending,
opRunning,
selected,