feat: link the foreign/domestic geo_filter boundary to a named area

Adds Config.HomeArea: when set to an existing area's key, that area's
geometry becomes the effective geo_filter (getGeoFilter), instead of
maintaining a second, independently-drawn boundary that can drift out
of sync -- exactly what happened with Germany's box bleeding into
southern Denmark earlier this session. Falls back to the standalone
GeoFilter field when HomeArea is unset or unresolved, so existing
deployments see no behavior change.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
dborupandClaude Sonnet 5 committed 2026-07-22 10:09:43 +02:00
1 parent 896a21cda7
commit e8e7b670e6
3 files changed
+73

No files matched your search

+9
View File
@@ -244,6 +244,15 @@ type Config struct {
Areas map[string]AreaEntry `json:"areas,omitempty"`
// HomeArea names an entry in Areas whose geometry defines "home" for
// the foreign/domestic classification (geo_filter above), instead of
// maintaining a second, separately-drawn boundary that can drift out
// of sync with the area map (see AreaForPoint/AreaKeysForPoint).
// When set and the named area exists, it takes priority over any
// standalone GeoFilter value — see (*Server).getGeoFilter. Leave
// unset to keep using GeoFilter exactly as before.
HomeArea string `json:"homeArea,omitempty"`
Timestamps *TimestampConfig `json:"timestamps,omitempty"`
// CORSAllowedOrigins is the list of origins permitted to make cross-origin
@@ -76,3 +76,49 @@ func TestConfigClientOmitsGeoFilterWhenUnconfigured(t *testing.T) {
t.Error("expected geoFilter to be omitted from /api/config/client when unconfigured")
}
}
// TestGetGeoFilter_HomeAreaTakesPriority covers the new HomeArea linkage:
// when cfg.HomeArea names an existing area, its geometry is the effective
// geo_filter -- a single boundary shared with the areas system instead of
// a second copy that can drift out of sync (exactly what happened with
// Germany's box vs. Denmark's area earlier this session).
func TestGetGeoFilter_HomeAreaTakesPriority(t *testing.T) {
srv, _ := setupTestServer(t)
standaloneLat := 10.0
srv.cfg.GeoFilter = &GeoFilterConfig{LatMin: &standaloneLat} // deliberately different from the area
f := func(v float64) *float64 { return &v }
srv.cfg.Areas = map[string]AreaEntry{
"DK": {Label: "Danmark", LatMin: f(54.5), LatMax: f(57.8), LonMin: f(8.0), LonMax: f(15.25)},
}
srv.cfg.HomeArea = "DK"
gf := srv.getGeoFilter()
if gf == nil || gf.LatMin == nil || *gf.LatMin != 54.5 {
t.Fatalf("getGeoFilter() = %+v, want DK area's LatMin=54.5 (HomeArea should win over the standalone GeoFilter)", gf)
}
}
// TestGetGeoFilter_FallsBackWhenHomeAreaUnresolved confirms existing
// deployments (no HomeArea configured, or one naming a since-removed area)
// see no behavior change -- the standalone GeoFilter still applies.
func TestGetGeoFilter_FallsBackWhenHomeAreaUnresolved(t *testing.T) {
srv, _ := setupTestServer(t)
standaloneLat := 10.0
srv.cfg.GeoFilter = &GeoFilterConfig{LatMin: &standaloneLat}
t.Run("HomeArea unset", func(t *testing.T) {
srv.cfg.HomeArea = ""
gf := srv.getGeoFilter()
if gf == nil || gf.LatMin == nil || *gf.LatMin != 10.0 {
t.Errorf("getGeoFilter() = %+v, want the standalone GeoFilter (LatMin=10.0)", gf)
}
})
t.Run("HomeArea names a nonexistent area", func(t *testing.T) {
srv.cfg.HomeArea = "NOPE"
gf := srv.getGeoFilter()
if gf == nil || gf.LatMin == nil || *gf.LatMin != 10.0 {
t.Errorf("getGeoFilter() = %+v, want the standalone GeoFilter (LatMin=10.0)", gf)
}
})
}
+18
View File
@@ -18,6 +18,7 @@ import (
"time"
"github.com/gorilla/mux"
"github.com/meshcore-analyzer/geofilter"
"github.com/meshcore-analyzer/packetpath"
"github.com/meshcore-analyzer/prunequeue"
regionutil "github.com/meshcore-analyzer/regions"
@@ -180,9 +181,26 @@ func (s *Server) isPubkeyHidden(pubkey string) bool {
return s.cfg.IsNameHidden(name)
}
// getGeoFilter returns the effective home-boundary geometry. If
// cfg.HomeArea names an existing entry in cfg.Areas, that area's geometry
// wins — a single boundary shared with the areas system, instead of a
// second copy that can silently drift (see cfg.HomeArea doc comment).
// Falls back to the standalone GeoFilter field when HomeArea is unset or
// doesn't resolve, so existing deployments see no behavior change.
func (s *Server) getGeoFilter() *GeoFilterConfig {
s.cfgMu.RLock()
defer s.cfgMu.RUnlock()
if s.cfg != nil && s.cfg.HomeArea != "" {
if area, ok := s.cfg.Areas[s.cfg.HomeArea]; ok {
return &geofilter.Config{
Polygon: area.Polygon,
LatMin: area.LatMin,
LatMax: area.LatMax,
LonMin: area.LonMin,
LonMax: area.LonMax,
}
}
}
return s.cfg.GeoFilter
}