From e8e7b670e677d56eafc2fabb9d4b32dd14521791 Mon Sep 17 00:00:00 2001 From: dborup Date: Wed, 22 Jul 2026 10:09:43 +0200 Subject: [PATCH] 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 --- cmd/server/config.go | 9 +++++ cmd/server/config_client_geofilter_test.go | 46 ++++++++++++++++++++++ cmd/server/routes.go | 18 +++++++++ 3 files changed, 73 insertions(+) diff --git a/cmd/server/config.go b/cmd/server/config.go index 46cd5e9a..9abd6580 100644 --- a/cmd/server/config.go +++ b/cmd/server/config.go @@ -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 diff --git a/cmd/server/config_client_geofilter_test.go b/cmd/server/config_client_geofilter_test.go index 7cb7fc8d..188c5794 100644 --- a/cmd/server/config_client_geofilter_test.go +++ b/cmd/server/config_client_geofilter_test.go @@ -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) + } + }) +} diff --git a/cmd/server/routes.go b/cmd/server/routes.go index 8cfcef18..b7a4cc68 100644 --- a/cmd/server/routes.go +++ b/cmd/server/routes.go @@ -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 }