Jelajahi Sumber

skill: web-topology — working manual for tuning the dashboard topology map (layout anchors, frame/hue algorithms, probe ladder, gotchas)

Alexander Korolev 1 bulan lalu
induk
melakukan
e32d2f054f
1 mengubah file dengan 104 tambahan dan 0 penghapusan
  1. 104 0
      .agents/skills/web-topology/SKILL.md

+ 104 - 0
.agents/skills/web-topology/SKILL.md

@@ -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.