node-26#9. Adds POST /telemetry/adsb (node-key authed via require_node_service_or_firebase_token) that upserts one Firestore doc per icao into a new `aircraft` collection, org_id stamped from the reporting node the same way upload.py defensively stamps `calls`. firestore.rules gets a matching docInMyOrg()-gated read rule. Frontend: useAircraft() mirrors useNodes()'s onSnapshot pattern, filtering docs older than 2 minutes client-side since nothing prunes a stale aircraft doc server-side yet. MapView gets an opt-in "Aircraft" overlay (unchecked by default, like the weather radar layer) rendering a rotated plane glyph per sighting. Unverified via typecheck — no Node.js/npm on this authoring box yet (see CLAUDE.md Testing reality). Server side is pytest-covered (test_telemetry.py). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
53 lines
1.8 KiB
TypeScript
53 lines
1.8 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import { collection, onSnapshot, query, where, FirestoreError } from "firebase/firestore";
|
|
import { onAuthStateChanged } from "firebase/auth";
|
|
import { db, auth } from "@/lib/firebase";
|
|
import { useAuth } from "@/components/AuthProvider";
|
|
import type { AircraftTrack } from "@/lib/types";
|
|
|
|
// `aircraft` docs are a live snapshot (one per icao, overwritten on every
|
|
// sighting, node-26#9) — nothing prunes a doc when a plane leaves range, so
|
|
// staleness is filtered client-side rather than assuming the collection only
|
|
// ever holds current traffic.
|
|
const STALE_AFTER_MS = 2 * 60 * 1000;
|
|
|
|
export function useAircraft() {
|
|
const [aircraft, setAircraft] = useState<AircraftTrack[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const { orgId } = useAuth();
|
|
|
|
useEffect(() => {
|
|
let unsubFirestore: (() => void) | undefined;
|
|
|
|
const unsubAuth = onAuthStateChanged(auth, (user) => {
|
|
if (unsubFirestore) { unsubFirestore(); unsubFirestore = undefined; }
|
|
|
|
if (!user || !orgId) {
|
|
setAircraft([]);
|
|
setLoading(false);
|
|
return;
|
|
}
|
|
|
|
const q = query(collection(db, "aircraft"), where("org_id", "==", orgId));
|
|
unsubFirestore = onSnapshot(q, (snap) => {
|
|
const now = Date.now();
|
|
const fresh = snap.docs
|
|
.map((d) => d.data() as AircraftTrack)
|
|
.filter((a) => now - new Date(a.last_seen).getTime() < STALE_AFTER_MS);
|
|
setAircraft(fresh);
|
|
setLoading(false);
|
|
}, (err: FirestoreError) => { console.error("useAircraft:", err); setError(err.message); setLoading(false); });
|
|
});
|
|
|
|
return () => {
|
|
unsubAuth();
|
|
if (unsubFirestore) unsubFirestore();
|
|
};
|
|
}, [orgId]);
|
|
|
|
return { aircraft, loading, error };
|
|
}
|