mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-10-23 23:12:49 +00:00
feat: allow multiple hashRegions scopes per area
AreaEntry.RegionScope (single string) -> RegionScopes ([]string), so a broad umbrella area (e.g. Europa) can link more than one scope name (e.g. both "eu" and "europe") -- a node matching any one of them now counts as supporting the area in computeScopeAdoptionByArea, the "Scope Adoption by Area" section, and the regionScope->label lookup used to annotate region codes elsewhere on the Scopes tab.
This commit is contained in:
1 parent
17b86361d9
commit
f11d3d69e5
9 files changed
+133
-68
No files matched your search
@@ -18,17 +18,17 @@ draw or which country you run CoreScope in.
|
||||
"areas": {
|
||||
"DK": {
|
||||
"label": "Danmark (alle)",
|
||||
"regionScope": "dk",
|
||||
"regionScopes": ["dk"],
|
||||
"polygon": [[54.85, 8.65], [55.50, 8.10], [57.10, 8.20], ...]
|
||||
},
|
||||
"AAR": {
|
||||
"label": "Aarhus by",
|
||||
"regionScope": "dk-aarhus",
|
||||
"regionScopes": ["dk-aarhus"],
|
||||
"polygon": [[56.35, 10.33], [56.31, 10.45], ...]
|
||||
},
|
||||
"EU": {
|
||||
"label": "Europa (alle)",
|
||||
"regionScope": "eu",
|
||||
"regionScopes": ["eu", "europe"],
|
||||
"latMin": 34.0, "latMax": 71.5, "lonMin": -25.0, "lonMax": 45.0
|
||||
}
|
||||
}
|
||||
@@ -49,14 +49,17 @@ Each entry has three parts:
|
||||
If `polygon` has at least 3 points, it's used; otherwise the code falls
|
||||
back to the box. An area can have one or the other, not both meaningfully
|
||||
at once.
|
||||
- **`regionScope`** (optional) — links this area to a hashRegions channel
|
||||
scope (e.g. `"dk-aarhus"`, stored *without* the leading `#`). This powers
|
||||
the Scopes tab's "Scope Adoption by Area" section: which nodes physically
|
||||
in this area actually use (via their own `default_scope`, or by relaying
|
||||
it) the region this area represents. Leave it unset if there's no
|
||||
matching hashRegion — the area still works for everything else (badges,
|
||||
node counts, the area filter), it just won't have anything to compare
|
||||
scope-adoption against.
|
||||
- **`regionScopes`** (optional) — links this area to one or more hashRegions
|
||||
channel scopes (e.g. `["dk-aarhus"]`, each stored *without* the leading
|
||||
`#`). This powers the Scopes tab's "Scope Adoption by Area" section: which
|
||||
nodes physically in this area actually use (via their own `default_scope`,
|
||||
or by relaying it) *any* of the regions this area represents. Most areas
|
||||
only need one scope, but a broad umbrella area can link several — e.g.
|
||||
Europa linking both `"eu"` and `"europe"` if both names see real traffic —
|
||||
and a node matching any one of them counts as supporting the area. Leave
|
||||
it unset/empty if there's no matching hashRegion — the area still works
|
||||
for everything else (badges, node counts, the area filter), it just won't
|
||||
have anything to compare scope-adoption against.
|
||||
|
||||
## Hierarchy: draw it, don't declare it
|
||||
|
||||
|
||||
@@ -296,10 +296,10 @@ func TestHandleConfigAreas(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleConfigAreas_RegionScope(t *testing.T) {
|
||||
func TestHandleConfigAreas_RegionScopes(t *testing.T) {
|
||||
db := setupTestDBv2(t)
|
||||
cfg := &Config{Areas: map[string]AreaEntry{
|
||||
"AAR": {Label: "Aarhus by", RegionScope: "dk-aarhus"},
|
||||
"AAR": {Label: "Aarhus by", RegionScopes: []string{"dk-aarhus"}},
|
||||
"MST": {Label: "Maastricht"},
|
||||
}}
|
||||
|
||||
@@ -319,11 +319,12 @@ func TestHandleConfigAreas_RegionScope(t *testing.T) {
|
||||
for _, entry := range result {
|
||||
byKey[entry["key"].(string)] = entry
|
||||
}
|
||||
if byKey["AAR"]["regionScope"] != "dk-aarhus" {
|
||||
t.Errorf("AAR regionScope = %v, want \"dk-aarhus\"", byKey["AAR"]["regionScope"])
|
||||
aarScopes, _ := byKey["AAR"]["regionScopes"].([]interface{})
|
||||
if len(aarScopes) != 1 || aarScopes[0] != "dk-aarhus" {
|
||||
t.Errorf("AAR regionScopes = %v, want [\"dk-aarhus\"]", byKey["AAR"]["regionScopes"])
|
||||
}
|
||||
if _, present := byKey["MST"]["regionScope"]; present {
|
||||
t.Errorf("MST should omit regionScope entirely (unset), got %v", byKey["MST"]["regionScope"])
|
||||
if _, present := byKey["MST"]["regionScopes"]; present {
|
||||
t.Errorf("MST should omit regionScopes entirely (unset), got %v", byKey["MST"]["regionScopes"])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,11 +25,13 @@ type AreaEntry struct {
|
||||
LonMin *float64 `json:"lonMin,omitempty"`
|
||||
LonMax *float64 `json:"lonMax,omitempty"`
|
||||
|
||||
// RegionScope links this area to a hashRegions channel scope (e.g.
|
||||
// "dk-aarhus"), stored without the leading "#" — callers should run it
|
||||
// through regions.Normalize before comparing against a scope_name.
|
||||
// Left empty when no confident area<->scope mapping exists.
|
||||
RegionScope string `json:"regionScope,omitempty"`
|
||||
// RegionScopes links this area to one or more hashRegions channel
|
||||
// scopes (e.g. "dk-aarhus"; a broad umbrella area can have several,
|
||||
// e.g. Europa linking both "eu" and "europe"), stored without the
|
||||
// leading "#" — callers should run each through regions.Normalize
|
||||
// before comparing against a scope_name. Left empty when no confident
|
||||
// area<->scope mapping exists.
|
||||
RegionScopes []string `json:"regionScopes,omitempty"`
|
||||
}
|
||||
|
||||
// AreaForPoint returns the label of the most specific configured area that
|
||||
@@ -43,7 +45,7 @@ func AreaForPoint(lat, lon float64, areas map[string]AreaEntry) (label string, o
|
||||
|
||||
// AreaKeyForPoint is AreaForPoint but returns the area's config key (e.g.
|
||||
// "ODE") instead of its display label — for callers that need to look up
|
||||
// other fields on the matched AreaEntry (e.g. RegionScope), not just show
|
||||
// other fields on the matched AreaEntry (e.g. RegionScopes), not just show
|
||||
// the name.
|
||||
func AreaKeyForPoint(lat, lon float64, areas map[string]AreaEntry) (key string, ok bool) {
|
||||
key, _, ok = areaMatchForPoint(lat, lon, areas)
|
||||
|
||||
+14
-8
@@ -2299,7 +2299,7 @@ func (db *DB) GetNodesForScopeAdoption() ([]nodeAreaScopeInput, error) {
|
||||
// computeScopeAdoptionByArea buckets nodes by their most specific
|
||||
// configured area and tallies, per area: how many nodes 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
|
||||
// RegionScopes 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.
|
||||
@@ -2322,7 +2322,7 @@ func (db *DB) GetNodesForScopeAdoption() ([]nodeAreaScopeInput, error) {
|
||||
// may be nil (in-memory store unavailable), in which case matching falls
|
||||
// back to default_scope only.
|
||||
//
|
||||
// For areas with a RegionScope link, also returns the actual node lists
|
||||
// For areas with a RegionScopes link, also returns the actual node lists
|
||||
// (Matching/NotMatching) — dborup wanted to see which specific nodes in
|
||||
// e.g. Østjylland relay dk-oj (correctly "support" the area) and which
|
||||
// don't, not just an aggregate count.
|
||||
@@ -2347,7 +2347,7 @@ func computeScopeAdoptionByArea(nodes []nodeAreaScopeInput, areas map[string]Are
|
||||
c, exists := counts[key]
|
||||
if !exists {
|
||||
a := areas[key]
|
||||
c = &AreaScopeAdoption{AreaKey: key, Label: a.Label, RegionScope: a.RegionScope}
|
||||
c = &AreaScopeAdoption{AreaKey: key, Label: a.Label, RegionScopes: a.RegionScopes}
|
||||
counts[key] = c
|
||||
}
|
||||
c.TotalNodes++
|
||||
@@ -2355,11 +2355,17 @@ func computeScopeAdoptionByArea(nodes []nodeAreaScopeInput, areas map[string]Are
|
||||
c.NodesWithAnyScope++
|
||||
}
|
||||
// Matching/NotMatching per-node lists only make sense when the
|
||||
// area actually has a region to compare against — an area
|
||||
// with no RegionScope link has nothing to be "not matching".
|
||||
if c.RegionScope != "" {
|
||||
normalizedRegion := strings.ToLower(c.RegionScope)
|
||||
regionMatch := ownScope == normalizedRegion || relayedRegions[normalizedRegion]
|
||||
// area actually has region(s) to compare against — an area
|
||||
// with no RegionScopes link has nothing to be "not matching".
|
||||
if len(c.RegionScopes) > 0 {
|
||||
regionMatch := false
|
||||
for _, rs := range c.RegionScopes {
|
||||
normalizedRegion := strings.ToLower(rs)
|
||||
if ownScope == normalizedRegion || relayedRegions[normalizedRegion] {
|
||||
regionMatch = true
|
||||
break
|
||||
}
|
||||
}
|
||||
ref := RepeaterRef{Name: n.Name, PublicKey: n.PublicKey}
|
||||
if regionMatch {
|
||||
c.NodesMatchingArea++
|
||||
|
||||
+56
-9
@@ -2331,15 +2331,15 @@ func TestLoadIndexesRelayHopsFromResolvedPath(t *testing.T) {
|
||||
func TestComputeScopeAdoptionByArea(t *testing.T) {
|
||||
f := func(v float64) *float64 { return &v }
|
||||
areas := map[string]AreaEntry{
|
||||
"ODE": {Label: "Odense by", RegionScope: "dk-fyn-odense", LatMin: f(55.32), LatMax: f(55.45), LonMin: f(10.3), LonMax: f(10.5)},
|
||||
"GOT": {Label: "Göteborg, SE", LatMin: f(57.35), LatMax: f(57.90), LonMin: f(11.85), LonMax: f(12.85)}, // no RegionScope link
|
||||
"ODE": {Label: "Odense by", RegionScopes: []string{"dk-fyn-odense"}, LatMin: f(55.32), LatMax: f(55.45), LonMin: f(10.3), LonMax: f(10.5)},
|
||||
"GOT": {Label: "Göteborg, SE", LatMin: f(57.35), LatMax: f(57.90), LonMin: f(11.85), LonMax: f(12.85)}, // no RegionScopes link
|
||||
}
|
||||
|
||||
nodes := []nodeAreaScopeInput{
|
||||
{Lat: 55.4047, Lon: 10.3810, DefaultScope: "#dk-fyn-odense"}, // Odense, matches area's own region
|
||||
{Lat: 55.40, Lon: 10.40, DefaultScope: "#dk-aarhus"}, // Odense, but a DIFFERENT region
|
||||
{Lat: 55.41, Lon: 10.41, DefaultScope: ""}, // Odense, no scope at all
|
||||
{Lat: 57.68, Lon: 11.97, DefaultScope: "#dk-aarhus"}, // Göteborg, has a scope, but area has no RegionScope link
|
||||
{Lat: 57.68, Lon: 11.97, DefaultScope: "#dk-aarhus"}, // Göteborg, has a scope, but area has no RegionScopes link
|
||||
{Lat: 57.70, Lon: 11.98, DefaultScope: ""}, // Göteborg, no scope
|
||||
{Lat: 0, Lon: 0, DefaultScope: "#dk-aarhus"}, // no-fix, must be excluded entirely
|
||||
{Lat: 51.0, Lon: 4.0, DefaultScope: "#belgium"}, // outside every configured area, excluded
|
||||
@@ -2365,7 +2365,7 @@ func TestComputeScopeAdoptionByArea(t *testing.T) {
|
||||
t.Errorf("ODE.NodesMatchingArea = %d, want 1 (only the dk-fyn-odense one matches, the dk-aarhus one doesn't)", ode.NodesMatchingArea)
|
||||
}
|
||||
if len(ode.Matching) != 1 || len(ode.NotMatching) != 2 {
|
||||
t.Errorf("ODE.Matching=%v NotMatching=%v, want 1 matching + 2 not-matching (RegionScope is set)", ode.Matching, ode.NotMatching)
|
||||
t.Errorf("ODE.Matching=%v NotMatching=%v, want 1 matching + 2 not-matching (RegionScopes is set)", ode.Matching, ode.NotMatching)
|
||||
}
|
||||
|
||||
got2 := byKey["GOT"]
|
||||
@@ -2376,10 +2376,10 @@ func TestComputeScopeAdoptionByArea(t *testing.T) {
|
||||
t.Errorf("GOT.NodesWithAnyScope = %d, want 1", got2.NodesWithAnyScope)
|
||||
}
|
||||
if got2.NodesMatchingArea != 0 {
|
||||
t.Errorf("GOT.NodesMatchingArea = %d, want 0 (area has no RegionScope link to match against)", got2.NodesMatchingArea)
|
||||
t.Errorf("GOT.NodesMatchingArea = %d, want 0 (area has no RegionScopes link to match against)", got2.NodesMatchingArea)
|
||||
}
|
||||
if len(got2.Matching) != 0 || len(got2.NotMatching) != 0 {
|
||||
t.Errorf("GOT.Matching=%v NotMatching=%v, want both empty (no RegionScope, nothing to split into two groups)", got2.Matching, got2.NotMatching)
|
||||
t.Errorf("GOT.Matching=%v NotMatching=%v, want both empty (no RegionScopes, nothing to split into two groups)", got2.Matching, got2.NotMatching)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2393,7 +2393,7 @@ func TestComputeScopeAdoptionByArea(t *testing.T) {
|
||||
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)},
|
||||
"HORSENS": {Label: "Horsens", RegionScopes: []string{"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
|
||||
@@ -2443,8 +2443,8 @@ func TestComputeScopeAdoptionByArea_RelayedRegionCounts(t *testing.T) {
|
||||
func TestComputeScopeAdoptionByArea_RollsUpIntoBroaderAreas(t *testing.T) {
|
||||
f := func(v float64) *float64 { return &v }
|
||||
areas := map[string]AreaEntry{
|
||||
"DK": {Label: "Danmark (alle)", RegionScope: "dk", LatMin: f(54.5), LatMax: f(57.8), LonMin: f(8.0), LonMax: f(15.25)},
|
||||
"ODE": {Label: "Odense by", RegionScope: "dk-fyn-odense", LatMin: f(55.32), LatMax: f(55.45), LonMin: f(10.3), LonMax: f(10.5)},
|
||||
"DK": {Label: "Danmark (alle)", RegionScopes: []string{"dk"}, LatMin: f(54.5), LatMax: f(57.8), LonMin: f(8.0), LonMax: f(15.25)},
|
||||
"ODE": {Label: "Odense by", RegionScopes: []string{"dk-fyn-odense"}, LatMin: f(55.32), LatMax: f(55.45), LonMin: f(10.3), LonMax: f(10.5)},
|
||||
}
|
||||
nodes := []nodeAreaScopeInput{
|
||||
{PublicKey: "odenode01", Name: "OdenseNode", Lat: 55.4047, Lon: 10.3810, DefaultScope: "#dk-fyn-odense"}, // inside BOTH DK and ODE
|
||||
@@ -2483,3 +2483,50 @@ func TestComputeScopeAdoptionByArea_Empty(t *testing.T) {
|
||||
t.Errorf("expected no areas with 0 nodes, got %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestComputeScopeAdoptionByArea_MultipleRegionScopes covers dborup's exact
|
||||
// request: a broad umbrella area (Europa) can be linked to more than one
|
||||
// hashRegions scope at once (e.g. both "eu" and "europe"), and a node using
|
||||
// EITHER counts as matching -- not just the first-configured one.
|
||||
func TestComputeScopeAdoptionByArea_MultipleRegionScopes(t *testing.T) {
|
||||
f := func(v float64) *float64 { return &v }
|
||||
areas := map[string]AreaEntry{
|
||||
"EU": {Label: "Europa", RegionScopes: []string{"eu", "europe"}, LatMin: f(34.0), LatMax: f(71.5), LonMin: f(-25.0), LonMax: f(45.0)},
|
||||
}
|
||||
nodes := []nodeAreaScopeInput{
|
||||
{PublicKey: "usesEu", Lat: 48.0, Lon: 10.0, DefaultScope: "#eu"},
|
||||
{PublicKey: "usesEurope", Lat: 48.0, Lon: 11.0, DefaultScope: "#europe"},
|
||||
{PublicKey: "relaysEurope", Lat: 48.0, Lon: 12.0, DefaultScope: ""},
|
||||
{PublicKey: "usesNeither", Lat: 48.0, Lon: 13.0, DefaultScope: "#dk"},
|
||||
}
|
||||
relayInfo := map[string]RepeaterRelayInfo{
|
||||
"relaysEurope": {TransportedScopes: []string{"#europe"}},
|
||||
}
|
||||
|
||||
got := computeScopeAdoptionByArea(nodes, areas, relayInfo)
|
||||
if len(got) != 1 {
|
||||
t.Fatalf("got %d areas, want 1", len(got))
|
||||
}
|
||||
eu := got[0]
|
||||
if eu.TotalNodes != 4 {
|
||||
t.Errorf("TotalNodes = %d, want 4", eu.TotalNodes)
|
||||
}
|
||||
// usesEu (#eu), usesEurope (#europe), and relaysEurope (relays
|
||||
// #europe) all match one of Europa's two linked scopes; usesNeither
|
||||
// (#dk) matches neither.
|
||||
if eu.NodesMatchingArea != 3 {
|
||||
t.Errorf("NodesMatchingArea = %d, want 3 (any of #eu or #europe should count)", eu.NodesMatchingArea)
|
||||
}
|
||||
matchingKeys := map[string]bool{}
|
||||
for _, m := range eu.Matching {
|
||||
matchingKeys[m.PublicKey] = true
|
||||
}
|
||||
for _, want := range []string{"usesEu", "usesEurope", "relaysEurope"} {
|
||||
if !matchingKeys[want] {
|
||||
t.Errorf("Matching missing %q, got %v", want, eu.Matching)
|
||||
}
|
||||
}
|
||||
if len(eu.NotMatching) != 1 || eu.NotMatching[0].PublicKey != "usesNeither" {
|
||||
t.Errorf("NotMatching = %v, want just usesNeither", eu.NotMatching)
|
||||
}
|
||||
}
|
||||
@@ -489,16 +489,16 @@ func (s *Server) handleConfigClient(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
func (s *Server) handleConfigAreas(w http.ResponseWriter, r *http.Request) {
|
||||
type areaListEntry struct {
|
||||
Key string `json:"key"`
|
||||
Label string `json:"label"`
|
||||
RegionScope string `json:"regionScope,omitempty"`
|
||||
Key string `json:"key"`
|
||||
Label string `json:"label"`
|
||||
RegionScopes []string `json:"regionScopes,omitempty"`
|
||||
}
|
||||
result := make([]areaListEntry, 0, len(s.cfg.Areas))
|
||||
for k, v := range s.cfg.Areas {
|
||||
if v.Label == "" {
|
||||
continue // skip comment/invalid entries (e.g. "_comment" keys in config)
|
||||
}
|
||||
result = append(result, areaListEntry{Key: k, Label: v.Label, RegionScope: v.RegionScope})
|
||||
result = append(result, areaListEntry{Key: k, Label: v.Label, RegionScopes: v.RegionScopes})
|
||||
}
|
||||
writeJSON(w, result)
|
||||
}
|
||||
|
||||
@@ -4802,7 +4802,7 @@ func TestHandleScopeStats_ScopeAdoptionByArea(t *testing.T) {
|
||||
}
|
||||
f := func(v float64) *float64 { return &v }
|
||||
srv.cfg.Areas = map[string]AreaEntry{
|
||||
"ODE": {Label: "Odense by", RegionScope: "dk-fyn-odense", LatMin: f(55.32), LatMax: f(55.45), LonMin: f(10.3), LonMax: f(10.5)},
|
||||
"ODE": {Label: "Odense by", RegionScopes: []string{"dk-fyn-odense"}, LatMin: f(55.32), LatMax: f(55.45), LonMin: f(10.3), LonMax: f(10.5)},
|
||||
}
|
||||
|
||||
insertNode := func(pk, defaultScope string, lat, lon float64) {
|
||||
@@ -4860,7 +4860,7 @@ func TestHandleScopeStats_ScopeAdoptionByArea_ZeroMatchKeyPresent(t *testing.T)
|
||||
}
|
||||
f := func(v float64) *float64 { return &v }
|
||||
srv.cfg.Areas = map[string]AreaEntry{
|
||||
"AAR": {Label: "Aarhus by", RegionScope: "dk-aarhus", LatMin: f(56.05), LatMax: f(56.25), LonMin: f(9.95), LonMax: f(10.35)},
|
||||
"AAR": {Label: "Aarhus by", RegionScopes: []string{"dk-aarhus"}, LatMin: f(56.05), LatMax: f(56.25), LonMin: f(9.95), LonMax: f(10.35)},
|
||||
}
|
||||
// A node with a scope, but NOT the area's own region — NodesMatchingArea
|
||||
// must come out as a real, present 0, same as the live #dk-vs-#dk-aarhus
|
||||
|
||||
+14
-14
@@ -206,10 +206,10 @@ type ScopeStatsResponse struct {
|
||||
// AreaScopeAdoption is one configured area's node count and scope adoption
|
||||
// — see ScopeStatsResponse.ScopeAdoptionByArea and computeScopeAdoptionByArea.
|
||||
type AreaScopeAdoption struct {
|
||||
AreaKey string `json:"areaKey"`
|
||||
Label string `json:"label"`
|
||||
RegionScope string `json:"regionScope,omitempty"`
|
||||
TotalNodes int `json:"totalNodes"`
|
||||
AreaKey string `json:"areaKey"`
|
||||
Label string `json:"label"`
|
||||
RegionScopes []string `json:"regionScopes,omitempty"`
|
||||
TotalNodes int `json:"totalNodes"`
|
||||
// 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
|
||||
@@ -217,18 +217,18 @@ type AreaScopeAdoption struct {
|
||||
// its own — see computeScopeAdoptionByArea).
|
||||
NodesWithAnyScope int `json:"nodesWithAnyScope"`
|
||||
// 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".
|
||||
// specifically use one of THIS area's own RegionScopes — via
|
||||
// default_scope OR by having relayed it. Only meaningful when
|
||||
// RegionScopes is non-empty — 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"`
|
||||
// Matching/NotMatching are the actual nodes behind NodesMatchingArea —
|
||||
// which specific nodes in this area relay/configure the area's own
|
||||
// region (correctly "support" it) and which sit here but don't. Only
|
||||
// populated when RegionScope is set (nothing to split into two groups
|
||||
// otherwise).
|
||||
// which specific nodes in this area relay/configure any of the area's
|
||||
// own regions (correctly "support" it) and which sit here but don't.
|
||||
// Only populated when RegionScopes is non-empty (nothing to split
|
||||
// into two groups otherwise).
|
||||
Matching []RepeaterRef `json:"matching,omitempty"`
|
||||
NotMatching []RepeaterRef `json:"notMatching,omitempty"`
|
||||
}
|
||||
|
||||
+14
-8
@@ -4828,8 +4828,11 @@ function destroy() { _stopRolesRefresh(); _stopScopesRefresh(); _stopForeignTraf
|
||||
}
|
||||
|
||||
// regionScope -> area label lookup, e.g. "dk-aarhus" -> "Aarhus by".
|
||||
// Areas rarely change, so this is fetched once and reused across every
|
||||
// region-code section below rather than re-fetched per render.
|
||||
// An area can link more than one scope (e.g. Europa: both "eu" and
|
||||
// "europe"), so each one gets its own entry pointing back to the same
|
||||
// label. Areas rarely change, so this is fetched once and reused
|
||||
// across every region-code section below rather than re-fetched per
|
||||
// render.
|
||||
var regionAreaLabels = null;
|
||||
async function loadRegionAreaLabels() {
|
||||
if (regionAreaLabels) return regionAreaLabels;
|
||||
@@ -4837,14 +4840,16 @@ function destroy() { _stopRolesRefresh(); _stopScopesRefresh(); _stopForeignTraf
|
||||
try {
|
||||
var areas = await api('/config/areas', { ttl: CLIENT_TTL.nodeDetail });
|
||||
(areas || []).forEach(function(a) {
|
||||
if (a.regionScope) regionAreaLabels[a.regionScope.toLowerCase()] = a.label;
|
||||
(a.regionScopes || []).forEach(function(rs) {
|
||||
regionAreaLabels[rs.toLowerCase()] = a.label;
|
||||
});
|
||||
});
|
||||
} catch (e) { /* leave empty -- region codes render unlabeled */ }
|
||||
return regionAreaLabels;
|
||||
}
|
||||
// A configured hashRegions name is always "#"-prefixed (see
|
||||
// internal/regions.Normalize); AreaEntry.RegionScope is stored without
|
||||
// it, so strip before looking up.
|
||||
// internal/regions.Normalize); AreaEntry.RegionScopes entries are
|
||||
// stored without it, so strip before looking up.
|
||||
function regionAreaLabel(rawRegionName) {
|
||||
if (!regionAreaLabels || !rawRegionName) return null;
|
||||
var key = String(rawRegionName).replace(/^#/, '').toLowerCase();
|
||||
@@ -5098,10 +5103,11 @@ function destroy() { _stopRolesRefresh(); _stopScopesRefresh(); _stopForeignTraf
|
||||
}
|
||||
var areaGroups = byArea.map(function(a) {
|
||||
var summary, body;
|
||||
if (a.regionScope) {
|
||||
if (a.regionScopes && a.regionScopes.length) {
|
||||
var matchCount = a.nodesMatchingArea || 0;
|
||||
var scopeCodes = a.regionScopes.map(function(rs) { return '<code>#' + esc(rs) + '</code>'; }).join(' or ');
|
||||
summary = esc(a.label) + ' — ' + matchCount.toLocaleString() + ' of ' + a.totalNodes.toLocaleString() +
|
||||
' support <code>#' + esc(a.regionScope) + '</code> (' + pct(matchCount, a.totalNodes) + ')';
|
||||
' support ' + scopeCodes + ' (' + pct(matchCount, a.totalNodes) + ')';
|
||||
body =
|
||||
'<div style="margin-bottom:6px"><strong>Supporting</strong> (' + (a.matching || []).length + '): ' +
|
||||
(a.matching && a.matching.length ? nodeLinks(a.matching) : '<span class="text-muted">none</span>') +
|
||||
@@ -5112,7 +5118,7 @@ function destroy() { _stopRolesRefresh(); _stopScopesRefresh(); _stopForeignTraf
|
||||
} else {
|
||||
summary = esc(a.label) + ' — ' + a.totalNodes.toLocaleString() + ' node' + (a.totalNodes === 1 ? '' : 's') +
|
||||
', <span class="text-muted">no region linked to this area</span>';
|
||||
body = '<p class="text-muted" style="margin:0;font-size:0.85em">This area has no regionScope configured, so there\'s nothing to check adoption against.</p>';
|
||||
body = '<p class="text-muted" style="margin:0;font-size:0.85em">This area has no regionScopes configured, so there\'s nothing to check adoption against.</p>';
|
||||
}
|
||||
return '<details style="margin-bottom:8px" data-key="area-adopt:' + esc(a.areaKey) + '">' +
|
||||
'<summary style="cursor:pointer">' + summary + '</summary>' +
|
||||
|
||||
Reference in new issue
Block a user