From 6b5c8cc51a8d8a405ef22b74fac5a36d66e80e2b Mon Sep 17 00:00:00 2001 From: iris Date: Mon, 28 Sep 2026 10:30:54 +0200 Subject: [PATCH 1/5] dashboard: flatten the agent list, drop dead topology-tree machinery (#4638) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- docs/web-ui/dashboard.md | 13 ++- frontend/packages/dashboard/src/dashboard.css | 78 -------------- frontend/packages/dashboard/src/swarm.js | 100 +++--------------- 3 files changed, 21 insertions(+), 170 deletions(-) diff --git a/docs/web-ui/dashboard.md b/docs/web-ui/dashboard.md index 9990aa9b..028011c5 100644 --- a/docs/web-ui/dashboard.md +++ b/docs/web-ui/dashboard.md @@ -976,11 +976,14 @@ 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. +Nothing nests. `swarm.js::sortedContainerRows` returns every container +in alphabetical order, full stop — the SW4RM list is a flat roster, no +indent, no joint, no continuation bar. This used to be a real forest +walk (`buildAgentTree`, keyed on each container's `parent` field) with +a DOM-painted `.tree-prefix` indent ladder. Once the backend dropped +the `parent` field entirely, every container became a root and the +tree machinery could only ever produce this same flat order — so it +was removed rather than kept as code that draws a tree with one level. ## Selection bar diff --git a/frontend/packages/dashboard/src/dashboard.css b/frontend/packages/dashboard/src/dashboard.css index 71106782..e87d1af7 100644 --- a/frontend/packages/dashboard/src/dashboard.css +++ b/frontend/packages/dashboard/src/dashboard.css @@ -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; diff --git a/frontend/packages/dashboard/src/swarm.js b/frontend/packages/dashboard/src/swarm.js index 69c77bab..5281cf80 100644 --- a/frontend/packages/dashboard/src/swarm.js +++ b/frontend/packages/dashboard/src/swarm.js @@ -226,89 +226,27 @@ 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 ────────────────────────────────────────────────────── +// See docs/web-ui/dashboard.md::Topology tree for why this is a flat sort +// rather than the forest walk it used to be. -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 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 +264,6 @@ function containerRowFingerprint( pending, opRunning, selected, - depth: node.depth, - isLast: node.isLast, - ancestorIsLast: node.ancestorIsLast, gatewayLinks, hostname, }); @@ -338,7 +273,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 +285,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 // + fire-and-forget load with /favicon.svg fallback. The @@ -744,13 +672,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
  • 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 +701,6 @@ export function renderContainers(s) { const fp = containerRowFingerprint( c, - node, pending, opRunning, selected, @@ -790,7 +716,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, From d447974536e754f8c820a617d5c18d1dcad615a5 Mon Sep 17 00:00:00 2001 From: iris Date: Mon, 28 Sep 2026 10:42:09 +0200 Subject: [PATCH 2/5] docs/web-ui/dashboard.md: fix vale hits in topology-tree rewrite - avoid 'backend' (Microsoft.Avoid) - reword 'was removed' passive voice (write-good.Passive) --- docs/web-ui/dashboard.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/web-ui/dashboard.md b/docs/web-ui/dashboard.md index 028011c5..f3851329 100644 --- a/docs/web-ui/dashboard.md +++ b/docs/web-ui/dashboard.md @@ -980,10 +980,11 @@ Nothing nests. `swarm.js::sortedContainerRows` returns every container in alphabetical order, full stop — the SW4RM list is a flat roster, no indent, no joint, no continuation bar. This used to be a real forest walk (`buildAgentTree`, keyed on each container's `parent` field) with -a DOM-painted `.tree-prefix` indent ladder. Once the backend dropped -the `parent` field entirely, every container became a root and the -tree machinery could only ever produce this same flat order — so it -was removed rather than kept as code that draws a tree with one level. +a DOM-painted `.tree-prefix` indent ladder. Once containers stopped +carrying a `parent` field entirely, every container became a root and +the tree machinery could only ever produce this same flat order — so +we removed it rather than keeping code that draws a tree with one +level. ## Selection bar From ef4e49805c616cfe71ad52c5ee7f32c81cd05b25 Mon Sep 17 00:00:00 2001 From: iris Date: Mon, 28 Sep 2026 11:52:19 +0200 Subject: [PATCH 3/5] docs/web-ui/dashboard.md: drop stale topology-tree history per mara review state current fact only, not the old-vs-new narrative --- docs/web-ui/dashboard.md | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/docs/web-ui/dashboard.md b/docs/web-ui/dashboard.md index f3851329..886e1df4 100644 --- a/docs/web-ui/dashboard.md +++ b/docs/web-ui/dashboard.md @@ -978,13 +978,7 @@ identically.) Nothing nests. `swarm.js::sortedContainerRows` returns every container in alphabetical order, full stop — the SW4RM list is a flat roster, no -indent, no joint, no continuation bar. This used to be a real forest -walk (`buildAgentTree`, keyed on each container's `parent` field) with -a DOM-painted `.tree-prefix` indent ladder. Once containers stopped -carrying a `parent` field entirely, every container became a root and -the tree machinery could only ever produce this same flat order — so -we removed it rather than keeping code that draws a tree with one -level. +indent, no joint, no continuation bar. ## Selection bar From d9beb965a5fc65f14b55253c3efb0b9d3a36b819 Mon Sep 17 00:00:00 2001 From: iris Date: Mon, 28 Sep 2026 11:55:22 +0200 Subject: [PATCH 4/5] 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. --- docs/web-ui/README.md | 3 +-- docs/web-ui/dashboard.md | 6 ------ frontend/packages/dashboard/src/swarm.js | 3 +-- 3 files changed, 2 insertions(+), 10 deletions(-) diff --git a/docs/web-ui/README.md b/docs/web-ui/README.md index c0af27d2..18fadbf0 100644 --- a/docs/web-ui/README.md +++ b/docs/web-ui/README.md @@ -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). diff --git a/docs/web-ui/dashboard.md b/docs/web-ui/dashboard.md index 886e1df4..496a398d 100644 --- a/docs/web-ui/dashboard.md +++ b/docs/web-ui/dashboard.md @@ -974,12 +974,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::sortedContainerRows` returns every container -in alphabetical order, full stop — the SW4RM list is a flat roster, no -indent, no joint, no continuation bar. - ## Selection bar Bulk actions (`R3ST4RT` / `ST0P` / `ST4RT` / `P4US3` / `R3SUM3` / diff --git a/frontend/packages/dashboard/src/swarm.js b/frontend/packages/dashboard/src/swarm.js index 5281cf80..7ae5c817 100644 --- a/frontend/packages/dashboard/src/swarm.js +++ b/frontend/packages/dashboard/src/swarm.js @@ -227,8 +227,7 @@ function derivePortConflicts(containers) { } // ─── container ordering ────────────────────────────────────────────────────── -// See docs/web-ui/dashboard.md::Topology tree for why this is a flat sort -// rather than the forest walk it used to be. +// Flat alphabetical sort — see docs/web-ui/dashboard.md::C0NTAINERS. function sortedContainerRows(containers) { // Close any open context menu before replacing the DOM tree — the From 2e67656de89fe4f4059f27951fdc77f0f5c2bf1a Mon Sep 17 00:00:00 2001 From: iris Date: Mon, 28 Sep 2026 13:12:45 +0200 Subject: [PATCH 5/5] 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. --- docs/web-ui/dashboard.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/web-ui/dashboard.md b/docs/web-ui/dashboard.md index 496a398d..89342d12 100644 --- a/docs/web-ui/dashboard.md +++ b/docs/web-ui/dashboard.md @@ -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