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:
@@ -243,6 +243,76 @@ def disk_snapshot(agent_snapshots: dict[str, dict[str, Any]], smart_shape: Any)
|
||||
return build
|
||||
|
||||
|
||||
# ---- phase 29: the gateway page (the /gateway source, API and page) -------- #
|
||||
|
||||
#: The captured gateway tree, as the paths the agent's own `--root` resolution expects.
|
||||
#: These are the verbatim phase-28 captures (`scripts/capture-gateway-config.sh`), so the
|
||||
#: config half of a `/gateway` test is the file the real ingress runs, not a memory of it.
|
||||
GATEWAY_TREE = {
|
||||
"etc/haproxy/haproxy.cfg": "gateway_haproxy.cfg",
|
||||
"etc/nginx/nginx.conf": "gateway_nginx_main.conf",
|
||||
"etc/nginx/conf.d/http.conf": "gateway_nginx_conf_d_http.conf",
|
||||
"etc/nginx/stream.d/stream.conf": "gateway_nginx_stream_d_stream.conf",
|
||||
"proc/net/tcp": "gateway_proc_net_tcp.txt",
|
||||
"proc/net/tcp6": "gateway_proc_net_tcp6.txt",
|
||||
}
|
||||
|
||||
GATEWAY_MODES = ("full", "predates", "no_haproxy", "no_nginx", "sockets_silent")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def gateway_snapshot(host_agent_module: Any, tmp_path: Path) -> Any:
|
||||
"""`build(mode="full")` -> a 1.2.0-style agent snapshot carrying the three proxy sections.
|
||||
|
||||
Not a hand-written dict: the captured configs and `/proc` tables are laid into a fake
|
||||
`--root` tree and read by the **real agent `Collector`**, so the sections handed to
|
||||
`sources/gateway.py` are the bytes agent 1.2.0 serves. A field rename on either side of the
|
||||
wire therefore fails a source test instead of drawing an empty pipe on the page (rule 7).
|
||||
|
||||
Modes are the degradation arms the page has to render:
|
||||
|
||||
* `full` — all three sections `available: true`, live sockets from the capture
|
||||
* `predates` — none of the three keys at all (an agent older than 1.2.0, or `--no-proxy`)
|
||||
* `no_haproxy` / `no_nginx` — that one config file is absent: `available: false` plus the
|
||||
agent's own **not found** line, the other half of the page untouched
|
||||
* `sockets_silent` — neither `/proc` table readable: `available: null`, so every live count
|
||||
must come back `None` and never `0`
|
||||
"""
|
||||
def build(mode: str = "full") -> dict[str, Any]:
|
||||
if mode not in GATEWAY_MODES:
|
||||
raise AssertionError(f"unknown gateway snapshot mode {mode!r}")
|
||||
root = tmp_path / f"root-{mode}"
|
||||
for relative, fixture in GATEWAY_TREE.items():
|
||||
if not fixture:
|
||||
continue
|
||||
target = root / relative
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
target.write_text((FIXTURES / fixture).read_text())
|
||||
if mode == "no_haproxy":
|
||||
(root / "etc/haproxy/haproxy.cfg").unlink()
|
||||
if mode == "no_nginx":
|
||||
for path in (root / "etc/nginx/nginx.conf", root / "etc/nginx/conf.d/http.conf",
|
||||
root / "etc/nginx/stream.d/stream.conf"):
|
||||
path.unlink()
|
||||
if mode == "sockets_silent":
|
||||
(root / "proc/net/tcp").unlink()
|
||||
(root / "proc/net/tcp6").unlink()
|
||||
|
||||
errors: list[str] = []
|
||||
collector = host_agent_module.Collector(root)
|
||||
snapshot: dict[str, Any] = {
|
||||
"ok": True, "schema": 1, "ts": 1_760_000_000.0, "host": "gateway",
|
||||
"agent_version": host_agent_module.AGENT_VERSION, "errors": errors,
|
||||
}
|
||||
if mode != "predates":
|
||||
snapshot["haproxy"] = collector.haproxy(errors)
|
||||
snapshot["nginx"] = collector.nginx(errors)
|
||||
snapshot["sockets"] = collector.sockets(errors)
|
||||
return snapshot
|
||||
|
||||
return build
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def metrics_text() -> Any:
|
||||
"""metrics_text('gufo') / metrics_text('vllm') -> the captured exposition verbatim."""
|
||||
|
||||
Reference in New Issue
Block a user