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