diff --git a/cmd/server/coverage_test.go b/cmd/server/coverage_test.go index fd947902..7c1e1dff 100644 --- a/cmd/server/coverage_test.go +++ b/cmd/server/coverage_test.go @@ -1426,6 +1426,40 @@ func TestGetNodeLocations(t *testing.T) { } } +// --- GetNodeLocationsByKeys --- + +func TestGetNodeLocationsByKeys(t *testing.T) { + db := setupTestDB(t) + defer db.Close() + seedTestData(t, db) + + // Query with a known key + pk := "aabbccdd11223344" + locs := db.GetNodeLocationsByKeys([]string{pk}) + if len(locs) != 1 { + t.Errorf("expected 1 location, got %d", len(locs)) + } + if entry, ok := locs[strings.ToLower(pk)]; ok { + if entry["lat"] == nil { + t.Error("expected non-nil lat") + } + } else { + t.Error("expected node location for test repeater") + } + + // Query with no keys returns empty map + empty := db.GetNodeLocationsByKeys([]string{}) + if len(empty) != 0 { + t.Errorf("expected 0 locations for empty keys, got %d", len(empty)) + } + + // Query with unknown key returns empty map + unknown := db.GetNodeLocationsByKeys([]string{"nonexistent"}) + if len(unknown) != 0 { + t.Errorf("expected 0 locations for unknown key, got %d", len(unknown)) + } +} + // --- Store edge cases --- func TestStoreQueryPacketsEdgeCases(t *testing.T) { diff --git a/cmd/server/db.go b/cmd/server/db.go index 99a0ff4d..5ea431a1 100644 --- a/cmd/server/db.go +++ b/cmd/server/db.go @@ -1497,6 +1497,39 @@ func (db *DB) GetNodeLocations() map[string]map[string]interface{} { return result } +// GetNodeLocationsByKeys returns location data only for the given public keys. +// This avoids fetching ALL nodes when only a few keys need to be matched. +func (db *DB) GetNodeLocationsByKeys(keys []string) map[string]map[string]interface{} { + result := make(map[string]map[string]interface{}) + if len(keys) == 0 { + return result + } + placeholders := make([]string, len(keys)) + args := make([]interface{}, len(keys)) + for i, k := range keys { + placeholders[i] = "?" + args[i] = strings.ToLower(k) + } + query := "SELECT public_key, lat, lon, role FROM nodes WHERE LOWER(public_key) IN (" + strings.Join(placeholders, ",") + ")" + rows, err := db.conn.Query(query, args...) + if err != nil { + return result + } + defer rows.Close() + for rows.Next() { + var pk string + var role sql.NullString + var lat, lon sql.NullFloat64 + rows.Scan(&pk, &lat, &lon, &role) + result[strings.ToLower(pk)] = map[string]interface{}{ + "lat": nullFloat(lat), + "lon": nullFloat(lon), + "role": nullStr(role), + } + } + return result +} + // QueryMultiNodePackets returns transmissions referencing any of the given pubkeys. func (db *DB) QueryMultiNodePackets(pubkeys []string, limit, offset int, order, since, until string) (*PacketResult, error) { if len(pubkeys) == 0 { diff --git a/cmd/server/routes.go b/cmd/server/routes.go index 1f7f7723..199ef2b2 100644 --- a/cmd/server/routes.go +++ b/cmd/server/routes.go @@ -1560,8 +1560,12 @@ func (s *Server) handleObservers(w http.ResponseWriter, r *http.Request) { oneHourAgo := time.Now().Add(-1 * time.Hour).Unix() pktCounts := s.db.GetObserverPacketCounts(oneHourAgo) - // Batch lookup: node locations (observer ID may match a node public_key) - nodeLocations := s.db.GetNodeLocations() + // Batch lookup: node locations only for observer IDs (not all nodes) + observerIDs := make([]string, len(observers)) + for i, o := range observers { + observerIDs[i] = o.ID + } + nodeLocations := s.db.GetNodeLocationsByKeys(observerIDs) result := make([]ObserverResp, 0, len(observers)) for _, o := range observers {