From ed02d8409b2059a804766843e137b507d40fe5f7 Mon Sep 17 00:00:00 2001 From: dborup Date: Sun, 26 Jul 2026 09:10:16 +0200 Subject: [PATCH 1/3] feat: show which regions are used under Region Utilization The "Region Utilization (39 of 1,098 used)" stat already listed the *unused* regions in a collapsed details block, but not which specific ones counted toward the "used" side -- dborup asked to see them too. Added UsedRegions (ScopeStatsResponse) alongside the existing UnusedRegions, computed in the same matched-regions pass so it's a free byproduct, not a second query. Frontend adds a matching "Show N used regions" details block next to the existing unused one. --- cmd/server/routes.go | 7 ++++++- cmd/server/routes_test.go | 11 +++++++++++ cmd/server/types.go | 14 +++++++++----- public/analytics.js | 12 +++++++++--- 4 files changed, 35 insertions(+), 9 deletions(-) diff --git a/cmd/server/routes.go b/cmd/server/routes.go index ad267ecd..f98b9a8f 100644 --- a/cmd/server/routes.go +++ b/cmd/server/routes.go @@ -3939,13 +3939,18 @@ func (s *Server) handleScopeStats(w http.ResponseWriter, r *http.Request) { resp.ConfiguredRegions = len(configured) if matched, err := s.db.GetMatchedRegionNames(); err == nil { unused := make([]string, 0, len(configured)) + used := make([]string, 0, len(configured)) for _, name := range configured { - if !matched[name] { + if matched[name] { + used = append(used, name) + } else { unused = append(unused, name) } } sort.Strings(unused) + sort.Strings(used) resp.UnusedRegions = unused + resp.UsedRegions = used } else { log.Printf("WARN GetMatchedRegionNames: %v", err) } diff --git a/cmd/server/routes_test.go b/cmd/server/routes_test.go index 0ace90ff..27aa49b4 100644 --- a/cmd/server/routes_test.go +++ b/cmd/server/routes_test.go @@ -4605,6 +4605,17 @@ func TestHandleScopeStats_UnusedRegions(t *testing.T) { t.Errorf("unusedRegions[%d] = %q, want %q", i, resp.UnusedRegions[i], name) } } + // UsedRegions is the complement -- together with UnusedRegions it's a + // complete partition of the (deduped) configured list. + wantUsed := []string{"#belgium"} + if len(resp.UsedRegions) != len(wantUsed) { + t.Fatalf("usedRegions = %v, want %v", resp.UsedRegions, wantUsed) + } + for i, name := range wantUsed { + if resp.UsedRegions[i] != name { + t.Errorf("usedRegions[%d] = %q, want %q", i, resp.UsedRegions[i], name) + } + } } // TestHandleScopeStats_RepeatersByRegion verifies the "which repeaters diff --git a/cmd/server/types.go b/cmd/server/types.go index 48f5948f..e8e2287d 100644 --- a/cmd/server/types.go +++ b/cmd/server/types.go @@ -157,13 +157,17 @@ type ScopeStatsResponse struct { Summary ScopeStatsSummary `json:"summary"` ByRegion []ScopeRegionCount `json:"byRegion"` TimeSeries []ScopeTimePoint `json:"timeSeries"` - // ConfiguredRegions/UnusedRegions are all-time (not window-scoped): - // how many hashRegions are configured, and which of them have never - // matched a single transmission still in retention. Lets an operator - // see at a glance how much of their region list is dead weight. - // Omitted (both zero-value) when the server config has no hashRegions. + // ConfiguredRegions/UnusedRegions/UsedRegions are all-time (not + // window-scoped): how many hashRegions are configured, and which of + // them have never matched a single transmission still in retention + // (UnusedRegions) vs which have (UsedRegions) -- together a + // complete partition of the configured list. Lets an operator see + // at a glance how much of their region list is dead weight, and + // which specific regions the used/unused counts refer to. Omitted + // (all zero-value) when the server config has no hashRegions. ConfiguredRegions int `json:"configuredRegions,omitempty"` UnusedRegions []string `json:"unusedRegions,omitempty"` + UsedRegions []string `json:"usedRegions,omitempty"` // RepeatersByRegion is all-time (not window-scoped), like // UnusedRegions: for each region that has ever matched a transmission, // which distinct repeaters/rooms have relayed traffic carrying that diff --git a/public/analytics.js b/public/analytics.js index e6154c11..91796317 100644 --- a/public/analytics.js +++ b/public/analytics.js @@ -5197,9 +5197,11 @@ function destroy() { _stopRolesRefresh(); _stopScopesRefresh(); _stopForeignTraf var configured = d.configuredRegions || 0; if (configured > 0) { var unused = d.unusedRegions || []; - var usedCount = configured - unused.length; + var used = d.usedRegions || []; + var usedCount = used.length; var unusedPct = (unused.length / configured * 100).toFixed(1); - var listHtml = unused.map(function(name) { return regionCodeHtml(name); }).join(', '); + var unusedListHtml = unused.map(function(name) { return regionCodeHtml(name); }).join(', '); + var usedListHtml = used.map(function(name) { return regionCodeHtml(name); }).join(', '); 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?', @@ -5207,9 +5209,13 @@ function destroy() { _stopRolesRefresh(); _stopScopesRefresh(); _stopForeignTraf '' + usedCount.toLocaleString() + ' of ' + configured.toLocaleString() + ' configured regions have matched at least once' + (unused.length > 0 ? ' — ' + unused.length.toLocaleString() + ' (' + unusedPct + '%) have never matched anything.' : '.') + '

