Adds MODIS NRT to Active Fires and a keyless Fire Perimeters layer (NIFC WFIGS) with
InciWeb incident links, served through a capped, cached /api/fire-perimeters proxy.
Co-authored-by: Gustavo Beneduzi <[email protected]>
Co-authored-by: James Cooke <[email protected]>
The harness called the compatibility facade's private _refreshTogglePanel,
which the layer-lifecycle refactor removed; Transit and Directions call
refreshLayerStats() on the bare lifecycle, which lacked it, so their rows
only repainted on the next tick. Add refreshLayerStats() to the lifecycle
and use it from the harness. With the crash gone, two further checks ran:
the selected-entity paint wait now also waits for Radio's own reveal, and
on phones the title bar sits 16 px from the top so both broadcast waves
stay on-screen.
Co-authored-by: Bilawal Sidhu <[email protected]>
Adds a Local ADS-B layer fed by your own receivers: a browser WebUSB
RTL-SDR (with FM listening) and local 1090/978 MHz decoder feeds
(LOCAL_RECEIVER_FEEDS, local and private addresses only). Local aircraft
draw in magenta with class, silhouettes, 3D models, trails and a position
sanity filter; the click card names the band and input that heard them.
Builds on Sameh Khamis's SDR prototype and the initial GEV port by
Mazeyar Moeini Feizabadi (#134); the decoder-feed design follows the
notes on #57. Closes#57.
Co-authored-by: Sameh Khamis <[email protected]>
Co-authored-by: Mazeyar Moeini Feizabadi <[email protected]>
The right-rail layout pass lifts the Context, CCTV, Weather and Recent
Imagery panel scrollers to measure their natural heights, which clamps
their scroll offset to zero, but it only restored scrollers tagged
data-rail-scroller. A layer that refreshes its Layers-panel row every
second snapped the Context panel back to the top while scrolling. Tag
every lifted scroller and pin the pairing with a test that reads the
measuring CSS.
Co-authored-by: Bilawal Sidhu <[email protected]>
Select a box on the world and browse the last 30 days of open satellite imagery over it, keyless: NASA GIBS Harmonized Landsat Sentinel-2 (30 m) and the VIIRS daily overview (250 m), with a day strip, IMAGE / VS BASEMAP / A / B modes with a swipe divider, PNG export and share links. The before/after swipe from the Nepal scene moves into shared modules so both use it.
Co-authored-by: manjunath22466 <[email protected]>
* feat(taps): shared address contract for receiver taps
Receiver taps (#57 RTL-SDR, #56 Rayhunter, #7 TAK, #6 Meshtastic) differ from
every other provider in this tree in one way that matters: the upstream address
comes from the user at runtime rather than from a fixed constant, so each tap
route is an SSRF surface unless the address is constrained. Today each tap
would answer that question for itself, and they would disagree.
parseTapAddress() is the single place the rule lives:
A tap may address loopback or an RFC1918 private network, plus the
`localhost` and `*.local` (mDNS) names. Nothing else.
That is what a tap is for — reading your own device on your own network. A tap
pointed at a public address is either a mistake or the proxy being used as a
confused deputy, and both get the same answer. Link-local (169.254/16) is
excluded wholesale, which covers the cloud metadata address by construction
rather than by blocklist.
Deliberately NOT reusing isNonGlobalIpv4() from radio/stations.js: it reports
malformed input like 999.1.1.1 as non-global, which is the safe direction for
the radio proxy's "is this public enough to fetch?" test but inverts into
*allowing* garbage under a tap's "is this local enough to fetch?" test. A
positive allowlist cannot invert that way, and the tests pin that case.
Two documented non-goals so they don't read as oversights: no general hostname
resolution (a name other than localhost/*.local is rejected rather than
resolved-and-pinned, avoiding DNS-rebinding defence for no gain), and no IPv6
(zone-index handling is platform-specific and no tap has needed it).
Scoped to the address rule alone. The recorded-fixture harness lands with the
first tap, designed against a real recorded response rather than guessed at
with no consumer.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01KGKBNNjPh4iQe8sviGZNPb
* style: adopt the tap address contract into the format scope
Mechanical. Adds server/providers/common/tap-address.js and its test to
scripts/format-scope.json and applies prettier, matching the six sibling
modules in server/providers/common/ that are already adopted. No behaviour
change.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01KGKBNNjPh4iQe8sviGZNPb
* docs(taps): state accurately which taps this contract governs
The header claimed #57, #56, #7 and #6 as consumers. Only #56 (Rayhunter)
actually is today: it takes ?base=host:port from the browser, which is the
shape that makes a route an SSRF surface. #57 would qualify if built
server-side.
The other three are different shapes, and forcing them through a private-only
check would be wrong rather than merely unnecessary:
- Meshtastic (#6) reads a fixed public broker, mqtt.meshtastic.org.
- WiGLE (#11) reads a fixed public API behind a token.
- TAK (#7) takes its host from operator-set env config rather than from the
browser, and a TAK server is frequently a legitimate remote host.
No behaviour change; comment only.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01KGKBNNjPh4iQe8sviGZNPb
* refactor(taps): move the tap address contract to src/data so both sides share it
The browser needs the same answer the server does — the server as a trust
boundary, the client to give immediate feedback on a typed address without a
round trip. Keeping the rule under server/providers/ would have meant the
client re-deriving it, which is how the Rayhunter branch already carried a
hand-synced copy of the regex with a comment admitting it.
#274 is open about eight independent copies of haversine, one of which had
silently swapped its arguments. Shipping a new duplicated validator across a
trust boundary in the same tree would be hard to defend.
server/providers/gbfs.js already imports src/data/gbfsSource.js, so shared
validation living in src/data with the server importing it is the established
direction here rather than a new one.
Pure move plus the path/docblock updates. No behaviour change; the 12 contract
tests are untouched and still pass.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01KGKBNNjPh4iQe8sviGZNPb
---------
Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
Co-authored-by: Bilawal Sidhu <[email protected]>
Reflect the weather stack, analyst records and feed provenance, satellite pass predictions and live camera video that landed since the last full pass.
Co-authored-by: Matt Van Horn <[email protected]>
Wind (NOAA GFS and ECMWF IFS: animated particles, colour field, a reading at map centre), observed weather (NOAA MRMS radar, GOES and NESDIS infrared clouds, lightning density) on one shared history timeline, NHC and CPHC cyclone advisories with tracks and cones, and a WEATHER panel in the right rail. On 3D Tiles the observed products render as raised shells with a sharper window around the view.
Co-authored-by: Gustavo Beneduzi <[email protected]>
Co-authored-by: Daniel Slay <[email protected]>
Co-authored-by: Rehaan Delmotra <[email protected]>
Co-authored-by: Cursor Agent <[email protected]>
At 720px and below both panel stacks scroll vertically, so each panel's absolutely positioned glow (negative inset) became scrollable overflow on both axes and drew a horizontal scrollbar band plus a spurious vertical scrollbar whenever CCTV, Context, Data Layers or Scenes was expanded. Pin the glow to its panel box in the narrow-screen block and add a stylesheet test that checks every 720px media block.
.gitignore listed only the bare filename `.env`, but the app reads four
credential files, not one. scripts/setup-doctor.mjs:92 walks
`['.env', '.env.local', `.env.${mode}`, `.env.${mode}.local`]`, and
server/standalone/vite.config.js:11 calls Vite's `loadEnv(mode, root, '')`,
which reads the same ladder.
gitignore matches literally, so three of those four files were
committable. A key placed in .env.local is fully live for the app --
the doctor reports it as configured and the provider proxies use it --
while `git add -A` stages it. Verified on a clean checkout of main:
.env ignored
.env.local COMMITTABLE
.env.production COMMITTABLE
.env.production.local COMMITTABLE
This matters more than usual here because the in-app Provider Settings
panel trains people to keep real keys in the checkout rather than in a
shell profile, and because the repository has ~7.7k forks that inherit
the same gap.
Replaces the single `.env` entry with `.env` plus `.env.*`, and re-includes
`.env.example` with a negation so the tracked template is unaffected.
Deliberately scoped to the dotenv ladder alone. Other credential-bearing
artifacts (a repo-local OPENSKY_CREDENTIALS_FILE, *.pem/*.key) are a
separate question and are left for a follow-up rather than widened here.
Co-Authored-By: Claude Opus 5 <[email protected]>
TOMTOM_DAILY_TILE_BUDGET defaulted to 40,000 per day against a free
tier that is granted per MONTH, not per day. TomTom currently allows
200,000 Traffic Flow & Incidents tile requests per month
(https://docs.tomtom.com/pricing/), so the default exhausted a user's
entire monthly allowance in five days and left the traffic layer dead
for the rest of the billing period.
The docs contradicted themselves on this. traffic.js described the
free tier as "~50k/day", while .env.example cited the correct 200K
per month two lines below quoting the same 40,000 default. 50k/day
would be 1.5M/month, 7.5x the actual allowance.
Sets the default to 6,000/day. A 31-day month lands at 186,000,
inside the allowance with headroom. Corrects the stale figures in
traffic.js, .env.example and DATA_SOURCES.md.
This remains an application-side safety ceiling, not a billing cap,
and the surrounding text still says so. TomTom's free tier requires
no card, so overrunning it cuts the layer off rather than generating
a charge — but a layer that dies three weeks into every month is
still worth avoiding by default.
Co-Authored-By: Claude Opus 5 <[email protected]>
adsbdb supplies the aircraft type, model name and registration behind every
enriched flight, and the airline and origin/destination pair behind the
tracked contact's FROM -> TO strip. It appeared in neither attribution
surface: DATA_SOURCES.md and src/data/dataCredits.js each held zero
occurrences of it, against the rule both files state for themselves and
CONTRIBUTING's fourth pull-request item. adsb.lol is a different operator and
its ODbL credit does not reach this data.
The reporter of #256 left the patch off deliberately, saying the terms were
what should not be guessed. They are published, so this takes them from the
source rather than inventing them: adsbdb credits PlaneBase for the aircraft
data and Guillaume Michel for the ICAO-to-N-number conversion, and states that
the flight route data "is the work of David Taylor, Edinburgh and Jim Mason,
Glasgow, and may not be copied, published, or incorporated into other
databases without the explicit permission of David J Taylor, Edinburgh". No
licence or rate limit is published for the service itself.
That restriction is the reason the note records what the proxy does with what
it fetches: responses are cached per key for 24 hours in .gev-cache/adsbdb.json,
which is gitignored, so the data stays on the operator's machine and is never
bundled with this repo or redistributed.
A test pins the credit. The attribution is a requirement rather than a
preference, so it should fail loudly if a later edit drops the names or the
restriction, instead of surviving on luck.
scripts/format.mjs skips *.test.mjs during runtime discovery, so a test
added under a discovered root is never seen by format:check until it is
listed in scripts/format-scope.json — which is what CODE-BOUNDARIES.md
means by adding tests to the explicit list as they are adopted. The file
went in unformatted and the check stayed green, hiding it.
Listed and formatted; format:check now covers 917 files instead of 916.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
CONTRIBUTING asks for docs/CURRENT-STATE.md and CHANGELOG.md in the same PR
as a runtime behaviour change.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
ITU-R M.1371 encodes speed over ground in 0.1-knot units and course over
ground in 0.1-degree units, reserving the top code of each field for "not
available": SOG 1023 reaches us as 102.3 knots and COG 3600 as 360 degrees.
Both are finite, so numberValue passed them through as ordinary readings and
every consumer — vessel cards, the HUD, voice analyst queries — treated a
missing measurement as a real one.
Observed on a live AISStream feed of 3,062 vessels: exactly 3 records at
102.3 knots with no reading anywhere between 40 and 102.3, and 404 records
at exactly 360 degrees. One Dutch rescue boat moored at its station carried
both sentinels at once, so an analyst summary reported a maximum speed of
102.3 knots — faster than any vessel afloat.
Filter both at the store, where AISStream messages are decoded, so the
sentinels never enter the cache. Heading already had this treatment through
normalizedHeading with its own separate sentinel of 511.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>