# Phase 31 — The gateway route is green end to end, and the row says *which domain* **Source:** operator request, 2026-10-23 (post-phase-30 review of `/gateway`) — "The only animated line is `:443 -> 127.0.0.1:4443`. I can't tell which `reeseapps.com` or `stackexpected.com` addresses flow to their reeselink counterparts (very important). Why are the dotted lines on the left grey? Shouldn't those be green too? They're active." **Story:** n/a **Context:** `src/dashboards/web/static/dash.css` (the `gateway pipes` section: `.pipe-path`, `.gate-link::before`/`::after`, `.gate-elbow`, `.gate-trunk`, `.gate-target`, `.gate-selector`, `.pipe-name`), `src/dashboards/web/static/page-gateway.js` (`leafRow`/`renderLeaf`/`groupElement`/`renderTrunk`/`selectorText`/`leafTitle`/`leafAria`/`endpointText`), `src/dashboards/web/pages/gateway.html` (the two panels, the four tallies, the three footer paragraphs), `src/dashboards/sources/gateway.py` (**read-only for this phase** — `connections[].out.{host,port,name,kind,selectors,address}`, `listeners[].{connections,clients,domains,metric}`), `tests/test_api.py` (the gateway page pins: `GATEWAY_PAGE_FIELDS`, `test_a_clipped_pipe_label_keeps_its_full_text_on_the_element`, `test_a_pipe_moves_only_while_it_carries_a_connection`, `test_the_pipe_animation_stills_in_both_states_under_reduced_motion`, `test_no_gateway_count_is_coloured_by_a_verdict`, `test_an_unattributed_endpoint_prints_a_dash_and_never_a_zero`, `test_every_pipe_is_an_accessible_image_built_from_the_payload`), `data/logs/27-task02-mobile-audit.cjs` (the nine-page sweep that must still exit 0). ## Objective `/gateway` answers the operator's actual question — *"which inbound domain flows to which counterpart?"* — from the row itself, and **every route a config declares is green from its trunk to its endpoint** instead of grey for two thirds of its length. The spine and the tick stop being structural ink and take the same existence pair as the run and the switch; the leaf row leads with the config's **inbound** word (`brain.reeseapps.com`) and puts the counterpart (`reeseapps-brain-of-reese.reeselink.com:443`) under it; the trunk draws its own short line that moves on the one live number the kernel genuinely reports for that **port** (`listeners[].connections`), so the page is not dead ink while a name target waits for phase 32; and one filter box finds a domain among the 33 routes without inventing a taxonomy. This phase touches **no Python**: the payload already carries every string and every number these four changes need. `sources/gateway.py` is deliberately left byte-identical so phase 32's join rewrite (`todo/32_gateway_resolved_target_liveness`) lands on an unchanged surface. ## Scope decisions taken with the operator (binding for this phase) | # | Decision | Consequence | |---|----------|-------------| | 1 | **Colour is still existence, and the whole route carries it.** | `.gate-link::before`, `.gate-link::after` and `.gate-elbow` take `--gate-on` / `--gate-off`, not `--border-strong`. `on`/`off` moves onto the `
  • ` carries `on`/`off`; CSS colours spine halves, tick, run and switch from it; no connector rule reads `--border-strong`), `test_the_leaf_row_leads_with_the_inbound_word` (line 1 is `selectors`-derived, line 2 is the endpoint; the served field table resolves both), `test_the_trunk_pipe_is_an_accessible_image_of_its_port`, `test_the_filter_matches_only_payload_text_and_costs_no_request` (no `fetchJSON` in the filter path, warnings panel untouched, `null` counts never match a query as text `"null"`). - No new Python, so coverage cannot rise: the gate is that it does **not** fall (`--cov-fail-under=94` green) and that `node --check` passes on the changed script (`.agents/validate.sh` runs it). - Integration: the page renders from a fixture-driven dev store in every degradation arm the payload can produce (`unreachable`, `supported: false`, per-section `available: false/null`), with the filter empty, matching, and matching nothing. - The phase is "Complete" only when the **entire** project test suite passes 100% (every test in the project, not just this phase's) and `bash .agents/validate.sh` is green, coverage floor included. ## Documentation & Consistency - The final pass reviews `README.md` and `AGENTS.md` in full against the project as it stands after this phase — fixing every inconsistency found, **including pre-existing ones not caused by this phase**. For this phase that specifically means: the `/gateway` UI-conventions paragraph in **both** files (the row now reads inbound word → counterpart; the connectors are existence-coloured; the trunk's line moves on the port's own count; the filter exists and what it may not do), the claim that the page's footer is three paragraphs (only if task 02 changes the count — it must not silently), and **gap 13** (the clipped-endpoint-label gap: task 02 shortens the primary line to the inbound word and task 04 makes long targets findable, so the gap is either closed with measurements or restated honestly with the new clip counts). - Tasks update `README.md`/`AGENTS.md` immediately when their own changes are user-visible. ## Completion Criteria - [ ] On live data at 1440 px: **every** drawn route — spine halves, tick, run, switch — computes to the `--gate-on` colour in dark *and* light theme; no grey dashed segment remains anywhere in the connection map; measured, not asserted in prose. - [ ] The nginx `:443` fan-out reads as `brain.reeseapps.com` on line 1 and `→ reeseapps-brain-of-reese.reeselink.com:443` on line 2 for all of its leaves, and typing `stackexpected` into `#gate-filter` leaves only the five `stackexpected*` routes visible with the panel tallies unchanged. - [ ] The trunk pipe moves on a port the kernel reports ≥ 1 ESTABLISHED socket for and sits still on one it reports `0` for; with the socket-table arm stubbed to `null` it draws green, motionless, and prints no `0`. `prefers-reduced-motion: reduce` stops every animated thing on the page including the trunk pipe and the switch thumb. - [ ] `grep -n "border-strong" src/dashboards/web/static/dash.css` shows no gateway connector rule; `grep -n "severity\|--ok\|--warn\|--fail" ` over the gateway CSS section and `page-gateway.js` still finds only the warnings-panel `.warn` row class that `alerts.py` owns. - [ ] No `addEventListener` for the switch exists; `routeInConfig()` still returns `true`; `git diff` for this phase touches **no** file under `src/dashboards/` except `web/` — the verb-enumeration pin still lists `/api/ups/command` as the only upstream write. - [ ] `uv run pytest` (incl. `--cov` ≥ 94 floor), `uv run ruff check src tests agent`, `uv run pyright`, `node --check` on the changed script, `bash .agents/validate.sh` — all green. - [ ] `data/logs/31-*.png` screenshots at 1440 px and 360 px (dark, light, filtered, and each degradation arm) and `data/logs/31-*.out` transcripts; `data/logs/27-task02-mobile-audit.cjs` still exits 0 across all nine pages with 0 FAIL / 0 FINDING. - [ ] `GET /api/gateway/snapshot` on the live ingress unchanged byte-for-byte from before the phase (proof the phase was page-only), and `./scripts/dev-server.sh status` still reads all ten sources `ok`. - [ ] README.md and AGENTS.md are accurate and consistent with the whole project (verified in the final pass). - [ ] No behaviour change in completed work: the other five dashboards and `/settings`/`/certs` render as before; the gateway charts, warnings panel, staleness dim and `404 {configured:false}` arm behave exactly as phase 29/30 left them.