mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-10-23 05:01:18 +00:00
fix: Scope Adoption by Area must count relayed regions, not just default_scope
dborup: a repeater that has relayed dk-horsens traffic supports the Horsens area, even if its own default_scope is something else (e.g. the generic #dk most nodes actually use) or unset entirely. The previous version only checked default_scope, missing this -- same runs-this-region vs carried-this-region's-traffic distinction the tab already draws between OriginatingNodesByRegion and RepeatersByRegion. Both NodesWithAnyScope and NodesMatchingArea now also check the node's entry in the cached RepeaterRelayInfo map (TransportedScopes), reusing the same cache RepeatersByRegion already populates. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
bb65e0893b
commit
9661fd0f4d
5 files changed
+120
-31
No files matched your search
+52
-19
@@ -2239,10 +2239,12 @@ func (db *DB) GetNodesByDefaultScope() (map[string][]RepeaterRef, error) {
|
||||
return result, rows.Err()
|
||||
}
|
||||
|
||||
// nodeAreaScopeInput is one node's position + default_scope — the raw
|
||||
// input to computeScopeAdoptionByArea. DefaultScope is "" when unset or
|
||||
// when this DB predates #899 (no default_scope column at all).
|
||||
// nodeAreaScopeInput is one node's position + default_scope + pubkey — the
|
||||
// raw input to computeScopeAdoptionByArea. DefaultScope is "" when unset or
|
||||
// when this DB predates #899 (no default_scope column at all). PublicKey is
|
||||
// lowercase, for looking a node up in a RepeaterRelayInfo map.
|
||||
type nodeAreaScopeInput struct {
|
||||
PublicKey string
|
||||
Lat, Lon float64
|
||||
DefaultScope string
|
||||
}
|
||||
@@ -2253,7 +2255,7 @@ type nodeAreaScopeInput struct {
|
||||
// area. Unlike GetNodesByDefaultScope, this includes nodes with NO scope
|
||||
// too — the whole point is measuring adoption, not just listing who has one.
|
||||
func (db *DB) GetNodesForScopeAdoption() ([]nodeAreaScopeInput, error) {
|
||||
query := "SELECT lat, lon"
|
||||
query := "SELECT public_key, lat, lon"
|
||||
if db.hasDefaultScope {
|
||||
query += ", default_scope"
|
||||
}
|
||||
@@ -2265,31 +2267,41 @@ func (db *DB) GetNodesForScopeAdoption() ([]nodeAreaScopeInput, error) {
|
||||
defer rows.Close()
|
||||
var out []nodeAreaScopeInput
|
||||
for rows.Next() {
|
||||
var pk string
|
||||
var lat, lon float64
|
||||
var scope sql.NullString
|
||||
var scanErr error
|
||||
if db.hasDefaultScope {
|
||||
scanErr = rows.Scan(&lat, &lon, &scope)
|
||||
scanErr = rows.Scan(&pk, &lat, &lon, &scope)
|
||||
} else {
|
||||
scanErr = rows.Scan(&lat, &lon)
|
||||
scanErr = rows.Scan(&pk, &lat, &lon)
|
||||
}
|
||||
if scanErr != nil {
|
||||
continue
|
||||
}
|
||||
out = append(out, nodeAreaScopeInput{Lat: lat, Lon: lon, DefaultScope: scope.String})
|
||||
out = append(out, nodeAreaScopeInput{PublicKey: strings.ToLower(pk), Lat: lat, Lon: lon, DefaultScope: scope.String})
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// computeScopeAdoptionByArea buckets nodes by their most specific
|
||||
// configured area (AreaKeyForPoint) and tallies, per area: how many nodes
|
||||
// sit there at all, how many have ANY default_scope configured, and (when
|
||||
// the area itself has a RegionScope link) how many of those specifically
|
||||
// match the area's own region — i.e. does this geographic community
|
||||
// actually use the scope the area is nominally tied to, or something else
|
||||
// entirely (or nothing at all). A node outside every configured area is
|
||||
// excluded, same as the area-badge features above.
|
||||
func computeScopeAdoptionByArea(nodes []nodeAreaScopeInput, areas map[string]AreaEntry) []AreaScopeAdoption {
|
||||
// sit there at all, how many "use scope" in ANY sense, and (when the area
|
||||
// itself has a RegionScope link) how many specifically use THAT region —
|
||||
// i.e. does this geographic community actually engage with the scope the
|
||||
// area is nominally tied to, or something else entirely (or nothing at
|
||||
// all). A node outside every configured area is excluded, same as the
|
||||
// area-badge features above.
|
||||
//
|
||||
// "Uses scope" counts two distinct signals, same runs-this-region vs
|
||||
// carried-this-region's-traffic distinction as OriginatingNodesByRegion vs
|
||||
// RepeatersByRegion above: (1) the node's own default_scope, and (2) any
|
||||
// region it has ever RELAYED (relayInfo/TransportedScopes) — a repeater
|
||||
// can carry dk-horsens traffic and thereby support the Horsens area
|
||||
// without ever configuring dk-horsens as its own default_scope. relayInfo
|
||||
// may be nil (in-memory store unavailable), in which case matching falls
|
||||
// back to default_scope only.
|
||||
func computeScopeAdoptionByArea(nodes []nodeAreaScopeInput, areas map[string]AreaEntry, relayInfo map[string]RepeaterRelayInfo) []AreaScopeAdoption {
|
||||
counts := make(map[string]*AreaScopeAdoption)
|
||||
for _, n := range nodes {
|
||||
key, ok := AreaKeyForPoint(n.Lat, n.Lon, areas)
|
||||
@@ -2303,13 +2315,34 @@ func computeScopeAdoptionByArea(nodes []nodeAreaScopeInput, areas map[string]Are
|
||||
counts[key] = c
|
||||
}
|
||||
c.TotalNodes++
|
||||
scope := strings.ToLower(strings.TrimPrefix(n.DefaultScope, "#"))
|
||||
if scope != "" {
|
||||
c.NodesWithAnyScope++
|
||||
if c.RegionScope != "" && scope == strings.ToLower(c.RegionScope) {
|
||||
c.NodesMatchingArea++
|
||||
|
||||
ownScope := strings.ToLower(strings.TrimPrefix(n.DefaultScope, "#"))
|
||||
hasAnyScope := ownScope != ""
|
||||
var normalizedRegion string
|
||||
regionMatch := false
|
||||
if c.RegionScope != "" {
|
||||
normalizedRegion = strings.ToLower(c.RegionScope)
|
||||
regionMatch = ownScope == normalizedRegion
|
||||
}
|
||||
|
||||
if info, ok := relayInfo[n.PublicKey]; ok && len(info.TransportedScopes) > 0 {
|
||||
hasAnyScope = true
|
||||
if normalizedRegion != "" && !regionMatch {
|
||||
for _, r := range info.TransportedScopes {
|
||||
if strings.ToLower(strings.TrimPrefix(r, "#")) == normalizedRegion {
|
||||
regionMatch = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if hasAnyScope {
|
||||
c.NodesWithAnyScope++
|
||||
}
|
||||
if regionMatch {
|
||||
c.NodesMatchingArea++
|
||||
}
|
||||
}
|
||||
result := make([]AreaScopeAdoption, 0, len(counts))
|
||||
for _, c := range counts {
|
||||
|
||||
+46
-2
@@ -2345,7 +2345,7 @@ func TestComputeScopeAdoptionByArea(t *testing.T) {
|
||||
{Lat: 51.0, Lon: 4.0, DefaultScope: "#belgium"}, // outside every configured area, excluded
|
||||
}
|
||||
|
||||
got := computeScopeAdoptionByArea(nodes, areas)
|
||||
got := computeScopeAdoptionByArea(nodes, areas, nil)
|
||||
if len(got) != 2 {
|
||||
t.Fatalf("got %d areas, want 2 (ODE and GOT) -- result: %+v", len(got), got)
|
||||
}
|
||||
@@ -2377,8 +2377,52 @@ func TestComputeScopeAdoptionByArea(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestComputeScopeAdoptionByArea_RelayedRegionCounts covers the case
|
||||
// dborup flagged directly: a repeater sitting inside the Horsens area that
|
||||
// has RELAYED dk-horsens traffic supports that region, even if its own
|
||||
// default_scope is something else (or unset entirely) — matching must not
|
||||
// be limited to default_scope, same runs-this-region vs
|
||||
// carried-this-region's-traffic distinction as RepeatersByRegion vs
|
||||
// OriginatingNodesByRegion elsewhere in this file.
|
||||
func TestComputeScopeAdoptionByArea_RelayedRegionCounts(t *testing.T) {
|
||||
f := func(v float64) *float64 { return &v }
|
||||
areas := map[string]AreaEntry{
|
||||
"HORSENS": {Label: "Horsens", RegionScope: "dk-horsens", LatMin: f(55.76), LatMax: f(55.94), LonMin: f(9.6), LonMax: f(9.96)},
|
||||
}
|
||||
nodes := []nodeAreaScopeInput{
|
||||
{PublicKey: "relayer01", Lat: 55.85, Lon: 9.85, DefaultScope: "#dk"}, // own scope is the generic #dk, NOT dk-horsens
|
||||
{PublicKey: "plainnode1", Lat: 55.86, Lon: 9.86, DefaultScope: ""}, // no scope, no relay activity either
|
||||
{PublicKey: "relayerother", Lat: 55.87, Lon: 9.87, DefaultScope: ""}, // relays something, but not dk-horsens
|
||||
}
|
||||
relayInfo := map[string]RepeaterRelayInfo{
|
||||
"relayer01": {TransportedScopes: []string{"#dk-horsens"}},
|
||||
"relayerother": {TransportedScopes: []string{"#dk-aarhus"}},
|
||||
}
|
||||
|
||||
got := computeScopeAdoptionByArea(nodes, areas, relayInfo)
|
||||
if len(got) != 1 {
|
||||
t.Fatalf("got %d areas, want 1", len(got))
|
||||
}
|
||||
h := got[0]
|
||||
if h.TotalNodes != 3 {
|
||||
t.Errorf("TotalNodes = %d, want 3", h.TotalNodes)
|
||||
}
|
||||
// relayer01 (relays dk-horsens) and relayerother (relays something,
|
||||
// just not dk-horsens) both "use scope" in some sense; plainnode1 does
|
||||
// nothing at all.
|
||||
if h.NodesWithAnyScope != 2 {
|
||||
t.Errorf("NodesWithAnyScope = %d, want 2", h.NodesWithAnyScope)
|
||||
}
|
||||
// Only relayer01 specifically relays THIS area's own region
|
||||
// (dk-horsens) -- despite its own default_scope being the unrelated,
|
||||
// generic #dk.
|
||||
if h.NodesMatchingArea != 1 {
|
||||
t.Errorf("NodesMatchingArea = %d, want 1 (relayer01 relays dk-horsens even though its default_scope is #dk)", h.NodesMatchingArea)
|
||||
}
|
||||
}
|
||||
|
||||
func TestComputeScopeAdoptionByArea_Empty(t *testing.T) {
|
||||
got := computeScopeAdoptionByArea(nil, map[string]AreaEntry{"DK": {Label: "Danmark"}})
|
||||
got := computeScopeAdoptionByArea(nil, map[string]AreaEntry{"DK": {Label: "Danmark"}}, nil)
|
||||
if len(got) != 0 {
|
||||
t.Errorf("expected no areas with 0 nodes, got %+v", got)
|
||||
}
|
||||
|
||||
@@ -3746,7 +3746,15 @@ func (s *Server) handleScopeStats(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if s.cfg != nil && len(s.cfg.Areas) > 0 {
|
||||
if nodes, err := s.db.GetNodesForScopeAdoption(); err == nil {
|
||||
resp.ScopeAdoptionByArea = computeScopeAdoptionByArea(nodes, s.cfg.Areas)
|
||||
// Reuses the same cached relay-info map as RepeatersByRegion
|
||||
// above (GetRepeaterRelayInfoMap never rebuilds inline on a
|
||||
// populated cache) so a node relaying an area's region counts
|
||||
// as using it, not just one with a matching default_scope.
|
||||
var relayInfo map[string]RepeaterRelayInfo
|
||||
if s.store != nil {
|
||||
relayInfo = s.store.GetRepeaterRelayInfoMap(s.cfg.GetHealthThresholds().RelayActiveHours)
|
||||
}
|
||||
resp.ScopeAdoptionByArea = computeScopeAdoptionByArea(nodes, s.cfg.Areas, relayInfo)
|
||||
} else {
|
||||
log.Printf("WARN GetNodesForScopeAdoption: %v", err)
|
||||
}
|
||||
|
||||
+12
-8
@@ -210,15 +210,19 @@ type AreaScopeAdoption struct {
|
||||
Label string `json:"label"`
|
||||
RegionScope string `json:"regionScope,omitempty"`
|
||||
TotalNodes int `json:"totalNodes"`
|
||||
// NodesWithAnyScope is how many of TotalNodes have ANY default_scope
|
||||
// configured, regardless of which region it is.
|
||||
// NodesWithAnyScope is how many of TotalNodes "use scope" in any
|
||||
// sense: either their own default_scope is set, or they've ever
|
||||
// relayed traffic carrying ANY region's scope (a repeater can support
|
||||
// a region purely by relaying it, without configuring that region as
|
||||
// its own — see computeScopeAdoptionByArea).
|
||||
NodesWithAnyScope int `json:"nodesWithAnyScope"`
|
||||
// NodesMatchingArea is the subset of NodesWithAnyScope whose scope
|
||||
// matches this area's own RegionScope specifically. Only meaningful
|
||||
// when RegionScope is set — 0 otherwise (not the same as "0 of them
|
||||
// match", there's simply nothing configured to match against). No
|
||||
// omitempty: a real 0 count must still serialize, or the frontend has
|
||||
// no way to distinguish it from "field absent".
|
||||
// NodesMatchingArea is the subset of NodesWithAnyScope that
|
||||
// specifically use THIS area's own RegionScope — via default_scope OR
|
||||
// by having relayed it. Only meaningful when RegionScope is set — 0
|
||||
// otherwise (not the same as "0 of them match", there's simply
|
||||
// nothing configured to match against). No omitempty: a real 0 count
|
||||
// must still serialize, or the frontend has no way to distinguish it
|
||||
// from "field absent".
|
||||
NodesMatchingArea int `json:"nodesMatchingArea"`
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -5091,7 +5091,7 @@ function destroy() { _stopRolesRefresh(); _stopScopesRefresh(); _stopForeignTraf
|
||||
}).join('');
|
||||
setSectionHtml(areaAdoptEl, detailsSection(
|
||||
'Scope Adoption by Area (' + byArea.length.toLocaleString() + ' areas)',
|
||||
'All-time — every configured area with at least one positioned node. Shows whether that geographic community actually runs the region-scope system, and whether it runs the specific region this area is linked to.',
|
||||
'All-time — every configured area with at least one positioned node. "With Any Scope" and "Matching" both count a node\'s own default_scope AND anything it has ever relayed — a repeater carrying dk-horsens traffic supports that region even without configuring it as its own.',
|
||||
'<table class="data-table analytics-table">' +
|
||||
'<thead><tr><th>Area</th><th>Nodes</th><th>With Any Scope</th><th>Matching Area’s Own Region</th></tr></thead>' +
|
||||
'<tbody>' + areaRows + '</tbody>' +
|
||||
|
||||
Reference in new issue
Block a user