Merge upstream/main into pr/mapillary-layer

Upstream took share token '2' for fire perimeters, so Street Level moves to
'3'. mapillary-js joins upstream's optimizeDeps include list instead of
overriding it; catalog and registry tests count the new upstream layers.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
Said TurkseverandClaude Fable 5.1 committed 2026-09-25 17:58:27 +01:00
commit 9ba6f96d87
204 files changed
+24525 -314

No files matched your search

+237 -25
View File
@@ -1,5 +1,42 @@
# God's Eye View Current State
## Cyber HUD — September 23, 2026
Display > HUD > Layout includes Cyber, also available through the HUD voice
action and shared visual state. An explicit first transition into Cyber selects
FLIR with Ironbow 0.42; restored links and subsequent visual tuning remain
authoritative. The skin uses shared red/slate panel treatments in map and cockpit,
with compact 200px collapsed controls and wider expanded panels. Other HUD
layouts retain their existing presentation.
Chamfered panel expand/collapse controls use an inset keyboard-focus outline and
red hover border in both map and cockpit, so clipping does not hide the highlight.
Cyber's map-side right rail opens one panel at a time while keeping collapsed
launchers available. Display, CCTV and Context scroll their contents inside fixed
headers and decorative frames. The narrow-screen rail remains scrollable to reach
each panel. Radio retains the shared nested Context player and compact
header disclosure, without relocating playback controls on theme changes.
Sonar has one contact-highlighting method. Native Cesium points, billboards and
labels are treated in GPU draw commands without replacing their positions,
identities or pick commands. Model and canvas/DOM overlays retain their own
rendering paths. Sonar OFF stops the sweep while retaining Cyber's neutral
contact treatment. Reduced motion disables the animated sweep; cockpit and
hidden HUD states do not apply the map treatment. If the native shader is not
supported, native contacts remain available and Display reports the limitation.
No CSS scene-dimming fallback or effect selector is exposed.
The Sonar controls adjust rings, range, power, opacity and sector. Sweep opacity
affects detection painting rather than label cohort admission. Theme exit and
renderer teardown release the derived GPU resources and restore owned model
colors without replacing newer layer-owned color changes.
The `set_cyber_sonar` voice action adjusts the same controls without switching
themes or enabling omitted settings. State readback distinguishes configured
settings from active map/contact effects. Sonar settings are session-only and
return to their defaults on reload. Sliders accept integer values matching the
action's ranges.
Wind appears in the Weather group before Utilities. The surface-weather prototype
uses keyless NOAA GFS or ECMWF IFS forecasts on an approximately 1° display grid.
It defaults to 10 m wind trails over the existing basemap. Speed shading is an explicit choice; earlier v2 links retain their original speed-shading meaning.
@@ -664,6 +701,25 @@ snapshots, lifecycle actions and row descriptors. Remount and teardown remove
listeners and row subscriptions; obsolete completions do not repaint old rows.
The clear control presents busy state while its existing action owns the transaction.
## Model atmosphere on Apple Metal
Cesium's per-vertex model atmosphere is kept out of the pipeline on devices
whose driver cannot link it. `AtmosphereStageVS` binds shader `out` parameters
directly to varyings, which ANGLE's Metal backend rejects at link time, tearing
down the render loop on iPadOS and iOS. `ModelSceneGraph.configurePipeline`
attaches that stage only when `fog.enabled && fog.renderable`, so the viewer
clears `scene.fog.renderable` and leaves `fog.enabled` — and the fog density
that drives 3D Tiles screen-space-error scaling — intact. Sky atmosphere and the
ground-atmosphere fragment path route through locals and are unaffected.
Affected devices lose distance fog on 3D tiles and on globe basemaps (Esri,
Bing, keyless), since the globe fog shader uses the same `renderable` flag.
The probe releases its throwaway WebGL2 context immediately.
Detection is a WebGL2 link probe of the same out-parameter/varying pattern, so a
future driver fix restores the effect with no code change; iOS/iPadOS platform
detection is the backstop for when no probe context can be created. Applied
during viewer construction, before any tile builds a draw command.
## Map Source control ownership
Map Source controls own chip listeners, source-state subscriptions and selection
@@ -757,10 +813,12 @@ their `configured: false` response.
CCTV media waits at most 15 seconds for upstream response headers and returns
504 on timeout. Its timer stops when headers arrive, so live bodies can continue
streaming; body idle deadlines are separate from this header deadline. Error
responses are cancelled. Buffered snapshots have a 16 MiB streaming cap; an
oversized image remains an upstream miss and uses the normal fallback chain.
The existing declared media size ceiling remains 64 MiB.
streaming; a separate 30-second idle deadline then bounds the gap between
upstream chunks and releases a body that has gone silent. A response that is
still waiting to drain reschedules that deadline, so a slow client does not read
as a stalled upstream. Error responses are cancelled. Buffered snapshots have a
16 MiB streaming cap; an oversized image remains an upstream miss and uses the
normal fallback chain. The existing declared media size ceiling remains 64 MiB.
## GBFS upstream bounds
@@ -821,6 +879,22 @@ under `gods-eye-view/server/providers/`. Portable terrain mechanics, traffic til
math and GBFS source rules are available under `gods-eye-view/sources/`.
The browser layers and their rendering remain in their existing modules.
## Terrain height cache bound
The client terrain-height resolver's in-memory cache is bounded at 20 000
entries (`DEFAULT_MAX_CACHE_ENTRIES`, overridable per instance through
`createTerrainHeights({ maxCacheEntries })`). Coordinate rounding makes repeated
visits to one place share a key but does not bound how many distinct places a
session visits, and the cache previously had no eviction, so a long session
retained every coordinate it ever resolved. Eviction is least-recently-used:
a consumer read promotes its entry, so the cell being watched is not dropped for
having been resolved early. The bound sits far above a city-scale session, so
normal use never evicts and issues no additional proxy requests. A batch's
results are assembled from what that call resolved, so eviction during a large
batch cannot report a just-resolved point as unresolved. Re:Earth and
geoid-fallback entry semantics, the fallback cooldown and the abort-time flush
are unchanged.
## Landmark annotation identity
When a landmark geocode contains only address components, annotations retain
@@ -1013,7 +1087,7 @@ Non-object or array-valued properties reject the response instead of being treat
Launch payloads with missing records now say PAYLOAD DATA UNAVAILABLE. Missing names use Unnamed payload; absent or invalid mass stays unknown instead of appearing as 0 KG.
Updated: September 15, 2026
Updated: September 23, 2026
## Aircraft and vessel server modules
@@ -2673,27 +2747,30 @@ nearby place—always use station selection. Unqualified “turn on/start the ra
requests use Play; a qualified Play-shaped tool call is normalized to Select so
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) |
| Mapped Installations ⌖ | OpenStreetMap mapped context; on-demand Google Maps Places supplement | `src/data/militaryInstallations.js` | `/api/military-installations`, `/api/google/text-search` | viewport-driven + user search; while unavailable, auto-retry 30 s → 240 s backoff |
| Earthquakes | USGS | `src/data/earthquakes.js` | — | 60s |
| Satellites | CelesTrak | `src/data/satellites.js` | `/api/celestrak` | 120s |
| Space Missions (30d) | Launch Library 2 + CelesTrak | `src/data/rocketLaunches.js` | `/api/launches` + `/api/celestrak/active` | 5 min |
| Traffic | OSM Overpass (+ optional TomTom live flow) | `src/data/traffic.js` | `/api/overpass` + `/api/tomtom` | viewport-driven |
| CCTV | Austin + Caltrans (CA) + TfL London + Ontario 511 + Fintraffic (FI) + DriveBC (BC) + TxDOT (TX) + Estonia (Tallinn, Tarktee) + Live Traffic NSW + Open Calgary Open Data + Street View fallback | `src/data/cctv.js` | `/api/cctv` | 10s (active) |
| Radio | Radio Browser (public-domain station directory) | `src/data/radio.js` | `/api/radio/stations`, `/api/radio/click/:uuid` | 45 min directory refresh |
| Transit 🚌 | Operator GTFS-Realtime VehiclePositions (7 keyless regions, `src/data/transitFeeds.js`) | `src/layers/transit/` via `src/app/layers/transit.js` | `/api/transit` | 15s (poll + delayed playback) |
| Bikeshare 🚲 | GBFS (Lyft + BCycle) | `src/data/bikeshare.js` | `/api/gbfs` | 60s |
| Directions 🧭 | OSRM on FOSSGIS servers (OpenStreetMap) | `src/data/directions.js` | `/api/route` (`steps=1`) | on placement / mode change |
| Datacenters ▣ | OSM extract (bundled) | `src/data/localLayers.js` | — | static |
| Dams ▰ | OpenInfraMap/OSM extract (bundled) | `src/data/localLayers.js` | — | static |
| Submarine Cables ◠ | TeleGeography public map (bundled) | `src/data/telegeographySubmarineCables.js` | — | static |
| FIRMS Active Fires ▲ | NASA FIRMS live (VIIRS ×3 NRT, trailing 24h) | `src/data/firmsHeatmap.js` | `/api/firms` (`FIRMS_MAP_KEY`) | 10 min (proxy TTL 30 min) |
| 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) |
| Mapped Installations ⌖ | OpenStreetMap mapped context; on-demand Google Maps Places supplement | `src/data/militaryInstallations.js` | `/api/military-installations`, `/api/google/text-search` | viewport-driven + user search; while unavailable, auto-retry 30 s → 240 s backoff |
| Earthquakes | USGS | `src/data/earthquakes.js` | — | 60s |
| Satellites | CelesTrak | `src/data/satellites.js` | `/api/celestrak` | 120s |
| Space Missions (30d) | Launch Library 2 + CelesTrak | `src/data/rocketLaunches.js` | `/api/launches` + `/api/celestrak/active` | 5 min |
| Traffic | OSM Overpass (+ optional TomTom live flow) | `src/data/traffic.js` | `/api/overpass` + `/api/tomtom` | viewport-driven |
| CCTV | Austin + Caltrans (CA) + TfL London + Ontario 511 + Fintraffic (FI) + DriveBC (BC) + TxDOT (TX) + Estonia (Tallinn, Tarktee) + Live Traffic NSW + Open Calgary Open Data + Street View fallback | `src/data/cctv.js` | `/api/cctv` | 10s (active) |
| Radio | Radio Browser (public-domain station directory) | `src/data/radio.js` | `/api/radio/stations`, `/api/radio/click/:uuid` | 45 min directory refresh |
| Transit 🚌 | Operator GTFS-Realtime VehiclePositions (7 keyless regions, `src/data/transitFeeds.js`) | `src/layers/transit/` via `src/app/layers/transit.js` | `/api/transit` | 15s (poll + delayed playback) |
| Bikeshare 🚲 | GBFS (Lyft + BCycle) | `src/data/bikeshare.js` | `/api/gbfs` | 60s |
| Directions 🧭 | OSRM on FOSSGIS servers (OpenStreetMap) | `src/data/directions.js` | `/api/route` (`steps=1`) | on placement / mode change |
| Datacenters ▣ | OSM extract (bundled) | `src/data/localLayers.js` | — | static |
| Dams ▰ | OpenInfraMap/OSM extract (bundled) | `src/data/localLayers.js` | — | static |
| Submarine Cables ◠ | TeleGeography public map (bundled) | `src/data/telegeographySubmarineCables.js` | — | static |
| FIRMS Active Fires ▲ | NASA FIRMS live (VIIRS ×3 NRT + MODIS NRT, trailing 24h) | `src/data/firmsHeatmap.js` | `/api/firms` (`FIRMS_MAP_KEY`) | 10 min (proxy TTL 30 min) |
| Street Level 📷 | Mapillary vector tiles + Graph API + MapillaryJS (optional Claude planner, server-side key) | `src/layers/mapillary/` via `src/app/layers/mapillary.js` | `/api/mapillary/status`, `/tiles`, `/features` (NDJSON), `/plan`, `/sprite` (`MAPILLARY_CLIENT_TOKEN`; `ANTHROPIC_API_KEY` optional) | camera-driven (320 ms debounce, ≤9 coverage tiles); queries on demand (≤600 tiles, cached 24 h) |
| Wind 🌬 | NOAA GFS 10 m wind (keyless, 0.25°→1° grid; animated particles) | `src/data/wind.js` | `/api/wind` | 1 h (forecast cycle) |
| Wind 🌬 | NOAA GFS 10 m wind (keyless, 0.25°→1° grid; animated particles) | `src/data/wind.js` | `/api/wind` | 1 h (forecast cycle) |
| Fire Perimeters 🔥 | NIFC WFIGS current interagency perimeters (keyless, paged past the 2000-record cap); InciWeb catalog + incident page origin and update time checks for verified incident-page links | `src/layers/perimeters/` via `src/app/layers/perimeters.js` | `/api/fire-perimeters` + `/api/fire-perimeters/inciweb/*` | 5 min (server caches: catalog 1 h; publication 30 min) |
Fire Perimeters uses capped, timed server reads with stale-on-error caching and a per-client limit. Unchanged snapshots retain geometry; link checks cancel on disable or selection change, and the row legend shows reported containment.
Street Level (Mapillary) lives in the right context rail next to CCTV as an
ordinary collapsible panel and starts collapsed. Without `MAPILLARY_CLIENT_TOKEN`
@@ -3321,6 +3398,12 @@ silently demoting every later lookup for the session.
- **The DENSE chip reports the dense LOAD, not the catalog param.** The param flips synchronously while the Starlink shell takes seconds to arrive over a chunked load, and CelesTrak 502s that feed regularly. So the chip reads `DENSE ···` (busy, disabled) while loading, ACTIVE only once dense points are actually on screen, and `DENSE ✕` with the reason on hover when the load fails — a failure also reverts `catalog` to `core`, drops any partial chunk, and leaves the chip clickable to retry. A load is judged by points added, not by HTTP status: a 200 carrying an empty body, a passed-through HTML error page, or only TLEs the core catalog already owns fails with the same revert semantics as a 502. Any explicit request for `core` clears a latched error even when the mode does not change, so a Space Missions restore of an already-core snapshot never leaves the user with a failure they did not cause. Because the load settles asynchronously, the layer pushes a re-render through the optional `setRowControlsListener()` hook; nothing else would repaint that row before the 5-minute catalog refresh, so the count and legend would otherwise sit stale.
- **A dependency owner takes the row with it.** Space Missions borrows this layer for TLE lookup with `showPoints:false`; while points are hidden the layer returns empty row controls, so the legend never describes an empty sky and the chip cannot accept a write that the owner's restore would silently revert.
- The detection-overlay record cache (`_detectionObjects`) is cleared with the catalog on every rebuild: it stamps id/class at creation only, and a rebuild can re-tag a satellite when a partial CelesTrak outage changes which group wins dedupe.
- **CCTV estimated bearings:** a camera whose pack marks `headingConfidence: 'low'` (the id-hash
`fallbackHeadingFromId` bearing) shows `HDG n° (ESTIMATED)` in the HUD and draws its coverage
wireframe dashed; colours, widths and active/idle emphasis are unchanged. A manual calibration
(`calSource: 'manual'`) or curated pose (`poseSource: 'curated'`) is never presented as
estimated, matching the CAL badge. Public camera state carries `headingConfidence` and
`headingEstimated` (`src/layers/cctv/headingConfidence.js`, #639).
- **FIRMS**: no ground clamping (zero 3D-tiles height sampling), ≤18 screen-decluttered ambient labels, click-to-inspect detail card, 2.5k/3k sprite budgets viewport-clipped by FRP.
- **CCTV v2 foundation:** a pitched
frustum wireframe (4 corner rays + far-cap rectangle) with a monitor plane at the frustum's
@@ -3420,6 +3503,7 @@ silently demoting every later lookup for the session.
- HTTP refusals such as 406 now rotate alongside the existing network, rate-limit, and runtime-error cases. A refusal from one mirror no longer prevents reaching healthy alternatives or persists under the seven-day road/month-long boundary cache TTLs. Concurrent identical queries share one mirror sequence; if it fails, both the initiating and joined callers can use the same last-good data.
- A refusal every mirror agrees on is still reported with the first mirror's status and body, so a genuinely malformed query says what upstream said — but only after every mirror has had the chance to answer it. `fetchOverpassPayload` takes injectable endpoints and fetch so the rotation is tested without a live mirror (`src/overpassProxy.test.mjs`).
- Every mirror is asked with a User-Agent that names the application, its version and the project address (`OVERPASS_USER_AGENT` in `server/providers/overpass/constants.js`), which is what the OSM API usage policy asks for. Mirrors may refuse a client they cannot identify; such a refusal is never treated as data and costs the fan-out that mirror, so the header is what keeps the list at full strength.
- When the fan-out has nothing left to offer, the traffic row says which upstream declined and how: `Overpass rate-limited`, `Overpass timed out`, or `Overpass refused the road query (HTTP 406)`; the proxy's own 502 (every mirror unreachable) and 503 (local limiter busy) read `Overpass mirrors unreachable` and `Overpass temporarily unavailable`. Street Traffic draws on two upstreams — OpenStreetMap for geometry, TomTom for flow — so a row that only said "road data unavailable" sent readers to check a TomTom key that was never the problem (#661). `roadRequestError` in `src/layers/traffic/source.js` owns the mapping and tags the error with its status. A refusal whose code is unreadable — the layer takes its source by injection, so an adapter may not supply one — reports `Overpass temporarily unavailable` rather than printing `HTTP undefined`, and anything the layer did not classify at all (a dropped connection, a malformed snapshot) still reads `Road data temporarily unavailable`, because a platform exception is not a sentence to put on a panel.
### Share-link v2 layer state (August 2026)
@@ -4118,6 +4202,134 @@ Existing catalog validation, category filters, tuning and voice playback behavio
remain unchanged. Audio connects directly to the broadcaster after an explicit
play action; the source does not relay or record streams.
## Local RTL-SDR and Local ADS-B
The Radio panel holds a Local RTL-SDR card below internet radio. One browser
WebUSB session (`src/sdr/controller.js`) drives a USB RTL-SDR in desktop Chrome
or Edge on localhost or HTTPS; nothing opens until CONNECT. Demodulation and
Mode S decoding run in a worker; FM audio plays through an audio worklet.
Local FM and internet radio are one listening surface: starting local FM stops
internet-radio playback, and starting internet radio stops local FM. ADS-B
reception does not interrupt internet radio.
- **Modes.** FM (87.5–108 MHz, tune, seek, volume) or ADS-B at 1090 MHz.
With a receiver open, selecting ADS-B enables the Local ADS-B layer;
selecting FM disables it.
Enabling the layer switches the receiver to ADS-B; disabling it leaves the
receiver mode unchanged.
- **Gain.** AUTO (tuner AGC) or a manual R820T step from 0.0 to 49.6 dB,
stored per mode in `gev:sdr:gain:v1`. Defaults: ADS-B 28.0 dB, FM AUTO.
Changes apply to the open receiver without reconnecting.
- **Receiver stats (ADS-B).** CRC-valid messages per second, aircraft heard,
aircraft with a fresh position and the IQ level.
- **Devices.** An already-authorized receiver opens without the WebUSB
picker. ADS-B prefers a device whose product name matches ADS-B or 1090 (the
1090 MHz channel of a dual-channel board); FM avoids ADS-B/UAT channels.
A device chosen in the picker is remembered per mode by vendor, product,
product name and serial in `gev:sdr:device:v1`. CHANGE DEVICE reopens the
picker. LOCATE uses browser location for receiver-relative CPR decoding.
The Local ADS-B layer (`local-adsb`, `src/layers/localAdsb/`) is off by default
and registered as local-only: it never enters share links or stored layer
state. Its aircraft get the public Flights treatment in magenta, alongside
the public Flights layer. Public Flights renders about one poll interval
(~30 s) behind for smooth interpolation while local aircraft are shown as
heard, so a local marker usually leads its public twin; both markers are drawn
on purpose:
- **Class.** Each aircraft is classified with `aircraftClass.js` from its
ADS-B emitter category (dump1090 strings: `A1` light, `A3` large, `A7`
rotorcraft; set A/B/C/D = type code 4/3/2/1) and, once known, its adsbdb
ICAO type. Silhouette and per-class scale come from `aircraftIcons.js`
(×0.8 on the ground), tinted magenta.
- **3D.** The DISPLAY rail's 3D toggle and its proximity/all mode apply: the
layer reads the Flights layer's `models3d` params and uses the same ceiling,
caps, add/keep radii, visible-first eligibility
(`src/data/modelEligibility.js`) and per-class GLB
(`src/layers/flights/modelSpec.js`), tinted magenta through the same MIX
colour blend Flights uses for military amber. Heights are barometric +
geoid N, as Flights renders a contact without a geometric altitude; a
grounded model rides the one-shot ground snap and belly offset.
- **Motion.** No display delay. The marker is extrapolated forward from the
newest fix with `motionModel.js` (arc extrapolation, path-derived course
blended over the reported track, rate-limited slew); a new fix is absorbed
by a correction that decays over 900 ms. Coasting continues while messages
arrive and stops 10 s after the last one (60 s at most).
- **Sanity filter.** For airborne positions the browser decoder prefers a
fresh even/odd pair, then a frame decoded relative to the aircraft's own
position (under 10 minutes old), then the receiver location. Surface
positions are stricter: a single surface frame decodes only against the
aircraft's own recent fix, and only when that fix is provably within 45 NM
(it could not have gone farther since at its speed limit); the receiver
location never decodes a lone surface frame and only picks the quadrant of
a surface pair. Without an existing suitable fix for that aircraft
(airborne or surface, provably within 45 NM), surface acquisition
requires an even/odd pair. Every fix
must be reachable from the last accepted one at 1.5 × reported ground
speed + 50 kt (1,000 kt without a speed, 350 kt for A1/A7/B1/B4) over the
elapsed time + 1 s, plus 500 m.
Refused fixes are counted (`positionsRejected` in the SDR state; the layer
applies the same check to merged feed records and reports the total as
`rejectedPositions` in its stats). Three refusals in a row re-anchor.
- **Trail.** A selected aircraft draws a magenta trail of the positions the
receiver heard (up to 10 minutes / 600 fixes, dropped with the aircraft),
with the tracked-flight trail look and a live head segment. No network
backfill.
- **Enrichment.** Only the selected aircraft (type + route) and aircraft
about to render as models (type) are looked up, through the Flights
source's cached adsbdb proxy with the same once-per-session, four-in-flight,
200 ms drip rules.
A marker drops once its newest position is 60 s old; an aircraft is forgotten
after 60 s without a message. Clicking a marker opens a readout card (ICAO,
callsign, class and category, adsbdb operator/type and a plausible route when
known, altitude, ground speed, track, vertical rate, position age, message
count, "Heard by your receiver"); markers are not camera-followed. Positioned
aircraft join detection boxes, labelled only with a decoded callsign.
Receivers publish one record shape from `src/sources/adsbRecords.js`
(`icao`, `callsign`, `category`, `onGround`, `lat`, `lon`, `altitudeFt`,
`groundSpeedKt`, `trackDeg`, `verticalRateFpm`, `lastPositionAt`,
`lastMessageAt`, `messageCount`, `rssiDbfs`, `band`, `source`). `band` is `1090` or `978`; `source` is
`webusb` (browser SDR) or `feed` (decoder feed).
`normalizeDump1090Aircraft(json, nowMs, { band })` maps a dump1090/readsb/
skyaware978 `aircraft.json` document into the same records. Voice
`set_layer_visibility` accepts `local-adsb` ("local ADS-B", "my receiver",
"my antenna").
**Decoder feeds.** The layer's second input is the server route
`GET /api/local-receivers/aircraft`
(`server/providers/local-receivers.js`), configured only by the
`LOCAL_RECEIVER_FEEDS` environment value (`band=url`, comma-separated, e.g.
`1090=http://localhost:8080/data/aircraft.json`). Every host must pass
`parseTapAddress` (loopback, RFC1918, `localhost`, `*.local`), the scheme must
be http(s) and the path must end in `aircraft.json`; an invalid entry is logged
at startup, reported `invalid` and never fetched. The route reads all feeds in
parallel (2 s timeout, redirects refused, 2 MB cap, single-flight with a 1 s
cache) and returns `{ configured, generatedAt, feeds: [{ band, label, status,
aircraft, ageMs }], records }`, where a feed is `live`, `stale` (its own `now`
is over 10 s old), `unreachable` or `invalid`. Labels name the band only;
addresses and upstream error text stay on the server. Unconfigured, it returns
`{ configured: false, feeds: [], records: [] }` without fetching.
The browser session (`src/layers/localAdsb/feeds.js`) polls the route every
second only while the layer is enabled and only while the route reports
`configured`; the Local RTL-SDR card makes one probe request at startup to show
its read-only "Decoder feeds: 1090 live · 978 live" line. Feed records are
rebased to the browser clock and kept up to 60 s after their last message.
`mergeLocalAdsbRecords` merges both inputs by ICAO, keeping the newest position
(tie: newest message) and the bands/sources heard in the last 60 s; the card's
receiver line names them ("Heard by your receiver · 1090 MHz + 978 MHz UAT ·
browser SDR + decoder feed"). Aircraft heard only on 978 MHz UAT draw with a
thin light-magenta ring. With feeds configured the row status reads "2 feeds
live · 14 heard" when every feed is live. While any input is producing (the
browser receiver streaming, or a feed live) the row stays nominal and names the
other feeds as a trailing note ("3 heard · USB 5.8 msg/s · feed 1090 stale"),
since a decoder stopped so the browser can take the dongle is not a fault. With
nothing producing, every readable feed stale reads STALE and anything else is
an error. Without feeds it keeps the WebUSB statuses. See
`docs/LOCAL-RECEIVERS.md`.
## Bundled geography and submarine cable components
Submarine cables use separate source, geometry, rendering, interaction and
+82
View File
@@ -0,0 +1,82 @@
# Local receivers
The Local ADS-B layer draws aircraft heard by your own receivers. It has two
inputs, and it can use both at once.
| Input | What it is | Bands |
| ------------ | ----------------------------------------------------------------------------------------------------------------------------------- | -------------------- |
| Browser SDR | A USB RTL-SDR opened by the browser through WebUSB (Radio panel, Local RTL-SDR card). Decoding runs in the page. | 1090 MHz |
| Decoder feed | The `aircraft.json` served by a decoder you run: dump1090-fa, readsb or tar1090 (1090 MHz), or dump978-fa + skyaware978 (978 MHz). | 1090 MHz, 978 MHz UAT |
Aircraft from both inputs are merged by ICAO address. The newest position
wins, and the click card names every band and source that heard the aircraft
in the last 60 s. Aircraft heard only on 978 MHz UAT draw with a thin ring.
Local aircraft usually lead their public twins. The public Flights layer
renders positions about one poll interval (~30 s) behind so it can interpolate
smoothly between fixes, while local receiver aircraft are shown as heard. An
aircraft in both layers therefore draws twice, the magenta local marker ahead
of the public one; both markers are drawn on purpose.
## Which to use
- **One plain RTL-SDR dongle, desktop Chrome or Edge:** use the browser SDR.
Nothing else to install.
- **A dual-band board, a Raspberry Pi feeder, or a dongle already used by a
decoder:** use a decoder feed. A USB dongle can be opened by only one program
at a time, so a dongle that dump1090 or dump978 owns cannot also be opened by
the browser.
## Configuring feeds
Feeds are set in the server environment only (`.env`), never in the browser:
```sh
LOCAL_RECEIVER_FEEDS=1090=http://localhost:8080/data/aircraft.json,978=http://localhost:8978/data/aircraft.json
```
Each entry is `band=url`, where `band` is `1090` or `978`. The URL must:
- use `http` or `https`;
- point at loopback, a private (RFC1918) address, `localhost` or a `*.local`
name. Public addresses, link-local addresses (including `169.254.169.254`),
IPv6 and other host names are refused;
- have a path ending in `aircraft.json`, with no credentials, query or
fragment.
An entry that breaks a rule is logged when the server starts, shown as
`invalid` and never fetched. Restart the server after changing the value.
The server reads every feed about once a second while the Local ADS-B layer is
on. A feed is `live`, `stale` (its own `now` is more than 10 s old, usually a
stopped decoder), `unreachable` or `invalid`. The Layers panel row and the
Local RTL-SDR card show these states.
## Example: dump1090-fa and dump978-fa on macOS or Linux
Write each decoder's JSON to a directory, then serve the directory on
localhost. Each command keeps running, so start each in its own terminal:
```sh
# 1090 MHz
dump1090-fa --device-index 0 --write-json /tmp/adsb1090
python3 -m http.server 8080 --bind 127.0.0.1 --directory /tmp/adsb1090
# 978 MHz UAT: dump978-fa decodes, skyaware978 writes aircraft.json
dump978-fa --sdr driver=rtlsdr,rtl=1 --raw-port 30978
skyaware978 --connect localhost:30978 --json-dir /tmp/adsb978
python3 -m http.server 8978 --bind 127.0.0.1 --directory /tmp/adsb978
```
```sh
LOCAL_RECEIVER_FEEDS=1090=http://localhost:8080/aircraft.json,978=http://localhost:8978/aircraft.json
```
A board with two identical-serial channels, such as the Nooelec FlyCatcher,
cannot be told apart by serial number. Select each channel by index:
`--device-index <n>` for dump1090-fa and `--sdr driver=rtlsdr,rtl=<n>` for
dump978-fa. `rtl_test` lists the indexes.
A tar1090 or readsb install on a Raspberry Pi already serves
`http://<pi-address>/tar1090/data/aircraft.json`. Use the Pi's private address
or its `.local` name.