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 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),
swSvg/fanSvg (boxes 210/290, fan cy=318), legend 526, TG_H 540.14+10Δ, top 12+18Δ, bottom 12+5Δ — outer cluster sits furthest
out on both edges, so chips land on separate "floors" (y 18/36/54) and
bottom edges don't stack into one line.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).</title> inside a regex literal needs <\/.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).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.