diff --git a/cmd/server/db.go b/cmd/server/db.go index be7e5310..c84dff2e 100644 --- a/cmd/server/db.go +++ b/cmd/server/db.go @@ -1105,6 +1105,17 @@ func (db *DB) GetObserverIdsForRegion(regionParam string) ([]string, error) { return ids, nil } +// normalizeRegionCodes parses a region query parameter into a list of upper-case +// IATA codes. Returns nil to signal "no filter" (match all regions). +// +// Sentinel handling (issue #770): the frontend region filter dropdown labels its +// catch-all option "All". When that option is selected the UI may send +// ?region=All; older code interpreted that literally and tried to match an +// IATA code "ALL", which never exists, returning an empty result set. Treat +// "All" / "ALL" / "all" (case-insensitive, optionally surrounded by whitespace +// or mixed with empty CSV slots) as equivalent to an empty value. +// +// Real IATA codes (e.g. "SJC", "PDX") still pass through unchanged. func normalizeRegionCodes(regionParam string) []string { if regionParam == "" { return nil @@ -1113,9 +1124,13 @@ func normalizeRegionCodes(regionParam string) []string { codes := make([]string, 0, len(tokens)) for _, token := range tokens { code := strings.TrimSpace(strings.ToUpper(token)) - if code != "" { - codes = append(codes, code) + if code == "" || code == "ALL" { + continue } + codes = append(codes, code) + } + if len(codes) == 0 { + return nil } return codes } diff --git a/cmd/server/region_filter_test.go b/cmd/server/region_filter_test.go new file mode 100644 index 00000000..6c4d74dd --- /dev/null +++ b/cmd/server/region_filter_test.go @@ -0,0 +1,41 @@ +package main + +import ( + "testing" +) + +// Issue #770: the region filter dropdown's "All" option was being sent to the +// backend as ?region=All. The backend then tried to match observers with IATA +// code "ALL", which never exists, producing an empty channel/packet list. +// +// "All" / "ALL" / "all" / "" must all be treated as "no region filter". +func TestNormalizeRegionCodes_AllIsNoFilter(t *testing.T) { + cases := []struct { + name string + in string + }{ + {"empty", ""}, + {"literal All (frontend dropdown label)", "All"}, + {"upper ALL", "ALL"}, + {"lower all", "all"}, + {"All with whitespace", " All "}, + {"All in csv with empty siblings", "All,"}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + got := normalizeRegionCodes(tc.in) + if got != nil { + t.Errorf("normalizeRegionCodes(%q) = %v, want nil (no filter)", tc.in, got) + } + }) + } +} + +// Real region codes must still pass through unchanged (case-folded to upper). +// This locks in that the "All" handling does not regress legitimate filters. +func TestNormalizeRegionCodes_RealCodesPreserved(t *testing.T) { + got := normalizeRegionCodes("sjc,PDX") + if len(got) != 2 || got[0] != "SJC" || got[1] != "PDX" { + t.Errorf("normalizeRegionCodes(\"sjc,PDX\") = %v, want [SJC PDX]", got) + } +} diff --git a/config.example.json b/config.example.json index 6dbf268c..1b8b0d27 100644 --- a/config.example.json +++ b/config.example.json @@ -233,5 +233,5 @@ "_comment_hashChannels": "Channel names whose keys are derived via SHA256. Key = SHA256(name)[:16]. Listed here so the ingestor can auto-derive keys.", "_comment_defaultRegion": "IATA code shown by default in region filters.", "_comment_mapDefaults": "Initial map center [lat, lon] and zoom level.", - "_comment_regions": "IATA code to display name mapping. Packets are tagged with region codes by MQTT topic structure." + "_comment_regions": "IATA code → display name mapping for the region filter UI. Each key is a 3-letter IATA code that an observer is tagged with (resolved priority: MQTT payload `region` field > topic-derived region > mqttSources.region). Observers without an IATA tag will not appear under any region filter — only under 'All Regions'. The region filter dropdown shows one entry per code listed here PLUS any extra IATA codes the server discovers from observers at runtime (so you can omit codes here and they will still be selectable, just labelled with the bare IATA code instead of a friendly name). Selecting 'All Regions' (or no region) returns results from every observer including those with no IATA tag; selecting one or more codes restricts results to packets observed by observers tagged with those codes. The reserved value 'All' (case-insensitive) is treated as 'no filter' on the server, so the URL ?region=All behaves identically to omitting the param. Issue #770." }