diff --git a/public/analytics.js b/public/analytics.js
index e0f384a0..5bf20317 100644
--- a/public/analytics.js
+++ b/public/analytics.js
@@ -6552,7 +6552,59 @@ function destroy() { _stopRolesRefresh(); _stopScopesRefresh(); _stopForeignTraf
return (n / total * 100).toFixed(1) + '%';
}
- var densityRows = density.map(function (a) {
+ // Every section below collapses to the top 10 with a "Show all N"
+ // toggle, same pattern as the Wardriving tab's Top Senders/Coverage
+ // by Observer sections — dborup flagged that a flat 36-row dump
+ // "fylder hele billedet" (fills the whole picture) and asked for
+ // this treatment across all three Areas sections, not just
+ // Position-Fix Coverage Gaps where it started.
+ var AREAS_TOP_N = 10;
+
+ // Generic collapsible-table renderer + click wiring shared by all
+ // three sections below. `items` is assumed already sorted into the
+ // order that matters for that section (worst-first for Density and
+ // Position Gaps, most-cross-area-reach-first for Bridge Nodes,
+ // both already established before this call).
+ function collapsibleTableHtml(items, theadHtml, rowFn, toggleAttr, noun, emptyMsg, expanded) {
+ if (!items.length) return emptyMsg;
+ var shown = expanded ? items : items.slice(0, AREAS_TOP_N);
+ var rows = shown.map(rowFn).join('');
+ var toggle = items.length > AREAS_TOP_N
+ ? '
'
+ : '';
+ return '
' + theadHtml + '
' + rows + '
' + toggle;
+ }
+ function wireCollapseToggle(containerId, toggleAttr, renderFn) {
+ var expanded = false;
+ function attach() {
+ var container = document.getElementById(containerId);
+ if (!container) return;
+ var btn = container.querySelector('[' + toggleAttr + ']');
+ if (btn) {
+ btn.addEventListener('click', function () {
+ expanded = !expanded;
+ container.innerHTML = renderFn(expanded);
+ attach();
+ });
+ }
+ }
+ attach();
+ }
+
+ // Density: sorted by % unhealthy (degraded+silent) descending, not
+ // the API's Total-desc order — a handful of big, mostly-healthy
+ // areas (Europa, Danmark) otherwise buried the small areas actually
+ // worth a look. Ties (including the common all-active 0% case)
+ // fall back to Total desc so the biggest areas still anchor each
+ // health tier.
+ var sortedDensity = density.slice().sort(function (a, b) {
+ var pctA = a.total ? (a.degraded + a.silent) / a.total : 0;
+ var pctB = b.total ? (b.degraded + b.silent) / b.total : 0;
+ if (pctB !== pctA) return pctB - pctA;
+ return b.total - a.total;
+ });
+ function densityRowHtml(a) {
var roleParts = Object.keys(a.roleCounts || {}).sort().map(function (r) {
return r + ': ' + a.roleCounts[r];
}).join(', ');
@@ -6564,14 +6616,17 @@ function destroy() { _stopRolesRefresh(); _stopScopesRefresh(); _stopForeignTraf
'
No positioned nodes fall inside any configured area.
', expanded);
+ }
- var bridgeRows = bridgeNodes.map(function (b) {
+ // Bridge nodes: already ranked most-cross-area-reach-first by the
+ // API (computeAreaBridgeNodes, and capped at 25) -- no re-sort
+ // needed, just the same collapse-to-10 treatment.
+ function bridgeRowHtml(b) {
return '
No packet-derived neighbor edges cross between two different areas yet.
', expanded);
+ }
- // Sorted worst-coverage-first (highest % estimated) rather than the
- // API's realFix-desc order — most areas have 0% estimated (fully
- // GPS-mapped already), so a plain dump buried the handful of areas
- // that actually have a gap worth looking at. Collapses to the top
- // 10 by default, same "Show all N" pattern as the Wardriving tab's
- // Top Senders/Coverage by Observer sections.
- var AREAS_TOP_N = 10;
+ // Position gaps: sorted worst-coverage-first (highest % estimated)
+ // rather than the API's realFix-desc order — most areas have 0%
+ // estimated (fully GPS-mapped already), so a plain dump buried the
+ // handful of areas that actually have a gap worth looking at.
var sortedGaps = positionGaps.slice().sort(function (a, b) {
var totalA = a.realFix + a.approximated, totalB = b.realFix + b.approximated;
var pctA = totalA ? a.approximated / totalA : 0;
var pctB = totalB ? b.approximated / totalB : 0;
return pctB - pctA;
});
- function gapsToggleHtml(expanded) {
- if (sortedGaps.length <= AREAS_TOP_N) return '';
- var label = expanded ? 'Show fewer' : 'Show all ' + sortedGaps.length.toLocaleString() + ' areas';
- return '';
+ function gapsRowHtml(g) {
+ var total = g.realFix + g.approximated;
+ return '
' +
+ '
' + esc(g.label) + '
' +
+ '
' + g.realFix.toLocaleString() + '
' +
+ '
' + g.approximated.toLocaleString() + '
' +
+ '
' + pct(g.approximated, total) + '
' +
+ '
';
}
+ var gapsThead = '
Area
Real GPS Fix
Estimated (via Neighbors)
% Estimated
';
function gapsTableHtml(expanded) {
- if (!sortedGaps.length) {
- return '
No unpositioned node could be placed in any area, even approximately.
';
- }
- var shown = expanded ? sortedGaps : sortedGaps.slice(0, AREAS_TOP_N);
- var rows = shown.map(function (g) {
- var total = g.realFix + g.approximated;
- return '
No unpositioned node could be placed in any area, even approximately.
', expanded);
}
+
var unpositionedNote = '
' +
(d.unpositionedTotal || 0).toLocaleString() + ' node' + (d.unpositionedTotal === 1 ? '' : 's') + ' network-wide have no real GPS fix' +
(d.unpositionedNoNeighborFix ? ', of which ' + d.unpositionedNoNeighborFix.toLocaleString() + ' also have no positioned neighbor to estimate from — those can\'t be placed anywhere, not even approximately, so they\'re absent from the table above entirely.' : '.') +
@@ -6630,13 +6674,13 @@ function destroy() { _stopRolesRefresh(); _stopScopesRefresh(); _stopForeignTraf
el.innerHTML =
'
' +
'
Node Density & Health by Area
' +
- '
Nodes with a real GPS fix inside each configured area — a node in a narrower sub-area also counts toward any broader area containing it (e.g. a city area rolls up into its region).
' +
- densityHtml +
+ '
Nodes with a real GPS fix inside each configured area — a node in a narrower sub-area also counts toward any broader area containing it (e.g. a city area rolls up into its region). Sorted worst-health-first.
' +
+ '
' + densityTableHtml(false) + '
' +
'
' +
'
' +
'
Cross-Area Bridge Nodes
' +
'
Nodes whose packet-derived neighbor connections reach into at least one OTHER area than their own, ranked by how many other areas they reach. Distinct from the network-wide Bridge Score elsewhere in this app, which has no concept of areas at all.
/g) || []).length;
+ assert.strictEqual(rowCount, 10, 'only 10 rows should render by default');
+ assert.ok(section.includes('Show all 12 nodes'), 'a "Show all" toggle should appear when there are more than 10 bridge nodes');
+ });
+
await testAsync('renders the Position-Fix Coverage Gaps table with a computed percentage', async () => {
const ctx = makeAnalyticsSandbox(makeApiStub(makeAreasResponse()));
const el = fakeEl();