|
@@ -0,0 +1,104 @@
|
|
|
|
|
+---
|
|
|
|
|
+name: web-topology
|
|
|
|
|
+description: Tune the sm121 web dashboard's topology map — the physical-layer visualization in the Topology window (2×4 GX10 grid, 200G/10G/DAC cables, HA glyphs, cluster frames). Use whenever adjusting the map's layout/geometry, colors and cluster frames, cable routing, device/switch/fan glyphs, HA state display, or its derivation rules and tests.
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+# Web Topology Map (the dashboard's physical layer)
|
|
|
|
|
+
|
|
|
|
|
+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.
|
|
|
|
|
+
|
|
|
|
|
+## Where things live
|
|
|
|
|
+
|
|
|
|
|
+- `src/web/ui.html` — everything visual:
|
|
|
|
|
+ - CSS block `/* --- topology map */` (all SVG styling; inline
|
|
|
|
|
+ `style="stroke:…" carries per-cluster hues, classes carry state)
|
|
|
|
|
+ - script section `// --- 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`, `renderTopo`
|
|
|
|
|
+ - `initManagedWindows` 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`).
|
|
|
|
|
+- AGENTS.md `sm121 web` blurb — keep in sync when semantics change.
|
|
|
|
|
+
|
|
|
|
|
+## Derivation rules (topo.rs — don't break these)
|
|
|
|
|
+
|
|
|
|
|
+- Nodes: unique names across clusters, sorted by the digits in the name
|
|
|
|
|
+ (`dgx10` > `dgx9`, not lexicographic). Grid slot = index in that list.
|
|
|
|
|
+- The cluster's **blocking** `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.
|
|
|
|
|
+- A cluster with `ib_ip` nodes but **no blocking switch** (`2nodes-dac`) =
|
|
|
|
|
+ direct-attach copper: `dac` links between consecutive ranks.
|
|
|
|
|
+- Every node gets a `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`.
|
|
|
|
|
+- Non-blocking extras that aren't node plugs = `"aux"` glyphs (the fan).
|
|
|
|
|
+
|
|
|
|
|
+## Layout & frame algorithm (ui.html)
|
|
|
|
|
+
|
|
|
|
|
+- Geometry anchors are hard-coded in **three places that must stay in
|
|
|
|
|
+ sync**: `tgPos` (rows y=74/386), `tgCablePath` (cable targets 210/266/290),
|
|
|
|
|
+ `swSvg`/`fanSvg` (boxes 210/290, fan cy=318), legend 526, `TG_H` 540.
|
|
|
|
|
+- Frames: clusters with **identical node sets merge into one rect** with
|
|
|
|
|
+ side-by-side chips (kills doubled frames + mutual-containment depth
|
|
|
|
|
+ inflation). Pads grow with region rank Δ = regionMaxDepth − depth:
|
|
|
|
|
+ x `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.
|
|
|
|
|
+- Render order: **all frame rects first, then all chips** — no frame edge
|
|
|
|
|
+ ever crosses a label. Chips x-stagger per floor band (cursor walks
|
|
|
|
|
+ `rect.x+12`, widths ≈ `len*6.6+30`).
|
|
|
|
|
+- Hues: `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).
|
|
|
|
|
+- State ticks: rebuild the SVG only when `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.
|
|
|
|
|
+- Flow semantics: 10g live = node reachable; 200g live = node occupied;
|
|
|
|
|
+ dac live = both endpoints occupied. Passive 10G glyph is never touched
|
|
|
|
|
+ by tick. Merged-frame tooltips come from `state.topoGroupTitles`.
|
|
|
|
|
+
|
|
|
|
|
+## Verification ladder (run all of it after visual changes)
|
|
|
|
|
+
|
|
|
|
|
+1. **Syntax**: extract the `<script>` from ui.html (python regex) →
|
|
|
|
|
+ `node --check`.
|
|
|
|
|
+2. **Logic probe** (`/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).
|
|
|
|
|
+3. **Stage + screenshot**: build a standalone page (extracted CSS + topo
|
|
|
|
|
+ slice + the real config.toml topology) → `deno run -A` Playwright →
|
|
|
|
|
+ screenshot; **dump the rendered `#topo` innerHTML** — the file text is
|
|
|
|
|
+ not the DOM (innerHTML assignment happens at runtime).
|
|
|
|
|
+4. **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 `<\/`.
|
|
|
|
|
+5. `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).
|
|
|
|
|
+6. 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.
|