Skip to content
Boym323Public

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

2,869 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

AirRadar

AirRadar is a personal dark-mode ADS-B radar for a local readsb receiver. It keeps live aircraft state in RAM, samples history to PostgreSQL, and streams snapshots to the browser over Server-Sent Events (SSE). The map remains useful when optional data sources are unavailable.

Documentation

The bilingual project wiki provides a practical entry point in English and Czech. The technical source of truth remains in the versioned documents below. A Czech localization is available in docs/cs/.

The compact agent entry point is AGENTS.md. Read it first and then only the linked document relevant to the task; do not assume the whole docs/ tree is required.

Codebase growth

The chart tracks non-empty physical source lines and separates production code from test code. Production excludes documentation, migrations, generated output, and public/vendor assets; test code includes tests/**, *.test.*, *.spec.*, __tests__, and test-runner configuration.

The timeline is reconstructed from the repository's first available commit using the last first-parent main commit of each day. After the initial backfill, the workflow keeps at most one point per day and replaces that day's point with the newest measured state, so the graph stays readable as the project grows.

AirRadar codebase growth

The history is stored in docs/metrics/code-history.json. After a successful main CI run, the unified Repository Metadata workflow refreshes both the changelog and codebase metrics on automation/repository-metadata, opens a single audit PR, and merges that generated metadata automatically. Metadata-only merges are excluded from production deployment. Run npm run metrics:code for the current snapshot or npm run metrics:code:backfill to rebuild the daily timeline from Git history.

Quick start — demo mode

Demo mode is automatic when READSB_BASE_URL is empty or absent.

cd /var/www/airradar
cp .env.example .env
npm install
npm run prisma:generate
npm run dev

Open http://localhost:3000. The mock provider generates moving sample traffic around the configured receiver position. PostgreSQL is optional in demo mode; history uses the in-memory trail when it is not configured.

Connect readsb

Set the base URL of the readsb/tar1090 web root. AirRadar appends the data paths; do not include /data/aircraft.json in the variable.

READSB_BASE_URL=http://192.168.1.50:8080
RECEIVER_LAT=50.0755
RECEIVER_LON=14.4378

The adapter reads /data/aircraft.json and optionally /data/receiver.json, preserves raw barometric/geometric fields, and derives altitude/vertical rate, distance, and bearing. Polling retries with bounded backoff; a receiver outage does not take down the UI or API.

ADSB.lol Extended Coverage

AirRadar can optionally add live network coverage from the authorized raw streams out.adsb.lol:1365 (BEAST) and out.adsb.lol:1366 (SBS/MLAT). The public https://api.adsb.lol geographic API is retained as a fallback; raw and HTTP snapshots are never summed.

The raw output is experimental, feeder-only, and available only to authorized feeder public IPs. AirRadar makes outbound connections only, never connects the stream to local readsb, and never forwards it to any feeder.

Enable it explicitly in the server environment:

ADSBLOL_ENABLED=true
ADSBLOL_RAW_ENABLED=true
ADSBLOL_BEAST_HOST=out.adsb.lol
ADSBLOL_BEAST_PORT=1365
ADSBLOL_MLAT_HOST=out.adsb.lol
ADSBLOL_MLAT_PORT=1366
ADSBLOL_NETWORK_RADIUS_NM=500
ADSBLOL_RAW_MAX_TRACKS=10000
ADSBLOL_HTTP_FALLBACK_ENABLED=true
ADSBLOL_BASE_URL=https://api.adsb.lol
ADSBLOL_RADIUS_NM=250
ADSBLOL_POLL_INTERVAL_MS=10000
ADSBLOL_REQUEST_TIMEOUT_MS=4000
ADSBLOL_STALE_AFTER_MS=30000
ADSBLOL_MAX_RETRY_INTERVAL_MS=60000
ADSBLOL_MAX_AIRCRAFT=3000
ADSBHUB_ENABLED=false
ADSBHUB_HOST=data.adsbhub.org
ADSBHUB_PORT=5002
ADSBHUB_RADIUS_NM=500
ADSBHUB_STALE_MS=15000
ADSBHUB_RECONNECT_MAX_MS=30000
ADSBHUB_MAX_TRACKS=20000

The default is conservative: one server-side poll every 10 seconds, bounded to 3,000 aircraft, with a 4-second timeout. The server uses the precise receiver coordinates for the upstream query, while the existing public receiver-coordinate privacy mode remains unchanged.

The radar defaults to LOCAL, showing only observations from the local receiver. EXTENDED combines local and network observations by normalized ICAO identity, shows a single aircraft marker, and labels the selected position source. Freshness is considered before source priority, so a fresh local observation wins when appropriate and a fresh ADSB.lol position can temporarily replace a stale local position. Network trails remain bounded and in memory.

In EXTENDED, merged aircraft are classified from provenance as LOCAL_ONLY, NETWORK_ONLY, or OVERLAP. Counters use LOCAL = LOCAL_ONLY + OVERLAP, NETWORK = NETWORK_ONLY + OVERLAP, and TOTAL = LOCAL_ONLY + NETWORK_ONLY + OVERLAP; the map source filter uses the same classification and is persisted in the browser.

Network observations are not local receiver evidence: they are never written to FlightPosition, never create local Flight history, never affect daily receiver statistics or reception records, and never use network RSSI/message counts as local measurements. Existing local enrichment, ATC matching, and default push-alert semantics remain local-only; network-only aircraft do not trigger an enrichment fan-out or push notification.

When enabled, network priority is ADSBHub TCP SBS/30003, ADSB.lol raw, then ADSB.lol HTTP. The ADSBHub feeder contribution is external to this repository: 192.168.1.50:30002 → data.adsbhub.org:5001; AirRadar consumes data.adsbhub.org:5002 and never manages the feeder service.

The public API is dynamically rate-limited. AirRadar sends no per-browser requests, never overlaps ADSB.lol requests, honors Retry-After on HTTP 429, and otherwise uses bounded backoff up to ADSBLOL_MAX_RETRY_INTERVAL_MS. Failed polls keep the last valid network snapshot until the stale timeout; local radar, history, and statistics continue independently. /system shows the network provider status, last attempt/success, latency, aircraft and MLAT counts, polling interval, radius, and rate-limit state.

Smoke test the public endpoint with operator-supplied coordinates (do not put the private receiver location in documentation):

curl -sS 'https://api.adsb.lol/v2/lat/LAT/lon/LON/dist/100' | jq '.total'

ADSB.lol publishes its API and public data under ODbL 1.0. Keep the in-app ADSB.lol attribution and comply with the current ODbL terms.

OGN / FLARM Integration v1

OGN is an optional, server-side APRS-IS live feed. It is disabled by default; when enabled, one process connects to aprs.glidernet.org:14580, sends a receive-only pass -1 login, and applies a radius filter around the existing canonical receiver coordinates. It sends only the documented #keepalive comment at the configured interval; it never sends aircraft telemetry. The browser uses a separate /api/ogn/state snapshot and /api/ogn/stream SSE channel; it never opens a TCP connection.

OGN targets stay in a bounded RAM-only state map. They do not enter the local ADS-B state service, FlightPosition, Flight history, daily statistics, reception records, alerts, enrichment, or receiver health. The OGN map layer is separate, off by default, and uses dedicated MapLibre DOM markers; the OGN list and detail panel are separate from ADS-B selection and filters.

Before publishing a target, AirRadar fail-closes on an unresolved or stale per-device OGN DDB resolution. Packet no-tracking and DDB tracked=N are dropped. A DDB miss, DDB identified=N, or packet stealth flag is anonymous: identity fields are removed at the server serialization boundary. Identified metadata is shown only when both the packet and DDB permit it. OGN timestamps are nearest-day UTC timestamps, packets older than 120 seconds are dropped, and identity is deduplicated by addressType + address, never by callsign.

APRS CSE/SPD uses CCC/SSS where course is degrees and speed is knots. AirRadar stores that value directly as groundSpeedKt; it does not apply a FANET source-specific conversion after OGN infrastructure has emitted APRS. The runtime does not require a full DDB download at startup. Each active OGN device is resolved through the official targeted JSON request ?j=1&t=1&device_id=<comma-separated-ids>, with bounded batching, debounce, minimum request spacing, one in-flight request, and global 429 backoff. A rich targeted failure may use the same batch with the official ?j=1 base representation; it never falls back to a full table download. The resolver keeps positive and short-lived negative resolutions in bounded RAM. An empty targeted devices array is a valid DDB miss, while HTTP/network/schema failures remain unresolved and fail closed. Exact device_type:device_id matching prevents a same-ID record under another device type from being used. The fallback is accepted only when every device record still contains the privacy-critical device_type, device_id, tracked, and identified fields; missing or invalid fields keep privacy fail-closed. aircraft_type is optional enrichment. A validated FOUND or MISSING resolution is also stored in the versioned local cache /var/lib/airradar/ogn-ddb-cache-v1.json (configurable with OGN_DDB_CACHE_FILE), using bounded debounced atomic writes and a final shutdown flush. The cache preserves the original resolvedAt; it never extends the positive 24-hour privacy stale limit or the negative 30-minute TTL, and corrupted/expired entries remain fail-closed. /system reports the targeted strategy, queue/cache counts, batch/request counters, HTTP status, backoff, aircraft-type availability, and cache persistence diagnostics.

The persistent official cache cannot refresh or bootstrap a new primary DDB snapshot while upstreams are unavailable. Without the separately enabled SoftRF whitelist, a new device therefore remains unresolved and hidden until a valid targeted DDB response has been received.

As an explicitly enabled emergency fallback, OGN_SOFTRF_DDB_PATH may point to the local SQLite ogn.db generated by the SoftRF project. The adjacent ogn.db.meta.json sidecar is required and must contain the trusted generatedAt, source: "SoftRF", sourceRunId, and lowercase SHA-256 sha256 fields. The hash binds the metadata to the exact SQLite file. AirRadar opens the database read-only, validates the devices schema and metadata age, and indexes only rows with track=1 and ident=1 into an in-memory whitelist. The fallback is below the current official DDB and its persistent cache, expires after OGN_SOFTRF_DDB_MAX_AGE_HOURS (168 hours by default), and is never used as a metadata replacement. Invalid, absent, or expired data remains fail-closed (UNKNOWN = HIDE). Set OGN_SOFTRF_DDB_ENABLED=false to disable it.

The snapshot is maintained by the separate airradar-ogn-softrf-update.service/timer, not by the Next.js runtime. The updater selects only a successful lyusupov/SoftRF aircraft-database workflow, uses the artifact's created_at as generatedAt, validates the archive and SQLite schema in a temporary directory, and performs a same-filesystem atomic replacement. See deploy/README.md for bootstrap, verification, and rollback commands. Set OGN_SOFTRF_UPDATE_ENABLED=false to disable the updater; OGN_SOFTRF_GITHUB_TOKEN is only needed when GitHub requires authentication for artifact downloads.

The implementation accepts the current v1 FLARM, OGN tracker, FANET, SafeSky, PilotAware, and ADS-L TOCALL variants that have a safe airborne interpretation; OGADSB, ground/weather/status, delayed, and unknown variants are counted and dropped. Official OGN source data and privacy choices are documented in docs/DATA-SOURCES.md. Configure the complete bounded set of OGN_* variables from .env.example; keep OGN_ENABLED=false in demo and release-gate environments.

PostgreSQL

The schema source is prisma/contract.prisma; checked-in forward migrations are under migrations/app/. Live state is not written for every ADS-B update: the service samples positions and stores Aircraft, Flight, and FlightPosition records. Daily receiver statistics, airport/ATC reference data, and the optional aircraft metadata catalog have separate tables. See docs/DATA-FLOWS.md and docs/RUNTIME-INVARIANTS.md for persistence semantics.

createdb airradar
# Set DATABASE_URL in .env
npm run prisma:generate
npm run prisma:deploy
npm run dev

Without DATABASE_URL, live radar remains available and /api/health reports the database as not_configured.

Optional integrations

All optional providers are server-side, bounded, and isolated from readsb polling. Configure them in the server-only .env; never use NEXT_PUBLIC_* for credentials.

Capability Configuration Default
ADSBDB metadata/routes ADSBDB_ENABLED=true Disabled
tar1090 aircraft catalog AIRCRAFT_METADATA_URL when using a tar1090 root Best effort, daily conditional sync
FlightAware flight plans FLIGHTAWARE_API_KEY Disabled; commercial/possibly billable
AviationWeather.gov METAR/TAF/SIGMET No key; server-side AWC integration Disabled; opt-in
Planespotters aircraft photos AIRCRAFT_PHOTOS_ENABLED=true Disabled
Server alerts/Pushover /var/lib/airradar/alerts.json in production, PUSHOVER_ENABLED=true plus server credentials Rules/no-op notifier until explicitly configured

Source, licensing, URL allowlists, cache behavior, and operational limits are in docs/DATA-SOURCES.md. Route airport metadata is resolved through the PostgreSQL catalog, then valid provider coordinates, then the small bundled fallback catalog.

Aviation Weather

The optional Aviation Weather integration is server-side only and uses the official Aviation Weather Center APIs. Enable it explicitly:

AVIATION_WEATHER_ENABLED=true
AVIATION_WEATHER_USER_AGENT="AirRadar/<version> (+https://example.invalid/contact)"

/api/weather/airport/:icao and the bounded batch form /api/weather/airport?icao=ICAO1,ICAO2 expose canonical-ICAO METAR/TAF data; /api/weather/sigmet exposes current worldwide international SIGMETs plus the CONUS domestic dataset. The radar SIGMET layer is off by default and loads only after the operator enables it. Flight-detail weather requests the destination first and then the origin in one batch request.

The provider uses HTTPS AWC endpoints, a custom User-Agent, bounded timeout, per-product TTLs (METAR 5 minutes, TAF 10 minutes, SIGMET 5 minutes), bounded RAM-only caches, negative caching, in-flight coalescing, stale-if-error, Retry-After backoff, and safe handling of 204 No Content. It never writes weather to PostgreSQL, changes aircraft SSE payloads, delays readsb, or feeds history/statistics. Public responses contain canonical normalized fields only; upstream errors and credentials are not serialized. The provider is subject to AWC's published request and result limits, so production UI fetches are on-demand and the SIGMET layer refreshes at a bounded cadence.

Operational counters and cache/provider state are visible on /system. See docs/DATA-SOURCES.md for provenance and the complete configuration list.

ATC and airport data

Sample ATC is automatic only in demo mode. With a real receiver, imported PostgreSQL data is used unless ATC_SAMPLE_ENABLED=true is explicitly set. ATC assignments are probable position/altitude/time matches and never claim the aircraft's actual tuned frequency.

npm run airports:import -- --dry-run
npm run airports:import
npm run airports:sync -- --dry-run
npm run airports:sync
# reproducible local input:
npm run airports:sync -- --dir ./data/ourairports --dry-run
npm run atc:import -- --dry-run data/atc/cz-atc.json
npm run atc:import -- data/atc/cz-atc.json
npm run atc:sync:cz -- --dry-run
npm run atc:sync:cz
npm run atc:status:cz

airports:sync downloads the current official OurAirports open-data files (airports.csv, runways.csv, airport-frequencies.csv, and navaids.csv) with a timeout and AirRadar User-Agent, validates all headers and rows before opening one database transaction, and supports --dry-run. Runways and frequencies are limited to the selected AirRadar airport catalog; navaids are kept worldwide. A failed or undersized feed cannot prune existing infrastructure. The existing airports:import remains a backward-compatible core-only importer and never deletes airport rows.

Airport infrastructure data: OurAirports, Public Domain. The data is community-sourced and has no guarantee of accuracy or fitness for use. AirRadar is an informational/reference display, not a certified navigation database; verify operational aviation data with official sources.

The import and Czech eAIP boundary rules are documented in data/atc/README.md and docs/DATA-SOURCES.md. AIM/eAIP, ČÚZK Data50, and BKG VG25 are sync-time inputs; the live service has no dependency on those hosts.

Alerts and watchlists

The /watchlist page and /api/watchlist manage shared server alert rules in /var/lib/airradar/alerts.json in production (data/alerts.json locally); updates are validated and atomically written. Rules support ICAO hex, registration, callsign, callsign pattern, aircraft type, airline, and optional maximum distance. Alert transitions use one server-wide cooldown and bounded asynchronous notification delivery.

The live map's browser watchlist is a separate localStorage filter. It is not a shared server rule and does not send notifications.

Useful commands

npm run dev
npm run lint
npm run typecheck
npm run test:targeted -- tests/aircraft-state.test.ts
npm run test:changed
npm test                         # complete Vitest suite
npm run build                    # local/isolated checkout only
npm run prisma:generate
npm run prisma:migrate
npm run prisma:deploy
npm run prisma:verify
npm run start

Use docs/DEVELOPMENT.md for targeted/changed/full testing and docs/RELEASE.md for production validation.

Public deployment and security

The configured production hostname is https://airradar.pomykal.cz. The public API is same-origin; no wildcard CORS policy is enabled. Exact receiver coordinates remain server-side by default. Set PUBLIC_RECEIVER_POSITION_MODE to approximate (default), hidden, or exact only when deliberately publishing the location. Public DTOs replace raw provider errors and never expose secrets.

Request/response APIs use bounded fixed-window limiting; /api/stream is excluded so SSE heartbeat/coalescing is not interrupted. The proxy must use HTTP/1.1, disable buffering/cache for SSE, and set a long read timeout. See deploy/README.md for systemd installation and the full Nginx Proxy Manager configuration.

Production deployment

The service runs as the unprivileged airradar user. The systemd unit starts scripts/start-production.mjs directly so systemd tracks the actual Node/Next process; the wrapper registers graceful shutdown ownership and waits for a complete build. Do not use npm run start as a replacement for the production unit without understanding that lifecycle contract.

Normal releases use deploy/release.sh, whose authoritative procedure is docs/RELEASE.md. It performs the full quality gates, migrations, restart, local/public health checks, and only then creates the automatic version tag. During the release it also generates CHANGELOG.md from commits since the previous release tag and commits the generated section. It never runs as part of ordinary development or documentation work.

Architecture summary

RTL-SDR / readsb → LocalReadsbProvider → AircraftStateService RAM
                 → SSE → Next.js / MapLibre UI
                 → sampled positions → PostgreSQL

For the complete current implementation, read docs/ARCHITECTURE.md and docs/RUNTIME-INVARIANTS.md.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages