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: 14 additions & 3 deletions public/scope-audit.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +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>' +
sourcesLineHtml() +
'<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 @@ -58,6 +58,17 @@
return '<span title="Declared regions answer captured ' + escapeHtml(row.declaredAt) + '">' + escapeHtml(age) + '</span>';
}

// 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 '<div class="sa-sources">The <strong>declared</strong> side is the repeater&rsquo;s own answer, read back off the node by an ' +
'<a href="https://observer.gessaman.com/" target="_blank" rel="noopener">ESP32 observer on the neighbour-report firmware</a> or by ' +
'<a href="https://rx.on8ar.eu/" target="_blank" rel="noopener">CoreDrive RX</a>. The newest answer per repeater wins, whichever collected it. ' +
'The <strong>observed</strong> side is forwarding CoreScope already sees in its own traffic.</div>';
}

// mergedScopeChips renders ONE chip per declared region, coloured by whether
// that region was actually observed forwarding in the window.
//
Expand All @@ -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 '<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>';
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://rx.on8ar.eu/" 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) {
Expand Down Expand Up @@ -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 });
Expand Down
36 changes: 35 additions & 1 deletion test-frontend-helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand All @@ -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(/<a /g) || []).length, 2, 'both collectors must be linked');
assert.ok(line.includes('observer.gessaman.com'), 'observer firmware link');
assert.ok(/rx\.on8ar\.eu|coredrive-rx/.test(line), 'companion app link');
});

test('external links are safe to open', () => {
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));
});
}
Loading