Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions public/scope-audit.css
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
12 changes: 10 additions & 2 deletions public/scope-audit.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
WINDOWS.map(function (w) { return windowBtn(w.key, win, w.label); }).join('') +
'</div></div>' +
'<div class="sa-intro">Network-wide comparison of declared vs. observed region-scope forwarding, across every repeater that has declared a region list over RF. <a href="#/nodes">Per-node detail lives on each node\'s page</a>.</div>' +
'<div class="sa-sources">The <strong>declared</strong> side is the repeater&rsquo;s own answer, read back off the node by an observer running the neighbour-report firmware or by the CoreDrive RX app. The <strong>observed</strong> side is forwarding CoreScope already sees in its own traffic.</div>' +
'<div class="sa-search-bar"><input type="text" class="nodes-search sa-search" id="saSearch" placeholder="Search by repeater, pubkey, or region…" aria-label="Search scope audit rows"></div>' +
'<div id="saBody"><div class="text-muted" style="padding:8px"><span class="spinner"></span> Loading scope audit…</div></div>' +
'</div>';
Expand All @@ -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 '<div class="ns-empty sa-empty"><strong>No repeater has answered with its configured region list yet</strong>, so there is nothing to audit here.<br><br>That answer has to be collected from the repeater itself; nothing else in CoreScope knows which regions a node is <em>configured</em> 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:<ul><li>an <a href="https://observer.gessaman.com/" target="_blank" rel="noopener">ESP32 observer on the neighbour-report firmware</a> (<code>set mqtt.neighbors on</code>), which reports its neighbours&rsquo; scopes every 24h</li><li>the <a href="https://github.com/efiten/coredrive-rx" target="_blank" rel="noopener">CoreDrive RX</a> companion app, which asks a repeater directly while you are in range of it</li></ul>The newest answer per repeater wins, whichever collected it.</div>';
}

function mergedScopeChips(row) {
var missing = Object.create(null);
row.notObserved.forEach(function (n) { missing[n] = true; });
Expand Down Expand Up @@ -249,7 +257,7 @@
sortCtl = null;
if (!d.repeaters.length) {
searchIndex = {};
el.innerHTML = '<div class="ns-empty">No repeater has declared a region list yet — this fills in as devices drive and answer over RF.</div>';
el.innerHTML = emptyStateHtml();
return;
}
searchIndex = buildSearchIndex(d.repeaters);
Expand Down Expand Up @@ -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 });
Expand Down
42 changes: 42 additions & 0 deletions test-frontend-helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -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));
});
}
Loading