diff --git a/public/analytics.js b/public/analytics.js index 19a899a7..e9898dc1 100644 --- a/public/analytics.js +++ b/public/analytics.js @@ -4578,14 +4578,39 @@ function destroy() { _stopRolesRefresh(); _stopScopesRefresh(); _stopForeignTraf // title can include a dynamic count ("Nodes Not Using Any Scope // (1423 of 1491)") since it's built at render time with the real data // already in hand, unlike a static skeleton-level heading would be. - function detailsSection(title, description, bodyHtml) { - return '
' + + // `key` is a stable identifier (independent of the dynamic count in + // `title`) used by setSectionHtml to restore this section's open/closed + // state across the 60s auto-refresh re-render below. + function detailsSection(title, description, bodyHtml, key) { + return '
' + '' + title + '' + (description ? '

' + description + '

' : '') + '
' + bodyHtml + '
' + '
'; } + // The 60s auto-refresh below rebuilds each section's innerHTML from + // scratch, which would otherwise silently re-collapse any
a + // visitor had open mid-read. Captures open state by data-key (falling + // back to summary text for nested details that don't set one, e.g. the + // per-region groups) before replacing, then reapplies it after. + function setSectionHtml(container, html) { + if (!container) return; + var openKeys = []; + container.querySelectorAll('details[open]').forEach(function(d) { + var s = d.querySelector('summary'); + openKeys.push(d.getAttribute('data-key') || (s ? s.textContent : '')); + }); + container.innerHTML = html; + if (openKeys.length) { + container.querySelectorAll('details').forEach(function(d) { + var s = d.querySelector('summary'); + var k = d.getAttribute('data-key') || (s ? s.textContent : ''); + if (k && openKeys.indexOf(k) !== -1) d.open = true; + }); + } + } + async function load(w) { var loadingEl = document.getElementById('scopes-loading'); if (loadingEl) loadingEl.style.display = ''; @@ -4685,11 +4710,12 @@ function destroy() { _stopRolesRefresh(); _stopScopesRefresh(); _stopForeignTraf } else { adoptBody = '

No channel messages in this window.

'; } - adoptEl.innerHTML = detailsSection( + setSectionHtml(adoptEl, detailsSection( 'Scope Adoption by Channel (' + adoption.length.toLocaleString() + ')', 'Which channels actually use region scoping vs which never do. Top 30 by message volume.', - adoptBody - ); + adoptBody, + 'scope-adoption' + )); } // Per-region table @@ -4785,17 +4811,18 @@ function destroy() { _stopRolesRefresh(); _stopScopesRefresh(); _stopForeignTraf '
' + cells + '
' + ''; }).join(''); - hourlyEl.innerHTML = detailsSection( + setSectionHtml(hourlyEl, detailsSection( 'Activity by Hour of Day (' + hourly.length.toLocaleString() + ' region' + (hourly.length === 1 ? '' : 's') + ')', 'When during a typical day (UTC) each region is active, aggregated across every day in the window above. Reads best on 7d — color is normalized per region, so quiet and busy regions are both visible.', '
' + '
' + '
' + hourLabels + '
' + '
' + - hourlyRows - ); + hourlyRows, + 'activity-hourly' + )); } else { - hourlyEl.innerHTML = detailsSection('Activity by Hour of Day', 'No scoped messages in this window to chart by hour of day.', ''); + setSectionHtml(hourlyEl, detailsSection('Activity by Hour of Day', 'No scoped messages in this window to chart by hour of day.', '', 'activity-hourly')); } } @@ -4811,7 +4838,7 @@ function destroy() { _stopRolesRefresh(); _stopScopesRefresh(); _stopForeignTraf var usedCount = configured - unused.length; var unusedPct = (unused.length / configured * 100).toFixed(1); var listHtml = unused.map(function(name) { return esc(name); }).join(', '); - utilEl.innerHTML = detailsSection( + setSectionHtml(utilEl, detailsSection( 'Region Utilization (' + usedCount.toLocaleString() + ' of ' + configured.toLocaleString() + ' used)', 'All-time, not limited to the window above — has this configured region ever matched a message still in retention?', '

' + @@ -4819,10 +4846,11 @@ function destroy() { _stopRolesRefresh(); _stopScopesRefresh(); _stopForeignTraf (unused.length > 0 ? ' — ' + unused.length.toLocaleString() + ' (' + unusedPct + '%) have never matched anything.' : '.') + '

' + (unused.length > 0 ? - '
Show ' + unused.length.toLocaleString() + ' unused region' + (unused.length === 1 ? '' : 's') + '' + + '
Show ' + unused.length.toLocaleString() + ' unused region' + (unused.length === 1 ? '' : 's') + '' + '
' + listHtml + '
' - : '') - ); + : ''), + 'region-utilization' + )); } else { utilEl.innerHTML = ''; } @@ -4846,18 +4874,19 @@ function destroy() { _stopRolesRefresh(); _stopScopesRefresh(); _stopForeignTraf var links = g.repeaters.map(function(rp) { return '' + esc(rp.name) + ''; }).join(', '); - return '
' + + return '
' + '' + esc(g.region) + ' — ' + g.count.toLocaleString() + ' ' + unitLabel + (g.count === 1 ? '' : 's') + '' + '
' + links + '
' + '
'; }).join('') : '

No data yet — path resolution catches up gradually as traffic flows; check back in a few minutes.

'; var count = groups ? groups.length : 0; - el.innerHTML = detailsSection( + setSectionHtml(el, detailsSection( esc(title) + ' (' + count.toLocaleString() + ' region' + (count === 1 ? '' : 's') + ')', esc(description), - body - ); + body, + elId + )); } renderRegionNodeGroups('scopes-repeaters', 'Repeaters by Region', @@ -4889,11 +4918,12 @@ function destroy() { _stopRolesRefresh(); _stopScopesRefresh(); _stopForeignTraf // empty longer, especially right after a restart. bridgeBody = '

No bridge repeaters found yet — this needs a repeater confirmed in 2+ different regions, which takes longer to accumulate than the single-region data above.

'; } - bridgeEl.innerHTML = detailsSection( + setSectionHtml(bridgeEl, detailsSection( 'Bridge Repeaters (' + bridges.length.toLocaleString() + ')', 'All-time — repeaters that have relayed traffic for more than one region. These connect otherwise-separate regional communities; losing one can split the mesh\'s regional coverage.', - bridgeBody - ); + bridgeBody, + 'bridge-repeaters' + )); } renderRegionNodeGroups('scopes-origin-nodes', 'Nodes Running This Region', @@ -4928,14 +4958,15 @@ function destroy() { _stopRolesRefresh(); _stopScopesRefresh(); _stopForeignTraf noScopeBody = '

Every known node has a configured scope.

'; } - noScopeEl.innerHTML = detailsSection( + setSectionHtml(noScopeEl, detailsSection( 'Nodes Not Using Any Scope (' + noScope.total.toLocaleString() + ' of ' + allNodes.length.toLocaleString() + ')', 'Nodes with no default_scope at all — never configured a hashRegions region for themselves. By role: ' + (roleSummary || 'none') + '. ' + 'Sorted by most-recently-active first' + (noScope.truncated ? ' (showing the ' + noScope.sortedCapped.length + ' most recent)' : '') + '.', - noScopeBody - ); + noScopeBody, + 'no-scope' + )); } catch (e) { - noScopeEl.innerHTML = detailsSection('Nodes Not Using Any Scope', null, '

Failed to load.

'); + setSectionHtml(noScopeEl, detailsSection('Nodes Not Using Any Scope', null, '

Failed to load.

', 'no-scope')); } } @@ -4964,14 +4995,15 @@ function destroy() { _stopRolesRefresh(); _stopScopesRefresh(); _stopForeignTraf } else { neverRelayBody = '

Every known repeater/room has relayed at least one region-scoped packet.

'; } - neverRelayEl.innerHTML = detailsSection( + setSectionHtml(neverRelayEl, detailsSection( 'Repeaters Never Relaying Any Scope (' + neverRelay.total.toLocaleString() + ')', '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 + ')' : '') + '.', - neverRelayBody - ); + neverRelayBody, + 'never-relay' + )); } catch (e) { - neverRelayEl.innerHTML = detailsSection('Repeaters Never Relaying Any Scope', null, '

Failed to load.

'); + setSectionHtml(neverRelayEl, detailsSection('Repeaters Never Relaying Any Scope', null, '

Failed to load.

', 'never-relay')); } } }