Skip unchanged overlay cohorts while detecting moved positions and lifecycle changes. Defer ground sampling until visible terrain settles, retain the loaded surface as a floor, and preserve roof heights and bounded retries. Keep regression and idle-render gates intact.
Use a fresh page for first-fetch assertions and wait for rendered camera state instead of fixed timing. Check connected focus retention during row reorder, departed-contact continuation, and attribution keyboard ownership in Cockpit.
Keep focus visible across controls and stable during Contacts, Cockpit and mission roster updates. Preserve native short Space activation, and claim push-to-talk only after a 500 ms hold. Add panel Escape handling and keyboard-accessible attribution controls.
Retains the Map Source foundation contributed in https://github.com/bilawalsidhu/gods-eye-view/pull/171.
Preserve the selected map source through delayed keyboard opening and cancel interrupted focus requests.
Retain Tom-Neverwinter's original focus repair and Manjunath's separately authored selection/cancellation refinements, with maintainer integration and verification commits.
Keep the Esri settlement assertion exact, verify selected-source focus and Tab departure in Chromium, cancel pending work during disposal, and document the focused behavior.
Build on Tom-Neverwinter’s Map Source focus retry. Prefer the selected tile, and revoke delayed focus when the user moves focus, clicks, or closes the tray.
Record FIRMS source success after appending its rows so aggregation failures do not produce contradictory source status. Cover large feeds, filtering, ordering and source failures with regression tests.
Four small, independently-verified issues from the audit backlog:
- #262/#263: the celestrak, terrain-heights, adsbdb and launch-library
proxies put raw `err.message` / raw upstream error bodies into HTTP
responses, contradicting the sanitized-error property KNOWN-ISSUES.md
documents. They now log the real error server-side (matching the GBFS
and CCTV proxy convention) and return a generic message.
- #265: scripts/dev-cctv.sh defaulted to HOST=0.0.0.0 while dev-fresh.sh
and dev-secure.sh default to loopback. Now loopback, opt in with
HOST=0.0.0.0.
- #274: naturalEarthRegions.js's internal haversineKm took (lon, lat,
lon, lat) while every other haversineKm in src/ takes (lat, lon, lat,
lon). Same name, same signature shape, opposite order — a silent
wrong-distance landmine. Reordered to match; the one call site was
updated, so behavior is unchanged.
- #270: removed CCTV_AUTO_CALIBRATE and CCTV_DRAPE_MESH from
.env.example; no code anywhere reads them.
Not done: deduplicating the ~10 haversine implementations into one
shared utility. That is issue #188's scope and a far larger diff;
removing the argument-order landmine is what makes the current state
safe.
Verified: npm test (2705 pass / 0 fail), npm run build green.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01C3eGYgDYBo2BCUeDGwGvrp
Six QA harnesses launched Chrome with `--use-angle=metal` unconditionally
(or defaulted to it). Metal is a macOS-only ANGLE backend: on Windows and
Linux it makes WebGL initialization fail outright, so CesiumWidget never
constructs, the app never boots, and the harness dies at its
`waitForFunction` boot wait — before running a single assertion:
Error constructing CesiumWidget.
RuntimeError: The browser supports WebGL, but initialization failed.
TimeoutError: Waiting failed: 60000ms exceeded
Affected: qa-map-source-tray, qa-cockpit-utility, qa-view-target-prewarm,
qa-firstrun (unconditional), qa-cockpit-plates (GPU branch taken off-Mac
too) and qa-floor-hold (`--angle` defaulted to metal everywhere).
Fixed by adopting the platform check qa-flyroute-cinema.mjs already uses:
real GPU via Metal on darwin, the portable swiftshader rasterizer
elsewhere — which is what the other ~20 QA scripts already pass. No
behavior change on macOS.
Verified on Windows 11: `npm run qa:map-source-tray` went from 0
assertions (boot timeout) to all 20 checks running.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01C3eGYgDYBo2BCUeDGwGvrp
The last fixed sleep in this harness: after the controller commits
`activeId = 'osm'`, the fallback callback re-syncs the chips and Cesium
drops the "Powered by Esri" credit on a later render frame again. A flat
50ms covered that most runs and not a slow one — observed failing once in
12 runs on Windows with `creditVisible: true` after the fallback.
Same root cause and same remedy as the tray timers above: poll the
observable truth (credit gone AND the pressed chip is osm), bounded at 3s,
instead of guessing a duration.
0 failures in 15 consecutive runs after this, against 1-in-12 before.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01C3eGYgDYBo2BCUeDGwGvrp
`qa:map-source-tray` failed on a clean checkout at "Enter opens the tray and
hands focus to a Map Source tile". The gate was right: this is a real
keyboard-accessibility bug, not a flaky assertion.
Opening the tray schedules the focus hand-off to a Map Source tile 240ms
later (scheduleMapSourceFocus). The tray itself fades in behind a 180ms
`visibility` transition on .dock-popover-content, and a chip inside it cannot
take focus until that transition lands. Those two timers race. When the
machine is slow enough that the fade has not finished at the 240ms mark,
`chip.focus()` is called on a `visibility: hidden` element — which is a
SILENT no-op — and nothing retries. The tray sits open with keyboard focus
stranded on the disclosure, unreachable.
Instrumenting HTMLElement.prototype.focus during a failing run showed exactly
that: focus() called once on the photoreal chip, the chip connected and not
disabled, computed visibility `hidden`, activeElement unchanged.
Fix the hand-off rather than the delay:
- focusMapSource now reports whether focus actually landed (activeElement ===
chip) instead of assuming .focus() worked.
- scheduleMapSourceFocus retries on a 30ms cadence until it lands, bounded to
~720ms past the first attempt so a permanently hidden tray cannot spin.
Also stop the QA gate racing the same timers. Its keyboard checks slept a
fixed 300ms and asserted focus had already moved; they now wait on controller
truth (aria-expanded plus where focus landed), the way the Bing-switch check
already did. That replaces four fixed sleeps, not just the long-Enter-hold
one the issue named. The wait only settles state — the check() below each one
is still the assertion, so a swallowed timeout surfaces as that check failing
with real values rather than an opaque puppeteer error.
Verified against a preview build: `qa:map-source-tray` goes from 3 failures
to 20/20 passing, twice in a row. Unit suite unchanged (2674 passing).
The #44 half of the original work is intentionally dropped: upstream's
track-regression.mjs already fixed the ground-3d sampleHeight floor, and more
thoroughly — it pins no absolute call count at all and adds a guard against a
timed-out driver falsely looking flat.
Closes#54.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01Q6VRsPswjNtZfv4jzgkCUf
Add explicit aria-label attributes to unlabelled interactive controls in index.html and dynamic parameter sliders in src/ui.js, fulfilling WCAG 2.1 Success Criterion 4.1.2 (Name, Role, Value).
- Add aria-label to #scope-feather-slider, #bloom-intensity-slider, #sharpen-intensity-slider, #location-search, #scene-import-file, and first-run suppression checkbox.
- Set aria-label to uniform label on dynamic style parameter sliders in ui.js.
- Add unit test suite (src/hudA11y.test.mjs) verifying accessible names across all inputs in index.html and dynamic slider construction.
Signed-off-by: Kushagra Kumar <[email protected]>
The keyless fallback landed on the LOCATION search box only. Two other places
forward-geocode a name and neither had it: the annotation resolver returned
null, so an annotation silently failed to anchor, and the Radio layer THREW,
which surfaced as a failed voice turn rather than as a station it could not
place. src/keylessGeocoder.test.mjs now COUNTS the call sites instead of
checking them file by file, so a second seam added to a file that already has
a fallback fails the suite too.
The annotation path carries the feature's own name out of Photon. Footprint
scoring matches OSM on that canonical name rather than on the user's words, so
incidental locality tokens cannot win: "the monument near the Texas Capitol,
Austin" scored on the raw utterance picks a downtown building that shares only
"Texas", "Capitol" and "Austin".
Photon's country is deliberately NOT re-exported. It comes back in the
feature's own language — "Việt Nam", "日本", "Россия" — and
normalizeRadioCountryInput fails CLOSED on a name it cannot map, so forwarding
it would filter the station list to nothing for exactly the places the lookup
had just resolved. The composed label still names the country. A keyless
lookup must not apply a filter the keyed one never applies.
A not-found is now remembered only when every source consulted returned a
verdict. Photon not replying is a network blip, not "no such place": memoizing
it kept the query answering not-found for the rest of the session on a network
that had since recovered — the same conflation the Overpass disk cache had to
be repaired for. geocodeMissIsDefinitive states the rule once and both entry
points use it, and the adapter's own memo follows it as well.
Reverse geocoding stays out of scope: the street-label reader consumes
Google's ranked address_components, and Photon's /reverse cannot reproduce
that shape.
Photon scores proximity against the name, so a viewport bias could outrank
an exact name match by distance alone. Searching "Hue" from Austin returned
Hutto, Texas; "Ha Long" returned Long Branch. Measured against the live
service over twelve Vietnamese place names, three resolved to the wrong
continent.
Google's `bounds` prefers a result inside the box without changing what
qualifies as a result, and that is the semantics this adapter promised to
reproduce. So a biased answer is now accepted only when a candidate leads
with the name searched for, and the query otherwise re-runs unbiased — one
extra request, on exactly the searches that were going to be wrong.
Matching leads rather than contains, which is what separates "Hue" from
"Nguyen Hue Road", and compares unaccented so "Hue" and "Huế" are one name.
On the unbiased pass the head of a "place, region" query is matched first;
the tail only says where to look.
Nearby searches are unaffected: "Sixth Street" and "Barton Springs" over
Austin still take the biased answer in a single request.
Place search was Google-only: searchAndFlyTo threw without a key, and with a
key whose Geocoding API is not enabled it returned nothing. Both cases leave
the LOCATION box and the fly_to_location voice tool unable to resolve a name,
even though every other layer in the app works keyless.
src/keylessGeocoder.js adapts Photon (komoot, over OpenStreetMap) into the
shape the Google path already produces — lat/lng, label, Google-style types,
{southwest,northeast} bounds — so geocodeNavigationMode, regionFramingPlan and
the region-swath cap keep running on unchanged inputs. The translation lives at
the boundary and nowhere else; Google remains primary and its branch is
unchanged when it answers.
Detection is the empty result, not an exception: an unenabled Geocoding API
answers HTTP 200 with REQUEST_DENIED. Places near-view recovery is now
attempted only when a key exists, since Places is a Google service too.
Three translations are load-bearing, and each was found by querying the live
service rather than by reasoning about OSM tagging:
- Google's `bounds` PREFERS results inside the box; Photon's `bbox` FILTERS to
it. Sent as a bbox, searching "Ho Guom, Ha Noi" while looking at Austin
returns features: []. Sent as Photon's lat/lon proximity bias it finds the
lake, and "Sixth Street" still resolves to Austin's over Austin and to
England's over London.
- Photon's `extent` is [west, north, east, south]. Read as [w,s,e,n] it yields
an inverted but finite box that frames the camera at nothing.
- A specific OSM key=value rule must beat the coarse `type`. Photon tags a lake
`water=lake` with `type: 'other'` — unmapped, that framed Hoan Kiem Lake as a
precise place and put the camera 26 m over the water — and tags a town square
`type: 'locality'`, which framed Times Square as if it were a city.
searchAndFlyTo no longer throws when no key is present. Both callers already
read a null return as not-found (ui.js renders its miss branch, gevActions
reports ok: false), so the removed throw has no consumer.
Verified end to end through the real search box, with a key Google refuses:
one denied Google geocode, one Photon request, camera from Austin
(30.2700, -97.7400) to 21.0288, 105.8524 — Hoan Kiem Lake to within metres —
and "Ha Noi" framed at province scale. npm test 2604/2604; npm run build
clean; npm run test:track 97/3, the same three failures unmodified main
produces on this machine.
Recover across existing Overpass mirrors, reject cached refusals, and serve consistent last-good data to coalesced callers. Preserve the original contribution from PR #165.