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
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>whosestroke-dasharray/stroke-dashoffsetCSS animation runs only while the pipe has ≥ 1 live connection (a zero-connection pipe is a static faint line + the count0, 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: reducerenders every pipe static (no animation at all, the repo's standing rule). - ASSUMPTION: the labels are
textContent-only (noinnerHTMLfor anything upstream-provided — a config domain is upstream text; theDashCharts.escapeHtmlidiom via the page's own escape), and every SVG pipe carriesrole="img"+ anaria-labelbuilt 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-statusbadges (thegatewayjob 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 — oneLineChartper card, theChartsdisclosure's/systemshape: collapsed, lazy-built, one/api/gateway/historyrequest naming every series the card could draw,ariaLabelon the plot), and the page's singlefaintfootnote: theby portjoin sentence (the only why-line the operator needs to read a count).
Work
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 noidthe script does not reference (the orphan-idaudit's shape).src/dashboards/web/static/page-gateway.js— the page script (thepage-disks.jsidiom):statekeyed per card;renderPipes(card, pipes, direction)— the SVG builder (theDashChartsescape discipline, thearia-labelper the assumption, the zero-connection static arm, theby portsub-label); the degradation arms (the unconfigured / predates / never-answered sentences, the.staledim,n/afor every count); theChartsdisclosure's one-history-request rule (the/systemshape: collapsed default, lazy build, a cached response from another window re-fetched); theunmatchedcard (the "N other connections" sentence — the operator's fact that the gateway talks to something the configs do not name).src/dashboards/web/static/dash.css— the pipe rules:.pipe(the<path>stroke, thestroke-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).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.stillarm; the page's absence of anyok|warn|failtoken in the gateway card section (the no-verdict pin, the phase-06 GPU shape); thearia-labelon everyLineChartthe page builds (the standing chart-audit pin, extended); the orphan-idaudit 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
.cjsbrowser 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 theunmatchedcard — against a fixture-driven dev store - a zero-connection pipe is static (
.still), a ≥ 1-connection pipe animates,prefers-reduced-motionrenders 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 pytestgreen,uv run ruff check src testsanduv run pyrightclean,node --checkon the new script- no behavior change in completed work: the pre-existing pages render byte-identical