Files
dashboards/.agents/phases/todo/29_gateway_dashboard_page/04_page.md
T
ducoterra 558a83d0be chore(agent): phase roadmap from TODO.md, 2 phases
Convert the gateway-visualisation TODO item into phases 28 (gateway_agent_proxy: agent sections for haproxy/nginx configs + live sockets, 1.2.0, capture, rollout) and 29 (gateway_dashboard_page: source, wiring, API, animated-pipe page, nav, docs). Clear TODO.md.

LLM-Generated: true
2026-10-21 16:24:01 -04:00

6.1 KiB

Task 04 — The /gateway page: animated pipes

Phase: 29_gateway_dashboard_page · Source: TODO.md:3 — "I want it to show animated pipes with data coming in (IPs, ports, domains, etc.) and data going out (IPs, ports, domains, etc.)." Story: n/a

Objective

The /gateway page renders the joined payload (task 01) as animated pipes: one inbound pipe per configured frontend — client → gateway — labelled with the listen port, the server_name domains (haproxy's L4 frontends label their port only), the live connection count, and up to a few client addresses; and, fanning out from the gateway node, one outbound pipe per configured target — gateway → target — labelled with the target host:port (and · by port on a by_port match), its live count, and its source (the haproxy backend / nginx location / stream-map key it came through). The animation is decorative at a fixed speed — moving dashes whose direction is the data flow — because a count is not a rate (rule 2): the page never claims a tok/s-shaped number from it. No count is coloured by a verdict (the net.* precedent: the stylesheet section carries no ok/warn/fail token), and the three degradation arms render their own sentence: unconfigured ("not configured — set GATEWAY_HOST"), a 1.1.0 agent ("agent predates gateway support — install 1.2.0", dimmed, n/a), a never-answered host (dimmed .stale, n/a, the row kept, invariant 4).

  • ASSUMPTION: the render is one card per source family (haproxy / nginx-https / nginx-stream / unmatched), each card a hand-rolled SVG: a left column of inbound pipe stubs, the gateway node mid-card (one rounded rect, its label the host name), a right column of outbound pipe stubs — the pipe is a <path> whose stroke-dasharray/stroke-dashoffset CSS animation runs only while the pipe has ≥ 1 live connection (a zero-connection pipe is a static faint line + the count 0, because animating a pipe nobody is using would be a shape claiming a flow the count denies — the sparkline's "a shape is not a verdict" rule applied to motion); prefers-reduced-motion: reduce renders every pipe static (no animation at all, the repo's standing rule).
  • ASSUMPTION: the labels are textContent-only (no innerHTML for anything upstream-provided — a config domain is upstream text; the DashCharts.escapeHtml idiom via the page's own escape), and every SVG pipe carries role="img" + an aria-label built from its inputs (the battery-glyph convention: the pipe carries information the markup must say in words) — e.g. "inbound pipe, port 4443, nextcloud.reeseapps.com, 3 connections".
  • ASSUMPTION: the page is the app's standing chrome — the masthead's sixth tab (task 05), the four #nav-status badges (the gateway job is the fifth dot, app.js's one-dot-per-job rule), the theme toggle, the window chips absent (the page's history is the count trend — one LineChart per card, the Charts disclosure's /system shape: collapsed, lazy-built, one /api/gateway/history request naming every series the card could draw, ariaLabel on the plot), and the page's single faint footnote: the by port join sentence (the only why-line the operator needs to read a count).

Work

  1. src/dashboards/web/pages/gateway.html — the page: the shared <head>/masthead (task 05's tab slot), the four badge container, the card grid (the four cards' skeletons with their empty-state sentences), the #nav-status, and no id the script does not reference (the orphan-id audit's shape).
  2. src/dashboards/web/static/page-gateway.js — the page script (the page-disks.js idiom): state keyed per card; renderPipes(card, pipes, direction) — the SVG builder (the DashCharts escape discipline, the aria-label per the assumption, the zero-connection static arm, the by port sub-label); the degradation arms (the unconfigured / predates / never-answered sentences, the .stale dim, n/a for every count); the Charts disclosure's one-history-request rule (the /system shape: collapsed default, lazy build, a cached response from another window re-fetched); the unmatched card (the "N other connections" sentence — the operator's fact that the gateway talks to something the configs do not name).
  3. src/dashboards/web/static/dash.css — the pipe rules: .pipe (the <path> stroke, the stroke-dasharray), @keyframes pipe-flow (the dashoffset drift), .pipe.still (the zero-connection arm), and the @media (prefers-reduced-motion: reduce) block naming both the animating and the still states (the phase-18 reduced-motion shape — naming only one leaves the other animating).
  4. tests/test_api.py — the served-bytes pins: the pipe @keyframes + the reduced-motion rule (both states, inside the file's one such block); the zero-connection .still arm; the page's absence of any ok|warn|fail token in the gateway card section (the no-verdict pin, the phase-06 GPU shape); the aria-label on every LineChart the page builds (the standing chart-audit pin, extended); the orphan-id audit over the served page; the CDN-reference absence (invariant 5).

Testing & Quality

  • Unit: the served-bytes pins above (the repo has no JS test runner — a .cjs browser pass is the behavioural proof, task 06)
  • Coverage: >90% on the new Python-side branches (the pins); the JS is pinned on served bytes + the browser pass

Completion Criteria

  • the page renders the four cards with their pipes, labels (port / domains / host:port / · by port / counts / clients), and the unmatched card — against a fixture-driven dev store
  • a zero-connection pipe is static (.still), a ≥ 1-connection pipe animates, prefers-reduced-motion renders all static (pinned + measured by task 06's browser pass)
  • the three degradation arms print their own sentence, dimmed where the invariant says, every count n/a (pinned)
  • no verdict token in the gateway card section; no CDN reference; every chart labelled (pinned)
  • uv run pytest green, uv run ruff check src tests and uv run pyright clean, node --check on the new script
  • no behavior change in completed work: the pre-existing pages render byte-identical