Merge upstream/main into feature/catalonia-cctv-cameras

Keeps PR #595 current with #730-#743. Clean merge, no conflicts.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
ulihhhhandClaude Opus 5.5 committed 2026-09-24 10:50:18 +02:00
commit b27e8a35c8
184 files changed
+23240 -252

No files matched your search

+170 -2
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.
@@ -1013,7 +1050,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
@@ -2691,8 +2728,11 @@ its criteria cannot be silently ignored.
| 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) |
| 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) |
| 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.
Directions is a keyless front end to the routing the voice agent already
uses. Its row chips are the whole interface: DRIVE / WALK / BIKE pick the
@@ -4104,6 +4144,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.