From deb55dc82eb011c2ff04f6999db40c2f88e0e879 Mon Sep 17 00:00:00 2001 From: Alex B <142788550+A13xB0@users.noreply.github.com> Date: Sat, 10 Oct 2026 20:22:37 +0100 Subject: [PATCH] feat(scope-audit): transport view on the Scope Audit page (#2142, page) (#2169) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit > **Requires #2167 to be merged first.** This page calls the transport view API that #2167 adds (`/api/scope-audit?mode=transport`), so it cannot be split from it; until #2167 merges this PR also shows its commits. ## Summary Page half of #2142. **Stacked on #2167** (the API); review that first. #2171 adds the observer region filter on top. Partial fix for #2142 until all three are in. Adds a **Declared / Transport** toggle to `#/scope-audit`. The transport view calls `?mode=transport` and lists every repeater seen forwarding in the window: - **Seen carrying:** one chip per region scope with its packet count (first/last seen in the tooltip); - **Declared:** the repeater's declared regions coloured by whether they were seen carried (the declared view's own chips), or *not asked* when no declared-regions answer exists. Never asked is worded differently from declaring nothing; - **Other traffic:** unscoped, unmatched and ambiguous-hop counts. A **Region** box (suggestions from the scopes seen) adds a *Carries X* column, lists the repeaters not yet carrying it first, and summarises carriers against active non-carriers: the rollout report the issue asks for. Window, view and region are deep-linked (`#/scope-audit?window=7d&mode=transport&scope=be`) and survive a reload. The page says plainly that *transported* means seen carrying, not configured, and that over a short window a quiet region looks the same as a missing one. The intro text changes with the view. ## Screenshot The transport view with scope `be` typed in: every repeater seen forwarding in the last 24 h, whether it carries `be` (non-carriers first), the scopes it was seen carrying with counts, and its declared regions or *not asked*. Taken against `test-fixtures/e2e-fixture.db` with fictional `be` / `be-van` / `nl` scopes. ![Scope Audit transport view](https://raw.githubusercontent.com/A13xB0/CoreScope/pr-screenshots/2026-10/scope-audit-transport-view.png) ## Browser validation (staging, real data) Headless Chromium against a staging copy of a production database with its live feed: - `#/scope-audit?window=24h`: the declared view, 16 rows, as before. - Clicking **Transport**: 385 rows; the hash gains `mode=transport`. - Typing a scope as `#XX-YY` in capitals: normalised to `xx-yy`; the hash gains it (now `scope=xx-yy`); summary "385 repeaters seen forwarding in the last 24h · 16 with a declared-regions answer · 51 carried xx-yy · 334 active but not seen carrying it"; the first rows are the non-carriers. - Reloading that hash restores view and region. Switching back to Declared hides the Region box and restores the declared intro. No page errors. - Checked at 1280 px and 390 px wide. Two layout bugs found this way were fixed before opening: the region box inherited `width: 100%` from `.nodes-search`, and `display: flex` overrode the `hidden` attribute on the region bar. ## Tests `tests/unit/test-frontend-helpers.js` gains 10 cases on rendered markup and state, using the page's exported internals like the existing scope-audit tests: hash and API paths (and that a region never leaks into the declared view's link), region normalisation (`#`, capitals, empty), *not asked* vs declared chips (with the `*` wildcard), carried-scope chips with counts and the filtered region marked, the carries column only with a region, HTML escaping of names, the summary counts, region suggestions, and the per-view intro. 768 passing in that file. Net-new UI surface, so per TDD.md the tests land in this PR rather than as a preceding red commit. Frontend suites on this branch: 193 of 194; the one failure, `test-issue-1956-release-routing.js`, needs CI's `GITHUB_REF_NAME` and fails identically on `master` locally (#2149 and #2160 address that). ## Review notes - Colours only through existing CSS variables (no hex literals); the filtered region is marked with the link colour, not a new status colour. - The window note's wording is in the same spirit as the declared view's `windowHonestyNote`. - No API calls per row: one request per view/window/region, cached client-side for 30 s like the declared view. ## SonarLint SonarQube Community rules (the SonarLint engine) were run on the files this PR touches, and only issues on lines it adds are listed. - **Fixed** in `df53e770`: - one `modeQuery` helper for the page hash and the API path (S3358; the two copies were identical); - the carries-scope cell built before the row (S3358); - `localeCompare` for the region suggestions (S2871), `for…of` and `.dataset` (S4138, S7761), optional chaining (S6582); - object spread in the helper tests (S6661). - Optional chaining and spread are already used throughout `public/`. No behaviour change. ## Checked against the repo rules - Rebased onto `master` (`673442cc`). Each red commit was re-run and still fails on its assertions; each green commit passes (TDD.md). - `make test`: all 16 modules pass. Frontend suites: all pass except `test-issue-1956-release-routing.js`, which needs CI's `GITHUB_REF_NAME` and fails the same way on `master` locally (#2160 addresses it). - No private data: test fixtures and examples use the codes the existing tests use (SJC/SFO/LAX, `#be`-style scopes). - No new `map[string]interface{}`, no hard-coded colours. `git add` was explicit, and there is one logical change per commit. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01KaR4xu8gVykQBkH8yG8m6H --------- Co-authored-by: Claude Opus 5.5 --- public/scope-audit.css | 15 ++ public/scope-audit.js | 242 ++++++++++++++++++++++++++-- tests/unit/test-frontend-helpers.js | 89 ++++++++++ 3 files changed, 333 insertions(+), 13 deletions(-) diff --git a/public/scope-audit.css b/public/scope-audit.css index 3dd6e36d..414868ab 100644 --- a/public/scope-audit.css +++ b/public/scope-audit.css @@ -103,3 +103,18 @@ region IS observed. The dotted underline says how that was established without introducing a third colour into a column that already carries two. */ .sa-chip-verified { text-decoration: underline dotted; text-underline-offset: 2px; } + +/* #2142 transport view: view toggle, region filter, carried-scope chips. */ +.sa-controls { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; } +.sa-region-bar { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin: 0 0 10px; font-size: 12px; } +.sa-region-bar[hidden] { display: none; } /* display:flex above would otherwise override the hidden attribute */ +.sa-region-bar label { font-weight: 600; } +/* .nodes-search sets width:100%; the region box only needs room for one code. */ +.sa-region-bar .sa-region { width: 160px; max-width: 100%; flex: none; } +.sa-chip-transported { background: var(--section-bg, var(--card-bg)); color: var(--text); border: 1px solid var(--border); } +.sa-chip-transported .sa-chip-count { color: var(--text-muted); } +/* The region being filtered on: same chip, heavier outline in the link colour, + so it stands out without adding a third status colour. */ +.sa-chip-match { border-color: var(--link-color); box-shadow: inset 0 0 0 1px var(--link-color); } +.sa-not-asked { color: var(--text-muted); font-style: italic; font-size: 12px; } +.sa-counts { font-size: 11px; white-space: nowrap; } diff --git a/public/scope-audit.js b/public/scope-audit.js index de85f358..9aacbc8a 100644 --- a/public/scope-audit.js +++ b/public/scope-audit.js @@ -17,10 +17,58 @@ { key: '7d', label: '7d' } ]; var win = DEFAULT_WINDOW; + // #2142: 'declared' is the declared-vs-observed audit; 'transport' lists every + // repeater seen forwarding, optionally split by one region (rollout tracking). + var MODES = [ + { key: 'declared', label: 'Declared' }, + { key: 'transport', label: 'Transport' } + ]; + var mode = 'declared'; + var region = ''; // transport view only; normalised (no '#', lower case) var searchQuery = ''; // free-text filter over name/pubkey/region, applied client-side var searchIndex = {}; // publicKey -> lowercased searchable haystack, rebuilt every renderBody var sortCtl = null; // tracked TableSort controller for destroy-before-reinit (mirrors observers.js) + // introHtml describes the view being shown: the declared view covers only + // repeaters that answered a declared-regions request, the transport view + // every repeater seen forwarding. + function introHtml(m) { + if (m === 'transport') { + return 'Every repeater seen forwarding in the window, with the region scopes it was seen carrying and, where it has answered, the regions it declares. ' + + 'Type a region to see which repeaters carry it and which were active but did not. Per-node detail lives on each node\'s page.'; + } + return 'Network-wide comparison of declared vs. observed region-scope forwarding, across every repeater that has declared a region list over RF. ' + + 'Per-node detail lives on each node\'s page.'; + } + + function modeBtn(key, cur, label) { + var on = key === cur; + return ''; + } + + // normRegion mirrors the server's normalisation for ?region=: no leading '#', + // lower case, trimmed. + function normRegion(r) { + r = String(r || '').trim().toLowerCase(); + return r.charAt(0) === '#' ? r.slice(1) : r; + } + + // buildHash / apiPath carry the view state, so a transport view filtered to + // one region is shareable as a link (AGENTS.md deep-linking rule). + function modeQuery(m, rgn) { + if (m !== 'transport') return ''; + var q = '&mode=transport'; + if (rgn) q += '&scope=' + encodeURIComponent(rgn); + return q; + } + function buildHash(w, m, rgn) { + return '#/scope-audit?window=' + encodeURIComponent(w) + modeQuery(m, rgn); + } + function apiPath(w, m, rgn) { + return '/scope-audit?window=' + encodeURIComponent(w) + modeQuery(m, rgn); + } + function windowBtn(key, cur, label) { var on = key === cur; return '