phase: 31_gateway_route_readability
**Phase 31 verification complete — all 5 tasks already in `complete/`; this was the final verification pass.**
Verified / changed
- Re-ran every phase-31 artifact live and green: `verify-31-page-only.py` 6 PASS/0 FAIL; browser pass `31-route-readability.cjs` **64 PASS / 0 FAIL** (exit 0); `27-task02-mobile-audit.cjs` **371 PASS / 0 FAIL / 0 FINDING** (exit 0); rebuilt `data/logs/31-route-readability.out` as the 4-part transcript.
- Added the 4 degradation-arm screenshots the criterion asked for but the pass lacked (`31-gateway-{unconfigured,unreachable,no-nginx,sockets-quiet}.png`) — 12 frames now, one per arm.
- Grep criteria: no gateway connector rule uses `border-strong`; no verdict token in the gateway CSS section or `page-gateway.js` beyond the `alerts.py`-owned `.warn`; exactly 4 `addEventListener` calls (none for the switch); `routeInConfig()` returns `true`; **no `.py` under `src/dashboards/` changed**.
Tests / lint (all 100% green)
- `uv run pytest` → **1796 passed, 16 skipped**; `--cov` → **98.94%** (floor 94); `ruff check src tests agent` → All checks passed; `pyright` → 0 errors; `node --check` clean; `bash .agents/validate.sh` → **exit 0**.
- `uv run pytest -m live --live` → **12 passed / 0 failed**; `./scripts/dev-server.sh status` → all **10 sources ok**.
Completion criteria: colour (33/33 rows every segment `--gate-on`, dark `rgb(63,185,80)` + light `rgb(26,127,55)`, zero grey dashes) ✔ · inbound-first rows + filter (33/33 line 1 = `out.selectors`, line 2 = `→ host:port · kind`; `stackexpected` → **6** rows, not the overview's "five" — the capture declares 6 such routes; tallies byte-identical `10/33/16/0`, zero requests) ✔ · trunk motion truth table + `null` arm prints `n/a` never `0` + reduced-motion stills 43 paths and every knob ✔ · screenshots/transcripts ✔ · payload page-only ✔ (literal byte-diff of a live socket table can never pass; answered by the no-`.py`-diff + shape-equality proof, as recorded in the transcript) · no behaviour change elsewhere ✔.
Docs: fixed 6 inconsistencies — two identifiers split across line breaks (`test_the_pipe_animation_stills_in_both_states_under_reduced_motion`, `32_gateway_resolved_target_liveness`), stale "sixteen routes" count, stale "four machines/hosts" in README **and** AGENTS (fleet is three), "three fields"→"three field types", gap-14 headline wording; updated the pass's screenshot list.
Deviations: `uv pip list --outdated` shows one advisory (`starlette 1.9.0 → 1.10.0`) — not upgraded, this phase moved no dependency (rule 8 / DoD apply "if deps moved"). `TODO.md` carries two new operator disk requests, left untouched.
Next pending phase: `32_gateway_resolved_target_liveness`.
This commit is contained in:
@@ -10,7 +10,7 @@ file:
|
||||
| `/disks` | Are any of my **physical** disks failing, and which one — health, temperature, power-on hours, wear, sector alarms | the same agents, running `smartctl -i -A -H` per whole drive (never a partition or a mount); a host whose agent has no `smartctl` is not a host on this page |
|
||||
| `/llm` | Tokens/sec, active sessions, queue depth, KV-cache pressure, latency percentiles | llama.cpp-family (gufo) `/metrics` + `/slots`, vLLM `/metrics` — read-only |
|
||||
| `/ups` | Battery health, runtime left, power events, and battery-test buttons | NUT `upsd`, on whatever port it really listens on (its own wire-protocol client) |
|
||||
| `/gateway` | Which domains are coming in on which port, and where each one goes — **two connection tables, one per proxy config file** (`nginx`, then `haproxy`): a trunk per bound port with its configured endpoints branching off it, each line labelled with the config's own word for the route (`server_name`, `server`, `map` key) and its live connection count | the agent on **one** host, `GATEWAY_HOST` (default `gateway`), reading its `haproxy.cfg` + nginx tree and `/proc/net/tcp{,6}` — so it needs **agent 1.2.0**, and the whole page is *unconfigured* (never red) unless `GATEWAY_HOST` names a `HOST_TARGETS` entry |
|
||||
| `/gateway` | Which domains are coming in on which port, and where each one goes — **two connection tables, one per proxy config file** (`nginx`, then `haproxy`): a trunk per bound port with its configured routes branching off it, each line reading the config's own **inbound** word (`server_name`, `map` key, `backend` name) on line 1 over the counterpart it routes to on line 2 (`→ host:port`), plus its live connection count — and one filter box above the map that hides the routes whose own labels do not contain what you typed, asking nothing of the server | the agent on **one** host, `GATEWAY_HOST` (default `gateway`), reading its `haproxy.cfg` + nginx tree and `/proc/net/tcp{,6}` — so it needs **agent 1.2.0**, and the whole page is *unconfigured* (never red) unless `GATEWAY_HOST` names a `HOST_TARGETS` entry |
|
||||
|
||||
No Prometheus, no Grafana, no CDN, no build step. Nothing is fetched from the internet at
|
||||
runtime; every asset is served by this app.
|
||||
@@ -732,7 +732,7 @@ certs.py ─ root CA + client cert, minted once at startup (unreadable row → n
|
||||
`alerts.py` is why one dead agent is still one line on the overview rather than two. The gateway
|
||||
page's job (phase 29) is the **third** reader of that same `/snapshot`, and one job for the whole
|
||||
deployment rather than one per host — `GATEWAY_HOST` names the single ingress, and a job per host
|
||||
would ask four machines a question only one can answer — on its own 30 s cadence with no
|
||||
would ask every machine a question only one of them can answer — on its own 30 s cadence with no
|
||||
`RateEstimator`, because a connection count is not a rate. And it never quietly *stops*: the task
|
||||
is that source's only poller, so a SQLite write that gets refused is reported (`ok: false`, the
|
||||
real reason, the numbers aging, one line in the event log) instead of freezing the source under a
|
||||
@@ -1078,21 +1078,54 @@ more. `GATEWAY_HOST` naming no `HOST_TARGETS` entry is
|
||||
what routes out of each, and how many connections are live on each. It is **two panels** — one per
|
||||
config file, `nginx` then `haproxy` — each hidden until a poll actually has rows for it, so a router
|
||||
with no nginx in it spends no panel saying so. Inside a panel the rows are **groups**: a *trunk* is
|
||||
one port that file binds, and hanging off it are the *leaves* — the endpoints it names, the endpoint
|
||||
**exactly as the config wrote it** with the config's own word for the route above it (a `server_name`,
|
||||
a haproxy `server` name, a stream `map` key). That is the shape of the file, which is why the stream
|
||||
one port that file binds, and hanging off it are the *leaves* — the routes that port's config names,
|
||||
each reading **inbound first**: line 1 is the config's own word for who reaches the endpoint (a
|
||||
`server_name`, a haproxy `backend` name, a stream `map` key, the literal `default`), and line 2 is
|
||||
the counterpart it flows to, **exactly as the config wrote it**, marked `→ host:port` with the kind
|
||||
of route after it. Both ends are verbatim, and both are carried whole on the row's tooltip and the
|
||||
line's accessible name whichever one CSS has to ellipsise. That is the shape of the file, which is
|
||||
why the stream
|
||||
`map` is not a third panel: its entries are the nginx panel's `:443` leaves, so every SNI domain
|
||||
reads beside the port it reaches. A trunk that names no target — this fleet's `:80` `return 301` —
|
||||
prints "no target named in the config" instead of drawing nothing. Each line is joined to its
|
||||
counterparts: nginx's ports **branch** — a dashed spine down the fan-out with a tick into every
|
||||
endpoint — and haproxy's one-`server` frontends run **straight across**, because a port with one
|
||||
route behind it is not a branch. A line is **green because the config declares the route**, an
|
||||
existence colour of the page's own, and it **animates only while it carries at least one live
|
||||
connection** — motion is the socket, colour is the config, and a resting route keeps the same green
|
||||
as a busy one. **Nothing on the page is coloured by a verdict** — a busy ingress is the job, so
|
||||
`severity.py` rates no `gateway.*` metric and no number gets a green/amber/red tint. A line to an endpoint the config wrote as a **name** prints `—`, never `0`:
|
||||
counting it would need a DNS lookup and the join never asks one, and the row's tooltip says which of
|
||||
the two facts it is looking at. The header's four tallies are `ports`, `connections`, `live` and
|
||||
route behind it is not a branch. A route is **green because the config declares it**, an existence
|
||||
colour of the page's own, and that green covers **every segment of the row** — spine, tick, line and
|
||||
switch all read one class on the row, so no part of an active route is drawn as grey plumbing — while
|
||||
the line **animates only while it carries at least one live connection**: motion is the socket,
|
||||
colour is the config, and a resting route keeps the same green as a busy one. The trunk carries a
|
||||
**short line of its own** for that reason, and it reads from a *different* field: a leaf moves on its
|
||||
endpoint's count, the trunk's stub moves on `listeners[].connections` — the sockets whose **local**
|
||||
end is that port, which `/proc` states whether or not the route's target is a resolvable address. So
|
||||
a busy port is visibly busy even when every leaf under it carries a `—`, which is the normal look of
|
||||
this fleet's `:443`. That line's claim is about **the port and never about one route**: its
|
||||
accessible name and tooltip say `:4443 — 9 connections are open on this port right now` (or
|
||||
`no connections…`, or `connections on this port are unknown: the socket table did not answer`) and
|
||||
name no endpoint, and the unknown arm prints `n/a` where the count is and no motion at all — never
|
||||
`0`, and never a dash, because "the socket table went quiet" is a different fact from "this target
|
||||
is a name".
|
||||
Above the map, one **filter box** finds a domain among 33 routes (`stackexpected`, `brain`, `4443`,
|
||||
`minecraft`) because the rows are in config order and the question is always about one name. It hides
|
||||
what the payload already sent and asks for **nothing**: no request, no re-derivation, no new route.
|
||||
It matches the strings the config wrote — the inbound word(s), the endpoint, the port, the haproxy
|
||||
`server` name, and a trunk's own domains and client addresses — case-insensitively, so a label CSS
|
||||
ellipsised still matches its whole text. A trunk that matches keeps its **whole fan-out**: typing
|
||||
`4443` asks for the port, and every route hanging off it is the answer. `Escape` clears it, a chip
|
||||
beside the field counts `N of M routes`, and a query that matches nothing leaves both panels standing
|
||||
with `no route matches "…"` in each. What it never touches is the header's four tallies, the warnings
|
||||
panel, or the charts — those describe the ingress, not the operator's current view of it — and it
|
||||
never **groups, sorts or colours** by domain suffix: `reeseapps.com` versus `stackexpected.com` is a
|
||||
fact neither config file states, so the operator types the suffix and the page hides the rest rather
|
||||
than inventing a zone no proxy declared.
|
||||
**Nothing on the page
|
||||
is coloured by a verdict** — a busy ingress is the job, so `severity.py` rates no `gateway.*` metric
|
||||
and no number gets a green/amber/red tint. A line to an endpoint the config wrote as a **name**
|
||||
prints `—`, never `0`: counting it would need a DNS lookup and the join never asks one, and the row's
|
||||
tooltip says which of the two facts it is looking at. Expect that on most of this ingress's routes —
|
||||
31 of its 33 are written as domains — and read it as *drawn, not counted* rather than as a fault; the
|
||||
footer says the same thing once, in plain terms, because a dash per row needs one explanation and not
|
||||
thirty-two. The header's four tallies are `ports`, `connections`, `live` and
|
||||
`warnings`, every one of them the payload's own figure. A `warnings` count above zero raises a panel
|
||||
above the map with the config-integrity rows (`a backend no frontend routes to`, a `proxy_pass` with
|
||||
no `upstream` behind it, an SNI entry that reaches no address) in the overview's alert vocabulary;
|
||||
@@ -1105,7 +1138,11 @@ more. `GATEWAY_HOST` naming no `HOST_TARGETS` entry is
|
||||
for a screen reader (`endpoint, reached by <selector>, in the nginx config, on`) — and it is
|
||||
**unwired on purpose**: `aria-disabled` with a tooltip that says so, and this app writes to no
|
||||
upstream, so a route is disabled by editing `haproxy.cfg` or an nginx `server` block and by nothing
|
||||
in a browser.
|
||||
in a browser. Giving either of those two dashes or knobs a real backing store is deliberately **not**
|
||||
documented here as a thing you can have: name-based liveness needs the agent to report resolved
|
||||
targets (a future agent version), and switching a route needs a second gated write path in front of
|
||||
the proxy configs, which is a change to this app's read-only promise rather than a feature. What runs
|
||||
today is **agent 1.2.0**, no name resolution, and no switching.
|
||||
The four event feeds (`/system`, `/disks`, `/llm`, `/ups`) are a **paged window** over the `event`
|
||||
table rather than a printout of it: the list is a bounded box that scrolls, 50 events to a
|
||||
page, with `1–50 of 200` between `‹ prev` and `next ›` (disabled at the ends) underneath.
|
||||
|
||||
Reference in New Issue
Block a user