Commit Graph
7 Commits
Author SHA1 Message Date
Aditya 557d3bb41e fix(military): hold the last track and speed when a poll omits them
MilitaryFlightRecords coerced a missing course to 0 (`courseDeg || 0`)
and a missing speed to 0 (`speedMps ?? 0`) before stickyNumber, so the
hold-last-known-good merge never ran. When an adsb.lol row arrived
without `track` or `gs`, the aircraft's icon swung to north and the
position history got a 0 kt / 0 degree fix, so dead reckoning stopped
or turned it north. The civil FlightRecords holds the last value for
the same input.

Pass the raw values to stickyNumber, with 0 as the fallback only until a
value is known, as before. A grounded row without a speed still reads
0, so a parked aircraft doesn't coast at its landing speed.
2026-10-16 16:52:22 +05:30
Aditya d6c1ea0f4b fix(maps): retry the real imagery and terrain provider after a fallback
In the keyless setup a single transient failure while loading the default
map layers locked in the degraded fallback until reload.

_getImageryProvider cached the construction-fallback resolution (OSM) under
the Esri stack id, so every later "Esri Satellite" selection showed OSM.
createKeylessTerrain turns any Re:Earth failure into a flat
EllipsoidTerrainProvider; the controller cached it and set _terrainMode to
'keyless', so a boot-time network blip left the globe flat.

Fallbacks are still cached and shown at once, and no switch waits on a
retry. One controller timer now retries the real provider behind whichever
fallback is on screen (Esri's OSM stand-in, or flat terrain on a shown
globe), starting at 30 seconds and doubling to a 10-minute cap while it
keeps failing. A failed retry leaves the cached fallback and the installed
terrain untouched. A success replaces the cache entry and applies it live:
terrain is installed in place with a render request (or, behind a 3D
tileset, picked up by the next globe activation), and Esri is reselected
automatically, after any in-flight switch settles. A switch that read the
flat fallback re-reads the terrain cache before installing, and destroy()
cancels the timer and releases a provider built after it.
2026-10-16 16:52:01 +05:30
Aditya a316e12323 fix(overpass): read failure phrases from the remark, not element tags
overpassLooksRuntimeError and overpassLooksRateLimited searched the whole
200 body, element tags included. A result containing a place named
"Timed Out Escape Rooms" (or any tag with "runtime error", "out of
memory", "too many requests" or "rate_limited") was refused as an
upstream failure, and the endpoint was put on cooldown for every other
query.

Parse the body once. A data reply (one with an elements array) reports
failure in its remark, so the phrase checks run on the remark. Any other
body, an error page or JSON without elements, is matched whole as
before.
2026-10-16 16:52:01 +05:30
Aditya 2c61d9e2b7 fix(geo): wrap longitude differences across the antimeridian
The flights ground-floor pipeline subtracted longitudes without wrapping.
approxDistanceKm, and its copy in the mesh-floor sampler, measured two
points 0.2° apart either side of 180° (21 km at Fiji) as about 38,000 km,
so low and grounded aircraft just across the line from the viewer were
never floored or mesh-sampled. corridorFloorCells walked a segment across
180° the long way round, filling its cell budget with ground behind the
contact and never requesting the cells it was about to cross, and
stickyFloorCell dropped its hysteresis for a contact on the seam. Each now
takes the short way round, and corridor points are normalised back into
[-180, 180].

SEARCH NEARBY SITES centred its Google Places search on the mean of west
and east, which for a view across 180° (west > east) is longitude 0, and
sized the radius from the view's height alone. It now measures the view's
width through 180° and searches its real centre.
2026-10-16 16:48:37 +05:30
Aditya bd48533b03 fix(director): land on the requested shot
A shot action seeks to the target shot's startProgress, which
sceneTimingForShot computes as start / total. sceneSeekState multiplied
that back into seconds and compared it with each shot's end, but
(start / total) * total can round one ulp below start. The comparison
then put the seek at the very end of the previous shot. With five equal
3.9 s shots, "go to" shots 2, 3 and 5 landed on shots 1, 2 and 4.

Compare the requested progress against each shot's end / total instead,
the same division the caller used, so a shot's own start always selects
that shot. Exact boundaries still select the next shot, and anything
below a start still selects the previous one.
2026-10-16 16:48:37 +05:30
Aditya b90bcf1829 fix(flights): don't treat OpenSky's position-less rows as a partial snapshot
OpenSky's state-vector spec allows null longitude/latitude, and every live
/states/all response carries such rows (about 50 of 7,800). The normaliser
rejects them, so admitRecords reported complete: false on effectively every
poll. While a snapshot is incomplete, FlightRecords.absence() retains any
aircraft received in the last five minutes, which meant the stale fade,
MISSING_POLL_LIMIT and the landed fast-cull never ran: vanished and landed
aircraft stayed on the globe at full opacity for up to five minutes.

admitRecords now takes an optional unpositionedId(row). A rejected row it
names is a contact without a position: its id is returned in retainIds and
the row does not count against completeness. OpenSky opts in for rows with an
ICAO24 address and strictly null longitude and latitude; the id comes from
the same helper the normaliser uses. Rows with no usable id, out-of-range or
half-null coordinates, truncated rows and positioned duplicates still make
the snapshot incomplete, and a nonempty feed with no positioned rows is
still malformed.

absence() retains exactly the retainIds aircraft under the existing
five-minute bound, except likely-landed ones, which keep the landed limit.
Everything else ages out normally. Military readsb admission does not opt
in and is unchanged.
2026-10-13 17:18:28 +05:30
Aditya fc48eda113 fix(radio): reject null-body and out-of-range mirror statuses instead of crashing
fetchPinnedRadioResponse built `new Response(stream, { status })` inside
the raw https response callback, which runs outside the surrounding
promise. A Radio Browser mirror answering 204, 205 or 304 (null-body
statuses), or any status outside 200-599, made the Response constructor
throw. Nothing caught it, so the whole dev/preview server process exited
and every layer went down, not only radio. The catalog's mirror failover
never saw the error.

Mirror the guard local-receivers.js already uses: null-body statuses get
a null body, statuses outside 200-599 are rejected, and anything thrown
in the callback rejects the promise so the existing error handling and
mirror failover run.
2026-10-13 17:04:20 +05:30