()
| 30 | }; |
| 31 | |
| 32 | export function Control() { |
| 33 | const { state, conn } = useStream("control"); |
| 34 | const cfg = state.config; |
| 35 | |
| 36 | // Location editor (Nominatim via the server's /api/geocode). |
| 37 | const [geoBusy, setGeoBusy] = useState(false); |
| 38 | const [geoErr, setGeoErr] = useState<string | null>(null); |
| 39 | |
| 40 | // Airport runway import (OurAirports via the server's /api/airport). |
| 41 | const [apBusy, setApBusy] = useState(false); |
| 42 | const [apErr, setApErr] = useState<string | null>(null); |
| 43 | |
| 44 | // ISS pass finder (for the Sky section). |
| 45 | const [tles, setTles] = useState<Tle[]>([]); |
| 46 | useEffect(() => { |
| 47 | let on = true; |
| 48 | fetch("/api/tle") |
| 49 | .then((r) => (r.ok ? r.json() : [])) |
| 50 | .then((t) => on && setTles(t as Tle[])) |
| 51 | .catch(() => {}); |
| 52 | return () => { |
| 53 | on = false; |
| 54 | }; |
| 55 | }, []); |
| 56 | const nextPass = useMemo( |
| 57 | () => (tles.length && cfg ? nextISSPass(Date.now(), cfg.centerLat, cfg.centerLon, tles) : null), |
| 58 | [tles, cfg?.centerLat, cfg?.centerLon], |
| 59 | ); |
| 60 | |
| 61 | if (!cfg) { |
| 62 | return ( |
| 63 | <div className="loading"> |
| 64 | <div className={`dot ${state.connected ? "ok" : "bad"}`} /> |
| 65 | {state.connected ? "Loading config…" : "Connecting to tracker…"} |
| 66 | </div> |
| 67 | ); |
| 68 | } |
| 69 | |
| 70 | const set = (patch: Partial<Config>) => conn.patchConfig(patch); |
| 71 | const setField = (k: keyof ShowFields, v: boolean) => |
| 72 | conn.patchConfig({ showFields: { ...cfg.showFields, [k]: v } }); |
| 73 | const statusMessage = state.status?.message ? ` · ${state.status.message}` : ""; |
| 74 | |
| 75 | const changeLocation = async (q: string) => { |
| 76 | if (!q.trim()) return; |
| 77 | setGeoBusy(true); |
| 78 | setGeoErr(null); |
| 79 | try { |
| 80 | const r = await fetch(`/api/geocode?q=${encodeURIComponent(q)}`); |
| 81 | if (!r.ok) { |
| 82 | setGeoErr(r.status === 404 ? `No match for “${q}”` : "Lookup failed"); |
| 83 | return; |
| 84 | } |
| 85 | const hit = (await r.json()) as { lat: number; lon: number; name: string }; |
| 86 | set({ centerLat: hit.lat, centerLon: hit.lon, locationName: hit.name }); |
| 87 | } catch { |
| 88 | setGeoErr("Lookup failed"); |
| 89 | } finally { |
nothing calls this directly
no test coverage detected