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 28 — The agent reads the gateway's proxy configs and its 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 agent half)
Story: n/a
Context: agent/host_agent.py (the stdlib-only single-file agent, Collector._safe best-effort sections, snapshot(), the SCHEMA docstring, AGENT_VERSION 1.1.0, the --no-smart opt-out and the three-way availability precedent it sets), tests/test_agent.py (the in-process harness over a fake root tree, the real-/proc test, the smartctl-capture fixture pins), scripts/capture-fixtures.sh (the ssh-capture convention: plain command first, sudo -n fallback, refuse to overwrite good fixtures with unreadable output), tests/fixtures/ (the verbatim-capture ground-truth directory), the gateway host's live state verified 2026-10-21 (read-only): nginx and haproxy both active, /etc/haproxy/haproxy.cfg (6 frontend blocks — bind :::22 v4v6 et cetera — each with one default_backend; 6 backend blocks, each one server <name> <domain>:<port> check inter 5s), /etc/nginx/nginx.conf (a catch-all listen 80 / server_name _ block plus include /etc/nginx/conf.d/*.conf; and include /etc/nginx/stream.d/*.conf;), /etc/nginx/conf.d/http.conf (server blocks: listen 4443 ssl proxy_protocol / listen 8443 ssl, server_name <domain>, location / { proxy_pass http://<domain>:<port>; }), /etc/nginx/stream.d/stream.conf (a map $ssl_preread_server_name $targetBackend { … } block and proxy_pass $targetBackend with ssl_preread on), all three files world-readable and readable by the dashboards-agent user, /proc/net/tcp readable the same way. The dashboard side (source, wiring, page) is phase 29; this phase makes the data exist on the wire.
Objective
host_agent.py 1.2.0 serves three new additive snapshot sections on the gateway host — haproxy (frontends + binds, backends + server targets, the frontend→backend routing), nginx (server blocks with listen + server_name, upstream, proxy_pass including the $var-to-map join, map blocks) and sockets (LISTEN + ESTABLISHED from /proc/net/tcp{,6}) — each best-effort (a missing config degrades its own section, never the snapshot), --no-proxy opting out silently, the real configs captured verbatim as fixtures, the rollout onto the gateway host done, and the sections proven live over mTLS. Nothing changes for token-mode or pre-1.2.0 consumers: an old agent's snapshot is byte-identical in every old field.
Dependencies
27_mobile_tables_and_audit(todo) — not a code dependency (different files); ordered after it so the fleet rollout in task 05 does not race the mobile-audit's browser passes on the shared dev box15_agent_mtls/24_dev_mtls_rollout(complete) — the installed-bundle install path (deploy/install-agent.sh --cert-bundle), the--statusCA-only probe, and the gap-10 re-stage-first rule the rollout follows
Tasks
01_haproxy_section.md— the agent reads + parseshaproxy.cfginto the additivehaproxysection, three-way availability,--no-proxyopt-out, never fails the snapshot02_nginx_section.md— the agent reads the main config + one level ofinclude <glob>(bounded) into the additivenginxsection, with theproxy_pass $var→mapjoin03_socket_section.md— the agent parses/proc/net/tcp+/proc/net/tcp6(bounded) into the additivesocketssection: LISTEN + ESTABLISHED with local/remote ip:port04_version_and_capture.md—AGENT_VERSION1.2.0, the SCHEMA docstring,scripts/capture-gateway-config.sh(real configs →tests/fixtures/), the live probe that the sections answer on the gateway host05_rollout_gateway.md— re-stage/home/ops/agent-install/, install 1.2.0 on the gateway host, restart the unit, verify the sections over mTLS — read-only live verification, transcript indata/logs/
Testing & Quality
- Unit:
tests/test_agent.py— every new section over a fakeroottree (present config → parsed shape; missing file →available: false+ exactly one errors line; unreadable →null+ one errors line;--no-proxy→ sections absent and no errors line; a corrupt config degrades its own section and the snapshot still succeeds with the other sections intact) - Fixture:
tests/fixtures/gateway_haproxy.cfg+gateway_nginx_*.conf+gateway_proc_net_tcp{,6}.txtare verbatim captures from the gateway host (invariant 7); the parser tests run against them, so a config drift fails a test before a dashboard does - Coverage: >90% on the new/modified code (the three collectors + the parsers)
- Live (read-only, invariant 10): task 04's probe over the mTLS dial, and task 05's post-restart verification with its transcript
Documentation & Consistency
- The final pass (task 05) updates
AGENTS.md's agent bullet (the new sections,AGENT_VERSION1.2.0, the--no-proxyline) and theSCHEMAdocstring is the self-documenting half on the wire.README.mdis checked for any agent-version sentence.
Completion Criteria
uv run pytestgreen (the hermetic suite, no network), coverage not down- a 1.2.0 agent over a captured-config root tree answers all three sections; over a tree without them it answers
available: false/nullwith one errors line each and the snapshot's other sections are intact AGENT_VERSIONis 1.2.0 and the SCHEMA docstring names the three new sections- the fixture files are verbatim captures (the capture script's refusal-to-overwrite-with-unreadable pin holds)
- the gateway host runs 1.2.0 (
systemctl is-active dashboards-agent→active, the file'sAGENT_VERSIONreads 1.2.0) and the sections answer over mTLS — transcript indata/logs/ bash .agents/validate.shgreen- no behavior change in completed work: a 1.2.0 agent's snapshot is identical to 1.1.0's in every pre-existing field for a non-gateway host (the new keys are the only difference, and
--no-proxyremoves even those)