feat: split the Scopes tab into Overview/Regions/Hygiene sub-tabs

The tab grew to 12 stacked sections as this branch's work landed
(windowed adoption stats, all-time region breakdowns, and the two new
node/repeater hygiene lists) — one long scroll with no way to jump to
a specific concern. Grouped into 3 sub-tabs:

- Overview: the 1h/24h/7d-windowed stuff (stat cards, channel
  messages, scope adoption by channel, per-region table, time series,
  hour-of-day heatmap). The window picker now lives inside this
  panel, not above the sub-tab bar, since it only affects this group
  — the other two ignore it entirely.
- Regions: all-time region breakdowns (utilization, repeaters by
  region, nodes running this region, bridge repeaters).
- Hygiene: the two adoption-gap lists (nodes not using any scope,
  repeaters never relaying any scope).

Purely presentational — all three groups' data still loads together
in one pass (one /scope-stats fetch + the hygiene sections' own
fetchAllNodes calls); only DOM visibility toggles on the active
sub-tab, so there's no new fetch/render logic to get wrong. Selection
persists in sessionStorage (scopes_subtab), same pattern as the
existing window-picker persistence.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
dborup
2026-07-19 22:46:26 +02:00
co-authored by Claude Sonnet 5
parent 30120bcf2e
commit 904fd5bf42
+63 -19
View File
@@ -4482,6 +4482,19 @@ function destroy() { _stopRolesRefresh(); _stopScopesRefresh(); _stopForeignTraf
var winKey = 'scopes_window';
var selectedWindow = (typeof sessionStorage !== 'undefined' && sessionStorage.getItem(winKey)) || '24h';
// #1852: the tab grew to 12 stacked sections (windowed adoption stats,
// all-time region breakdowns, all-time node/repeater hygiene lists) —
// grouped into 3 sub-tabs so a visitor sees one coherent screen at a
// time instead of one long scroll. Purely presentational: all three
// groups' data still loads together (one #scope-stats fetch + the two
// hygiene sections' own fetchAllNodes calls), only DOM visibility is
// gated on the active sub-tab. The 1h/24h/7d window picker only
// affects the Overview group (the all-time Regions/Hygiene sections
// ignore it entirely), so it lives inside that panel, not above the
// sub-tab bar.
var subtabKey = 'scopes_subtab';
var selectedSubtab = (typeof sessionStorage !== 'undefined' && sessionStorage.getItem(subtabKey)) || 'overview';
// Fix 5: write static frame only once
if (!el.querySelector('#scopes-cards')) {
el.innerHTML =
@@ -4489,27 +4502,58 @@ function destroy() { _stopRolesRefresh(); _stopScopesRefresh(); _stopForeignTraf
'<p class="text-muted" style="margin:0 0 12px;font-size:0.85em">' +
'Denominator is all observed transmissions. Only TRANSPORT_FLOOD (0) and TRANSPORT_DIRECT (3) routes carry a scope; FLOOD (1) and DIRECT (2) are inherently unscoped per MeshCore protocol.' +
'</p>' +
'<div style="margin-bottom:12px">' +
['1h', '24h', '7d'].map(function(v) {
return '<button class="tab-btn' + (selectedWindow === v ? ' active' : '') + '" data-win="' + v + '">' + v + '</button>';
'<div class="analytics-tabs" id="scopesSubtabs" style="margin-bottom:12px">' +
[
{ key: 'overview', label: 'Overview' },
{ key: 'regions', label: 'Regions' },
{ key: 'hygiene', label: 'Hygiene' },
].map(function(t) {
return '<button class="tab-btn' + (selectedSubtab === t.key ? ' active' : '') + '" data-subtab="' + t.key + '">' + t.label + '</button>';
}).join('') +
'</div>' +
'<div id="scopes-cards" class="stats-grid" style="margin-bottom:16px"></div>' +
'<div id="scopes-channel-messages" style="margin-bottom:16px"></div>' +
'<div id="scopes-channel-adoption" style="margin-bottom:16px"></div>' +
'<div class="text-center text-muted" id="scopes-loading" style="padding:20px">Loading scope stats…</div>' +
'<table class="data-table analytics-table" style="margin-bottom:8px">' +
'<thead><tr><th>Region</th><th>Messages</th><th>% of Scoped</th></tr></thead>' +
'<tbody id="scopes-tbody"></tbody>' +
'</table>' +
'<div id="scopes-chart"></div>' +
'<div id="scopes-hourly" style="margin-top:16px"></div>' +
'<div id="scopes-utilization" style="margin-top:16px"></div>' +
'<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>';
'<div id="scopes-panel-overview" style="display:' + (selectedSubtab === 'overview' ? '' : 'none') + '">' +
'<div style="margin-bottom:12px">' +
['1h', '24h', '7d'].map(function(v) {
return '<button class="tab-btn' + (selectedWindow === v ? ' active' : '') + '" data-win="' + v + '">' + v + '</button>';
}).join('') +
'</div>' +
'<div id="scopes-cards" class="stats-grid" style="margin-bottom:16px"></div>' +
'<div id="scopes-channel-messages" style="margin-bottom:16px"></div>' +
'<div id="scopes-channel-adoption" style="margin-bottom:16px"></div>' +
'<div class="text-center text-muted" id="scopes-loading" style="padding:20px">Loading scope stats…</div>' +
'<table class="data-table analytics-table" style="margin-bottom:8px">' +
'<thead><tr><th>Region</th><th>Messages</th><th>% of Scoped</th></tr></thead>' +
'<tbody id="scopes-tbody"></tbody>' +
'</table>' +
'<div id="scopes-chart"></div>' +
'<div id="scopes-hourly" style="margin-top:16px"></div>' +
'</div>' +
'<div id="scopes-panel-regions" style="display:' + (selectedSubtab === 'regions' ? '' : 'none') + '">' +
'<div id="scopes-utilization"></div>' +
'<div id="scopes-repeaters" style="margin-top:16px"></div>' +
'<div id="scopes-origin-nodes" style="margin-top:16px"></div>' +
'<div id="scopes-bridges" style="margin-top:16px"></div>' +
'</div>' +
'<div id="scopes-panel-hygiene" style="display:' + (selectedSubtab === 'hygiene' ? '' : 'none') + '">' +
'<div id="scopes-no-scope"></div>' +
'<div id="scopes-never-relay-scope" style="margin-top:16px"></div>' +
'</div>';
// Sub-tab click listener (once) — pure visibility toggle, no re-fetch.
var subtabsEl = el.querySelector('#scopesSubtabs');
if (subtabsEl) {
subtabsEl.addEventListener('click', function(e) {
var btn = e.target.closest('[data-subtab]');
if (!btn) return;
selectedSubtab = btn.dataset.subtab;
if (typeof sessionStorage !== 'undefined') sessionStorage.setItem(subtabKey, selectedSubtab);
subtabsEl.querySelectorAll('[data-subtab]').forEach(function(b) { b.classList.toggle('active', b.dataset.subtab === selectedSubtab); });
['overview', 'regions', 'hygiene'].forEach(function(key) {
var panel = document.getElementById('scopes-panel-' + key);
if (panel) panel.style.display = key === selectedSubtab ? '' : 'none';
});
});
}
// Attach window-button click listeners (once)
el.querySelectorAll('[data-win]').forEach(function(btn) {