mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-10-23 09:12:29 +00:00
feat: Repeaters Never Relaying Any Scope section on the Scopes tab
Distinct from "Nodes Not Using Any Scope": a repeater's own default_scope (does it send its own scoped adverts?) and transported_scopes (has it ever relayed ANYONE's region-scoped traffic?) are different signals. Checked the real numbers on stg.meshview.dk before building this — of 1181 repeaters with no default_scope, 168 (14%) still relay scope for others (their hashRegions config enables relaying even though their own adverts never carry a matching transport code), so the two lists aren't interchangeable. Filters to repeater/room nodes only (the only roles that can relay at all) with empty/absent transported_scopes, sorted by relay_count_24h descending — the busiest still-unconfigured repeaters are the most consequential to fix first. Same pure-function pattern as computeNodesWithoutScope (computeRepeatersNeverRelayingScope), reusing the node list already fetched for the sibling section instead of a second fetchAllNodes call. 6 new unit tests, including one pinning the exact "no default_scope but does relay for others" case found in the real data. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
40391b126d
commit
30120bcf2e
2 files changed
+133
No files matched your search
@@ -2709,6 +2709,7 @@ function destroy() { _stopRolesRefresh(); _stopScopesRefresh(); _stopForeignTraf
|
||||
window._analyticsRenderForeignTrafficTab = renderForeignTrafficTab;
|
||||
window._analyticsStopForeignTrafficRefresh = _stopForeignTrafficRefresh;
|
||||
window._analyticsComputeNodesWithoutScope = computeNodesWithoutScope;
|
||||
window._analyticsComputeRepeatersNeverRelayingScope = computeRepeatersNeverRelayingScope;
|
||||
}
|
||||
|
||||
// ─── Neighbor Graph Tab ─────────────────────────────────────────────────────
|
||||
@@ -4507,6 +4508,7 @@ function destroy() { _stopRolesRefresh(); _stopScopesRefresh(); _stopForeignTraf
|
||||
'<div id="scopes-repeaters" style="margin-top:16px"></div>' +
|
||||
'<div id="scopes-origin-nodes" style="margin-top:16px"></div>' +
|
||||
'<div id="scopes-no-scope" style="margin-top:16px"></div>' +
|
||||
'<div id="scopes-never-relay-scope" style="margin-top:16px"></div>' +
|
||||
'<div id="scopes-bridges" style="margin-top:16px"></div>';
|
||||
|
||||
// Attach window-button click listeners (once)
|
||||
@@ -4882,6 +4884,43 @@ function destroy() { _stopRolesRefresh(); _stopScopesRefresh(); _stopForeignTraf
|
||||
noScopeEl.innerHTML = '<h4 style="margin:0 0 4px">Nodes Not Using Any Scope</h4><p class="text-muted">Failed to load.</p>';
|
||||
}
|
||||
}
|
||||
|
||||
// Repeaters Never Relaying Any Scope: distinct from the section
|
||||
// above — a repeater's own default_scope and whether it has ever
|
||||
// relayed ANYONE's region-scoped traffic are different signals (see
|
||||
// computeRepeatersNeverRelayingScope doc comment). Reuses the
|
||||
// node list already fetched for the section above.
|
||||
var neverRelayEl = document.getElementById('scopes-never-relay-scope');
|
||||
if (neverRelayEl) {
|
||||
try {
|
||||
var nodesForRelay = typeof allNodes !== 'undefined' ? allNodes : (await fetchAllNodes('', { ttl: CLIENT_TTL.nodeList })).nodes;
|
||||
var neverRelay = computeRepeatersNeverRelayingScope(nodesForRelay, 100);
|
||||
var neverRelayBody;
|
||||
if (neverRelay.sortedCapped.length > 0) {
|
||||
var neverRelayRows = neverRelay.sortedCapped.map(function(n) {
|
||||
return '<tr><td><a href="#/nodes/' + encodeURIComponent(n.public_key) + '">' + esc(n.name || n.public_key) + '</a></td>' +
|
||||
'<td>' + esc(n.role || '—') + '</td>' +
|
||||
'<td>' + (n.relay_count_24h || 0).toLocaleString() + '</td>' +
|
||||
'<td>' + timeAgo(n.last_seen) + '</td></tr>';
|
||||
}).join('');
|
||||
neverRelayBody = '<table class="data-table analytics-table">' +
|
||||
'<thead><tr><th>Repeater</th><th>Role</th><th>Relays (24h)</th><th>Last Seen</th></tr></thead>' +
|
||||
'<tbody>' + neverRelayRows + '</tbody>' +
|
||||
'</table>';
|
||||
} else {
|
||||
neverRelayBody = '<p class="text-muted" style="font-size:0.85em">Every known repeater/room has relayed at least one region-scoped packet.</p>';
|
||||
}
|
||||
neverRelayEl.innerHTML =
|
||||
'<h4 style="margin:0 0 4px">Repeaters Never Relaying Any Scope (' + neverRelay.total.toLocaleString() + ')</h4>' +
|
||||
'<p class="text-muted" style="margin:0 0 8px;font-size:0.85em">' +
|
||||
'Repeater/room nodes that have never carried a single region-scoped (TRANSPORT_FLOOD/DIRECT) packet, ever — not the same set as "no default_scope" above: a repeater\'s hashRegions config can let it relay for others even when its own adverts never carry a matching transport code. ' +
|
||||
'Sorted by current relay volume — the busiest ones are the most consequential to configure first' + (neverRelay.truncated ? ' (showing the top ' + neverRelay.sortedCapped.length + ')' : '') + '.' +
|
||||
'</p>' +
|
||||
neverRelayBody;
|
||||
} catch (e) {
|
||||
neverRelayEl.innerHTML = '<h4 style="margin:0 0 4px">Repeaters Never Relaying Any Scope</h4><p class="text-muted">Failed to load.</p>';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4922,6 +4961,33 @@ function destroy() { _stopRolesRefresh(); _stopScopesRefresh(); _stopForeignTraf
|
||||
};
|
||||
}
|
||||
|
||||
// Pure (no DOM/network) computation behind the Scopes tab's "Repeaters
|
||||
// Never Relaying Any Scope" section. Distinct from
|
||||
// computeNodesWithoutScope: a repeater's OWN default_scope (does it send
|
||||
// its own scoped adverts?) and transported_scopes (has it ever relayed
|
||||
// ANYONE's region-scoped traffic, ever?) are different signals — on
|
||||
// stg.meshview.dk ~14% of no-default_scope repeaters still relay scope
|
||||
// for others (their hashRegions config enables relaying even though
|
||||
// their own adverts never carry a matching transport code). Only
|
||||
// repeater/room nodes can relay at all — every other role is filtered
|
||||
// out rather than silently 0-counted alongside them. Sorted by
|
||||
// relay_count_24h descending: the busiest unconfigured repeaters are the
|
||||
// most consequential ones to fix first.
|
||||
function computeRepeatersNeverRelayingScope(allNodes, cap) {
|
||||
var candidates = allNodes.filter(function(n) {
|
||||
return (n.role === 'repeater' || n.role === 'room') &&
|
||||
(!n.transported_scopes || n.transported_scopes.length === 0);
|
||||
});
|
||||
var sorted = candidates.slice().sort(function(a, b) {
|
||||
return Number(b.relay_count_24h || 0) - Number(a.relay_count_24h || 0);
|
||||
});
|
||||
return {
|
||||
total: candidates.length,
|
||||
sortedCapped: sorted.slice(0, cap),
|
||||
truncated: sorted.length > cap,
|
||||
};
|
||||
}
|
||||
|
||||
// Repeaters relaying unscoped (route_type FLOOD) traffic, sorted by volume
|
||||
// — a quick, no-new-backend-work way to surface an existing but previously
|
||||
// unsurfaced metric (unscoped_relay_count_24h, see openapi.go). A little
|
||||
|
||||
@@ -90,6 +90,7 @@ function makeAnalyticsSandbox() {
|
||||
|
||||
const ctx = makeAnalyticsSandbox();
|
||||
const computeNodesWithoutScope = ctx.window._analyticsComputeNodesWithoutScope;
|
||||
const computeRepeatersNeverRelayingScope = ctx.window._analyticsComputeRepeatersNeverRelayingScope;
|
||||
|
||||
console.log('\n=== analytics.js: computeNodesWithoutScope ===');
|
||||
|
||||
@@ -172,6 +173,72 @@ test('returns zero total and empty rows when every node has a scope', () => {
|
||||
assert.deepStrictEqual(result.roleSummary, []);
|
||||
});
|
||||
|
||||
console.log('\n=== analytics.js: computeRepeatersNeverRelayingScope ===');
|
||||
|
||||
test('is exported for testing', () => {
|
||||
assert.strictEqual(typeof computeRepeatersNeverRelayingScope, 'function');
|
||||
});
|
||||
|
||||
test('includes only repeater/room nodes with no transported_scopes, excludes other roles entirely', () => {
|
||||
const nodes = [
|
||||
{ public_key: 'pk1', name: 'NeverRelays', role: 'repeater', transported_scopes: null },
|
||||
{ public_key: 'pk2', name: 'RelaysDK', role: 'repeater', transported_scopes: ['#dk'] },
|
||||
{ public_key: 'pk3', name: 'RoomNeverRelays', role: 'room', transported_scopes: [] },
|
||||
{ public_key: 'pk4', name: 'CompanionNeverRelays', role: 'companion', transported_scopes: null },
|
||||
];
|
||||
const result = computeRepeatersNeverRelayingScope(nodes, 100);
|
||||
const names = result.sortedCapped.map(n => n.name);
|
||||
assert.strictEqual(result.total, 2, 'only NeverRelays and RoomNeverRelays should count');
|
||||
assert.ok(names.includes('NeverRelays') && names.includes('RoomNeverRelays'));
|
||||
assert.ok(!names.includes('RelaysDK'), 'a repeater that has relayed a scope must not appear');
|
||||
assert.ok(!names.includes('CompanionNeverRelays'), 'a companion can never relay at all — must not appear regardless of transported_scopes');
|
||||
});
|
||||
|
||||
test('a node missing default_scope but WITH transported_scopes is correctly excluded (the two signals differ)', () => {
|
||||
// Real-world case found on stg.meshview.dk: a repeater's hashRegions
|
||||
// config can let it relay for others even when its own adverts never
|
||||
// carry a matching transport code (no default_scope).
|
||||
const nodes = [
|
||||
{ public_key: 'pk1', name: 'RelaysForOthers', role: 'repeater', default_scope: null, transported_scopes: ['#dk', '#dk3'] },
|
||||
];
|
||||
const result = computeRepeatersNeverRelayingScope(nodes, 100);
|
||||
assert.strictEqual(result.total, 0, 'a repeater with transported_scopes must not appear here even without its own default_scope');
|
||||
});
|
||||
|
||||
test('sorts by relay_count_24h descending — the busiest unconfigured repeaters first', () => {
|
||||
const nodes = [
|
||||
{ public_key: 'pkQuiet', name: 'Quiet', role: 'repeater', relay_count_24h: 2, transported_scopes: null },
|
||||
{ public_key: 'pkBusy', name: 'Busy', role: 'repeater', relay_count_24h: 500, transported_scopes: null },
|
||||
{ public_key: 'pkMid', name: 'Mid', role: 'room', relay_count_24h: 40, transported_scopes: [] },
|
||||
];
|
||||
const result = computeRepeatersNeverRelayingScope(nodes, 100);
|
||||
assert.deepStrictEqual(result.sortedCapped.map(n => n.name), ['Busy', 'Mid', 'Quiet']);
|
||||
});
|
||||
|
||||
test('caps sortedCapped at `cap` and sets truncated accordingly', () => {
|
||||
const nodes = [];
|
||||
for (let i = 0; i < 5; i++) {
|
||||
nodes.push({ public_key: 'pk' + i, name: 'N' + i, role: 'repeater', relay_count_24h: i, transported_scopes: null });
|
||||
}
|
||||
const capped = computeRepeatersNeverRelayingScope(nodes, 3);
|
||||
assert.strictEqual(capped.sortedCapped.length, 3);
|
||||
assert.strictEqual(capped.total, 5);
|
||||
assert.strictEqual(capped.truncated, true);
|
||||
|
||||
const uncapped = computeRepeatersNeverRelayingScope(nodes, 10);
|
||||
assert.strictEqual(uncapped.truncated, false);
|
||||
});
|
||||
|
||||
test('returns zero total when every repeater/room has relayed at least one scope', () => {
|
||||
const nodes = [
|
||||
{ public_key: 'pk1', role: 'repeater', transported_scopes: ['#dk'] },
|
||||
{ public_key: 'pk2', role: 'room', transported_scopes: ['#dk-oj'] },
|
||||
];
|
||||
const result = computeRepeatersNeverRelayingScope(nodes, 100);
|
||||
assert.strictEqual(result.total, 0);
|
||||
assert.deepStrictEqual(result.sortedCapped, []);
|
||||
});
|
||||
|
||||
console.log('\n════════════════════════════════════════');
|
||||
console.log(` Nodes Without Scope: ${passed} passed, ${failed} failed`);
|
||||
console.log('════════════════════════════════════════');
|
||||
|
||||
Reference in new issue
Block a user