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..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' )); @@ -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' )); 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 + '×)' : ''}