diff --git a/public/scope-audit.css b/public/scope-audit.css index f782b2c9..f5074e8e 100644 --- a/public/scope-audit.css +++ b/public/scope-audit.css @@ -62,3 +62,20 @@ a short window is weak evidence, which the page header says in words, so it should not shout in colour. */ .sa-chip-unobserved { background: var(--section-bg, var(--card-bg)); color: var(--text-muted); border: 1px solid var(--border); } + +/* Provenance note under the intro. Same muted treatment as .sa-intro; it is + context, not a finding, and must not compete with the table. */ +.sa-sources { color: var(--text-muted); font-size: 12px; margin: 0 0 10px; line-height: 1.5; } +.sa-sources a { color: var(--link-color); } +.sa-sources code { font-family: var(--mono); font-size: 11px; } + +/* Provenance note under the intro, and the empty state that has to carry the + same explanation on its own: on a stock install the table IS empty, so that + is where an operator actually reads it. Muted like .sa-intro; this is + context, not a finding, and must not compete with the table. */ +.sa-sources { color: var(--text-muted); font-size: 12px; margin: 0 0 10px; line-height: 1.5; } +.sa-sources a, .sa-empty a { color: var(--link-color); } +.sa-sources code, .sa-empty code { font-family: var(--mono); font-size: 11px; } +.sa-empty { line-height: 1.6; max-width: 70ch; } +.sa-empty ul { margin: 8px 0; padding-left: 20px; } +.sa-empty li { margin: 4px 0; } diff --git a/public/scope-audit.js b/public/scope-audit.js index 47fea207..7ef128b8 100644 --- a/public/scope-audit.js +++ b/public/scope-audit.js @@ -47,6 +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.
' + '' + '
Loading scope audit…
' + ''; @@ -71,6 +72,13 @@ // No new claim is made about the data: a region present in declaredRegions // and absent from notObserved is exactly what the server already means by // "observed forwarding in this window". + // emptyStateHtml is what a stock install sees, so it carries the whole + // 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.
'; + } + function mergedScopeChips(row) { var missing = Object.create(null); row.notObserved.forEach(function (n) { missing[n] = true; }); @@ -249,7 +257,7 @@ sortCtl = null; if (!d.repeaters.length) { searchIndex = {}; - el.innerHTML = '
No repeater has declared a region list yet — this fills in as devices drive and answer over RF.
'; + el.innerHTML = emptyStateHtml(); return; } searchIndex = buildSearchIndex(d.repeaters); @@ -341,7 +349,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 }; + window.__meshcoreScopeAuditInternals = { mergedScopeChips: mergedScopeChips, emptyStateHtml: emptyStateHtml }; } registerPage('scope-audit', { init: init, destroy: destroy }); diff --git a/test-frontend-helpers.js b/test-frontend-helpers.js index 469c0d33..4fc9f4fc 100644 --- a/test-frontend-helpers.js +++ b/test-frontend-helpers.js @@ -7037,3 +7037,45 @@ console.log('\n=== scope-audit.js: mergedScopeChips ==='); assert.ok(!h.includes('ghost')); }); } + +// The empty state is what a stock install sees: neither collector ships with +// CoreScope, so most deployments open this page and find nothing. It therefore +// has to explain where the data comes from, not just report its absence. +console.log('\n=== scope-audit.js: emptyStateHtml ==='); +{ + const ctx = makeSandbox(); + ctx.registerPage = () => {}; + loadInCtx(ctx, 'public/app.js'); + loadInCtx(ctx, 'public/scope-audit.js'); + const empty = ctx.__meshcoreScopeAuditInternals.emptyStateHtml(); + + test('names both collectors', () => { + assert.ok(/neighbour-report firmware/i.test(empty), 'should name the observer firmware'); + assert.ok(/CoreDrive RX/i.test(empty), 'should name the companion app'); + }); + + 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'); + }); + + test('says an empty table is normal, not a fault', () => { + assert.ok(/normal state/i.test(empty)); + }); + + test('states the precedence rule, so two collectors are not confusing', () => { + assert.ok(/newest answer per repeater wins/i.test(empty)); + }); + + test('says what nothing else in CoreScope can tell you', () => { + // The reason the page exists at all: observed traffic shows which scopes a + // node carried, never which it is configured for. + assert.ok(/configured/i.test(empty) && /traffic was seen under/i.test(empty)); + }); + + test('does not use the old drive-around wording', () => { + // "fills in as devices drive" was CoreDrive-specific jargon that meant + // nothing to an operator running the observer firmware instead. + assert.ok(!/as devices drive/i.test(empty)); + }); +}