Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion backend/core/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,13 @@

# ── Connected node state tracking ─────────────────────────────────────────────
connected_nodes: dict[str, dict] = {}
# node_id β†’ {config_hash, config, status, last_heartbeat, peer, is_synthetic, capabilities}
# node_id β†’ {config_hash: str, config: dict, status: str,
# last_heartbeat: ISO-8601 str, peer: str, is_synthetic: bool,
# capabilities: dict, node_ref: str}
# node_ref is present on mirrored nodes only; see services/node_refs._mirrored_ref.
# status is set locally to "active" on ingest and "disconnected" when the TCP
# peer drops, but a heartbeat overwrites it with whatever the node reported, so
# it is not a closed set β€” do not branch on it as if it were.

# Empirical FOV (see retina_analytics.empirical_coverage / manager /
# association). off/shadow/active, ASSOC_CLAIM_MODE precedent β€” an
Expand Down Expand Up @@ -396,6 +402,10 @@ def _adsb_for_seeding() -> dict[str, dict]:

# ── ADS-B positions reported inside detection frames ──────────────────────────
adsb_aircraft: dict[str, dict] = {}
# normalize_hex_key'd hex β†’ one record, built in services/tcp_handler and
# extended by adsb_derived_fields; see services/feed_helpers.adsb_store for the
# staleness rule that orders writes. last_seen_ms is the node's clock and
# recv_ms is ours, so the two are not interchangeable.

# ── Known-target claims registry (KNOWN_LANE_MODE) ────────────────────────────
# Written by services/known_claiming.py once per frame; the interface between
Expand Down
73 changes: 0 additions & 73 deletions backend/core/types.py

This file was deleted.

