phase: 29_gateway_dashboard_page

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.
This commit is contained in:
2026-10-23 09:52:30 -04:00
parent 44d61d3ab9
commit fa4f38c694
68 changed files with 5714 additions and 200 deletions
+48
View File
@@ -87,6 +87,54 @@ def test_settings_default_to_no_sources_configured() -> None:
assert settings.any_source_enabled is False
def test_the_gateway_source_names_the_ingress_host_and_its_own_cadence() -> None:
"""Phase 29: one `HOST_TARGETS` name is the gateway, and it is polled on its own clock.
`gateway_host` defaults to the fleet's real name so installing agent 1.2.0 on a host called
`gateway` is the whole configuration — but the **default is not a target**: with no
`HOST_TARGETS` entry of that name the source is unconfigured, `/api/gateway/*` 404s without
dialling, and the page says so (invariant 3). The interval sits between the system poll and
SMART because a connection count is the shape of half a minute, not of ten seconds.
"""
settings = Settings()
assert settings.gateway_host == "gateway"
assert settings.gateway_interval == 30.0
assert settings.gateway_timeout == 10.0
assert settings.gateway_enabled is False
assert settings.any_source_enabled is False
def test_gateway_enabled_is_true_only_for_a_name_that_is_a_target() -> None:
"""A name with no agent behind it is unconfigured, not broken — the `nut_enabled` shape."""
def with_targets(raw: str, name: str = "gateway") -> bool:
targets, _problems = parse_targets(raw)
return Settings(host_targets=tuple(targets), gateway_host=name).gateway_enabled
assert with_targets("gateway=https://gw.example.net:9120") is True
assert with_targets(
"deskwork=https://d.example.net:9120 gateway=https://gw.example.net:9120") is True
assert with_targets("deskwork=https://d.example.net:9120") is False
assert with_targets("router=https://gw.example.net:9120", name="router") is True
assert with_targets("Gateway=https://gw.example.net:9120") is False, ("names are lowercase")
assert with_targets("") is False
def test_the_gateway_settings_come_from_the_environment(monkeypatch: pytest.MonkeyPatch) -> None:
"""`GATEWAY_HOST=router` is how a deployment whose ingress is called something else opts in."""
monkeypatch.setenv("GATEWAY_HOST", "router")
monkeypatch.setenv("GATEWAY_INTERVAL_SECONDS", "15")
monkeypatch.setenv("GATEWAY_TIMEOUT_SECONDS", "20")
reloaded = importlib.reload(config)
try:
assert reloaded.SETTINGS.gateway_host == "router"
assert reloaded.SETTINGS.gateway_interval == 15.0
assert reloaded.SETTINGS.gateway_timeout == 20.0
finally:
for name in ("GATEWAY_HOST", "GATEWAY_INTERVAL_SECONDS", "GATEWAY_TIMEOUT_SECONDS"):
monkeypatch.delenv(name)
importlib.reload(config)
def test_the_disk_source_defaults_to_a_five_minute_cadence() -> None:
"""SMART is not a per-second signal (phase 09), and the fetch waits for the agent's
own 20 s smartctl budget. Both are env-only, like every other interval."""