' + + (used.length > 0 ? + '
Show ' + used.length.toLocaleString() + ' used region' + (used.length === 1 ? '' : 's') + '' + + '
' + usedListHtml + '
' + : '') + (unused.length > 0 ? '
Show ' + unused.length.toLocaleString() + ' unused region' + (unused.length === 1 ? '' : 's') + '' + - '
' + listHtml + '
' + '
' + unusedListHtml + '
' : ''), 'region-utilization' )); From 1aaa04466673473add6cdd3b46e5fab88d6b62ad Mon Sep 17 00:00:00 2001 From: dborup Date: Sun, 26 Jul 2026 09:21:47 +0200 Subject: [PATCH 2/3] fix: Scope Adoption by Area section had no description, unlike its siblings Region Utilization and the other Scope Statistics sections all have a one-line description under the summary explaining what the numbers mean; this one passed null. Added one matching the section's own doc comment: of the real nodes in each configured area, how many actually support its linked region. --- public/analytics.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/analytics.js b/public/analytics.js index 91796317..b469d684 100644 --- a/public/analytics.js +++ b/public/analytics.js @@ -5179,7 +5179,7 @@ function destroy() { _stopRolesRefresh(); _stopScopesRefresh(); _stopForeignTraf }).join(''); setSectionHtml(areaAdoptEl, detailsSection( 'Scope Adoption by Area (' + byArea.length.toLocaleString() + ' areas)', - null, + 'All-time, not limited to the window above — of the real nodes sitting in each configured area, how many actually support its linked region (own default_scope, or ever relayed it)?', areaGroups, 'scope-adoption-by-area' )); From 04cb9c955feebb13dc1adea355aa14c305d57bc3 Mon Sep 17 00:00:00 2001 From: dborup Date: Sun, 26 Jul 2026 09:28:50 +0200 Subject: [PATCH 3/3] feat: node detail page shows its own configured scope, not just transported The Transported Scopes row shows regions a repeater has relayed for OTHERS, but not what the node's own hashRegions scope actually is -- dborup asked for that distinction to be visible too. Added an "Own scope" row right above it, for any node (not just repeater/room -- any node can configure its own scope). Mirrors live.js's existing null (no schema support) / empty ("unknown scope") / value distinction for default_scope. --- public/nodes.js | 1 + 1 file changed, 1 insertion(+) diff --git a/public/nodes.js b/public/nodes.js index a22a8d43..78d78dd7 100644 --- a/public/nodes.js +++ b/public/nodes.js @@ -679,6 +679,7 @@ const btooltip = "Normalized betweenness centrality (0..1). How often this node sits on the shortest path between other pairs of nodes in the affinity graph. 1.0 = the most structurally critical node on the mesh. High Bridge + low Traffic share = a quiet but irreplaceable chokepoint."; return `Bridge score ⓘ${bpct}% ${blabel}`; })() : ''} + ${'default_scope' in n ? `Own scope${n.default_scope === null ? '—' : n.default_scope === '' ? 'unknown scope' : '' + escapeHtml(n.default_scope) + ''}` : ''} ${(n.role === 'repeater' || n.role === 'room') && Array.isArray(n.transported_scopes) && n.transported_scopes.length ? `Transported scopes${n.transported_scopes.map(sc => '' + escapeHtml(String(sc)) + '').join('')}` : ''} First Seen${renderNodeTimestampHtml(n.first_seen)} Total Packets${stats.totalTransmissions || stats.totalPackets || n.advert_count || 0}${stats.totalObservations && stats.totalObservations !== (stats.totalTransmissions || stats.totalPackets) ? ' (seen ' + stats.totalObservations + '×)' : ''}