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
Phase 29 — The gateway dashboard page: animated pipes from config + live sockets
Source: TODO.md L3 — "Create a 'gateway' page that visualizes my incoming connections based on the gateway host's nginx and haproxy configs. I want it to show animated pipes with data coming in (IPs, ports, domains, etc.) and data going out (IPs, ports, domains, etc.)." (the dashboard half)
Story: n/a
Context: phase 28 (the haproxy/nginx/sockets agent sections, AGENT_VERSION 1.2.0, the gateway host rolled out), src/dashboards/config.py (Settings, _env, the nut_enabled/actions_allowed gate properties), src/dashboards/sources/disks.py (the "a source of its own reading the same agents" precedent — the disks:<host> job dials the same /snapshot), src/dashboards/poller.py (job loop, health records, stale/last-good), src/dashboards/app.py (build_hub — the job list wired once for production and the injected-store test path; the agent_tls_map the host:/disks: jobs share per host), src/dashboards/api.py (the snapshot/history route pairs, the 404 {"configured": false} gate that runs before any dial, _history_batch, the /api/summary additive blocks), src/dashboards/alerts.py (the "an unconfigured source never alerts" rule), src/dashboards/store.py (the sample(source, host, metric, res, ts) + rollup/retention story — invariant 9), src/dashboards/web/pages/ + static/ (the five dashboards, the five-tab masthead pinned by tests/test_api.py, app.js's initChrome badges, Dash.hero/DashCharts — no framework, no CDN, invariant 5), tests/test_api.py (the served-bytes pins), tests/test_config.py, tests/test_live_fleet.py (the read-only live suite).
Objective
The fifth dashboard page — /gateway — renders the gateway host's connection topology as animated pipes: for every configured frontend (haproxy bind, nginx listen, the stream 443) an inbound pipe (client → gateway: the port, the server_name domains, the live connection count and client addresses) and, fanning out from the gateway, one outbound pipe per configured target (haproxy server, nginx proxy_pass, the stream map entries: the target host:port, the live count). The page is a source of its own (gateway, its own poller job on the same /snapshot the host: job dials, its own interval/timeout/health/events), its counts are stored as gauges (invariant 9's rollup/retention story) and graphed from /api/gateway/history, and it keeps every invariant the fleet already keeps: unconfigured is 404 without a dial (invariant 3), a dead source ages instead of zeroing (invariant 4), no count is a rate (rule 2 — no estimator, no Δ<window>), no count carries a verdict (like net.*), and an old agent (1.1.0, no sections) reads as "predates gateway support" — dimmed, n/a, never red.
Dependencies
28_gateway_agent_proxy(todo) — the three agent sections on the wire and the gateway host running 1.2.0 (the live half of this phase's verification dials what 28 installed)27_mobile_tables_and_audit(todo) — the page's 360 px behaviour is audited by 27's script shape (the table, if any, scrolls inside its card)
Tasks
01_config_and_source.md—GATEWAY_HOSTinSettings+ the gate properties;sources/gateway.py: the config + sockets join into inbound/outbound pipes, gauges only, the three-way agent support02_wiring.md— thegatewaypoller job inbuild_hub(30 s), health, the alerts rules, the/api/summaryblock, the/api/configentry03_api_routes.md—/api/gateway/snapshot+/api/gateway/history: the configured gate before any dial (spy-pinned), history through_history_batch, unknown names as empty series04_page.md—/gateway+page-gateway.js: the animated pipes (hand-rolled SVG + CSS, no CDN,prefers-reduced-motion,aria-label), the live counts, the three degradation arms05_nav_and_summary.md— the sixth tab on every signed-in page (the pinned masthead pins updated + browser-measured), the/api/summarypin extended06_docs_and_verify.md— README/AGENTS (the page row, theGATEWAY_HOSTrow, the fleet fact), the browser screenshot pass against live data, the read-only live verification
Testing & Quality
- Unit:
tests/test_gateway_source.py(new — the join over fixture snapshots: an inbound join by listen port, the domain-target port-only join labelled "by port", the raw-IP exact join, the$var→mapjoin, the 1.1.0-predates arm, the never-answered stub arm);tests/test_config.py(GATEWAY_HOSTdefault, the gate property);tests/test_api.py(the route pins, the summary block, the nav pins) - Coverage: >90% on
sources/gateway.py+ the new config field - Real-browser check (invariant 10): task 05's measurement + task 06's screenshots against the live dev server
Documentation & Consistency
- Task 06 updates
AGENTS.md's page table (/gatewayrow), theconfig.pybullet (GATEWAY_HOST), and the metric-naming paragraph (gateway.*gauges);README.mdgains the page's how-to row. The invariant 9 story: thegateway.*series roll up raw → 1 m → 1 h like every other gauge.
Completion Criteria
uv run pytestgreen (the hermetic suite), coverage not down;uv run pytest --covholds the 94% floorGATEWAY_HOSTunset (or a name not inHOST_TARGETS) →/api/gateway/snapshotanswers404 {"configured": false}with no dial (spy-pinned) and the page says "not configured"- a 1.1.0-style snapshot (no sections) → the page's "agent predates gateway support" arm, dimmed, every number
n/a, the job stillok(the invariant-3 shape, pinned) - the gateway host (1.2.0, post-phase-28) → the page renders the six haproxy frontends, the nginx server blocks and the stream map, with live counts that match a direct
/proc/net/tcpread (task 06's verification) bash .agents/validate.shgreen- no behavior change in completed work: the nine existing sources read
okin./scripts/dev-server.sh status, the five existing dashboards render as before, the/api/summarypre-existing blocks are byte-identical in shape (thegatewayblock is additive)