From af2c1053373263951fbe4b29c56f98ca276a3625 Mon Sep 17 00:00:00 2001
From: dborup
Date: Wed, 22 Jul 2026 18:25:08 +0200
Subject: [PATCH] feat: collapse Foreign-Flagged Nodes and Repeaters Relaying
Unscoped to top 10
Same "Show all N / Show fewer" pattern as the Wardriving tab's
Sessions/Entry Points sections -- both lists on Foreign Traffic can
grow long on a busy network, dwarfing the rest of the tab.
---
public/analytics.js | 121 +++++++++++++++++---------
test-analytics-foreign-traffic-tab.js | 50 +++++++++++
2 files changed, 128 insertions(+), 43 deletions(-)
diff --git a/public/analytics.js b/public/analytics.js
index 2144c869..33b094b1 100644
--- a/public/analytics.js
+++ b/public/analytics.js
@@ -5471,6 +5471,35 @@ function destroy() { _stopRolesRefresh(); _stopScopesRefresh(); _stopForeignTraf
if (!total) return '—';
return (n / total * 100).toFixed(1) + '%';
}
+
+ // "Show all N / Show fewer" collapse for the two lists below, same
+ // pattern (and same TOP_N_LIMIT) as the Wardriving tab's Sessions/
+ // Entry Points sections.
+ var TOP_N_LIMIT = 10;
+ function topNToggleHtml(total, expanded, noun) {
+ if (total <= TOP_N_LIMIT) return '';
+ var label = expanded ? 'Show fewer' : 'Show all ' + total.toLocaleString() + ' ' + noun;
+ return '';
+ }
+ // Re-renders just one section's content on click and rewires the
+ // (freshly-created) toggle button, since innerHTML replacement drops
+ // the old node's listener.
+ function wireExpandToggle(containerId, renderFn) {
+ var expanded = false;
+ function attach() {
+ var container = document.getElementById(containerId);
+ if (!container) return;
+ var btn = container.querySelector('[data-ft-toggle]');
+ if (btn) {
+ btn.addEventListener('click', function() {
+ expanded = !expanded;
+ container.innerHTML = renderFn(expanded);
+ attach();
+ });
+ }
+ }
+ attach();
+ }
// Classifies live from the node's own lat/lon against the currently
// configured geo_filter box/polygon (window.MC_GEO_FILTER, set from
// /api/config/client — see public/roles.js), NOT the node's `foreign`
@@ -5485,6 +5514,50 @@ function destroy() { _stopRolesRefresh(); _stopScopesRefresh(); _stopForeignTraf
function isForeignNode(n) {
return !nodePassesGeoFilter(n.lat, n.lon, window.MC_GEO_FILTER);
}
+ function relaysHtml(relays, expanded) {
+ if (relays.length === 0) {
+ return '
No repeater has relayed an unscoped flood packet in the last 24 hours.
';
+ }
+ var shown = expanded ? relays : relays.slice(0, TOP_N_LIMIT);
+ var rows = shown.map(function(n) {
+ var total = n.relay_count_24h || 0;
+ var unscoped = n.unscoped_relay_count_24h || 0;
+ return '
No repeater has relayed an unscoped flood packet in the last 24 hours.
';
- }
-
const foreignNote = foreignCount > 0
? foreignCount.toLocaleString() + ' node(s) so far carry an advertised GPS position outside the configured geo_filter — cross-referencing which rows below actually trace back to those is a planned follow-up once more accumulates.'
: 'geo_filter is configured, but no foreign-origin node has advertised since — check back once traffic accumulates to cross-reference which of the unscoped relays below trace back to a foreign sender.';
@@ -5531,26 +5583,6 @@ function destroy() { _stopRolesRefresh(); _stopScopesRefresh(); _stopForeignTraf
.map(function(n) { return { n: n, t: new Date(n.last_seen || 0).getTime() }; })
.sort(function(a, b) { return b.t - a.t; })
.forEach(function(entry, i) { foreignNodes[i] = entry.n; });
- let foreignNodesBody;
- if (foreignNodes.length > 0) {
- const foreignRows = foreignNodes.map(function(n) {
- const lat = typeof n.lat === 'number' ? n.lat.toFixed(3) : '—';
- const lon = typeof n.lon === 'number' ? n.lon.toFixed(3) : '—';
- return '
None yet — a node is only listed here after it advertises with a GPS position outside the configured geo_filter.
';
- }
el.innerHTML =
'
Foreign Traffic
' +
@@ -5564,9 +5596,12 @@ function destroy() { _stopRolesRefresh(); _stopScopesRefresh(); _stopForeignTraf
'Nodes whose most recent advertised GPS position fell outside the configured geo_filter — self-reported GPS from the node\'s own ADVERT. ' +
'A FLOOD packet can legitimately travel many hops through a dense mesh, so a node whose traffic you can trace through a real local relay chain (check its Trace) is genuinely reachable via that chain — that\'s foreign traffic actually entering the mesh, not a data artifact.' +
'