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:
@@ -29,7 +29,7 @@ file — plus an overview page that says which of the five you need:
|
||||
| `/disks` | Are any of my physical disks failing, and which one — health, temp, hours, wear | the same agents, `smartctl -i -A -H` per whole drive (never a partition) |
|
||||
| `/llm` | Tokens/sec, active sessions, queue depth, KV pressure, latency percentiles | gufo `/metrics` + `/slots`, vLLM `/metrics` |
|
||||
| `/ups` | Battery health, runtime left, power events, battery-test buttons | NUT `upsd` (our own wire-protocol client; 3051 here, not 3493) |
|
||||
| `/gateway` | Which domains come 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 carrying the config's own word for the route and its live socket count | the same agent (1.2.0) on the one host `GATEWAY_HOST` names: its `haproxy` + `nginx` config sections joined to its `/proc/net/tcp{,6}` sockets |
|
||||
| `/gateway` | Which domains come 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) over the counterpart it routes to, plus its live socket count, with one filter box above the map that hides the routes whose own labels do not contain what was typed | the same agent (1.2.0) on the one host `GATEWAY_HOST` names: its `haproxy` + `nginx` config sections joined to its `/proc/net/tcp{,6}` sockets |
|
||||
|
||||
History is a first-class feature, not a sparkline afterthought: every number worth graphing
|
||||
is stored, rolled up (raw → 1m → 1h), and served from `/api/*/history`.
|
||||
@@ -373,8 +373,8 @@ Responsibilities, one line each:
|
||||
30 s, `GATEWAY_TIMEOUT_SECONDS` 10 s, health and events under the dashboard name `gateway` — but
|
||||
**one** job rather than one per host (the `nut` shape), dialling the `HOST_TARGETS` entry
|
||||
`GATEWAY_HOST` names with its `label` set to that host's name, because the page is about the single
|
||||
ingress and a record per machine would ask four hosts a question only one can answer. It exists
|
||||
only when that name is a target: an unconfigured source has no task that could look red
|
||||
ingress and a record per machine would ask every host a question only one of them can answer. It exists only
|
||||
when that name is a target: an unconfigured source has no task that could look red
|
||||
(invariant 3), and it is handed no `RateEstimator` because a count is not a rate (rule 2).
|
||||
The job list is built once (`app.build_hub()`) because it is wired twice — production and
|
||||
the injected-store test path — and two hand-written copies drifting is how a source ends up
|
||||
@@ -1616,7 +1616,7 @@ uv run python scripts/dev-certs-export.py --url http://127.0.0.1:8090 \
|
||||
# GET /api/certs/<name>/bundle and writes
|
||||
# data/certs/<name>.bundle at 0600 — mints nothing
|
||||
|
||||
uv run pytest # hermetic: 1791 passed + 16 skipped (12 live,
|
||||
uv run pytest # hermetic: 1796 passed + 16 skipped (12 live,
|
||||
# 4 store_perf), no network
|
||||
uv run pytest --cov # coverage gate (98.9%; floor 94%)
|
||||
uv run pytest -m live --live # opt-in: real fleet / engines / upsd
|
||||
@@ -2158,7 +2158,8 @@ charge float jitters every `NUT_INTERVAL_SECONDS`, and rebuilding the `<svg>` on
|
||||
an accessible name for a difference no eye can see.
|
||||
|
||||
**The `/gateway` page is the config's own tree, and a line's only claim is a count** (phase 29;
|
||||
joined differently by phase 30 task 02; laid out as trunk→leaf panels by phase 30 task 04): **exactly
|
||||
joined differently by phase 30 task 02; laid out as trunk→leaf panels by phase 30 task 04; made
|
||||
readable — whole-row colour, inbound-first rows, a moving trunk, a filter — by phase 31): **exactly
|
||||
two panels**, one per config file, in the order `nginx` then `haproxy` (`GATEWAY_CARD_KEYS`), each
|
||||
shipping `hidden` and appearing on the poll that has rows for it — a gateway with no nginx in it
|
||||
spends no panel saying so, and a section that did not answer at all is the page's one `why` line
|
||||
@@ -2169,10 +2170,37 @@ the left-over of a rule the join stopped applying.
|
||||
|
||||
Inside a panel the rows are **groups** (`groupsFor()` → `renderGroups()` → `renderTrunk()` +
|
||||
`renderLeaves()`): a *trunk* is one port that file binds (`:<port>`, its live count, up to
|
||||
`MAX_CLIENTS` client addresses, and its `server_name` count with the whole list on `title`), and the
|
||||
*leaves* hanging off it are the endpoints that port names, each with the config's own word for the
|
||||
route above it (a haproxy `server` name, a vhost's `server_name`, a stream `map` key) and its own
|
||||
`metric`. The page draws what the config declares and nothing else: the deleted `0`-padding
|
||||
`MAX_CLIENTS` client addresses, its `server_name` count with the whole list on `title`, and — since
|
||||
phase 31 task 03 — a **short `.pipe.pipe-trunk` of its own**, appended through the DOM as the trunk's
|
||||
last child because an `<svg>` needs `createElementNS` and cannot come out of `groupElement()`'s static
|
||||
`innerHTML` template), and the *leaves* hanging off it are the routes that port's config declares,
|
||||
each with its own `metric`. That stub is the port's live count drawn as a line: `renderTrunk` puts
|
||||
`live` on it when `listeners[].connections >= 1`, `still` when it is `0` **or** when it is `null` —
|
||||
never `live` for an unknown, and never a `0` in `.pipe-count` for one either — and `on` always, because
|
||||
`listeners[]` contains only ports a config names, so the row existing *is* the existence fact. Its
|
||||
claim is **the port and never a leaf**: `trunkAria()` writes
|
||||
`:4443 — 9 connections are open on this port right now` / `… no connections are open …` /
|
||||
`… connections on this port are unknown: the socket table did not answer` into both `aria-label` and
|
||||
`title` — the latter with `setAttribute`, because `title` is a reflected IDL attribute of
|
||||
`HTMLElement` and **not** of `SVGElement`, so `svg.title = …` is a JavaScript property no tooltip ever
|
||||
reads (task 05's browser pass found the served code doing exactly that) — and it indexes nothing but
|
||||
`listeners[].connections`: no `endpointText`, no `out.*`, no client address, because attributing one
|
||||
port's sockets to one of the routes hanging under it is a claim `sources/gateway.py` does not make.
|
||||
`margin-inline-start: auto` on `.pipe-trunk` is measured, not polish: the trunk wraps, and
|
||||
`.pipe-count`'s own auto margin flushes only the item that *starts* a
|
||||
line, so without it a two-line trunk (`:80` on the live ingress) dropped the dash alone at the far
|
||||
left — ink stranded on the opposite side of the trunk from the spine.
|
||||
**A leaf reads inbound first** (phase 31 task 02, correcting phase 30's rank): line 1 is
|
||||
`.pipe-name` and carries the config's word for **who reaches** the endpoint — a vhost's
|
||||
`server_name`, a stream `map` key (the literal `default` verbatim), or a haproxy `backend` name —
|
||||
from `inboundText()`, which joins **every** `out.selectors` entry, falls back to `out.kind`, and
|
||||
never to `out.host`; line 2 is `.pipe-end` and carries `counterpartText()`'s
|
||||
`→ host:port · kind`, the counterpart it flows to. Both lines are mono so a fan-out of eighteen rows reads
|
||||
down its columns, `.pipe-name` stays the primary line of its row for the trunk too (a trunk prints
|
||||
its port there), and neither helper cuts a string: CSS clips, and `leafTitle()` / `leafAria()` carry
|
||||
**both ends whole** (gap 13). Phase 30 had the two reversed, which buried the domain the operator
|
||||
types into a browser under the address they never type. The page draws what the config declares and
|
||||
nothing else: the deleted `0`-padding
|
||||
(listeners with no connection, endpoints no line reaches) is gone because a group *is* a listener and
|
||||
an unrendered endpoint has no row to sit in. **A trunk whose config names no target is a fact and not
|
||||
a gap in the drawing** — this fleet's nginx `:80` is a `return 301`, so its leaf list renders the
|
||||
@@ -2181,7 +2209,8 @@ for it; `sources/gateway.py` refuses to warn about that shape for the same reaso
|
||||
on the port, and rows on the connection's own `id`, in per-panel `Map`s (`state.groups`,
|
||||
`state.rows`):
|
||||
built once, patched afterwards, reordered with `appendChild` of the existing node and pruned once per
|
||||
panel by id, because rebuilding a 17-row fan-out every 30 s re-announces all of it to assistive tech
|
||||
panel by id, because rebuilding a fan-out the size of this ingress's `:443` every 30 s re-announces
|
||||
all of it to assistive tech
|
||||
for a difference of one connection (the `/llm` slot-grid rule). `pruneCards()` wiping a panel's whole
|
||||
map is the only bulk clear, for the case where the family itself left the payload.
|
||||
|
||||
@@ -2191,20 +2220,44 @@ quiet prints `n/a` (`setTxt`'s own word), while a **leaf** whose endpoint the co
|
||||
**name** prints an em dash (`NO_COUNT`) whose `title` says `connections not attributed — the config
|
||||
writes a name and nothing here resolves names`. That is the normal look of 31 of this ingress's 33
|
||||
lines, and a column of `n/a` would be twelve identical words competing with the page's one footnote.
|
||||
The footer's second paragraph is the same sentence in the operator's terms rather than the join's —
|
||||
*a dash means the endpoint is written in the config as a name; the page matches sockets by address and
|
||||
resolves nothing, so this route is drawn but not counted* — because a column of dashes needs one
|
||||
explanation on the page and not thirty-two on the rows, and because it is a **name** that is the reason
|
||||
and not a fault. `32_gateway_resolved_target_liveness` is what removes the reason; until then the dash
|
||||
stays unexplained-looking by design rather than borrowing the trunk's `n/a` word.
|
||||
The line itself is hand-rolled SVG (`role="img"` + an `aria-label` naming the inbound port, the
|
||||
endpoint, the selector and the count) and it carries **two independent class pairs** (phase 30 task
|
||||
05), because the operator's green and the socket count are two different facts:
|
||||
endpoint, the selector and the count) and the row carries **two independent class pairs** (phase 30
|
||||
task 05, moved onto the `<li>` by phase 31 task 01), because the operator's green and the socket count
|
||||
are two different facts:
|
||||
|
||||
* `live` / `still` — "≥ 1 ESTABLISHED socket on this pipe right now" — decides **motion only**. One
|
||||
`pipe-flow` keyframe whose sole property is `stroke-dashoffset`; the live rule declares no `stroke`,
|
||||
so a busy line and a resting line are the same colour and motion can never become a verdict.
|
||||
* `on` / `off` — "a proxy config declares this route" — decides **stroke only**, through the sheet's
|
||||
own existence pair `--gate-on` / `--gate-off` (declared once in `:root` as a substitution of the
|
||||
theme colours, so both themes follow and no gateway rule names `--ok`). `routeInConfig()` answers
|
||||
`true` unconditionally, which is honest rather than a stub: `connections[]` **is** the config's
|
||||
declaration list, so `off` is the styled seam the write phase will fill and never a second state in
|
||||
use. The base `.pipe-path` declares no `stroke` at all, which is what makes the state class the only
|
||||
way a colour can arrive.
|
||||
* `live` / `still` — "≥ 1 ESTABLISHED socket on this pipe right now" — decides **motion only**, and it
|
||||
stays on the `<svg>` alone. One `pipe-flow` keyframe whose sole property is `stroke-dashoffset`; the
|
||||
live rule declares no `stroke`, so a busy line and a resting line are the same colour and motion can
|
||||
never become a verdict. Motion may not spread: the spine segment between two rows is **shared** by
|
||||
both leaves, so a dash travelling down it could not be attributed to one route, and animating it
|
||||
would put a per-route claim on a per-list structure. There are **two** moving pipes and they read two
|
||||
different payload paths (phase 31 task 03): a leaf's run from `connections[].connections` and a
|
||||
trunk's stub from `listeners[].connections`. One `.pipe` component serves both — `buildPipe(trunk)`
|
||||
adds `pipe-trunk` and nothing else — which is also why the file's single
|
||||
`@media (prefers-reduced-motion: reduce)` block stills the stub without naming it: the stilling rules
|
||||
are descendant selectors (`.pipe.live .pipe-path`), and
|
||||
`test_the_pipe_animation_stills_in_both_states_under_reduced_motion` pins that no `.pipe-trunk` rule
|
||||
may style `animation`/`transition` itself and quietly step outside that block.
|
||||
* `on` / `off` — "a proxy config declares this route" — decides **colour for the whole row**, through
|
||||
the sheet's own existence pair `--gate-on` / `--gate-off` (declared once in `:root` as a substitution
|
||||
of the theme colours, so both themes follow and no gateway rule names `--ok`). `renderLeaf` toggles
|
||||
it on the `<li>` **and** the `<svg>` off the same value in the same pass, and four rules carry it to
|
||||
the two spine halves and the tick, so a route is green from its trunk to its endpoint — phase 30
|
||||
coloured `.pipe-path` only and the operator read that as two thirds of every route being inactive
|
||||
(`test_the_whole_route_takes_one_existence_class` pins the row class, the four connector rules and
|
||||
that no gateway rule names the structure token any more). `routeInConfig()` answers `true`
|
||||
unconditionally, which is honest rather than a stub: `connections[]` **is** the config's declaration
|
||||
list, so `off` is the styled seam the write phase will fill and never a second state in use. Colour
|
||||
still only ever *arrives* from a state class: the base `.pipe-path` declares no `stroke` and the base
|
||||
`.gate-link::before` / `::after` / `.gate-elbow` rules declare their border `transparent` — an SVG's
|
||||
default stroke is `none` for free, a border's default colour is `currentColor`, so "a row built and
|
||||
never rendered draws nothing" has to be written down for a border.
|
||||
|
||||
So the page is **green because the route exists**, never because a number is good: `severity.py` has
|
||||
no `gateway.*` row, the payload carries no `severity` at any depth, and
|
||||
@@ -2217,18 +2270,23 @@ one of them off) and coloured from the same existence pair, carrying a full acce
|
||||
(`endpoint, reached by <selector>, in the nginx config, on`) because a colour cannot say that to a
|
||||
screen reader. It is `aria-disabled="true"` — **never** the `disabled` attribute, which would take the
|
||||
reading out of the a11y tree — with a `title` naming the missing write path, no listener (the file's
|
||||
only `addEventListener` calls are the charts `toggle` and `DOMContentLoaded`) and no `:hover` or
|
||||
`cursor: pointer` anywhere in the map, because invariant 1 means no browser can disable a route:
|
||||
`addEventListener` calls are exactly four — the charts' `toggle`, `DOMContentLoaded`, and the filter's
|
||||
`input`/`keydown` — and `test_the_switch_is_built_and_explicitly_unwired` pins that list as an
|
||||
allow-list whose missing member is the point: **no `click` is bound anywhere on this page**) and no
|
||||
`:hover` or `cursor: pointer` anywhere in the map, because invariant 1 means no browser can disable a
|
||||
route:
|
||||
`haproxy.cfg` and the nginx `server` block are the only switches on this fleet. The one `transition`
|
||||
is the knob's, and the reduced-motion block names it beside both pipe states.
|
||||
|
||||
The branch is four CSS rules and no JavaScript: each `.gate-link` carries its own upper and lower half
|
||||
of the spine at the gutter's left edge (`::before` / `::after`, each reaching half a leaf gap past its
|
||||
row so consecutive halves meet), the first and last rows drop theirs so the spine starts at the first
|
||||
branch and stops at the last with no mask, no measured height and no background-coloured eraser —
|
||||
which is also why it stays right on the poll that adds a leaf — and `.gate-group.solo`, set by the
|
||||
script from the group's own leaf count, takes the vertical away so a one-route port runs **straight
|
||||
across** (every haproxy frontend here). `.gate-group { column-gap: 0 }` is what makes the trunk's
|
||||
The branch is four CSS rules of geometry plus four of colour, and no JavaScript: each `.gate-link`
|
||||
carries its own upper and lower half of the spine at the gutter's left edge (`::before` / `::after`,
|
||||
each reaching half a leaf gap past its row so consecutive halves meet), the first and last rows drop
|
||||
theirs so the spine starts at the first branch and stops at the last with no mask, no measured height
|
||||
and no background-coloured eraser — which is also why it stays right on the poll that adds a leaf — and
|
||||
`.gate-group.solo`, set by the script from the group's own leaf count, takes the vertical away so a
|
||||
one-route port runs **straight across** (every haproxy frontend here). The ≤640px block restates the
|
||||
reaching half's geometry and **no colour**, because a per-width copy of the existence pair is how a
|
||||
pair drifts from its theme. `.gate-group { column-gap: 0 }` is what makes the trunk's
|
||||
right edge and the spine the same x, so each tick starts *at* its trunk, and at ≤640px the stacked
|
||||
trunk is reconnected by bringing the first row's upper half back at `top: calc(var(--gate-row-gap) * -1)`.
|
||||
|
||||
@@ -2250,9 +2308,80 @@ and the legend names pipes because the panel heading already says which config f
|
||||
degradation arm uses the payload's own words — the `why` sentence for a pre-1.2.0/`--no-proxy` agent or
|
||||
a silent section, the `unreachable` dim for a host that has never answered, and `#gate-empty` (naming
|
||||
`GATEWAY_HOST` and agent **1.2.0**) for `404 {configured: false}` — and a failed poll re-draws nothing,
|
||||
so the last topology stays on screen. The footer is **three** paragraphs and no more: why a line is
|
||||
drawn and what its motion means; why a **name** endpoint carries no number; and that the switch is not
|
||||
wired. The layout is three load-bearing declarations (`minmax(0, 9.5rem) minmax(0, 1fr)` per group so
|
||||
so the last topology stays on screen. The footer is **four** paragraphs and no more: why a line is
|
||||
drawn and what its motion means; why a **name** endpoint carries no number; that the switch is not
|
||||
wired; and what the filter box does to the map and what it leaves alone — the sentence is there because
|
||||
the field is the one control that could be read as a query, and it is not one.
|
||||
|
||||
**`#gate-filter` is an eye, not a query** (phase 31 task 04). 33 routes in config order is not a
|
||||
lookup, and the operator's question is always about one domain, so one field above the panels hides the
|
||||
rows whose **own payload text** does not contain what was typed: `in.port`, `out.host`, `out.name`,
|
||||
`out.kind` and every `out.selectors` entry for a route (`haystack()`), `:${port}` plus
|
||||
`listeners[].domains` and `.clients` for a trunk (`trunkHaystack()`). Nothing else is readable and
|
||||
nothing else is matched — no DNS, no re-ranking, no rule about `.reeselink.com`, and no page-level
|
||||
split by domain suffix, because "`reeseapps.com` vs `stackexpected.com`" is a fact neither config file
|
||||
states and grouping by it would be inventing a taxonomy the ingress never declared (rule 2); the
|
||||
operator types the suffix and the page hides the rest. A **trunk** that matches keeps its whole
|
||||
fan-out: typing `4443` asks for the port, and its routes are the answer. Visibility is applied inside
|
||||
the patch pass (`renderLeaves` sets `li.hidden` by index into `group.connections`, `renderGroups` sets
|
||||
`el.hidden` and the panel's `[data-k="no-match"]`), so a poll cannot undo the filter, a hidden row
|
||||
keeps its identity and its `<svg>`, and only the *config* removing a route ever deletes a node. The
|
||||
repaint is `renderPanels(state.snapshot, state.row)` — **no** `fetchJSON` and no `refresh()` on the
|
||||
filter path, which is what the type of the feature is (`refresh()` would add a history pair per
|
||||
keystroke and change nothing), `Escape` clears the field because a filter you cannot leave with the
|
||||
keyboard is a trap on a page whose other shortcuts are all keys, `#gate-filter-count` reads
|
||||
`N of M routes` (leaf routes, both panels, and hidden while the field is empty), and
|
||||
`renderHeader()`/`renderWarnings()` hold no reference to `state.filter` at all: `ports`,
|
||||
`connections`, `live` and `warnings` describe the ingress, not the operator's current view of it, and
|
||||
the warnings panel is a different claim rather than a shorter version of this one. The field is the
|
||||
page's own markup wearing the sheet's shared field skin (`input[type="search"]` joined the
|
||||
`input[type="text"], input[type="password"]` rule — the only three field types the app has), it is
|
||||
`autocomplete="off" spellcheck="false"` with an `aria-label`, `.gate-filter` caps it at `18rem` on a
|
||||
desktop and goes `width: 100%` at ≤640px, and it declares no `:hover`, no `cursor` and no
|
||||
`transition` because the connection map owns one interactive-looking thing (the unwired knob) and this
|
||||
is not it. `data/logs/31-task04-filter.cjs` measured it on the live ingress (transcript
|
||||
`31-task04-filter.out`, screenshots `31-task04-filter-{stackexpected,no-match,cleared,360}.png`):
|
||||
`stackexpected` → 6 of 33 rows with `nginx:443` and `haproxy:2000` the only trunks standing, a client
|
||||
address → that port's whole fan-out, `no-such-domain-anywhere` → every group hidden with both panels
|
||||
saying `no route matches "…"` and neither the tallies (`10 / 33 / 12 / 0`, byte-identical either side
|
||||
of every keystroke) nor the warnings panel moving, **zero** requests across five keystroke states, the
|
||||
same filtered view after a real 30 s poll as before it, and 0 px of sideways document scroll at 360 px
|
||||
where the field is 320 px wide. `34 PASS / 0 FAIL`.
|
||||
|
||||
Phase 31's own pass measured the whole map rather than its parts, and is the pin that the three
|
||||
readability changes landed together: `data/logs/31-route-readability.cjs` (transcript
|
||||
`31-route-readability.out`, which opens with `data/logs/verify-31-page-only.py`'s payload proof,
|
||||
screenshots `31-gateway-*.png`, twelve frames — 1440, 360, light, filtered, no-match, trunk-live,
|
||||
phone, and one per degradation arm: predates, unconfigured, unreachable, no-nginx, sockets-quiet) —
|
||||
signs in and reads **computed** colour, not classes — every segment of all 33 rows (both spine
|
||||
halves, the tick, the run, the knob) computes to `--gate-on`, `rgb(63, 185, 80)` in dark and `rgb(26, 127, 55)` after the real
|
||||
theme toggle, and **no dashed border anywhere inside `.gate-map` computes `--border-strong`**
|
||||
(`rgb(51, 59, 77)` dark / `rgb(195, 202, 214)` light — the grey plumbing the operator read as
|
||||
inactivity), with **0** boxes in the map carrying a verdict class in either theme. The motion truth
|
||||
table comes out of the DOM against the payload's own numbers: every trunk the kernel reported ≥ 1 for
|
||||
animated, every one it reported 0 for drew a motionless line, and each `0 live` string corresponded to
|
||||
a port the payload carried 0 for — a reading of traffic, never a stored fact. A second, deterministic
|
||||
arm settles the direction of the claim: with `:443` handed 7 sockets and **every** leaf's count set to
|
||||
`null`, the trunk animated and 0 of its 18 leaves did, so a port's number provably does not leak into
|
||||
its fan-out. The arm where the socket table
|
||||
answers nothing draws **ten motionless `n/a` stubs** reading `connections on this port are unknown: the
|
||||
socket table did not answer`, with **no** `0 live` ink anywhere, every leaf keeping its `—`, the header's
|
||||
`live` tally `n/a` — and no panel taking the shared `stale` class, because the configs *did* answer. It
|
||||
then holds that the **leaf** keeps its own claim: with a trunk live and the leaf under it at `—`, the
|
||||
trunk animates and that leaf does not, because a port's sockets are not one route's. The filter pass is
|
||||
zero requests across five keystroke states with the four tallies byte-identical either side of every
|
||||
keystroke and `Escape` clearing the same way; `prefers-reduced-motion: reduce` stills all 43 paths (10 of
|
||||
them trunk stubs, through the block's descendant selectors) and every knob; the degradation arms each
|
||||
show their own sentence (`404 {configured: false}`, a `supported: false` agent, a silent socket section,
|
||||
a never-answered host); and the document scrolls **0 px** sideways at 1440 px and at 360 px with both
|
||||
charts open and the widest fan-out on screen. It also found, and fixed, the one defect of the phase that no class-based check could see:
|
||||
`svg.title = aria` assigned a JavaScript property and produced **no tooltip**, because `title` is a
|
||||
reflected IDL attribute of `HTMLElement` and not of `SVGElement` — the two map tooltips are
|
||||
`setAttribute('title', …)` calls now, which is what the browser can show and what
|
||||
`tests/test_api.py` demands. Its mobile-audit sibling is `31-task05-mobile-audit.out` (the
|
||||
`27-task02-mobile-audit.cjs` sweep, 371 checks, 0 failures, 0 findings, exit 0).
|
||||
|
||||
The layout is three load-bearing declarations (`minmax(0, 9.5rem) minmax(0, 1fr)` per group so
|
||||
a fan-out lines its leaves up instead of re-flowing them to the widest target, `minmax(0, …)` on every
|
||||
track so a 40-character hostname ellipsises instead of growing the card — phase 27's rule — and one
|
||||
column with the trunk above its leaves at ≤640 px). Task 04 measured that layout with
|
||||
@@ -3145,9 +3274,10 @@ memory plot; the `cpu.usage_pct@coreN` series is still stored and still queryabl
|
||||
ordinary rotation hygiene, not a missing step.
|
||||
|
||||
13. **`/gateway`'s long endpoint labels are ellipsised on a phone — a column-width question, not a
|
||||
lost fact** (found by phase 29 task 06's live pass, **closed at the desktop width by phase 30
|
||||
task 04's trunk→leaf row and re-measured on 2026-10-23 by task 06's pass**;
|
||||
`data/logs/30-connection-map.out` is the current measurement, `data/logs/30-task04-panels.out`,
|
||||
lost fact, and the filter is what a clipped label used to block** (found by phase 29 task 06's
|
||||
live pass, **closed at the desktop width by phase 30 task 04's trunk→leaf row and re-measured
|
||||
after phase 31 re-ranked the row on 2026-10-26**; `data/logs/31-route-readability.out` is the
|
||||
current measurement, `data/logs/30-connection-map.out`, `data/logs/30-task04-panels.out`,
|
||||
`data/logs/29-task06-live.out` and `data/logs/29-task06-card-haproxy.png` the history).
|
||||
|
||||
**Phase 30 task 04 closed the desktop half by changing the row, not the font.** The two
|
||||
@@ -3160,26 +3290,35 @@ memory plot; the `cpu.usage_pct@coreN` series is still stored and still queryabl
|
||||
and not a bug: the longest target here is 42 characters
|
||||
(`reeseapps-brain-of-code.reeselink.com:443`) and it fits.
|
||||
|
||||
The arm that remains is the **phone**, and it got narrower before it got better: task 05's
|
||||
**switch** moved into the leaf row and a group stacks to one column at 360 px, so the same labels
|
||||
now clip at **31 of 33** targets and **22 of 33** selectors in a **139 px** column (this phase's
|
||||
re-run; 27 / 13 in a 177 px column when task 04 measured it, the difference being the switch
|
||||
pill's 34 px and the tick's 26 px taken off the label). Nothing is lost —
|
||||
`renderLeaf()` puts the endpoint *and* the config's own selector on the row as `title` and
|
||||
`leafAria()` carries the whole endpoint on the line, which is what
|
||||
`tests/test_api.py::test_a_clipped_pipe_label_keeps_its_full_text_on_the_element` pins, alongside
|
||||
the declarations that decide *how much* it clips (`min-width: 0` on `.gate-target`,
|
||||
`minmax(0, 1fr)` on the leaf track, `max-content` on `.pipe-name`, `flex-wrap` on the trunk) so a
|
||||
future edit cannot squeeze a label back to the seven characters phase 29 measured. In order of cost:
|
||||
(a) at ≤640 px let the leaf row put the count on the target's second line instead of its own
|
||||
column, which buys roughly two more characters per label and is a stylesheet-only change;
|
||||
(b) accept it — the fan-out is read as a set, the part that fits is the selector, and the number
|
||||
the row is about is the count. Whoever picks this up re-runs
|
||||
`data/logs/30-connection-map.cjs` (task 06's pass, which supersedes
|
||||
`data/logs/30-task04-panels.cjs`): its two `FINDING` lines are the counts at both widths, and the
|
||||
run **fails** if any line ever stops carrying its full text — 33 of 33 rows carried a `title`
|
||||
naming their whole endpoint at 1440 px and at 360 px as this phase closed, which is the assertion
|
||||
the gap is actually about.
|
||||
**Phase 31 kept the desktop half closed and moved the phone's clipping to where it belongs.** Its
|
||||
pass (`data/logs/31-route-readability.cjs`, both widths, both themes) reads **0 of 33** inbound
|
||||
lines and **0 of 33** counterpart lines ellipsised at 1440 px in the same **361 px** leaf column,
|
||||
with the longest inbound word 35 characters and the longest counterpart line 50 — the row got a
|
||||
second line and a `→ … · kind` wrapper and still fits. At **360 px** a group stacks and the column
|
||||
is **139 px**, so the two lines now clip at **19 of 33** inbound and **33 of 33** counterpart.
|
||||
That is the intended direction of the trade: phase 31 task 02 made line 1 the config's own short
|
||||
word for *who reaches* the endpoint (`server_name`, `map` key, `backend` name) and demoted the long
|
||||
`host:port` to line 2, so what still fits is the part an operator reads and what clips is the part
|
||||
they read once. Nothing is lost: `renderLeaf()` puts **both** ends whole on the row's `title` and
|
||||
`leafAria()`/`leafTitle()` carry them on the line, which is what
|
||||
`tests/test_api.py::test_a_clipped_pipe_label_keeps_its_full_text_on_the_element` pins, and the
|
||||
browser pass measured 33 of 33 rows carrying their whole pair at both widths — as a **painted**
|
||||
tooltip now, because that pass found `svg.title = aria` had been assigning a property
|
||||
`SVGElement` does not reflect and showing nothing, and the fix (`setAttribute`) is what makes
|
||||
"nothing is lost" true in a browser rather than only in the DOM. The declarations that decide *how
|
||||
much* clips stay pinned (`min-width: 0` on `.gate-target`, `minmax(0, 1fr)` on the leaf track,
|
||||
`max-content` on `.pipe-name`, `flex-wrap` on the trunk) so a future edit cannot squeeze a label
|
||||
back to the seven characters phase 29 measured. What changed the gap's *cost* is task 04's filter:
|
||||
the reason a clipped label mattered was that a long target could not be **found**, and typing
|
||||
`brain` now hides 31 of 33 rows without one request, matching whole text CSS never showed. In order
|
||||
of cost for what is left: (a) at ≤640 px let the leaf row put the count on the counterpart's second
|
||||
line instead of its own column, which buys roughly two more characters per label and is a
|
||||
stylesheet-only change; (b) accept it — the fan-out is read as a set, the part that fits is the
|
||||
inbound word, and the number the row is about is the count. Whoever picks this up re-runs
|
||||
`data/logs/31-route-readability.cjs` (which supersedes `data/logs/30-connection-map.cjs`): its two
|
||||
`FINDING` lines are the counts at both widths, and the run **fails** if any row ever stops carrying
|
||||
its full pair — 33 of 33 at 1440 px and at 360 px as this phase closed, which is the assertion the
|
||||
gap is actually about.
|
||||
|
||||
How it got here, kept because the two rejected fixes are the reason the current shape looks the
|
||||
way it does: at 1440 px phase 29's outbound name showed **seven characters** (`reeseapps-…`) for a
|
||||
@@ -3191,6 +3330,26 @@ memory plot; the `cpu.usage_pct@coreN` series is still stored and still queryabl
|
||||
declarations and removed the reason they were needed: with the trunk in its own track, the leaf's
|
||||
name is the widest thing in a column that is now wide enough for it.
|
||||
|
||||
14. **A route whose endpoint the config writes as a *name* is drawn and never counted —
|
||||
`32_gateway_resolved_target_liveness` is the phase that removes the reason** (recorded by phase
|
||||
31 task 05, which wrote no design for it). Today 31 of this ingress's 33 routes end in a
|
||||
`server_name`/`map` target and so carry an em dash where a live count would go, because
|
||||
`sources/gateway.py` matches sockets by
|
||||
address and resolves nothing (rule 2 by choice, and pinned as an absence). The follow-on phase is
|
||||
the one that makes such a target answer with its liveness and its live connections anyway — the
|
||||
agent reporting resolved targets on the box that owns the config, which is an `AGENT_VERSION` bump
|
||||
to **1.3.0** and therefore an agent + source + pin change, not a page change. Its acceptance line
|
||||
is that a dash disappears from `/gateway` for a reason and not by hiding the column.
|
||||
|
||||
15. **`/gateway`'s switches are knobs with no write behind them — `33_gateway_route_switching` is the
|
||||
phase that decides whether they ever get one** (recorded by phase 31 task 05, no design written).
|
||||
Enabling or disabling a route is a second gated upstream write path in front of the proxy configs,
|
||||
which **amends invariant 1** (today `POST /api/ups/command` is the app's only upstream write, and
|
||||
the route enumeration in `tests/test_api.py` is the pin that says so) and needs the operator's
|
||||
sign-off in `.agents/PLAN.md` **before** it is written as a phase. Until that decision exists the
|
||||
knobs stay `aria-disabled`, unbound, and explained in the footer; the `PATCH`-shaped endpoint, the
|
||||
config writer and the rollback are that phase's work and none of it is here.
|
||||
|
||||
## Locked decisions (and why)
|
||||
|
||||
- **SQLite, not Prometheus/Grafana.** Handful of boxes, 1–10s resolution, one file the
|
||||
|
||||
Reference in New Issue
Block a user