diff --git a/public/scope-audit.js b/public/scope-audit.js index 7ef128b8..7aa2017a 100644 --- a/public/scope-audit.js +++ b/public/scope-audit.js @@ -47,7 +47,7 @@ WINDOWS.map(function (w) { return windowBtn(w.key, win, w.label); }).join('') + '' + '
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.
' + - '
The declared side is the repeater’s own answer, read back off the node by an observer running the neighbour-report firmware or by the CoreDrive RX app. The observed side is forwarding CoreScope already sees in its own traffic.
' + + sourcesLineHtml() + '' + '
Loading scope audit…
' + ''; @@ -58,6 +58,17 @@ return '' + escapeHtml(age) + ''; } + // sourcesLineHtml is the always-visible half of the provenance note. It has + // to carry the links itself: the fuller explanation lives in emptyStateHtml, + // which by definition never renders on an instance that HAS data, so an + // operator with a full table would otherwise never see where it came from. + function sourcesLineHtml() { + return '
The declared side is the repeater’s own answer, read back off the node by an ' + + 'ESP32 observer on the neighbour-report firmware or by ' + + 'CoreDrive RX. The newest answer per repeater wins, whichever collected it. ' + + 'The observed side is forwarding CoreScope already sees in its own traffic.
'; + } + // mergedScopeChips renders ONE chip per declared region, coloured by whether // that region was actually observed forwarding in the window. // @@ -76,7 +87,7 @@ // explanation rather than deferring to the intro: on a fresh deployment this // IS the page. Named and returned rather than inlined so it can be asserted. function emptyStateHtml() { - return '
No repeater has answered with its configured region list yet, so there is nothing to audit here.

That answer has to be collected from the repeater itself; nothing else in CoreScope knows which regions a node is configured for, only which ones its traffic was seen under. Two things can collect it, and neither ships with CoreScope, so an empty table is the normal state until you run one:The newest answer per repeater wins, whichever collected it.
'; + return '
No repeater has answered with its configured region list yet, so there is nothing to audit here.

That answer has to be collected from the repeater itself; nothing else in CoreScope knows which regions a node is configured for, only which ones its traffic was seen under. Two things can collect it, and neither ships with CoreScope, so an empty table is the normal state until you run one:The newest answer per repeater wins, whichever collected it.
'; } function mergedScopeChips(row) { @@ -349,7 +360,7 @@ // Exposed so the helper tests can assert what the Scopes column RENDERS // rather than grepping this file, the same reason map.js exposes its label // builder (#1356/#1933). - window.__meshcoreScopeAuditInternals = { mergedScopeChips: mergedScopeChips, emptyStateHtml: emptyStateHtml }; + window.__meshcoreScopeAuditInternals = { mergedScopeChips: mergedScopeChips, emptyStateHtml: emptyStateHtml, sourcesLineHtml: sourcesLineHtml }; } registerPage('scope-audit', { init: init, destroy: destroy }); diff --git a/test-frontend-helpers.js b/test-frontend-helpers.js index 4fc9f4fc..a854603e 100644 --- a/test-frontend-helpers.js +++ b/test-frontend-helpers.js @@ -7056,7 +7056,8 @@ console.log('\n=== scope-audit.js: emptyStateHtml ==='); test('links to both so the reader can act on it', () => { assert.ok(empty.includes('observer.gessaman.com'), 'observer firmware link'); - assert.ok(empty.includes('coredrive-rx'), 'companion app link'); + // Upstream links the repo, this fork links its own hosted instance. + assert.ok(/rx\.on8ar\.eu|coredrive-rx/.test(empty), 'companion app link'); }); test('says an empty table is normal, not a fault', () => { @@ -7079,3 +7080,36 @@ console.log('\n=== scope-audit.js: emptyStateHtml ==='); assert.ok(!/as devices drive/i.test(empty)); }); } + +// The always-visible provenance line. It must carry the links itself: the +// fuller explanation lives in the empty state, which never renders on an +// instance that HAS data, so an operator with a full table would otherwise +// never learn where the declared column came from. That was the actual bug. +console.log('\n=== scope-audit.js: sourcesLineHtml ==='); +{ + const ctx = makeSandbox(); + ctx.registerPage = () => {}; + loadInCtx(ctx, 'public/app.js'); + loadInCtx(ctx, 'public/scope-audit.js'); + const line = ctx.__meshcoreScopeAuditInternals.sourcesLineHtml(); + + test('carries a link for each collector, not just their names', () => { + assert.strictEqual((line.match(/ { + assert.strictEqual((line.match(/rel="noopener"/g) || []).length, 2); + }); + + test('separates the two claims the page rests on', () => { + assert.ok(/declared/i.test(line) && /observed/i.test(line)); + assert.ok(/read back off the node/i.test(line), 'declared is the node answering'); + assert.ok(/already sees in its own traffic/i.test(line), 'observed is our own data'); + }); + + test('states the precedence rule where a reader with data will see it', () => { + assert.ok(/newest answer per repeater wins/i.test(line)); + }); +}