Phase 29 completion pass is done. Final state:
**Verification (all re-run this pass, no task files touched)**
- `uv run pytest` → **1746 passed, 16 skipped**; `uv run pytest --cov` → **99.05 %** (gate 94 %); `bash .agents/validate.sh` → **rc=0** (ruff clean, pyright `0 errors, 0 warnings`, `node --check` clean); `uv run pytest -m live --live` → **12 passed**.
- Live, read-only: dev server **10/10 sources `ok`** (incl. `gateway`); `data/logs/29-task06-live.cjs` → **49 PASS / 0 FAIL** with per-port inbound counts reconciled against a direct `/proc/net/tcp{,6}` read (divergence 0); `data/logs/verify-29-task06-predates.py` → 0 failed (1.1.0 arm renders `WHY_PREDATES`, no pipes, every tally `n/a`, job still `ok`); phase-27 mobile audit re-run over all nine served pages → **371 PASS / 0 FAIL / 0 FINDING** (`data/logs/29-final-mobile-audit.out`).
- Criteria: (1) unconfigured `GATEWAY_HOST` → `404 {configured:false}` with dial spies + `#gate-empty` sentence ✅; (2) 1.1.0/`--no-proxy` → `supported:false`, no `0`s, never red ✅; (3) live gateway 1.2.0 → pipes animate, totals reconcile ✅; (4) no `severity`/verdict colour anywhere, validate green ✅; (5) docs updated + gap 13 logged ✅; (6) the five existing dashboards, jobs and `/api/summary` unchanged (live control leg) ✅.
**Docs (consistency pass, incl. pre-existing drift)**
- `README.md`: architecture diagram now lists `sources/gateway.py`, the 30 s gateway job and the third `/snapshot` reader; `GATEWAY_HOST` row says *a name, never a URL*; the `POLL_MS_*` row states `/gateway`'s fixed 15 s browser poll; "nine pollers" → per-source cadences; phase-28 "until phase 29" wording → current state; new Testing paragraph for `tests/test_gateway_source.py` (join pinned from the captures, reconciliation identity, `getaddrinfo` spy).
- `AGENTS.md`: phase-28 stop-point wording replaced with the shipped phase-29 state; suite/coverage figures → 1746 / 16 skipped / 99.08 %→99.05 % band and `~99 %`; window-chips paragraph records that `/gateway` has **no** chips (fixed 24 h); new testing-policy bullet for the join; stale page counts fixed (seven → eight tabbed pages / `MARKUP_PAGES`, clock-order and badge-count transcripts annotated); gap 12 now records the phase-29 nine-page audit re-run.
- `tests/test_api.py`: comment/docstring-only fixes for the "seven pages" wording that phase 29 made eight.
**Deviations / notes**
- Nothing implemented: all six task files were already in `complete/`, so this run was the final verification + docs pass; no git index or phase file was touched.
- Coverage wobbles 99.05–99.08 % between runs on a pre-existing timing-dependent line (`collectors.py` `/slots`-alias `except`/`break`), not a phase-29 path (`sources/gateway.py` is 100 %).
- `uv pip list --outdated` still lists upgrades; dependencies did not move in this phase, so they were left alone rather than upgraded mid-verification.
- Historical browser-transcript counts (phase 18/20/25 script runs) were left as recorded history and annotated instead of rewritten.
**Next phase:** none — `.agents/phases/todo/` is empty after 29; further work needs a new phase authored first.
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)