osint-dashboard/docs/free-data-streams.md
Sirius DevOps 2ce0e9ba4e
Some checks failed
build-and-deploy / build (push) Failing after 39s
ci: retrigger deploy (previous run raced the force-push)
2026-08-27 17:42:03 -04:00

613 lines
28 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Free data streams for the OSINT dashboard
Builder brief for backend + frontend. Researched 2026-08-27. Every endpoint below was either live-probed from this machine or taken from the providers current docs. Prefer **free, no-key, CORS-open** sources first. Keys are called out explicitly.
This is **not** a camera-discovery / masscan change. Existing camera rules still apply: never emit `rtsp://` hrefs; masscan pins go through `/api/cameras/{id}/snapshot`; HTTP directory cams use `/stream` MJPEG.
---
## 0. What is already in the repo (do not rebuild)
| Layer | Status | Notes |
|---|---|---|
| NASA FIRMS VIIRS hotspots | Ingested (`app/fire_sources.py` → NATS `events.fire``fires` hypertable → `GET /api/fires`) | Needs free `FIRMS_MAP_KEY`. See `docs/firms.md`. |
| NASA GIBS basemaps | Frontend tiles via `app/gibs_map.py` | No key. CORS `*`. |
| GIBS VIIRS thermal tiles | Documented, not wired as overlay | Same GIBS stack; no key. |
| Cameras | Scraper + masscan → `cameras` table | Defaults already include ALERTWest JPEGs + Live-Environment-Streams HLS/YouTube GeoJSON. |
| News / RSS / GDELT / USGS quakes | Ingest | Out of scope for this brief. |
**Action for existing fire ingest:** NASA will stop Suomi NPP product delivery on **2026-11-01**. Switch `FIRMS_DATASET` from `VIIRS_SNPP_NRT` to `VIIRS_NOAA20_NRT` and/or `VIIRS_NOAA21_NRT` before then.[20]
---
## 1. Architecture rules (backend + frontend)
Moving objects (aircraft, vessels, trains) and alerts are **vectors**. Radar / satellite / GIBS fire tiles are **rasters**.
| Kind | Where it runs | Persist? | API shape |
|---|---|---|---|
| Radar, GOES, GIBS, OpenRailwayMap tiles | **Browser** `L.tileLayer` (CORS `*` on the recommended ones) | No | Tile URL template |
| Aircraft / trains / last-known ships | **Backend poll** → last-known row → `GET /api/{layer}?bbox=` | Last-known only (Timescale, 1-day chunks, drop after 2472 h) | JSON array of points |
| AIS live stream | **Backend WebSocket** (aisstream forbids browsers)[5] | Last-known + optional 15-min trail | Same `/api/vessels` |
| NWS / SPC polygons | Backend poll **or** browser (NWS CORS `*`)[17] | Optional 24 h | GeoJSON FeatureCollection |
| Official JPEG/HLS cameras | Existing `cameras` scraper | Yes, same table | Existing `/stream` + HLS |
**Do not** hit third-party APIs from the Leaflet client when: (a) an API key would leak, (b) CORS is closed, (c) the provider rate-limits by IP and many map users would share one Pi egress. Proxy those through FastAPI.
**Pi / home-uplink constraints:** viewport bbox only. Never poll global ADS-B or global AIS. Cap aircraft poll to 510 s, trains 1530 s, FIRMS already 900 s. Do not store 1 Hz AIS history.
Suggested NATS subjects (match `events.fire`): `events.aircraft`, `events.vessel`, `events.train`, `events.alert`.
Suggested last-known tables (natural keys):
```
aircraft(icao24 PK, callsign, lat, lon, alt_baro_m, gs_kt, track_deg, squawk, src, seen_at)
vessels(mmsi PK, name, lat, lon, sog_kt, cog_deg, navstat, dest, src, seen_at)
trains(train_id PK, route, lat, lon, heading, late_min, src, seen_at)
```
---
## 2. Build order (what to ship first)
### Phase A — frontend-only, no new tables (12 days)
1. RainViewer radar overlay (global, no key, CORS `*`).[13][14]
2. IEM NEXRAD CONUS overlay as US-quality alternative (no key, CORS `*`).[15]
3. GIBS `VIIRS_SNPP_Thermal_Anomalies_375m_All` fire-pixel overlay (already documented in `docs/firms.md`).
4. NWS active alerts GeoJSON overlay (no key, User-Agent required, CORS `*`).[17]
5. IEM current storm-based warnings GeoJSON.[16]
### Phase B — backend last-known overlays
6. ADSB.lol viewport aircraft (no key today).[3]
7. OpenSky bbox as fallback / research-grade (anonymous works; OAuth recommended).[1]
8. Amtraker trains (no key; **must** send identifying `User-Agent`).[9]
9. WFIGS incident points + current perimeters (no key).[21]
10. Caltrans CWWP2 cameras into existing `cameras` scraper (JPEG + HLS).[27]
### Phase C — free keys, streams
11. AISStream WebSocket (free GitHub-login key; **server-side only**).[5]
12. Optional OpenSky OAuth client for 4,000 credits/day vs 400 anonymous.[1]
13. Optional Transitland key later for multi-agency GTFS-RT (10k REST/month free).[12]
Skip paid: MarineTraffic, ADS-B Exchange, FlightAware, TrafficLand, OpenWeatherMap One Call paid tiers.
---
## 3. Air traffic
### 3.1 ADSB.lol — **recommended primary for the map**
| | |
|---|---|
| Cost / key | Free, no key today. Docs say a feeder key *may* be required in the future; contact them before treating this as a production SLA.[3] |
| License | ODbL 1.0 (same family as OSM; share-alike on the database).[4] |
| CORS | Treat as **backend-only**. Do not assume browser CORS. |
| Live probe 2026-08-27 | `GET https://api.adsb.lol/v2/lat/35.88/lon/-78.79/dist/50` → HTTP 200, 27 aircraft near RDU. |
Useful endpoints:[3]
```
GET /v2/lat/{lat}/lon/{lon}/dist/{radius_nm} # radius cap 250 nm
GET /v2/point/{lat}/{lon}/{radius}
GET /v2/sqk/{squawk} # e.g. 7700/7600/7500
GET /v2/mil
GET /v2/hex/{icao}
GET /v2/callsign/{callsign}
```
Sample fields from live payload: `hex, flight, r, t, lat, lon, alt_baro, gs, track, squawk, emergency, category, seen_pos`.
**Backend:** poll the **map viewport** (center + radius from bbox, clamp ≤ 150 nm) every 510 s. Dedup on `hex`. Do not poll `/v2/mil` on a timer unless the user toggles a mil filter.
**Frontend:** circleMarker or rotating icon, tooltip = `flight`/`hex`/`alt`. Cluster above ~200 markers. Color by altitude or `emergency != none`.
### 3.2 OpenSky Network — research-grade fallback
| | |
|---|---|
| Cost / key | Free for non-commercial / research. Anonymous OK. Authenticated uses OAuth2 **client credentials** (not basic auth). Create `client_id` + `client_secret` on the account page.[1][26] |
| Terms | Cite the OpenSky paper + URL for publications. Commercial needs a license. They may block AWS/hyperscaler IPs.[2] |
| Live probe | `GET /api/states/all?lamin=33.8&lomin=-84.5&lamax=36.6&lomax=-75.4` → HTTP 200, **270** state vectors over NC in 0.63 s. |
Root: `https://opensky-network.org/api`[1]
```
GET /states/all?lamin=&lomin=&lamax=&lomax=&extended=1
```
State vector indexes: `0 icao24, 1 callsign, 2 origin_country, 5 lon, 6 lat, 7 baro_alt m, 8 on_ground, 9 velocity m/s, 10 true_track, 14 squawk`.[1]
Credits (independent buckets for `/states/*`, `/tracks/*`, `/flights/*`):[1]
| Tier | Credits / refill |
|---|---|
| Anonymous | 400 / day |
| Standard user (OAuth) | 4,000 / day |
| Active feeder ≥30% uptime | 8,000 / day |
`/states/all` costs **14 credits** by bbox area (≤25 sq° = 1 credit; global = 4). Exhaustion → HTTP 429 + `X-Rate-Limit-Retry-After-Seconds`.[1]
Anonymous: live only, 10 s time resolution. Authenticated: up to 1 hour of history, 5 s resolution.[1]
Token:
```
POST https://auth.opensky-network.org/auth/realms/opensky-network/protocol/openid-connect/token
grant_type=client_credentials&client_id=&client_secret=
Authorization: Bearer <access_token> # ~30 min TTL
```
**Backend:** use OpenSky as fallback when ADSB.lol 429s, or for a “research” toggle. Always send a bbox. NC-sized box is ~25 sq° → 1 credit; 10 s poll as anonymous = 8,640 credits/day → **will 429**. Authenticated 4,000/day ≈ one poll every ~22 s if cost=1. Prefer ADSB.lol for the live map; keep OpenSky for sparse/historical.
**Do not** run this from AWS/the Pi if OpenSky starts blocking the IP; the docs warn about hyperscalers.[2]
### 3.3 AviationWeather.gov METARs (context, not tracks)
No key. User-Agent required. **100 req/min**. **No CORS** — backend only.[22]
```
GET https://aviationweather.gov/api/data/metar?ids=KRDU&format=json
```
Live probe: KRDU thunderstorm METAR returned JSON with `lat/lon/temp/rawOb`. Use as airport popup, not as an aircraft layer. Prefer the gzip cache files for bulk.
### 3.4 Skip
- ADS-B Exchange — paid gateway / API key.
- FlightAware / AeroAPI — paid.
- OpenSky Trino historical — overkill for the live map.
---
## 4. Boat / AIS traffic
### 4.1 AISStream — **recommended live ships**
| | |
|---|---|
| Cost / key | Free. Sign in with GitHub, create key at `/account`. Shown once; rotate from Account.[5][6] |
| Transport | `wss://stream.aisstream.io/v0/stream` |
| Browser | **Forbidden.** Connect from the FastAPI/ingest container and proxy a downsampled last-known feed.[5] |
| Limits | 3 connections / account, 3 / IP. Subscribe JSON **within 3 seconds** or the socket closes. Subscription replace ≤ 1/s. No SLA, no replay.[5] |
| Compression | Negotiate `permessage-deflate`. Uncompressed connections get bandwidth caps starting **September 2026**.[5] |
Subscribe (bbox is **\[\[lat, lon\], \[lat, lon\]\]** — not lon/lat):[5]
```json
{
"APIKey": "<AISSTREAM_API_KEY>",
"BoundingBoxes": [[[33.8, -84.5], [36.6, -75.4]]],
"FilterMessageTypes": [
"PositionReport",
"StandardClassBPositionReport",
"ExtendedClassBPositionReport",
"ShipStaticData"
]
}
```
Frames are **binary WebSocket** containing UTF-8 JSON. Decode bytes, then `json.loads`.[5]
Position envelope (docs + site examples): `MessageType`, `MetaData.{MMSI,ShipName,Latitude,Longitude}`, `Message.PositionReport.{Sog,Cog,TrueHeading,NavigationalStatus}`.[5][6]
**Backend:** one long-lived WS in the `ingest` profile. Filter to the dashboards configured bbox (default CONUS coasts + Great Lakes, not world). Upsert last-known by MMSI. Expose `GET /api/vessels?bbox=`. Reconnect with jittered backoff; resend full subscription in <3 s. Do not fan the raw stream to browsers.
**Frontend:** circleMarker, color by nav status / SOG, tooltip name+MMSI. Optional 5-minute trail from in-memory ring, not DB.
### 4.2 AISHub — only if you feed a receiver
Free **if you contribute a live AIS UDP feed**. Then JSON/XML/CSV at most **once per minute**.[7][8]
```
https://data.aishub.net/ws.php?username=USER&format=1&output=json&latmin=&latmax=&lonmin=&lonmax=
```
`format=1` = human-readable degrees / knots. Faster than once/minute returns empty.[8]
Not useful on the Pi unless Lance later stands up an AIS dongle. Keep as a footnote.
### 4.3 Skip / later
- MarineTraffic REST commercial.
- Global Fishing Watch free **non-commercial** token, fishing-effort tiles / vessel identity, not a general AIS live layer.[24] Optional Phase D for a fishing effort overlay.
- AISHub without a feeder no data.
---
## 5. Train traffic
There is **no free global live train API**. US intercity is solvable; European freight is not, cheaply.
### 5.1 Amtraker — **recommended US passenger trains**
Community API that rehosts Amtrak + Brightline + VIA Rail. Free. **Identifying `User-Agent` required or the request is blocked.**[9] Data license ODC-By 1.0; attribute Amtraker on the map (collapsed OSM-style credit is OK).[9][10]
Live probe: `GET https://api.amtraker.com/v3/trains` and `https://api-v3.amtraker.com/v3/trains` both HTTP 200, **~1.3 MB**, **197 train numbers**. Sample: Sunset Limited `#1`, `lat/lon`, `heading`, `stations[]`, `iconColor`.
```
GET https://api.amtraker.com/v3/trains
GET https://api.amtraker.com/v3/trains/:trainId # e.g. 5-9
GET https://api.amtraker.com/v3/stations
GET https://api.amtraker.com/v3/stale # stale=true if Amtrak >15 min old
```
Poll **1530 s**. Response is `{ "1": [Train, ...], "5": [...], ... }` flatten to one row per `trainID`. Do not hammer; the author asks not to spam.[10]
**Frontend:** train icon, color from `iconColor` (or compute lateness). Popup: route, number, next station, delay.
### 5.2 Transitland — multi-agency GTFS-RT (optional, key)
Free plan: **10,000 REST queries/month**, 100k vector tiles/month, 1,000 routing/month. Key via Interline signup (`apikey=` query or header; **not** `Authorization`). Attribution required.[11][12]
```
https://transit.land/api/v2/rest/
```
Use later if you want commuter rail / subway vehicle positions (LA Metro, MTA, etc.). 10k/month is only ~13 queries/hour **cannot** poll every agency every 15 s. Cache GTFS-RT protobufs yourself from the **agencys own feed URL** (those are usually free, no Transitland meter). Transitland is a directory; the live positions should come from the agency GTFS-RT URL listed in the Atlas.
### 5.3 OpenRailwayMap — infrastructure tiles only
`https://tiles.openrailwaymap.org/{style}/{z}/{x}/{y}.png` OSM-derived tracks, not live trains.[25] This probes HEAD got **HTTP 403** (UA/hotlink?). Treat as optional overlay; do not depend on it. No vehicle data.
### 5.4 Skip
- Official Amtrak Track-A-Train JSON unofficial reverse-engineering; Amtraker already does this legally enough for a hobby map, with a documented UA policy.
- Deutsche Bahn / Network Rail free-ish developer portals but EU-centric and key + ToS heavy. Out of scope unless the map goes global-rail.
---
## 6. Open video / camera feeds (official public only)
Do **not** add Insecam-style random IP cams as a new source. The scraper already has a public list + masscan; this section is **agency-published** JPEG/HLS.
### 6.1 Already wired
- ALERTWest / ALERTCalifornia: `https://api.cdn.prod.alertwest.com/api/getCameraDataByLoc` live probe HTTP 200, **6.6 MB** JSON with `data.locs` + `data.cams`. Snapshots at `https://img.cdn.prod.alertwest.com/data/img/{cid}/...` (see `parse_alertwest_json`).
- Live-Environment-Streams GeoJSON (HLS / YouTube / JPEG).
- `public-ip-cams` README list.
### 6.2 Caltrans CWWP2 — **add to CAMERA_SOURCE_URLS**
Official CA DOT CCTV. No key.
```
https://cwwp2.dot.ca.gov/data/d{N}/cctv/cctvStatusD{NN}.json
```
Live probe D4: HTTP 200, **2.4 MB**, **749 cameras**.
Each `data[].cctv`:
- `location.{latitude,longitude,locationName,nearbyPlace,district,route,county,direction}`
- `inService` (`"true"`/`"false"`)
- `imageData.static.currentImageURL` JPEG, updates ~5 s (`https://cwwp2.dot.ca.gov/data/d4/cctv/image/...jpg`)
- `imageData.streamingVideoURL` **HLS** `https://wzmedia.dot.ca.gov/D4/....stream/playlist.m3u8`
Districts 112 exist with the same schema (D4 verified). Backend: new parser next to `parse_alertwest_json`, skip `inService != true`, store JPEG as `snapshot_url`, HLS as stream URL (dashboard already vendors `hls.min.js`). Frontend: same camera popup; prefer JPEG for map thumbs, HLS in the preview pane. Never RTSP.
Politeness: scrape hourly like other sources (`CAMERA_SCRAPE_INTERVAL=3600`). Do not scrape every JPEG every cycle store URLs, let the snapshot cache fetch on demand.
### 6.3 IEM webcams — small, Iowa-centric bonus
```
GET https://mesonet.agron.iastate.edu/geojson/webcam.geojson
```
No key, CORS permissive.[23] Live probe: HTTP 200, **3** cameras at probe time (not a national network). Fields: `cid, name, imgurl, utc_valid, state`. Nice extra source, low value vs ALERTWest/Caltrans.
### 6.4 YouTube live (already in Live-Environment-Streams)
Do not call the YouTube Data API unless you want search. Embedding existing stream URLs from the GeoJSON is enough. YouTube Data API **requires a Google key** and quota.
### 6.5 Skip
- Insecam / random public IP cam aggregators ToS / privacy / already covered by masscan ethics.
- TrafficLand, EarthCam commercial APIs.
- SkylineWebcams scraping, not an API.
---
## 7. Fire maps (beyond current FIRMS points)
FIRMS CSV = hotspot **points** (already ingested). WFIGS = **incident identity + perimeters**. GIBS = **pixel overlay**. Use all three; they answer different questions.
### 7.1 WFIGS current incident locations — **recommended vector layer**
No key. ArcGIS FeatureServer, GeoJSON.
```
https://services3.arcgis.com/T4QMspbfLg3qTGWY/arcgis/rest/services/WFIGS_Incident_Locations_Current/FeatureServer/0/query
?where=1=1&outFields=IncidentName,IncidentSize,FireDiscoveryDateTime,POOState,PercentContained,IncidentTypeCategory,FireCause
&outSR=4326&f=geojson
```
Live probe: **635** current locations. Sample: `Foster Bridge`, 675 acres, `US-FL`, 100% contained, cause Human. `FireDiscoveryDateTime` is **epoch ms**.
Poll 515 min (NIFC says related perimeter service refreshes ~5 min).[21] Backend can proxy `GET /api/fire-incidents` so the keyless URL is cached. Frontend: labeled point, popup acres / contained %.
Some similarly named services (`Current_WildlandFire_Locations`) return **Token Required** stick to `WFIGS_Incident_Locations_Current`.
### 7.2 WFIGS current perimeters — **recommended polygon overlay**
```
https://services3.arcgis.com/T4QMspbfLg3qTGWY/arcgis/rest/services/WFIGS_Interagency_Perimeters_Current/FeatureServer/0/query
?where=1=1&outFields=poly_IncidentName,poly_GISAcres,attr_IncidentSize,attr_PercentContained,attr_FireDiscoveryDateTime
&outSR=4326&f=geojson
```
Live probe: **236** current perimeters. Historic `WFIGS_Interagency_Perimeters` counted **41,078** do not fetch that unfiltered. Use Current only, or `resultRecordCount` + bbox `geometry` + `spatialRel=esriSpatialRelIntersects`.
Authoritative US wildland-fire polygons; not every incident has a perimeter. Refresh ~5 min, fall-off rules drop stale small fires.[21]
**Frontend:** `L.geoJSON` fill red/orange by acres. Click for name/acres. This is the layer that makes FIRMS dots make sense.
### 7.3 GIBS thermal tiles (no key, no storage)
Already in `docs/firms.md`. Leaflet:
```
https://gibs.earthdata.nasa.gov/wmts/epsg3857/best/VIIRS_SNPP_Thermal_Anomalies_375m_All/default/{time}/GoogleMapsCompatible_Level{z}/{y}/{x}.png
```
Max zoom ~9. `{time}` = `YYYY-MM-DD`. Same GIBS CORS `*` as existing basemaps. Use as a toggle under FIRMS points.
### 7.4 FIRMS dataset note
Keep the CSV ingest. Add NOAA-20/21 datasets. Ultra-real-time US/Canada is available on those products (seconds-scale for much of US/Canada).[20]
---
## 8. Weather radar + alerts
### 8.1 RainViewer — **recommended global radar overlay**
No key. Personal / educational / small community. Attribution: Weather data by RainViewer + link.[13]
Live probe: `GET https://api.rainviewer.com/public/weather-maps.json` version `2.0`, host `https://tilecache.rainviewer.com`, **13** past frames (~2 h, 10 min steps). Tile HEAD: HTTP 200, CORS `*`, **`x-ratelimit-limit: 500` / 60 s**, burst 300.
Tile URL:[14]
```
{host}{path}/{size}/{z}/{x}/{y}/{color}/{smooth}_{snow}.png
```
Example: `https://tilecache.rainviewer.com/v2/radar/cb581daa2c0f/256/{z}/{x}/{y}/2/1_1.png`
- `{size}` 256 or 512
- `{color}` scheme id (2 is a common default)
- `{smooth}_{snow}` e.g. `1_1`
- **max zoom 7** (coarse vs IEM)
**Frontend:** fetch the JSON every 5 min, set `L.tileLayer` to the latest `past[].path`. Optional animation through `past` frames. No backend.
### 8.2 IEM NEXRAD / MRMS — **recommended CONUS quality radar**
Iowa Environmental Mesonet. No key. Free including commercial, as-is, no SLA.[16] CORS `*` on tiles (live HEAD confirmed).
Current CONUS base reflectivity TMS:[15]
```
https://mesonet.agron.iastate.edu/cache/tile.py/1.0.0/nexrad-n0q/{z}/{x}/{y}.png
```
Cache-Control 5 min. Aliases `mesonet1/2/3.agron.iastate.edu` for parallel browser tile fetches.[15]
Loop (past 555 min, step 5): layer `nexrad-n0q-mXXm` e.g. `nexrad-n0q-m05m`.
Also useful:
| Layer | Meaning |
|---|---|
| `nexrad-eet` | Echo tops |
| `q2-hsr` | MRMS seamless hybrid-scan reflectivity |
| `q2-n1p` / `q2-p24h` | MRMS 1 h / 24 h precip |
| `goes_east_conus_ch02` | GOES-East visible |
| `goes_east_conus_ch13` | GOES-East IR |
WMS alternative: `https://mesonet.agron.iastate.edu/cgi-bin/wms/nexrad/n0q.cgi?` TMS is simpler for Leaflet.
**Frontend:** use IEM as default when the map is over CONUS (higher res than RainViewer z7); RainViewer when zoomed outside the US or as a global fallback.
### 8.3 NWS API — alerts / forecasts (no radar tiles)
`https://api.weather.gov` US public domain, no fee. **User-Agent required** (app + contact). Rate limit unpublished; 429s clear in ~5 s. CORS `*`.[17]
Live probe: `GET /alerts/active?area=NC` HTTP 200 GeoJSON, 4 alerts, CORS `*`.
```
GET https://api.weather.gov/alerts/active?area=NC
GET https://api.weather.gov/alerts/active?point={lat},{lon}
GET https://api.weather.gov/points/{lat},{lon} # then follow forecast URLs
```
**Frontend can hit this directly** (CORS open) **or** backend can cache 30 s to protect the Pi if many tabs are open. Prefer backend cache. Draw `features[].geometry` with color by `properties.severity` / `event`.
### 8.4 IEM storm-based warnings GeoJSON
```
GET https://mesonet.agron.iastate.edu/geojson/sbw.geojson
```
Live probe: **46** current polygons. Sample: Severe Thunderstorm Warning, WFO BGM. Good companion to NWS alerts (polygon-accurate).
### 8.5 Open-Meteo — point forecast, not radar
No key for non-commercial **< 10,000 calls/day**. CC BY 4.0 data, attribution required. Commercial needs a plan or self-host.[18][19]
```
GET https://api.open-meteo.com/v1/forecast?latitude=35.88&longitude=-78.79&current=temperature_2m,precipitation,weather_code,wind_speed_10m
```
Live probe: RDU 28.4 °C, weather_code 80 (rain showers). Use for click-to-forecast popups, not a map overlay. Cache by grid cell.
### 8.6 NHC active storms
```
GET https://www.nhc.noaa.gov/CurrentStorms.json
```
Live probe: HTTP 200, `activeStorms[]` Tropical Storm Dolly (`al042026`, 13.6N 38.7W, 35 kt). No key. Plot as a hurricane marker + optional NHC cone GeoJSON (separate NHC GIS products). Nice extra during Atlantic season.
---
## 9. Keys / env vars to add
Store new secrets in the existing Keys UI (`api_keys` table) **and** `.env` fallback, same planned pattern as FIRMS.
| Var | Required for | How to get | Cost |
|---|---|---|---|
| `FIRMS_MAP_KEY` | Existing fire CSV | https://firms.modaps.eosdis.nasa.gov/api/map_key_info/ | Free |
| `OPENSKY_CLIENT_ID` / `OPENSKY_CLIENT_SECRET` | OpenSky authenticated | OpenSky account API Client | Free |
| `AISSTREAM_API_KEY` | Live AIS | GitHub login at aisstream.io/account | Free |
| `TRANSITLAND_API_KEY` | Transitland REST (optional) | Interline free plan | Free 10k/mo |
| `GFW_TOKEN` | Fishing-effort overlay (optional) | globalfishingwatch.org/our-apis/tokens | Free, non-commercial |
| *(none)* | ADSB.lol, Amtraker, RainViewer, IEM, NWS, WFIGS, Caltrans, Open-Meteo, NHC | | Free |
Amtraker: no key, but set `OSINT_USER_AGENT` to something like `osint-dashboard/1.0 (lancewalters94@gmail.com)` already have `USER_AGENT` in `camera_config.py`; reuse it on **all** outbound HTTP.
NWS: same User-Agent header is mandatory.[17]
---
## 10. Suggested API surface (backend engineer)
Keep FIRMS `GET /api/fires`. Add:
```
GET /api/aircraft?bbox=minlon,minlat,maxlon,maxlat
GET /api/vessels?bbox=...
GET /api/trains?bbox=...
GET /api/fire-incidents?bbox=... # WFIGS points
GET /api/fire-perimeters?bbox=... # WFIGS polygons (or proxy GeoJSON)
GET /api/alerts?area=NC|&bbox=... # cached NWS + IEM SBW
GET /api/map/radar # { provider, tileUrl, frames[] } for RainViewer metadata
GET /api/map/layers # already have GIBS; extend with radar/fire tile templates
```
All vector endpoints: default `limit=2000`, require bbox except trains (Amtrak is ~200 rows total). Return arrays of `{id, lat, lon, heading, speed, label, extra}` so the frontend can share one marker renderer.
Do **not** proxy RainViewer/IEM/GIBS tiles through the Pi (bandwidth). Return the template; browser fetches tiles.
AISStream stays inside `ingest` profile (needs a long-lived process, like FIRMS). Aircraft/trains can live in `run_ingester.py` loops.
---
## 11. Suggested UI (frontend bot)
Layer toggles on the existing Leaflet map (same pattern as GIBS + fire heatmap):
| Toggle | Default | Source |
|---|---|---|
| Radar | on (CONUS) | IEM `nexrad-n0q` if view intersects US, else RainViewer |
| NWS alerts | on | polygons |
| FIRMS heat | existing | existing |
| WFIGS perimeters | on | polygons |
| WFIGS incidents | off | points |
| Aircraft | on | `/api/aircraft` |
| Vessels | off until AIS key | `/api/vessels` |
| Trains | on | `/api/trains` |
| Cameras | existing | existing |
Viewport-driven: on `moveend` (debounced 300 ms), refetch aircraft/vessels for the new bbox. Do not refetch radar tiles except RainViewer frame JSON every 5 min.
Marker performance: canvas renderer (`L.canvas()`), cluster at zoom < 7, hide labels until zoom 8.
Camera preview: Caltrans HLS via existing hls.js path; JPEG thumb from `currentImageURL`. Still never emit RTSP.
Attribution bar (required): OpenSky / ADSB.lol ODbL / Amtraker / RainViewer / IEM / NWS / NASA FIRMS-GIBS / NIFC WFIGS / OSM as applicable.
---
## 12. Legal / ethics (non-negotiable)
- Masscan / RTSP policy unchanged.
- AISStream: server-side only; do not put the key in JS.[5]
- OpenSky: non-commercial unless licensed; cite if you publish.[2]
- ADSB.lol: ODbL share-alike on derived databases.[4]
- Amtraker: ODC-By, visible credit, identifying UA.[9]
- RainViewer: personal/educational; credit + link.[13]
- Open-Meteo: non-commercial 10k/day on the public instance.[18]
- GFW: non-commercial.[24]
- Caltrans / NWS / NIFC / NASA / IEM: US public or explicitly open; still cache politely.
- Do not scrape MarineTraffic / FlightRadar24 / FlightAware map tiles.
---
## 13. Verification log (this research session)
| Endpoint | Result |
|---|---|
| OpenSky `/states/all` NC bbox | 200, 270 aircraft, 0.63 s |
| ADSB.lol `/v2/lat/35.88/lon/-78.79/dist/50` | 200, 27 aircraft |
| RainViewer `weather-maps.json` | 200, 13 past frames |
| RainViewer tile | 200, CORS `*`, 500 req/min header |
| Open-Meteo forecast RDU | 200, 28.4 °C, code 80 |
| NWS `/alerts/active?area=NC` | 200, 4 alerts, CORS `*` |
| IEM `nexrad-n0q` tile | 200, CORS `*`, max-age 300 |
| IEM `sbw.geojson` | 200, 46 polygons |
| IEM `webcam.geojson` | 200, 3 cams |
| WFIGS locations count | 635 |
| WFIGS current perimeters count | 236 |
| ALERTWest `getCameraDataByLoc` | 200, 6.6 MB |
| Caltrans D4 CCTV JSON | 200, 749 cams, JPEG + HLS |
| Amtraker `/v3/trains` | 200, ~197 numbers, 1.3 MB |
| AviationWeather METAR KRDU | 200 JSON |
| NHC `CurrentStorms.json` | 200, TS Dolly |
| OpenRailwayMap tile HEAD | **403** do not rely on |
---
## 14. Concrete next tickets
**Backend**
1. `FIRMS_DATASET` dual-write NOAA-20/21; document S-NPP sunset.
2. `aircraft` ingest: ADSB.lol viewport poll + `/api/aircraft`.
3. `trains` ingest: Amtraker 30 s poll + User-Agent + `/api/trains`.
4. Camera parser: Caltrans CWWP2 D1D12 existing `cameras` rows (`discovery_source=caltrans`).
5. WFIGS proxy endpoints (locations + current perimeters), 515 min cache.
6. AISStream worker behind `AISSTREAM_API_KEY` (idle if unset, same as FIRMS).
7. `/api/alerts` 30 s cache of NWS + IEM SBW.
**Frontend**
1. Layer control: radar (IEM/RainViewer), alerts, WFIGS, aircraft, trains, vessels.
2. Shared point-layer component (id, lat, lon, heading, label).
3. RainViewer frame JSON animated overlay.
4. Caltrans HLS in existing preview pane.
5. Attribution strip.
**Do not start with:** Transitland, GFW, AISHub, OpenSky OAuth (until ADSB.lol is live and still needs a fallback).
## Sources
[1] https://openskynetwork.github.io/opensky-api/rest.html
[2] https://openskynetwork.github.io/opensky-api
[3] https://api.adsb.lol
[4] https://www.adsb.lol/docs/open-data/api
[5] https://aisstream.io/documentation
[6] https://aisstream.io
[7] https://www.aishub.net
[8] https://www.aishub.net/api
[9] https://github.com/piemadd/amtrak
[10] https://amtraker.com/about
[11] https://www.transit.land/documentation
[12] https://www.transit.land/plans-pricing
[13] https://www.rainviewer.com/api.html
[14] https://www.rainviewer.com/api/weather-maps-api.html
[15] https://mesonet.agron.iastate.edu/ogc
[16] https://mesonet.agron.iastate.edu/api
[17] https://www.weather.gov/documentation/services-web-API
[18] https://open-meteo.com
[19] https://open-meteo.com/en/docs
[20] https://firms.modaps.eosdis.nasa.gov/api/area
[21] https://data-nifc.opendata.arcgis.com/datasets/nifc::wfigs-current-interagency-fire-perimeters/about
[22] https://aviationweather.gov/data/api
[23] https://mesonet.agron.iastate.edu/geojson/webcam.py?help
[24] https://globalfishingwatch.org/our-apis/documentation
[25] https://openrailwaymap.org
[26] https://opensky-network.org/about/faq
[27] https://cwwp2.dot.ca.gov/vm/iframemap.htm