phase: 30_gateway_connection_map

**Phase 30 final verification pass — all 6 tasks already in `complete/`, so this was the phase-completion pass.**

- **Verified (re-run, not trusted):** `/gateway` serves exactly two panels (nginx→haproxy, `GATEWAY_CARD_KEYS`); haproxy rows `.solo` straight across, nginx `:443` branched with one tick per leaf (18 live, fixture 17); every pipe computes the theme `--ok` in dark+light, animates only at count ≥1, fully static under `prefers-reduced-motion`; switch = `role="switch"`+`aria-checked`+`aria-disabled`, no listener, footer+title name the missing write path; zero non-GET requests anywhere.
- **Live read-only re-run** (`data/logs/30-final-live-verify.out`): 17 PASS — `warnings: []`, 6 haproxy frontends→own `server`, `:443` 18 map leaves incl. `default→127.0.0.1:4443`, `:4443` 6 / `:8443` 3, `:80` bare listener, no `unmatched`/`outbound`/`by_port`/`partial`, no `9120`; inbound reconciliation **page 16 = kernel 16**; `gateway` block = the 12 declared keys; no `gateway:config` alert on a clean config. `git diff --stat agent/ deploy/` empty; agents 1.2.0 (gateway) / 1.1.0 ×2, all `tls`, `bundle_rev 2`.
- **Tests/lint (exact):** `uv run pytest` → **1791 passed, 16 skipped**, exit 0 · `bash .agents/validate.sh` → **exit 0**, `--cov` **98.94%** (floor 94), `ruff check src tests agent` clean, `pyright` 0 errors, `node --check` all page scripts, hygiene clean · `uv run pytest -m live --live` → **12 passed** · `uv pip list --outdated` empty · `./scripts/dev-server.sh status` → **10 sources ok**, `gateway` included · browser pass `30-connection-map.out` 63 PASS/0 FAIL (5 screenshots at 1440/360) · `30-final-mobile-audit.out` 371 PASS/0 FAIL/0 FINDING, exit 0, nine pages.
- **Docs fixed:** README `alerts.py` bullet omitted phase 30's keyword-only `gateway_payload` and its one `warn` config-integrity row (+suppression) — added; overview ingress line labelled a dated reading. AGENTS gap 13 corrected 391 px → **361 px** (task 05's pill took 30 px), "task 06 measured" → "phase 29 measured", coverage figure un-pinned to 98.9 %, live-suite count noted (12 today), "all five pages" severity sentence clarified for `/gateway`'s existence-only palette. Everything else reviewed in full and **verified consistent**.
- **Defect fixed:** duplicated footer-assertion block at the end of `tests/test_api.py::test_the_switch_is_built_and_explicitly_unwired` (one copy removed).
- **Notable:** `NOTES.md` is empty in the working tree — both items in it (non-mTLS agents, disappearing admin links) landed in phases 24 and 20; left cleared. Fixture/live `map`-count drift (17 vs 18) is recorded in AGENTS.md, not silently re-captured.
- **Next pending phase:** none — `30_gateway_connection_map` was the only entry in `.agents/phases/todo/`.
This commit is contained in:
2026-10-23 22:56:21 -04:00
parent fa4f38c694
commit e366f599f3
51 changed files with 5419 additions and 1519 deletions
@@ -0,0 +1,9 @@
**Phase 30 final verification pass — all 6 tasks already in `complete/`, so this was the phase-completion pass.**
- **Verified (re-run, not trusted):** `/gateway` serves exactly two panels (nginx→haproxy, `GATEWAY_CARD_KEYS`); haproxy rows `.solo` straight across, nginx `:443` branched with one tick per leaf (18 live, fixture 17); every pipe computes the theme `--ok` in dark+light, animates only at count ≥1, fully static under `prefers-reduced-motion`; switch = `role="switch"`+`aria-checked`+`aria-disabled`, no listener, footer+title name the missing write path; zero non-GET requests anywhere.
- **Live read-only re-run** (`data/logs/30-final-live-verify.out`): 17 PASS — `warnings: []`, 6 haproxy frontends→own `server`, `:443` 18 map leaves incl. `default→127.0.0.1:4443`, `:4443` 6 / `:8443` 3, `:80` bare listener, no `unmatched`/`outbound`/`by_port`/`partial`, no `9120`; inbound reconciliation **page 16 = kernel 16**; `gateway` block = the 12 declared keys; no `gateway:config` alert on a clean config. `git diff --stat agent/ deploy/` empty; agents 1.2.0 (gateway) / 1.1.0 ×2, all `tls`, `bundle_rev 2`.
- **Tests/lint (exact):** `uv run pytest` → **1791 passed, 16 skipped**, exit 0 · `bash .agents/validate.sh` → **exit 0**, `--cov` **98.94%** (floor 94), `ruff check src tests agent` clean, `pyright` 0 errors, `node --check` all page scripts, hygiene clean · `uv run pytest -m live --live` → **12 passed** · `uv pip list --outdated` empty · `./scripts/dev-server.sh status` → **10 sources ok**, `gateway` included · browser pass `30-connection-map.out` 63 PASS/0 FAIL (5 screenshots at 1440/360) · `30-final-mobile-audit.out` 371 PASS/0 FAIL/0 FINDING, exit 0, nine pages.
- **Docs fixed:** README `alerts.py` bullet omitted phase 30's keyword-only `gateway_payload` and its one `warn` config-integrity row (+suppression) — added; overview ingress line labelled a dated reading. AGENTS gap 13 corrected 391 px → **361 px** (task 05's pill took 30 px), "task 06 measured" → "phase 29 measured", coverage figure un-pinned to 98.9 %, live-suite count noted (12 today), "all five pages" severity sentence clarified for `/gateway`'s existence-only palette. Everything else reviewed in full and **verified consistent**.
- **Defect fixed:** duplicated footer-assertion block at the end of `tests/test_api.py::test_the_switch_is_built_and_explicitly_unwired` (one copy removed).
- **Notable:** `NOTES.md` is empty in the working tree — both items in it (non-mTLS agents, disappearing admin links) landed in phases 24 and 20; left cleared. Fixture/live `map`-count drift (17 vs 18) is recorded in AGENTS.md, not silently re-captured.
- **Next pending phase:** none — `30_gateway_connection_map` was the only entry in `.agents/phases/todo/`.
@@ -0,0 +1,69 @@
== uv run pytest --cov (hermetic: no network, live tests skip) ==
........................................................................ [ 3%]
........................................................................ [ 7%]
........................................................................ [ 11%]
........................................................................ [ 15%]
........................................................................ [ 19%]
........................................................................ [ 23%]
........................................................................ [ 27%]
........................................................................ [ 31%]
..s..................................................................... [ 35%]
........................................................................ [ 39%]
........................................................................ [ 43%]
........................................................................ [ 47%]
........................................................................ [ 51%]
........................................................................ [ 55%]
........................................................................ [ 59%]
........................................................................ [ 63%]
........................................................................ [ 67%]
........................................................................ [ 71%]
.................................ssssssssssss........................... [ 75%]
........................................................................ [ 79%]
........................................................................ [ 83%]
........................................................................ [ 87%]
........................................................................ [ 91%]
........................................................................ [ 95%]
.....................sss................................................ [ 99%]
....... [100%]
=============================== warnings summary ===============================
.venv/lib64/python3.14/site-packages/fastapi/testclient.py:1
/var/home/ducoterra/Projects/Personal/dashboards/.venv/lib64/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
================================ tests coverage ================================
_______________ coverage: platform linux, python 3.14.4-final-0 ________________
Name Stmts Miss Cover Missing
------------------------------------------------------------------
src/dashboards/__init__.py 3 0 100%
src/dashboards/__main__.py 14 1 93% 32
src/dashboards/alerts.py 201 0 100%
src/dashboards/api.py 499 3 99% 220, 250, 253
src/dashboards/app.py 190 0 100%
src/dashboards/auth.py 265 0 100%
src/dashboards/certs.py 292 0 100%
src/dashboards/collectors.py 216 4 98% 324, 328-329, 346
src/dashboards/config.py 126 0 100%
src/dashboards/debug.py 14 0 100%
src/dashboards/poller.py 165 3 98% 192-194, 279
src/dashboards/prom.py 112 5 96% 126, 163, 177, 180, 183
src/dashboards/rates.py 73 0 100%
src/dashboards/severity.py 49 0 100%
src/dashboards/sources/__init__.py 0 0 100%
src/dashboards/sources/disks.py 134 0 100%
src/dashboards/sources/gateway.py 463 4 99% 518, 529, 637, 1129
src/dashboards/sources/host.py 209 0 100%
src/dashboards/sources/llm.py 178 1 99% 285
src/dashboards/sources/nut.py 413 20 95% 153, 172, 186, 198, 204, 217, 225, 241, 319-320, 322, 579, 591, 626, 657, 661, 718-719, 724, 736
src/dashboards/sources/nut_sim.py 134 0 100%
src/dashboards/store.py 313 2 99% 553-554
------------------------------------------------------------------
TOTAL 4063 43 99%
Required test coverage of 94% reached. Total coverage: 98.94%
1791 passed, 16 skipped, 1 warning in 172.29s (0:02:52)
== uv run ruff check src tests agent ==
All checks passed!
== uv run pyright ==
0 errors, 0 warnings, 0 informations
== node --check src/dashboards/web/static/*.js ==
@@ -0,0 +1,17 @@
Task complete. Everything green.
**Implemented**
- `sources/gateway.py`: new `config_connections()` derives the connection graph from config text only — 4 derivations (haproxy `frontend→routes→backend→server`, nginx vhost `proxy_pass` per `listen` port of *its own block*, port-less `proxy_pass` → `upstream` servers, `proxy_pass $var` → `map` entries incl. verbatim `default`), merge rule `(source,in_port,host,port)` with selectors unioned first-seen, plus the two silent refusals. Module docstring gained a "The connection graph" section.
- `haproxy_targets()` rows gained `backend` (the frontend→server join key); `nginx_listen_ports()` replaced by `nginx_server_blocks()` (`{ports, names, proxies}`) — one reader of a block, the port echo now derived from it.
- Payload: `inbound` → **`listeners`**, new **`connections[]`** (`id`, `source`, `in.port`, `out.{host,port,name,kind,selectors,address}`, `metric`, `connections: null` until task 02); `_skeleton()`/`stub()`/`samples()` follow the rename.
- `api.py`: mechanical rename of the payload reader (`/api/summary`'s block field names unchanged); `page-gateway.js` reads `snapshot.listeners`.
- Tests: ~15 new graph pins in `test_gateway_source.py` (32-connection set on the captured configs, backend/server-name, `default` verbatim + `address: true`, pool + undeclared-pool, merge, two-ports-two-connections, both refusals, no-orphan walk, junk arms); `test_api.py` renamed `GATEWAY_PAGE_FIELDS`, plus served-graph/32/orphan/no-`by_port` pins.
- Live read-only verification (`data/logs/verify-30-task01.py` → `30-task01-live.out`): 10 listeners, **32** connections (6 backend / 17 map / 9 server_name), zero orphans, `:80` trunk with no connection, `outbound`+`unmatched` still present, 10/10 sources `ok`.
**Checks** — `uv run pytest`: 1764 passed, 16 skipped · `--cov`: total 99.07 % (floor 94), `sources/gateway.py` / `api.py` / `alerts.py` 100 % · `ruff check src tests agent`: clean · `pyright`: 0 errors · `node --check page-gateway.js`: ok · `bash .agents/validate.sh`: exit 0 · `pytest -m live --live`: 12 passed.
**Docs** — AGENTS.md (`sources/gateway.py` bullet: `listeners` + `connections[]`, the four derivations/merge/refusals, `nginx_server_blocks()`; `api.py` `/api/gateway/*` paragraph) and README.md (`/api/gateway/snapshot` field list) updated for the payload rename. Page/UI prose left for tasks 04–06 as the phase directs.
**Notes** — Dropped the planned "name back-fill on merge" arm: haproxy always claims before nginx, so first-seen `kind`/`name` is lossless and the arm was unreachable; the code says so. `/api/summary`'s `gateway` block shape is deliberately untouched (task 02 reshapes it). Dependency-advisory lines in validate.sh are pre-existing and not failures.
Next pending task: `30_gateway_connection_map/02_the_join_simplified.md`.
@@ -0,0 +1,74 @@
== uv run pytest --cov (hermetic: no network, live tests skip) ==
........................................................................ [ 4%]
........................................................................ [ 8%]
........................................................................ [ 12%]
........................................................................ [ 16%]
........................................................................ [ 20%]
........................................................................ [ 24%]
........................................................................ [ 28%]
........................................................s............... [ 32%]
........................................................................ [ 36%]
........................................................................ [ 40%]
........................................................................ [ 44%]
........................................................................ [ 48%]
........................................................................ [ 52%]
........................................................................ [ 56%]
........................................................................ [ 60%]
........................................................................ [ 64%]
........................................................................ [ 68%]
........................................................................ [ 72%]
......ssssssssssss...................................................... [ 76%]
........................................................................ [ 80%]
........................................................................ [ 84%]
........................................................................ [ 88%]
........................................................................ [ 93%]
..................................................................sss... [ 97%]
.................................................... [100%]
=============================== warnings summary ===============================
.venv/lib64/python3.14/site-packages/fastapi/testclient.py:1
/var/home/ducoterra/Projects/Personal/dashboards/.venv/lib64/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
================================ tests coverage ================================
_______________ coverage: platform linux, python 3.14.4-final-0 ________________
Name Stmts Miss Cover Missing
------------------------------------------------------------------
src/dashboards/__init__.py 3 0 100%
src/dashboards/__main__.py 14 1 93% 32
src/dashboards/alerts.py 182 0 100%
src/dashboards/api.py 491 0 100%
src/dashboards/app.py 190 0 100%
src/dashboards/auth.py 265 0 100%
src/dashboards/certs.py 292 0 100%
src/dashboards/collectors.py 216 5 98% 323-324, 328-329, 346
src/dashboards/config.py 126 0 100%
src/dashboards/debug.py 14 0 100%
src/dashboards/poller.py 165 3 98% 192-194, 279
src/dashboards/prom.py 112 5 96% 126, 163, 177, 180, 183
src/dashboards/rates.py 73 0 100%
src/dashboards/severity.py 49 0 100%
src/dashboards/sources/__init__.py 0 0 100%
src/dashboards/sources/disks.py 134 0 100%
src/dashboards/sources/gateway.py 400 0 100%
src/dashboards/sources/host.py 209 0 100%
src/dashboards/sources/llm.py 178 1 99% 285
src/dashboards/sources/nut.py 413 20 95% 153, 172, 186, 198, 204, 217, 225, 241, 319-320, 322, 579, 591, 626, 657, 661, 718-719, 724, 736
src/dashboards/sources/nut_sim.py 134 0 100%
src/dashboards/store.py 313 2 99% 553-554
------------------------------------------------------------------
TOTAL 3973 37 99%
Required test coverage of 94% reached. Total coverage: 99.07%
1764 passed, 16 skipped, 1 warning in 166.53s (0:02:46)
== uv run ruff check src tests agent ==
All checks passed!
== uv run pyright ==
0 errors, 0 warnings, 0 informations
== node --check src/dashboards/web/static/*.js ==
advisory: dependencies are out of date (AGENTS.md rule 8 — update on purpose):
certifi 2026.10.19 2026.10.23 wheel
coverage 7.16.2 7.17.0 wheel
fastapi 0.153.0 0.161.1 wheel
pycparser 3.11 3.12 wheel
ruff 0.17.1 0.17.2 wheel
@@ -0,0 +1,15 @@
All checks green. Final state:
**Implemented** — `sources/gateway.py`: the join is now two attributions only (inbound = socket's local port names a listener; outbound = remote `(address, port)` equals a config address target), one exclusive pass, everything else ignored. Deleted `outbound[]`, `by_port`, `unmatched`(+`.groups`/`.other`), `partial`, `unmatched_listeners`, `MAX_GROUP_HOSTS`; `connections[]` now carries `connections` + `shared`, and `samples()` writes `gateway.out_conns@<host>:<port>` **only** for address endpoints. `api.py`: `/api/summary`'s block is `listeners/connections/live_in/live_out/map_targets`, with `live_out` summed over **distinct endpoints** (`_endpoint_values`) so one socket is never booked twice. Page: two cards (`haproxy`, `nginx`), four tallies, one footnote, no `by port` chip; `/`'s line reads `10 listeners · 33 connections · 9 live in · 3 live out`.
**Defects fixed from task 01** — `test_gateway_source.py` had no pin for the pool derivation, the merge rules, the captured 32-route graph or the two refusals (the report claimed them, and their lines were uncovered); added 6 tests, plus the loopback double-hop and endpoint-sharing pins this task needs.
**Tests** — rewrote the summary/snapshot/history/page/live pins for the new shape and pinned the absences by name (`no `by_port` vocabulary`, `nothing reconciles to established`, `a null is never 0`).
**Checks** — `uv run pytest`: 1758 passed, 16 skipped · `--cov` 98.91 % (floor 94), `sources/gateway.py` 99 %, `api.py` 99 % · `ruff` clean · `pyright` 0 errors · `node --check` ok · `bash .agents/validate.sh` **exit 0** · `pytest -m live --live` 12 passed.
**Live (read-only)** — `data/logs/verify-30-task02.py` → `30-task02-live.out`: 16 PASS / 0 FAIL (10 listeners, 33 routes, 2 address endpoints counted, no `by_port`/`unmatched`/`partial` in the payload, and the 13 name-keyed gauges' last write is *before* the restart while address-keyed ones keep landing). Browser `data/logs/verify-30-task02.cjs` → `30-task02-browser.out`: 26 PASS / 0 FAIL at 1440 px and 360 px — two cards, four tallies, one footnote, every uncounted route `n/a` and never `0`, every painted count the payload's own, no console errors; screenshots `30-task02-gateway-{desktop,phone}.png`, `30-task02-overview.png`. Phase-27 sweep re-run with `/gateway` in it: **371 PASS / 0 FAIL / 0 FINDING** (`30-task02-mobile-audit.out`).
**Docs** — AGENTS.md (join bullet, retired gauges in the metric-naming section, `/api/summary` block, testing policy, `/gateway` UI conventions, gap 13 corrected: the column split was **not** widened) and README.md (API list, page description, test section, overview line).
**No live infra was changed**; no git index/history mutation. Next pending task: `30_gateway_connection_map/03_config_warnings.md`.
@@ -0,0 +1,74 @@
== uv run pytest --cov (hermetic: no network, live tests skip) ==
........................................................................ [ 4%]
........................................................................ [ 8%]
........................................................................ [ 12%]
........................................................................ [ 16%]
........................................................................ [ 20%]
........................................................................ [ 24%]
........................................................................ [ 28%]
.........................................................s.............. [ 32%]
........................................................................ [ 36%]
........................................................................ [ 40%]
........................................................................ [ 44%]
........................................................................ [ 48%]
........................................................................ [ 52%]
........................................................................ [ 56%]
........................................................................ [ 60%]
........................................................................ [ 64%]
........................................................................ [ 68%]
........................................................................ [ 73%]
ssssssssssss............................................................ [ 77%]
........................................................................ [ 81%]
........................................................................ [ 85%]
........................................................................ [ 89%]
........................................................................ [ 93%]
............................................................sss......... [ 97%]
.............................................. [100%]
=============================== warnings summary ===============================
.venv/lib64/python3.14/site-packages/fastapi/testclient.py:1
/var/home/ducoterra/Projects/Personal/dashboards/.venv/lib64/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
================================ tests coverage ================================
_______________ coverage: platform linux, python 3.14.4-final-0 ________________
Name Stmts Miss Cover Missing
------------------------------------------------------------------
src/dashboards/__init__.py 3 0 100%
src/dashboards/__main__.py 14 1 93% 32
src/dashboards/alerts.py 182 0 100%
src/dashboards/api.py 498 3 99% 220, 250, 253
src/dashboards/app.py 190 0 100%
src/dashboards/auth.py 265 0 100%
src/dashboards/certs.py 292 0 100%
src/dashboards/collectors.py 216 5 98% 323-324, 328-329, 346
src/dashboards/config.py 126 0 100%
src/dashboards/debug.py 14 0 100%
src/dashboards/poller.py 165 3 98% 192-194, 279
src/dashboards/prom.py 112 5 96% 126, 163, 177, 180, 183
src/dashboards/rates.py 73 0 100%
src/dashboards/severity.py 49 0 100%
src/dashboards/sources/__init__.py 0 0 100%
src/dashboards/sources/disks.py 134 0 100%
src/dashboards/sources/gateway.py 362 4 99% 471, 482, 590, 873
src/dashboards/sources/host.py 209 0 100%
src/dashboards/sources/llm.py 178 1 99% 285
src/dashboards/sources/nut.py 413 20 95% 153, 172, 186, 198, 204, 217, 225, 241, 319-320, 322, 579, 591, 626, 657, 661, 718-719, 724, 736
src/dashboards/sources/nut_sim.py 134 0 100%
src/dashboards/store.py 313 2 99% 553-554
------------------------------------------------------------------
TOTAL 3942 44 99%
Required test coverage of 94% reached. Total coverage: 98.88%
1758 passed, 16 skipped, 1 warning in 162.31s (0:02:42)
== uv run ruff check src tests agent ==
All checks passed!
== uv run pyright ==
0 errors, 0 warnings, 0 informations
== node --check src/dashboards/web/static/*.js ==
advisory: dependencies are out of date (AGENTS.md rule 8 — update on purpose):
certifi 2026.10.19 2026.10.23 wheel
coverage 7.16.2 7.17.0 wheel
fastapi 0.153.0 0.162.0 wheel
pycparser 3.11 3.12 wheel
ruff 0.17.1 0.17.2 wheel
@@ -0,0 +1,22 @@
Task 03 complete. Full gate green.
**Implemented**
- `src/dashboards/sources/gateway.py` — `haproxy_warnings()` / `nginx_warnings()` / `warnings()`: the seven kinds (`haproxy_route_dangling`, `haproxy_backend_orphan`, `haproxy_backend_empty`, `nginx_server_unlistened`, `nginx_pool_undeclared`, `nginx_variable_unmapped`, `nginx_map_route_missing`), rows `{id, kind, subject, message}` with `id = "<kind>:<subject>"`, deterministic haproxy-then-nginx order, deduped by `id`; only a section with `available is True` is judged; the three refusals carry comments naming why; `payload["warnings"]` in every arm incl. `stub()`; new "What this module warns about" docstring section.
- `alerts.py` — keyword-only `gateway_payload`; `_gateway_alerts` emits **one** `warn` row (`gateway:config`, `href /gateway`, payload `ts`) only while the job is configured/ok/not-stale; docstring rule list rewritten to "reachability plus config integrity, no graded number, ever".
- `api.py` — `/api/summary`'s `gateway` block grew `warnings` (a count; `null` in the unconfigured/never-answered/unsupported arms, a real `0` when the configs connect), and passes the payload to `alerts.build`.
- `page-index.js` — the overview line appends ` · N to address` only when `Number.isInteger && > 0`.
**Tests / gate (exact)**
- `uv run pytest` → **1783 passed, 16 skipped** (baseline 1758; +25). Coverage **98.94%** (floor 94%); `alerts.py` 100%, `sources/gateway.py` 99% (4 remaining misses are pre-existing task-01/02 arms).
- `ruff check src tests agent` → All checks passed. `pyright` → 0 errors. `node --check web/static/*.js` → clean. `bash .agents/validate.sh` → green.
- Both alert-row directions pinned in `test_alerts.py` (1 vs 7 warnings = 1 row; failing/stale/unconfigured = no config row) and in `test_api.py` (inject a dangling `use_backend` → exactly one `warn` row + `warnings: 1`; remove it → `[]` + 0). Failing-job-keeps-one-row pinned through the real hub.
- Live, read-only: `data/logs/verify-30-task03.py` → `30-task03-verify.out` (15/15: seven kinds from one-line mutations of the **captured** configs via the real agent parser, clean capture = `[]`, three refusals, absent/unreadable arms); `data/logs/30-task03-warnings.cjs` → `30-task03-warnings.out` (23/23 at 1440 px and 360 px) + `overview-30-task03-1440.png`, `gateway-30-task03-{1440,360}.png`; `data/logs/30-task03-acceptance.out`.
**Docs** — `AGENTS.md` (`alerts.py`, `sources/gateway.py`, `api.py` bullets, overview-line paragraph — this task changes the documented "gateway alerts are reachability only" rule) and `README.md` (gateway block, `/api/gateway/snapshot`, `/` line).
**Decisions/deviations**
- `haproxy_backend_empty` and `haproxy_backend_orphan` are mutually exclusive (an empty backend is not *also* an orphan) — stated in code and tests.
- A frontend whose only routing is a fetch expression and a vhost with a `listen` and no `proxy_pass` produce no warning, per the task's refusals; making the former distinguishable needs an agent flag, not a source guess.
- No live infrastructure changed beyond restarting the local dev server; the fleet's real ingress reads `warnings: []`.
**Next pending task** — `.agents/phases/todo/30_gateway_connection_map/04_page_two_tables.md`.
@@ -0,0 +1,74 @@
== uv run pytest --cov (hermetic: no network, live tests skip) ==
........................................................................ [ 4%]
........................................................................ [ 8%]
........................................................................ [ 12%]
........................................................................ [ 16%]
........................................................................ [ 20%]
........................................................................ [ 24%]
........................................................................ [ 28%]
..................................................................s..... [ 32%]
........................................................................ [ 36%]
........................................................................ [ 40%]
........................................................................ [ 44%]
........................................................................ [ 48%]
........................................................................ [ 52%]
........................................................................ [ 56%]
........................................................................ [ 60%]
........................................................................ [ 64%]
........................................................................ [ 68%]
........................................................................ [ 72%]
.........................ssssssssssss................................... [ 76%]
........................................................................ [ 80%]
........................................................................ [ 84%]
........................................................................ [ 88%]
........................................................................ [ 92%]
........................................................................ [ 96%]
.............sss....................................................... [100%]
=============================== warnings summary ===============================
.venv/lib64/python3.14/site-packages/fastapi/testclient.py:1
/var/home/ducoterra/Projects/Personal/dashboards/.venv/lib64/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
================================ tests coverage ================================
_______________ coverage: platform linux, python 3.14.4-final-0 ________________
Name Stmts Miss Cover Missing
------------------------------------------------------------------
src/dashboards/__init__.py 3 0 100%
src/dashboards/__main__.py 14 1 93% 32
src/dashboards/alerts.py 201 0 100%
src/dashboards/api.py 499 3 99% 220, 250, 253
src/dashboards/app.py 190 0 100%
src/dashboards/auth.py 265 0 100%
src/dashboards/certs.py 292 0 100%
src/dashboards/collectors.py 216 5 98% 323-324, 328-329, 346
src/dashboards/config.py 126 0 100%
src/dashboards/debug.py 14 0 100%
src/dashboards/poller.py 165 3 98% 192-194, 279
src/dashboards/prom.py 112 5 96% 126, 163, 177, 180, 183
src/dashboards/rates.py 73 0 100%
src/dashboards/severity.py 49 0 100%
src/dashboards/sources/__init__.py 0 0 100%
src/dashboards/sources/disks.py 134 0 100%
src/dashboards/sources/gateway.py 463 4 99% 518, 529, 637, 1129
src/dashboards/sources/host.py 209 0 100%
src/dashboards/sources/llm.py 178 1 99% 285
src/dashboards/sources/nut.py 413 20 95% 153, 172, 186, 198, 204, 217, 225, 241, 319-320, 322, 579, 591, 626, 657, 661, 718-719, 724, 736
src/dashboards/sources/nut_sim.py 134 0 100%
src/dashboards/store.py 313 2 99% 553-554
------------------------------------------------------------------
TOTAL 4063 44 99%
Required test coverage of 94% reached. Total coverage: 98.92%
1783 passed, 16 skipped, 1 warning in 166.68s (0:02:46)
== uv run ruff check src tests agent ==
All checks passed!
== uv run pyright ==
0 errors, 0 warnings, 0 informations
== node --check src/dashboards/web/static/*.js ==
advisory: dependencies are out of date (AGENTS.md rule 8 — update on purpose):
certifi 2026.10.19 2026.10.23 wheel
coverage 7.16.2 7.17.0 wheel
fastapi 0.153.0 0.162.0 wheel
pycparser 3.11 3.12 wheel
ruff 0.17.1 0.17.2 wheel
@@ -0,0 +1,22 @@
**Task 04 complete — `/gateway` is now two trunk→leaf panels plus one warnings list.**
Implementation
- `web/pages/gateway.html`: exactly two `article[data-gate]` panels (`nginx` then `haproxy`), `map`/`unmatched` deleted; new `section.card#gate-warnings` (ships `hidden`, `ul.alerts#warnings-list`); four tallies `#t-ports/#t-connections/#t-live/#t-warnings` (`#t-listeners/#t-established/#t-unmatched` gone); footer rewritten to three sentences.
- `web/static/page-gateway.js`: `groupsFor()` (ports ascending, a no-target listener still gets a group → `no target named in the config`), `renderGroup/renderTrunk/renderLeaves` built-once-and-patched via keyed `Map`s + `appendChild`, `renderWarnings()` in the overview's `li.warn/.src/.msg` vocabulary, `countText()`→`null` so a name endpoint prints `—` with its why-title (never `0`), drawn-and-inert switch (`buildSwitch()`, `aria-hidden`, no listener), charts discipline unchanged.
- `dash.css`: `.gate-map/.gate-group (minmax(0,9.5rem) minmax(0,1fr))/.gate-leaves/.gate-link/.gate-target/.gate-selector` + ≤640px stacking; only switch ink added (needed so the drawn switch isn't a black blob) — elbows/colour stay task 05.
- `tests/test_api.py`: rewrote the page-shape/degradation/clipped-label/live-sum pins; added 5 tests (two-panels+warnings, stream-map-inside-nginx, `:80` refusal, warnings both directions, dash-not-zero, switch-inert); fixed the stale `wait:` selectors in `data/logs/27-task02-mobile-audit.cjs` and `30-task03-warnings.cjs` so both standing sweeps stay runnable.
Checks
- `bash .agents/validate.sh` → rc 0: 1789 passed / 16 skipped, coverage 98.94% (floor 94), ruff clean, pyright 0 errors, `node --check` clean.
- Browser, live fleet: `data/logs/30-task04-panels.cjs` → **49/49** (`30-task04-panels.out`; `gateway-30-task04-{1440,360,reduced}.png`): nginx 4 ports·27 routes (`:443` 18 leaves — the live map today — `:4443` 6, `:8443` 3, `:80` refusal), haproxy 6 groups of one, 31 `—` vs 2 numbered, warnings panel hidden live and raised by one injected row, 0 px sideways scroll at 1440/360; FINDING: 0/33 labels clip at 1440, 27/33 at 360.
- Mobile audit re-run: 370 PASS / 0 FAIL / 0 FINDING; task-03 sweep 23/23; dev server restarted, **10 sources `ok`**.
Docs
- README `/gateway` paragraph and AGENTS.md UI-conventions paragraph rewritten (two panels, map is nginx's, trunk→leaf, warnings in alert vocabulary, drawn-not-wired switch); gap 13 updated (desktop arm closed by measurement, phone residue re-numbered) and gap 12 records this audit re-run.
Decisions/deviations
- Switch is drawn now (task 04 requires it) with minimal ink; task 05 still owns geometry/colour/elbows.
- History request stays one per visible panel per refresh with the plot gated on `chartsOpen()` — phase-29 rule, verified by measurement rather than changed.
- No dependency changes, so `uv pip list --outdated` advisory was left alone; no `<table>` introduced; other pages byte-identical.
Next pending task: `05_lines_and_switches.md`.
@@ -0,0 +1,75 @@
== uv run pytest --cov (hermetic: no network, live tests skip) ==
........................................................................ [ 3%]
........................................................................ [ 7%]
........................................................................ [ 11%]
........................................................................ [ 15%]
........................................................................ [ 19%]
........................................................................ [ 23%]
........................................................................ [ 27%]
........................................................................ [ 31%]
s....................................................................... [ 35%]
........................................................................ [ 39%]
........................................................................ [ 43%]
........................................................................ [ 47%]
........................................................................ [ 51%]
........................................................................ [ 55%]
........................................................................ [ 59%]
........................................................................ [ 63%]
........................................................................ [ 67%]
........................................................................ [ 71%]
...............................ssssssssssss............................. [ 75%]
........................................................................ [ 79%]
........................................................................ [ 83%]
........................................................................ [ 87%]
........................................................................ [ 91%]
........................................................................ [ 95%]
...................sss.................................................. [ 99%]
..... [100%]
=============================== warnings summary ===============================
.venv/lib64/python3.14/site-packages/fastapi/testclient.py:1
/var/home/ducoterra/Projects/Personal/dashboards/.venv/lib64/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
================================ tests coverage ================================
_______________ coverage: platform linux, python 3.14.4-final-0 ________________
Name Stmts Miss Cover Missing
------------------------------------------------------------------
src/dashboards/__init__.py 3 0 100%
src/dashboards/__main__.py 14 1 93% 32
src/dashboards/alerts.py 201 0 100%
src/dashboards/api.py 499 3 99% 220, 250, 253
src/dashboards/app.py 190 0 100%
src/dashboards/auth.py 265 0 100%
src/dashboards/certs.py 292 0 100%
src/dashboards/collectors.py 216 5 98% 323-324, 328-329, 346
src/dashboards/config.py 126 0 100%
src/dashboards/debug.py 14 0 100%
src/dashboards/poller.py 165 3 98% 192-194, 279
src/dashboards/prom.py 112 5 96% 126, 163, 177, 180, 183
src/dashboards/rates.py 73 0 100%
src/dashboards/severity.py 49 0 100%
src/dashboards/sources/__init__.py 0 0 100%
src/dashboards/sources/disks.py 134 0 100%
src/dashboards/sources/gateway.py 463 4 99% 518, 529, 637, 1129
src/dashboards/sources/host.py 209 0 100%
src/dashboards/sources/llm.py 178 1 99% 285
src/dashboards/sources/nut.py 413 20 95% 153, 172, 186, 198, 204, 217, 225, 241, 319-320, 322, 579, 591, 626, 657, 661, 718-719, 724, 736
src/dashboards/sources/nut_sim.py 134 0 100%
src/dashboards/store.py 313 2 99% 553-554
------------------------------------------------------------------
TOTAL 4063 44 99%
Required test coverage of 94% reached. Total coverage: 98.92%
1789 passed, 16 skipped, 1 warning in 166.82s (0:02:46)
== uv run ruff check src tests agent ==
All checks passed!
== uv run pyright ==
0 errors, 0 warnings, 0 informations
== node --check src/dashboards/web/static/*.js ==
advisory: dependencies are out of date (AGENTS.md rule 8 — update on purpose):
certifi 2026.10.19 2026.10.23 wheel
coverage 7.16.2 7.17.0 wheel
fastapi 0.153.0 0.162.0 wheel
pycparser 3.11 3.12 wheel
ruff 0.17.1 0.17.2 wheel
@@ -0,0 +1,26 @@
Done. Task 05 (`/gateway` connection lines and switches) is implemented, tested, and verified against the live ingress.
```
Changed src/dashboards/web/static/dash.css --gate-on/--gate-off existence pair (+ --gate-leaf-gap/
row-gap); spine = each .gate-link's ::before/::after halves meeting across the leaf gap, first/last
dropped, .gate-group.solo → straight across; pipe colour from on/off, motion from live/still only.
page-gateway.js routeInConfig() → every drawn row `on`; switch is now <button role="switch"
aria-checked aria-disabled="true"> + knob, named ("endpoint, reached by <selector>, in the nginx
config, on"); renderGroups sets .solo from the group's own leaf count; no new listener.
tests/test_api.py new: test_a_connection_is_green_because_it_is_in_the_config,
test_haproxy_draws_straight_across_and_nginx_branches; rewrote test_the_switch_is_built_and_explicitly_
unwired (was the aria-hidden SVG mark), the motion pin (two class pairs) and the reduced-motion pin
(+.gate-switch-thumb, + "nothing in the map animates that isn't stilled"), and the no-verdict docstring.
AGENTS.md / README.md the /gateway UI paragraphs now say colour=config existence, motion=socket,
and describe the real-but-unwired switch and the branch geometry.
Green uv run pytest → 1791 passed, 16 skipped · coverage 98.92% (floor 94) · ruff clean · pyright 0 errors ·
bash .agents/validate.sh → exit 0.
Live data/logs/30-task05-lines.cjs → 46 PASS / 0 FAIL (exit 0; gateway-30-task05-{1440,360,reduced}.png):
33/33 lines compute to --gate-on in BOTH themes; 1 live line animates, 32 resting stay green;
reduced motion stills pipes AND knob; 6 haproxy groups solo/straight; nginx :443/18, :4443/6, :8443/3
spines at one x (186 = trunk right = tick left), continuous, stopping at first/last; 33 rows × 1 switch;
Playwright itself refused the click as "not enabled", forced → nothing changed, 0 non-GET requests;
360 px 0px sideways scroll, stacked trunk still connected. 27-task02-mobile-audit re-run: 371 PASS /
0 FAIL / 0 FINDING; 30-task04-panels re-run (its one switch check updated to the shipped shape): 49/0.
Left .agents/phases/todo/.../05_lines_and_switches.md untouched (not my file to move); task 06 still to run.
```
@@ -0,0 +1,75 @@
== uv run pytest --cov (hermetic: no network, live tests skip) ==
........................................................................ [ 3%]
........................................................................ [ 7%]
........................................................................ [ 11%]
........................................................................ [ 15%]
........................................................................ [ 19%]
........................................................................ [ 23%]
........................................................................ [ 27%]
........................................................................ [ 31%]
..s..................................................................... [ 35%]
........................................................................ [ 39%]
........................................................................ [ 43%]
........................................................................ [ 47%]
........................................................................ [ 51%]
........................................................................ [ 55%]
........................................................................ [ 59%]
........................................................................ [ 63%]
........................................................................ [ 67%]
........................................................................ [ 71%]
.................................ssssssssssss........................... [ 75%]
........................................................................ [ 79%]
........................................................................ [ 83%]
........................................................................ [ 87%]
........................................................................ [ 91%]
........................................................................ [ 95%]
.....................sss................................................ [ 99%]
....... [100%]
=============================== warnings summary ===============================
.venv/lib64/python3.14/site-packages/fastapi/testclient.py:1
/var/home/ducoterra/Projects/Personal/dashboards/.venv/lib64/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
================================ tests coverage ================================
_______________ coverage: platform linux, python 3.14.4-final-0 ________________
Name Stmts Miss Cover Missing
------------------------------------------------------------------
src/dashboards/__init__.py 3 0 100%
src/dashboards/__main__.py 14 1 93% 32
src/dashboards/alerts.py 201 0 100%
src/dashboards/api.py 499 3 99% 220, 250, 253
src/dashboards/app.py 190 0 100%
src/dashboards/auth.py 265 0 100%
src/dashboards/certs.py 292 0 100%
src/dashboards/collectors.py 216 4 98% 324, 328-329, 346
src/dashboards/config.py 126 0 100%
src/dashboards/debug.py 14 0 100%
src/dashboards/poller.py 165 3 98% 192-194, 279
src/dashboards/prom.py 112 5 96% 126, 163, 177, 180, 183
src/dashboards/rates.py 73 0 100%
src/dashboards/severity.py 49 0 100%
src/dashboards/sources/__init__.py 0 0 100%
src/dashboards/sources/disks.py 134 0 100%
src/dashboards/sources/gateway.py 463 4 99% 518, 529, 637, 1129
src/dashboards/sources/host.py 209 0 100%
src/dashboards/sources/llm.py 178 1 99% 285
src/dashboards/sources/nut.py 413 20 95% 153, 172, 186, 198, 204, 217, 225, 241, 319-320, 322, 579, 591, 626, 657, 661, 718-719, 724, 736
src/dashboards/sources/nut_sim.py 134 0 100%
src/dashboards/store.py 313 2 99% 553-554
------------------------------------------------------------------
TOTAL 4063 43 99%
Required test coverage of 94% reached. Total coverage: 98.94%
1791 passed, 16 skipped, 1 warning in 169.28s (0:02:49)
== uv run ruff check src tests agent ==
All checks passed!
== uv run pyright ==
0 errors, 0 warnings, 0 informations
== node --check src/dashboards/web/static/*.js ==
advisory: dependencies are out of date (AGENTS.md rule 8 — update on purpose):
certifi 2026.10.19 2026.10.23 wheel
coverage 7.16.2 7.17.0 wheel
fastapi 0.153.0 0.162.0 wheel
pycparser 3.11 3.12 wheel
ruff 0.17.1 0.17.2 wheel
@@ -0,0 +1,13 @@
Done. Task 06 complete.
**Implementation (docs + verification only — no behaviour change):**
- `AGENTS.md`: rewrote the `/gateway` page-table row (two panels, trunk→leaf), the `/api/summary` gateway block key list (now the twelve it serves, `warnings` included), `sumConnections()`→`sumLive()`, gap 12's audit re-run, gap 13 with today's measurements (1440 px 0/33+0/33 in a 361 px column; 360 px 31/33+22/33 in 139 px — the switch's 34+26 px — every row still carrying its full text on `title`), the fixture-vs-live drift note (fixture 17 map entries, live 18), and the commands block's counts.
- `README.md`: page-table row, ingress line figures, dropped hard-coded domain counts; `dash.css` + `gateway.html` comments likewise de-numbered. `GATEWAY_HOST` config row unchanged — the phase added no env setting.
- New: `data/logs/30-connection-map.cjs` (**63 PASS / 0 FAIL**, 5 screenshots), `data/logs/verify-30-connection-map.py` (**17 PASS**), `data/logs/30-final-mobile-audit.out` (**371 PASS / 0 FAIL / 0 FINDING**), `data/logs/NOTES-30-connection-map.md`.
- Live read-only: `warnings: []`; no `unmatched`/`outbound`/`by_port`/`partial`; 6 haproxy frontends → exactly their own `server`; 18 `map` entries = 18 `:443` leaves incl. `default → 127.0.0.1:4443`; `:4443` 6 / `:8443` 3; `:80` listener with no connection; no `9120`. **Inbound reconciliation: page 17 = kernel 17** (`ssh gateway-ops cat /proc/net/tcp{,6}`, bracketed reads in the transcript). 10 sources `ok`; `git diff --stat agent/ deploy/` empty.
**Tests/lint:** `bash .agents/validate.sh` exit 0 — 1791 passed, 16 skipped; coverage 98.94% (floor 94); ruff + pyright + `node --check` clean; `uv pip list --outdated` empty.
**Notable decisions:** rule 8 forced a lock refresh (certifi/coverage/fastapi 0.153→0.162/pycparser/ruff) and re-verified everything, including the browser pass, after a dev-server restart; that exposed a fixed-sleep flake in `tests/test_poller.py`'s maintenance tests, now a `_wait_for()` predicate on the real claim (no production code moved). Fixture/live map-count drift is recorded, not silently re-captured. Git commit left to the operator (harness forbids mutating the repo history).
**Next pending:** none — 06 was the last task in `.agents/phases/todo/30_gateway_connection_map/`.
@@ -0,0 +1,69 @@
== uv run pytest --cov (hermetic: no network, live tests skip) ==
........................................................................ [ 3%]
........................................................................ [ 7%]
........................................................................ [ 11%]
........................................................................ [ 15%]
........................................................................ [ 19%]
........................................................................ [ 23%]
........................................................................ [ 27%]
........................................................................ [ 31%]
..s..................................................................... [ 35%]
........................................................................ [ 39%]
........................................................................ [ 43%]
........................................................................ [ 47%]
........................................................................ [ 51%]
........................................................................ [ 55%]
........................................................................ [ 59%]
........................................................................ [ 63%]
........................................................................ [ 67%]
........................................................................ [ 71%]
.................................ssssssssssss........................... [ 75%]
........................................................................ [ 79%]
........................................................................ [ 83%]
........................................................................ [ 87%]
........................................................................ [ 91%]
........................................................................ [ 95%]
.....................sss................................................ [ 99%]
....... [100%]
=============================== warnings summary ===============================
.venv/lib64/python3.14/site-packages/fastapi/testclient.py:1
/var/home/ducoterra/Projects/Personal/dashboards/.venv/lib64/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
================================ tests coverage ================================
_______________ coverage: platform linux, python 3.14.4-final-0 ________________
Name Stmts Miss Cover Missing
------------------------------------------------------------------
src/dashboards/__init__.py 3 0 100%
src/dashboards/__main__.py 14 1 93% 32
src/dashboards/alerts.py 201 0 100%
src/dashboards/api.py 499 3 99% 220, 250, 253
src/dashboards/app.py 190 0 100%
src/dashboards/auth.py 265 0 100%
src/dashboards/certs.py 292 0 100%
src/dashboards/collectors.py 216 5 98% 323-324, 328-329, 346
src/dashboards/config.py 126 0 100%
src/dashboards/debug.py 14 0 100%
src/dashboards/poller.py 165 3 98% 192-194, 279
src/dashboards/prom.py 112 5 96% 126, 163, 177, 180, 183
src/dashboards/rates.py 73 0 100%
src/dashboards/severity.py 49 0 100%
src/dashboards/sources/__init__.py 0 0 100%
src/dashboards/sources/disks.py 134 0 100%
src/dashboards/sources/gateway.py 463 4 99% 518, 529, 637, 1129
src/dashboards/sources/host.py 209 0 100%
src/dashboards/sources/llm.py 178 1 99% 285
src/dashboards/sources/nut.py 413 20 95% 153, 172, 186, 198, 204, 217, 225, 241, 319-320, 322, 579, 591, 626, 657, 661, 718-719, 724, 736
src/dashboards/sources/nut_sim.py 134 0 100%
src/dashboards/store.py 313 2 99% 553-554
------------------------------------------------------------------
TOTAL 4063 44 99%
Required test coverage of 94% reached. Total coverage: 98.92%
1791 passed, 16 skipped, 1 warning in 168.76s (0:02:48)
== uv run ruff check src tests agent ==
All checks passed!
== uv run pyright ==
0 errors, 0 warnings, 0 informations
== node --check src/dashboards/web/static/*.js ==