name: web-topology
The Topology window in sm121 web renders the fixed physical map as an
animated SVG infographic: 8 GX10 boxes on a 2×4 grid, the 200G fabric
switch + passive 10G switch in the middle corridor, DAC direct links,
HA glyphs (fan), cluster frames with per-cluster hues. Built to be tuned —
this is the working manual.
src/web/ui.html — everything visual:
/* --- topology map */ (all SVG styling; inline
`style="stroke:…" carries per-cluster hues, classes carry state)// --- topology map: geometry constants (TG_W/TG_H,
tgPos, switch/fan y-anchors), hueOf/TG_HUES, topoSig,
tgCablePath, devSvg, swSvg, fanSvg, topoFrames, cablesSvg,
switchesSvg, devsSvg, legendSvg, topoTick, renderTopoinitManagedWindows places Topology as the primary window (left);
state/jobs stack right. Vertical budget = #winlayer clientHeight
minus header/dock, minus 40px dock reserve (dock appears when a
window is minimized; windows overflow the layer otherwise and the
layer silently scrolls, breaking click tests).src/web/topo.rs — derive_topo(config) -> TopoSnap (pure, unit-tested):
(the map is scrollable: #topo draws the SVG at container width with
min-width: 720px and scrolls; a cluster filter — #topofilter
chip top-right, checkboxes, persisted to localStorage
sm121-topo-hidden — drops clusters from topoFrames before the depth
calc, so hiding an outer frame shrinks the nest; the filter state is part
of topoSig):
the semantic contract below. Snapshot builder in events.rs fills
TopoSnap.states from probe HA states and NodeSnap.ha per node plug.src/web/probe.rs — ha_states(): one parallel HA get_state batch per
poll for every node plug + extra entity (Some(true/false), None =
unknown/unreachable — never dim a device on None).sm121 web blurb — keep in sync when semantics change.dgx10 > dgx9, not lexicographic). Grid slot = index in that list.extra_entity that is not some node's plug =
the 200G fabric switch ("ib" glyph). Every node of that cluster with an
ib_ip gets a 200g link, deduped across clusters sharing the fabric.ib_ip nodes but no blocking switch (2nodes-dac) =
direct-attach copper: dac links between consecutive ranks.10g link to the pseudo-switch "lan" — the passive
10G switch is drawn UI-side (switchesSvg always appends it); it has no
HA state, ever. Don't add it to topo.switches."aux" glyphs (the fan).tgPos (rows y=74/386), tgCablePath (cable targets 210/266/290;
row-1 fabric = S-curve in the 346..386 corridor so the port approach is
vertical; swSvg ib ports 4+4 across top/bottom edges),
swSvg/fanSvg (boxes 210/290, fan cy=318), legend 526, TG_H 540.
Cable mode (legend toggle data-topobus, state.topoBus, persisted
sm121-topobus, folded into the rebuild sig): direct = bezier runs;
bus + bridges = tgCablePathBus — row-1 Manhattan runs (200g lane
y=366, 10g lane y=278; 10g detours around the fabric box via x=350/710)
with bridgesSvg hop arcs (semicircle over the 200g lane) where a 10g
vertical strictly crosses it — detour devices turn on the lane (a T), so
no hop. Row 0 and DAC are unchanged in both modes.x 14+10·min(Δ,2), top 12+18·min(Δ,2), bottom
12+5·min(Δ,2)) — nesting now runs 5 deep (4nodes ⊃ 6nodes ⊃ 8nodes ⊃
dgx58 ⊃ dgx56) and uncapped pads pushed the outer frame above y=0, where
the viewBox clipped it. Chip bands are quantized (round(y/16)) so
near-colliding floors stagger horizontally.rect.x+12, widths ≈ len*6.6+30).TG_HUES palette (10 hues, green/red reserved for up/down),
stable hash of the cluster name; a chip never shares its hue with a frame
containing it (rotate +1 until unique; containers sort first by area).topoSig changes (config edit);
topoTick flips classes in place (pled on/b/off, .unpowered when
ha === false, .live flows, port .on, frame .down = dashed+dim,
chip dot) so the CSS animations (dash flow, fan spin, LED pulse) keep
running. prefers-reduced-motion is covered by the global kill switch.state.topoGroupTitles.<script> from ui.html (python regex) →
node --check./tmp/topo_probe.cjs, recreate as needed): stubs $,
esc, state, wins, front, minimal document; evals only the
topo slice. Gotchas: the slice must be prefixed with // (the split
eats the comment start); const bindings don't leak from eval —
export them via globalThis.__x = {...} appended to the eval string;
the probe must assert no Infinity/NaN in output (a name-vs-object
membership bug once shipped as Infinity coords).deno run -A Playwright →
screenshot; dump the rendered #topo innerHTML — the file text is
not the DOM (innerHTML assignment happens at runtime).Geometry audit on the rendered HTML: counts, viewBox bounds, no
device overlaps, switches clear of devices, frames clear of the switch
corridor, no frame edge cuts, no chip overlaps (the whole point of
the floors), cable endpoints hit the right switch edges. Parse tags
attr-wise — browsers re-serialize attribute order, positional regexes
break; </title> inside a regex literal needs <\/.
4b. Contrast probe (/tmp/topo_contrast.mjs): open the stage page,
toggle data-theme, read getComputedStyle(...).fill/stroke + group
opacity, resolve against --panel (devices sit on the window panel,
not on --bg) and print WCAG ratios. Gotcha: .dev transitions
opacity/filter for .5s — reading computed style right after adding a
class returns the pre-transition value (filter: grayscale(0)); wait
~800ms. Reference ratios: powered outline/panel 5.3 dark / 3.6 light,
unpowered dashed outline 3.2 / 2.3. Never style with var(--border)
for outlines — on white it is ~1.4:1, i.e. invisible; use
color-mix(in srgb, var(--fg) 55%, transparent) (theme-safe) and a
theme-scoped --shade for drop-shadow.
cargo test ui_playwright_checks (fake fleet: 2 devs, 2 lan cables,
2 frames, 1 passive glyph; chip-click selector
#topo .fchip[data-cluster="fake2"]; hide all other windows before
clicking — earlier steps leave log/raw windows stacked over the map).
Routine: cargo fmt → make check → cargo install --path .
(ui.html is include_str!'d — a stale binary serves stale pages) →
commit → push. Parallel sessions may sweep your working-tree changes
into their commits — check git log --stat after pushing.