From 0d1961b88f4637370c7588add35e4bbd88355ba6 Mon Sep 17 00:00:00 2001 From: efiten Date: Sun, 6 Sep 2026 14:06:28 +0200 Subject: [PATCH] feat(scope-audit): say where the declared data comes from, and why yours is empty Nobody reading this page could tell how the declared list reaches the database or why theirs might hold nothing. The old empty state said it "fills in as devices drive and answer over RF", which is CoreDrive-specific jargon that says nothing to an operator running the observer firmware instead, and names no collector at all. Two changes: - A provenance line under the intro, always visible: the declared side is the repeater's own answer read back off the node, the observed side is forwarding CoreScope already sees in its own traffic. That distinction is the whole point of the page and it was never stated. - The empty state now carries the full explanation, because on a stock install it IS the page. It names both collectors with links (the ESP32 neighbour-report firmware and the CoreDrive RX app), states that neither ships with CoreScope so an empty table is the normal state rather than a fault, gives the precedence rule now that there are two sources, and says the thing that justifies the page existing: nothing else in CoreScope knows which regions a repeater is CONFIGURED for, only which ones its traffic was seen under. emptyStateHtml is extracted and exported rather than left inline so it can be asserted. Six tests: both collectors named, both linked, empty-is-normal stated, precedence stated, the configured-vs-observed distinction present, and the old drive-around wording gone. 670 to 676 passed, 0 failed. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01PQS3XLoPD98yu9pxdRujqg --- public/scope-audit.css | 17 ++++++++++++++++ public/scope-audit.js | 12 ++++++++++-- test-frontend-helpers.js | 42 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 69 insertions(+), 2 deletions(-) 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)); + }); +}