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>
126 lines
3.7 KiB
Python
126 lines
3.7 KiB
Python
from datetime import datetime, timezone
|
|
from typing import List, Optional
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|
from pydantic import BaseModel
|
|
|
|
from app.internal import firestore as fstore
|
|
from app.internal.auth import require_node_service_or_firebase_token
|
|
from app.internal.logger import logger
|
|
|
|
router = APIRouter(prefix="/telemetry", tags=["telemetry"])
|
|
|
|
|
|
class AircraftReport(BaseModel):
|
|
icao: str
|
|
callsign: Optional[str] = None
|
|
lat: Optional[float] = None
|
|
lon: Optional[float] = None
|
|
altitude_ft: Optional[float] = None
|
|
ground_speed_kt: Optional[float] = None
|
|
track_deg: Optional[float] = None
|
|
|
|
|
|
class AdsbUploadBody(BaseModel):
|
|
aircraft: List[AircraftReport]
|
|
|
|
|
|
@router.post("/adsb")
|
|
async def upload_adsb(
|
|
body: AdsbUploadBody,
|
|
decoded: dict = Depends(require_node_service_or_firebase_token),
|
|
):
|
|
"""
|
|
Node-initiated: a second-SDR ADS-B decoder (node-26#9) periodically posts
|
|
its current aircraft snapshot here. One doc per icao, last-seen-wins —
|
|
this is a live-map overlay, not a flight history.
|
|
"""
|
|
node_id = decoded.get("node_id")
|
|
if not node_id:
|
|
raise HTTPException(400, "This endpoint requires node identity, not a service/admin token.")
|
|
|
|
node = await fstore.doc_get_cached("nodes", node_id)
|
|
org_id = node.get("org_id") if node else None
|
|
now = datetime.now(timezone.utc).isoformat()
|
|
|
|
writes = []
|
|
for ac in body.aircraft:
|
|
if not ac.icao:
|
|
continue
|
|
doc = {
|
|
"icao": ac.icao,
|
|
"node_id": node_id,
|
|
"callsign": ac.callsign,
|
|
"lat": ac.lat,
|
|
"lon": ac.lon,
|
|
"altitude_ft": ac.altitude_ft,
|
|
"ground_speed_kt": ac.ground_speed_kt,
|
|
"track_deg": ac.track_deg,
|
|
"last_seen": now,
|
|
}
|
|
if org_id:
|
|
doc["org_id"] = org_id
|
|
writes.append(("aircraft", ac.icao, doc))
|
|
|
|
for collection, doc_id, doc in writes:
|
|
try:
|
|
await fstore.doc_set(collection, doc_id, doc, merge=True)
|
|
except Exception as e:
|
|
logger.warning(f"Failed to upsert {collection}/{doc_id} from node {node_id}: {e}")
|
|
|
|
return {"ok": True, "count": len(writes)}
|
|
|
|
|
|
class VesselReport(BaseModel):
|
|
mmsi: str
|
|
name: Optional[str] = None
|
|
lat: Optional[float] = None
|
|
lon: Optional[float] = None
|
|
speed_kt: Optional[float] = None
|
|
heading_deg: Optional[float] = None
|
|
|
|
|
|
class AisUploadBody(BaseModel):
|
|
vessels: List[VesselReport]
|
|
|
|
|
|
@router.post("/ais")
|
|
async def upload_ais(
|
|
body: AisUploadBody,
|
|
decoded: dict = Depends(require_node_service_or_firebase_token),
|
|
):
|
|
"""Same shape as /telemetry/adsb, one doc per mmsi in `vessels`."""
|
|
node_id = decoded.get("node_id")
|
|
if not node_id:
|
|
raise HTTPException(400, "This endpoint requires node identity, not a service/admin token.")
|
|
|
|
node = await fstore.doc_get_cached("nodes", node_id)
|
|
org_id = node.get("org_id") if node else None
|
|
now = datetime.now(timezone.utc).isoformat()
|
|
|
|
writes = []
|
|
for v in body.vessels:
|
|
if not v.mmsi:
|
|
continue
|
|
doc = {
|
|
"mmsi": v.mmsi,
|
|
"node_id": node_id,
|
|
"name": v.name,
|
|
"lat": v.lat,
|
|
"lon": v.lon,
|
|
"speed_kt": v.speed_kt,
|
|
"heading_deg": v.heading_deg,
|
|
"last_seen": now,
|
|
}
|
|
if org_id:
|
|
doc["org_id"] = org_id
|
|
writes.append(("vessels", v.mmsi, doc))
|
|
|
|
for collection, doc_id, doc in writes:
|
|
try:
|
|
await fstore.doc_set(collection, doc_id, doc, merge=True)
|
|
except Exception as e:
|
|
logger.warning(f"Failed to upsert {collection}/{doc_id} from node {node_id}: {e}")
|
|
|
|
return {"ok": True, "count": len(writes)}
|