From add53ba7e75dac40279344dbd46e440fdb21e462 Mon Sep 17 00:00:00 2001 From: MrAlders0n Date: Tue, 9 Jun 2026 22:30:05 -0400 Subject: [PATCH] feat(stats): observer-list search queries all observers server-side Search the /observers name endpoint (region-scoped) instead of filtering the loaded top-15, so any observer is findable. Top observers remain the default list; search results show name + IATA + status. --- src/features/stats/ObserverTab.tsx | 73 ++++++++++++++++++++---------- 1 file changed, 48 insertions(+), 25 deletions(-) diff --git a/src/features/stats/ObserverTab.tsx b/src/features/stats/ObserverTab.tsx index 79de71c..023b4ff 100644 --- a/src/features/stats/ObserverTab.tsx +++ b/src/features/stats/ObserverTab.tsx @@ -1,7 +1,10 @@ import { useEffect, useMemo, useState } from "react"; +import { useQuery, keepPreviousData } from "@tanstack/react-query"; import { Badge } from "../../components/Badge"; import { EmptyState } from "../../components/EmptyState"; import { formatBattery, formatCount, formatUptime } from "../../lib/formatters"; +import { getObserversPage } from "../../api/client"; +import { useRegion } from "../../hooks/useRegion"; import { useChartColors } from "./chartTheme"; import { useTopObservers } from "./useStats"; import { useObserver, useObserverTelemetry } from "./useTelemetry"; @@ -22,15 +25,30 @@ function ObserverList({ selectedId: string | null; onSelect: (id: string) => void; }) { - const { data, isLoading } = useTopObservers(range, 15); - const max = useMemo(() => Math.max(1, ...(data ?? []).map((o) => o.observationCount)), [data]); + const { iatas, regionKey } = useRegion(); const [query, setQuery] = useState(""); + const q = query.trim(); + const searching = q.length > 0; - const nameOf = (o: { displayName: string | null; observerId: string }) => o.displayName ?? o.observerId.slice(0, 8); - const filtered = useMemo(() => { - const q = query.trim().toLowerCase(); - return q ? (data ?? []).filter((o) => nameOf(o).toLowerCase().includes(q)) : (data ?? []); - }, [data, query]); + // default: top observers by activity (with count + activity bar). Searching swaps to a server-side + // name lookup across ALL observers in the region, not just the loaded top rows. + const top = useTopObservers(range, 15); + const max = useMemo(() => Math.max(1, ...(top.data ?? []).map((o) => o.observationCount)), [top.data]); + + const search = useQuery({ + queryKey: ["observer-search", regionKey, q], + queryFn: () => getObserversPage(iatas, { name: q, limit: 50 }), + enabled: searching, + staleTime: 30_000, + placeholderData: keepPreviousData, + }); + + type Row = { id: string; name: string; count?: number; iata?: string; online?: boolean }; + const rows: Row[] = searching + ? (search.data?.items ?? []).map((o) => ({ id: o.id, name: o.displayName ?? o.id.slice(0, 8), iata: o.iata, online: o.status === "online" })) + : (top.data ?? []).map((o) => ({ id: o.observerId, name: o.displayName ?? o.observerId.slice(0, 8), count: o.observationCount })); + + const loading = searching ? search.isLoading : top.isLoading; return ( @@ -42,33 +60,38 @@ function ObserverList({ className="mb-2 w-full rounded border border-border bg-bg-base px-2 py-1 font-mono text-[12px] text-text-normal placeholder:text-text-dim" />
- {isLoading &&
Loading…
} - {!isLoading && (data ?? []).length === 0 && ( -
No observers
+ {loading &&
{searching ? "Searching…" : "Loading…"}
} + {!loading && rows.length === 0 && ( +
{searching ? "No matches" : "No observers"}
)} - {!isLoading && (data ?? []).length > 0 && filtered.length === 0 && ( -
No matches
- )} - {filtered.map((o) => { - const active = o.observerId === selectedId; - const name = o.displayName ?? o.observerId.slice(0, 8); + {rows.map((r) => { + const active = r.id === selectedId; return ( );