node-26#9, same shape as the ADS-B commit. Adds POST /telemetry/ais (same node-key auth, same org_id-stamped upsert-by-key pattern, this time by mmsi into a new `vessels` collection) and its docInMyOrg() firestore rule. Frontend gets useVessels() (mirrors useAircraft(), longer staleness window since AIS position reports are minutes apart, not seconds) and an opt-in "Vessels" map overlay. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
54 lines
1.9 KiB
TypeScript
54 lines
1.9 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 { VesselTrack } from "@/lib/types";
|
|
|
|
// Same shape as useAircraft — `vessels` is a live snapshot (one per mmsi,
|
|
// overwritten on every sighting, node-26#9), nothing prunes a doc when a
|
|
// vessel goes out of range, so staleness is filtered client-side. AIS
|
|
// position reports are much less frequent than ADS-B (minutes, not
|
|
// seconds), so this window is longer than useAircraft's.
|
|
const STALE_AFTER_MS = 10 * 60 * 1000;
|
|
|
|
export function useVessels() {
|
|
const [vessels, setVessels] = useState<VesselTrack[]>([]);
|
|
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) {
|
|
setVessels([]);
|
|
setLoading(false);
|
|
return;
|
|
}
|
|
|
|
const q = query(collection(db, "vessels"), where("org_id", "==", orgId));
|
|
unsubFirestore = onSnapshot(q, (snap) => {
|
|
const now = Date.now();
|
|
const fresh = snap.docs
|
|
.map((d) => d.data() as VesselTrack)
|
|
.filter((v) => now - new Date(v.last_seen).getTime() < STALE_AFTER_MS);
|
|
setVessels(fresh);
|
|
setLoading(false);
|
|
}, (err: FirestoreError) => { console.error("useVessels:", err); setError(err.message); setLoading(false); });
|
|
});
|
|
|
|
return () => {
|
|
unsubAuth();
|
|
if (unsubFirestore) unsubFirestore();
|
|
};
|
|
}, [orgId]);
|
|
|
|
return { vessels, loading, error };
|
|
}
|