diff --git a/cmd/ingestor/db.go b/cmd/ingestor/db.go index 1fe7d7d8..732fb98c 100644 --- a/cmd/ingestor/db.go +++ b/cmd/ingestor/db.go @@ -16,6 +16,7 @@ import ( "github.com/meshcore-analyzer/dbschema" "github.com/meshcore-analyzer/packetpath" + "github.com/meshcore-analyzer/regions" _ "modernc.org/sqlite" ) @@ -1729,6 +1730,7 @@ func (s *Store) UpdateNodeConfiguredScope(pubkey, scope, reportedAt string) erro if pubkey == "" { return nil } + scope = normalizeConfiguredScopeList(scope) // Last-write-wins: skip if the stored confirmation is newer-or-equal. if reportedAt != "" { var curAt sql.NullString @@ -1749,6 +1751,40 @@ func (s *Store) UpdateNodeConfiguredScope(pubkey, scope, reportedAt string) erro return err } +// normalizeConfiguredScopeList applies the same "#"-prefix normalization +// default_scope already gets (regions.Normalize, via matchScope) to a +// comma-separated /neighbors-report scope list, so both fields display +// consistently (flagged by cwichura on PR #1867: configured_scope was +// stored as the observer's raw OTA-query string -- "dk", not "#dk"). +// +// "*" is passed through UNCHANGED: it's a MeshCore protocol wildcard +// ("responds to any scope"), not a real hashRegion name, so "#"-prefixing +// it would misleadingly present it as one. Empty input stays empty -- a +// responded-with-no-scopes report is a valid, meaningful "none configured" +// state (see UpdateNodeConfiguredScope's doc comment) and must not gain a +// stray "#". +func normalizeConfiguredScopeList(raw string) string { + if raw == "" { + return "" + } + parts := strings.Split(raw, ",") + out := make([]string, 0, len(parts)) + for _, p := range parts { + p = strings.TrimSpace(p) + if p == "" { + continue + } + if p == "*" { + out = append(out, p) + continue + } + if name, ok := regions.Normalize(p); ok { + out = append(out, name) + } + } + return strings.Join(out, ",") +} + // RecordNaiveSkew is called when resolveRxTime() clamps a packet's envelope // timestamp because the observer is emitting a zone-less local-time string // off from UTC by more than 15 min (issue #1478). Stamps the observer's diff --git a/cmd/ingestor/issue1865_test.go b/cmd/ingestor/issue1865_test.go index 79a3c244..401c7df8 100644 --- a/cmd/ingestor/issue1865_test.go +++ b/cmd/ingestor/issue1865_test.go @@ -77,13 +77,13 @@ func TestHandleNeighborsReportWritesSelfAndResponded(t *testing.T) { if sc, _ := configuredScope(t, store, originLower); !sc.Valid || sc.String != "*" { t.Errorf("self configured_scope = %v, want '*'", sc) } - // responded neighbor written, lowercased. - if sc, _ := configuredScope(t, store, respLower); !sc.Valid || sc.String != "de,eu" { - t.Errorf("responded configured_scope = %v, want 'de,eu'", sc) + // responded neighbor written, lowercased, and normalized to #-prefixed scope form. + if sc, _ := configuredScope(t, store, respLower); !sc.Valid || sc.String != "#de,#eu" { + t.Errorf("responded configured_scope = %v, want '#de,#eu'", sc) } // timeout neighbor untouched — prior confirmed value survives. - if sc, _ := configuredScope(t, store, timeoutLower); !sc.Valid || sc.String != "eu" { - t.Errorf("timeout node configured_scope = %v, want prior 'eu' (must not be cleared)", sc) + if sc, _ := configuredScope(t, store, timeoutLower); !sc.Valid || sc.String != "#eu" { + t.Errorf("timeout node configured_scope = %v, want prior '#eu' (must not be cleared)", sc) } } @@ -139,22 +139,22 @@ func TestUpdateNodeConfiguredScopeLastWriteWins(t *testing.T) { if err := store.UpdateNodeConfiguredScope(pk, "stale", "2026-07-24T00:00:00Z"); err != nil { t.Fatal(err) } - if sc, _ := configuredScope(t, store, pk); sc.String != "eu" { - t.Errorf("configured_scope = %q, want 'eu' (older report must not overwrite)", sc.String) + if sc, _ := configuredScope(t, store, pk); sc.String != "#eu" { + t.Errorf("configured_scope = %q, want '#eu' (older report must not overwrite)", sc.String) } // Newer report updates. if err := store.UpdateNodeConfiguredScope(pk, "de", "2026-07-26T00:00:00Z"); err != nil { t.Fatal(err) } - if sc, _ := configuredScope(t, store, pk); sc.String != "de" { - t.Errorf("configured_scope = %q, want 'de' (newer report should update)", sc.String) + if sc, _ := configuredScope(t, store, pk); sc.String != "#de" { + t.Errorf("configured_scope = %q, want '#de' (newer report should update)", sc.String) } // inactive_nodes mirrored. var inactive sql.NullString if err := store.db.QueryRow(`SELECT configured_scope FROM inactive_nodes WHERE public_key = ?`, pk).Scan(&inactive); err != nil { t.Fatal(err) } - if inactive.String != "de" { - t.Errorf("inactive_nodes.configured_scope = %q, want 'de'", inactive.String) + if inactive.String != "#de" { + t.Errorf("inactive_nodes.configured_scope = %q, want '#de'", inactive.String) } }