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 '