8 changes: 4 additions & 4 deletions backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,10 @@
from routes.test import router as test_router
from services import detection_mirror, publication
from services.alerting import log_destination
from services.background import (
from services.runtime_coverage import start as _start_coverage
from services.runtime_coverage import stop as _stop_coverage
from services.state_snapshot import SAVE_INTERVAL_S, restore_snapshot, save_snapshot
from services.tasks import (
adsb_truth_fetcher,
aircraft_flush_task,
analytics_refresh_task,
Expand All @@ -77,9 +80,6 @@
track_flush_task,
users_backup_task,
)
from services.runtime_coverage import start as _start_coverage
from services.runtime_coverage import stop as _stop_coverage
from services.state_snapshot import SAVE_INTERVAL_S, restore_snapshot, save_snapshot
from services.tasks.executor import task_executor, unfinished_task_executors
from services.tasks.solver import solver_workers_stopping
from services.tcp_handler import handle_tcp_client
Expand Down
59 changes: 0 additions & 59 deletions backend/pipeline/passive_radar.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@
Pipeline: detection data β†’ retina-tracker β†’ retina-geolocator β†’ tar1090 JSON
"""

import glob
import json
import logging
import math
import os
Expand Down Expand Up @@ -819,28 +817,6 @@ def process_frame(self, frame: dict):
self._last_prune_time = _now_mono
self._prune_stale_tracks()

def process_file(self, filepath: str) -> list:
"""Process an entire .detection file. Returns geolocated tracks."""
with open(filepath) as f:
content = f.read().strip()
if not content.startswith("["):
content = "[" + content + "]"
frames = json.loads(content)

# Feed all frames to tracker only (skip per-frame geolocation for batch)
for frame in frames:
ts = frame["timestamp"]
delays = frame.get("delay", [])
dopplers = frame.get("doppler", [])
snrs = frame.get("snr", [])
detections = [{"delay": d, "doppler": f, "snr": s} for d, f, s in zip(delays, dopplers, snrs)]
self.tracker.process_frame(detections, ts)

# Run geolocation once after all frames are processed
self._run_geolocation()

return list(self.geolocated_tracks.values())

def generate_aircraft_json(self) -> dict:
"""Generate tar1090-compatible aircraft.json from geolocated tracks."""
now = time.time()
Expand Down Expand Up @@ -903,38 +879,3 @@ def generate_receiver_json(self) -> dict:
"lat": lat,
"lon": lon,
}


def process_detection_folder(folder: str, output_dir: str, node_config: dict = None):
"""Process all .detection files in a folder and write tar1090 JSON to output_dir."""
pipeline = PassiveRadarPipeline(node_config)

detection_files = sorted(glob.glob(os.path.join(folder, "*.detection")))
if not detection_files:
logger.info("No .detection files found in %s", folder)
return

os.makedirs(output_dir, exist_ok=True)

receiver = pipeline.generate_receiver_json()
with open(os.path.join(output_dir, "receiver.json"), "w") as f:
json.dump(receiver, f)

for filepath in detection_files:
logger.info("Processing: %s", os.path.basename(filepath))
pipeline.process_file(filepath)

aircraft_data = pipeline.generate_aircraft_json()
with open(os.path.join(output_dir, "aircraft.json"), "w") as f:
json.dump(aircraft_data, f)

logger.info("Output: %d geolocated targets", len(aircraft_data["aircraft"]))
return aircraft_data


if __name__ == "__main__":
import sys

folder = sys.argv[1] if len(sys.argv) > 1 else "."
output = sys.argv[2] if len(sys.argv) > 2 else "./tar1090_data"
process_detection_folder(folder, output)
2 changes: 0 additions & 2 deletions backend/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,6 @@ omit = [
"routes/auth.py",
# Simulation/test-only endpoints β€” not production code paths:
"routes/test.py",
# Pure TypedDict definitions β€” no runtime code to cover:
"core/types.py",
]

[tool.coverage.report]
Expand Down
7 changes: 1 addition & 6 deletions backend/routes/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,6 @@
logger = logging.getLogger(__name__)


# Was a byte-identical copy of routes/test.py's; the rule now lives beside the
# interval table it reads.
_get_stale_tasks = get_stale_tasks


def _mn_pos_history_size() -> int:
"""Size of the solver's per-hex smoothing buffer (soak observability)."""
from services.tasks import solver as _solver
Expand Down Expand Up @@ -769,7 +764,7 @@ async def system_metrics(_user=Depends(require_admin)):
"ws_clients": len(state.ws_clients),
"ws_send_timeouts": state.ws_send_timeouts,
"ws_live_clients": len(state.ws_live_clients),
"stale_tasks": _get_stale_tasks(),
"stale_tasks": get_stale_tasks(),
"process_rss_mb": round(rss_mb, 1),
"load_avg": list(os.getloadavg()),
"disk_total_gb": round(disk.total / (1024**3), 1),
Expand Down
30 changes: 1 addition & 29 deletions backend/routes/radar.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,12 @@
import time
from datetime import datetime, timezone

from fastapi import APIRouter, Depends, Header, HTTPException, Request
from fastapi import APIRouter, Header, HTTPException, Request
from fastapi.responses import Response
from pydantic import BaseModel, Field

from config.constants import RATE_BUCKETS_MAX_IPS
from core import state
from core.users import require_admin
from pipeline.passive_radar import PassiveRadarPipeline
from routes.node_schemas import NodeRef
from services import node_refs, node_registration
Expand Down Expand Up @@ -51,10 +50,6 @@ class BulkDetectionRequest(BaseModel):
nodes: list[BulkNodeEntry] = Field(..., max_length=500)


class LoadFileRequest(BaseModel):
path: str = Field(..., min_length=1)


RADAR_API_KEY = os.getenv("RADAR_API_KEY", "")
_RETINA_ENV = os.getenv("RETINA_ENV", "").lower()
if not RADAR_API_KEY:
Expand All @@ -65,9 +60,6 @@ class LoadFileRequest(BaseModel):
logging.warning(_msg)
_RATE_LIMIT = int(os.getenv("RADAR_RATE_LIMIT", "60"))
_RATE_WINDOW = int(os.getenv("RADAR_RATE_WINDOW", "60"))
_ALLOWED_DETECTION_DIR = os.path.realpath(
os.path.join(os.path.dirname(os.path.dirname(__file__)), "coverage_data", "archive")
)

# Module-level reference to default pipeline; set from main.py at startup
_default_pipeline: PassiveRadarPipeline | None = None
Expand Down Expand Up @@ -272,26 +264,6 @@ async def ingest_detections_bulk(
return {"status": "ok", "nodes_registered": registered, "frames_queued": queued}


@router.post("/api/radar/load-file")
async def load_detection_file(body: LoadFileRequest, _admin=Depends(require_admin)):
filepath = os.path.realpath(body.path)
if not filepath.startswith(_ALLOWED_DETECTION_DIR + os.sep):
raise HTTPException(status_code=400, detail="Path must be inside the coverage archive directory")
if not os.path.isfile(filepath):
raise HTTPException(status_code=400, detail="File not found")
if not filepath.endswith(".detection"):
raise HTTPException(status_code=400, detail="Only .detection files accepted")

tracks = _default_pipeline.process_file(filepath)
aircraft_data = _default_pipeline.generate_aircraft_json()
# Do NOT write aircraft.json here. The flush task is the file's single
# writer (a different schema was being raced in from this request
# thread); marking dirty makes the next 1 Hz flush pick the load up.
state.aircraft_dirty = True

return {"status": "ok", "tracks": len(tracks), "aircraft": aircraft_data["aircraft"]}


@router.get("/api/radar/status")
async def radar_status():
# Unauthenticated, and the config block quotes the default pipeline's
Expand Down
22 changes: 1 addition & 21 deletions backend/routes/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,6 @@ def _verify_sim_key(x_api_key: str = Header(default="", alias="X-API-Key")):
raise HTTPException(status_code=401, detail="Invalid or missing X-API-Key")


# Was a byte-identical copy of routes/admin.py's; the rule now lives beside the
# interval table it reads.
_get_stale_tasks = get_stale_tasks


# Module-level reference set from main.py at startup
_default_pipeline = None

Expand Down Expand Up @@ -300,7 +295,7 @@ def _build_dashboard_data() -> bytes:
"task_health": {
"last_success": dict(state.task_last_success),
"error_counts": state.task_error_snapshot(),
"stale_tasks": _get_stale_tasks(),
"stale_tasks": get_stale_tasks(),
},
}
)
Expand Down Expand Up @@ -471,21 +466,6 @@ async def get_ground_truth_trail(hex_code: str):
}


@router.get("/api/test/anomalies")
async def get_anomaly_log():
"""Return the anomaly event log and currently flagged hex codes."""
return Response(
content=orjson.dumps(
{
"flagged_count": len(state.anomaly_hexes),
"flagged_hexes": sorted(state.anomaly_hexes),
"events": state.anomaly_log[-100:],
}
),
media_type="application/json",
)


# ── Known-track hold (path H) ─────────────────────────────────────────────────


Expand Down
39 changes: 0 additions & 39 deletions backend/scripts/smoke_parquet_archive.py

This file was deleted.

Loading
Loading