diff --git a/.env.example b/.env.example index 68fb3481a..8cb85ecf4 100644 --- a/.env.example +++ b/.env.example @@ -122,7 +122,7 @@ PORT=4173 # Optional: Live AIS vessels layer # Browser fetch path. Keep same-origin so the private stream key stays server-side. -VITE_AIS_LIVE_API_URL=/api/ais-live +VITE_AIS_LIVE_API_URL=/api/vessels # Client render cap. The server can keep a larger AISStream cache, but drawing # every global vessel at once can overwhelm WebGL on laptops. VITE_AIS_LIVE_MAX_ROWS=12000 diff --git a/SECURITY.md b/SECURITY.md index 71e4ce922..8a4256523 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -18,8 +18,8 @@ The golden rule: **secret-bearing API keys stay on the server side.** The dev/pr | Key | Where it lives | How the browser uses it | |-----|----------------|--------------------------| | `OPENAI_API_KEY` | Server only | Browser fetches a short-lived **ephemeral** Realtime session token from `/api/realtime/token`; the real key never ships | -| `AISSTREAM_API_KEY` | Server only | Server holds the AISStream websocket; browser polls the same-origin `/api/ais-live` cache | -| OpenSky OAuth (`OPENSKY_CLIENT_ID/SECRET`) | Server only | Server mints + refreshes the token behind `/api/opensky` | +| `AISSTREAM_API_KEY` | Server only | Server holds the AISStream websocket; browser polls the same-origin `/api/vessels` cache | +| OpenSky OAuth (`OPENSKY_CLIENT_ID/SECRET`) | Server only | Server mints + refreshes the token behind `/api/flights` | | `GOOGLE_MAPS_SERVER_API_KEY` (optional, #33) | Server only | Server calls Places (`/api/google/nearby-places`, `/api/google/text-search`) and the Street View fallback with this key; falls back to `GOOGLE_MAPS_API_KEY` when unset | ### Two deliberately client-side keys — restrict them diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index f7530d789..7c06724e2 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -1461,7 +1461,7 @@ This is the current runtime/source-of-truth snapshot for the project. > dependency list rather than a fix for one layer, and the dependencies reach > it by different routes: > - **AIS vessels is its reachable producer.** `enable()`/`update()` both -> resolve as soon as the first `/api/ais-live` poll answers, so the manager's +> resolve as soon as the first `/api/vessels` poll answers, so the manager's > lifecycle settles to `enabled` — but until the server-side socket delivers > a position, `firstConnectPhase` stays `'loading'` and `getStats()` reports > `loading: true`, `lastUpdate: null`, count 0, and an UNDEFINED status. @@ -1865,7 +1865,7 @@ isCurrent })` gates BOTH halves of the map-stack switch, which is its only > the share payload; Radio restores only its allowlisted filter and volume. > - **AIS feed watchdog (2026-08-18):** feed liveness is judged by DATA, not > socket state — AISStream can complete the handshake and then deliver -> nothing forever. `/api/ais-live` reports `live | stale | reconnecting | +> nothing forever. `/api/vessels` reports `live | stale | reconnecting | down | auth-failed` (plus the unchanged `missing-key`/`unsupported`) with > `silentForMs`, `reconnectAttempt` and `nextAttemptAt`. Silence is REPORTED > at 120s and ACTED ON at 300s; recovery walks a 5s/15s/60s/300s ladder and @@ -2911,9 +2911,9 @@ its criteria cannot be silently ignored. | Layer | Source | File | Proxy | Update Interval | | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | -------------------------------------------------------- | --------------------------------------------------------------------------------- | -| Live Flights ✈️ | OpenSky Network; bounded adsb.lol regional fallback | `src/data/flights.js` | `/api/opensky` (OAuth + fallback) | 30s | -| Military Flights 🎖️ | adsb.lol /v2/mil | `src/data/militaryFlights.js` | `/api/adsblol/mil` | 15s | -| Live AIS Vessels 🚢 | AISStream websocket | `src/data/aisLiveVessels.js` | `/api/ais-live` | 60s (+800ms visibility pass) | +| Live Flights ✈️ | OpenSky Network; bounded adsb.lol regional fallback | `src/data/flights.js` | `/api/flights` (OAuth + fallback) | 30s | +| Military Flights 🎖️ | adsb.lol /v2/mil | `src/data/militaryFlights.js` | `/api/military` | 15s | +| Live AIS Vessels 🚢 | AISStream websocket | `src/data/aisLiveVessels.js` | `/api/vessels` | 60s (+800ms visibility pass) | | Mapped Installations ⌖ | OpenFreeMap military areas; optional configured Overpass names and Google Places search | `src/data/militaryInstallations.js` | OpenFreeMap tiles, `/api/military-installations`, `/api/google/text-search` | viewport-driven; transient errors back off, missing capability switches to tiles | | Earthquakes | USGS | `src/data/earthquakes.js` | — | 60s | | Satellites | CelesTrak | `src/data/satellites.js` | `/api/celestrak` | 120s | @@ -3643,7 +3643,7 @@ silently demoting every later lookup for the session. A selected mission renders its orbit as four repeating tactical sectors, each containing one prominent cyan dot followed by one hundred thin translucent dashes. The bright dots act as orbit anchors while the subdued dash field remains depth-tested against the globe and is shown only for the selected mission. Close selected-pad views add one static 500 m-radius cyan launch-zone ring with a low-opacity translucent fill over the sampled photoreal launch-site surface. The single scene primitive is created only for the visible selected site and is otherwise dormant. It appears during Focus, sufficiently close manual zoom, and the replay countdown, but is suppressed above 120 km camera altitude, beyond 180 km direct camera-to-pad range, for unselected missions, and whenever Space Missions is inactive. Focus establishes a launch-site-centered camera transform once; subsequent manual heading and pitch changes remain centered on that site without an automated per-frame correction. Surface mission markers and labels use an additional conservative globe-limb margin before the exact ellipsoid occluder boundary, preventing near-horizon visibility from alternating between frames. - **AIS vessels**: chevron symbology (naval cyan base, type tints), world-space headings, MMSI-keyed reconciliation (selection survives refreshes; pinned 3 refreshes with STALE marker when absent), detection-overlay integration (`type: 'SEA'`), contextStore registration for voice Q&A. Empty-space clicks, id-less photorealistic-tile picks, and Escape dismiss the vessel card/HUD/context and clear its trail; picks owned by another layer (including `gev-trail:*`) and raw vessel-record picks without a live MMSI key are no-ops for vessel selection. Click and key handlers detach while the layer is disabled and reinstall on enable. Selecting another vessel replaces the selection and trail, and reconciliation clears a trail if its owning vessel is evicted. -- **Track trails**: server accumulates per-MMSI ring buffers (`/api/ais-live/track?mmsi=`, Float32+Uint32, 64 samples, 30s/25m thinning); aircraft backfill proxies `/api/opensky-track` (OAuth, own credit bucket) and `/api/adsblol/trace` (tar1090 readsb, ~24h history, ODbL — credit adsb.lol). +- **Track trails**: server accumulates per-MMSI ring buffers (`/api/vessels/track?mmsi=`, Float32+Uint32, 64 samples, 30s/25m thinning); aircraft backfill proxies `/api/flights/track` (OAuth, own credit bucket) and `/api/military/track` (tar1090 readsb, ~24h history, ODbL — credit adsb.lol). - Shared `src/data/pickRegistry.js` stops the two flight layers' click handlers from fighting over the camera. ### Optional Overpass configuration @@ -3862,7 +3862,7 @@ and unreachable upstream (502/504) separately from road geometry. ### Live AIS Vessels (June 2026) - Server-side `ws` websocket to `wss://stream.aisstream.io/v0/stream` maintained by Vite middleware; `AISSTREAM_API_KEY` never reaches the browser (AISStream has no browser CORS). The `ws` package is used rather than Node's built-in WebSocket specifically because only it can hard-abort a wedged socket (see the watchdog note in the delta block at the top). -- Browser polls same-origin `/api/ais-live` cache every 60s. +- Browser polls same-origin `/api/vessels` cache every 60s. - The first enable in a session starts one 30-second client grace timer. Until an accepted vessel position arrives, `live`/`open`/`connecting` transport reports `LOADING`; the timer is not restarted by the 60-second poll. Expiry or a @@ -4135,13 +4135,13 @@ are omitted rather than framing the wrong part of the globe. - Proxy error payloads are sanitized (no internal error details returned to clients). - That holds for the OpenAI and CCTV media paths too: `/api/openai/hud-summary` never relays OpenAI's own `error.message`, `/api/realtime/token` passes successful ephemeral-token responses through but answers with a fixed error when minting fails or upstream rejects the request, and a failed CCTV media fetch stores a fixed camera health `message` — `GET /api/cctv/health` serializes that field and the CCTV panel renders it as a status label, so it is a client surface as much as the response body is. - `OPENAI_API_KEY` is server-side only; the browser receives ephemeral Realtime client secrets from `/api/realtime/token`. -- `AISSTREAM_API_KEY` is server-side only; the browser reads the same-origin `/api/ais-live` cache. +- `AISSTREAM_API_KEY` is server-side only; the browser reads the same-origin `/api/vessels` cache. - `/api/google/nearby-places` keeps the Google key out of Places requests issued for voice scene context. - `/api/google/text-search` keeps the Google key server-side for view-biased Places recovery used by annotation resolution. - `/api/overpass` is bounded by body/response caps, per-client/global rate limits, concurrency limits, operator-configured upstreams, in-flight dedupe, cache bounds, and static validation that every selector is spatially bounded. - `/api/military-installations` uses an independent limiter with the same 90-per-client/300-global one-minute bounds, so viewport installation refreshes never consume `/api/overpass` annotation capacity. - `/api/route` proxies bounded OSRM route requests for annotation routes, with profile allowlisting, distance caps, response caps, caching, and sanitized "no route found" errors. -- Track endpoints: `/api/ais-live/track?mmsi=` (server-accumulated ring buffers; sub-route handled before the rows snapshot), `/api/opensky-track?icao24=` (OAuth, 60s cache, sanitized errors, independent OpenSky credit bucket), `/api/adsblol/trace?hex=` (60s cache, 5MB cap, ODbL attribution required in UI). +- Track endpoints: `/api/vessels/track?mmsi=` (server-accumulated ring buffers; sub-route handled before the rows snapshot), `/api/flights/track?icao24=` (OAuth, 60s cache, sanitized errors, independent OpenSky credit bucket), `/api/military/track?hex=` (60s cache, 5MB cap, ODbL attribution required in UI). - Realtime debug logs redact API keys, bearer tokens, client secrets, and image data URLs before writing to disk; request bodies are size-capped. - `/api/realtime/debug-log` enforces an always-on 120-per-client/400-global one-minute limiter (not the opt-in `GEV_RATELIMIT_OPENAI_PER_MIN` bucket the cost-bearing OpenAI routes share), appends asynchronously through a serialized queue, and rotates `realtime-conversations.jsonl` at 32 MB keeping one prior generation, so the sink is bounded at twice that regardless of session length. A malformed record answers 400 and a failed write 500, both with a fixed message. diff --git a/docs/opensky-auth.md b/docs/opensky-auth.md index c12b726bc..67844f912 100644 --- a/docs/opensky-auth.md +++ b/docs/opensky-auth.md @@ -1,6 +1,6 @@ # OpenSky Auth Setup -God's Eye View uses explicit auth modes for `/api/opensky`: +God's Eye View uses explicit auth modes for `/api/flights`: - `OPENSKY_AUTH_MODE=oauth` (default, recommended) - `OPENSKY_AUTH_MODE=auto` (OAuth first, then Basic fallback) @@ -54,7 +54,7 @@ OPENSKY_AUTH_MODE=basic ./scripts/dev-fresh.sh Inspect proxy headers: ```bash -curl -si http://localhost:4173/api/opensky | grep -iE 'HTTP/|X-OpenSky-Auth' +curl -si http://localhost:4173/api/flights | grep -iE 'HTTP/|X-OpenSky-Auth' ``` Useful reasons: diff --git a/scripts/qa-cockpit-plates.mjs b/scripts/qa-cockpit-plates.mjs index bc42ab821..4e725fd8f 100644 --- a/scripts/qa-cockpit-plates.mjs +++ b/scripts/qa-cockpit-plates.mjs @@ -253,19 +253,19 @@ async function main() { const url = new URL(request.url()); if (url.origin !== APP_ORIGIN) return void request.continue(); // Silence the live feeds: the injected field is the only population. - if (url.pathname === '/api/ais-live') { + if (url.pathname === '/api/vessels') { return void request.respond(stubJson({ rows: [], source: 'QA', status: 'open', error: null, refreshing: false, newestPositionAt: null, lastMessageAt: null, })); } - if (url.pathname === '/api/adsblol/mil') { + if (url.pathname === '/api/military') { return void request.respond(stubJson({ msg: 'No error', now: Date.now(), ac: [] })); } - if (url.pathname === '/api/opensky') { + if (url.pathname === '/api/flights') { return void request.respond(stubJson({ time: Math.floor(Date.now() / 1000), states: [] })); } - if (url.pathname === '/api/opensky-track') { + if (url.pathname === '/api/flights/track') { return void request.respond(stubJson({ path: [] })); } if (url.pathname === '/api/openai/hud-summary') { diff --git a/scripts/qa-cockpit-utility.mjs b/scripts/qa-cockpit-utility.mjs index c28fabfe6..cb3515ed2 100644 --- a/scripts/qa-cockpit-utility.mjs +++ b/scripts/qa-cockpit-utility.mjs @@ -42,7 +42,7 @@ page.on('pageerror', (error) => consoleErrors.push(error.message)); page.on('response', (response) => { const url = new URL(response.url()); const expectedOptionalTrackMiss = response.status() === 404 - && url.pathname === '/api/opensky-track'; + && url.pathname === '/api/flights/track'; if ( url.origin === new URL(appUrl).origin && response.status() >= 400 @@ -108,7 +108,7 @@ try { }); return; } - if (url.origin === new URL(appUrl).origin && url.pathname === '/api/ais-live') { + if (url.origin === new URL(appUrl).origin && url.pathname === '/api/vessels') { request.respond({ status: 200, contentType: 'application/json', @@ -124,7 +124,7 @@ try { }); return; } - if (url.origin === new URL(appUrl).origin && url.pathname === '/api/adsblol/mil') { + if (url.origin === new URL(appUrl).origin && url.pathname === '/api/military') { request.respond({ status: 200, contentType: 'application/json', @@ -132,7 +132,7 @@ try { }); return; } - if (url.origin === new URL(appUrl).origin && url.pathname === '/api/opensky-track') { + if (url.origin === new URL(appUrl).origin && url.pathname === '/api/flights/track') { request.respond({ status: 200, contentType: 'application/json', @@ -832,7 +832,7 @@ try { await readinessPage.setRequestInterception(true); readinessPage.on('request', (request) => { const url = new URL(request.url()); - if (url.origin === new URL(appUrl).origin && url.pathname === '/api/opensky') { + if (url.origin === new URL(appUrl).origin && url.pathname === '/api/flights') { const now = Math.floor(Date.now() / 1000); request.respond({ status: 200, contentType: 'application/json', diff --git a/scripts/qa-context-arrivals.mjs b/scripts/qa-context-arrivals.mjs index 4fb66cadc..0128f44e5 100644 --- a/scripts/qa-context-arrivals.mjs +++ b/scripts/qa-context-arrivals.mjs @@ -12,7 +12,7 @@ * least 60 m above the rendered surface under it and under the view centre. * * Contacts: serves two synthetic aircraft through the page's own - * /api/opensky response (one flying north over Camp Mabry at 2,500 m, one + * /api/flights response (one flying north over Camp Mabry at 2,500 m, one * near Fort Cavazos) and an empty military feed, starts from a wide Texas * view, and presses CONTACTS as a user would. Asserts the Mapped * installations row lists at least one site with a distance within 15 s, @@ -110,7 +110,7 @@ async function openPage({ aircraft = false } = {}) { } catch { return realFetch(input, init); } - if (url.origin === location.origin && url.pathname === '/api/opensky') { + if (url.origin === location.origin && url.pathname === '/api/flights') { const now = Math.floor(Date.now() / 1000); const seconds = (Date.now() - started) / 1000; const lat = 30.29 + (seconds * 120) / 111_320; @@ -159,7 +159,7 @@ async function openPage({ aircraft = false } = {}) { }; return Promise.resolve(Response.json(json)); } - if (url.origin === location.origin && url.pathname === '/api/adsblol/mil') + if (url.origin === location.origin && url.pathname === '/api/military') return Promise.resolve( Response.json({ ac: [], now: Date.now(), total: 0 }), ); diff --git a/scripts/qa-enrich-ambient.mjs b/scripts/qa-enrich-ambient.mjs index 400857b54..abdfddb9d 100644 --- a/scripts/qa-enrich-ambient.mjs +++ b/scripts/qa-enrich-ambient.mjs @@ -12,7 +12,7 @@ * honoring the politeness bounds. * * Fetch shim (installed before app code, same pattern as qa-sprites-b5.mjs): - * - /api/opensky → 12 straight-flying planes, category 0 + * - /api/flights → 12 straight-flying planes, category 0 * - /api/adsbdb/type/:hex → varied REAL type codes (C172, B744, DH8D, H60, * F16, GLID, B77W, A320, B738, PC12, R44) + one * found:false miss; responses are HELD until the @@ -260,14 +260,14 @@ async function main() { }); } if (url.includes('/api/adsbdb/')) return Promise.resolve(jsonResponse({ found: false })); - if (url.includes('/api/opensky-track')) return Promise.resolve(jsonResponse({ path: [] })); - if (url.includes('/api/adsblol/trace')) { + if (url.includes('/api/flights/track')) return Promise.resolve(jsonResponse({ path: [] })); + if (url.includes('/api/military/track')) { return Promise.resolve(jsonResponse({ timestamp: Math.floor(nowSec), trace: [] })); } - if (url.includes('/api/adsblol/mil')) { + if (url.includes('/api/military')) { return Promise.resolve(jsonResponse({ msg: 'No error', now: Date.now(), ac: [] })); } - if (url.includes('/api/opensky')) { + if (url.includes('/api/flights')) { const states = S.planes.map((f) => { const s = S.stateAt(f, tRel); return [ diff --git a/scripts/qa-failstate-b10.mjs b/scripts/qa-failstate-b10.mjs index 55d77a310..983562fa7 100644 --- a/scripts/qa-failstate-b10.mjs +++ b/scripts/qa-failstate-b10.mjs @@ -10,7 +10,7 @@ * upstream failures so the run is deterministic and never depends on live * AISStream / CelesTrak availability: * - * (i) AIS invalid key — /api/ais-live is answered with + * (i) AIS invalid key — /api/vessels is answered with * {rows:[],status:'error',error:'invalid key'}. Assert the layer's * getStats().error is set (feed down) — NOT a clean 'just now · 0'. * @@ -193,7 +193,7 @@ async function main() { page.on('request', (req) => { const url = req.url(); // (i) AIS invalid-key: the exact payload the spec calls for. - if (url.includes('/api/ais-live') && !url.includes('/track')) { + if (url.includes('/api/vessels') && !url.includes('/track')) { req.respond({ status: 200, contentType: 'application/json', diff --git a/scripts/qa-floor-hold.mjs b/scripts/qa-floor-hold.mjs index 82bdf6baa..0b67b3994 100644 --- a/scripts/qa-floor-hold.mjs +++ b/scripts/qa-floor-hold.mjs @@ -89,7 +89,7 @@ await page.evaluateOnNewDocument((site, icao) => { window.__TERRAIN.ok += 1; return realFetch(input, init); } - if (app && url.pathname === '/api/opensky') { + if (app && url.pathname === '/api/flights') { const c = window.__CONTACT; const t = Math.floor(Date.now() / 1000); // baro_altitude (7) and geo_altitude (13) both null, on_ground true. @@ -99,7 +99,7 @@ await page.evaluateOnNewDocument((site, icao) => { null, true, 1.5, 90, 0, null, null, null, false, 0]], })); } - if (app && url.pathname === '/api/opensky-track') return Promise.resolve(json({ path: [] })); + if (app && url.pathname === '/api/flights/track') return Promise.resolve(json({ path: [] })); if (app && /^\/api\/adsbdb\//.test(url.pathname)) return Promise.resolve(json({})); return realFetch(input, init); }; diff --git a/scripts/qa-focus-evidence.mjs b/scripts/qa-focus-evidence.mjs index bff4d1cf3..1baaf6083 100644 --- a/scripts/qa-focus-evidence.mjs +++ b/scripts/qa-focus-evidence.mjs @@ -268,11 +268,11 @@ async function installSyntheticFetches(page) { })); window.fetch = (input, init) => { const url = typeof input === 'string' ? input : input?.url || ''; - if (url.includes('/api/opensky-track')) return json({ path: [] }); - if (url.includes('/api/adsblol/trace')) return json({ timestamp: Date.now() / 1000, trace: [] }); - if (url.includes('/api/opensky')) return json({ time: Math.floor(Date.now() / 1000), states: [] }); + if (url.includes('/api/flights/track')) return json({ path: [] }); + if (url.includes('/api/military/track')) return json({ timestamp: Date.now() / 1000, trace: [] }); + if (url.includes('/api/flights')) return json({ time: Math.floor(Date.now() / 1000), states: [] }); if (url.includes('/api/adsbdb/')) return json({ found: false }); - if (url.includes('/api/ais-live')) return json({ status: 'open', rows: [] }); + if (url.includes('/api/vessels')) return json({ status: 'open', rows: [] }); return realFetch(input, init); }; }); diff --git a/scripts/qa-heading-b3.mjs b/scripts/qa-heading-b3.mjs index b585e71ae..cbba1b88e 100644 --- a/scripts/qa-heading-b3.mjs +++ b/scripts/qa-heading-b3.mjs @@ -525,11 +525,11 @@ async function main() { const nowSec = Date.now() / 1000 + (T.timeOffsetSec || 0); const tRel = nowSec - T.epochMs / 1000; - if (url.includes('/api/opensky-track')) return Promise.resolve(jsonResponse({ path: [] })); - if (url.includes('/api/adsblol/trace')) { + if (url.includes('/api/flights/track')) return Promise.resolve(jsonResponse({ path: [] })); + if (url.includes('/api/military/track')) { return Promise.resolve(jsonResponse({ timestamp: Math.floor(nowSec), trace: [] })); } - if (url.includes('/api/opensky')) { + if (url.includes('/api/flights')) { T.__hitsGuard = ++window.__TURN_HITS.opensky; const row = (id, callsign, s, altM, category) => [ id, callsign, 'Synthetica', @@ -548,7 +548,7 @@ async function main() { if (T.heli65.active) states.push(row(T.heli65.icao, T.heli65.callsign, T.stateAt(T.heli65, tRel), T.heli65.altM, T.heli65.category)); return Promise.resolve(jsonResponse({ time: Math.floor(nowSec), states })); } - if (url.includes('/api/adsblol/mil')) { + if (url.includes('/api/military')) { window.__TURN_HITS.mil++; const ac = T.military.map((m) => { const s = T.stateAt(m, tRel); diff --git a/scripts/qa-l9-matrix.mjs b/scripts/qa-l9-matrix.mjs index bb43d968f..2a3d7328e 100644 --- a/scripts/qa-l9-matrix.mjs +++ b/scripts/qa-l9-matrix.mjs @@ -738,9 +738,9 @@ check({ }); check({ - id: 'B2', group: 'B', desc: 'Flights proxy returns live contacts (/api/opensky)', + id: 'B2', group: 'B', desc: 'Flights proxy returns live contacts (/api/flights)', run: async () => { - const r = await jget('/api/opensky?lamin=24&lomin=-125&lamax=50&lomax=-66', { timeoutMs: 45000 }); + const r = await jget('/api/flights?lamin=24&lomin=-125&lamax=50&lomax=-66', { timeoutMs: 45000 }); if (!r.ok) return fail(`HTTP ${r.status}: ${r.text.slice(0, 120)}`); const n = r.json?.states?.length || 0; return n > 0 ? pass(`${n} states, cache=${r.headers.get('x-opensky-cache') || 'n/a'}`) : fail('0 states returned'); @@ -750,7 +750,7 @@ check({ check({ id: 'B3', group: 'B', desc: 'OpenSky credentials are actually in use (not the anonymous/fallback path)', run: async () => { - const r = await jget('/api/opensky?lamin=24&lomin=-125&lamax=50&lomax=-66', { timeoutMs: 45000 }); + const r = await jget('/api/flights?lamin=24&lomin=-125&lamax=50&lomax=-66', { timeoutMs: 45000 }); // Header names are exact: X-OpenSky-Auth-Mode-Used / X-OpenSky-Auth-Reason. // (An earlier guess at these names made this check pass vacuously.) const reason = r.headers.get('X-OpenSky-Auth-Reason') || ''; @@ -826,9 +826,9 @@ check({ }); check({ - id: 'B8', group: 'B', desc: 'AIS vessel feed is live (/api/ais-live)', needsKey: 'AIS', + id: 'B8', group: 'B', desc: 'AIS vessel feed is live (/api/vessels)', needsKey: 'AIS', run: async () => { - const r = await jget('/api/ais-live', { timeoutMs: 40000 }); + const r = await jget('/api/vessels', { timeoutMs: 40000 }); const rows = r.json?.rows?.length || 0; const status = r.json?.status; if (!r.ok) return fail(`HTTP ${r.status} status=${status}`); @@ -864,7 +864,7 @@ check({ const guard = keyGuard('AIS', env.keys.AIS); if (guard) return guard; if (env.keys.AIS === true) return skip('server HAS an AISStream key', 'N/A'); - const r = await jget('/api/ais-live'); + const r = await jget('/api/vessels'); return r.status === 503 && r.json?.status === 'missing-key' && Array.isArray(r.json?.rows) ? pass(`503 status=missing-key, rows=[] — "${String(r.json?.error).slice(0, 60)}"`) : fail(`expected 503/missing-key, got ${r.status} ${r.text.slice(0, 120)}`); @@ -1022,7 +1022,7 @@ check({ check({ id: 'B21', group: 'B', desc: 'No proxy echoes credential material back to the client (P1-5 acceptance #4)', run: async () => { - const paths = ['/api/cctv/sources', '/api/tomtom/status', '/api/firms/status', '/api/celestrak/stations', '/api/ais-live']; + const paths = ['/api/cctv/sources', '/api/tomtom/status', '/api/firms/status', '/api/celestrak/stations', '/api/vessels']; const leaked = []; const unscannable = []; for (const p of paths) { @@ -1036,7 +1036,7 @@ check({ // An error page is not a payload. Scanning five 500s and finding no key // is trivially true and proves nothing — a broken app must not satisfy a // negative assertion. The one documented exception is the keyless - // 503 {status:'missing-key'} from /api/ais-live, which IS its real shape. + // 503 {status:'missing-key'} from /api/vessels, which IS its real shape. const documentedKeyless = r.status === 503 && (r.json?.status === 'missing-key' || r.json?.error === 'no_key' || /OPENAI_API_KEY is not set/.test(r.text)); if (!r.ok && !documentedKeyless) { @@ -2071,13 +2071,13 @@ async function preflight() { env.keys.FIRMS = await statusKey('/api/firms/status'); env.keys.TOMTOM = await statusKey('/api/tomtom/status'); try { - const ais = await jget('/api/ais-live'); + const ais = await jget('/api/vessels'); if (ais.status === 503 && ais.json?.status === 'missing-key') env.keys.AIS = false; else if (ais.status === 200 && ais.json && Array.isArray(ais.json.rows)) env.keys.AIS = true; else env.keys.AIS = 'error'; } catch { env.keys.AIS = 'error'; } try { - const os = await jget('/api/opensky?lamin=29&lomin=-99&lamax=31&lomax=-97', { timeoutMs: 40000 }); + const os = await jget('/api/flights?lamin=29&lomin=-99&lamax=31&lomax=-97', { timeoutMs: 40000 }); const reason = os.headers.get('X-OpenSky-Auth-Reason') || ''; const used = os.headers.get('X-OpenSky-Auth-Mode-Used') || os.headers.get('X-OpenSky-Auth') || ''; if (!os.ok) env.keys.OPENSKY = 'error'; diff --git a/scripts/qa-overlay-baseline.mjs b/scripts/qa-overlay-baseline.mjs index 8018ddfe5..57dc1fcd7 100644 --- a/scripts/qa-overlay-baseline.mjs +++ b/scripts/qa-overlay-baseline.mjs @@ -180,7 +180,7 @@ async function installDeterministicDevEndpoints(page) { }); return; } - if (url.pathname === '/api/ais-live') { + if (url.pathname === '/api/vessels') { await request.respond({ status: 200, contentType: 'application/json', diff --git a/scripts/qa-radio.mjs b/scripts/qa-radio.mjs index c30a17ee0..adbbfdf81 100644 --- a/scripts/qa-radio.mjs +++ b/scripts/qa-radio.mjs @@ -219,7 +219,7 @@ async function main() { }); return; } - if (url.origin === APP_ORIGIN && url.pathname === '/api/ais-live') { + if (url.origin === APP_ORIGIN && url.pathname === '/api/vessels') { request.respond({ status: 200, contentType: 'application/json', diff --git a/scripts/qa-sprites-b5.mjs b/scripts/qa-sprites-b5.mjs index 93eb6eb02..a70eb1b2d 100644 --- a/scripts/qa-sprites-b5.mjs +++ b/scripts/qa-sprites-b5.mjs @@ -229,11 +229,11 @@ async function main() { const nowSec = Date.now() / 1000 + (S.timeOffsetSec || 0); const tRel = nowSec - S.epochMs / 1000; - if (url.includes('/api/opensky-track')) return Promise.resolve(jsonResponse({ path: [] })); - if (url.includes('/api/adsblol/trace')) { + if (url.includes('/api/flights/track')) return Promise.resolve(jsonResponse({ path: [] })); + if (url.includes('/api/military/track')) { return Promise.resolve(jsonResponse({ timestamp: Math.floor(nowSec), trace: [] })); } - if (url.includes('/api/opensky')) { + if (url.includes('/api/flights')) { window.__SPR_HITS.opensky++; const states = S.flights.map((f) => { const s = S.stateAt(f, tRel); @@ -249,7 +249,7 @@ async function main() { }); return Promise.resolve(jsonResponse({ time: Math.floor(nowSec), states })); } - if (url.includes('/api/adsblol/mil')) { + if (url.includes('/api/military')) { window.__SPR_HITS.mil++; const ac = S.military.map((m) => { const s = S.stateAt(m, tRel); diff --git a/scripts/track-regression.mjs b/scripts/track-regression.mjs index 934d26781..84aa2494e 100644 --- a/scripts/track-regression.mjs +++ b/scripts/track-regression.mjs @@ -17,9 +17,9 @@ * `fetch` shim in the page that intercepts the poll endpoints and returns * SYNTHETIC aircraft positioned near the camera, in the EXACT upstream payload * shapes the layers parse: - * - flights → GET /api/opensky → { states: [ ] } - * - military → GET /api/adsblol/mil → { ac: [ ] } - * - vessels → GET /api/ais-live → connected, zero-row snapshot + * - flights → GET /api/flights → { states: [ ] } + * - military → GET /api/military → { ac: [ ] } + * - vessels → GET /api/vessels → connected, zero-row snapshot * The shim stays installed so the layers' setInterval pollers keep the * synthetic planes alive (under MISSING_POLL_LIMIT=3 so they're never pruned). * @@ -343,7 +343,7 @@ async function main() { const url = new URL(requestUrl, window.location.href); const isAppRequest = url.origin === appOrigin; // OpenSky (commercial flights): { states: [ state-vector[] ] } - if (isAppRequest && url.pathname === '/api/opensky') { + if (isAppRequest && url.pathname === '/api/flights') { window.__SYNTH_HITS.opensky++; // A withheld contact models the ordinary case where the recipient's // first authoritative refresh does not yet carry the shared aircraft. @@ -371,11 +371,11 @@ async function main() { // Trail backfill (fires on trackById). Stub with valid-but-empty // payloads so the deterministic run never 404s against the dev proxy. // flights: { path: [ [time, lat, lon, baroAlt, true_track, on_ground] ] } - if (isAppRequest && url.pathname === '/api/opensky-track') { + if (isAppRequest && url.pathname === '/api/flights/track') { return Promise.resolve(jsonResponse({ path: [] })); } // military: { timestamp: , trace: [ [secAfter, lat, lon, ...] ] } - if (isAppRequest && url.pathname === '/api/adsblol/trace') { + if (isAppRequest && url.pathname === '/api/military/track') { return Promise.resolve(jsonResponse({ timestamp: Math.floor(Date.now() / 1000), trace: [] })); } // adsbdb enrichment (fires for tracked/model-eligible planes): empty @@ -388,7 +388,7 @@ async function main() { // Contacts enables the optional AIS source alongside aircraft. Keep // this tracking harness independent of local AIS credentials while // preserving the truthful "awaiting first message" lifecycle. - if (isAppRequest && url.pathname === '/api/ais-live') { + if (isAppRequest && url.pathname === '/api/vessels') { return Promise.resolve(jsonResponse({ status: 'connected', rows: [], @@ -450,7 +450,7 @@ async function main() { return Promise.resolve(jsonResponse({ places: [] })); } // adsb.lol military: { ac: [ aircraft{} ] } (must come AFTER /trace check) - if (isAppRequest && url.pathname === '/api/adsblol/mil') { + if (isAppRequest && url.pathname === '/api/military') { window.__SYNTH_HITS.mil++; const ac = window.__SYNTH.military.map((m) => ({ hex: m.hex, diff --git a/server/providers/aircraft/adsb-lol.js b/server/providers/aircraft/adsb-lol.js index fc5ab0bd3..f67afed1d 100644 --- a/server/providers/aircraft/adsb-lol.js +++ b/server/providers/aircraft/adsb-lol.js @@ -1,7 +1,7 @@ /** * Vite plugin: adsb.lol military aircraft proxy with 12 s response cache. * - * Proxies GET /api/adsblol/mil to https://api.adsb.lol/v2/mil. When upstream + * Serves GET /api/military from https://api.adsb.lol/v2/mil. When upstream * fails — a thrown fetch OR a non-OK status such as 429 — the proxy serves its * cached body as STALE instead of relaying the failure, and it backs off from * upstream for the Retry-After period (bounded) so a rate limit is never @@ -55,10 +55,11 @@ export function adsbLolProxy() { res.writeHead(status, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store', - 'X-ADS-B-Cache': cacheStatus, + 'X-Feed-Source': 'adsb.lol', + 'X-Feed-Cache': cacheStatus, ...(['HIT', 'STALE'].includes(cacheStatus) ? { - 'X-ADS-B-Cache-Age-Ms': String(Math.max(0, Date.now() - _cacheAt)), + 'X-Feed-Age-Ms': String(Math.max(0, Date.now() - _cacheAt)), } : {}), ...extra, @@ -67,8 +68,11 @@ export function adsbLolProxy() { } const installMiddleware = (server) => { - server.middlewares.use('/api/adsblol/mil', async (req, res) => { + server.middlewares.use('/api/military', async (req, res, next) => { try { + // This mount also matches its /track route, served by its own handler. + if (new URL(req.url || '/', 'http://localhost').pathname !== '/') + return next(); const now = Date.now(); if (_cache && now - _cacheAt < CACHE_MS) { serve(res, 200, _cache, 'HIT'); diff --git a/server/providers/aircraft/opensky.js b/server/providers/aircraft/opensky.js index db4920668..187049aea 100644 --- a/server/providers/aircraft/opensky.js +++ b/server/providers/aircraft/opensky.js @@ -380,8 +380,12 @@ function openSkySourceIsStale(sourceEpochMs, now = Date.now()) { */ export function openSkyProxy() { const installMiddleware = (server) => { - server.middlewares.use('/api/opensky', async (req, res) => { + server.middlewares.use('/api/flights', async (req, res, next) => { try { + // This mount also matches its /track route, served by its own handler. + if (new URL(req.url || '/', 'http://localhost').pathname !== '/') + return next(); + res.setHeader('X-Feed-Source', 'OpenSky Network'); const requestedMode = normalizeOpenSkyAuthMode( process.env.OPENSKY_AUTH_MODE, ); diff --git a/server/providers/aircraft/tracks.js b/server/providers/aircraft/tracks.js index 2c7b78a78..5f800ac59 100644 --- a/server/providers/aircraft/tracks.js +++ b/server/providers/aircraft/tracks.js @@ -3,11 +3,11 @@ import { readCappedResponseText } from '../common/http.js'; /** * Vite plugin: aircraft track-history backfill proxies (PRD WS-F F1/F2). * - * /api/opensky-track?icao24= — OpenSky GET /tracks/all (experimental; + * /api/flights/track?icao24= — OpenSky GET /tracks/all (experimental; * own credit bucket, 4 credits per call on the free tier). OAuth via the * shared coalesced token. 60s per-icao cache; 404/429 forwarded so the * client can fall back to its accumulated trail silently. - * /api/adsblol/trace?hex= — adsb.lol tar1090 readsb trace + * /api/military/track?hex= — adsb.lol tar1090 readsb trace * (undocumented but live; no browser CORS, hence this proxy). Up to ~24h * of real history per aircraft. Treat as best-effort; data is ODbL — * credit "adsb.lol (ODbL)" in the UI. @@ -65,7 +65,7 @@ export function trackBackfillProxies() { } function install(middlewares) { - middlewares.use('/api/opensky-track', async (req, res) => { + middlewares.use('/api/flights/track', async (req, res) => { try { const incoming = new URL(req.url || '', 'http://localhost'); const icao24 = String(incoming.searchParams.get('icao24') || '') @@ -93,7 +93,7 @@ export function trackBackfillProxies() { } }); - middlewares.use('/api/adsblol/trace', async (req, res) => { + middlewares.use('/api/military/track', async (req, res) => { try { const incoming = new URL(req.url || '', 'http://localhost'); const hex = String(incoming.searchParams.get('hex') || '') diff --git a/server/providers/vessels/ais-live.js b/server/providers/vessels/ais-live.js index 2f8f8a25f..cb1dd36b0 100644 --- a/server/providers/vessels/ais-live.js +++ b/server/providers/vessels/ais-live.js @@ -71,14 +71,15 @@ let _aisWebSocketImpl; */ export function aisLiveProxy() { function install(middlewares) { - middlewares.use('/api/ais-live', async (req, res) => { + middlewares.use('/api/vessels', async (req, res) => { try { + res.setHeader('X-Feed-Source', 'AISStream'); ensureAisStreamConnection(); const incoming = new URL(req.url || '', 'http://localhost'); // Track sub-route MUST be handled before the rows snapshot — this // mount prefix-matches every subpath, so without this branch - // /api/ais-live/track would be silently answered with vessel rows. + // /api/vessels/track would be silently answered with vessel rows. if ( incoming.pathname === '/track' || incoming.pathname.startsWith('/track/') @@ -288,7 +289,7 @@ function aisKeyFingerprint() { } /** - * Drive the watchdog once. Called on every /api/ais-live request and on the + * Drive the watchdog once. Called on every /api/vessels request and on the * background interval, so recovery does not depend on browser traffic. */ function ensureAisStreamConnection() { @@ -307,7 +308,7 @@ function ensureAisStreamConnection() { keyFingerprint: aisKeyFingerprint(), }); } -/** Status metadata for /api/ais-live, safe to call before the first connect. */ +/** Status metadata for /api/vessels, safe to call before the first connect. */ function aisStreamStatusSnapshot() { const snapshot = _aisAdapter ? _aisAdapter.snapshot() : null; if (snapshot) return snapshot; diff --git a/src/data/aisLiveVessels.analyst.test.mjs b/src/data/aisLiveVessels.analyst.test.mjs index 6b2a7426c..636f84e31 100644 --- a/src/data/aisLiveVessels.analyst.test.mjs +++ b/src/data/aisLiveVessels.analyst.test.mjs @@ -28,7 +28,7 @@ test('ais analyst record: full record maps every contract field', () => { courseDeg: 214.0, shipType: 'Cargo', destination: 'OAKLAND', - navStatus: null, // /api/ais-live does not surface NavigationalStatus + navStatus: null, // /api/vessels does not surface NavigationalStatus }); }); diff --git a/src/data/aisLiveVessels.js b/src/data/aisLiveVessels.js index b3800db68..8fbbe5496 100644 --- a/src/data/aisLiveVessels.js +++ b/src/data/aisLiveVessels.js @@ -1,10 +1,10 @@ import { createApplicationVessels } from '../app/layers/aisLiveVessels.js'; -import { createAisStreamSource } from '../sources/live/standalone.js'; +import { createVesselSource } from '../sources/live/standalone.js'; const aisLiveVesselsLayer = createApplicationVessels({ - source: createAisStreamSource({ - apiUrl: import.meta.env?.VITE_AIS_LIVE_API_URL || '/api/ais-live', + source: createVesselSource({ + apiUrl: import.meta.env?.VITE_AIS_LIVE_API_URL || '/api/vessels', // Resolved against the document's address, which a panel host may serve // from its own scheme. origin: () => globalThis.document?.baseURI ?? 'http://localhost', diff --git a/src/data/aisLiveVessels.test.mjs b/src/data/aisLiveVessels.test.mjs index 7beb2508b..b5a2c8de9 100644 --- a/src/data/aisLiveVessels.test.mjs +++ b/src/data/aisLiveVessels.test.mjs @@ -1668,7 +1668,7 @@ test('a vessel analyst record carries the MMSI the tracker keys on', () => { }); test('vessel selection passes the opaque source reference to optional history', async () => { - const { createAisStreamSource } = await import('../sources/live/standalone.js'); + const { createVesselSource } = await import('../sources/live/standalone.js'); _setVesselStateForTest({ enabled: false }); const requests = []; aisLiveVesselsLayer.setSource({ @@ -1688,6 +1688,6 @@ test('vessel selection passes the opaque source reference to optional history', } finally { harness.cleanup(); _setVesselStateForTest({ enabled: false }); - aisLiveVesselsLayer.setSource(createAisStreamSource()); + aisLiveVesselsLayer.setSource(createVesselSource()); } }); diff --git a/src/data/aisWatchdog.js b/src/data/aisWatchdog.js index 511644482..6d88f93f8 100644 --- a/src/data/aisWatchdog.js +++ b/src/data/aisWatchdog.js @@ -450,7 +450,7 @@ export function createAisWatchdog(options = {}) { return generation; } - /** Status metadata for /api/ais-live. */ + /** Status metadata for /api/vessels. */ function snapshot() { const monoNow = clock.mono(); const wallNow = clock.wall(); diff --git a/src/data/flights.js b/src/data/flights.js index 0b4d38d3b..6e2d35dbc 100644 --- a/src/data/flights.js +++ b/src/data/flights.js @@ -1,12 +1,12 @@ import { defaultSurface } from './surfaceServices.js'; import { createApplicationFlights } from '../app/layers/flights.js'; -import { createOpenSkySource } from '../sources/live/standalone.js'; +import { createFlightSource } from '../sources/live/standalone.js'; import * as militaryRegistry from './militaryRegistry.js'; const flightsLayer = createApplicationFlights({ surface: defaultSurface, - source: createOpenSkySource(), + source: createFlightSource(), militaryRegistry, }); export { TRACKED_MODEL_MAX_PX } from '../layers/flights/policy.js'; diff --git a/src/data/flights.test.mjs b/src/data/flights.test.mjs index 2834e8833..abb047384 100644 --- a/src/data/flights.test.mjs +++ b/src/data/flights.test.mjs @@ -203,7 +203,7 @@ test('flights poll refreshes tracked callsign/FL/kts and marks a missed poll STA const nowSec = Math.floor(Date.now() / 1000); let openskyPoll = 0; globalThis.fetch = async (url) => { - if (!String(url).startsWith('/api/opensky')) { + if (!String(url).startsWith('/api/flights')) { return { ok: true, status: 200, json: async () => ({ ac: [] }) }; } const states = openskyPoll++ === 0 diff --git a/src/data/militaryAwareness.test.mjs b/src/data/militaryAwareness.test.mjs index e4deae82e..8db51af7a 100644 --- a/src/data/militaryAwareness.test.mjs +++ b/src/data/militaryAwareness.test.mjs @@ -476,7 +476,7 @@ test('pending mapped installations render as unknown instead of a false zero', a // the dependencies reach it by different routes: // // - ais-live-vessels is its REACHABLE producer. enable()/update() both resolve -// as soon as the first /api/ais-live poll answers, so the manager's +// as soon as the first /api/vessels poll answers, so the manager's // lifecycle settles to `enabled` — but until the server-side socket delivers // a position, firstConnectPhase stays 'loading' and the module reports busy, // no lastUpdate, count 0, and an UNDEFINED status, so the status list alone diff --git a/src/data/militaryFlights.js b/src/data/militaryFlights.js index 12119ed6c..db8276b02 100644 --- a/src/data/militaryFlights.js +++ b/src/data/militaryFlights.js @@ -1,12 +1,12 @@ import { defaultSurface } from './surfaceServices.js'; import { createApplicationMilitary } from '../app/layers/militaryFlights.js'; -import { createAdsbLolSource } from '../sources/live/standalone.js'; +import { createMilitarySource } from '../sources/live/standalone.js'; import * as militaryRegistry from './militaryRegistry.js'; const militaryFlightsLayer = createApplicationMilitary({ surface: defaultSurface, - source: createAdsbLolSource(), + source: createMilitarySource(), militaryRegistry, }); export { TRACKED_MODEL_MAX_PX } from '../layers/military/policy.js'; diff --git a/src/data/militaryFlights.test.mjs b/src/data/militaryFlights.test.mjs index 122b6b132..597211bb7 100644 --- a/src/data/militaryFlights.test.mjs +++ b/src/data/militaryFlights.test.mjs @@ -24,8 +24,12 @@ test('share-Follow absence requires an accepted adsb.lol snapshot', async () => (await militaryFlightsLayer.resolveTrackingRestoreTarget('ae1234')).status, 'source-unavailable', ); - _setMilitaryTrackingRefreshOutcomeForTest({ status: 'accepted', ids: ['different'] }); - const missing = await militaryFlightsLayer.resolveTrackingRestoreTarget('ae1234'); + _setMilitaryTrackingRefreshOutcomeForTest({ + status: 'accepted', + ids: ['different'], + }); + const missing = + await militaryFlightsLayer.resolveTrackingRestoreTarget('ae1234'); assert.equal(missing.status, 'missing'); assert.equal(missing.reason, 'target-absent-from-snapshot'); }); @@ -44,9 +48,9 @@ const FULL_INFO = { operator: 'United States Air Force', }; -test('military stats identify adsb.lol as the primary feed, not a fallback', () => { +test('military stats name the feed until the server names its provider, not a fallback', () => { const stats = militaryFlightsLayer.getStats(); - assert.equal(stats.source, 'adsb.lol'); + assert.equal(stats.source, 'Military aircraft'); assert.equal(stats.fallback, false); }); @@ -81,8 +85,14 @@ test('military analyst record: military is ALWAYS true, routes/country always nu }); test('military analyst record: no callsign falls back to registration, then icao24', () => { - assert.equal(mapAnalystRecord('ae01ce', { ...FULL_INFO, callsign: '' }).id, '05-8152'); - assert.equal(mapAnalystRecord('ae01ce', { callsign: '', registration: ' ' }).id, 'ae01ce'); + assert.equal( + mapAnalystRecord('ae01ce', { ...FULL_INFO, callsign: '' }).id, + '05-8152', + ); + assert.equal( + mapAnalystRecord('ae01ce', { callsign: '', registration: ' ' }).id, + 'ae01ce', + ); }); test('military analyst record: empty info yields nulls, never NaN/undefined', () => { @@ -91,7 +101,8 @@ test('military analyst record: empty info yields nulls, never NaN/undefined', () assert.equal(r.onGround, false); for (const [key, value] of Object.entries(r)) { assert.notEqual(value, undefined, `${key} must not be undefined`); - if (typeof value === 'number') assert.ok(Number.isFinite(value), `${key} must not be NaN`); + if (typeof value === 'number') + assert.ok(Number.isFinite(value), `${key} must not be NaN`); } }); @@ -103,12 +114,17 @@ test('military analyst record: output is JSON-safe (no Cesium types)', () => { test('military first update forwards caller cancellation into the feed request', async () => { const realFetch = globalThis.fetch; let observedSignal = null; - globalThis.fetch = (_url, options = {}) => new Promise((_resolve, reject) => { - observedSignal = options.signal; - options.signal?.addEventListener('abort', () => { - reject(new DOMException('aborted', 'AbortError')); - }, { once: true }); - }); + globalThis.fetch = (_url, options = {}) => + new Promise((_resolve, reject) => { + observedSignal = options.signal; + options.signal?.addEventListener( + 'abort', + () => { + reject(new DOMException('aborted', 'AbortError')); + }, + { once: true }, + ); + }); try { const controller = new AbortController(); const work = militaryFlightsLayer.update({}, { signal: controller.signal }); @@ -138,11 +154,22 @@ test('nonempty adsb.lol payload with zero usable rows cannot prove share target json: async () => ({ ac: [null, {}, { hex: 'ae1234' }] }), }); try { - await militaryFlightsLayer.update({ camera: { positionCartographic: null }, scene: {} }); - const resolution = await militaryFlightsLayer.resolveTrackingRestoreTarget('ae1234'); + await militaryFlightsLayer.update({ + camera: { positionCartographic: null }, + scene: {}, + }); + const resolution = + await militaryFlightsLayer.resolveTrackingRestoreTarget('ae1234'); assert.equal(resolution.status, 'source-unavailable'); - assert.match(militaryFlightsLayer.getStats().error, /Malformed adsb\.lol aircraft rows/); - assert.equal(militaryFlightsLayer.getAnalystRecords().length, 1, 'warm aircraft data is preserved'); + assert.match( + militaryFlightsLayer.getStats().error, + /Malformed adsb\.lol aircraft rows/, + ); + assert.equal( + militaryFlightsLayer.getAnalystRecords().length, + 1, + 'warm aircraft data is preserved', + ); } finally { globalThis.fetch = realFetch; } @@ -188,21 +215,26 @@ test('military poll refreshes tracked callsign/altitude/kts and marks a missed p ok: true, status: 200, json: async () => ({ - ac: poll++ === 0 ? [{ - hex: icao24, - lon: -96.9, - lat: 31.1, - alt_baro: 28_000, - alt_geom: 28_100, - track: 95, - gs: 400, - seen: 0, - seen_pos: 0, - flight: 'RCH451 ', - t: 'C17', - r: '05-8152', - ownOp: 'United States Air Force', - }] : [], + ac: + poll++ === 0 + ? [ + { + hex: icao24, + lon: -96.9, + lat: 31.1, + alt_baro: 28_000, + alt_geom: 28_100, + track: 95, + gs: 400, + seen: 0, + seen_pos: 0, + flight: 'RCH451 ', + t: 'C17', + r: '05-8152', + ownOp: 'United States Air Force', + }, + ] + : [], }), }); @@ -227,7 +259,10 @@ test('cached military snapshots retain their observation time and expose stale f _setTrackedMilitaryRefreshStateForTest({ icao24: 'ae01ce', entity: { gevLabelModel: { title: 'RCH451', details: [] } }, - billboard: { position: Cesium.Cartesian3.fromDegrees(-97, 31, 8000), show: false }, + billboard: { + position: Cesium.Cartesian3.fromDegrees(-97, 31, 8000), + show: false, + }, billboardCollection: { show: false, remove() {} }, viewer, history, @@ -235,21 +270,48 @@ test('cached military snapshots retain their observation time and expose stale f }); let cache = 'MISS'; t.mock.method(Date, 'now', () => now); - t.mock.method(globalThis, 'fetch', async () => Response.json({ - ac: [{ hex: 'ae01ce', lon: -97, lat: 31, alt_baro: 28000, - track: 95, gs: 400, seen: 1, seen_pos: 2, flight: 'RCH451' }], - }, { headers: { - 'X-ADS-B-Cache': cache, - ...(cache === 'MISS' ? {} : { 'X-ADS-B-Cache-Age-Ms': String(now - receivedAt) }), - } })); + t.mock.method(globalThis, 'fetch', async () => + Response.json( + { + ac: [ + { + hex: 'ae01ce', + lon: -97, + lat: 31, + alt_baro: 28000, + track: 95, + gs: 400, + seen: 1, + seen_pos: 2, + flight: 'RCH451', + }, + ], + }, + { + headers: { + 'X-Feed-Cache': cache, + ...(cache === 'MISS' + ? {} + : { 'X-Feed-Age-Ms': String(now - receivedAt) }), + }, + }, + ), + ); await militaryFlightsLayer.update(viewer); assert.equal(history.length, 1); assert.equal(history[0].epochMs, receivedAt - 2000); - for (const [status, age] of [['HIT', 5000], ['STALE', 45000]]) { + for (const [status, age] of [ + ['HIT', 5000], + ['STALE', 45000], + ]) { cache = status; now = receivedAt + age; await militaryFlightsLayer.update(viewer); - assert.equal(history.length, 1, 'replayed positions cannot create a new fix'); + assert.equal( + history.length, + 1, + 'replayed positions cannot create a new fix', + ); assert.equal(militaryFlightsLayer.getStats().lastUpdate, receivedAt); assert.equal(militaryFlightsLayer.getStats().stale, status === 'STALE'); assert.equal(militaryFlightsLayer.getStats().error, null); @@ -276,14 +338,18 @@ test('real military track path creates no native label and publishes every cache trackedEntityChanged, clock: { currentTime: now }, camera: { - cancelFlight() { cancelledFlights += 1; }, + cancelFlight() { + cancelledFlights += 1; + }, position: new Cesium.Cartesian3(0, -10_000, 7_000), positionWC: new Cesium.Cartesian3(0, -10_000, 7_000), direction: Cesium.Cartesian3.UNIT_Y, transform: Cesium.Matrix4.IDENTITY, viewMatrix: Cesium.Matrix4.IDENTITY, frustum: { projectionMatrix: Cesium.Matrix4.IDENTITY }, - lookAtTransform() { appliedFrames += 1; }, + lookAtTransform() { + appliedFrames += 1; + }, }, scene: { canvas: { clientWidth: 1600, clientHeight: 900 }, @@ -322,9 +388,12 @@ test('real military track path creates no native label and publishes every cache const realFetch = globalThis.fetch; globalThis.window = new EventTarget(); const selectionEvents = []; - globalThis.window.addEventListener('gev:awareness-subject-selected', (event) => { - selectionEvents.push(event.detail); - }); + globalThis.window.addEventListener( + 'gev:awareness-subject-selected', + (event) => { + selectionEvents.push(event.detail); + }, + ); globalThis.fetch = async () => ({ ok: false }); _setTrackedOverlayHostForTest(host); try { @@ -336,13 +405,15 @@ test('real military track path creates no native label and publishes every cache billboardCollection: { show: true, remove() {} }, viewer, tracked: false, - history: [{ - time: now, - epochMs: Date.now(), - position, - velocity: 231.5, - track: 92.1, - }], + history: [ + { + time: now, + epochMs: Date.now(), + position, + velocity: 231.5, + track: 92.1, + }, + ], meta: { ...FULL_INFO, type: 'C17', @@ -352,34 +423,72 @@ test('real military track path creates no native label and publishes every cache }, }); - assert.equal(militaryFlightsLayer.trackById(icao24, { origin: 'programmatic' }), true); + assert.equal( + militaryFlightsLayer.trackById(icao24, { origin: 'programmatic' }), + true, + ); const entity = viewer.trackedEntity; - assert.ok(entity instanceof Cesium.Entity, 'trackById must create the real Cesium entity'); + assert.ok( + entity instanceof Cesium.Entity, + 'trackById must create the real Cesium entity', + ); assert.equal(entity.label, undefined); - assert.ok(entities.values.every((candidate) => candidate.label === undefined)); + assert.ok( + entities.values.every((candidate) => candidate.label === undefined), + ); assert.deepEqual(entity.gevLabelModel, { title: 'RCH451', - details: [ - 'C17 · 05-8152', - 'United States Air Force · 28000 ft · 450 kt', - ], + details: ['C17 · 05-8152', 'United States Air Force · 28000 ft · 450 kt'], accent: '#ffd166', }); viewer.scene.preUpdate.raiseEvent(); const initialAppliedFrames = appliedFrames; const initialCancelledFlights = cancelledFlights; - assert.equal(militaryFlightsLayer.trackById(icao24, { origin: 'user' }), true); - assert.equal(cancelledFlights, initialCancelledFlights, 'ordinary repeated tracking stays camera-idempotent'); - assert.equal(selectionEvents.at(-1)?.origin, 'user', 'same-target selection upgrades durable authority'); + assert.equal( + militaryFlightsLayer.trackById(icao24, { origin: 'user' }), + true, + ); + assert.equal( + cancelledFlights, + initialCancelledFlights, + 'ordinary repeated tracking stays camera-idempotent', + ); + assert.equal( + selectionEvents.at(-1)?.origin, + 'user', + 'same-target selection upgrades durable authority', + ); assert.equal(entity.gevSelectionOrigin, 'user'); - assert.equal(militaryFlightsLayer.refocusTrackedById('different-flight'), false); - assert.equal(militaryFlightsLayer.refocusTrackedById(icao24, { origin: 'voice' }), true); - assert.equal(selectionEvents.at(-1)?.origin, 'voice', 'same-target refocus forwards explicit authority'); + assert.equal( + militaryFlightsLayer.refocusTrackedById('different-flight'), + false, + ); + assert.equal( + militaryFlightsLayer.refocusTrackedById(icao24, { origin: 'voice' }), + true, + ); + assert.equal( + selectionEvents.at(-1)?.origin, + 'voice', + 'same-target refocus forwards explicit authority', + ); assert.equal(militaryFlightsLayer.refocusTrackedById(icao24), true); viewer.scene.preUpdate.raiseEvent(); - assert.equal(viewer.trackedEntity, entity, 'refocus must retain the exact tracked entity'); - assert.equal(appliedFrames, initialAppliedFrames + 1, 'repeated refocus keeps one camera-frame owner'); - assert.equal(cancelledFlights, initialCancelledFlights + 2, 'only explicit refocus requests cancel camera flights'); + assert.equal( + viewer.trackedEntity, + entity, + 'refocus must retain the exact tracked entity', + ); + assert.equal( + appliedFrames, + initialAppliedFrames + 1, + 'repeated refocus keeps one camera-frame owner', + ); + assert.equal( + cancelledFlights, + initialCancelledFlights + 2, + 'only explicit refocus requests cancel camera flights', + ); const publication = publications.at(-1); assert.equal(publication.sourceId, 'tracked'); const entry = publication.entries[0]; @@ -390,23 +499,40 @@ test('real military track path creates no native label and publishes every cache const display = entity.position.getValue(now); assert.ok(display, 'tracked position callback must seed its frame cache'); - assert.equal(entry.position(), display, 'host must reuse the exact cached display Cartesian'); + assert.equal( + entry.position(), + display, + 'host must reuse the exact cached display Cartesian', + ); militaryFlightsLayer.setParams( { selectedMilitaryTrackingId: 'late001' }, { origin: 'share-restore' }, ); assert.equal(_pendingMilitaryTrackingRestoreForTest(), 'late001'); - assert.equal(militaryFlightsLayer.trackById(icao24, { origin: 'user' }), true); - assert.equal(_pendingMilitaryTrackingRestoreForTest(), null, 'new user selection cancels stale restore'); + assert.equal( + militaryFlightsLayer.trackById(icao24, { origin: 'user' }), + true, + ); + assert.equal( + _pendingMilitaryTrackingRestoreForTest(), + null, + 'new user selection cancels stale restore', + ); _addMilitaryTrackingCandidateForTest({ icao24: 'late001', - billboard: { ...billboard, position: Cesium.Cartesian3.fromDegrees(-96.9, 31.1, 8_000) }, + billboard: { + ...billboard, + position: Cesium.Cartesian3.fromDegrees(-96.9, 31.1, 8_000), + }, meta: { ...FULL_INFO, callsign: 'LATE1', rawLat: 31.1, rawLon: -96.9 }, history: [], }); _applyPendingMilitaryTrackingRestoreForTest(); - assert.equal(militaryFlightsLayer.getParams().selectedMilitaryTrackingId, icao24); + assert.equal( + militaryFlightsLayer.getParams().selectedMilitaryTrackingId, + icao24, + ); militaryFlightsLayer.stopTracking(); militaryFlightsLayer.setParams( @@ -415,15 +541,25 @@ test('real military track path creates no native label and publishes every cache ); assert.equal(_pendingMilitaryTrackingRestoreForTest(), 'late002'); militaryFlightsLayer.stopTracking({ origin: 'user' }); - assert.equal(_pendingMilitaryTrackingRestoreForTest(), null, 'explicit clear cancels stale restore'); + assert.equal( + _pendingMilitaryTrackingRestoreForTest(), + null, + 'explicit clear cancels stale restore', + ); _addMilitaryTrackingCandidateForTest({ icao24: 'late002', - billboard: { ...billboard, position: Cesium.Cartesian3.fromDegrees(-96.8, 31.2, 7_000) }, + billboard: { + ...billboard, + position: Cesium.Cartesian3.fromDegrees(-96.8, 31.2, 7_000), + }, meta: { ...FULL_INFO, callsign: 'LATE2', rawLat: 31.2, rawLon: -96.8 }, history: [], }); _applyPendingMilitaryTrackingRestoreForTest(); - assert.equal(militaryFlightsLayer.getParams().selectedMilitaryTrackingId, null); + assert.equal( + militaryFlightsLayer.getParams().selectedMilitaryTrackingId, + null, + ); militaryFlightsLayer.setParams( { selectedMilitaryTrackingId: 'late003' }, @@ -438,12 +574,18 @@ test('real military track path creates no native label and publishes every cache ); _addMilitaryTrackingCandidateForTest({ icao24: 'late003', - billboard: { ...billboard, position: Cesium.Cartesian3.fromDegrees(-97.3, 30.6, 7_000) }, + billboard: { + ...billboard, + position: Cesium.Cartesian3.fromDegrees(-97.3, 30.6, 7_000), + }, meta: { ...FULL_INFO, callsign: 'LATE3', rawLat: 30.6, rawLon: -97.3 }, history: [], }); _applyPendingMilitaryTrackingRestoreForTest(); - assert.equal(militaryFlightsLayer.getParams().selectedMilitaryTrackingId, null); + assert.equal( + militaryFlightsLayer.getParams().selectedMilitaryTrackingId, + null, + ); militaryFlightsLayer.setParams( { selectedMilitaryTrackingId: 'late004' }, @@ -452,18 +594,27 @@ test('real military track path creates no native label and publishes every cache assert.equal(_pendingMilitaryTrackingRestoreForTest(), 'late004'); _addMilitaryTrackingCandidateForTest({ icao24: 'late004', - billboard: { ...billboard, position: Cesium.Cartesian3.fromDegrees(-97.2, 30.7, 6_000) }, + billboard: { + ...billboard, + position: Cesium.Cartesian3.fromDegrees(-97.2, 30.7, 6_000), + }, meta: { ...FULL_INFO, callsign: 'LATE4', rawLat: 30.7, rawLon: -97.2 }, history: [], }); assert.equal(_applyPendingMilitaryTrackingRestoreForTest(), true); - assert.equal(militaryFlightsLayer.getParams().selectedMilitaryTrackingId, 'late004'); + assert.equal( + militaryFlightsLayer.getParams().selectedMilitaryTrackingId, + 'late004', + ); militaryFlightsLayer.setParams( { selectedMilitaryTrackingId: 'late005' }, { origin: 'local-restore' }, ); - assert.equal(militaryFlightsLayer.trackById(icao24, { origin: 'programmatic' }), true); + assert.equal( + militaryFlightsLayer.trackById(icao24, { origin: 'programmatic' }), + true, + ); assert.equal( _pendingMilitaryTrackingRestoreForTest(), 'late005', @@ -471,12 +622,18 @@ test('real military track path creates no native label and publishes every cache ); _addMilitaryTrackingCandidateForTest({ icao24: 'late005', - billboard: { ...billboard, position: Cesium.Cartesian3.fromDegrees(-96.6, 31.4, 5_000) }, + billboard: { + ...billboard, + position: Cesium.Cartesian3.fromDegrees(-96.6, 31.4, 5_000), + }, meta: { ...FULL_INFO, callsign: 'LATE5', rawLat: 31.4, rawLon: -96.6 }, history: [], }); assert.equal(_applyPendingMilitaryTrackingRestoreForTest(), true); - assert.equal(militaryFlightsLayer.getParams().selectedMilitaryTrackingId, 'late005'); + assert.equal( + militaryFlightsLayer.getParams().selectedMilitaryTrackingId, + 'late005', + ); } finally { militaryFlightsLayer.stopTracking(); viewer.scene.preUpdate.raiseEvent(); @@ -521,21 +678,47 @@ test('a callsign-less contact is findable by the tail number the app displays', // The identity analyst_query hands back for this contact. const analystId = mapAnalystRecord(icao24, { - callsign: null, registration: '6606', rawLat: 40.71, rawLon: -74.01, + callsign: null, + registration: '6606', + rawLat: 40.71, + rawLon: -74.01, }).id; - assert.equal(analystId, '6606', 'the analyst names a callsign-less contact by its tail'); + assert.equal( + analystId, + '6606', + 'the analyst names a callsign-less contact by its tail', + ); const found = militaryFlightsLayer.findByQuery(analystId); - assert.equal(found?.icao24, icao24, 'and the tracker must resolve that same identity'); - assert.equal(militaryFlightsLayer.findByQuery('660')?.icao24, icao24, 'prefix match too'); - assert.equal(militaryFlightsLayer.findByQuery(icao24)?.icao24, icao24, 'hex still wins outright'); + assert.equal( + found?.icao24, + icao24, + 'and the tracker must resolve that same identity', + ); + assert.equal( + militaryFlightsLayer.findByQuery('660')?.icao24, + icao24, + 'prefix match too', + ); + assert.equal( + militaryFlightsLayer.findByQuery(icao24)?.icao24, + icao24, + 'hex still wins outright', + ); }); test('the analyst record carries the hex key the tracker keys on', () => { const record = mapAnalystRecord('ae7f01', { - callsign: null, registration: '6606', rawLat: 40.71, rawLon: -74.01, + callsign: null, + registration: '6606', + rawLat: 40.71, + rawLon: -74.01, }); - assert.equal(record.icao24, 'ae7f01', 'a resolvable key must ride along with the display label'); + assert.equal( + record.icao24, + 'ae7f01', + 'a resolvable key must ride along with the display label', + ); }); test('a registration never outranks another contact’s exact callsign', () => { @@ -548,18 +731,35 @@ test('a registration never outranks another contact’s exact callsign', () => { show: true, }); const base = { - type: 'C17', operator: 'USAF', altitudeFt: 25000, speedMps: 180, - track: 90, klass: 'widebody', onGround: false, + type: 'C17', + operator: 'USAF', + altitudeFt: 25000, + speedMps: 180, + track: 90, + klass: 'widebody', + onGround: false, }; // Registered FIRST, so insertion order favours it. _addMilitaryTrackingCandidateForTest({ icao24: 'ae0aa1', - meta: { ...base, callsign: null, registration: 'ZULU777', rawLat: 31.0, rawLon: -97.0 }, + meta: { + ...base, + callsign: null, + registration: 'ZULU777', + rawLat: 31.0, + rawLon: -97.0, + }, billboard: billboardAt(-97.0, 31.0), }); _addMilitaryTrackingCandidateForTest({ icao24: 'ae0bb2', - meta: { ...base, callsign: 'ZULU777', registration: '05-9999', rawLat: 31.2, rawLon: -97.2 }, + meta: { + ...base, + callsign: 'ZULU777', + registration: '05-9999', + rawLat: 31.2, + rawLon: -97.2, + }, billboard: billboardAt(-97.2, 31.2), }); @@ -582,24 +782,49 @@ test('two contacts matching at the same strength resolve deterministically', () show: true, }); const base = { - type: 'C130', operator: 'USAF', altitudeFt: 21000, speedMps: 150, - track: 45, klass: 'turboprop', onGround: false, + type: 'C130', + operator: 'USAF', + altitudeFt: 21000, + speedMps: 150, + track: 45, + klass: 'turboprop', + onGround: false, }; // Both registrations begin with QQ7 — a same-tier prefix tie. Registered // highest-hex first so insertion order would pick the wrong one. _addMilitaryTrackingCandidateForTest({ icao24: 'aef002', - meta: { ...base, callsign: null, registration: 'QQ7-222', rawLat: 32.0, rawLon: -98.0 }, + meta: { + ...base, + callsign: null, + registration: 'QQ7-222', + rawLat: 32.0, + rawLon: -98.0, + }, billboard: billboardAt(-98.0, 32.0), }); _addMilitaryTrackingCandidateForTest({ icao24: 'aef001', - meta: { ...base, callsign: null, registration: 'QQ7-111', rawLat: 32.1, rawLon: -98.1 }, + meta: { + ...base, + callsign: null, + registration: 'QQ7-111', + rawLat: 32.1, + rawLon: -98.1, + }, billboard: billboardAt(-98.1, 32.1), }); const first = militaryFlightsLayer.findByQuery('QQ7')?.icao24; const second = militaryFlightsLayer.findByQuery('qq7')?.icao24; - assert.equal(first, 'aef001', 'the stable key (lowest hex) wins, not the feed order'); - assert.equal(second, first, 'and the same query resolves the same way every time'); + assert.equal( + first, + 'aef001', + 'the stable key (lowest hex) wins, not the feed order', + ); + assert.equal( + second, + first, + 'and the same query resolves the same way every time', + ); }); diff --git a/src/data/militaryRegistry.js b/src/data/militaryRegistry.js index f581c3ff0..f6e47fa56 100644 --- a/src/data/militaryRegistry.js +++ b/src/data/militaryRegistry.js @@ -1,7 +1,7 @@ import { createMilitaryRegistry } from '../layers/aircraft/classification.js'; -import { createAdsbLolSource } from '../sources/live/standalone.js'; +import { createMilitarySource } from '../sources/live/standalone.js'; -const registry = createMilitaryRegistry({ source: createAdsbLolSource() }); +const registry = createMilitaryRegistry({ source: createMilitarySource() }); export const isMilitaryLayerActive = registry.isMilitaryLayerActive; export const setMilitaryLayerActive = registry.setMilitaryLayerActive; export const onMilitaryLayerActiveChange = registry.onMilitaryLayerActiveChange; diff --git a/src/data/tr3bRegistry.test.mjs b/src/data/tr3bRegistry.test.mjs index 0fb5f3623..98fd534c4 100644 --- a/src/data/tr3bRegistry.test.mjs +++ b/src/data/tr3bRegistry.test.mjs @@ -189,7 +189,7 @@ test('a conversion survives a poll refresh, in both the billboard and the tracke const realFetch = globalThis.fetch; const nowSec = Math.floor(Date.now() / 1000); globalThis.fetch = async (url) => { - if (!String(url).startsWith('/api/opensky')) { + if (!String(url).startsWith('/api/flights')) { return { ok: true, status: 200, json: async () => ({ ac: [] }) }; } return { @@ -309,7 +309,7 @@ test('conversions are session-scoped and no lifecycle path clears them', async ( meta: { callsign: 'OLD1', altitude: 9_000, klass: 'airliner', rawLat: 30.2, rawLon: -97.7 }, }); const realFetch = globalThis.fetch; - globalThis.fetch = async (url) => (String(url).startsWith('/api/opensky') + globalThis.fetch = async (url) => (String(url).startsWith('/api/flights') ? { ok: true, status: 200, headers: { get: () => null }, json: async () => ({ time: 0, states: [] }) } : { ok: true, status: 200, json: async () => ({ ac: [] }) }); try { diff --git a/src/layers/awareness/queries.js b/src/layers/awareness/queries.js index a32f855ba..93bef3532 100644 --- a/src/layers/awareness/queries.js +++ b/src/layers/awareness/queries.js @@ -35,7 +35,7 @@ export function createQueries({ state: layerState, services, parts, source }) { // setInstallationStatus is only ever called with // loading/zoom-in/ready/stale/empty/unavailable. // - ais-live-vessels is what the predicate below is FOR. Its enable() and - // update() both resolve as soon as the first /api/ais-live poll answers, + // update() both resolve as soon as the first /api/vessels poll answers, // so the lifecycle settles to `enabled` — but until the server-side socket // delivers a position, firstConnectPhase is 'loading' and getStats() // reports loading: true, lastUpdate: null, count 0, and an UNDEFINED diff --git a/src/layers/vessels/queries.js b/src/layers/vessels/queries.js index 5544379fe..520af4b0b 100644 --- a/src/layers/vessels/queries.js +++ b/src/layers/vessels/queries.js @@ -36,7 +36,7 @@ export function createQueries({ /** * Chip text for a feed the server has reported as not delivering. * @param {string} status - 'stale' | 'reconnecting' | 'down' - * @param {Object} payload - Parsed /api/ais-live JSON. + * @param {Object} payload - Parsed /api/vessels JSON. * @returns {string} */ @@ -61,12 +61,12 @@ export function createQueries({ } /** - * Derive a surfaced error string from an /api/ais-live payload, or null when the + * Derive a surfaced error string from an /api/vessels payload, or null when the * feed has accepted product data. Socket transport, message receipt, and usable * vessel positions are separate health stages: an open socket with no message * or no accepted positions must not read as a fresh successful update. * - * @param {Object|null|undefined} payload - Parsed /api/ais-live JSON. + * @param {Object|null|undefined} payload - Parsed /api/vessels JSON. * @param {number} acceptedRowCount - Number of rows accepted by vessel normalization. * @returns {string|null} A short reason for the chip, or null if healthy. */ @@ -106,7 +106,7 @@ export function createQueries({ /** * Classify one server snapshot before any destructive reconciliation. - * @param {Object|null|undefined} payload - Parsed /api/ais-live payload. + * @param {Object|null|undefined} payload - Parsed /api/vessels payload. * @returns {{transportStatus: string|null, lastMessageAt: number|string|null, * rawRows: Array, acceptedRows: Array, rawRowCount: number, * acceptedRowCount: number, error: string|null}} @@ -140,7 +140,7 @@ export function createQueries({ * Map one internal vessel record to a plain JSON-safe analyst record * (analyst query engine seam). Pure — no Cesium types. Missing/unknown * fields are null, never NaN/undefined. navStatus is always null: the - * /api/ais-live proxy does not surface AIS NavigationalStatus, so it + * /api/vessels proxy does not surface AIS NavigationalStatus, so it * cannot be derived client-side. * @param {Object|null|undefined} record - `state.records.byMmsi`/`state.records.all` entry. * @returns {{id: string|null, mmsi: string|null, name: string|null, diff --git a/src/sources/live/contract.test.mjs b/src/sources/live/contract.test.mjs index 671a2e5e4..391fe1e72 100644 --- a/src/sources/live/contract.test.mjs +++ b/src/sources/live/contract.test.mjs @@ -1,9 +1,9 @@ import test from 'node:test'; import assert from 'node:assert/strict'; import { - createOpenSkySource, - createAdsbLolSource, - createAisStreamSource, + createFlightSource, + createMilitarySource, + createVesselSource, normalizeReadsbAircraft, normalizeVesselObservation, openSkySnapshot, @@ -133,7 +133,7 @@ test('construction is inert; adapters preserve routes, viewport query, cache epo const requests = []; const fetchImpl = async (url, init) => { requests.push({ url, init }); - if (url.startsWith('/api/opensky?')) + if (url.startsWith('/api/flights?')) return response( { time: now / 1000, states: [aircraft] }, { @@ -141,27 +141,27 @@ test('construction is inert; adapters preserve routes, viewport query, cache epo 'x-flight-coverage': '250 nm regional', }, ); - if (url.startsWith('/api/opensky-track')) + if (url.startsWith('/api/flights/track')) return response({ path: [[now / 1000 - 30, 30, -97, 1000, 45, false]] }); - if (url === '/api/adsblol/mil') + if (url === '/api/military') return response( { ac: [{ hex: 'abc123', lat: 30, lon: -97, seen_pos: 15 }] }, - { 'x-ads-b-cache-age-ms': '60000', 'x-ads-b-cache': 'STALE' }, + { 'x-feed-age-ms': '60000', 'x-feed-cache': 'STALE' }, ); return response({ timestamp: now / 1000 - 60, trace: [[10, 30, -97, 1000]], }); }; - const civil = createOpenSkySource({ fetchImpl, now: () => now }); - const military = createAdsbLolSource({ fetchImpl, now: () => now }); + const civil = createFlightSource({ fetchImpl, now: () => now }); + const military = createMilitarySource({ fetchImpl, now: () => now }); assert.equal(requests.length, 0); const signal = new AbortController().signal; const snapshot = await civil.getSnapshot( { latitude: 30.123456, longitude: -97 }, { signal }, ); - assert.equal(requests[0].url, '/api/opensky?lat=30.1235&lon=-97.0000'); + assert.equal(requests[0].url, '/api/flights?lat=30.1235&lon=-97.0000'); assert.equal(requests[0].init.signal, signal); assert.equal(snapshot.source, 'adsb.lol'); assert.equal(snapshot.coverage, '250 nm regional'); @@ -180,7 +180,7 @@ test('construction is inert; adapters preserve routes, viewport query, cache epo test('cancellation after slow body parsing rejects even with a transport that ignores abort', async () => { let release; const controller = new AbortController(); - const source = createOpenSkySource({ + const source = createFlightSource({ fetchImpl: async () => ({ ok: true, json: () => @@ -199,7 +199,7 @@ test('cancellation after slow body parsing rejects even with a transport that ig test('denials and outages never start another source and do not echo arbitrary response bodies', async () => { for (const status of [401, 403, 429, 502]) { let calls = 0; - const source = createOpenSkySource({ + const source = createFlightSource({ fetchImpl: async () => { calls++; return response({ error: 'sensitive upstream detail' }, {}, status); @@ -216,12 +216,12 @@ test('denials and outages never start another source and do not echo arbitrary r }); test('AIS reports limited received coverage and keeps connection state separate from positions', async () => { - const source = createAisStreamSource({ + const source = createVesselSource({ origin: () => 'http://example.test', fetchImpl: async (url) => { if (url.includes('/track?')) return response({ samples: [{ lat: 30, lon: -97, t: now / 1000 }] }); - assert.equal(url, 'http://example.test/api/ais-live?maxRows=500'); + assert.equal(url, 'http://example.test/api/vessels?maxRows=500'); return response({ status: 'reconnecting', refreshing: true, @@ -249,7 +249,7 @@ test('AIS reports limited received coverage and keeps connection state separate }); test('a malformed vessel row cannot prevent admission of valid positions', async () => { - const source = createAisStreamSource({ + const source = createVesselSource({ fetchImpl: async () => response({ status: 'live', @@ -273,7 +273,7 @@ test('out-of-range source epochs do not reach Date or globe time constructors', }); test('classification identities include positionless aircraft without admitting them to rendering', async () => { - const source = createAdsbLolSource({ + const source = createMilitarySource({ fetchImpl: async () => response({ ac: [ @@ -293,12 +293,12 @@ test('classification identities include positionless aircraft without admitting }); test('identity lookup validates its response and honors body-parse cancellation', async () => { - const malformed = createAdsbLolSource({ + const malformed = createMilitarySource({ fetchImpl: async () => response({ ac: [{}] }), }); await assert.rejects(malformed.getIdentities(), /Malformed/); const abort = new AbortController(); - const source = createAdsbLolSource({ + const source = createMilitarySource({ fetchImpl: async () => ({ ok: true, status: 200, diff --git a/src/sources/live/index.js b/src/sources/live/index.js index 73f492500..dcc4b5d91 100644 --- a/src/sources/live/index.js +++ b/src/sources/live/index.js @@ -13,7 +13,7 @@ export { vesselSnapshot, } from './vessels.js'; export { - createOpenSkySource, - createAdsbLolSource, - createAisStreamSource, + createFlightSource, + createMilitarySource, + createVesselSource, } from './standalone.js'; diff --git a/src/sources/live/standalone.js b/src/sources/live/standalone.js index 270a6d099..68ac5b6be 100644 --- a/src/sources/live/standalone.js +++ b/src/sources/live/standalone.js @@ -17,7 +17,7 @@ const defaultFetch = (...args) => globalThis.fetch(...args); const header = (response, name) => response.headers?.get?.(name); function openSkyError(response) { - const error = httpError(response, 'OpenSky'); + const error = httpError(response, 'Flights'); const mode = String( header(response, 'x-opensky-auth-mode-used') || header(response, 'x-opensky-auth') || @@ -49,13 +49,24 @@ function openSkyError(response) { return error; } -/** Existing same-origin aircraft routes; no request starts during construction. */ -export function createOpenSkySource({ +/** + * The provider a feed response names in `X-Feed-Source`, so a server that + * answers a feed route from another provider is reported as that provider. + */ +function feedSource(response, fallback) { + const named = header(response, 'x-feed-source'); + return typeof named === 'string' && named.trim() + ? named.trim().slice(0, 80) + : fallback; +} + +/** The flights feed (/api/flights); no request starts during construction. */ +export function createFlightSource({ fetchImpl = defaultFetch, now = () => Date.now(), } = {}) { return { - label: 'OpenSky Network', + label: 'Flights', async getSnapshot(query = {}, { signal } = {}) { const params = new URLSearchParams(); if (Number.isFinite(query.latitude) && Number.isFinite(query.longitude)) { @@ -64,14 +75,17 @@ export function createOpenSkySource({ } const { response, payload } = await readResponse( fetchImpl, - `/api/opensky${params.size ? '?' + params : ''}`, + `/api/flights${params.size ? '?' + params : ''}`, { signal }, - 'OpenSky', + 'Flights', ); if (!response.ok) throw openSkyError(response); return { ...openSkySnapshot(payload, { - source: header(response, 'x-flight-source') || 'OpenSky Network', + // A fallback provider names itself in X-Flight-Source. + source: + header(response, 'x-flight-source') || + feedSource(response, 'Flights'), coverage: header(response, 'x-flight-coverage') || 'worldwide upstream snapshot', @@ -83,11 +97,11 @@ export function createOpenSkySource({ async getTrack(reference, { signal } = {}) { const { response, payload } = await readResponse( fetchImpl, - '/api/opensky-track?icao24=' + encodeURIComponent(reference), + '/api/flights/track?icao24=' + encodeURIComponent(reference), { signal }, - 'OpenSky', + 'Flights', ); - if (!response.ok) throw httpError(response, 'OpenSky'); + if (!response.ok) throw httpError(response, 'Flights'); return { records: normalizeAircraftTrack(payload?.path), complete: false, @@ -108,36 +122,37 @@ export function createOpenSkySource({ }; } -export function createAdsbLolSource({ +export function createMilitarySource({ fetchImpl = defaultFetch, now = () => Date.now(), } = {}) { return { - label: 'adsb.lol', + label: 'Military aircraft', async getIdentities(_query = {}, { signal } = {}) { const { response, payload } = await readResponse( fetchImpl, - '/api/adsblol/mil', + '/api/military', { signal }, - 'adsb.lol', + 'Military aircraft', ); - if (!response.ok) throw httpError(response, 'adsb.lol'); + if (!response.ok) throw httpError(response, 'Military aircraft'); return readsbIdentities(payload); }, async getSnapshot(_query = {}, { signal } = {}) { const { response, payload } = await readResponse( fetchImpl, - '/api/adsblol/mil', + '/api/military', { signal }, - 'adsb.lol', + 'Military aircraft', ); - if (!response.ok) throw httpError(response, 'adsb.lol'); - const age = finite(header(response, 'x-ads-b-cache-age-ms')); + if (!response.ok) throw httpError(response, 'Military aircraft'); + const age = finite(header(response, 'x-feed-age-ms')); return { ...readsbSnapshot(payload, { + source: feedSource(response, 'Military aircraft'), observedAtMs: now() - (age != null && age > 0 ? age : 0), now: now(), - stale: header(response, 'x-ads-b-cache') === 'STALE', + stale: header(response, 'x-feed-cache') === 'STALE', }), status: response.status, }; @@ -145,11 +160,11 @@ export function createAdsbLolSource({ async getTrack(reference, { signal } = {}) { const { response, payload } = await readResponse( fetchImpl, - '/api/adsblol/trace?hex=' + encodeURIComponent(reference), + '/api/military/track?hex=' + encodeURIComponent(reference), { signal }, - 'adsb.lol', + 'Military aircraft', ); - if (!response.ok) throw httpError(response, 'adsb.lol'); + if (!response.ok) throw httpError(response, 'Military aircraft'); const baseTimeMs = epoch(payload?.timestamp, 1000); return { records: @@ -165,13 +180,13 @@ export function createAdsbLolSource({ }; } -export function createAisStreamSource({ +export function createVesselSource({ fetchImpl = defaultFetch, - apiUrl = '/api/ais-live', + apiUrl = '/api/vessels', origin = () => globalThis.location?.origin || 'http://localhost', } = {}) { return { - label: 'AISStream', + label: 'Vessels', async getSnapshot({ maxRows = 12000, area } = {}, { signal } = {}) { const url = new URL(apiUrl, origin()); url.searchParams.set('maxRows', String(maxRows)); @@ -190,10 +205,10 @@ export function createAisStreamSource({ fetchImpl, url.toString(), { signal, cache: 'no-store' }, - 'AIS live', + 'Vessels', ); if (!response.ok) { - const error = httpError(response, 'AIS live'); + const error = httpError(response, 'Vessels'); const reasons = { 'missing-key': 'AISSTREAM_API_KEY not set', 'auth-failed': 'API key rejected — check AISSTREAM_API_KEY', @@ -204,16 +219,19 @@ export function createAisStreamSource({ error.message = reasons[payload?.status] || error.message; throw error; } - return { ...vesselSnapshot(payload), status: response.status }; + return { + ...vesselSnapshot(payload, { source: feedSource(response, 'Vessels') }), + status: response.status, + }; }, async getTrack(reference, { signal } = {}) { const { response, payload } = await readResponse( fetchImpl, - '/api/ais-live/track?mmsi=' + encodeURIComponent(reference), + '/api/vessels/track?mmsi=' + encodeURIComponent(reference), { signal }, - 'AIS live', + 'Vessels', ); - if (!response.ok) throw httpError(response, 'AIS live'); + if (!response.ok) throw httpError(response, 'Vessels'); return { records: normalizeVesselTrack(payload?.samples), complete: false, diff --git a/src/standalone/layerSources.js b/src/standalone/layerSources.js index c84595483..de94d2fe0 100644 --- a/src/standalone/layerSources.js +++ b/src/standalone/layerSources.js @@ -1,8 +1,8 @@ import { createOpenFreeMapSource } from '../sources/openFreeMap.js'; import { - createOpenSkySource, - createAdsbLolSource, - createAisStreamSource, + createFlightSource, + createMilitarySource, + createVesselSource, } from '../sources/live/standalone.js'; import { createCctvSource } from '../layers/cctv/source.js'; import { createRadioSource } from '../layers/radio/source.js'; @@ -25,10 +25,10 @@ export function createStandaloneLayerSources() { const mapTiles = createOpenFreeMapSource(); return { ...createReferenceSources(), - flights: createOpenSkySource(), - military: createAdsbLolSource(), - vessels: createAisStreamSource({ - apiUrl: import.meta.env?.VITE_AIS_LIVE_API_URL || '/api/ais-live', + flights: createFlightSource(), + military: createMilitarySource(), + vessels: createVesselSource({ + apiUrl: import.meta.env?.VITE_AIS_LIVE_API_URL || '/api/vessels', // Resolved against the document's address, which a panel host may // serve from its own scheme. origin: () => globalThis.document?.baseURI ?? 'http://localhost', diff --git a/src/tooling/liveProviders.test.mjs b/src/tooling/liveProviders.test.mjs index ae136b994..d7b740668 100644 --- a/src/tooling/liveProviders.test.mjs +++ b/src/tooling/liveProviders.test.mjs @@ -93,11 +93,11 @@ test('OpenSky state and track routes share tokens, retain cache and use regional }); const states = install(providers.openSkyProxy()); assert.equal( - (await states('/api/opensky', '?lat=30&lon=-97')).statusCode, + (await states('/api/flights', '?lat=30&lon=-97')).statusCode, 200, ); assert.equal( - (await states('/api/opensky', '?lat=30&lon=-97')).headers[ + (await states('/api/flights', '?lat=30&lon=-97')).headers[ 'x-opensky-cache' ], 'HIT', @@ -105,14 +105,14 @@ test('OpenSky state and track routes share tokens, retain cache and use regional assert.equal(calls.length, 2); const tracks = install(providers.trackBackfillProxies(), true); assert.equal( - (await tracks('/api/opensky-track', '?icao24=ABC123')).statusCode, + (await tracks('/api/flights/track', '?icao24=ABC123')).statusCode, 200, ); - await tracks('/api/opensky-track', '?icao24=abc123'); + await tracks('/api/flights/track', '?icao24=abc123'); assert.equal(calls.filter((call) => call.url.includes('/token')).length, 1); assert.equal(calls.filter((call) => call.url.includes('/tracks/')).length, 1); assert.equal( - (await tracks('/api/adsblol/trace', '?hex=invalid')).statusCode, + (await tracks('/api/military/track', '?hex=invalid')).statusCode, 400, ); now += 130_000; @@ -131,7 +131,7 @@ test('OpenSky state and track routes share tokens, retain cache and use regional ); process.env.OPENSKY_AUTH_MODE = 'anon'; const fallback = await install(fresh.openSkyProxy())( - '/api/opensky', + '/api/flights', '?lat=30&lon=-97', ); assert.equal(fallback.statusCode, 200); @@ -174,7 +174,7 @@ test('OpenSky global snapshot attempts are time-limited, retried once, then fall `../../server/providers/aircraft/opensky.js?timeout=${Math.random()}` ); const response = await install(fresh.openSkyProxy())( - '/api/opensky', + '/api/flights', '?lat=30&lon=-97', ); return { response, attempts }; @@ -203,11 +203,11 @@ test('military aircraft route preserves fresh cache and stale response after ups return Response.json({ ac: [{ hex: 'abc123' }] }); }); const request = install(providers.adsbLolProxy()); - const first = await request('/api/adsblol/mil'); - assert.equal((await request('/api/adsblol/mil')).body, first.body); + const first = await request('/api/military'); + assert.equal((await request('/api/military')).body, first.body); assert.equal(calls, 1); now += 13_000; - assert.equal((await request('/api/adsblol/mil')).body, first.body); + assert.equal((await request('/api/military')).body, first.body); assert.equal(calls, 2); }); @@ -261,7 +261,7 @@ test('AIS preview route ingests through the socket, returns tracks and disposes const request = install(plugin, true); let res; for (let i = 0; i < 100; i++) { - res = await request('/api/ais-live'); + res = await request('/api/vessels'); if (JSON.parse(res.body).rows.length) break; await delay(10); } @@ -269,10 +269,10 @@ test('AIS preview route ingests through the socket, returns tracks and disposes assert.equal(data.status, 'live'); assert.equal(data.rows[0].mmsi, '123456789'); assert.equal(data.rows[0].heading, null); - const history = await request('/api/ais-live', '/track?mmsi=123456789'); + const history = await request('/api/vessels', '/track?mmsi=123456789'); assert.equal(JSON.parse(history.body).samples.length, 2); assert.equal( - (await request('/api/ais-live', '/track?mmsi=bad')).statusCode, + (await request('/api/vessels', '/track?mmsi=bad')).statusCode, 400, ); assert.equal(sockets.length, 1); @@ -280,7 +280,7 @@ test('AIS preview route ingests through the socket, returns tracks and disposes for (let i = 0; i < 100 && sockets[0].readyState !== 3; i++) await delay(10); assert.equal(sockets[0].readyState, 3); const restarted = install(plugin); - await restarted('/api/ais-live'); + await restarted('/api/vessels'); for (let i = 0; i < 100 && sockets.length < 2; i++) await delay(10); assert.equal(sockets.length, 2); }); @@ -299,10 +299,10 @@ test('military aircraft route serves stale cache on an upstream 429 and cools do }); }); const request = install(providers.adsbLolProxy()); - const first = await request('/api/adsblol/mil'); + const first = await request('/api/military'); assert.equal(first.statusCode, 200); now += 13_000; - const limited = await request('/api/adsblol/mil'); + const limited = await request('/api/military'); assert.equal(calls, 2); assert.equal( limited.statusCode, @@ -310,16 +310,16 @@ test('military aircraft route serves stale cache on an upstream 429 and cools do 'a 429 with a cached body is never relayed', ); assert.equal(limited.body, first.body); - assert.equal(limited.headers['x-ads-b-cache'], 'STALE'); + assert.equal(limited.headers['x-feed-cache'], 'STALE'); assert.equal(limited.headers['x-ads-b-upstream-status'], '429'); - assert.equal(limited.headers['x-ads-b-cache-age-ms'], '13000'); + assert.equal(limited.headers['x-feed-age-ms'], '13000'); now += 5_000; - const cooling = await request('/api/adsblol/mil'); + const cooling = await request('/api/military'); assert.equal(calls, 2, 'no upstream call inside the Retry-After window'); - assert.equal(cooling.headers['x-ads-b-cache'], 'STALE'); - assert.equal(cooling.headers['x-ads-b-cache-age-ms'], '18000'); + assert.equal(cooling.headers['x-feed-cache'], 'STALE'); + assert.equal(cooling.headers['x-feed-age-ms'], '18000'); now += 16_000; - await request('/api/adsblol/mil'); + await request('/api/military'); assert.equal(calls, 3, 'upstream is retried once Retry-After elapses'); }); @@ -331,10 +331,10 @@ test('military aircraft route relays an upstream 429 when nothing is cached', as return new Response('{"error":"rate limited"}', { status: 429 }); }); const request = install(providers.adsbLolProxy()); - const limited = await request('/api/adsblol/mil'); + const limited = await request('/api/military'); assert.equal(limited.statusCode, 429); assert.ok(limited.headers['retry-after']); - const again = await request('/api/adsblol/mil'); + const again = await request('/api/military'); assert.equal(again.statusCode, 429); assert.equal( calls, @@ -369,21 +369,21 @@ test('military fallback cancels a stalled 5xx body and starts cooldown at receip ); }); const request = install(providers.adsbLolProxy()); - await request('/api/adsblol/mil'); + await request('/api/military'); now += 1000; - const hit = await request('/api/adsblol/mil'); - assert.equal(hit.headers['x-ads-b-cache-age-ms'], '1000'); + const hit = await request('/api/military'); + assert.equal(hit.headers['x-feed-age-ms'], '1000'); now += 12000; - const fallback = await request('/api/adsblol/mil'); + const fallback = await request('/api/military'); assert.equal(fallback.statusCode, 200); assert.equal(cancelled, true); - assert.equal(fallback.headers['x-ads-b-cache-age-ms'], '23000'); + assert.equal(fallback.headers['x-feed-age-ms'], '23000'); assert.equal(fallback.headers['x-ads-b-retry-after-seconds'], '20'); now += 19000; - await request('/api/adsblol/mil'); + await request('/api/military'); assert.equal(calls, 2); now += 2000; - await request('/api/adsblol/mil'); + await request('/api/military'); assert.equal(calls, 3); }); @@ -403,7 +403,7 @@ test('military cooldown bounds untrusted Retry-After and defaults server errors' headers: { 'Retry-After': raw }, }), ); - const result = await install(providers.adsbLolProxy())('/api/adsblol/mil'); + const result = await install(providers.adsbLolProxy())('/api/military'); assert.equal(Number(result.headers['retry-after']), seconds); } }); @@ -423,7 +423,7 @@ test('track backfill proxy returns 502 on an oversized upstream body and caches }; }); - const res1 = await tracks('/api/opensky-track', '?icao24=def456'); + const res1 = await tracks('/api/flights/track', '?icao24=def456'); assert.equal(res1.statusCode, 502); assert.deepEqual(JSON.parse(res1.body), { error: 'Upstream track response too large', @@ -431,7 +431,7 @@ test('track backfill proxy returns 502 on an oversized upstream body and caches // Cached as a 502 (never a 200): a retry inside the window neither // reads as an empty track nor spends OpenSky credits on another download. - const res2 = await tracks('/api/opensky-track', '?icao24=def456'); + const res2 = await tracks('/api/flights/track', '?icao24=def456'); assert.equal(res2.statusCode, 502); assert.equal(callCount, 1); }); diff --git a/src/tooling/previewServing.test.mjs b/src/tooling/previewServing.test.mjs index 7926c588b..51c143164 100644 --- a/src/tooling/previewServing.test.mjs +++ b/src/tooling/previewServing.test.mjs @@ -97,8 +97,8 @@ test('real dev and built-preview servers serve provider JSON and terminate unkno const origin = `http://127.0.0.1:${server.httpServer.address().port}`; try { for (const [route, status] of [ - ['/api/opensky', 200], - ['/api/adsblol/mil', 200], + ['/api/flights', 200], + ['/api/military', 200], ['/api/adsbdb/type/invalid', 400], ['/api/firms/status', 200], ['/api/fire-perimeters/inciweb/publication/invalid', 400], diff --git a/src/tools/queries/aviation.test.mjs b/src/tools/queries/aviation.test.mjs index ca800f8e9..20ec833aa 100644 --- a/src/tools/queries/aviation.test.mjs +++ b/src/tools/queries/aviation.test.mjs @@ -373,10 +373,10 @@ test('aircraft searches say when a feed that answered is stale', async () => { }); test('the real OpenSky source marks an hour-old snapshot stale for searches', async () => { - const { createOpenSkySource } = + const { createFlightSource } = await import('../../sources/live/standalone.js'); const now = Date.UTC(2026, 0, 1, 12); - const opensky = createOpenSkySource({ + const opensky = createFlightSource({ now: () => now, fetchImpl: async () => Response.json({ time: now / 1000 - 3600, states: [] }), diff --git a/src/tools/queries/maritime.test.mjs b/src/tools/queries/maritime.test.mjs index 5675ebf36..785e38223 100644 --- a/src/tools/queries/maritime.test.mjs +++ b/src/tools/queries/maritime.test.mjs @@ -2,7 +2,7 @@ import assert from 'node:assert/strict'; import test from 'node:test'; import { composeCatalog, coreTools } from '../index.js'; import { LiveSourceError } from '../../sources/live/contract.js'; -import { createAisStreamSource } from '../../sources/live/standalone.js'; +import { createVesselSource } from '../../sources/live/standalone.js'; import { AISSTREAM_CACHE_MAX } from '../../../server/providers/vessels/ais-store.js'; const vessel = (id, latitude, longitude, extra = {}) => ({ @@ -184,7 +184,7 @@ test('a missing AIS key surfaces as an unavailable feed', async () => { test('vessel searches ask for every vessel the server retains', async () => { const requested = []; - const source = createAisStreamSource({ + const source = createVesselSource({ origin: () => 'http://localhost', fetchImpl: async (url) => { requested.push(new URL(url).searchParams.get('maxRows')); @@ -204,7 +204,7 @@ test('vessel searches ask for every vessel the server retains', async () => { test('an area search names its area to the server; a vessel lookup asks for every vessel', async () => { const requested = []; - const source = createAisStreamSource({ + const source = createVesselSource({ origin: () => 'http://localhost', fetchImpl: async (url) => { requested.push(new URL(url).searchParams); diff --git a/src/tools/services.js b/src/tools/services.js index 961946f98..b0edf9f7d 100644 --- a/src/tools/services.js +++ b/src/tools/services.js @@ -31,9 +31,9 @@ import { createApplicationRequestServices } from '../services/requests.js'; import { createEventPackSource } from '../sources/eventPacks.js'; import { createInfrastructureSource } from '../sources/infrastructureData.js'; import { - createAdsbLolSource, - createAisStreamSource, - createOpenSkySource, + createMilitarySource, + createVesselSource, + createFlightSource, } from '../sources/live/standalone.js'; import { createGeocodePlaceService, @@ -51,9 +51,9 @@ export function createToolServices({ fetchImpl, appUrl, panelKey }) { earthquakes: createUsgsEarthquakeSource({ fetchImpl }), fires: createFirmsSource({ fetchImpl }), launches: createLaunchSource({ fetchImpl }), - aircraft: createOpenSkySource({ fetchImpl }), - military: createAdsbLolSource({ fetchImpl }), - vessels: createAisStreamSource({ + aircraft: createFlightSource({ fetchImpl }), + military: createMilitarySource({ fetchImpl }), + vessels: createVesselSource({ fetchImpl, origin: () => new URL(appUrl).origin, }),