Read OpenSky OAuth credentials from OPENSKY_CREDENTIALS_FILE and stop sending retired Basic authentication. Update setup diagnostics and guidance.
Keep credential-file warnings free of parser excerpts and filesystem error details, cover both failure paths with a regression test, and use a launcher-compatible absolute path example.
Original contribution by quanticsoul4772, with credential-warning and documentation follow-up by Sameh Khamis.
Co-authored-by: quanticsoul4772 <[email protected]>
Co-authored-by: Sameh Khamis <[email protected]>
Wrap longitude differences in annotation proximity checks and nearby-place distances so points across the antimeridian remain close.
Original contribution by quanticsoul4772.
Co-authored-by: quanticsoul4772 <[email protected]>
* feat(street-level): export the KartaView and Panoramax providers
A host that composes Street Level with its own provider list can already
import the Mapillary and Google Street View providers; KartaView and Panoramax
were reachable only through the default list, which needs a Mapillary source.
Exporting them lets a host offer the keyless providers without Mapillary.
* chore: changelog entry for the Street Level provider exports
* feat(street-level): send Mapillary Graph lookups to a source endpoint
A Mapillary source can take a graphEndpoint, a Graph API base that adds
the token itself. Its sequence lookups go there through the source's
transport, and the photo viewer uses MapillaryJS's stock GraphDataProvider
at that endpoint, so a host that proxies Mapillary needs no client token in
the page. Without it, behaviour is unchanged: direct Graph API calls with
the client token.
The proxy now skips malformed upstream rows individually but counts
them, capped at 10,000, in the catalog metadata and the /stations
envelope (rejectedStationCount). The count never fails health policy,
so an otherwise healthy generation is still accepted. The client
validates the field, adds its own rejected rows after admission, and
reports degraded coverage when either side skipped rows. Adds an
end-to-end proxy-to-client test and aligns DATA_SOURCES.md and
CURRENT-STATE.md.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Clear _rejectedStationCount alongside _degraded so a destroyed and
re-initialized layer does not report the previous catalog's count.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A caller that joins a cold reservation no longer inherits another client's
RATE_LIMITED result. Once the rejected reservation is released it retries:
it either owns the next reservation and is admitted on its own quota, or joins
an admitted fetch. Adds a two-client regression (exhausted owner, available
joiner, exhausted joiner coalescing onto the admitted fetch).
Also drops the duplicate memory publication for a disk hit in the handler;
preflight already sets it, the handler only trims.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- The DOCTYPE scan now jumps over whole comments and processing
instructions found outside a declaration string literal, so a quote or
bracket inside them no longer leaves the scanner stuck before the root.
- Root namespace repair walks the root's attributes in order, so
declaration-like text inside a quoted value can no longer fill `seen`
and cause the real declaration to be dropped as a duplicate.
- Regression tests for both, plus linear-time cases in the isolated
deadline test.
- CURRENT-STATE: describe the scanner as a boundary check and leave the
rest of the XML grammar to DOMParser.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- CelesTrak allowlist includes `active`, which Space Missions'
getActiveTle() requests; unknown groups still answer 400.
- The CelesTrak gate uses admitSameSiteKeyless so the documented reverse
proxy keeps working while foreign/opaque Origins and cross-site
Sec-Fetch-Site are still refused with 403.
- A provider failure never shortens an existing cooldown (CelesTrak and
Launch Library keep the later deadline).
- Tests: real getActiveTle() through the middleware, proxied app request
vs unrelated Origin, concurrent failures finishing in both orders.
- docs/CURRENT-STATE.md: eight groups, keyless gate, non-shortening cooldown.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The standalone military source no longer has getTrack, so the MCP
get_aircraft_track tool threw a TypeError and MCP advised a retry. It
now returns an `unsupported` ToolError when the selected feed has no
getTrack, while custom sources that supply history still work. Tests
cover the default composition through the catalog and MCP.
Also drop the unused epoch import and update the CURRENT-STATE track
sections to describe local trails and 503 history_unavailable.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A move inside the 120 ms hover throttle window was dropped, so a pointer
that stopped right after a pick kept a stale cursor (pointer over empty
space, or default over a mission) until the next move. Keep a copy of the
last suppressed position (Cesium reuses the movement object) and run one
trailing check when the window ends. The check rechecks hover eligibility
and is cancelled by reset, disable, camera motion, and destroy.
Tests cover a stopped pointer in both directions without another event,
the copied position, one timer at a time, cancellation, and the
eligibility recheck.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
An overdue refresh on return to a visible tab advanced the due time but kept
the old setInterval phase, so the next phase tick was skipped and the gap
could approach two intervals (also after a failed resume refresh). The
catch-up now re-arms the interval at the new deadline. Interval ticks no
longer compare against the due time, since the phase and the deadline share
one origin. Overlap prevention and teardown are unchanged, and a stale
listener cannot re-arm a stopped loop.
Tests cover the next poll one interval after a between-ticks resume, with a
successful and a failed catch-up refresh.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A closure that removes the last animated points mid height correction
released the render hold, and animate() then returned before easeHeights()
could run, leaving _heightPending (and getStats().loading) true. Settle
every retained road on its corrected heights before the hold is released.
Adds a regression combining height correction, a late closure and an
empty population.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
At 720px and narrower the panels live in the phone shell's tab sheets, so the
camera, scene and map-source tray harnesses open the matching tab (from the
keyboard where they check focus rings) before measuring, wait for the shell to
follow a viewport change, measure the container that actually scrolls, and run
the pin and compact-wing checks at 800px where the narrow desktop dock still
exists.
At 720px and narrower the globe stays full screen and a bottom tab bar (Layers,
Cameras, Context, Search, More) opens one bottom sheet at a time, presenting the
existing rail and dock panels in place. Sheets borrow panels without persisting
or sharing them and put back what they changed. Cockpit, recording and clean
view close the sheet; scene playback keeps STOP reachable. Focus moves into the
sheet and back to its tab, the required attribution stays visible above the
voice control and any sheet, and the page uses safe-area insets. Wider
viewports are unchanged.
Adapted from the phone tab-shell proposed in #203 by @blearymoth.
The README source tree now lists src/ui/ instead of the removed
src/ui.js. docs/CODE-BOUNDARIES.md names src/ui/applicationShell.js as
the owner of panel layout, persistence and share restoration, and
docs/CURRENT-STATE.md places GLOBAL_POST_DEFAULTS in
src/ui/visualPresets.js. QA script comments and test assertion messages
that still named ui.js are updated the same way.
An ESLint no-unused-vars scan over src, server, scripts, tools, build
and pinokio, repeated until it is stable, reported these.
Runtime modules keep every call and drop only the unread binding:
- src/ui/cockpitInput.js: the unread result of enter()/exit().
- src/layers/launches/rendering.js: the unread entity returned by
entities.add().
- src/layers/radio/rendering.js: the unread map key in the horizon scan,
which now iterates the map's values.
QA scripts and tools drop an unused helper, two unused in-page module
imports, unused counters and destructured fields, and an unread wait
result. Tests drop source files read into variables that no assertion
uses, redundant StyleManager imports (the shell-source helper already
imports it), and unused destructured fields.
The one remaining report is the `_` loop variable in src/tools/schema.js,
which counts iterations and is intentional.
QA harnesses import some modules in the page by path, for example
import('/src/data/radio.js'). Those calls name exactly which bindings
they use, and every other export of these modules has no importer:
- src/data/radio.js: 20 aliases of radio layer methods and two
re-exported altitude constants. The radio layer keeps the methods.
- src/layers/transit/movement.js: tuning constants and course helpers
used only inside the module lose `export`; FIX_HISTORY_MAX,
DISPLAY_LAG_MIN_MS, DISPLAY_LAG_RESET_MS, FIX_NOISE_M, STOPPED_FIXES
and courseBetween, which nothing reads, are deleted.
- src/overlays/worldOverlay.js, src/splitFlap.js,
src/data/contactPlayback.js and src/voice/cloudVoiceAuth.js: helpers
and constants used only locally lose `export`.
- src/data/local_data/dams/dams.geojson held the same 704 features as
dams.geojsonl, feature for feature. Only the JSONL file is loaded
(src/sources/infrastructureData.js), so the GeoJSON copy is dropped
and the dataset README no longer mentions it.
- public/models/ship.glb is not loaded anywhere: no module, template,
stylesheet or script names it. Its row in public/models/README.md
goes with it.
Together these remove 960,345 bytes of tracked data.
Knip, configured with the application, package exports, scripts, QA
harnesses, workers, tools and tests as entry points, reports exports
that no module imports. Each was checked by hand against string-based
and namespace imports.
- Symbols that are still used inside their own module lose only the
`export` keyword (176 constants and helpers).
- Unused re-exports are removed from barrel modules (flight, military,
vessel, CCTV, directions, transit history, local ADS-B and street-level
policy modules).
- Declarations that nothing uses at all are deleted, among them
AIS_WATCHDOG_STATUSES, applyFilter, isRadioCountryCode, compactText,
DENSITY_STOPS, the unused CCTV endpoint constants, and the SDR band
constants.
- Default exports that nothing imports are removed. The ones in
src/data/earthquakes.js, src/data/telegeographySubmarineCables.js,
src/data/wind.js and src/data/bhoteKoshiLocator.js each constructed a
layer instance at import time that nothing used; those instances are
no longer created.
Every export of a package.json entry stays, as does TOMTOM_CREDIT,
which the traffic layer reads from the credits namespace it is given.
Delete three modules that no entry point, package export, test or script
imports:
- src/ui.js, a StyleManager subclass that pre-selected the standalone
catalog. Application controls are built through src/app/controls.js
and src/standalone/controls.js.
- src/standalone/ui.js, a bare re-export of StyleManager.
- src/data/localLayers.js, a list of default infrastructure, cable and
FIRMS layer instances. The catalog builds these layers itself.
With src/ui.js gone, the import-direction check keeps a single
compatibility exception (server/providers/local.js to
server/standalone/key-setup.js), and its test now asserts that a
src/ui.js importing standalone code is rejected. The boundary and
format registries and the docs that named these files are updated.
Remove locals that are assigned but never read: two unused
JulianDate.now() calls in the flights and military nearby queries, an
unread record lookup in the military snapshot renderer, unused axes from
the CCTV gizmo, an unused vessel state alias, and two unused ALPR
presentation handles. Each removed expression is side-effect free.
Drop module-local helpers that nothing calls (linkAbort, summarize,
stringOption, summarizeEntity and the polygonCenter, propertyObject and
unwrapProperties helpers only it used, CALLOUT_DOT_SIZE_PX) and unused
import specifiers in runtime modules, tests, QA scripts and tools.
None of the removed bindings is exported. Re-exports such as the ALPR
policy constants and every package export are unchanged.
CAMERA_PRESETS and flyToPreset have no callers. Startup uses flyToAustin,
which stays, and city navigation uses flyToPresetLocation from
src/locations.js. Neither removed symbol is a package export.
i18n: Simplified Chinese (zh-CN)
Add a complete Simplified Chinese translation and language picker support.
Improve Chinese typography, date formatting, and localized readouts.
Include translation guidelines and regression tests.
Based on translations by abiyeenzo (#914) and EDIT-Aeterna (#979).
Co-authored-by: German Khokhlov <[email protected]>
Co-authored-by: Enzo Abyie <[email protected]>
Co-authored-by: EDIT-Aeterna <[email protected]>
A fresh full review of the catalog (52 changes in 48 entries):
- Age readouts that render as bare durations got their missing 前
(LIVE · updated {age}, POSITION {age}).
- Unified the rate-limit family on 已被限流 (12 entries had 已限流,
正在限制请求频率 or 受到频率限制).
- Unified photorealistic on 照片级逼真 (planet strings, 3D Tiles).
- Round-1 stragglers: 括标 → 括号标记, FLIGHT/VESSEL WINDOW →
监视窗口, mapped-ALPR loading strings, plain Talk → 脱口秀, the
hardcoded Current style twin.
- Collocations: 与…同源, 不超过…上限, 单击放置顶点, 随语音分享.
- Mainland usage and consistency polish: 气象高度, 地面风, 标签页,
已获取航迹 (acquire ≠ lock), 缩放级别 6, 发布时间, 独立视角,
视野中心, 播报当前视野, and similar one-word fixes.
Kept deliberately: the 图像 (image) vs 影像 (imagery) distinction,
which mirrors the English and is consistent across the catalog.
Review of all 2,677 zh-CN entries (scripted checks + full meaning
review) confirmed the catalog fully Simplified and mechanically clean.
83 fixes in two passes:
Meaning and terms — PARTLY CLOUDY lost 'partly' (多云 → 晴间多云);
AIR/SEA WINDOW said scheduled flights (航班 → 空/海监视窗口); Mapped
ALPR Cameras collided with ALPR Cameras; globe terms unified on 地球
(球体地图/球体影像); later/earlier observation buttons read as future
actions (稍后/早前 → 较晚/较早); 从未叠加… misread as 'never'; 风流线
→ 风场流线; 访问飞行器 → 来访航天器; 目击 → 目击者; downrange is
落点距离 not 航程.
Polish — 加载 over 载入; 预警 for advisories; {amount}K/M/B 美元;
空载; 窗口 over 视窗; 分享的 {subject} matches its siblings; missing
CJK↔Latin spaces restored around {store} and {mode}; 拦截点击,
已被限流, 精选, 逼真世界, and other one-word mainland-usage fixes.
The router reads an absolute-form target's path without resolving dot
segments, so a target such as http://host/api/../x still reaches /api
middleware. Take that path the same way when deciding whether the API
response headers apply.
The Street View fallback now treats any forwarding header as forwarded, even
an empty one, before it spends the key.
The URL parser already turns backslashes into separators, so only encoded
separators need refusing; a pin prefix without a trailing slash is treated as
a directory.
The CCTV routes now use the keyless cross-site checks, which refuse cross-site
browser requests but accept forwarding headers, so cameras work behind the
documented reverse proxy. The Street View fallback is the one step that spends
a key; a request with forwarding headers skips it and gets the placeholder
frame, as when no key is configured.
The router mounts /api without regard to letter case, accepts a '.' as well
as '/' after the prefix, and routes absolute-form request targets, so the
API response headers were missing on those spellings. Match the same way, on
both the raw and the parsed path. The test now runs real dev and preview
servers.
Camera catalog image URLs were pinned by string prefix, so a URL that only
began with the agency's directory before dot segments or an encoded separator
were resolved still counted as inside it. The pin now parses the URL and
requires the same scheme, the exact host, no credentials, the default port and
a normalized path inside the pinned directory. Redirects on a pinned host must
stay inside the same directory.
Document the response format checks and API response headers alongside the
existing server hardening notes.
CCTV frames and the Street View fallback accept only JPEG, PNG, GIF, WebP and
AVIF, parsed from the media type essence and sent under the canonical type;
any other type is cancelled and falls through to the next fallback as a
failed upstream does. Live media must match its feed kind (video/mp4,
video/webm, or multipart/x-mixed-replace with a boundary) before the camera
is reported healthy or any byte is relayed, and its redirects now stay on the
registered origin like frames. Every /api/cctv route uses the same-site gate.
The GBFS proxy relays only bodies that parse as JSON, re-serialized under a
fixed application/json type.
Every /api response, including errors, partial responses, Host refusals and
unknown routes, now carries a sandboxing Content-Security-Policy and
X-Content-Type-Options: nosniff on the dev and preview servers. Vite's server
and preview headers only reach the documents and assets it serves itself, so
a small plugin sets them ahead of the Host check. A route's own policy is sent
as an additional policy rather than replacing it.
Merge origin/main and retain both format-scope additions.
Give StarrySky a separate black environment cube map and release it on destroy.
Guard anime updates when StarrySky is absent, require a visible Normal sky
before anime QA, and compare a lit airplane against black and white controls.
Document the sky background contract and its verification.
Co-Authored-By: GPT-6 (Codex) <[email protected]>
Preserve Chinese unit casing and spacing in the HUD and cockpit, localize FIRMS cache ages, CCTV health and radio counts, and keep imagery resolution and route modes readable. Add regression coverage for localized output and unchanged English.
Co-Authored-By: Codex (GPT-6) <[email protected]>
Correct weather, imagery, scene, voice, and status wording after the Chinese UI review. Align the translation glossary with the revised terminology.
Co-Authored-By: Codex (GPT-6) <[email protected]>
Enzo's commit email is not linked to his GitHub account, so credit his #914
translation here as well.
Co-authored-by: Enzo Abyie <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Translate the remaining 910 strings and review the whole catalog (meaning,
terms, punctuation, label length) against the screens.
- Register zh-CN: any zh tag maps to it except Traditional ones (Hant, TW,
HK, MO), which keep English until a Traditional catalog ships.
- Hemisphere letters get their own context so the HUD sector line reads
21.33N, not a compass word; the Bhote Koshi imagery dates follow
year-first languages (2021年10月16日).
- cjk.css: tight tracking for Chinese text and a 9px floor on the dock voice
labels and layer buttons; Latin HUD readouts keep their look.
- docs/translations/zh-CN.md: style, plurals and glossary.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The Simplified Chinese localization by EDIT-Aeterna (#979), converted to the
English-keyed catalog: 823 more entries for UI text that #914 did not cover.
The Simplified Chinese locale by abiyeenzo (#914), converted from its
namespaced JS catalogs to the English-keyed zh-CN.json: 891 entries whose
English still matches the current UI.
- taiwan.js: a failed download serves the last good table (up to 24 h)
and is not retried for an hour. The table host does not answer from
some networks outside Taiwan, and each attempt held the shared catalog
refresh for the ~10 s connect timeout.
- Own server/providers/cctv/taiwan.js in package-boundaries.json
(check:boundaries failed).
- Decode numeric character references; name falls back to the CCTVID
when a row has no road details.
- Credit names the dataset and links the OGDL Taiwan licence.
- Document CCTV_TAIWAN_URL; list Taiwan in the CURRENT-STATE source table.
- Tests: isolate catalog env, reset loader state, cover the byte cap,
failed fetch, backoff, last-good fallback, cap clamping, redirect with
a valid body, name fallback, numeric entities and UUID camera ids.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Move the Hong Kong CCTV changelog entry into changelog.d/ (main now takes
fragments instead of CHANGELOG.md edits) and document CCTV_HK_ENABLED,
CCTV_HK_MAX_SOURCES and CCTV_HK_CAMERAS_URL beside the other packs.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Fetch keyless DATA.GOV.HK camera locations at runtime and pin stills to
tdcctv.data.one.gov.hk with required attribution. Default cap 1,100 covers
the ~1,013-camera TD catalog, prioritized across Hong Kong Island, Kowloon
and the New Territories.
Co-authored-by: Cursor <[email protected]>
The slim-down in #974 dropped --sl-text-small, which .sl-since-note still
used, so with KartaView lit "KartaView: by upload date" rendered on its
own line larger than the value. The note now sits beside the value in the
same mono type and letter spacing, a size smaller (0.5rem), as
"(by upload date)". One source is lit at a time, so the provider name is
dropped. es/ru catalogs, tests, README, CURRENT-STATE and the registry
JSDoc follow.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Google 3D: the OPEN button re-measures a ground whose tiles are still
loading (a few times, a second apart) and on a map change, not only
after the next camera move.
- Mapillary: two mounts on one host during the first download share one
build instead of leaking a second WebGL viewer.
- Retired floating-panel keys (v8 panelPos, panelFloatHintShown) are
dropped from localStorage; dead inline-position resets, the drag JSDoc
and the dead qa-ui-disposal observer branch are gone.
- Docs: z ladder, panel chrome, position storage and the fixture gate's
actual coverage; changelog fragment for #974 (old .gev-cache/mapillary
can be deleted).
- ru/es: the layer is named as everywhere else in the UI.
- Fixtures and test fakes: unused exports removed; a fake credit removal
of an absent credit no longer drops another one; the tile test reset
frees upstream slots.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The far-plane comment said z = w (the code puts z just under w), the Milky
Way constant is a brightness, not an opacity, and the CDS acknowledgement
lives in the Data attribution popover, not DATA_SOURCES.md.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- qa-anime-sky keeps only the Milky Way case; the satellites and keyless
cases checked an anime shader this branch no longer changes.
- Drop qa-sky-sun-moon.mjs: the viewer-options unit test covers it.
- VisualEffects sets the sky's visibility directly: the viewer always
carries a StarrySky and never outlives the effects.
- Drop the unused star intensity knob, the malformed-row tolerance for
our own pack, the test-only Transforms parameter and a private-name test.
- DATA_SOURCES points to sky/README.md instead of repeating it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Scene reads skyBox._cubeMap as the environment map that lights models, in
the TEME, flipY layout of Cesium's own sky box; the ICRF Milky Way would
light them rotated. Rename the field so models keep Cesium's default.
Test the anime fade on a real StarrySky and that the viewer gets the sky
as an option (skyBox: false would drop the Sun and Moon). Correct three
comments on the star limit and the Milky Way levels.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The fine 0.4 degree blur kept the residual glow of unresolved stars as
mottled patches at the band's edges. Blend towards a 2 degree blur where
the coarse copy is faint (smoothstep over 0.5-8 % of the range), so the
core keeps its dust lanes and the faint sky goes smooth. Faces 85 -> 62 kB.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Resolve conflicts with #1234: keep redirect: 'manual' on both DATEX
fetches and keep main's 30 s TARKTEE_FETCH_TIMEOUT_MS. Drop this
branch's 15 s constant and its import, which auto-merged into a
duplicate export. Keep both sets of Estonia tests and drop the 15 s
claim from the changelog fragment.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
page.waitForFunction polls on requestAnimationFrame by default. In a
minimised or hidden headful window rAF stops while timers keep running, so
each 120 s slice only checked the predicate when it started: a drain that
finished in the last slice was recorded as a FAIL on the sign-off path.
- waitForPredicate replaces waitForFunctionSliced and the other three
waitForFunction calls: one short page.evaluate per tick (1 s for the
drain, 250 ms elsewhere), a monotonic performance.now() deadline, retry
across a context swap, and a throwing predicate fails the run instead of
silently spending the budget.
- One in-page window.__qaRenderFrames(count, capMs) helper serves both the
gizmo-drag and the sibling-owner frame waits.
- The sibling pick counts as a stalled renderer only at 0 of 3 frames, so a
slow SwiftShader frame still reports a real miss; the record flattens and
prints the frame count once.
- Review screenshots log and continue when a capture fails.
- Launch with pipe: true (no localhost CDP port) and protocolTimeout
300_000 like the other harnesses; drop the occlusion flag that puppeteer
already passes by default.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
When the sibling-owner pick misses after fewer than 3 rendered frames, the
cause is a stalled renderer, not a pick regression. Record it as a failure
on the headful sign-off path and as inconclusive under headless GL, with
"renderer stalled: N/3 frames" in the detail. A missed pick after 3 frames
still fails in both modes. Also corrects the catalog-size comment (~3,700).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The headful drain budget is uncapped (camCount * 800 + 30000 ms, ~50 min
at N≈3,700), but each page.waitForFunction is one CDP call that puppeteer
aborts after its 180 s default protocolTimeout. The .catch then recorded
"timed out waiting for drain on the real-GPU sign-off path" after ~3 min.
- Poll the drain in <=120 s slices (waitForFunctionSliced) so no single
CDP call outlives protocolTimeout, whatever the catalog size.
- Launch with protocolTimeout 240 s, headroom above the largest slice and
the 60 s fixed waits.
- Bound the sibling-owner 3-frame postRender wait at 10 s and report the
frame count, so a renderer that stops producing frames fails with
evidence instead of hanging the evaluate.
- List --disable-backgrounding-occluded-windows explicitly. Puppeteer
25.10 already passes it by default, so this documents the headful
contract rather than changing behaviour.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Conflicts: the Street Level and Gemini voice strings both appended to the
es/ru catalogs, and both sides' modules to package-boundaries.json — kept
both. scripts/qa-panel-resize.mjs stays deleted (floating panels removed).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Away from the galactic plane the ESO panorama keeps a mottled glow of
unresolved stars and dust. With the black level at the darkest quarter of
the sky and a hard cut, it showed as grey clouds with black holes around
the globe. The black level now sits at the median sky and eases in with a
soft knee on luminance, keeping each pixel's hue: high galactic latitudes
are black, the band keeps its glow. Faces shrink from 247 to 85 KB.
The changelog fragment also notes the anime fade and the new asset weight.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- VisualEffects.destroy() puts skyBox.milkyWayVisibility back to 1, as it
already restores the borrowed bloom stage, so a surviving viewer does not
keep a faded sky.
- Rename scripts/qa-anime-space-mask.mjs to qa-anime-sky.mjs: rewrite the
header for the current approach, drop --space-expr and --repo, import
puppeteer as a package, reject unknown cases and count the milkyway case
as reached only while the anime stage is on.
- Put both sky QA scripts under format:check.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
in push-to-talk, answer a Gemini tool call that arrives with no input, reply
or tool result outstanding with a failure instead of running it, and refuse
every other call in the reply it starts, including one after unprompted
speech; the reply stays tracked so the next input retires it and its late
boundary cannot close the user's turn
in push-to-talk, a caption that arrives after its turn ended no longer opens
a reply, so the next request is not retired with its tool calls discarded
open mic cannot tell either case from a new reply and stays unchanged
check packet ownership after the narration callback before queuing tools,
and raise the per-connection call history to 4096 IDs
move the pointer turn event, context item and announcement into
pointerContext so the OpenAI and Gemini controllers share them, and drop the
Gemini token route's copy of the shared exact-Origin comparison
- The selected sequence is drawn past the per-tile caps again, and its
highlight takes every loaded part, drawn or not: in a dense tile a
clicked Mapillary or Panoramax line kept its cones but lost its line.
- photoViewer tries :fullscreen and :popover-open one at a time, each in
its own try: Safari before 16.4 parses neither, and the fallback threw
on every flat KartaView or Panoramax photo.
- A lit Street View off Google 3D says it needs the Google 3D map again,
instead of "every provider is switched off".
- Chips carry their provider colour again (--chip-color, as on main).
Each comes with the test that reproduced it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The far-plane mask in the anime shader read the post-process depth, which
cannot tell the sky from far geometry: with a layer that clamps the near
plane to 0.1 m (satellites) the whole Earth lost its cel bands, keyless
terrain above the ellipsoid horizon did too, and the atmosphere, Sun and
Moon stopped banding.
Restore the anime shader from main and fade the Milky Way glow with the
style instead: VisualEffects sets skyBox.milkyWayVisibility to
1 - anime intensity. scripts/qa-anime-space-mask.mjs checks all three
cases headless and that the anime sky shows no bands.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
On CI's SwiftShader the ground-clamped cones take a few frames after a
sequence is selected; a single pick search found none.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Default backoff is 30 min, longer than the 15-minute catalog refresh,
so a failing Kartverket costs one attempt per backoff, not per refresh.
- A point a 200 answer leaves out is cached as no height (6 h TTL)
instead of being asked again, and waited on, every refresh.
- Only 408, 429 and 5xx stop the lookup; any other 4xx marks that
request's points as no height and the lookup carries on.
- Each request has its own 10 s timeout inside the shared 30 s deadline;
a lookup cut short only by the deadline resumes without a backoff.
- The lane waits only on a cold cache (>10% of cameras missing); a few
new cameras or expired misses are looked up in the background.
- applyVegvesenGroundHeights skips cameras without finite coordinates,
so it really never throws.
- clearKartverketHeightCache bumps a generation, so a lookup started
before a clear cannot write to the cache, backoff or pending slot.
- Credit and DATA_SOURCES note the modification (water set to 0 m,
rounded to 0.1 m) as CC BY 4.0 asks; docs match the stop rules.
- Tests for the deadline being shared, per-request timeouts, 503,
oversized and non-JSON bodies, 4xx, left-out points, Retry-After end
to end, overlapping refreshes, the warm-cache wait, bad coordinates,
the cache-clear generation and the loader's wait budget.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- A background catalog refresh that rejects now keeps the old catalog for
another TTL, as an empty refresh already did, so a refresh that keeps
failing is retried once per TTL instead of on every request.
- Trim notes are keyed by the source list they describe, and /sources asks
for the list it served, so a request answered during a refresh never pairs
the old cameras with the new list's notes.
- Tests: a failed background refresh (one warning, no unhandled rejection,
no retry before the TTL, trim notes stay with their list) and an empty
background refresh (old catalog kept, no upstream fetch before the TTL).
The race assertion's message now names the failure; its timer is cleared.
- The Tarktee test no longer imports the new constant, so on main it fails
on its own assertion instead of taking the whole file down.
- precompute-cctv-heights waits 60 s for a cold catalog, past Tarktee's 30 s.
- CURRENT-STATE notes the 30 s bound and the retry spacing.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Assert the key never reaches the camera records sent to the browser or
the "Loaded" log line.
- Count fetches in the keyless test instead of relying on assert.fail
inside the mock, which the loader's catch would swallow; a
whitespace-only key is treated as missing.
- Cover the key hint for 400/401/403 and its absence for 429/500/503,
and log the failure as one string without a trailing empty argument.
- Add _resetOntarioKeyWarningForTest so the logged-once check no longer
depends on test order.
- Read CCTV_ONTARIO_MAX_SOURCES from the same env as the key, and keep
console output out of the suite.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
refreshCctvFocusStyles passes cameraColorFor as baseColorFor, but only
refreshCoverageStyles was tested. The new rendering.test.mjs runs the
focus pass through createRendering with stubbed focus services and
checks that an idle video camera fades from violet, a still from cyan
and the active camera from amber. It fails if the focus callback goes
back to the old active/idle check. Also adds the missing semicolon on
the policy.js import in cctv.test.mjs.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Drive drawProjectionFrame with a fake runtime and a recording canvas:
a 640x480 still draws at its fitted rectangle over the matte, the same
pixels at the same size skip the redraw, and the same pixels at a new
aspect redraw at the new fit. The last case fails on the pixel-only
signature; the first fails if drawImage stretches to the canvas again.
Expected rectangles now read the canvas size from policy.js.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Pass the custom sky box to the Viewer instead of `skyBox: false`, which
also stopped CesiumWidget from creating the Sun and the Moon. Guarded by
scripts/qa-sky-sun-moon.mjs.
- Outside the IAU 2006 tables (before 1975, after ~2050) the preload
resolves without data; remember that span and stay on TEME instead of
preloading and rendering every frame.
- The anime style no longer cel-shades the far plane, where four bands
turned the faint Milky Way into blotches.
- Paint the planets out after the star-removing opening and with the
ring median, so no brighter plateau is left; faces rebuilt.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The parity test compared getWorldPacket with voice, which now calls
getWorldPacket. Compare both against a fixed payload captured from main's
get_current_view_state before the extraction, key order included, and
cover a non-finite now.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A default parameter only covers undefined, so now: null aged every feed
against the epoch and reported stale data as just now.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- State FL511's terms the same way everywhere: individual, personal use
only, and any other use (a public deployment included) needs FDOT's
express written consent. .env.example, README, dataCredits, the source
licence string, the catalog comment and the changelog fragment said only
"individual, non-commercial use".
- Page the ArcGIS catalog on exceededTransferLimit alone and advance the
offset by the rows each page returned, so a lower server-side max
record count no longer cuts the catalog short at the first short page.
- Order pages by OBJECTID_1 (this layer's object id; a plain OBJECTID is
rejected with HTTP 400) so offset paging is stable.
- Raise the page ceiling to 40 and warn when it is hit.
- Tests: short pages with exceededTransferLimit keep paging, orderByFields
is sent, and a service that ignores resultOffset stops at the ceiling.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
aircraft_in_area stats summarized altitude over airborne aircraft but
speed over every match, so parked aircraft pulled the mean speed toward
zero. Both figures now use the same airborne subset, and the on-ground
fixture is stationary so the test can tell the difference.
Reserved AIS ship-type codes 100 to 255 now read as unknown, as type 0
does, instead of showing up in the breakdown as their own type.
Adds tests for an area where every aircraft is on the ground and for
find_vessel type and destination normalization, and notes in the
changelog fragment that every vessel answer is now normalized.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
#686 moved its changelog entry from CHANGELOG.md into
changelog.d/686-terrain-heights-bounds.md. Take #686's side so this
branch no longer edits CHANGELOG.md directly and no longer conflicts
with #686.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Attach the background-refresh warning where the refresh starts, so a
failed refresh logs once instead of once per request served meanwhile.
The test now checks that requests during a refresh share it, and turns
the hang off before releasing the fetches so a retrying loader can't
stall it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Mapillary was never in DATA_CREDITS (since #768), and Google Street View,
KartaView and Panoramax came without entries. HAL-530.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Read ~1,800 national freeway cameras at runtime from the Freeway Bureau's
keyless MOTC 2.0 CCTV.xml table. Each published MJPEG stream maps to the
same camera server's single-JPEG path; only those exact HTTPS pairs on the
four freeway.gov.tw camera servers are registered. On by default,
CCTV_TAIWAN_ENABLED=0 skips it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Main gained three imagery providers (Google Street View, KartaView,
Panoramax), shared line-coverage, cone and photo-viewer modules, one source
lit at a time and the translated interface. Their shared modules still
carried what this branch removes, so the merge ports them onto it:
- lineCoverage.js is this branch's coverage.js made provider-neutral
(draped lines near the ground only, the selection drawn over them, key
and rate-limit blocking); sequenceCones.js is sequences.js the same way.
Mapillary, KartaView and Panoramax all build on them; Mapillary moves
back to providers/mapillary/ beside the others.
- No ground casting, mesh sampling, surface modes, FOLLOW, legend, prewarm,
orbit overview points (Panoramax's density dots included), stale-tile
swaps or water-filled point budget come back.
- Kept from main because the new providers need them: requiresMapStack and
attachMapStackController (Street View on Google 3D only), ground clicks,
openNearest with an optional provider nearestImage, openImage {frame},
reportError, the view-cone marker (now an SVG glyph), filters /
coverageLabel / sinceBasis, OPEN STREET VIEW, keyAlternatives and i18n.
- photoViewer.js treats the viewer as expanded when its wrap is full screen
or the iPhone popover, as EXPAND now is.
- The fixture browser gate keeps its 8 steps and adds one each for
KartaView and Panoramax; catalogs, docs, boundaries and format scope
follow.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Cesium's default Tycho-2 sky box is six 1024² JPEGs stretched 3–6× around
the whole-globe view, so space read as an over-compressed JPEG (HAL-627).
The scene now draws its own sky in the Scene#skyBox slot: the 9,096 stars
of the Yale Bright Star Catalogue as point sprites sized in CSS pixels
(magnitude → width and opacity, B−V → colour), over a soft, star-free
Milky Way cube map built from ESO/S. Brunier's panorama (CC BY 4.0). Both
layers share one ICRF→Earth-fixed rotation at the scene clock, with the
TEME rotation filling in until Cesium's IAU 2006 tables load.
Assets total 482 KB, built by scripts/build-bright-stars.mjs and
scripts/build-milky-way.mjs. Frame time and the idle render governor are
unchanged.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
An expired catalog made every /api/cctv request (camera list and frames)
wait for the full refresh, which lasts as long as the slowest of the live
packs: Tarktee's DATEX feeds took 2 to 16.6 s across ten requests. With
Tarktee's timeout raised to 30 s, that wait could reach 30 s at each
15-minute refresh. The cached list is now returned while a single-flight
refresh replaces it in the background; only a first load, with nothing
to serve, waits. A background refresh that throws is logged.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The Estonian Transpordiamet feeds answered in 2 to 16.6 s across ten
requests (120 and 160 KB), and both must arrive for any camera to load,
so the shared 15 s catalog timeout dropped the whole pack on about a
third of loads ("Tarktee DATEX download error: The operation was aborted
due to timeout"), for the 15 minutes until the next catalog refresh.
TARKTEE_FETCH_TIMEOUT_MS gives the two requests 30 s; other packs keep
15 s.
Live: 175 cameras in 3.3 s.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Review fixes for #1228: CURRENT-STATE.md and the constants comment no
longer call Ontario 511 keyless. DATA_SOURCES.md notes Ontario 511's
Developer's Access Agreement (511on.ca/developers/resources): the data
stays under the Open Government Licence - Ontario, key requests are
reviewed case by case, attribution by link is encouraged and the logo
rules don't apply because the app shows none, and the "as is" disclaimer
should be passed on, which the credit now does.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A wrong or revoked key logged only "download failed: 400"; a 400, 401 or
403 now adds "(check ONTARIO_511_API_KEY)". The README's CCTV row listed
Ontario among the default cameras with no mention of the key it now
needs.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
511on.ca/developers is a 404. Sign up at 511on.ca/my511/register, then
request the key while logged in at 511on.ca/developers/doc (both checked).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The Ontario 511 camera list answered 400 "Invalid Key" on every catalog load
("[CCTV] Ontario 511 camera download failed: 400"), so the pack loaded
nothing. 511on.ca's developer docs now say the API "Requires a developer
key" (free, from 511on.ca/developers; 10 calls a minute). The loader reads
ONTARIO_511_API_KEY and sends it as `key`; without it the pack is skipped
with one log line instead of a failed request on every load. The key is
never logged. .env.example and DATA_SOURCES no longer call the endpoint
keyless.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Main now takes pending entries as changelog.d/<pr>-<slug>.md
files (#787), so the entry leaves CHANGELOG.md for changelog.d/686-terrain-heights-bounds.md.
AIS sends ship types as ITU-R M.1371 codes, so vessels_in_area grouped
"70", "74" and "79" as three types, and type: "tanker" matched nothing on
feeds that send codes. Codes now read as the family the vessel cards already
show (CARGO, TANKER, TUG), for rows, group_by and the type filter. The code
table moves to src/data/vesselTypes.js, which has no i18n import, so the
server-side tools can use it; vesselLabels re-exports it.
A destination with no letters ("0", "---") now counts as unknown.
Live, Singapore: "most by type: CARGO (3), TANKER (3), PASSENGER (1)"; "how
many tankers near Singapore" found 3.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ICAO flight-plan Item 7 caps aircraft identification at seven characters, so
the flight number after the designator is one to four characters, not five.
Philippine RP-C1234 and Cuban CU-T1240 registrations read like a designator
plus a flight number once the hyphen is dropped; they now count as unknown.
Cubana's CUB123 still counts as CUB.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- dataCredits `gbfs`: names the seven PBSC operators, with BiciMAD's
required "Powered by EMT de Madrid" linking to emtmadrid.es.
- dataCredits `jcdecaux-gbfs`: adds Valenbisi and Sevici.
- DATA_SOURCES.md: a BiciMAD row (Madrid's open-data catalogue publishes
the feed under EMT's terms, attribution required), one row for the other
six PBSC feeds (no licence published; attribution to the operator), and
the JCDecaux row widened to Valencia and Seville.
- CURRENT-STATE.md: the Bikeshare row, and the underscore and
geofenced-capacity rules in "GBFS upstream bounds".
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Adds the station-based public bike systems of seven Spanish cities on
PBSC and two on JCDecaux, 1,843 stations in all (live feeds, 2026-10-17):
- PBSC (`<city>.publicbikesystem.net`, GBFS v2 like Bicing): BiciMAD
(Madrid), Bizi Zaragoza, BIKI (Valladolid), BiciCoruña, Dbizi
(Donostia-San Sebastián), Bilbao Bizi and BicinRivas (Rivas-Vaciamadrid).
- JCDecaux (`api.cyclocity.fr/contracts/…`, like Lillestrøm): Valenbisi
(Valencia) and Sevici (Seville).
Both hosts are already allowed by the proxy. Every system uses a 40 km
load radius; the farthest station is 11.7 km from its centre (BiciMAD).
The radius also keeps out what two feeds list away from their city:
Sevici's workshop at (0,0) and lab in Madrid, and BiciCoruña's planned
stations with no position yet.
spainCitiesSystems.test.mjs checks the registry and proxy for each feed,
that the outermost live stations activate their system and sit inside
its radius, that central Madrid activates BiciMAD and BicinRivas, that
the far-away stations are not loaded through the real ingestion path,
and that JCDecaux names arrive without underscores and PBSC capacities
as docks.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
PBSC adds the bikes a station accepts in the geofenced area around its
docks to `capacity`, and reports that part as `geofenced_capacity`, which
is not a GBFS field. Bilbao Bizi publishes 39 docks as capacity 839 with
geofenced_capacity 800, so its stations would read as nearly empty: red,
drawn at maximum size, and carded "39 avail · 0 docks · 839 cap".
BiciMAD (671 of 678 stations, about twice the docks) and Dbizi (11 of 70)
do the same.
The shared parser now reads `capacity − geofenced_capacity` when the
extension is present and no larger than the total, so the layer, its
colour ratio and get_bike_share all see the docks. No registered system
changes: none of the 52 publishes a geofenced capacity above 0 (live
feeds, 2026-10-17, all 13,397 capacities identical).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
JCDecaux's Spanish contracts prefix every station name with "_", and
Valenbisi joins its words with it ("_PLAZA_MUSICO_LOPEZ_CHAVARRI"), so
the card and HUD would show the raw identifier. The shared parser now
reads a run of underscores, with the spaces around it, as one word break
and trims the ends. It applies to `name`, the `short_name` fallback and
GBFS v3 localized names, so the layer and get_bike_share both read it.
No registered system changes: run over all 52 live station_information
feeds on 2026-10-17, the old and new parsers give identical names for
all 13,397 stations. Other spacing stays as the operator published it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
OpenSky, the default civil feed, reports no operator, so group_by: operator
said "no operator reported". aircraft_in_area now also takes
group_by: 'airline', read from the ICAO designator in each callsign: three
letters followed by a flight number that starts with a digit (SWA1234,
UAL12A). Registrations used as callsigns (N123AB) count as unknown. Rows are
unchanged; the designator is derived for the breakdown only.
Live, Texas: "785 aircraft ...; most by airline: AAL (89), SWA (65), UAL (52);
283 unknown."
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Providers write cache files and logs into .gev-cache and .gev-logs while the app runs. A serve-only standalone plugin adds both to server.watch.ignored, so the dev watcher no longer stats them; source changes still reload.
Route tleCatalogNumber() and noradIdFromSatnum() through one decoder that
accepts only plain digits or a valid Alpha-5 field. parseInt and Number()
read '2554A', '255 4', '-1234', '0x1F0', '1.5' and blank as numbers; they
now return null/NaN. Digit satnums of any length still decode, so OMM
satrecs numbered past 99999 keep working.
Add a layer test that several Alpha-5 satellites survive ingestion dedupe,
the changelog fragment for #767, and note in CURRENT-STATE that this is
Alpha-5 TLE compatibility and malformed fields are rejected.
World Packet #880 removes the duplicate tracking table from gevActions; the Cockpit refusal source-scan test should inspect the actual exported source of truth instead of assuming the previous file position. Fix failing Node 24 exact-head CI without duplicating runtime definitions.
Reuse one context/contacts/tracking owner in voice and World Packet. Compare voice outputs after stripping presentation fields; remove the unconsumed fingerprint API pending an actual caller. Document the exported packet boundary and reject null options safely.
Review guidance: asturksever on origin #880; Windows parity diagnosis by quanticsoul4772. Existing source ownership retained.
CONTRIBUTING.md said "Only a couple (`qa:map-source-tray`, `qa:transit`) have
an `npm run` alias". package.json defines five: `qa:map-source-tray`,
`qa:panel-resize`, `qa:street-level`, `qa:street-level:fixtures` and
`qa:transit`.
The two missing names are not incidental -- .github/workflows/ci.yml already
invokes `npm run qa:street-level:fixtures` and `npm run qa:panel-resize`, so a
contributor reading the guide is told to reach for `node scripts/<gate>.mjs`
for gates the project itself runs through npm.
Verified against package.json scripts and .github/workflows/ci.yml on main
(1df2a27).
Reconcile #880 with current main. Delegate the view-state builder while keeping enabled-only layers, disabledLayerCount, provenance, context semantics, and existing voice behavior. Add parity and roster-read regressions; retain package export, boundaries and changelog fragment.
Preserve original GEV voice, layer and packet contributor credit.
Carry reviewed #816 onto current main without conflicts; preserve i18n updates, replace obsolete changelog edit with a fragment, and add a constructor-wiring regression alongside the owner and rail tests.
Credit: raysung-2026 (report), asturksever (review), quanticsoul4772 (Windows diagnosis).
Review follow-up on #975. The shared same-site gate refuses any forwarding header, which is right for routes that spend a key, but terrain is keyless and needed for rendering: behind the reverse proxy the README documents, every /api/terrain/heights chunk got 403 and fell back to the geoid. Terrain now uses admitSameSiteKeyless, which keeps the Sec-Fetch-Site and exact-Origin checks (comparing Origin against a single X-Forwarded-Proto scheme) but does not refuse forwarding headers. The shared admitSameSite gate is unchanged for every other route. The changelog entry moves to changelog.d/975-terrain-cross-site-gate.md.
Co-authored-by: daikaginza <[email protected]>
The CCTV frame proxy marks a Street View fallback with X-CCTV-Source, but the portable source dropped the header and get_cctv_snapshot always reported a current view credited to the camera's agency. The source now keeps the recognised provenance, and the tool labels Street View frames as historical Google imagery, refuses synthetic placeholders, and reports unknown provenance without claiming a current view.
destroy() now expires every retry attempt in flight, clearing its deadline
timer and aborting its signal at once rather than when the attempt settles,
and leaves no retry marked as running.
A successful terrain retry overwrote the cached fallback without releasing
it. The replaced fallback is now disposed as soon as the real terrain takes
its place on screen, or, when it stayed installed behind a 3D tileset, once
the next globe activation replaces it.
An Esri or keyless terrain attempt that never settled kept the single-flight
retry open forever, so neither provider was retried again. Each attempt now
counts as failed after 60 seconds and the retry backs off as usual; its
signal is aborted, and a provider that lands after the deadline is released
rather than installed or cached.
CURRENT-STATE now says only the construction fallback to OSM is retried, not
the switch made after repeated Esri tile failures.
An activation that re-showed a fallback while a retry was in flight armed a
second timer, so two retries could run at once and land out of order, leaving
the cache and the screen on different providers. Retries now run one at a
time; a request made meanwhile re-arms once the current one ends, and a
result that is obsolete or no longer wanted is disposed.
Imagery and terrain retries are now handled as each settles, so a stalled
terrain attempt no longer keeps recovered imagery off screen, or the reverse.
Main now takes release notes as one fragment per pull request (#787). The browser asks Google through /api/google/geocode, so the no-geocoder-request check now watches that route.
renderDetails() revealed the card inside render(), whose preserveScroll() wrapper restored the previous scrollTop in its finally block, undoing the reveal. The reveal now runs in the DETAILS open handler after render() returns, once per opening; closing and refreshes do not scroll.
A superseded open wait cleared the shared cancelShown slot that belonged to the newer wait, leaving its auth listener and timer alive after close; open() now settles the previous wait first. A lookup whose viewer unmounted while the Street View service loaded still sent a billed getPanorama; describe() now rechecks the lookup after acquisition.
The standalone director971.test.mjs rebuilt the browser, storage and viewer
doubles that director.test.mjs already provides. Its cases now live in the
director suite on makeDirector/installSceneRuntime, which gains optional
seeded storage entries and a quota-full switch; existing tests are unchanged.
The checkpoint and failed-save-status cases are split so each fails on its own.
Windows runs npm.cmd through a shell, and passing an argument array with `shell: true` is deprecated in Node 24 (DEP0190). Every `npm run doctor` and every Pinokio install on Windows printed the warning. npmProcessSpec now takes the npm arguments and, on Windows, joins them into the command string; the arguments are fixed subcommands (`--version`, `ci`). Unix behavior is unchanged.
Main takes release notes as one fragment per pull request (#787). A missing value still falls back to a matching seed camera, and a datasheet can still set the field of view.
The flights records and the mesh-floor sampler kept identical copies of the equirectangular distance; both now import src/data/approxDistance.js. The changelog entry moves to changelog.d (#787), and CURRENT-STATE notes the antimeridian behaviour.
aircraft_in_area compared a nullable altitude, so a contact with neither barometric nor geometric altitude matched max_altitude_m as though it were at zero. Require a reported finite altitude only when a band is requested; unfiltered queries keep the contact.
Move the changelog entry into changelog.d/1056-area-tool-breakdowns.md
(upstream's one-file-per-PR rule, #787) and leave CHANGELOG.md as on main.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
AIS type 0 means "not available" (ITU-R M.1371), but the source keeps it
as '0', so group_by: 'type' listed "0" as a type and understated
unknown_count. vessels_in_area rows now report it as null, and a
regression fixture mixes type 0 with Cargo and Tanker.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Rebase #1142 onto current main, keeping both sides of the changelog and runtime documentation conflicts. Retain unwrapped containment and circular query coordinates.
Address review feedback on perimeterAnchorDegrees: use circular longitude for the selected outer ring, excluding its closing duplicate. Compare multipart areas along short longitude edges so a small dateline part cannot steal the card anchor from a larger polygon.
Three new card regressions fail before the fix and pass afterward. Coverage includes reversed/rotated rings, vertices at +/-180 and both multipart selection directions. Local validation: 43 focused tests; full suite 7,636 passed, one skipped, zero failures; production build, formatting, module boundaries and whitespace checks pass on Node 24.19.0. Browser tracking validation follows on the QA branch.
- i18n scanner: lastError sink, no false sinks on defaults and ternaries
- shared src/i18n/marker.js for modules that only mark strings
- wind field messages move to wind/fields.js; CITY_POIS imports from cityPois.js
- drop review IDs, change history and stale references from comments and docs
Co-Authored-By: Claude Opus 5.5 <[email protected]>
keep typed and tool-follow-up responses attached to their original requests
reject duplicate events before they affect newer work
preserve newer input and Radio handoffs during old callback cleanup
integrate the tested main snapshot without dropping existing history
Keep USB reads in a bounded ordered pipeline, acknowledge and cap decoder
work, terminate stopped workers, and buffer short playback jitter. Throttle
display telemetry and compute spectra only while seeking.
Preserve PR #1174's wrapped seeking and fresh-spectrum tagging. Add opt-in
bounded timing diagnostics and decoder/playback lifecycle regressions.
Co-authored-by: Aditya <[email protected]>
- Status pieces translate in one place (src/ui/statusText.js): a Local
ADS-B error with a single piece now translates, and the analyst card
splits empty-layer notes into pieces (Transit, Local ADS-B).
- Translate the ALPR retry chip text, the ECCC error on the Air quality
card, "Upstream response too large" and the "entry {n}" feed label.
- The unmarked-text check also sees error/loadingLabel/statusMessage/
status assignments, `status:` properties and showToast?.( /
this._showToast( calls; the scene import preview marks its pack
statuses, data and model-facing text carry i18n-ignore with a reason.
- Tests check the real code: the HUD locality formatter moves to
src/hudLocalityFormat.js, the unsaved-keys test pins the teardown.
- One copy each: city names come from src/cityPois.js, the GNSS HTTP
template and wind "field unavailable" texts; dead fallbacks removed.
- Leftovers of the reverted vessel-label change and the reformatting of
detectionHost.test.mjs / keySetup.test.mjs are back to main.
Catalogs: es/ru 2617 messages each, complete.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The vessels layer hid the detection callout for every vessel whose card
it published to the world overlay. The overlay paints only part of the
published cards (shared budget, collisions, UI chrome), so vessels whose
card was dropped lost all their text: with 60 vessels and Detection on,
21 showed only an arrow. Back to main's behaviour: the callout always
draws. Hiding it only under a painted card is HAL-628.
The translated speed unit in the callout stays.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Fixes the P1 and the confirmed P2 findings of the final review of the
i18n branch.
- CI: the air-quality model no longer imports i18n (portable module),
so check:boundaries passes again.
- Air quality layer: named in the layer panel, HUD and voice; its
Weather card, bands and errors translate. A test keeps the layer-name
table in step with the layer registry.
- Layer statuses that still reached ru/es in English: GNSS, CelesTrak,
Transit loading and feed errors, launches, perimeters, ALPR, ECCC,
wind "field unavailable", Local ADS-B streaming lines.
- Voice and HUD: preset ids show city names ("Fly to San Francisco"),
the HUD city translates, cockpit refusals cover frame, aircraft and
satellite; empty Transit notes translate on the analyst card.
- tn() keeps a caller's formatted count ("12,345 map objects").
- Cyber HUD: the language button no longer covers the bottom-right
card or the cockpit position readout.
- CCTV buttons wrap only in non-English locales; English layout matches
main again.
- Switching language asks first when Provider Settings holds unsaved
keys.
Catalogs: es/ru 2611 messages each, complete.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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.
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.
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.
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.
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.
Bogotá's and Santiago's PBSC feeds list depot stations in São Paulo and
Madrid as installed and renting, so activating either city drew live
docks thousands of kilometres away and counted them in the layer total.
Station information is now limited to the city's registered load radius;
every other registered feed already sits inside its radius (checked live
2026-10-16), so nothing else changes.
Adds a Bogotá/Santiago registry test matching the Brazilian and Barcelona
ones: proxy allowlist, outermost-station activation and radius reach, and
the depot filter through the real ingestion path.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The map range check floors every radius at 100 km, so the station test
passed with any loadRadiusKm up to that. get_bike_share uses the radius as
registered, so assert each system's radius reaches the stations it lists.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`--write` wrote CHANGELOG.md and then removed fragments one by one, so a
failure after the write left surviving fragments that a rerun spliced in
a second time.
The write is now staged. The assembled file goes into
changelog.d/.assembling/, the fragments are renamed in beside it, and
renaming the assembled file over CHANGELOG.md is the single commit
point. A run that fails rolls itself back when it can; otherwise the next
`npm run changelog` reads the staging directory and either restores the
fragments (assembled file still staged) or deletes the leftovers (batch
landed). `--check` reports a staged batch instead of reporting past it.
A failure-injection test makes every filesystem call in a `--write` fail
in turn, both as a one-off error and as a crash with no in-process
rollback. It asserts the retry produces exactly the uninterrupted
CHANGELOG.md, with each entry once and an empty directory. The test
fails against the previous write-then-delete and against delete-first.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Conflicts:
- CCTV HUD: main's datasheet FOV, capability range and spec summary,
with their labels translated; specFovCapabilityToken takes a format
so the HUD can pass a translated one. Datasheet resolution labels are
camera data (i18n-ignore).
- Transit: main's route labels and operator mode words, with the
generic words still translated; an operator's own word stays a name.
Catalogs: es/ru 2566 messages each, complete.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Bicing (543 stations, PBSC) and AMBici (237 e-bike stations across the
metropolitan area, nextbike) join the GBFS registry from the operators'
keyless GBFS 2.3 feeds, both listed in MobilityData's systems catalogue.
- gbfs.nextbike.net joins the proxy allowlist as an exact host.
- Credits, DATA_SOURCES, CURRENT-STATE and CHANGELOG updated; a test pins
each system's proxy reachability and the range check for its outermost
stations.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
main now rejects out-of-range coordinates at the proxy itself (19fcc00b), the
same check this branch added. main's version is kept, and the changelog entry
no longer claims that part; the cache cap, file ceiling and rate limit remain.
Replacement buses on 5xx streetcar routes were drawn as streetcars, and
Flexity streetcars reporting no route were drawn as buses. A feed can now
supply vehicleMode(id); TTC maps 4400-4699 to streetcars and every other
fleet number to buses, falling back to the route otherwise.
Codec, product and resolution words never identify a model; generation suffixes such as Mk II no longer fall back to the base model; a mismatched manufacturer never matches, and brand-only numeric names need the brand. The datasheet FOV only fills when the source and seed give none and the pose is not curated, and DATASHEET is shown only while no calibration offset applies. Adds the CC0 extract to DATA_SOURCES.
The seven feeds publish no licence, operator or attribution fields, so DATA_SOURCES records that and credits the operator. Adds a registry test per city: allowed host and path, provider, and activation over its outermost live station.
black-translucent needs viewport-fit=cover and safe-area padding, which this change does not add. List the PNG favicon before the SVG so browsers that take the last usable icon keep the SVG, add mobile-web-app-capable, and give the manifest a stable id.
Number() reads null, blank strings and booleans as 0 (or 1), and
/api/cctv/sources sends an unknown number as JSON null. A source-pack
camera that left out its pose therefore reached the browser as zeros
clamped to each field's floor (north, pitch -2, FOV 20, range 120 m,
mount 6 m, ground 0 m) instead of taking the defaults, and a null or
blank position put the camera at 0,0.
toFiniteNumber (server) and safeNumber (browser) now return the fallback
for absent, blank and boolean values, as validateBox already does for
Recent Imagery. The same conversion fixes the per-pack paths: a Tallinn
row with a null heading no longer reads as a high-confidence north-facing
camera, a Caltrans camera with a blank elevation keeps its 150 m ground
prior, and TfL or Catalonia cameras with a null or blank coordinate are
dropped instead of kept at 0,0 or on the zero meridian.
The Transit row names the regions a reader can expect to see vehicles in,
and still stopped at South East Queensland after nine Ontario feeds were
added. Someone scanning the table for Toronto would have concluded the
layer does not cover it.
Toronto carries the subway caveat here too: the row would otherwise
promise metros in a city whose metro publishes no positions.
Two gaps. The Hamilton entry never said its route ids are internal keys,
which the Guelph, Burlington and YRT entries all say of theirs; the
route-table sentence in the transit notes named those three and not
Hamilton, though it now carries the largest table of the four. Both were
stale the moment Hamilton got a table.
The exclusions were half-recorded: GO Transit and Grand River had their
reasons written down, but Ottawa, London, Thunder Bay and the static-only
majority did not, so a later reader would have re-investigated feeds that
were already surveyed and rejected. All of them are now named with the
rule each one falls to, and the survey is stated as complete against the
Mobility Database's Ontario catalogue.
Also records that a route label is never invented, which is why YRT's
unpublished 9101 renders as the feed sends it.
Not one of HSR's 80 route ids equals the number on the bus. Hamilton was
registered without a routeLabel, so every vehicle in the city read as
"Route 5783" where a rider would say 5 DELAWARE — 228 of them in an
afternoon sample. The mechanism to fix it already existed and simply was
never pointed at this feed.
Two details the table cannot skip:
Hamilton runs two generations of route id at once. `5688` and `5780` are
both the 2 BARTON, and both arrive in the live feed, so both are listed.
That is why 80 ids collapse to 46 distinct routes.
The seasonal and event shuttles publish a name where the local routes
publish a number — TC230, PEACH, ROCKTON. Those are already what a rider
would say, so they survive as published rather than being forced into a
number they do not have. Numbers are unpadded to match the rest of the
registry: routes.txt says `02`, a rider says 2.
Ids and names are from the City's own published GTFS routes.txt, which
sits under the same open-data licence as the realtime feed. Checked
against the live feed: all 30 route ids in service translate, none fall
through.
YRT encodes a branch as a two-digit suffix on the base route id. The Viva
table already depended on that scheme — `60102` is Viva Blue's B branch —
but local routes were passed through whole, on the reasoning that a YRT
route id is already the number on the bus. For a plain route it is. For a
branch it is not: a rider watching route 83's B-branch bus saw "8301".
A 15:41 EDT sample of the live feed found four branch ids in service.
Three are published in York Region's GTFS `routes.txt` and are named here
from it, unpadded to match the local routes beside them (the file says
`083A`; the registry has always preferred `105` to `005`):
10702 -> 107B KEELE
8301 -> 83A TRENCH
9002 -> 90B LESLIE
The fourth, `9101`, runs in the realtime feed but appears nowhere in
`routes.txt`. The pattern all but spells "91A" and that is precisely why
it is absent: the registry translates published names, it does not infer
them. It stays opaque until York Region publishes it.
Renamed YRT_VIVA_ROUTE_NAMES to YRT_ROUTE_NAMES, since the table is no
longer only about Viva.
Burlington's route ids are internal keys with a trap in them: `351` is
route 1 while `3510` is route 10, so no prefix trim is correct and the
table is a real join against the City's own published GTFS. Its licence is
the same 2011 municipal template as Mississauga — revocable, and the terms
must travel with the data, so licenseUrl is the terms document itself.
Plain http:// has no listener at all, so https is mandatory, not preferred.
Barrie and YRT ship on. Both operators invite acceptance of their licence
through a form, and both licences also state that use is itself acceptance
("By using Barrie Transit Data, you agree to be bound by these terms";
"Your Use of Information indicates Your acceptance"). This deployment
relies on that clause, and both entries record it so the reasoning does not
have to be re-derived.
Nine Ontario feeds now, all verified live against their upstreams.
York Region's feed is first-party infrastructure under an open licence that
permits commercial use and does not even require attribution. It is the one
feed that fills the gap north of Toronto, and it is switched off anyway.
The operator asks that its licence be accepted through a form collecting a
name, company, phone and email. No credential is issued and nothing gates
the endpoint — the licence body says use is itself acceptance — but a form
asking for contact details is close enough to the registration this registry
refuses that a person should decide it, not a poller. Barrie is off for the
same reason. Both are one line from on.
Viva route ids get labels: YRT publishes `601` where every station sign says
Viva Blue. The numeric local routes already read correctly and carry no
entry, so the table is six lines, not 125.
No vehicle in this feed carries a bearing; heading comes from observed
movement, as with Durham.
Guelph's feed publishes the internal database key of a route (`2991`)
where the rider sees the number on the bus (`1`), which would have put
"Route 2991" on the label a person reads. So a feed may now carry its own
`routeLabel`, the same way it may carry its own `modeWords`. Guelph's table
is its 33 routes, taken from the City's own published GTFS; an id missing
from it renders unchanged, because a raw id you can compare against the bus
beats a blank. Every other feed is identity and carries no table.
Kingston is catalogued by the City against this exact URL. Its licence PDF
link is pinned to the canonical location rather than the legacy path that
currently redirects there — Durham's entry records what happens when such a
redirect is eventually dropped.
Barrie ships REGISTERED AND OFF. The City offers its licence for acceptance
behind a click-through gate, and nobody on this project has accepted it.
Whether to accept is a person's decision, not an assumption a poller should
make, so the feed is visible, documented, and one line from enabled.
Three keyless GTFS-Realtime feeds around Toronto, each verified live: all
vehicles carry real coordinates, and each is polled only near its own
region rather than across the Golden Horseshoe.
The licences differ in ways the registry now records rather than flattens:
Hamilton PRESCRIBES its attribution wording, so the credit is that exact
sentence; Durham makes credit optional and its catalogued licence PDF is
dead after a site relaunch, so licenseUrl points at the live page where the
v.1.0 text was actually read; Mississauga's licence is revocable and asks
that the terms travel with the data, so licenseUrl is the terms themselves.
All three are bus-only and set no route hint, so their modes default rather
than resolve. Durham publishes no bearing on any vehicle — heading comes
from observed movement, as with any feed that omits it.
GO Transit / Metrolinx stays out: its feed requires an API key.
A Toronto 501 and a Helsinki 1010 are both the `tram` mode. Only one of
them is called a tram by the people who ride it, and the detection label
is documented as "what a person standing at the stop would call it".
So the mode stays shared — same silhouette, same speed limits — and a
registry entry may carry `modeWords` naming the modes it runs. Everything
without an override falls through to the shared table, so this renames one
word for one operator and nothing else.
Threading the feed to the detection metric needed a resolver in queries.js,
since the cached-contact path has only the entry. It answers from live feeds
first, so a feed disabled mid-session stops supplying vocabulary.
TTC BusTime publishes keyless GTFS-Realtime VehiclePositions with real
coordinates for the surface network: ~1,180 buses and streetcars with
bearing and per-vehicle timestamps. Registering it is one registry row.
The subway is deliberately absent. BusTime covers surface vehicles only
and the subway publishes no positions, so Lines 1/2/4 are left out rather
than inferred from schedules — the same rule that keeps NYCT out.
Route numbers establish the mode: the three-digit 500-series is streetcar,
as are the four Blue Night routes on streetcar trackage. A leading 5 alone
would misclassify bus routes 50-59, so the rule is anchored and tested.
Licensing is the portal-wide Open Government Licence - Toronto; the CKAN
record for the BusTime NVAS dataset leaves its own licence field blank,
and the terms note says so rather than overstating the grant.
Refs #70
Maintainer follow-up for #741, no runtime change.
- CURRENT-STATE: advisory issue and position times up to and including
90 minutes ahead of the server clock are accepted because NHC can release
advisories before their nominal time; 90 minutes is the app's tolerance,
not an NHC limit. Later, non-canonical and over-12-hour-old times still
reject the snapshot.
- Tests: pin exact 90-minute acceptance and +1 ms rejection, and exact
12-hour acceptance and +1 ms rejection, for issuance and lastUpdate
independently; the early-publication test now asserts
invalid_cyclone_data.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Resolve the CHANGELOG.md conflict by keeping both sides: this PR's cyclone
entry stays above main's newer entries; no text changed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Resolve conflicts with main by keeping both sides:
- CHANGELOG.md: keep this PR's flights entry above main's newer entries.
- src/layers/flights/records.test.mjs: keep this PR's position-less
retention test alongside main's civil emergency-status test.
src/sources/live/aircraft.js merged cleanly: openSkySnapshot keeps main's
emergencyByIcao normalizer and passes openSkyUnpositionedId to admitRecords.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
/api/cctv/media relayed still-image cameras with an ordinary fetch: it followed
redirects to any host, passed through any content type and had no body cap.
Stills are served by /api/cctv/frame, which keeps redirects on the registered
origin, accepts only image/* and caps the body, and no client requests a still
through /media. /media now answers 404 for anything but HLS, MP4/WebM and
MJPEG, before any upstream request.
Ids were built from the feed's fid, which embeds an upstream server-start stamp,
so every upstream restart renamed all cameras (it already had since the PR
fixture). Ids now come from the validated frame URL (cat-sct-<stem>, or host
and path for partner cameras), which also collapses the five duplicate rows.
Frame links must sit directly in each host's frame directory with no explicit
port, and sctidcam must be a bare image filename. Numeric entity decoding
handles hex references and leaves invalid code points as written, so one bad
name no longer empties the pack. Counts in the docs are now ~130.
The catalog fetch now refuses redirects, cancels rejected bodies and reads
through the shared capped reader before the windows-1252 decode. The feed sends
cardinal codes plus B (both), O and null, not BW or diagonals; tests, comments
and docs now say so, with unchanged id-hash fallbacks. Documents the 8-1,200
per-pack range, drops the unverified low-volume compliance claim for facts and
a link to WSDOT's terms, and stops claiming the per-pack defaults fit under
the 4,000 cap: it is filled round-robin.
The camera endpoints are the eismoinfo.lt website backend, which the Via
Lietuva open-data statement does not list, so the pack now follows the NYC
opt-in pattern (CCTV_LITHUANIA_ENABLED=1) and the docs say why. A failed
catalog request now aborts its sibling instead of leaving it open until the
timeout, and a redirect is logged as not followed.
The catalog fetch now refuses redirects, cancels rejected bodies and reads
through the shared capped JSON reader. Duplicate ids are dropped before
prioritization so they cannot use up the per-pack cap. Vendor-generic model
strings (PTZ, PTZ IP, BOSCH/PTZ) are treated as no model. Comments and credits
say why WSDOT rows are skipped (their frames are WSDOT-hosted, outside the
pinned origin) and name Newcastle as the partner city that appears.
Adds the Toronto settings to .env.example and a current-state section for the
pack, restores an approximate camera count in the README, and corrects the
claim that adding a pack never trims another region: with the shared
4,000-camera cap, the largest packs give way round-robin. Fixes two stale
comments about the list's refresh and the ground-height sidecar.
Catalog numbers above 99999 arrive in a TLE as Alpha-5: a letter A-Z,
skipping I and O, stands for 10-33 in the ten-thousands place, so 100000
is A0000. satellite.js keeps the field as the string satrec.satnum, and
the three catalog builders converted it with Number(), which gives NaN;
the Set and Map used to deduplicate and store treat NaN as equal to NaN,
so the first such satellite was stored under NaN and every later one was
dropped as a duplicate (#751). The satellite tools read the same field
with parseInt and reported no number, so such a satellite could not be
found by it.
The decoder lives in src/sources/tle.js, beside parseTleText, which the
layer and the tools both import already: noradIdFromSatnum() for the
three builders, and tleCatalogNumber() decodes through the same function.
Every other field reads as it did before: Number() in the layer,
parseInt or null in the tools.
Merges cleanly; weatherPanel.test.mjs arrived unformatted and is run
through prettier so format:check passes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Replaces the EN/ES/RU buttons in the Visual Presets dock with a picker
that scales to many languages: a globe button with the current code
next to the POWER UP chip, opening a list of languages named in their
own language with their codes, the current one first and checked. A
search field appears past 8 languages. Keyboard: arrows, Home/End,
Enter/Space, Esc, type-ahead; keys stay in the list while it is open.
Choosing a language still asks before dropping unsaved work, then
reloads. Phones get the globe alone and a full-width sheet; the layout
uses logical properties for right-to-left languages.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Keeps main's 4,000 catalog-wide camera cap, which main pins in tests; WSDOT
shares it round-robin with the other packs. The branch's earlier cap raise and
its removal of main's cap tests are reverted in this merge.
Upstream's #568 adds the Air quality layer to the Weather group (ECCC
AQHI, Canada only) with a band legend, worst-first list and voice
support.
- package-boundaries: both sides' entries unioned.
- layerPanel/weatherPanel tests: both sides' new tests kept; the
weatherPanel fixture follows upstream (legendCategorical: true, the
'list' block id) while our localized-history (K-6) test stays.
- air-quality strings marked: band labels and the provider label as
msg() data, the readings-list aria-label through t(); es/ru entries
added (+6 keys each).
- The synthetic air-quality allocation-benchmark titles follow the
file's own convention (// i18n-ignore) like TRK/VESSEL/CAMERA.
Also verified the auto-merged weatherPanel.js against upstream: ORDER
gains 'air-quality', OBSERVED stays slice(2, 5), and the data-driven
legendCategorical path is upstream's.
npm run i18n: complete (2559), no unmarked strings. npm test: only the
pre-existing environmental OPENAI_API_KEY leak fails. Build passes.
Upstream's #641 (bikeshare Toronto, city failures scoped to range with
partial outages read DEGRADED/STALE) replaced the layer-level GBFS
error strings this branch had msg()-marked, in the two paths that
conflicted.
- ingestion.js/viewport.js taken as upstream's per-city
_failedCityIds tracking; the layer-level error assignments are gone.
- 'GBFS fetch error' still reaches the panel via stats.error from
controls.js, so its mark moved there (translateFeedError keeps
translating it).
- 'GBFS status update failed' no longer exists anywhere: pruned from
es/ru and from the layerStatusText SITES table, which now pins the
controls.js site instead of the removed ones.
Also brings #581 (upstream-read caps, OpenSky 429 cooldown) - no i18n
surface.
npm run i18n: complete (2553), no unmarked strings. npm test: only the
pre-existing environmental OPENAI_API_KEY leak in devFreshDotenv fails.
Build passes.
Panoramax's coverageLabel action is 'click a line for its nearest
photo' (like KartaView's) while it counts sequences, but the count-line
discriminator keyed only off the noun, so Panoramax fell through to the
default pair and promised 'its photos'. Discriminate on the action
first, then the noun, and add the sequences + nearest-photo pair
(KartaView keeps streets + nearest-photo; the default keeps sequences +
photos).
The card never appeared: the Weather panel only passed five fixed ids and only
rendered lists and categorical legends for cyclones. Air quality now reaches
the panel, the legend style comes from the layer's descriptor, and any layer
with list items gets its list. Adds the voice manifest entry main now requires,
reads ECCC through the shared capped JSON reader with a 15 s deadline and the
row limit enforced, reports stale when every reading was too old, adds source
tests, links the ECCC licence and moves the changelog entry into Unreleased.
A failure now ends when its city leaves range or the camera rises above the
activation gate, so an empty map no longer reads LOAD FAILED. Late activation
and status rejections are ignored like late successes. Partial failure reads
DEGRADED, every in-range city failing over cached points reads STALE, and
freshness advances only on a refresh that succeeded. Behavioural tests drive
the real proximity, update and enable paths. Docs say no licence is published
for Toronto and list Social Bicycles and PBSC. The merge also restored the #61
GBFS v3 changelog entry that an earlier merge on main dropped.
A 429 now starts the credit cooldown from its headers and drops the body
unread, so a stalled or oversized 429 cannot skip the governor; an oversized
snapshot is not retried. A 429 without retry-after now waits the intended
2 minutes (headers.get() returns null, which Number() read as 0 s). Adds
oversized-stream tests for OpenSky, adsb.lol, CelesTrak and TomTom that fail
on main, names the six reads in the changelog, and corrects the docs on
what was already bounded.
The build context excluded only .env, so .env.local and the like or
pinokio/ENVIRONMENT would be baked into image layers; exclude every dotenv
file except .env.example, plus local state and QA output. Pin the base to
CI's Node 24.14.0 by digest, default to the image's node user, and correct
the HOST comment. Log the trusted peers as the operator's when set. README
notes that MCP and ChatGPT sign-in voice are unavailable under Docker, and
how to trust no gateway when widening the publish.
Conflicts:
- KartaView viewer moved into the shared photoViewer.js on main; its
labels now take the provider name ("{name} street-level photo") and
are translated there.
- Weather: main's cloud filter strength slider, with its labels in t().
- Street Level caption: translated creator line plus main's licence.
Panoramax status and error texts are marked like KartaView's, and
"Panoramax answered HTTP {status}" gets a template in the panel and the
layer row. Catalogs: es/ru 2553 messages each, complete.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- The help tray over the mic clipped the longer ru/es hint ("Hold Space
to speak · tap Space to activate focused controls"); let it wrap in
non-English locales. English layout is unchanged.
- The fly-to headline and the nearest-aircraft distance line now show a
preset city in the interface language (Tokyo → Токио / Tokio); speech
sent to the model stays English.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Both sides had merged the same main features (cockpit emergencies, GNSS
integrity, geo file import) with their own i18n. Conflicts keep this
branch's reviewed version; the 7 catalog entries only the other side
used are pruned. Newer main commits come in unchanged.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Fixes the 51 P0–P2 findings from the full review of the i18n branch and
the issues found while checking every new string on screen in ru and es.
- Translate errors and statuses that still reached ru/es in English:
feed and tile errors, provider statuses, the ALPR chip, the traffic
mode chip, voice plan step labels, fire feed values, receiver bands,
scene save reasons and the Cesium attribution link.
- Show preset city names in the UI language (src/ui/placeNames.js);
data and everything sent to the model stay English.
- Share layer names between the layer panel, HUD and voice card
(src/ui/layerNames.js).
- Keep long ru/es strings whole: voice card error lines, analyst
titles, the voice tier status, the Nepal evidence button, the ALPR
retry chip. English layout is unchanged.
- Ask before a language switch drops unsaved work.
- Catalog fixes: glossary terms (пуск, пресет), gender agreement and
consistent "peticiones" in es, fewer calques in both languages.
- Fix English text that was itself wrong (e.g. "Exit cockpit to fly to
a weather", raw tool ids in voice plan steps).
Catalogs: es/ru 2535 messages each, complete.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Port to current main: main reserved layer-state token 0 for street-level, so
air-quality takes the next free token, 4 (registry, ledger, and the allocation
tests shift by one). Main moved source contracts into sourceComposition.js, so
air-quality registers its getSnapshot contract there. Package boundaries keep
main's lists plus this branch's air-quality modules and packages.
Blend the existing infrared brightness key from Full (0%) to Clouds only (100%), preserving Soft/Vivid as independent opacity presets. Commit filter changes on slider release and retain legacy share-link mode codes.
Uses a full SHA-256 of the sorted instance list, so reordering the same
instances for failover keeps the cache. Adds the installation-cache repoint
test (tagged, reordered, repointed, untagged, unconfigured) and the changelog
and current-state notes.
Upstream advanced past the KartaView merge: transponder-emergency HUD
wording (formatCockpitEmergency in cockpitMath), the GNSS integrity
layer joined the Events group, and a GeoJSON/KML/KMZ/GPX file-import
control landed - all in regions this branch localizes.
- cockpitMath: upstream's formatCockpitEmergency kept alongside the
branch's formatter-injected Context scope (module stays i18n-free;
squawk/ADS-B codes and the broadcast label are data).
- cockpitInstruments: the emergency HUD line composed with t()
placeholders; fleet/feedState stay translated, the 'emergency' class
toggle kept.
- layerPanel: upstream's gnss-integrity id with msg('Events').
- package-boundaries: both sides' entries unioned.
- New upstream UI strings marked and translated, as the branch's
unmarked-string detector requires: the geo-import control's markup
(data-i18n-title/-aria-label/text), its list titles, hints and
summarize() lines (t/tn with placeholders), the aircraft emergency
labels (msg data), and the GNSS legend blurb (t with placeholders).
- es/ru catalogs: 26 new keys each (2019 messages).
npm run i18n: complete, no unmarked strings. npm test: 6739/6741 (the
one failure is the pre-existing environmental OPENAI_API_KEY leak in
devFreshDotenv). Build passes.
Resolve the CHANGELOG conflict by keeping every entry from main and
adding the radio entry on top; the previous main merge (cebd870)
had dropped 19 of main's entries.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Image loading (photoViewer.js)
- One AbortController per open: a new open, close or unmount stops the
previous photo's first and sharper downloads (an <img> by src = '').
A collapsed viewer never falls back to the original; Panoramax's flat
fallback is the thumbnail. A superseded open resolves quietly.
- Flat HD tracked as "loading" and "shown" apart: a discarded response no
longer blocks later upgrades, and resizes while it loads ask once.
- No upgrade of the photo still shown while another opens.
- Size limits before decoding: imageSize.js reads JPEG/PNG/WebP/GIF
headers; more than 64 MP or 16,384 px a side is refused undecoded, an
<img> that decodes larger is refused and let go, flat HD needs a known
width of at most 8,192 px, and panoramas always decode with a resize.
Panoramax provider
- Retry-After parsed as seconds or an HTTP date; the browser cannot read
it here, so busy answers back off 5 s doubling to 300 s, reset once
answered.
- Headers timed, then the body by an idle timeout (15 s of silence), so a
slow link still gets its megabyte tiles.
- Cones centre only on the picture shown (onShowing), never on a late
lookup; lookups nobody waits for are aborted (shared, refcounted).
- onReopen: a GPU reset redraws in the viewer, not through the core.
- Cones draw the home z15 tile at once and add neighbours as they arrive;
a neighbour that failed is not counted as drawn.
- Coverage tiles decode in ~8 ms slices (a 2 MB z10 tile over Paris was
one ~85-200 ms task); sorted by measured length without a Map.
- Box search asks for 20, not 100 (the API returns them nearest first).
- looksLikeEmail checks every '@'; creatorOf de-duplicates with a Set.
lineCoverage.js
- A redraw while a swap still builds keeps what is on screen (the swap's
old lines and its ready new ones) and drops only the unready ones.
Tests for each (mutation-checked), two vacuous tests made real, the
cones' pictures lane and readable()'s busy/timeout paths covered; the
fixture's box search is nearest first. Docs: README filters and share
links, CHANGELOG HD wording, CURRENT-STATE.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
From the maintainer review of #1134 (approve with follow-ups):
- lineCoverage: a share is judged against the cap the tile was last drawn
with, not against later shares that redrew nothing, so small increases
add up (a retained tile stayed at 100 points after its share grew to
116). A rebuild's measuring pass no longer counts as a draw.
- photoViewer: a picture refused for its size (bytes, streamed bytes or
decoded side) is marked `tooLarge`, and the viewer never loads that file
again by any path; the flat fallback had retried an oversized SD file
through an <img>, which has no size limit.
- Regressions for both: cumulative share growth below the threshold, and a
refused SD panorama that is not fetched again as a flat photo.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The pill step read #sl-status straight after the credit wait. Tiles
requested while Cesium painted the credit can flip it back to LOADING,
which failed CI on PR #1038 (actual 'BUTTON:LOADING:true'). Wait for
coverage to settle and the pill to read ON before asserting.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Reverse geocoding now goes to /api/google/reverse-geocode on the app
server, so the harness stub for maps.googleapis.com never matched and a
keyed run would reach Google live. Answer both proxy routes with
ZERO_RESULTS instead.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Node always sets statusCode on client responses, but keeping the
fallback stops a missing status from reading as 200, as main did.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Resolve SECURITY.md against the Street View provider (#966): the browser
key now loads the Maps JavaScript API as well as Map Tiles, so the
'Map Tiles only' guidance becomes 'Map Tiles + Maps JavaScript API'
here and in CHANGELOG, CURRENT-STATE and google-key.js. The CHANGELOG
entry moves to [Unreleased]; its old section shipped in 0.2.0.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Conflicts resolved by keeping both sides: the Events group gains
gnss-integrity with a translated label, the cockpit aircraft line shows
main's transponder emergency through a localized formatter (cockpitMath.js
stays i18n-free, like the Context scope line), and the boundary lists keep
both the i18n modules and main's new data modules.
Main's new UI is translated into es and ru: the GNSS Integrity row, legend
and notes; geo file import (button, hints, list actions, every import
error, with numbers passed as placeholders); transponder status names.
Also formats kartaview/viewer.js so format:check passes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
CHANGELOG: keep both new [Unreleased] entries (Panoramax, and the
emergency-transponder highlight from #856).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Keep both: Catalonia CCTV alongside the NYC DOT TMC pack in the source
imports, README layer table and CURRENT-STATE data table, and both
CHANGELOG entries.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
isoTime returned toISOString() for any finite number, which throws past ±8.64e15 ms, so one bad record (e.g. a bike-share last_reported) failed the whole tool answer. Such timestamps are now null. The voice analyst's result rows had the same conversion and get the same guard.
Co-authored-by: Sameh Khamis <[email protected]>
A listener could destroy the controller and re-subscribe one still pending in
the snapshot, which then ran. Delivery now stops at destruction and a destroyed
controller takes no listeners. Adds ordering tests for adds, duplicates and
re-adds during delivery, and moves the current-state note after the section
intro.
The finally block awaits reader.cancel(), so a cancel arriving there still let
the bytes return. Rechecks the signal after cleanup, with a test that keeps the
caller's abort reason, and wraps the current-state note.
Normalize GeoJSON ground footprints, preserve explicit KML track heights,
and tessellate ground paths before native loading. Validate coordinates,
geometry types and decoded image budgets before allocating native resources;
strip loader-sensitive metadata and settle deferred failures and cancellation.
Render static pins in owned billboard collections instead of walking tens
of thousands of point entities each frame. Refine nearby ground heights in
batches, isolate imported pins from height picks, and release owned resources
and point selections on removal. Use safe ray origins above a hidden coarse
globe and retain valid mesh heights outside the current footprint.
Frame complete geometry with viewport-aware nadir arrivals, including walls
and rotated overlays; respect camera gestures and accept document file drops.
Strengthen real-loader, native 3D Tiles, paint, clock and lifecycle coverage.
Addresses both P1 and all 25 P2 findings in the follow-up review of #1122.
A warm entry could be evicted by the same call's later fetches and then
reported unresolved. The call now remembers warm hits as it does fetched ones.
Moves the batch LRU test beside the existing cache tests and folds the docs
note into the section's eviction paragraph.
Share-link integers and Cesium radians were converted before normalizeBox
checked them, so a blank edge still became 0°. Edges are now accepted only as
numbers or non-blank numeric strings, which also refuses [] and {}. Tests cover
all four edges through validateBox, dequantizeBox, boxFromRectangle and
searchHls.
Adds a case that fails before de-duplication (a 24-slot overview held repeats
and reported itself truncated) and says why the date-line halves overlap.
The existing "route that fits" case used 199 steps, which is how the
off-by-one went unnoticed. Folds the docs note into the existing cap sentence
and makes the changelog entry specific.
Preserve upstream changes, restore the reviewed Radio ownership regression,
and retain reporter raysung-2026 credit and #804 provenance.
Reconciles upstream main 81cf437 and PR #816 head 702ecc3; no force push.
Keep this branch's CHANGELOG entry above upstream's; upstream main no longer lists the GBFS station-name entry an earlier merge carried here.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Seven QA scripts hard-coded macOS Google Chrome. They now share chromeExecutablePath(): PUPPETEER_EXECUTABLE_PATH when set, then macOS Chrome when installed, otherwise Puppeteer's managed browser. The cables screenshot path is built with fileURLToPath so it works on Windows. qa-overlay-baseline keeps its fixed Chrome because its baselines record the executable.
Co-authored-by: Sameh Khamis <[email protected]>
Number(null) is 0, so the cockpit cloud profile treated a missing weather code
as clear but available. The briefing's three checks now share one helper, and
the current-state note is its own bullet instead of folding into the route one.
A single % made the seek delta negative when the target angle was more than
540° below the start, so 350° to -350° turned 340° through 180°. Uses the
same double modulo as playback, and adds tests for exact ±180° endpoints and
a near-antipodal seek.
Clears the timeout guards once each race settles, measures the 15,000-vehicle transit ingestion with main-thread CPU time instead of wall time, and times the region-timeout case from the boundary lookup with a 2 s ceiling, below the 3 s default budget, so an ignored budget still fails.
Co-authored-by: Sameh Khamis <[email protected]>
process.exit(2) after a successful fetch trips a libuv assertion on Windows and turns the exit code into 127. The four gates set process.exitCode and return instead, and discard the unread reachability response so an early exit does not wait for the server to finish the body.
Co-authored-by: Sameh Khamis <[email protected]>
The test's access stub matched a forward-slash suffix, so on Windows the resolver's backslash path never matched and it fell back to PATH. The stub and the expected value now use the same path.join result. The Windows CI job also runs this test file now.
Co-authored-by: Sameh Khamis <[email protected]>
The review-driven rewrite added a narrower entry but left the original
bullet in place, so the PR carried two entries for one change.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The review-driven rewrite added a narrower entry but left the original
bullet in place, so the PR carried two entries for one change.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
It reads the token, model, transcription model and tier for diagnostics and
cost accounting, so the credential is not opaque to it. Also lists requestToken's
authMode option in APPLICATION.md.
The horizontal offset multiplied elapsed time by a sine, so flakes swung faster the longer the app ran. It is now the integral of the intended sway velocity, which stays bounded at any session age; Wind still scales the amplitude.
Co-authored-by: Sameh Khamis <[email protected]>
Exclusive presentation hides collapsed right-rail panels with display: none.
Skipping every display: none panel also skipped those, so panels the rail had
auto-collapsed stayed collapsed after the HUD was hidden or left Tactical.
Only panels hidden by other CSS are skipped now. Adds the changelog and
current-state notes.
- Pin finiteCoordinate helper behavior across numeric, non-finite, and string inputs
- Verify recovery stage downrange distance, flight numbers, and coordinates fall back safely to null on non-finite data
- Pin recovery status transitions for LOST and EXPENDABLE stage categories
- Verify payload massKg and amount normalization edge cases for blank, negative, and non-numeric values
- Guard against legacy pack bootstrap during unreadable storage recovery
- Report failed saves honestly across captures, updates, and imports
- Resolve default scene id correctly when advancing past a specific shot
- Count unique scenes executed rather than queued shots in run telemetry
- Return run failure status when playback throws instead of false success
- Pin five regression scenarios in src/scenes/director971.test.mjs
The merge from main replaced the NYC DOT opt-in pack entry instead of
adding the Bing entry above it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This branch moved twice at once: upstream merged KartaView (#1121, a
third keyless street-level provider sharing a new lineCoverage.js), and
the remote i18n-spanish gained the whole-interface translation work
(1993 catalog entries, translateKnown, data-i18n-runtime, the unmarked-
string detector in npm run i18n) built on the Street View merge.
Integration keeps the remote's mechanisms and re-adds KartaView on top:
- presentMeta: remote's translateHint for coverage hints, plus the
KartaView count line as its own literal tn pair (Russian plurals need
the noun inside the template; keyed off coverageLabel.noun with the
sequences pair as the default).
- uiState: upstream's highlightsSequence legend gate (KartaView's lines
do not highlight) with t('Selected').
- layer-panels.html: remote's data-i18n/data-i18n-runtime hooks plus the
sl-since-note wiring KartaView's upload-date SINCE note needs.
- kartaview viewer: the photo-navigation aria-label and img alt go
through t(), as the remote's unmarked-string detector requires.
- catalogs: remote's 1987-entry base (its refined keys supersede this
branch's earlier Street View wording) plus the KartaView entries:
coverage hints, the upload-date note, the streets count line (with
Russian one/few/many/other forms) and the viewer strings.
npm run i18n: complete, no unmarked strings. npm test: 6520/6522 (the
one failure is the pre-existing environmental OPENAI_API_KEY leak in
devFreshDotenv). Build passes.
Upstream's KartaView PR (#1121) extracted the Mapillary coverage engine
into a shared lineCoverage.js and made the panel wording
provider-specific (coverage noun/action, SINCE upload-date note,
highlightsSequence legend gate), again in the regions this branch
localizes.
- mapillary/coverage.js: taken as upstream's thin re-export of the new
lineCoverage.js; the generic too-high hint stays msg()-marked, now in
lineCoverage.js where the default lives.
- kartaview/policy.js: coverage hints (tooHigh, noCoverage) marked as
msg() data so they translate like every other coverage hint; thrown
error messages stay English, matching the Mapillary precedent.
- presentMeta: KartaView counts streets, and Russian plurals need the
noun inside the template, so the count line gets its own literal tn
pair keyed off coverageLabel.noun, falling back to the sequences pair;
upstream's noun/action data contract is untouched.
- uiState legend: upstream highlightsSequence gate with t('Selected');
sinceNote localized with a {names} placeholder.
- layer-panels.html: sl-since-note wiring kept, data-i18n hooks kept.
- CHANGELOG: both sides kept. es/ru: 4 new keys each.
npm run i18n complete (953 messages each); npm test 6504/6506 (the one
failure is the pre-existing environmental OPENAI_API_KEY leak in
devFreshDotenv, unrelated); build passes.
meirm merged the same upstream main in parallel and translated its Street
View additions. This branch already resolved that merge and translates the
same text for its own key-guidance code ({source} instead of {label}), so
its tree is kept; meirm's commits stay in the history.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The selected fire card read "4.9 MW" and "8.389°S 44.162°W" in every
language; it now uses the locale's decimal separator and hemisphere
words ("4,9 МВт", "8,389° ю. ш."). The data labels stay as they were.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The round-3 verification found the shared point budget rebuilding a dense
tile by removing its lines before the new ones were built, so central
Paris vanished for 1–2 s after every pan:
- Tiles redrawn for a new share, or for a selected line the cap had left
out, go through the swap path: the old lines stay until the new ones
are ready.
- A share must move by 10% to redraw a tile (a 1.6% change rebuilt it).
- The selected line, admitted past the cap, is not counted against it,
so its tile no longer redraws on every settle.
- A selection the imagery filter hides is not redrawn for.
- Shares are recomputed when a refresh ends loading without a tile
finishing.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
KartaView's later viewer changes are carried into the shared
photoViewer.js: the camera bearing (`bearingOf`), redrawing the same
photo after a GPU reset through `onReopen`, no pose while a lost context
recovers, the 2,560 px flat version only once the viewer outgrows
1,280 px, `refreshPose`, and one sharper-panorama load at a time with a
second look once it lands. Its tile cache's `keyOf` and `clear` move to
the shared tileCache.js. KartaView gets `onEscape` beside `onReopen`.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A final review of both PDFs found text the earlier checks missed:
- Map cards: fire confidence (high/nominal/low) and age (24h), vessel
types (TOWING, TANKER) and installation classes (BASE, AIRFIELD) showed
in English. They now go through the catalog; the data stays English.
- The wind readout kept "10-14 14:00 UTC" and "29.93°N 97.74°W"; it now
uses the locale's date, decimal separator and hemisphere letters.
- Catalog: one term per concept (label/подпись, basemap/подложка,
PROVIDER CONTROLS, HEARD, zoom in, REGIONAL NEWS), calques and broken
grammar in the scene editor, provider settings and Bhote Koshi notes,
and Spanish wording that read wrong ("restringidas por el proveedor",
"Incluye … no se incluyen", "justo ahora", "Sal").
Co-Authored-By: Claude Opus 5.5 <[email protected]>
From the round-2 verification (approve with follow-ups), all follow-ups:
- The point budget is shared across the tiles in view once loading
settles (water-filling): a sparse tile keeps what it has, a dense city
centre takes the rest instead of drawing thinner than its suburbs. The
selected sequence is always drawn, and selecting one a cap had left out
draws it.
- The cones' picture-list cache keeps only finished lists: a re-centre
while the cones loaded inherited the aborted load's tiles and drew
none in them.
- Douglas–Peucker measures to the segment, so dead ends and U-turns keep
their far end.
- Switching Panoramax off lets its decoded tiles and lists go.
- Picture downloads are counted while they stream (a host sending no
length cannot fill memory before the cap).
- Comments on the via caption and the simplification tolerance match the
code.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Esc moved to the viewer's box (`.sl-viewer-wrap`) in the last commit, but
without one it was a second keydown listener on the root; Esc is now taken
by the root's own handler there. format-scope.json listed
panorama.test.mjs twice, which made the formatter refuse the scope; the
files it skipped are formatted.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
From a five-lens IC8 review (correctness, security, tests with mutation
testing, performance, UX):
- P0: over Paris at 12–60 km the unsimplified z10–z12 lines sent ~2M
points to Cesium's ground-polyline builder (gigabytes of geometry; the
tab hung). decode.js now simplifies lines (Douglas–Peucker, 4 tile
units: 6–10% of the points), and lineCoverage gains an opt-in point
budget (`maxPoints`, 10,000 a tile for Panoramax).
- Decoded coverage tiles are kept (KartaView's tile cache moves to the
shared tileCache.js); a sequence's z15 picture lists are kept and load
on their own connection; cones know their tiles from the start of a
load, so a picture opening meanwhile re-centres them and ‹ › no longer
restarts the load.
- The HD panorama loads only for the expanded viewer (a docked one on a
dense screen asked for 3 MB originals) and is aborted when another
photo opens; picture files over 48 MB or 16,384 px are refused; a
bitmap no renderer took after a context loss is closed.
- A nearest lookup survives one of its two searches failing; only a
timeout is retried; offline reads as unreachable; Retry-After is
capped at five minutes; the search box stays on the map at ±180°;
cones near both edges of a narrow (high-latitude) tile read both.
- Every producer is credited (app and photographer); with none to name,
the caption says "via <instance>" rather than crediting the host.
- Esc anywhere in the docked viewer (toolbar, caption) clears the
sequence before the panel takes it. A filter that hides every density
dot says so; wording aligned ("coverage", "photo").
- Tests for every surviving mutant (Esc consumed, keepOrder and dot-size
wiring, licence in the core, limiter aborts, radius floor, bbox corner,
first-producer-only, byte and size caps), a limiter test, and a gate
fixture that answers box searches and leaves out pictures facing away;
the gate now clicks over the other sequence's pictures, so only the
line click's own-sequence lookup can pass.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
KartaView's server filters by upload date only, so SINCE goes by it
throughout for KartaView: the lines (as before), the photo a click opens
and the cones, the cut-off taken as its UTC day as KartaView's startDate
(passesKartaViewFilter). Providers may declare sinceBasis: 'upload'
(validated); while one is lit the panel shows "KartaView: by upload date"
under the SINCE slider, read out with it. The gate checks the note, and
step 13's picks get 60 s on a loaded machine.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
KartaView filters coverage on its server only in its PNG tiles (its JSON
tiles come back empty with a filter), so a filtered tile's PNG is a mask
over the cached JSON segments: a segment stays when at least half the
points along it lie on drawn pixels. A filter change reloads the lines
(lineCoverage.reload keeps the drawn ones until the new ones are in);
masks are cached per filter and tile, and a filtered tile KartaView is
still drawing (504 or a timeout) is asked again, four tries in all.
The nearest lookup asks the photo API for the projection and date, and
the cones follow the filter like Mapillary's. The gate fixture draws
filtered PNG tiles and adds an older flat-only street; a new gate step
checks 360 and LAST YEAR thin KartaView's lines and ALL brings them back.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
From a UX QA round in the browser against the live API:
- A line click could time out while coverage loaded: the meta-catalogue
speaks HTTP/1.1 only (six connections), and six tiles plus four
searches queued the lookup behind megabyte tiles. Tiles now take
three connections and searches two; a search that timed out is asked
once more.
- Between 30 and ~100 km Paris read as empty: the density grid is ~10 km
a cell. Lines now draw to 60 km (z10), as Mapillary's do; above, dots
grow with the pictures they stand for.
- Esc in the docked viewer collapsed the whole panel instead of clearing
the sequence the panel says it clears (shared photo viewer: KartaView
too). The viewer now offers Esc to the provider first; expanded, the
dialog keeps it.
- A picture whose producer is an email address is credited to the
instance holding it (its `via` host), so Licence Ouverte pictures name
their source.
- Empty states say why: "no Panoramax photos here match the filters"
when the filter hid every line, and nothing to zoom into over empty
sea. Wording follows KartaView's ("photos", "click a line for its
nearest photo").
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Two other models reviewed the ru and es review PDFs. What held up:
- Directions: Russian reads "держитесь левее" at a fork and "займите
левую полосу" for a lane, Spanish "toma el carril izquierdo". Forks and
lanes get their own direction words; English is unchanged.
- Weather timestamps read "13.10 19:04 UTC" / "13/10 19:04 UTC" instead
of "10-13", which read as a range. English keeps "10-13".
- Cockpit coordinates write the hemisphere out in Russian ("30,312° с.
ш."): a lone «С» after a degree sign read as Celsius.
- A layer whose source joins several providers shows each name in the UI
language ("Mapillary · Google Просмотр улиц").
- Vessel speed uses the locale's decimal separator.
- Layer names no longer hyphenate into two-letter pieces ("es-paciales").
- Catalog: wrong meanings (cyclone centre, STREAMING, credited source,
"DE OÍDAS"), calques ("Пацифика", "Активная погода", "Прочитать вид",
"referrer", worker), Russian "щёлкните по …", shorter Spanish labels
that were cut (voice status, PREAJUSTES, search placeholder) and one
term per concept.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Resolve the CHANGELOG.md conflict by keeping both new entries: this
branch's group_by/stats entry above main's GBFS v3 station-name entry.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
From an adversarial functional QA round against the live API:
- The nearest lookup missed pictures looking away from the click
(place_position leaves them out at any tolerance): it now runs a
bounding-box search beside it and ranks both by distance. A line
clicked from high up searches its sequence within height / 100
(50–400 m).
- The per-tile line cap kept the newest sequences, not the longest
(lineCoverage re-sorted them): providers can now keep their decoded
order (`keepOrder`).
- A long producer name froze the tab (quadratic email regex): linear
check. A malformed link escape no longer fails the lookup; null or
string coordinates no longer land at 0,0.
- A line with no camera type or day stays drawn under the filters.
- A sky click on a line changes nothing (as KartaView), instead of
selecting a sequence with no cones.
- An unreadable tile reads as such; a failed neighbour z15 tile only
thins the cones; unknown or refused ids say the picture is gone.
- Shared viewer: the sharp-panorama guard is per open (a late download
of the previous photo no longer lets a second one start); an HD-only
360° picture is shrunk or skipped like any original; panorama.js read
the bitmap size after closing it, so 2:1 power-of-two panoramas
clamped instead of wrapping.
- Tests for each, and for the gaps mutation testing found (preferred
point, cone order, no reload within drawn tiles, newest-first gate,
stale bitmap release, flat upgrade retry).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The review fixes had turned RANGE into DIST. and SPD into VEL.; the
Spanish glossary keeps HDG, BRG, CRS, FOV, DEST, SPD, RANGE and ALT in
English like cockpit avionics. The glossary now records the terms the
review settled: ACTIVO / INACTIVO, marcador, resplandor, Ciber,
NOTICIAS, INCL. / ALT.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Panoramax, the open federated street-level commons, registers beside
Mapillary, Google Street View and KartaView. Its meta-catalogue
(api.panoramax.xyz) is keyless and CORS-open, so it runs in the browser:
- coverage from its vector tiles: sequence lines at z11–14 below 30 km
(longest first under the per-tile cap), and above that the grid layer's
density cells as binned points, not counted as sequences;
- a click on a line opens that sequence's picture nearest the click
(STAC search, any direction), without moving the camera, then any
sequence's if it has none within 50 m; the 360° and since filters go to
the lines and to the API;
- cones from the z15 tiles' pictures layer around the open picture
(~110 bytes a picture, against ~7.5 kB for a STAC item), re-centred when
a picture opens outside them; ‹ › follow the picture's prev/next links;
- the caption names the author (not an account named by an email
address) and the picture's licence (StreetPose `license`), and links to
the picture in the Panoramax viewer; the globe credits Panoramax.
KartaView's viewer becomes the shared photoViewer.js (flat <img>, 360° on
panorama.js, ‹ ›, keyboard, pinch, context loss), driven by a per-provider
PhotoSources table; the request limiter moves to limiter.js. Panoramax shows
the 2,048 px SD derivative and fetches the HD original only when the viewer
is wide enough, once, shrunk while it decodes (never above 8,192 px).
Orange #ff6f00 (Panoramax's own), off by default, share option
0.m.0_0.r.panoramax-1. The MCP panel allows the meta-catalogue and the
biggest instances. Tests: policy, decode, source, viewer, provider, and a
Panoramax fixture in the hermetic browser gate.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Viewer: resizes while a sharper photo loads start no second load (the
last size is looked at once it lands); decoded bitmaps nobody will draw
(an overtaken preview or upgrade) are closed.
- Cones: a photo opened while the drive's first page loads is the one
they centre on.
- A sequence page that lands while a lost WebGL context recovers keeps
its cones: no pose is emitted without a renderer, and a viewer error
never rejects page data.
- Source: an answer turned away unread (HTTP error, 429, length past the
cap) aborts its request and cancels its body, freeing the connection.
- Gate: findPickPoint no longer picks each centre five times at radius 0,
aims step 13's cone pick at the drawn billboards, and caps each
evaluation at 4 s, resuming from a cursor on the next poll, so a slow
machine cannot spend a whole wait in one evaluation.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Marker and direction (reported on the PR):
- KartaView has its own marker, a purple view cone (Street View's is
blue), not GEV's default pin that Mapillary uses.
- The camera direction (marker, cones, FOLLOW, a panorama's centre) is
the direction of travel to the next photo a few metres on. KartaView's
heading is the device compass: whole drives read 0 (no compass) and
others sit a steady ~110° off. Until the drive loads the compass stands
in (0 = unknown), and the pose is sent again once the course is known.
Remaining P4s from the review of #1121:
- Segment chaining is linear (a per-point cursor) and capped at 20,000
segments a tile and 5,000 points a line: a hostile tile of 60k segments
sharing one point took 1.5 s, now tens of ms.
- Decoded tiles are dropped when KartaView is switched off.
- No network / a refused request says so and is not retried as a timeout;
an unreadable answer says so.
- A docked flat photo skips the 2,560 px download until the viewer grows.
- The panel counts the drive's photos (countActivePhotos), not the thinned
page of cones (2,250, not 217).
- After a GPU reset the viewer redraws the photo itself, so a lookup the
user started is not cancelled; a failure is still reported.
- The gate's keyless page lights KartaView and checks it draws with no key;
the fixture answers the sequence count; the line-click step checks the
camera looks along the drive when the compass says 200°.
- README/CHANGELOG show the share-link form the app writes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
NYC DOT asks developers who use its camera feed to sign a data-sharing
agreement, and this project does not hold one that covers downstream
installs. The pack now loads only when CCTV_NYC_ENABLED=1; unset, empty
or any other value makes no request to webcams.nyctmc.org (tested).
The NYC credit carries NYC DOT's requested application disclaimer
verbatim, and DATA_SOURCES, README, CHANGELOG, CURRENT-STATE and
.env.example describe the prerequisite.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
From the IC8 review of #1121 (2 P2, 7 P3; each with a test that fails on
the old code):
P2
- A WebGL context lost while a newer panorama loaded left a black viewer
and a 'successful' open: the panorama now builds on the new canvas (or
falls back to its front view).
- The streaming body read every real answer takes was tested with one
chunk only: answers split mid-character are reassembled; the shared test
fake now streams, so every source test runs the production path.
P3
- Lighting another source by its chip (params) now stops a running lookup
and clears its spinner and error, as setProviderEnabled did; a restore
that changes nothing leaves them alone.
- Panoramas decode off the main thread (ImageBitmap, RGBA upload: ~90 ms →
~15 ms at 4,096 px on a phone-class CPU) and are 2,048 px docked, 4,096
once the viewer grows; a closed viewer keeps a 1×1 texture.
- The lit KARTAVIEW chip label is #8f8aff (~6:1; #635bff was 3.7:1).
- The viewer's keyboard focus ring is drawn above the photo.
- Tests: a second context loss and a drag on the new canvas; the look
direction kept across a loss; a page's 30 s first try.
- The browser gate loses a real WebGL context in the 360° step and checks
the 429 lookup was asked again (the fixture counter is reset first).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The layer recorded failure in a single `_error` string. Every city feed is
fetched independently, so one retired operator set that flag for the whole
layer — and the only code that cleared it ran on a successful *activation*,
which `runProximityCheck` skips for a city that already has rendered points.
A dead feed that re-entered camera range therefore set the flag on every
camera move and nothing ever cleared it: a permanent LOAD FAILED over a map
drawing every other city's stations.
Failure is now tracked per city in `_failedCityIds`, and `getStats()` reports
it only when the failures left nothing to draw. A city that recovers removes
its own entry rather than waiting for a neighbour to succeed.
This was reachable only because Buffalo sits 95 km from Toronto, inside the
100 km activation radius — the one dead city in the registry with a healthy
neighbour in range. The other four failed alone, where LOAD FAILED with an
empty map reads as correct, which is why the latch went unnoticed.
Probing all 33 registry systems found five dead. Three had moved and are
repointed: Reddy Bikeshare (Buffalo) to Social Bicycles, Tugo (Tucson) and
Trinity Metro (Fort Worth) to PBSC, all verified serving stations. Two are
dropped because the systems wound down rather than moved — Broward B-cycle
404s at every documented endpoint, and CoGo's replacement Lyft feed publishes
zero stations with status last reported in March 2025. `gbfs.cogobikeshare.com`
no longer resolves, so its allowlist entry goes too.
All 31 remaining systems were verified live, and every registry URL was
checked against the proxy host and path allowlists.
Toronto runs the same PBSC platform as the Austin, Honolulu and
Chattanooga systems already in the registry, and publishes the identical
GBFS v2 endpoint pair under `toronto.publicbikesystem.net`. The proxy
allowlist already admits the host through its `*.publicbikesystem.net`
wildcard and the paths end in the expected `station_information.json` /
`station_status.json`, so the system needs no new plumbing — only the
registry entry that names it.
Verified against the live feeds: 1,069 stations in both documents, all
1,069 joining on `station_id`. The layer reads `num_bikes_available`,
which Toronto populates normally, so availability renders without a
vendor-specific mapping. It does not populate the standard
`num_bikes_available_types`, reporting per-type counts under PBSC ids
(ICONIC/BOOST/EFIT/ASTRO) instead; nothing reads that field today, and
this entry deliberately does not translate it rather than adding a
mapping with no consumer.
The center is downtown at 43.6532, -79.3832 with the group's 90 km load
radius. The stations span roughly 37 km east-west, from Etobicoke to
Scarborough, so a tighter radius would drop the outer ends on a downtown
pan.
The new registry test is the reason this was cheap to be sure about: it
holds every city's feed URLs against the same host and path predicates
the proxy enforces, so a city the proxy would silently refuse now fails
the suite instead of rendering an empty layer.
Credited to Bike Share Toronto (Toronto Parking Authority). The City's
open-data package declares no licence and the feed publishes no
`license_url`, so it is treated as attribution-only, like the other GBFS
operators, rather than claiming a licence that was never granted.
Manifest start_url, scope and icon src resolve against the manifest URL,
so root-relative values sent an app served under a path prefix to the
domain root. Make them relative, and add a build test under
base: '/example/' that asserts every emitted link and manifest URL stays
inside the base. index.html is unchanged: Vite already rewrites its
links under base.
The pack shipped a flat 90 m. Sampling a DEM at all 336 camera sites puts
the network between 76 m (Queens Quay) and 209 m (Keele & Steeles), mean
116 m: a median error of 14 m against the old value, 88 m at the 90th
percentile and 119 m at worst, with the Allen Road cameras — 170-197 m —
the furthest underground.
No constant fits a city with that much relief; the precomputed ground
heights are what place a camera properly, and until this pack has them the
prior is the fallback for a sidecar miss or an edited pose. The mean is at
least wrong in both directions rather than only one.
This value is not part of the served pose, so it changes no pose hash and
invalidates no precomputed entry.
The province's 511 pack covers Ontario's highways; this adds the municipal
network underneath it — roughly 336 cameras across downtown arterials plus
the Gardiner, DVP, Allen and Lake Shore, from the City's own open data.
The list is keyless and carries no frame URL, so frame URLs are built from
the convention the dataset documents (`CameraImages/loc####.jpg`) and a
camera number validated as plain digits. The frame proxy is therefore
pinned to the portal origin by construction rather than by trusting a
field. The list fetch refuses redirects, bounds the body it buffers, and
releases the transport on every rejection path.
The dataset publishes no camera facing. `D1`-`D4` look like a compass and
map trivially onto one, but they describe the *comparison* images — static
reference stills captured in 2016 and served from a different path — not
the live pan-tilt-zoom frame. A heading derived from them would be
confidently wrong for every camera in the city, so every camera takes the
shared id-hash fallback at low confidence, as headingless Austin, TfL,
Fintraffic and Calgary cameras do. A test pins that down so it is not
"fixed" later.
Names stay verbatim: the feed's labels are upper-case, and title-casing
would mangle MCCOWAN RD and ST HELEN'S AVE more often than it helped.
Carries the Open Government Licence – Toronto attribution the licence
requires. Disable with `CCTV_TORONTO_ENABLED=0`.
Adding the app to a phone home screen produced a blank tile: index.html
linked only an SVG favicon, and Safari ignores SVG for apple-touch-icon,
substituting a screenshot of the page. There was no manifest at all, so
Android had nothing to install against either.
Generate the rasters from public/logo.svg with sharp, which the repo
already carries. The output is committed so a plain `vite build` needs no
image toolchain; re-run scripts/generate-icons.mjs after changing the
logo. The logo's viewBox is wider than it is tall, so each icon centres
the mark on a square #0a0a0f canvas rather than stretching it, inset to
78% normally and 54% for the maskable variant to survive Android's
circular crop.
The SVG favicon stays as the browser tab icon.
GBFS v3 uses `LocalizedString` for `name` and `short_name` of the stations. This caused stations from v3 to have names like `[object Object]`.
This commit makes the code handle v3 names. It default to locale 'en' then fallbacks to the first option in the array.
Documentation for v3: https://gbfs.org/documentation/reference/#station_informationjson
From the second IC8 pass (launch ready; P3s closed here):
- A lost WebGL context is redrawn on a new canvas (the old one only gives
back its lost context), through the core, and only if no newer photo is
opening; verified live with WEBGL_lose_context, twice.
- The 16 MB answer cap counts bytes while reading: KartaView sends
chunked answers with no Content-Length, so the header check never fired.
- A lookup gets 8 s, then 15 s once more: KartaView sometimes sits on a
lookup for 11 s and answers the same one in 3 s.
Left as follow-ups: cancelling abandoned sequence pages; finding a page
by binary search on the index when thousands of photos are missing.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A mutation review (91, then 130 mutants) found tests that could not fail
(quick presses with synchronous fakes, a no-flight check with a provider
that never posed), and untested paths. New and rewritten tests kill 123 of
130 mutants; the 7 survivors are equivalent or harmless redraws.
- viewer: held-back loads for quick presses, stale sharp/flat loads,
close mid-load, ends checks for an older photo, WebGL retried per mount,
focus handed back, look direction kept on ›.
- provider: id-ordered pages with out-of-order indexes, page and probe
failures retried, overtaken lookups, cones re-centred on ‹ ›, leftover
imagery filters hide no line or cone, coverage hints, tile cache.
- source: 429 backoff and Retry-After under mock timers, hang timeouts,
the nearest query's parameters. policy: storage-host injection, FOV 360,
longitudes past ±180. panorama: a stub WebGL context (angles, lost
context, re-measuring). lineCoverage.test.mjs: tooHigh hint, passes().
- core: openImage({frame:false}) and openNearest leave the camera
(with a posing provider and a default-open control), KartaView's legend
and wording end to end; viewerHost supportsRenderModes.
- Fixed tick-count waits replaced by an until() poll.
- Browser gate fixture: odd photos archived (409 at storage2, served by
the proxy), the first lookup rate-limited (429, Retry-After: 0); the
line-click step asserts the camera stays and › skips the archived photo.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The Contacts panel builds its markup in a variable before innerHTML, so
the unmarked-text check missed it: the window caption, "Named areas", the
"at least" count and each cohort's reason stayed English, and distances
read "4.6 km" in every language.
The reason stays English in the cohort summary, which the voice analyst
reads too; it now also carries its template, and the panel shows it in
the UI language. Distances and counts use the locale's number format
and unit.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Screens that only open with provider keys showed a few gaps:
- Google Street View's own controls (Map Data, Terms, Report a problem)
and its addresses were always English: the Maps script now loads with
the UI language. English adds no parameter, as before.
- The selected-vessel HUD line kept KT and DEG after translated labels.
- Layer counts used a decimal point (12.0K); other languages use their
own separator (12,0K).
- A collapsed rail title was cut with an ellipsis in Russian; tighter
tracking fits it whole. English is unchanged.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
P1s from the correctness review, each with a regression test:
- Cones showed the wrong page of a long sequence (cache keyed by
sequence; no re-centre after a close or a same-drive line click):
KartaView always reselects, and re-centres whenever a photo of the
selected sequence opens on another page.
- A 360° photo's first frame was one colour: the canvas was sized while
hidden (1×1). It is shown first, and re-measured every frame.
P2/P3s: a storage timeout or 5xx is not remembered as archived (only
403/404/409/410 are); dense z13 tiles draw up to 1,600 lines (a third of
downtown Detroit was cut); decoded tiles are kept 10 min; a timed-out tile
is asked once more; pages are 250 photos and the newest page request goes
first; a photo far past deleted ones, or unlisted at a page edge, still
finds its neighbours; a step that gets no answer is not the end; a lost
WebGL context redraws the photo, and a failed one is retried on the next
mount; the first flat image comes from the proxy (KartaView's own can be
1.7 MB); fileurl fallbacks must be https storage hosts; oversized answers
are refused; keyboard (Shift+← →, ↑ ↓, + −) and pinch look around a
panorama; focus stays in the viewer when a step button disables. Docs:
share links with 0.m.0, keyless count, CURRENT-STATE matched to the code.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Six candidates at once on photo storage (not the API host): the nearest
showable one wins. Jakarta line clicks opened 5/8 (0/5 before the QA
round; the rest are archived and say so), Detroit 8/8.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
From two agentic QA passes (UX and failure modes):
- A line click opens the nearest photo that can be shown: archived photos
(409; a HEAD says 200) and records without files are skipped, a few at
most, else 'KartaView's photos here are archived'. ‹ › skip them too.
- api.kartaview.org is HTTP/1.1 (six connections): tiles take three,
lookups two, sequence pages one, so a lookup never times out behind
tiles; a timed-out lookup is asked once more; waits are capped at 10 s.
- Sequence pages follow photo ids, not the index: a photo's page is found
by search, a neighbour page joins near an edge, so ‹ › neither skip at
page edges nor loop past gaps. Pages are 500 photos (~1.2 MB).
- ‹ › open through the core without framing: loading, errors and cones
behave as for any open; presses step on from the photo on its way; the
ends disable up front.
- Cones ignore the hidden IMAGERY / SINCE filters; no 'Selected' swatch
for a source whose lines never highlight; 'no KartaView photos in view';
an unreadable tile is not cached as empty; a 0,0 fix is no photo; a
failed lookup's error goes when another source is lit; the link goes
through the image proxy; a stalled panorama shows its front view after
15 s instead of 60.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A screenshot review of the Spanish UI found:
- Text left in English. Catalog entries copied from English (Bloom,
Cyber, Pin, NEWS, SITE, ON/OFF, RANGE, PITCH, HGT…) and the radio
music genres, which never went through t(). The genres are translated
in es and ru now.
- Wording: gender agreement, calques of the English, hints that name
buttons that do not exist, and one concept worded two ways.
- Layout: the DESACTIVADO pill squeezed layer names until words broke
mid-word; the scene and camera selects showed one word; a collapsed
rail title pushed its button out of the card; the image slot line and
a comparison readout were cut or split.
The layout fixes are scoped to languages other than English, so English
stays pixel-identical. Visual-style slider values now use the locale's
decimal separator (0,85).
npm run i18n now reports a translation that is identical to the
English, unless src/i18n/same-as-english.json lists it for that language
(codes, product names, words spelled the same).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The fixture gate answers api.kartaview.org, cdn.kartaview.org and
*.openstreetcam.org from fixtures (a drive with a 360° photo, stored on
storage2 while the API names storage1) and adds three steps: keyless
coverage with its credit and hidden filters, a line click opening the
nearest photo with cones and ›, and a 360° photo on the panorama without
FIT / FILL. The Street View share-link check reads the switch field instead
of one exact entry, since a chip click records every provider's switch.
README, DATA_SOURCES, CHANGELOG and CURRENT-STATE describe KartaView.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Coverage lines from KartaView's z13-14 JSON tiles (OSM ways with photos,
chained end to start), the photo nearest a clicked line, cones along its
sequence, and a viewer for flat photos and 360° panoramas (a small WebGL
equirectangular renderer, no new dependency) with ‹ › along the sequence.
Mapillary's camera-driven line coverage and sequence cones move to the
provider-neutral core (lineCoverage.js, sequenceCones.js) with the tile
fetch, zoom ladder, colours and pick prefixes injected; Mapillary keeps
thin wrappers and its tests pass unchanged.
KartaView quirks handled: api.kartaview.org (the documented
api.openstreetcam.org serves empty tiles), photo URLs that name storage1
for photos stored elsewhere, and a photo API that answers 429 to more than
about three requests at once (two in flight, retried with backoff).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The Street Level error line and its toasts showed provider errors as
thrown (Google's key-rejected message stayed English). They go through
translateKnown now. In a translation the coverage hint no longer shrinks
to a narrow column beside OPEN GOOGLE STREET VIEW: the button takes its
own line when both do not fit. English layout is unchanged.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Main added the Google Street View street-level provider and key guidance
that offers another source. Its new text goes through t()/msg() with ru
and es entries; the key guidance the panel shows is built in the UI
language by src/ui/keyGuidance.js, while keySetupCore.mjs keeps the
English sentences the server and AI tools use.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Upstream's Street View rework (second imagery provider, one source at a
time, OPEN STREET VIEW button, key-alternative guidance) rewrote exactly
the street-level regions this branch localizes.
Resolved as upstream logic + re-applied localization:
- keySetupCore: new unlock copy kept as msg() data; added
keySetupAlternativeKeyNames() returning machine values (registry key
names) so key-setup core stays English while presentation localizes.
- New src/ui/keyGuidance.js: localized key-guidance composer shared by
layerPanel and streetLevelPresentation (requirement sentence plus the
"or light X to use your Y key" alternatives tail, placeholders only).
- streetLevelPresentation: keyGuidance()/presentNearest()/presentMeta
relocalized (t/tn + formatNumber instead of toLocaleString);
renderModes and month-precision dates kept from upstream.
- layer-panels.html: upstream row ids, sl-meta-row and the new button
kept, data-i18n hooks re-applied (incl. OPEN STREET VIEW fallback).
- es/ru catalogs: 13 new keys, stale unlock key replaced in place.
npm run i18n: complete for es and ru. npm test: 6345/6347; the one
failure (devFreshDotenv provenance) also fails on plain upstream/main
and on the pre-merge tip here - this machine leaks OPENAI_API_KEY into
spawned children with a replaced env, so it is environmental.
The fallback summary reads "NORMAL CITY NEAR …" again in English; the
translations keep the " · " separators that stop the style and view band
reading as one phrase.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Satellite class in analyst records and voice payloads stays English
("STATION", "COMMS"), so voice filters on the class work in every
language; cards and the legend still show the translated label.
- Layer source names (OSM routing, ALPR sources) stay English in layer
state; the layer row translates them.
- New translateKnown(): catalog text when there is an entry, the given text
otherwise, with no warning. The layer row uses it for loading labels and
errors, Recent Imagery for its notices.
- Mark the remaining Recent Imagery and wind messages.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- The Space Missions cancellation failure toast read "[object Object]" in
every language: the options object was passed as the message. The English
message is the second argument again, the translation the third.
- Header text is translated before panel chrome builds its collapse labels,
so the dock and display toggles no longer read "Expand VISUAL PRESETS" in
another language.
- The voice "show data layers" result keeps the English layer name
(row data-layer-name), not the translated row text.
- The Google 3D unavailable reason is translated in full.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
From the English-parity and breakage review against main:
- English wording back to main: "Configured Source", the "SHARED SUBJECT"
fallback, CCTV calibration "RANGE 120m".
- Voice header and CCTV calibration chip/select tweaks for long labels now
apply to non-English only (html:not([lang='en'])); English matches main.
The scene select font change is scoped the same way.
- New scenes and shots store their default title in English ("Scene 3",
"Shot 2") and translate it when shown, so a title captured in one language
does not leak into projects, storage or shared JSON.
- Choosing a language with storage blocked keeps it in ?lang instead of
silently doing nothing.
- One picked import file shows its name again.
- qa-l9-matrix reads the voice state from data-status, not the label.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A side-by-side English comparison with main found text the branch changed:
- Layer list names: #601 let its own catalog names win over the panel's
English names, so "Live Vessels" read "Live AIS Vessels", "Bike Share"
"Bikeshare", "Cameras" "CCTV", "Data Centers" "Datacenters", "Active Fires"
"FIRMS Active Fires". The panel names win again.
- HUD feed-state suffix is "UNAVAILABLE LIVE AIS VESSELS" again (Russian keeps
its colon); the mission panel aria-label is "Selected Space Mission" again;
scene shot timing is "4.0s + 0.9s" again.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Review on #856 (kvnloo): the analyst projection flattened a 7700 squawk and
the readsb ADS-B emergency field into the same emergency/emergencyKind pair.
- mapAnalystRecord (flights, military) adds emergencySource ('squawk' |
'ads-b' | null, from aircraftEmergency().source), emergencyBroadcast (new
pure emergencyBroadcastPhrase: "broadcasting squawk 7700 (general
emergency)" vs "ADS-B reports general emergency") and observedAtMs (the
record's lastContactEpochMs: last transponder message, epoch ms).
- emergencySource is a queryable analyst text field; the voice payload passes
it and emergencyBroadcast through, and observedAtMs only for aircraft with
a status. Tool description asks the model to narrate with the phrase.
- Source-to-analyst fixture test: OpenSky 7700 vs adsb.lol emergency=general
on squawk 1200 differ in provenance and timestamp; filterable by source.
- Voice tool-payload pins re-derived for the additive description change.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
- aircraftEmergency: `source` is 'squawk' only when the Mode A code itself
names the status; the Cockpit HUD no longer quotes an unrelated squawk
(e.g. SQUAWK 1200 on an ADS-B `downed` status).
- military: new billboards and the untrack restore use the fleet tint, so an
emergency stays red instead of snapping back to amber; tint helpers take the
record already in hand instead of a second lookup.
- cockpit/cyber CSS: emergency colour via a custom property / theme token,
meta line bolded so the status does not rely on colour alone in the HUD.
- tests: revert Prettier churn in unscoped test files; only semantic edits.
- docs/CURRENT-STATE.md: transponder emergency highlighting section.
- qa-emergency-squawk: (iv) RELEASE check; new files added to format scope.
Co-Authored-By: Claude <[email protected]>
Flights and Military Flights tint aircraft red while the latest poll
carries squawk 7500/7600/7700 or an adsb.lol ADS-B emergency status
(general/unlawful/nordo/downed). Minimum-fuel and lifeguard priorities
are reported but not tinted. The Cockpit HUD aircraft-meta line names
the broadcast code in red. The status is not sticky: a cleared code
stops highlighting on the next refresh.
The analyst/voice query engine gains squawk, emergency and
emergencyKind fields. No new source or request: OpenSky state vectors
already carry the squawk and adsb.lol/readsb carries both fields.
Built by the Hex project (Crew Leo Agile dev team).
Co-Authored-By: Claude <[email protected]>
The layer compared the server's fetchedAt with the browser's clock, so a
skewed client clock could shift or void the 30-minute window. The proxy now
returns ageMs (its own now minus fetchedAt, at serve time, on fresh, cached
and stale answers); the source refuses a snapshot without a finite,
non-negative ageMs, and the layer dates rows as its now() - ageMs. fetchedAt
stays in the payload for display.
Tests: proxy cached/stale answers report ageMs on the server clock; the
source rejects missing/invalid/negative ageMs; the layer replay test drives
ageMs. qa-gnss stubs send ageMs.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Re-enabling after a disable longer than the window no longer shows the
old cells until the next refresh. A healthy report arriving 31 minutes
after a degraded one, with no prune in between, starts clean; exactly
30 minutes still counts.
Maintainer review on #855 (mazy1998, 2026-10-06):
- records: presence (seenAt) and degraded evidence (degradedAt) age
separately, so continuing healthy reports no longer keep an older
degraded flag alive past 30 minutes; an aircraft returning after the
window starts fresh. pruneGnssObservations ages the store with no new
rows.
- Rows are dated by the snapshot's fetchedAt, which the proxy keeps on
cached and stale answers: a replay never restarts the window, the
proxy stops serving a stale snapshot older than the window, the source
refuses an undated one, and cells expire during fetch failures and
while there is no view anchor.
- Normalization accepts ADS-B versions 1 and 2 only; a missing, invalid
or negative seen_pos is not a confirmed position from the last minute.
- qa-gnss: keyboard, 390x844 viewport (fresh page: isMobile reloads) and
replay-age sections; the live cell assertion bins the returned rows
with the layer's own rules instead of a raw row count.
Review on #855: the per-aircraft rule (NIC < 7 || NACp < 8, borrowed from
the US 14 CFR 91.227 ADS-B Out minima) is GEV-defined, not gpsjam's, and
gpsjam's map shows low navigation accuracy, not necessarily jamming.
- Primary cell state is now `navigation-integrity-anomaly` (medium/high) or
`nominal`; `suspected-gnss-interference` is a secondary `interpretation`
marked validated: false.
- GNSS_PROVENANCE / gnssProvenance({ windowMs }) records the classifier
(gev-nic-nacp-v1, GEV, unvalidated), the gpsjam aggregation (formula,
bands [0.02, 0.1], FAQ source) and the GEV 30-minute visited-view window,
built from the constants the code applies. Exposed as layer.provenance
and getStats().provenance; the proxy tags responses with `classifier`.
- Visible name "GNSS Integrity", legend "... low accuracy", blurb leads with
low navigation accuracy and states the threshold is a GEV choice.
- Layer id `gnss-interference` and share token `0` unchanged.
- Voice aliases add "gnss integrity", "navigation integrity",
"low navigation accuracy"; the interference aliases stay.
- Docs describe the method as GEV threshold + gpsjam aggregation.
- Tests: provenance object, renamed state/label, proxy classifier tag;
qa-gnss asserts provenance and wording in the real app.
Hex project - Crew Leo Agile dev team.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
- Route the adsb.lol GNSS source through createReferenceSources and the
constructCatalog contract; app layer module is a thin pass-through.
- Describe the method honestly: gpsjam.org's published cell formula and
bands; per-aircraft thresholds are the 14 CFR 91.227(c) ADS-B Out minima
(NIC < 7, NACp < 8) plus readsb gpsOkBefore.
- Proxy: reuse the adsb.lol anchor parser, honour upstream Retry-After
(clamped 5-120 s), serve stale during cooldown, cap the body size.
- Layer: redraw only when a cell id or band changes, treat a settling
camera as a retry rather than a rejected enable, keep low cells visible,
skip rows without finite coordinates, state the 3-aircraft rule in the
legend blurb.
- Voice: gnss-interference in set_layer_visibility / show_data_layers_menu
plus common aliases; voice pins re-derived.
- qa-gnss: stub the route in-page (request interception stalls Cesium's
geometry workers), assert every cell is built, cover all bands over land
and sea, screenshot every state with the Data Layers row open.
Hex project - Crew Leo Agile dev team.
Co-Authored-By: Claude <[email protected]>
Bins airborne ADS-B v1/v2 aircraft that report degraded navigation
integrity (NIC < 7, NACp < 8, or readsb's GPS-loss flag) into 0.5 degree
cells over a rolling 30-minute window, coloured with the gpsjam.org
bands. Keyless: reads the capped adsb.lol point snapshot around the view
through /api/gnss-integrity, which returns integrity fields only, caches
60 s per whole-degree anchor, backs off on upstream 429 and serves the
last snapshot on failure. Adds scripts/qa-gnss.mjs.
Built by the Hex project (Crew Leo Agile dev team).
Co-Authored-By: Claude <[email protected]>
From the re-shot Russian screenshots:
- Translated: weather frame/imagery status errors, HUD summary provenance
("НЕДОСТУПНО: СУДА AIS ОНЛАЙН"), Bhote Koshi map labels, cockpit signal
distances (КМ). The HUD summary fields are separated with " · " in every
language, so the style name and view band no longer read as one phrase.
- Numbers follow the locale: cockpit altitude ("26 175"), precipitation.
- No breaks inside codes and dates: ADS-B and similar in layer rows,
Sentinel-2, cyclone issue dates (word joiner; the UI font has no U+2011),
no-break space after label colons in cyclone details.
- Layout: count lines under ALPR, short unavailable chip (context
"status chip"), roomier draw segments, tighter CCTV calibration chips,
two-line beat titles and media card titles, wrapping weather status.
- Wording: ru advisory is «сводка» (was «бюллетень»; "Advisory center" is
«Центр циклона по сводке»), Street View is «Просмотр улиц», OPACITY
«НЕПРОЗР.», synced «синхр.», Area «Зона»; the import dialog no longer says
"this scene" for several scenes. Glossary updated.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- One provider is lit at a time: lighting a chip darkens the others, and a
chip click records every registered provider's switch in one update so
the share link keeps the swap (also off Google 3D, where Street View is
not listed). The layer starts with the first default-on provider
(Mapillary, free); params that light several keep the one already on,
so reapplying them never flips (litByParams). The now-unreachable
"several lit" chip paths are gone.
- Leaving Google 3D during a Street View lookup ends it without an error
or a stuck loading state (openImage's early return resets it; the
lookup loop rechecks availability).
- A source swap during a lookup no longer reports "no imagery".
- Street View lit off Google 3D says the panel needs the Google 3D map.
- Comments trimmed to the why; keySetupCore.test.mjs back to upstream's
formatting plus the one new test (was a 551-line reformat).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Assert through computeInRangeCities that a camera over each system's
outermost live-feed station activates it, including both Kona and Hilo
for HIBike, and that HIBike stays off over Honolulu. Drop Pennsylvania
from the changelog: Philadelphia's Indego is already registered.
CHANGELOG-only conflict: the bullet stays first under [Unreleased], ahead of
the entries upstream added there. No code changes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- The chip reads GOOGLE STREET VIEW (and its caption link likewise).
- Providers declare the imagery filters they honour (`filters`, default
pano + since). Street View declares none: every panorama is 360° and it
draws no coverage to thin, so its lookups ignore the filters and the
panel hides IMAGERY and SINCE while it is the only lit provider.
- The legend lists only providers that draw coverage lines, with
Selected only beside one: Street View alone shows no legend.
- Providers may pick their on-globe marker (`marker`); Street View's is a
Google-blue field-of-view cone from a white-ringed dot, in place of
GEV's amber pin, and switches back for a Mapillary photo.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
.scene-btn (flex: 1) loads after street-level.css and outranked the
button's flex: 0 0 auto, so with Mapillary and Street View both lit the
button shrank to its 72px minimum and its one-line label ran past the
panel edge. Scope the rule under .sl-meta-row; the coverage line wraps
instead. The live gate gives the share-link check 30 s, as the link is
written only once the view rests.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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.
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.
Draped lines build in a worker after their tile decoded: on a slow CI runner
the single pick scan ran before the line existed and the gate failed at
"a sequence line across the middle of the view". The scan now retries for up
to 30 s.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Coverage lines are 2.5 px wide and there is no hover cue any more: with the
default 3 px pick window a click just beside a line selected nothing.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Review of #974 (asturksever):
- After a 429 the other tiles of the view, and the fetches queued behind the
first, still went to Mapillary. The proxy now answers 429 itself until the
wait is over, scoped to the token that was refused.
- A Retry-After sent as an HTTP date read as 60 s; it is now the seconds
left, with the same 10-minute cap.
- A body that failed to trim (up to the 48 MiB cap) could fill the 120-entry
cache; anything over 4 MiB is served but not kept.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
main grew the Street Level provider registry while this branch removed it
(#982–#985: injected providers, an empty composition, the `r` switch field).
The registry, params, uiState and pick router come back as main has them and
the slimmed layer sits behind the provider contract:
- The core (index.js) drives providers as on main, without surface modes,
ground casting, FOLLOW, prewarm and nearest-image lookups. A provider's key
status is asked with a 10 s timeout and asked again on a switch-on while no
key is known.
- Mapillary is mapillaryProvider.js over the slim coverage, sequences and
viewer modules, which read the filter and the active flag from the provider
context.
- The viewer host mounts one provider adapter at a time, no prewarm.
- The panel has a chip per provider again (the pill switches the layer);
no legend, EXPAND stays the Fullscreen API.
- The contract drops what no provider implements: groundCaster, meshSampler,
getSurface/setSurface, nearestImage, prewarm, reportError, the legend and
surface fields of the UI snapshot.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Register the keyless webcams.nyctmc.org catalog as a live CCTV source,
synthesize frame URLs from validated camera UUIDs, and keep the nearest
cameras to the five borough cores under the existing catalog ceiling.
A review of the Russian UI screenshots found ~90 places where longer words
broke the layout or text was still English. Fixed so they hold for any
language, then shortened where a control is short by design.
- Layout: label columns that size to their text (weather and wind settings,
Recent Imagery slots and opacity, style sliders, display panel), layer
list names and icons, buttons that wrap instead of overflowing (CCTV,
scenes, Bhote Koshi), voice header that always shows the cost, styled
EN | ES | RU chips, voice error tray above the dock.
- t() context: t('CLEAR', { context: 'action' }) is «СБРОСИТЬ» while the sky
stays «ЯСНО»; also used for radio categories, voice states and directions.
- Units and dates follow the locale through src/ui/unitLabels.js
(formatQuantity: localized number, translated unit, non-breaking space):
wind, pressure, cyclones, directions, CCTV, SDR gain, scene timing, flight
tooltip, Recent Imagery dates and resolution.
- Newly translated: directions turn-by-turn steps (built from the maneuver;
English matches the server word for word), HUD mode and summary, voice
card states, radio categories on the globe, CCTV status, layer source
lines, scene dialogs (Cancel, file pickers), Bhote Koshi statuses, titles,
comparison dates and caveat, Recent Imagery notices. Stored data and text
for AI tools stay English.
- es and ru: 1,885 messages each.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Register four live US systems on Lyft's PBSC platform: MoGo (Detroit, 81
stations), POGOH (Pittsburgh, 60), WE-cycle (Aspen, 101) and HIBike (Kona
and Hilo, 23). The proxy already allows *.publicbikesystem.net, so no
server change is needed. HIBike's centre sits between Kailua-Kona and
Hilo so a camera over either town loads the whole system.
The test checks each system is registered, passes the proxy's host and
endpoint checks, and has an activation radius covering its farthest
station as measured against the live feed.
Retain upstream changes to the panel controller, formatting scope, changelog and docs. Preserve the #804 ownership fix and its rail restoration regression (original report: @raysung-2026).
Close the overlay when adaptive layout restores an auto-collapsed Context directly. Regression covers the reachable rail reflow path; changelog credits the original report by raysung-2026 (#804).
Mark every UI string the unmarked-text check reported (534) plus text it
cannot see (helpers, joined labels, runtime-built panels), and translate
the result to Spanish and Russian: 1,679 messages each.
- Kept on purpose, with the reason where the text lives: provider and
product names, place and camera names, codes and units, dB values, the HUD
terminal readouts, demo shot titles that saved projects look up by title,
and text for AI tools and the voice model.
- Modules shared with the server or AI tools stay free of the i18n import
(local msg); the wind reading and voice session stay English and are
translated where the page shows them.
- npm test now runs the unmarked-text check.
- Fixes on the way: a duplicated radio privacy paragraph, the contact panel
toggle saying Expand while open, the CSS filter mistaking & for CSS.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
npm run i18n now also lists, and fails on, markup text and title /
aria-label / placeholder without data-i18n*, and string literals that reach
the page (textContent, innerHTML, setAttribute, toasts, label:/title:
properties and similar) without t(), tn() or msg(). Opt-outs say why where
the text lives: translate="no" and data-i18n-runtime in markup,
// i18n-ignore in code. src/tools/ is skipped: its text is for AI clients.
534 places are reported today; npm test does not run this part yet.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Both panels landed after the original string extraction, so they rendered
English in every locale. Mark their markup and runtime text (status, SINCE
stops, provider tooltips, coverage meta, viewer captions, draw hints and
colours), the layer-list names kept in PANEL_LABELS, and add 90 es and ru
entries. Street Level is «Уличные снимки» / «A pie de calle».
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The slider labels (Sensitivity, Bloom, Pixelation, …) live in the shader
modules and were rendered raw, so they stayed English in every locale.
Mark them with msg() and render them through t(); add es and ru entries.
WHOT/BHOT and Ironbow keep their names. Russian Bloom is now «Свечение».
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- An open after a failed one is no longer failed by the old panorama's
status: only status_changed can reject; position_changed only resolves.
- A nearest lookup aborted, timed out or refused while Google's script
loads no longer sends its getPanorama request once the script arrives.
- A panorama-details lookup that never answers times out after 10 s,
cools down and retries; an unmount releases lookups in flight, and
their late answers are dropped.
- Every Maps loader on a page shares one script load and ready callback,
and either can retry a failed script.
- OPEN STREET VIEW aimed at the sky opens nothing and says to aim at a
street, instead of looking up the camera footprint.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The Catalonia feed has no camera status field, so an offline camera keeps
its catalog entry and SCT or Barcelona answer its frame with their own
"image unavailable" still (HTTP 200). Say so in DATA_SOURCES.md and stop
the loader comment from implying every camera returns a live frame.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The changelog entry sat below the 0.2.0/0.2.1 release headings after those
were cut; move it to [Unreleased] beside the Norway pack. Name Catalonia in
the README's CCTV Mesh row the way the other packs are listed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Register es next to ru (selector shows EN | ES | RU) and complete the
catalog against the current English: the 13 messages added since #602,
the "many" plural form Intl.PluralRules('es') selects for exact millions,
and fixes where the English had changed (e.g. "hace hace" in LIVE ·
updated, minutes written as metres). Terminology made consistent (buque,
aeronave, incendio activo); key names stay in English. Meir's wording is
kept everywhere else. Adds docs/translations/es.md.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Conflicts with upstream's Street Level provider composition and share-state
codec, resolved:
- Street View's switch moves from its own share option (`g`) to upstream's
provider-switch field (`r`, e.g. `0.r.google-1`); a link without it keeps
Street View off. Docs and the browser gate follow.
- createApplicationStreetLevel keeps upstream's injectable provider list;
its default, once the application supplies its Street Level source, is
Mapillary plus Google Street View. Without that source there are none, as
upstream intends.
- The coverage hint keeps upstream's empty-composition message and joins
every switched-on provider's hint otherwise.
- CHANGELOG, README and SECURITY keep both sides' entries.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Meir Michanie's Spanish translation from #602 (9d69e11, 4ba5d69), converted
to the English-keyed format: one entry per English message in
src/i18n/locales/es.json, translations unchanged. Also carries the es-only
CCTV control-row wrap rule.
Co-Authored-By: German Khokhlov <[email protected]>
Replace the semantic keys with the English source text, so code reads as the
UI does and English needs no catalog: t('Save keys'), tn(count, one, other)
for plurals, msg() for text stored in data, and bare data-i18n* markup that
translates the element's own English. Other languages are one flat JSON each.
- Simplify src/i18n: drop namespaces, the configurable locale pair
(GEV_DEFAULT_LOCALE / GEV_SECONDARY_LOCALE) and its build defines. Locale
comes from ?lang=, the stored choice, then browser languages.
- Add scripts/i18n.mjs (npm run i18n): extracts every message from t/tn/msg
and markup, and fails on missing, unused, placeholder-mismatched or
wrongly-shaped plural entries. npm test runs it via catalog.test.mjs.
- Add Russian (src/i18n/locales/ru.json, 833 messages), started from the
community catalog in #312 and reviewed against the current English.
- Fix data-i18n placed on parents that would wipe their children (cockpit
LEVEL and local weather cells, first-run EXPLORE MANUALLY tile), and a
doubled degree sign in the TRACK ACQUIRED signal.
- Rewrite docs/TRANSLATORS.md for the new workflow; drop sections the earlier
merge resurrected in docs/CURRENT-STATE.md.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A KMZ could name one large packed image in thousands of <href>s, and each
one made a fresh Blob copy for Cesium to decode. kmzImageResolver now keeps
one blob URL per archive entry, and countKmlGeometry refuses a document
with more than 2,000 <href> and <Style> elements (MAX_KML_LINKS) before
Cesium reads it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Both systems publish keyless GBFS 2.3 feeds on publicbikesystem.net,
the same platform as the existing Austin, Honolulu and Chattanooga
entries (listed in MobilityData's systems.csv).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The three mutation gates matched source text that has since moved or been
reflowed, so 37 of their 108 reverts reported STALE and could no longer
prove anything:
- qa-floorhold-mutations (2): the neighbour-floor code moved from
src/data/groundFloor.js into src/services/groundFloor.js (ac977bd).
- qa-flyroute-mutations (8): src/cameraVerbs.js was reflowed by the
formatter (2aca7ae); the anchors now match the formatted lines.
- qa-firstrun-mutations (27): the launcher markup, styles and startup moved
to src/ui/templates/welcome.html, src/ui/styles/first-run.css and
src/app/startupChrome.js; the rail default to panelPositionControls.js and
the radio ESC handling to radioBindings.js (a67bc60 and follow-ups). The
tool-schema pin now edits a tool description, since the context-mode enum
it used to edit no longer lives in tools.js.
Every revert still reintroduces the same defect. Results: floor-hold 22/22,
fly_route 19/19, first-run 67/67 caught, with all source files restored.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Voice leaves aircraft_in_area and vessels_in_area out, so the changelog no
longer suggests spoken questions gain breakdowns. It also stops leading with
operators, which the default civil feed never reports.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
OpenSky, the default civil feed, reports no operator or type code, so a
breakdown by either put every aircraft under unknown while the summary said
nothing. The summary now says "no operator reported" in that case, and
otherwise appends the unknown count after the largest groups.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
aircraft_in_area and vessels_in_area return at most 200 rows, so a client
asked which operators dominate an area, or the average altitude there, could
only estimate from a truncated sample. Both tools now take an optional
group_by (aircraft: operator, origin_country, type_code; ships: type,
destination) and stats (airborne altitude and speed for aircraft, speed for
ships). Each is computed over every match before rows are capped. Breakdowns
list the ten largest groups plus other and unknown counts, and the summary
names the three largest.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- keySetup.unlocks.openai follows the registry's new wording
('API key or ChatGPT OAuth')
- add keySetup.unlocks.mapillary for the new Mapillary client-token key
- add vision.styleNameAnime/styleNameSnow for the new anime/snow cockpit
vision modes
- add layers.meta.dataSourceUnavailable for the unavailable-layer tooltip
fallback introduced upstream
The Path 3 section assumed readers already knew how a bind-mounted container
persists state (14traits, #208). It now lists what the app saves and where,
with the NAS and reverse-proxy detail in the collapsed block that was already
there, so the open part stays about the same length.
It also fixes two commands that did not do what they said in bash, confirmed
in bash and dash by PoBruno: `UID=1026 GID=100 docker compose up` prints
"UID: readonly variable" and still runs as the caller's UID, and
`export UID GID` exports an empty GID because bash does not define one. Both
are now `env UID=... GID=...`, which `docker compose config` resolves to the
requested ids.
Suggested-by: 14traits
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Two findings from PoBruno's Docker Engine on Linux run (#208), and one from
re-running the suite in the container after rebasing:
- The launcher pinned its own allowedHosts, including '.local'. Vite reads a
leading dot as a suffix match, so `Host: anything.local` reached the page and
the data proxies. Main has since moved the policy into build/allowedHosts.js,
which drops suffix entries on purpose and applies before the app's routes, so
the launcher's override is deleted rather than patched. As a side effect
GEV_ALLOWED_HOSTS now also works from `.env`, as it does under `npm run dev`.
- A checkout the container cannot write - typically a clone made as root on a
NAS or Proxmox host - made Save Keys answer 500 and never created .gev-cache,
with nothing logged. The launcher now warns at start, naming the owner, the
ids it runs as, and the two fixes.
- On Docker Desktop the bind-mounted repo always shows an owner other than the
container user, so git refuses it as "dubious ownership" and the tests that
shell out to git fail (src/data/bundledJson.test.mjs, new on main). The image
now marks /app, and only /app, as a safe directory.
Verified in a container on this base: `anything.local` 403 on the page and on
/api routes, localhost 200, GEV_ALLOWED_HOSTS honoured from the environment and
from .env, the warning on a root-owned /app run as 1000:1000, and
bundledJson.test.mjs 6/6.
Reported-by: PoBruno
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Docker delivers the host's own browser to the container from the container
network's gateway, never from 127.0.0.1, so the loopback-only admission gate
answered every POWER UP request with 403 and no key could be saved. The README
and CHANGELOG claimed otherwise; both are corrected here.
- keySetupCore: GEV_KEY_SETUP_TRUSTED_PEERS, an exact-address list (no
wildcards, prefixes or hostnames) admitted alongside loopback. Unset trusts
nothing, and the sharing, Host, Origin, Content-Type and proxy-header checks
all still apply to a listed address. Twelve assertions pin each refusal.
- scripts/docker-start.mjs: the container entry point, shaped after
scripts/pinokio-start.mjs. It reads the default gateway from /proc/net/route,
trusts exactly that address, and starts Vite. No gateway (--network none)
trusts nothing. The pure helpers are unit-tested and a contract test pins the
Dockerfile CMD and the loopback-only publish.
- Dockerfile: install git. `npm test` builds fixtures with `git init`, so the
suite failed 71 tests inside the image and the README's claim that it runs in
the container was wrong.
- SECURITY.md and .env.example document the setting and why it is safe only
behind a 127.0.0.1 publish; README gains the Linux/NAS id note and .dockerignore
drops macOS AppleDouble sidecars, both reported on the PR.
Verified in Docker on a clean build: saving a key from the panel writes the
bind-mounted .env (0600 in the container), it survives `compose down`/`up`, and
REMOVE comments it back out. A foreign Origin, a foreign Host, a proxy header
and a sibling container on the same network are each refused. `npm run doctor`,
`npm test` (5067 passed, 1 skipped, 0 failed) and `npm run build` are green
inside the container.
Reported-by: @jklaze
Co-Authored-By: Bogdan Petrut <[email protected]>
Co-Authored-By: Claude Opus 5 <[email protected]>
Vite's dev server is the whole app here — it serves the client and runs every
API proxy as middleware — so one container running `npm run dev` covers it, and
HOST/PORT are already env-driven.
Three details are deliberate:
- The port publishes to 127.0.0.1. Docker's usual "4173:4173" binds every host
interface, which would silently make the README's LAN warning the default for
a server that brokers configured provider keys.
- The repo is bind-mounted, so keys saved from Provider Settings, .gev-cache
and .gev-logs persist on the host exactly as they do under `npm run dev`.
node_modules stays in the image so a host install cannot shadow the
Linux-built native deps.
- The container runs as the host user, since everything it writes lands in
that bind mount. Vite's dep optimizer still needs to write inside the image's
node_modules, hence the two loosened directories.
Puppeteer's Chromium download is skipped: it is only needed by `npm run
test:track`, which the README points at the host.
Co-Authored-By: Claude Opus 5 <[email protected]>
Review of #966: with a Google Maps key but no Mapillary token, switching
Street Level on read KEY REQUIRED / "Needs MAPILLARY_CLIENT_TOKEN" with
nothing about Street View, which stayed off (billing), so the layer looked
broken.
- While the switched-on providers lack their keys, the layer reports the
switched-off ones that would draw with a key already set
(keyAlternatives: Street View on Google 3D with a Google key). The
panel's KEY REQUIRED tooltip, its coverage line and the layer row's
tooltip then read "Needs MAPILLARY_CLIENT_TOKEN — add it in Provider
Settings, or light STREET VIEW to use your Google Maps key"
(keySetupWithAlternatives). Off Google 3D the plain KEY REQUIRED stays.
Street View stays off by default.
- OPEN STREET VIEW moves beside the coverage hint: at the default panel
width the PROVIDERS row has no room for it, so it wrapped onto a line of
its own.
- The panel gate's corner drag names what lay under the pointer when it
fails.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The Street View step's cleanup switched the Street View chip off after the
step had left Google 3D, where the chip is hidden by design. The click then
threw "failed to find element", replacing the step's real result (here, a
Google key refusal) with a selector error. Return to Google 3D first, then
switch the chips and restore the original map.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Doctor and dev-fresh told every user without an OpenAI key that voice could
use ChatGPT OAuth, whether or not Codex was installed. Say voice is off
until a key is added or a Codex sign-in is selected, and mark that path
experimental.
The login child inherited the whole server environment, including
OPENAI_API_KEY and every other provider credential. Pass only what Codex
needs to find its home, open a browser and reach OpenAI through a proxy.
Voice-card captions from main priced transcription usage into the API-key
dollar meter. In a ChatGPT OAuth session that could trip the $2 warning or
$5 cap while the readout said COST UNKNOWN. Count them with the session's
other OAuth usage instead.
Maintainer follow-up to #966 review:
- A failed panorama-details lookup now waits DETAILS_RETRY_COOLDOWN_MS
(5 s) per panorama before a view event can retry it, so dragging the
view after a failure no longer issues a lookup on every pov change. A
success clears the pause; one panorama's failure does not delay another.
- SECURITY.md notes that allowing maps.googleapis.com in script-src also
trusts its JSONP endpoints, an accepted limitation alongside the
existing 'unsafe-eval' allowance.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Follow-up to #686 requested on #923. The route's per-client limiter keys on the socket address, which for a cross-site <img> is the visitor's own browser, so any web page could fill the bounded cache with junk keys and spend the visitor's 90/min quota, leaving the app's own terrain lookups on 429. Gate the route with admitSameSite before the limiter, like the Places routes.
Co-authored-by: daikaginza <[email protected]>
/api/tomtom/flow was the only keyed, billed proxy left outside the cross-site gate added in #900. Every uncached tile is an upstream attempt counted against the persistent daily budget, so any web page the user visits could fire <img> requests at distinct z8-z16 tiles, spend the TomTom quota and leave the traffic layer on 429 budget for the rest of the UTC day. Gate both routes with admitSameSite, like the Places routes.
- No pointer cursor over coverage lines (no other layer has one); a click
test replaces the hover tests.
- No legend: one source, one colour.
- The on-globe credit lives in index.js; credits.js is gone.
- Billboard glyphs are SVG data URLs with the same geometry, so tests need
no canvas stub.
- The tile cache keeps the 120 most recently used tiles, same 24 h TTL,
instead of a byte budget; listTileLayers was test-only.
- providers/mapillary/ is flattened into the layer and its policy merged
into one policy.js; the source's method list is a literal in the catalog.
- One fakeCesiumViewer for the layer tests.
- The fixture gate keeps the steps the unit tests do not cover: coverage and
credit, a rejected key, line -> cone -> photo, close, a keyless page.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
No markup carries a drag handle, so nothing called it: panel position
controls keep only collapsed state and the layout-reset notice.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- A failed or missing key status is asked again on the next switch-on, and
the check gives up after 10 s instead of holding LOADING forever.
- Waiting for the key status reads as LOADING while the layer is on.
- Esc shrinks the photo in full screen too, before the panel's own Esc.
- Destroying the panel with the iPhone popover open leaves no popover behind.
- An empty view says whether the filter emptied it.
- A freed upstream tile slot passes straight to the oldest waiter.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Resolve the source-pack conflicts with Statens vegvesen (keep both packs).
The Norway pack's 900 default pushed the per-pack defaults to 5,204, past
the 4,500 catalog cap this PR enforces, so raise DEFAULT_CCTV_MAX_SOURCES
to 5,500 and CCTV_MAX_SOURCES_CEILING to 6,000.
- visibleBbox: no view-rectangle fallback or grid option; its one caller
always ranges the rays
- tilesForBbox returns the tiles; normalizeBbox takes arrays only
- viewerHost: no try/catch around adapter calls that catch for themselves
- coverage: counts come from the drawn sequences; no sequenceCount export
- one keyStatusLabel for the pill and the layer list
- no constructor source check, double-init guard or selectSequence /
clearSequence on the layer
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A client drops the tiles a camera move leaves behind; their fetches no
longer run on or hold the six upstream slots ahead of the tiles in view.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A release with a modifier held left the LEFT_DOWN/LEFT_UP flag stuck, and
right, middle or modified drags picked every frame. Track pointer buttons
instead, as main did, and pick a still pointer again on moveEnd.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Tiles a camera move leaves behind keep downloading after the client drops
them; the cap bounds their memory and quota, as main's proxy did.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- OPEN STREET VIEW in the PROVIDERS row opens Street View's nearest
panorama at the ground under the middle of the screen, without moving
the camera: Street View no longer needs a pointer. It is disabled (and
says so) until the camera is at street zoom.
- Off the Google 3D map Street View does not appear at all: no chip,
legend swatch, globe credit, layer source or button, and Google's script
is not loaded (the viewer has no prewarm; the script loads on first use).
The share option keeps the user's choice for when Google 3D returns.
- A ground click measures the camera height at the click rather than
reading the cached flag.
- A chip without a usable key explains it instead of lighting; it darkens
beside a lit chip, and the other chips keep switching, so a refused
Street View key cannot lock Mapillary out.
- The gate checks that nothing of Street View shows off Google 3D even when
a share link switches it on, presses OPEN STREET VIEW on Google 3D, and
asserts that each chip it switches really switched.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The panel connected before the shell had its data manager, so it never
heard switch-on requests and a user switch-on left it collapsed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Google's Maps Platform terms forbid Street View imagery beside non-Google
maps, so the provider declares requiresMapStack: 'photoreal': off Google
3D its chip is dashed with the reason, lookups and opens skip it, and
leaving Google 3D closes its panorama.
- Ground clicks: only a click on the map itself (scenePick.isSurfacePick:
nothing picked or Google 3D tile content) at street zoom opens Street
View, and the camera stays put; higher up the hint says to zoom in.
scenePick.pickGroundPosition replaces the copy in Directions.
- Viewer: poses go out at once and again when the date and address
arrive, so an open never hangs on that lookup; one lookup per panorama,
seeded by the nearest lookup; a close settles a pending open; failed
lookups are asked again; poses follow position changes, not pano_changed.
- Loader: the gm_authFailure handler is installed once, so a retried load
reports a refusal once.
- Viewer host: a prewarm that lands after another provider took the host
is released; FIT/FILL availability is set on every open.
- Provider: a refusal while switched off reports no error over another
photo; SINCE compares against the end of Google's month; messages name
the referrer and network causes; FLAT says Street View is 360° only.
- The layer row names the key of the providers that are on; the MCP panel
CSP admits Street View's tiles, control images and photos.
- Tests for each, against fakes that now follow Google's event order; the
browser gate restores its state, waits instead of sleeping, aims its
panel clicks and corner drags, and asserts the camera stays put.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Google Street View registers beside Mapillary as provider `google`. It runs
on the Maps JavaScript API with the browser GOOGLE_MAPS_API_KEY, loaded on
first use (mapsLoader.js), and uses Google's own StreetViewPanorama as its
viewer adapter, emitting provider-neutral poses (address and month in the
caption, a link to Google Maps, steps along Google's arrows, FOLLOW on
Google 3D). Its chip starts off because every panorama is billed to the
key; the share option is `g`.
Google offers no coverage layer for other maps, so the provider draws
none: with it on, a click on bare ground at street zoom opens its nearest
Google-collected outdoor panorama (groundClick.js). A key Google refuses
reads KEY REJECTED and fails a pending open at once; a lookup Google never
answers fails after 10 s.
Core: providers may declare `groundClick` and `defaultOn`; poses may carry
`capturedAtPrecision` and `title`; the coverage line keeps every
provider's hint; opening one provider's image releases another's
prewarmed viewer; FIT/FILL hides for viewers without render modes. CSP
script-src admits https://maps.googleapis.com. The browser gate gains two
Street View steps (the keyed one skips as 'no Google key' in CI).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Credit the equivalent contribution in PR #959 alongside PR #934, while preserving the original PR author's commits.
Co-authored-by: shirunjies8-png <[email protected]>
Three regressions from the slimming, found in review:
- Clean View, recording and the cockpit hid the old expanded viewer with
CSS; native fullscreen sits above all of that, so the photo stayed over the
globe (and in recordings). While expanded, the panel now watches the
<body> classes of those modes and leaves full screen when one starts.
- iPhone Safari has no element fullscreen, so EXPAND had disappeared there.
Where requestFullscreen is missing but popovers exist, EXPAND opens the
viewer as a top-layer popover sized to the viewport (it stays in the
panel's DOM; the popover attribute is set only while open, since a closed
popover is hidden). Checked in Chrome: the popover fills the viewport over
the panel's backdrop filter.
- Without the provider chips, a layer that was on with Mapillary switched off
by a share link or tool (`0.m.0`) showed ON and drew nothing. The pill now
reads OFF with a hint, and pressing it brings Mapillary back without
switching the layer off.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Two regressions from the slimming, found in review:
- Google 3D hides the globe, so groundUnderCamera returned null once the
ground caster was gone: in high cities (Denver ~1,600 m) the camera read
kilometres too high, the coverage zoom came out coarse and the visible box
slid kilometres ahead. With the globe hidden (or not yet loaded) the
ground now comes from the rendered surface (scene.sampleHeight), checked
for plausibility and ignored when above the camera. The plausibility check
is shared with photo framing (plausibleSurfaceHeight, sampledSurfaceHeight).
- Tiles were ranked from the camera's ground point alone, so a tilted view
from a few kilometres up (z13) could miss the tile at the centre of the
screen. They are ranked from viewFocus: halfway between that point and the
ground at the screen centre (or the nadir when the centre ray misses).
A test pins the 3 km / 70° case: the nadir ranking leaves the centre tile
out, the focus ranking loads it.
Also ignore .playwright-mcp/: its browser console logs can carry tokens.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The browser gate is one pass over the remaining flows: rail strip and
expand, coverage with fixtures, the ON/OFF pill, 360° and SINCE filters,
KEY REJECTED, nearest photo with caption, × close, phone fit, keyless
KEY REQUIRED on a second page and no page errors. Panel controls are
pressed through the DOM; pick-point search, line/cone clicks, the
framing-cancel, reload/share and status-route probes, the second
viewport and pointer-settling retries are gone (unit tests cover them).
Tests that pinned constants, copy, CSS or source text, or repeated
another test, are removed or merged. CSS loses an unused variable and
duplicate rules. Docs describe the slimmed feature; unused exports and
the layer's dead setCoverageFilter/refreshCoverage surface are removed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Drop the disk tile cache (.gev-cache/mapillary/tiles, background sweep,
size cap, legacy retrim), the server-side 401/403/429 holds, the upstream
concurrency semaphore and per-waiter abort bookkeeping. The client already
pauses on 429 and stops on a rejected key.
Keep: server-side token, {configured}-only status, strict coverage path
validation (z11-14), image-layer trimming, same-site gate and per-IP limit,
HTTPS redirect pinning to the tile origin, the streamed size cap, the error
mapping the client reads, a 24 h byte-budget LRU memory cache, in-flight
coalescing via the shared coalesceProxyRequest, and browser Cache-Control.
Routes and constants fold into mapillary.js and mapillary/tiles.js.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The layer now owns its Mapillary parts directly: the provider registry,
pick router, multi-provider UI summary, params codec helper and provider
context are gone. createStreetLevelLayer({ source }) builds coverage,
sequences, the nearest-image lookup and the MapillaryJS viewer itself; a
single providerOn switch keeps the share link's `mapillary` option (off
draws nothing), and getParams/setParams keep { mapillary, pano, sinceDays }.
No z0-5 overview points: above 60 km nothing is drawn and the panel says to
zoom in; the proxy now serves only coverage z11-14. Tiles come from the
visible ground within range of the camera, ranked from the ground under it
(no line-of-sight ranking or near-range padding). MapillaryJS loads on the
first photo; viewer prewarm is removed. Key status, a rejected key and a
rate limit share one `blocked` state on coverage.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
FOLLOW is gone: no setFollow, follow state, map-stack availability or
handoff listener. Opening a photo still frames it with one flight through
the application's deferred navigation (begin/reassert), so a slow photo
never overrides newer navigation and tracking is released as before.
Ground casting is gone: groundCast.js, meshSampler.js, the surface mode,
the draped-to-cast swap and remesh. Coverage lines are always draped
GroundPolylinePrimitives (ClassificationType.BOTH); cones and the marker
clamp to the ground and keep the horizon cull. The terrain/meshFloor/ground
services and meshFloor.visibleTilesetLoaded are no longer needed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
EXPAND now uses the browser's Fullscreen API on the viewer element, which
stays in the panel; the hand-built modal (re-parenting to <body>, inert
siblings, focus trap, document-level Esc/Tab, Clean View/recording/cockpit
hide rules) is gone. The button is hidden where the API is missing.
The FOLLOW button and the PROVIDERS chip row are removed. The header pill is
the only layer switch and re-enables a provider switched off elsewhere; under
KEY REQUIRED the error line says which key to add. Chip colour support in
chipGroup, dead CSS and tests pinning the removed behaviour go with them.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
PR #768 bundled a portable-panel windowing system with the Street Level
layer. Remove it so CCTV and Street Level are ordinary docked right-rail
panels again, as on main before the merge.
Removed:
- header drag to float, 8-way edge/corner resize, double-press snap-back,
dockOnCollapse, command-dock clearance, float hint toast, floating z floor,
persisted floating geometry and ResizeObserver re-clamping
(panelPositionControls.js restored to its pre-#768 version)
- src/ui/panelResize.js and its tests, panelDock.test.mjs
- onPanelResized/dockPanel wiring in PanelChrome, the application shell and
StreetLevelControls (EXPAND/SHRINK no longer docks)
- :not(.panel-floating) guards and .panel-floating/.panel-portable/resize CSS
- qa:panel-resize, qa-panel-resize.mjs, qa-panelDrag.mjs, their tests, the
CI "Portable panel gate" step, QA_FAIL_ON_RETRY, and the Street Level
gate's float/resize/dock steps
- floating-panel docs in README, CHANGELOG, CURRENT-STATE, KNOWN-ISSUES
Kept: the Street Level panel as a docked rail panel (share/cockpit/rail
lists, layout controller, rail and Cyber CSS, EXPAND viewer), and the
independent right-rail alignment fix (settledLeftRailTop, 9dde7b4).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Imported metadata stays inert: descriptions, native labels, properties and
KML extended data are dropped from every entity; names are kept as short
plain text. GeoJSON loads with a describe that builds no HTML, and
simplestyle (including marker-symbol) is dropped, so no icon asset is
fetched for a file and GeoJSON draws in its row's color.
- Bound geometry before loading: features and vertices are counted in the
parsed GeoJSON, the KML document and while reading GPX, and a file over
20,000 features or 250,000 vertices is refused before Cesium builds it.
- Imports run one batch at a time, so concurrent picks or drops cannot pass
the 12-file cap.
- Fly-to framing walks long tracks by index instead of spreading them into a
call.
- Fly-to is an immediate navigation through the application shell; the fly
after an import is a deferred navigation stamped when the batch starts and
re-asserted just before flying.
- Dated imports never change the shared clock: imported sources expose no
clock, entities are available for all time, and a KML gx:Track is drawn as
its line.
- GPX is read into GeoJSON with one line per track segment, without times.
- KML/KMZ rows show a hatched swatch (they keep their own styles); keyboard
focus is kept on the same control after hide/show and moves to the next
row after remove.
- Unit regressions for each, and the rendered QA now imports hostile
metadata, dated KML/GPX and a two-segment track, and fails on any request
for a file's contents or icon assets, a clock change, or surviving
metadata.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
viewer.flyTo frames ground-clamped billboards before the destination's
terrain has loaded, and their bounding sphere then sits kilometers below
the surface: flying to a points-only GeoJSON from another city left the
camera about 2.3 km underground (a black screen). Frame the features'
longitudes and latitudes at the surface instead, and let the existing
camera ground guard lift the eye once the terrain can be measured
(src/ui/flyToDataSource.js, with tests).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
DISPLAY ▸ Import (and drop onto the globe) puts the person's own files on
the map as Cesium data sources, clamped to the ground, one color per file,
with a row to show/hide, fly to or remove each. Files stay in the browser
for the session.
KML/KMZ are sanitized before Cesium reads them (network links, remote
icons/styles, models, photo/screen overlays and balloon HTML removed; KMZ
images served from blob URLs), so an imported file cannot make the page
fetch anything. KMZ is read with a small dependency-free zip reader on the
platform DecompressionStream, with a zip-bomb limit. Bounded file size,
entity count and number of files.
Tests: src/data/geoFileImport.test.mjs, src/ui/geoFileImport.test.mjs.
Gate: scripts/qa-geo-import.mjs. The import half of #890.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A tile whose terrain lookup cast some lines kept castRequested set, so a
later refresh skipped it and the lines that failed stayed draped over
trees and roofs until the tile was rebuilt. A refresh now retries any tile
with lines still draped, and redraws it only when more of its lines can be
cast, so a tile that cannot be cast fully costs a cache check, not a
rebuild, per refresh. castRequested still stops the redraw from casting
again at once.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Fixesbilawalsidhu/gods-eye-view#927. Each stdio MCP server made its own
random panel key, and panel_request accepts only the key in the panel
page. A client that starts two server processes for one connection and
reads the page from one while sending the page's requests to the other
(seen in Claude Desktop on Windows) therefore always got "Only the God's
Eye View panel may make this request".
The stdio entry now passes a key shared by the install, created once in
.gev-cache/mcp-panel-key. The file is staged 0600 and published with
link(), so processes starting together agree on one key and never read a
half-written one. If it cannot be stored, the process keeps a key of its
own, as before. createLocalMcpServer still makes a fresh key unless given
one, so in-process servers (the /mcp HTTP plugin, tests) are unchanged.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- A photo takes a deferred navigation ticket when it starts opening
(begin/reassert, the application's _beginDeferredNavigation and
_reassertNavigationHandoff) and frames only if that ticket still owns
the camera once loaded, so a slow photo no longer flies the camera back
over newer navigation or clears newer aircraft tracking.
- A failed mesh refresh keeps the last good sample and the distance it was
taken from, and is retried after the miss cooldown, instead of recording
the failed distance and never refreshing again.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- A floating portable panel saved its measured box, so dragging a
collapsed window stored its 50 px header strip and a reload clamped it
to the minimum. It now saves the inline size the user chose. A unit
test and a collapse -> drag -> reload -> expand step in the panel gate
pin it.
- Coverage awaits the tile fetch and the one-time terrain table load
together, so a tile that fails before the table loads is reported at
once instead of surfacing as an unhandled rejection.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Photo framing and FOLLOW claim the camera through the application's
navigation, injected with attachNavigation({ run, subscribeHandoff }):
aircraft and satellite tracking is released first, the cockpit refuses,
and FOLLOW stops when another feature takes the camera. No UI imports
in the layer.
- The mesh sampler follows the mesh floor's rules: it waits for the
visible tileset to finish streaming (visibleTilesetLoaded, now exposed
by the mesh floor service), needs a real bare-earth prior and keeps to
the shared meshFloorSampleWithinPrior window, which cameraFollow now
uses too. A cell is probed again once the camera is half as far from
it (down to 40 m), so a coarse early sample is replaced and redrawn.
- The MCP globe panel's CSP allows the Mapillary Graph API and its image
CDN (https://*.xx.fbcdn.net).
- The tile proxy reads bodies with the shared readResponseBytesCapped.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
CI's fixture gate timed out on the header ON/OFF pill: page.click reads a
point and then presses it, and the rail can still be resizing the panel
after coverage arrives, so the press missed. Panel clicks now wait for
the panel to hold still and check that the control is what lies under the
point before pressing. Also correct the header note on --fail-on-retry.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Trim the doc text this PR adds (README, CHANGELOG, CURRENT-STATE,
KNOWN-ISSUES, SECURITY, DATA_SOURCES, .env.example, the CI job comment),
drop code comments that restate the code, cut design-note JSDoc to a line
or two, and drop review tags from test names. Comments only; no code
changes.
Also: README key count (seven keys, two metered), CURRENT-STATE's gate
paragraph (CI runs a production build with a dummy token), the stacked
doc above meshBoxInRange and the orphaned inMeshRange line in
coverage.js, and a duplicate ALPR row in DATA_SOURCES that a merge had
brought back.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The fleet draws a grounded contact at its delayed position lifted onto the floor under it, but a new billboard was created at the delayed position without that lift. A taxiing aircraft whose reported fix sat lower than the ground under its delayed position started on the fix's floor and then rose on the next fleet tick. Creation now applies the same floor the fleet pass does.
The client lease id came from crypto.randomUUID, which browsers only
expose in secure contexts. Opened over http://<lan-ip>:4173 (the
HOST=0.0.0.0 opt-in), starting a live camera feed threw before playback.
Fall back to a v4 UUID from crypto.getRandomValues, which the server's
lease check accepts.
The military fleet also renders behind real time and projects a fresh contact's first fix back to that delayed time, but its billboard was created at the raw fix. A new contact therefore appeared ahead of where the next fleet tick put it and jumped back. The snapshot renderer now receives the layer's motion and creates the billboard at the dead-reckoned delayed position.
The fleet renders behind real time, so a newly seen aircraft is displayed at its fix projected back to that delayed time. Its billboard was created at the raw fix instead, so it appeared ahead of where the next fleet tick put it and then jumped back about one render delay of travel before moving forward.
From gekh's review of db79027, each with a test that fails before:
- P2: image cones and the photo marker no longer disappear where the
ground lies below the WGS84 ellipsoid (NYC -22 m, Colombo -95 m). The
horizon cull's occluder is WGS84 shrunk by 1 km (HORIZON_CULL_DEPTH_M),
below any real surface, so a camera near or under the ellipsoid no
longer treats every lower point as behind the horizon; the far side of
the Earth is still hidden from orbit (tested).
- Mesh range across the date line: the camera's longitude is taken to the
box's side of +-180 before clamping (meshBoxInRange), so a tile just
across the line is not measured from its far edge.
- MapillaryJS no longer tracks window resizes (trackResize: false): the
panel's ResizeObserver resizes the viewer and skips it while hidden, so
a window resize after closing a photo cannot ask for a tile at z=NaN.
- A key-less install sends no tile requests before the key status is
known: coverage waits for the first status answer and the provider
refreshes it once the status lands.
- constructCatalog takes the source method list from MAPILLARY_SOURCE_
METHODS; sequences.js imports view.js once; the provider's unused
re-exports are gone.
Verified: unit 5970 pass; strict fixture gate on a production build 28 ok
+ 2 "no Google 3D" skips and the panel gate 6/6 (QA_FAIL_ON_RETRY=1);
live gate with a real token 25 ok + 3 fixture-only skips.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Send 20 points per request, not 50: above ~20 Kartverket answers the
extra points from contour lines (hoydekurver) or not at all. At 20,
nearly every camera resolves from dtm1, so the null-retry pass is gone.
Eight requests in flight keep a cold lookup ~6 s.
- Cache misses: positions with no height are not asked again for 6 h.
- Transport failures: the first 429, 5xx or network error stops the
lookup with no retries, and no lookup starts until the backoff ends
(Retry-After when sent, else 10 min; bounded to 1 min-1 h).
- Budget the 12 s wait from the start of the loader, catalog fetch
included.
- Record answers only for the points each request asked about.
- Send the CCTV User-Agent; link CC BY 4.0 in the credit.
- Docs: bridge cameras sit at the water surface, below the deck.
- Tests: batching, 8-in-flight cap, request count on failure, the shared
deadline, Retry-After bounds, null TTL, backoff, the -20 m bound and
each water marker on its own.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
CI on fb024d5 failed step 24 with QA_FAIL_ON_RETRY=1: the press after a
double-click dock landed on the globe because the panel, showing a photo
that was still settling, moved 116 px up and lost 46 px of height between
the pre-press hit check and the press (box 1058,382 330x471 -> 1058,266
330x425, from the new diagnostics and the uploaded screenshot). That is
the panel's own content changing its layout, not the rail race fixed in
9dde7b4.
- Fail-on-retry now fails a miss only when the panel's box was the same
at the hit check and at the press (the pointer missed a header that
stood still); a miss the panel moving explains is retried with a
::warning:: naming the move. New harness test with a moving panel.
- Step 24 waits for the open photo and its sequence to finish loading
before pressing the header again.
Verified: strict fixture gate on a production build 28 ok + 2 "no Google
3D" skips and the panel gate 6/6, both with QA_FAIL_ON_RETRY=1.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- The CI gate builds the app and serves it with `vite preview`, with a
dummy non-secret client token (a fork PR never gets secrets), and
answers the Graph API, image tiles and thumbnails from fixtures: 41
photos on a line under the parked camera and one generated 640x320 JPEG
(scripts/fixtures/street-level). All 7 photo steps now run in CI, plus a
real click on a coverage line and on a cone, the framing flight stopping
on close, the inert modal, the viewer returning into the panel, and a
reload keeping a collapsed panel collapsed (share ui token t).
- Fixture interception is limited to Mapillary hosts via CDP
Fetch.enable: page-wide request interception stalled Cesium's workers,
so the old fixture mode never drew anything on the globe.
- Failure evidence (screenshots, UI state, console, failed requests,
render errors) is written to qa-artifacts/ with keys and tokens redacted
and uploaded on failure; qa-artifacts/ is gitignored.
- Cesium render-loop errors fail the gate; Google 3D branches that cannot
run print as skips, and --strict rejects any other skip.
- Panel presses log their diagnostics, warn on a retry, and CI now fails
on any retry (QA_FAIL_ON_RETRY=1) since the rail settles; a press with
no pointerdown is a failure, not a retry.
- Assertion-gating sleeps 27 -> 1; the rejected-key "no requests" check
has a positive control.
Verified locally: strict fixture gate on the production build 28 ok + 2
"no Google 3D" skips (twice, fail-on-retry on); panel gate 5/5 plus no
render errors; live gate with a real token 25 ok + 3 fixture-only skips.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A mutation review left 18 earlier survivors; each now has a test that
fails with the mutant applied:
- the expanded viewer returns into the panel on shrink, close and destroy
(the fake DOM had no parentNode, so reinsertion was never exercised); the
fake DOM gains offsetParent and a MutationObserver that honours its
options, and a <body> child added while expanded goes inert;
- each pose moves the marker, drives FOLLOW and selects a new sequence;
close() retires an open still mounting or opening; a provider toggled
while the layer is off activates nothing; the adapter drops late and
superseded poses;
- share-link defaults: "any date" (0 days), the codec max pinned to the
filter's MAX_SINCE_DAYS (layerState cannot import it across package
boundaries, so a test ties the two), Mapillary on when a link omits it;
- Esc that something else handled, or typed in a field, keeps the
selection; a key problem outranks LOADING on the pill;
- the marker's terrain height (mesh, DEM, clamp then re-place);
- proxy: LRU memory cache and its half-budget rule, Retry-After capped,
a request after a cancelled fetch starts a fresh one;
- date-line tile ranking with and without focus; the horizon cull idles
while the camera stands still; the old zoom goes as soon as the new one
lands; a rate limit holds every refresh inside its wait.
One real bug found and fixed: Esc inside a contenteditable cleared the
selected sequence (selection.js now ignores isContentEditable targets).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The right rail took its top from the left stack's bounding rect while the
left stack's `top` was still transitioning (150 ms): the left layout sets
its target and calls onAligned() synchronously, the right pass runs a
frame later mid-transition, and nothing re-ran it when the transition
ended. The rail then sat ~32 px off and jumped on the next unrelated
relayout - under the pointer in CI's panel gates, which is why a header
press missed on every run.
settledLeftRailTop() reads the end value of a running `top` transition
(falling back to dataset.safeTopPct, then the rect). A settled stack is
read from its rect as before, which keeps Cyber's CSS top override (and
cockpit's transition: none) exactly as they were. The Cyber test's
pinned pattern follows the new helper.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
From the IC8 review of #768, each with tests that fail before:
- P2: the expanded viewer is a real modal: while it is on screen every
other <body> child is inert (only the ones it set are restored, on
shrink, close and destroy, and when Clean View/recording/cockpit hide
it). The voice pill and toasts are not clickable while it is expanded.
- P2: the tile proxy follows redirects by hand, at most 3 hops, each one
HTTPS on exactly https://tiles.mapillary.com without credentials;
anything else is a 502 and the token never leaves that origin.
- P2: the real dev and preview server smoke tests request
/api/mapillary/status and a tile, and the browser gate first checks the
server's real status route before any interception, so CI fails if the
Mapillary plugin is not registered.
- P2: both gates press panel headers through one shared helper
(scripts/qa-panelDrag.mjs): re-resolve the header until it is under the
pointer, record what the press hit, wait on the real floating/docked
state, and retry only a press that provably missed the header.
Verified: unit 5910 pass; fixture gate 17/17; panel gate 5/5; live gate
with a real Mapillary token 22/22.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
From the IC8 review of #768, each with tests that fail before:
- P1: a mesh batch no longer rebuilds the selected sequence's cones. The
drawn cones are indexed by mesh cell; a batch that touches none of them
is ignored and the affected cones are moved in place (no removeAll/add).
- P2: mesh sampling keeps to its 6 ms slices. Requests are taken in inside
the budget, only cells within range of the camera are queued, the queue
is a persistent min-heap re-ranked in one pass only after a 100 m camera
move, and the deadline starts before any work. Coverage keeps a bounding
box per line and densifies a line only once it comes within range. On a
synthetic dense tile (600 x 40 points): slice median ~10 ms -> 5 ms, max
18-30 ms -> 6.7 ms, refresh cost 11-24 ms -> 1.5-3 ms.
- P2: image cones and the photo marker are horizon-culled like the
overview dots (one createHorizonCull in view.js, shared by all three).
- P2: a selection change recolours the previous zoom's lines still on
screen and the old lines a cast swap keeps showing.
- P2: distances (metresBetween), line densification and ground-corner
requests go the short way round +-180 deg (wrapLon in tileMath.js), so
nearest ordering, cone thinning, mesh range and cast lines are right
across the date line.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
From the IC8 review of #768, each with tests that fail before:
- P1: closing a photo, switching the layer off or destroying it stops the
1.6 s framing flight toward that photo. The flight is claimed with a
token before flyToBoundingSphere and released by its complete/cancel
callbacks, so cancelFraming() calls camera.cancelFlight() only while
the flight is still Street Level's: a newer navigation flight (a search
result, a scene) is never cancelled. Turning FOLLOW on also stops it, so
the tween cannot overwrite the follow view.
- P2: an older nearest-photo lookup can no longer replace a newer choice.
The core owns one AbortController per lookup, aborted by a newer lookup,
a direct open or cone click, closing the viewer, disable and destroy;
the signal reaches source.nearestImages through the provider, and an
aborted lookup never surfaces as an error.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A duplicate scan of every module the PR adds (repeated 4-line blocks,
normalised) after gekh's dedup round left these:
- glyphs.js: the cone, ring and position glyphs share cachedGlyph(), wedge(),
ring() and dot(). Checked with a recording canvas: all 20 draw calls keep
the same path and fill/stroke state, so the glyphs are pixel-identical.
- panelPositionControls.js: one placeAt() for the three places that pin a
panel's top-left corner as a fixed window.
- tiles.js: one warn() for the three background-failure logs.
- Tests: src/testSupport/streetLevelFakes.mjs holds the provider snapshot
(uiState + presentation tests), the stand-in provider (core + controls
tests) and one real-ray pinhole camera (view + coverage tests, which had
each written their own). The coverage line-of-sight test still fails when
refresh() stops passing its focus.
- qa-street-level.mjs: one waitForCoverage() for the five copies of the
"on, drawn, settled, not key-gated" wait.
No behaviour change: unit 5862 pass, fixture gate 16/16, panel gate 5/5.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Cover every Norwegian station-based city-bike system with an open GBFS
feed: Kolumbus Bysykkel (Stavanger) and Farte Bysykkel (Skien/Porsgrunn)
through Entur Mobility, and Bysykkel Lillestrøm through JCDecaux
Cyclocity.
api.entur.io and api.cyclocity.fr join the GBFS allowlist, each kept to
its GBFS path prefix since both hosts front other APIs. Entur serves its
feeds without a .json suffix, so the endpoint checks accept either form,
and requests to Entur carry the ET-Client-Name header it requires.
Attributed under NLOD (Entur) and Licence Ouverte (JCDecaux).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Add Norway's three Urban Sharing city-bike systems to the Bike Share
layer and get_bike_share. The feeds are keyless GBFS on
gbfs.urbansharing.com, which joins the proxy allowlist. The proxy now
sends the Client-Identifier header the operators ask clients to send.
Attributed under NLOD 2.0 in DATA_SOURCES.md and the data credits.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The Vegvesen camera feed is 2D, so every camera shared one flat 150 m
ground prior and mountain-pass cameras could sit hundreds of metres below
the terrain where neither the point-height prior nor the ground snap
resolves. Each camera now gets its terrain height from Kartverket's
keyless point-height API (CC BY 4.0): ~840 of ~845 cameras.
- 50 points per request, six in flight; points a full batch answers null
for are retried once in batches of 10 (the API drops points from large
batches).
- Sea-floor depths (dybdekurver / Havflate) under bridges and quays
count as the water surface, 0 m.
- Heights are cached by position, so the 15-minute refresh only asks
about new or moved cameras. A catalog load waits at most 12 s; a slower
lookup keeps filling the cache for the next refresh.
- CCTV_VEGVESEN_HEIGHTS=0 turns it off; Kartverket is credited beside
Statens vegvesen in the attribution popover.
Follow-up to #929.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
From gekh's review of #768, each with a test that fails before:
- The routes refuse cross-site requests (sameSiteGated, as the paid
proxies do) and the tile route has a generous per-IP limit (600/min), so
another page cannot burn the token's quota or trip the shared 429 hold.
- At most six upstream fetches run at once; a queued request that is
abandoned never fetches, nor does one abandoned during the disk read.
- A body past the 48 MB cap aborts the upstream request as it streams.
- Empty tiles are charged against the memory and disk budgets.
- Each disk write uses its own temp file and a retrim is skipped while a
write of that tile is pending, so concurrent hits cannot extend a tile's
life or fail a rename.
- Upstream refusals other than 401/403/429 answer 502.
- The tile route's success path is tested end to end (200 with the image
layer stripped and the right headers, 204 when empty, an abandoned
response aborts upstream); the teardown race that made this file flaky
is gone (240 parallel runs, no failure); the QA-constant tautologies are
replaced by a check against the real provider registry; the boundary
group lists the modules the provider actually imports.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
From gekh's review of #768, each bug with a test that fails before:
- P2: a failed sequence load clears the previous sequence's cones, resets
the selection and can be retried; a second click while loading starts no
second lookup. sequences.js had no tests: sequences.test.mjs now covers
failure and retry, a superseded late answer, the cache and clearSelection.
- P2: closing a photo stops MapillaryJS sequence playback, which kept the
hidden viewer stepping and downloading images.
- openNearest without the panel reports it instead of loading forever, and
stops if the layer (or its provider) went off during the lookup; the
mesh sampler's misses are bounded and forgotten with its samples; the
sequence count is the union of ids across tiles; a provider withdraws
only its own error, so a sequence error cannot mask KEY REJECTED; hover
stays on (throttled) under an orbiting camera and re-picks when it
stops; "since N days" follows the clock in a long-open tab.
- A prewarm waits for one in flight and then builds if it gave up.
- New coverage for the layer core (credits, KEY REJECTED vs REQUIRED,
switching a provider off), the refresh wiring of the tilted-view fix and
the draped-to-cast swap completion.
- Cleanup: loading is derived from pending; freshStreet(); tile ranking
computes distances once and shares one path for the date line; shared
metresBetween / whenIdle / redrawTile; one NEAREST_RADIUS_M and one
source method list; unused contract fields, exports, getImage and the
decoder's image branch removed; PROVIDER_COLORS lists only Mapillary.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
From gekh's review of #768, each with a test that fails before:
- P2: the panel no longer reopens on every page load. The shell reports
who asked for Street Level to switch on; only a user, voice or tool
request (isExplicitLayerStateOrigin) or an opening photo opens it, and
those automatic opens are never stored. Without the hook, a stored or
shared "collapsed" choice wins during the restore window.
- P2: the expanded photo hides in Clean View, recording and the cockpit,
and holds neither Esc nor Tab while hidden.
- Esc/Tab are taken only from the viewer, body or globe, not app-wide;
FIT/FILL show the right choice while expanded; a 0x0 viewer is never
resized (no z=NaN tile request); the expanded viewer keeps the panel's
CSS tokens; KEY REQUIRED hides the raw "no_key" code.
- Share links carry the panel's collapsed state (ui token t); the Cyber
theme frames the panel like its rail peers (the pinned Cyber selector
tests now list it).
- Accessibility: radiogroups move with arrow keys and are one tab stop,
their focus ring is no longer clipped, and the provider chips sit
outside the disabled fieldset so the "how to add the key" hint stays
reachable (a gated chip explains instead of toggling).
- Renders write only changed attributes (the rail's MutationObserver
relaid out on every write); one coalesced resize per open; the legacy
render-mode key migration is gone.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
From gekh's review of #768, each with a test that fails before:
- P2: a floating panel whose content grows (CCTV expanded, a photo opened)
is pulled back on-screen: every portable panel is observed and re-clamped,
not only on window resize.
- P2: a floating header can never land under #command-dock or the voice
pill (the rail's stacking context paints below them): clamping keeps the
header clear of their rects on drag, restore and every re-clamp.
- A live window resize shrinks a floating window wider or taller than the
viewport; Cyber's accordion no longer collapses docked panels when a
floating one opens.
- The three pointer gestures share one helper (threshold, mouse/pen only,
cancel reverts), and the header-drag threshold now has a test.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The diagnostics from d9b13c7 named the CI failure: the press landed on the
globe canvas, not the header, because the CCTV panel had moved 32 px down
between the hit check and the press (docked at 234, 266 at the press) -
something above it in the rail grows on the runner after load. A press that
provably missed the header (the page records the pointerdown target) says
nothing about dragging, so it is retried up to twice, logging what sat
above CCTV at dock and at the retry; a press that landed in the header and
did not lift still fails at once.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
From the IC8 review of #768 (P2s), each with a test that fails before:
- A tile read from disk keeps its real age in memory, and the trim rewrite
keeps the file's mtime, so nothing is served past the 24 h TTL.
- The disk cache is swept in the background at most every 15 min: expired
tiles go, then the oldest past 1 GiB. Coverage zooms the app never
requests (z6-10) are refused with a 400, and layer names are matched as
own keys only.
- The tests run against a temporary cache directory (a test seam) instead
of writing into and deleting from the developer's real .gev-cache.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
From the IC8 review of #768 (P2s), each with a test that fails before:
- Switching the layer off and on during the first photo download no longer
leaves the viewer stuck on "not mounted": an outdated mount only unmounts
an adapter nobody else holds and subscribes poses only once current, and
the MapillaryJS adapter never builds a viewer after it was unmounted.
- Hover picks at most every 120 ms (the CCTV convention), not while the
camera moves or a button is held, and not after the layer went off.
- Esc shrinks the expanded viewer wherever focus is (document capture), so
the map keeps its selection, and Tab from outside returns into it.
Closing a photo moves focus to the panel instead of dropping it on body.
- Portable panels lift only after a mouse or pen moves past the drag
threshold: a click on a docked panel's edge or a touch swipe on its
header no longer turns it into a floating window, and pointercancel
saves nothing and puts the panel back.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
From the IC8 review of #768 (P2s), each with a test that fails before:
- Far-side overview dots no longer show through the globe. They skip the
depth test so they stay above Google 3D terrain, so points behind the
horizon are now hidden with an EllipsoidalOccluder (as the cyclones layer
does), re-culled only when the camera moves.
- The per-tile cap applies after the imagery filter: a dense tile of newer
flat captures kept no 360° lines at all. Lookup, picking and counts use
the drawn set.
- Lines whose ground heights were evicted, or whose terrain lookup failed,
are cast again on the next refresh instead of staying draped for good
(castRequested no longer latches; castAbort marks a cast in flight). The
height cache now drops its oldest corners instead of all of them.
- A selection made or cleared while a tile's lines rebuild is applied once
they are ready (each primitive remembers the selection it was built with).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The 92 KB stylesheet was vendored into the main CSS, so every page load
paid for it (render-blocking) although the viewer itself is lazy. It now
comes with the lazy chunk: `import('mapillary-js/dist/mapillary.css')`
next to `import('mapillary-js')`, which Vite applies before the import
resolves, so the viewer is styled from its first frame. Main CSS in a
production build: 386 KB -> 302 KB; the MapillaryJS CSS is its own 84 KB
file. Checked in the dev server that its rules arrive with the viewer.
The vendored copy also named the wrong copyright holder and omitted the
MIT text; it is gone, and THIRD_PARTY_NOTICES.md now records mapillary-js
(MIT, Facebook, Inc. and its affiliates) and its rxjs (Apache-2.0) and
three (MIT) dependencies.
Found in the IC8 review of #768 (P2).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
On the CI runner the header drag stopped lifting CCTV (it did not float),
right after a run where its box never held still: the rail is shifting
there, so the press can land beside the header. It does not reproduce
locally (both gates pass on a fresh key-less dev server). The gate now
waits, bounded at 10 s, until the title point is on the CCTV header before
pressing, and a failed lift reports the element pressed, whether it was in
the header, and where the panel was when docked and at the press.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
On the CI runner the CCTV box never held pixel-identical for ten animation
frames within 15 s, so settle() timed out and failed the gate although the
street-level gate before it passed. The wait now rounds the box to whole
pixels, polls every 100 ms for half a second of stillness, and only logs if
the rail keeps moving: the lift is checked against the box recorded at
pointerdown, so settling is only there to make that box the docked one.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The fixture gate always reported a configured key, so the KEY REQUIRED path
most installs see never ran in CI, nothing checked that the lazily imported
MapillaryJS viewer loads, and the FOLLOW step (which needs an open photo)
failed for anyone with a Google key. Fixture runs now:
- check the viewer is stood up in the panel ahead of the first photo;
- serve a rejected key and assert KEY REJECTED, no further tile requests on
a pan, and no stale message once the layer is off;
- open a second page whose server has no key and assert KEY REQUIRED;
- treat FOLLOW as a photo step.
Passes 16/16 locally against a dev server with Google 3D available.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Tilted views: the visible box came from screen rays against the bare
ellipsoid and the 9 tiles kept were the ones nearest its centre, so a view
over high ground (Denver, 1,600 m up) or near the horizon dropped the
streets in front of the camera. At street zooms the rays now meet an
ellipsoid raised to the ground under the camera, hits beyond a range tied
to camera height are ignored, tilted views also keep the ground around the
camera, and tiles are ranked along the line of sight (nadir to screen
centre). Simulated with real Cesium ray maths over Sacramento and Denver
from 2 m to 1,000 m: the nearest visible ground and the screen centre are
always loaded.
Rejected token: Mapillary's 401/403 reached the panel as a raw "Mapillary
tiles HTTP 403" while the pill said ON, and every pan asked Mapillary again.
The proxy now answers a refusal as {keyRejected: true} and holds it for that
token (5 min), and a 429 with its Retry-After (held until then). The panel
reads KEY REJECTED, names MAPILLARY_CLIENT_TOKEN and Provider Settings,
gates its controls like KEY REQUIRED, requests nothing more, and clears the
message when the layer goes off. A rate limit keeps the drawn tiles and
refreshes once the wait is over.
Found in the IC8 review of #768 (P1 tile selection, P2 rejected token).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
castLine densifies at 0.0002° when mesh heights refine it, but prepareLines
only fetched grid corners for the 0.0005° points. The finer points could
land in cells nobody fetched, so the cast came back null and the whole line
stayed draped over roofs and trees on Google 3D. prepareLines now prepares
both sets (corners are deduplicated, so the extra cost is small).
A seeded test of 300 walks with 50-300 m segments: 130 failed before, 0 now.
Found in the IC8 review of #768 (P1).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Fixes from gekh's re-review of 061d269, each with a regression test that
fails on the old code:
- A pan that drops a tile still loading now settles the loading count
(the aborted request no longer owns its key, so its `finally` never
did), and purges the previous zoom's tiles once nothing is pending.
LOADING no longer sticks on the status pill and chip.
- Zoom 0 (above 15,000 km) is a zoom: `zoom == null` instead of `!zoom`.
- A pose that arrives after a photo was closed is dropped, in the
MapillaryJS adapter and in the viewer host, so the marker and sequence
selection do not come back.
- Ground casting passes the tile's signal to the terrain request, so a
cancelled tile stops its height batch instead of holding the queue.
- The server's in-memory tile cache honours the 24 h TTL like the disk.
- The viewer's one-time resize runs for every photo, not just the first.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Both disk caches fed by OVERPASS_UPSTREAMS key only on the query — the Overpass
proxy on the sanitized request body, the installations proxy on the bbox — so an
entry written against one instance is a hit for the same query against another.
After an operator repoints OVERPASS_UPSTREAMS, the previous upstream's answers
keep being served for 7 days, 30 for boundary-class queries, and indefinitely
through the serve-stale path. An install that predates #742 has a cache written
from the public instances that were the default then, and configuring your own
instance does not displace it.
Cached answers now carry a tag identifying the configured upstream set, and a
disk read refuses an entry that does not match it, on the stale path too. With
nothing configured any entry is still accepted: there is no instance to
contradict, and serving last-good data at any age is what the not-configured
path exists for. The tag is a hash of the resolved endpoint list, never the
URLs; it is written to disk and compared on read, and is not sent to clients or
surfaced in errors.
One line is logged once per process when a foreign entry is ignored, naming the
variable and what will happen, as local-receivers.js already does for a
rejected feed.
Cost worth naming: on upgrade every existing entry is untagged, so the first
outage after upgrading serves nothing where it previously served stale data,
until each query is fetched once from the configured instance.
`createVectorTileSource` took `tileJsonUrl` and `allowedOrigin` as independent
options, and `createOpenFreeMapSource` hardcoded both to the default host. A
caller that repointed only `tileJsonUrl` — the obvious single change — kept the
default `allowedOrigin`, so the TileJSON's absolute upstream `tiles[0]` passed
the origin check and every tile was fetched from the default host instead. No
error, no warning, and nothing reached the configured mirror.
`allowedOrigin` now defaults to the origin of `tileJsonUrl`, so repointing one
option is enough, and a mirror that serves the default host's TileJSON verbatim
fails with the existing `Invalid vector tile origin` instead of silently
bypassing itself. The origin check is unchanged: a TileJSON still cannot point
the client at a host the caller did not configure.
All three existing callers pass `allowedOrigin` explicitly
(`openFreeMap.js`, `alpr/source.js`, `traffic/flowSource.js`), and an explicit
value still overrides the derived one, so behaviour is unchanged for every
current call site.
Address review feedback on #903: the DATA_SOURCES terms link now points
at the actual terms page instead of a sign-in page, key-creation links
go straight to my.tomtom.com/keys, and the README table row is realigned.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
- Lower the Vegvesen default cap to 900, still above the ~850 working
cameras, so the per-pack defaults stay closer to the catalog default.
- Name the flat ground-elevation prior as a known limitation: the feed
is 2D, and mountain-pass cameras can sit below terrain where neither
the point-height prior nor the ground snap resolves.
- README: ~3,900 public cameras.
- Use one HLS figure (~135 working) in CHANGELOG and DATA_SOURCES.
- DATA_SOURCES: list the frame and HLS hosts alongside the catalog host.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The panel gate read the docked box after a fixed 600 ms sleep and expected
the lifted window 100 px above it. On the Linux CI runner the rail was still
moving and the panel sat 8 px lower when the drag began, so the gate failed
("lifted top: expected 156.5, got 164.8") although the window followed the
pointer exactly. Wait for the box to hold still for ten frames, record it at
pointerdown, and compare the lifted window against that.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
preserve Bilawal's merge history, OpenAI schema-combinator startup fix, and Contacts viewport note
keep the validated voice and analyst behavior while adopting the current provider-neutral feed routes and Google token fallback
The Street Level and panel gates never ran in CI, and the live gate needs a
Mapillary token, the Mapillary network and real coverage at its test site,
so everything users see was guarded only by manual runs (which a bad token
and expired tile caches blocked twice this week).
`qa-street-level.mjs --fixtures` (npm run qa:street-level:fixtures) answers
the page's /api/mapillary/status and coverage-tile requests from generated
tiles (a street grid with flat and 360 deg sequences, half captured three
years ago; overview points from orbit) and blocks mapillary.com. Only the
six steps that open a photo are skipped. Because the data is known, the 360
deg and SINCE steps also assert that the filters remove sequences, not just
that counts do not grow. A unit test pins the fixture contents.
New CI job "Street Level browser gate": install with headless Chrome, start
a key-less dev server, run the fixture gate and qa:panel-resize. Locally,
against a dev server with every key blanked: 14/14 on three consecutive
runs, panel gate all pass.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
From the test-engineering review of #768: the rendering and panel code had
thin unit coverage (coverage.js 29% of lines, sequences/viewer/selection
~30%, cameraFollow 50%, streetLevelControls only source-text checks), and
two shipped bugs were caught only by the manual browser gate.
- coverage.test.mjs drives the real createCoverage against a fake scene and
a source whose tile requests the test resolves. It reproduces gekh's P0 #2:
a request superseded by retire() finishes after a newer one for the same
tile. Without the ownership checks it drops the newer request's key and a
refresh starts a duplicate whose lines nothing can remove (fails on the old
loadTile, passes now). It also covers the draped-to-cast swap keeping both
line sets on the globe until the cast one is built and clear() removing
both mid-swap, and a tile with nothing castable not being rebuilt.
- coverageFixture.mjs encodes Mapillary-shaped coverage tiles (sequence
lines, overview points) from lon/lat; the tests and the hermetic gate use it.
- cameraFollow.test.mjs pins the underground-camera fix: a -14,886 m sample
is ignored for bare earth, a plausible sample wins, and following stands at
eye height. Three of the four fail on the pre-fix module (the first reports
"centre at -14884 m").
- streetLevelControls.test.mjs is a contract test through the real
DataLayerManager and LayerStateCoordinator: a 360 deg click and a SINCE
release reach the durable layer state and the encoded share link under
Street Level's token. Against a stand-in layer it also covers the chip rule
(darkening the last lit chip turns the layer off, provider kept on; lighting
a dark chip is a user params request, then enable), the legend rebuilding
on a same-count swap (review #9) and the viewer resizing only on open
(review #8). With the old direct-to-layer routing, three of these fail,
including both share-link tests.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Norway's ~850 working road cameras load from one keyless GeoJSON request
to the agency's OGC API view of its DATEX 3.1 CCTV table, so no DATEX
account is needed. Faulty cameras are dropped, every camera sits under a
"Norway" category, and the ~135 that publish HLS play as live video
through the existing in-memory relay with the still as fallback. Frames
and manifests are pinned to each camera's own path on the agency hosts.
CCTV_VEGVESEN_ENABLED=0 disables the pack, CCTV_VEGVESEN_MAX_SOURCES
caps it (nearest the largest cities first) and CCTV_VEGVESEN_VIDEO=0
keeps stills only. Attributed under NLOD 2.0.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Without a browser key, the app asks its server for a short-lived Google
access token (GET /api/google/tiles-token, answering accessToken and
expiresAt) and loads Photorealistic 3D Tiles directly with it as a
header. A tile refused for an expired token gets a renewed one and is
retried once. A server that does not offer tokens changes nothing: the
app continues through ion or the keyless globe as before.
Keeps the voice-core prompt rules and adds main's world-query and
vessels-vs-visuals rules; the tool catalog wraps the deixis-aware runner.
Also fixes two things the branch had before the merge:
- Live voice could not start: OpenAI rejects track_entity's top-level
anyOf. Model-facing tools now omit top-level combinators; the runner
and benchmark still require a query or a referent.
- The Contacts-centred analyst answer dropped the viewport coverage note.
The fixture answers /api/vessels without X-Feed-Source, so the source
fell back to the feed's name and the attribution check failed. Send
AISStream as the bundled server does.
The flight, military and vessel feeds were named after the providers the
bundled server uses (/api/opensky, /api/adsblol/mil, /api/ais-live and
their track routes; createOpenSkySource, createAdsbLolSource,
createAisStreamSource). Any server should be able to answer a feed from
its own provider, so the feeds are now:
/api/flights /api/flights/track?icao24=
/api/military /api/military/track?hex=
/api/vessels /api/vessels/track?mmsi=
with createFlightSource, createMilitarySource and createVesselSource. The
old routes are removed. Each server names its provider in X-Feed-Source
(the bundled server sends OpenSky Network, adsb.lol and AISStream, so its
attribution is unchanged) and the military feed's cache state and age in
X-Feed-Cache and X-Feed-Age-Ms. Sources report the provider the server
names and fall back to the feed's name.
b4067f9 moved the OpenSky states/all fetch into fetchOpenSkyStates, with a
10-second limit per attempt and one retry. Keep that and drop this branch's
20-second exchange deadline; the 8 MiB body cap now reads inside the helper
under each attempt's signal. The CHANGELOG bullet moves under [Unreleased],
and CURRENT-STATE describes the per-attempt OpenSky deadline.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The vessel source accepts an optional area and sends it as lat, lon and
radius_km. The vessel layer asks for the area the camera shows (centered
on the middle of the screen, sized by camera height), or every vessel
from high enough up, and asks again when the view moves or zooms by a
quarter of that radius. vessels_in_area names its area too. Servers that
hold every vessel may ignore the area; callers still keep only what is
inside it.
MCP servers that compose Core's tools can now serve the globe panel:
@gods-eye-view/core/tools/panel exports createGlobePanelResource,
panelRuntime, GLOBE_PANEL_URI and PANEL_BASE. The local MCP server uses
the same entry point.
The submarine cable source and the infrastructure data URLs built their
asset URLs from import.meta.url while the module loaded. Importing the
tool services therefore failed in runtimes without a usable module URL,
even for tools that never read those files. Build the URLs when they are
read; Vite still rewrites them in browser builds.
Brings in 123 commits from main, including the v0.2.0 and v0.2.1 releases
(MCP server and tool catalog, server security hardening, build hygiene).
Conflicts:
- package.json: kept main's script order and added this PR's
qa:panel-resize and qa:street-level; exports keep both sides (main's
tools/view entries and this PR's street-level and Mapillary entries).
- src/tooling/viteBuild.test.mjs: kept this PR's Mapillary-token define
assertion and main's new wildcard-host allowedHosts assertions.
- CHANGELOG.md: main now has release sections; this PR's entries go under
[Unreleased].
Checks after the merge: doctor, format:check, check:boundaries,
layer-token:check (28 published, 1 new: street-level 0), npm test (5726
pass), build, the Windows job's focused tests (70 pass), qa:panel-resize,
and the keyless street-level gate. The new CSP allows Mapillary (https:
connect/img, blob workers). The keyed gate stops at coverage locally
because the dev .env Mapillary token is invalid (upstream 403), not because
of the merge.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The Cockpit utility gate still expected five modes and an inherited NOIR
entry. Cycle all seven modes, check each drives only its own shader and that
Normal clears them all, enter on a Noir map preset, and restore it through
Exit Cockpit and Reset.
Co-authored-by: Sameh Khamis <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Resolve the CHANGELOG and CURRENT-STATE conflicts: the PR's entries go under
Unreleased, after the 0.2.0 release.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
loadBundledJson's callers passed a literal JSON import as a Node fallback,
which made the production build emit a JavaScript copy of every pack beside
the JSON the browser fetches. The loader now reads file: URLs itself under
Node (through process.getBuiltinModule, synchronously, as promptly as the
cached import it replaces), and the callers pass only the URL. On a Node too
old to provide getBuiltinModule it says that Node 24.14 or newer is needed.
dist/ drops from 54 MB to 42 MB. A test keeps app code from importing a pack
as a module.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The Content-Security-Policy blocked the YouTube player API, the Facebook
video SDK and X's post widget that src/data/bhoteKoshiEmbeddedMedia.js
loads, along with their players and posts in frames. script-src now allows
exactly those three script origins and a new frame-src allows YouTube,
youtube-nocookie, Facebook and X embeds. A test ties the policy to the
frames the media module builds and pins the allowed script origins.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A limit such as 0.5 floored to 0 and disabled the throttle, although only an
explicit 0 is meant to. Positive values below 1 now count as 1.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
/api/google/text-search spends the same Google quota as nearby-places but
was not behind the cross-site gate. It now refuses cross-site requests
before calling Google, and a test covers both paid Google routes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Vite installs its Host check after the middleware plugins add in
configureServer, so the provider and /api routes answered any Host,
including a DNS-rebinding name. hostCheckPlugin runs first on the dev and
preview servers and applies Vite's rule with the same allowed hosts: IP
addresses, localhost and *.localhost, the configured server host, and the
listed names. A test drives a real Vite server with an unlisted Host.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The server routes a mounted path when a dot follows it, so /mcp.json reached
the MCP endpoint while panel_request refused only /mcp and /mcp/...; the same
held for the provider-settings, credential and model routes. The refusals now
match a dot after the route as well. A regression test drives panel_request
against a real Vite server.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
localRequestGate.mjs owns the reverse-proxy header list and hasProxySignals;
Provider Settings, the cross-site gate and /mcp all use it instead of two
copies of the same nine names. The changelog credits the cross-site gate.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
The voice debug log writes the server's timestamp after the client's record,
so a record can no longer replace loggedAt. The dev and preview servers also
send X-Content-Type-Options: nosniff with their shared browser headers.
From the findings in #710.
Co-authored-by: Sunil <[email protected]>
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Binding HOST=0.0.0.0 no longer sets Vite's allowedHosts to true. The dev and
preview servers accept IP addresses, localhost and the LAN hostnames listed
in GEV_ALLOWED_HOSTS; suffix and wildcard entries are ignored. The
configuration factory takes the resolved list as an input, so build/vite.js
still reads no environment.
Ported from #97 to the current build/vite.js layout. Fixes#21.
Co-authored-by: Sameh Khamis <[email protected]>
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
The OpenAI endpoints (/api/realtime/token, /api/openai/hud-summary) now
allow 30 requests per minute per client IP and the Google Places endpoints
120, the caps the Pinokio build already ships. GEV_RATELIMIT_OPENAI_PER_MIN
and GEV_RATELIMIT_GOOGLE_PER_MIN still override them and exactly 0 disables
them; an unreadable value falls back to the default rather than to
unlimited.
Salvaged from #683 onto current main.
Co-authored-by: Sameh Khamis <[email protected]>
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
The cost-bearing endpoints (/api/realtime/token, /api/openai/hud-summary,
/api/google/nearby-places) and the voice debug log refuse cross-site browser
requests: a foreign or opaque Origin, a Sec-Fetch-Site other than
same-origin/none, or proxy forwarding headers. Loopback tools without an
Origin and the LAN opt-in keep working. The dev and preview servers send one
shared Content-Security-Policy, and embed-mode framing rewrites only the
frame-ancestors directive of it.
Salvaged from #242 onto current main.
Co-authored-by: Sameh Khamis <[email protected]>
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
SECURITY.md gains a section on what /mcp accepts, the panel's request key,
the browser keys the panel build carries and embed-mode framing. TOOLS.md
now describes what panel_request may request, and the changelog records
the recent MCP and embed changes.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Panels on one MCP server share six requests in flight; further requests
wait their turn (up to 256 waiting) instead of failing, and a waiting
request leaves the queue when its caller cancels. Held responses belong to
their own MCP server rather than to the process. /mcp request bodies must
arrive within 30 seconds. Over stdio, a client's notifications/cancelled
aborts the named request, which then gets no response.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
The /mcp route now applies the same proxy and sharing checks as Provider
Settings: requests carrying forwarding headers, or arriving while launcher
sharing is on, are refused. The Host must name the port the request reached
and a browser Origin must match that host exactly. The proxy and sharing
checks move into shared helpers in keySetupCore; Provider Settings behaves
as before.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Embed-mode documents now keep the server's framing protection unless
GEV_EMBED_FRAME_ANCESTORS names the pages that may frame them (or * for any
page). The MCP Apps panel loads the app into its own page and needs no
framing. View answers are posted to the origin that sent the view rather
than to any origin.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
panel_request now requires a key that each MCP server generates and places
in its panel page, which keeps the tool from clients that list it to the
model without loading the panel. The key is not access control: any MCP
client can read the panel page. Routes the panel never loads (provider
settings, credential and model endpoints, the MCP endpoint and development
server internals) are refused to every caller, regardless of letter case or
encoding.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Steps people and coding agents can follow to run the app, add the MCP
server to Claude Code, Claude Desktop, the Codex CLI and the Codex and
ChatGPT desktop apps, and check it works, linked from the README and the
tools guide.
Asked for an aircraft's cockpit view, assistants passed only the follow,
which failed for want of an area or camera, so every such request took
a second call. show_in_gods_eye_view now looks the aircraft up and frames
the view on it, and says so when the aircraft is not reported.
Cesium's script sets CESIUM_WORKERS itself when it loads, so the panel's
combined workers script was never used, and neither was the embedded
terrain-heights file: the worker still requested it from the panel
page's site. The panel build now writes only the prelude, and the panel
puts it ahead of whatever Cesium assigns to CESIUM_WORKERS. The separate
esbuild bundle of Cesium's workers is gone.
The ground-polyline worker loads approximateTerrainHeights.json itself,
and a worker's requests go to the panel page's own site, where they
failed with 404 and stopped rendering once ground-clamped lines such as
military installations appeared. The panel build now embeds that file in
the workers script, which answers the worker's request from memory.
A view's follow may ask for cockpit view, which the app enters after it
follows the aircraft; links cannot carry it and open the app following.
Applying a view no longer flies the camera when the followed entity is
already there, since the flight ended the follow; it flies only while
waiting for the entity. The panel applies its first view again only
when it asks for cockpit view.
The panel starts the app from the first view's link, which the app
restores, following included. Sending the same view again once the app
was ready flew the camera to it, which ended the follow and left cockpit
view. The panel now sends only views that arrive while the app loads.
panel_request resolved paths with a check for a leading //, but URL
parsing also reads /\host as another host, so it could request other
servers. It now resolves the path against the app's address, refuses
any other origin, matches refused routes decoded as well, and does not
follow redirects. Responses are capped at 64 MiB while reading, with a
deadline; held responses make room on arrival and expire by time only,
so a response being read keeps its parts.
The panel keeps converted images only for its build files, so live
camera frames no longer accumulate, and failed build loads are retried.
The dev server answers a malformed /panel/ path with 400 instead of
crashing.
Drop the render restarts added while chasing the black overlays; keep
the full render-error report and stop the timer after an error, as
Cesium does. Correct comments the investigation outran, share the
panel_request name, and document the host differences the panel works
around.
In Claude, the app's 2D overlay canvases covered the globe in black
while the host treated the panel as off screen, and the globe showed
once they were hidden. The panel now asks for 2D canvases backed by
memory rather than the GPU.
Where the host offers fullscreen, an Expand button asks for it; the page
fills the room it is given. Images in markup the app inserts as HTML
text now wait for their files instead of requesting the panel host's
site, which ended the mic.svg and Cesium credit 404s.
Cesium stops drawing after a render error and prints worker failures as
[object Object]. An inline panel now logs the error's details and starts
drawing again, up to three times.
Claude reports a panel on screen as hidden, which stopped the app's
render loop and the browser's animation frames, so no map was drawn. An
inline panel no longer suspends rendering when hidden, and draws from a
timer while animation frames stop arriving.
Images and stylesheet files load as data: URLs, which hosts allow where
some refuse blob: images; the scene-asset source gets an https app
address the panel serves, since Codex pages use their own scheme; and a
panel whose 3D context the browser takes back says so.
Hosts serve panels from their own sites and may refuse other addresses;
Codex refuses any address on the user's machine. The panel now never
requests the app's server: it loads the app's panel build and data
through panel_request, a tool only the panel may call, which requests
the path from the app's server. One path in every host; only map
providers load directly.
npm run build:panel writes the panel build to dist/panel, served at
/panel/: one app script, one stylesheet, Cesium's script, and Cesium's
workers as one script Cesium runs from memory. Tools may now be marked
app-only. The panel CORS rules are gone.
Codex serves panels from codex-sandbox:// pages, where origins are
opaque: the panel recognizes the app's own addresses by scheme and host,
the vessels feed resolves against the document address, and the dev
server answers Codex's sandbox origins with CORS.
The app treats those images as its own site's and loads them without
CORS, so once they come from the app's server WebGL refuses them and
Cesium stops rendering.
Claude refuses <base> in panels, so relative addresses reached the
host's site: styles, Cesium assets and API calls all failed. The panel
now points fetch, XHR, beacons, images, media, links, workers and added
style url()s at the app's server, and declares its policy under OpenAI's
widget key as well.
A panel page loads the app with <base> pointing at the app's server, so
addresses the app builds resolve against the document base instead of
the page location, and hash updates stay on the page's own address.
The dev server answers panel origins with CORS on the API routes too.
Hosts such as Claude do not let a panel frame other sites, so the panel
now loads the app's page, styles and scripts into itself in inline embed
mode, and declares the app and map providers as resource and connect
domains. The dev server answers panel origins with CORS.
When a host's content security policy refuses part of the panel, such as
its frame, the panel now names the blocked directive and address instead of
waiting until its load timeout.
The panel sent clientInfo and capabilities in ui/initialize, where the MCP
Apps schema requires appInfo, so hosts such as Claude rejected the handshake
and never showed the panel. It now sends exactly appCapabilities, appInfo and
protocolVersion, sends initialized without parameters, says so when a host
rejects it, and offers the link when God's Eye View does not load within 20
seconds. Checked against the MCP Apps SDK's own host bridge.
The stdio server writes each request's method, and the tool or resource it
names, to stderr, which clients such as Claude Desktop copy into their logs.
Arguments and results are never logged.
show_on_globe replaces open_in_gods_eye_view and names an MCP Apps view, a
ui:// panel that loads the app in embed mode and posts each later view to it,
so the globe in the conversation changes without reloading. It also takes a
view another answer returned. Tools can declare a UI resource, and the MCP
adapter serves resources/list and resources/read and lists each tool's
resource in _meta.ui.resourceUri.
?embed=1 shows the globe alone: clean view with the HUD, panels, welcome and
setup prompts hidden, and provider attribution kept. The page that frames it
sends views with postMessage; the app applies each through its own actions
and reports every step. Annotations in any link are drawn once it restores.
Only embed-mode documents may be framed; every other page keeps
X-Frame-Options DENY and frame-ancestors 'none', and
GEV_EMBED_FRAME_ANCESTORS can restrict who frames them.
A view can carry the marks the app's annotate_map action draws, and links
carry them in the an parameter, bounded in count and size. A tilted view of
an area now places the camera behind its center so the area stays in the
middle of the frame.
MCP no longer lists place search, routing, plain weather and wind, the
regional brief, the HUD caption, radio, bike share and transit. Assistants
already cover these or they add little without the globe; voice keeps them,
and tools that combine others still reach them.
Tool answers with something to show now include data.view: the view with
the matching layers on, the area framed from above or a single aircraft or
satellite followed, and the link that opens it. Area framing moves to
src/tools/views.js so links and answers frame alike.
A view is a camera, data layers, visual style, map imagery and an aircraft,
military aircraft or satellite to follow, written in the share-link format
the app already restores. gods-eye-view/view builds, encodes and decodes one,
and share links take their style names from it. open_in_gods_eye_view now
builds its link from a full view: an area or camera, layers, style, map and
something to follow.
Voice hears audio directly, and "vessels" is easily heard as "visuals",
which the instructions route to visual styles, so asking for vessels turned
on other layers or a CRT style. The instructions now say a vessels request
without a style name means the ships layer, and a style needs its name.
The voice instructions recommended aircraft_in_area, vessels_in_area and
get_earthquakes, which voice no longer offers, and the docs said voice gets
nearly every query. The instructions now name tools voice has and send
aircraft, ship and hazard questions to analyst_query; a test keeps the
instructions to tools the voice session lists.
find_aircraft merged the records of the feeds that answered and dropped their
freshness, so a stale OpenSky snapshot read as a current "not reported".
Answers now list each feed with its freshness and coverage and say which
feed's data may be stale.
Vessel tools read the source's default of the newest 12,000 vessels while
the server keeps up to 50,000, so a lookup could miss a vessel still held.
They now ask for more than any server keeps and receive everything it has.
An empty camera answer outside the catalog's regions read as "0 public
cameras", as if the area had none. It now says the catalog has no cameras
there.
Coverage was checked only for packs among the cameras found, so an area
whose cameras were all trimmed away answered zero cameras, complete. The
catalog now reports where each trimmed pack's cameras are, and the tool
marks an answer partial whenever its area overlaps that region.
The map window was moved inside the product's bounds without checking that
it still covered the requested area, so an area across the antimeridian got
an image of only one side. A window must now hold the whole area, clipped to
the product, or the tool returns the product's full extent.
A box's radius was the distance to its farthest corner, which falls short for
large boxes: a whole-world box measured about 10,000 km, so bike-share and
transit selection never considered systems beyond it. The radius now checks
whether the box holds the point opposite its center and otherwise samples
every edge.
The place cache shared one lookup between callers but started it without a
signal or deadline, so a cancelled request kept running and stayed cached.
A shared lookup now has its own deadline, is cancelled when every caller has
given up, and only a successful answer is kept.
Camera packs keep their nearest cameras and the catalog cap thins them
further, so London answered 250 cameras while TfL offers many more. The
catalog now records each pack's full count, tags cameras with their pack,
and serves the packs it trimmed; find_cctv_cameras names them.
A sweep for the same pattern found more answers presented as current and
complete: launches and satellite orbits served from a stale proxy cache, a
regional aircraft fallback read as worldwide, license plate cameras trimmed
or partly loaded, and a radio directory of popular stations read as every
station. The launch and satellite sources now report the proxies' stale
markers, and each answer says what it covers.
Inherited names such as constructor, toString and __proto__ passed as
declared properties despite additionalProperties: false, and a schema-valued
additionalProperties was accepted but never enforced. Validation now uses own
properties, and definitions must give additionalProperties as a boolean.
The /mcp route built its Request without a signal, so a tool kept working
after the client left. The request's signal now aborts when the connection
closes before the answer is sent.
Installation tile results can arrive before the name pack, and the tool
answered with generic names such as "Military area" where a moment later
it would say "Camp Mabry". It now waits up to five seconds for the named
result and says when names are still loading.
Traffic figures for a radius area counted roads anywhere in its bounding
box, and a tile that failed to load silently changed them. Roads are now cut
to the circle, and the flow source reports per request whether any tile
failed so the answer can say its figures are partial.
Bike share and transit read only the three nearest systems, and place
search returns at most five matches within 50 km, while answers read as
complete. Answers now name the systems not searched and say when a place
search limit may have left places out.
get_weather dropped the proxy's stale status and get_active_fires dropped
FIRMS staleness and failed satellite sources, so answers and the HUD caption
read them as current. Both now report them, and the caption context marks
such sections stale or degraded.
Fire perimeters were placed at their average vertex, so a fire enclosing the
whole requested area was missed. Polygons now count when a vertex is inside
the area, an edge crosses it, or the polygon contains it outside any hole.
Brief sections received a named place as its bounding box, so they centered
on the box's middle: Tokyo's aircraft were sorted from a point in the
Pacific. Sections now receive the place name and resolve it like a direct
query, and place lookups are cached briefly so a brief resolves it once.
The HTTP handler read the whole body before checking its size, so a body
without Content-Length could be buffered without bound. It now reads the
stream and stops as soon as the body passes the limit.
Cable searches only tested vertices, so a cable crossing an area with no
vertex inside it was missed. Lines now test each segment against the area's
bounds, across the antimeridian too, and against a radius area's great-circle
distance.
The longitude span divided the angular radius by the center's cosine, which
is too narrow for large radii and near the poles. Areas now use the exact
tangent bound and span every longitude when the circle reaches a pole.
The caption request declared every answered section nominal, so a stale
aircraft snapshot reached the caption as live. Each section now reports
stale or unavailable from its own result, and the provenance envelope is
built with the app's feedProvenanceEnvelope.
get_transit_vehicles returned positions from a feed the proxy served from
its error cache, and hour-old fixes, as current. It now applies the transit
layer's rules: a feed is stale when the proxy says so or the operator has
not answered recently, and fixes older than the vehicle limit are dropped
and counted.
find_aircraft failed whenever either feed failed. It now answers from the
feeds that responded, names the ones that did not, and searches only the
military feed for registrations, the only feed that reports them.
situation_brief, military_awareness and the HUD caption called other tools
directly, so replaced tools and interceptors did not apply to their sections.
Tools now reach each other through the catalog, and interceptors see these
calls with the calling tool as parent. Surfaces become a view over the
composed catalog, so a composite offered on voice keeps sections that voice
does not list.
The installation source keeps one named site per display cell for the map.
Queries now ask for every site and the exact box, and report when the
source returned only part of the mapped sites.
Reworks #568 to the shape asked for in review.
Generalized from "AQHI" to a provider-backed Air quality layer, with ECCC's
AQHI as the first registered provider. Readings are carried as
{ provider, scale, value, band } rather than a bare number, because
air-quality indices are not interchangeable: AQHI runs 1-10+ on health risk,
the US AQI runs 0-500 on concentration breakpoints, and Europe's EAQI is a
five-step category. Naming the index a value belongs to is what lets another
network be added without inventing a shared numeric meaning. Coverage is
stated as "Canada only" on the provider, the layer row and the card.
Adds the WEATHER panel readout (readout: true) carrying the band legend and
the worst readings currently in view, with getRowControls/setRowControlsListener
following the wind and cyclone cards. An unresolvable view rectangle degrades
to the worst readings overall rather than to a blank card, and a rectangle
crossing the antimeridian is handled.
update() now takes the earthquakes layer's abort-and-guard shape: it aborts any
in-flight request, passes the signal through to the source, and refuses to
touch the scene when the request was superseded, aborted, or the layer was
disabled while it was outstanding. disable() and destroy() abort too. Tests
cover the late-response, superseded-refresh and disabled-update paths.
Rebased on main: the source is registered in src/sources/reference.js, and the
allocation profile was re-measured against the current worker — median 3,900
B/frame, max 4,114, 85.7 B/candidate, inside the shared 154 ceiling, budgeted
at 5,200 for 26% headroom. The earlier figures no longer held.
Freshness is now judged on the observation time rather than on when the last
response arrived. lastUpdate only says the feed answered; a station that keeps
returning a reading from four hours ago would otherwise be narrated as live by
the HUD and the voice provenance envelope. getStats() reports observedAt and
stale, so layerFeedState() calls the layer stale past two missed hourly
publications, and degraded when a refresh fails over readings that are still
good.
The share-link token is 0, which is what layer-token:next allocates. Taking it
moves the ledger fixtures that had hard-coded 0 as the free digit and 3 as the
one after; those shift to 3 and 4. The pinned equality between the reservation
ledger and LEGACY_LAYER_STATE_TOKENS also had to become "the closed legacy set
plus air-quality", since that set is documented as never growing. Happy to
re-pin this differently if you would rather the fixtures derive the provisional
digits from nextLayerStateToken() so the next layer does not move them again.
The layer and its source are registered as ./layers/air-quality and
./layers/air-quality/source, each with its own boundary group, following the
perimeters pair. check:boundaries requires the package exports and the group
exports to be the same set, so a layer that ships a source needs both.
P0
- tileMath: lonToTileX(180) wrapped to column 0, so a whole-world view at
z1 loaded only the western column; 180 is now the last column.
- coverage.loadTile: a superseded request (after a retire or clear) still
deleted the newer request's pending entry and loading count in `finally`,
letting a duplicate load attach lines that nothing could remove. Only the
request that still owns the key attaches its tile or settles the key.
P1
- view/tileMath: a view across the date line read as a ~358 deg box and
loaded tiles on the far side of the globe. visibleBbox now returns
west > east for such views and tilesForBbox splits them at +/-180;
viewCentre averages across the line.
- decode: the parts of a multi-line sequence were joined into one line,
drawing straight segments over capture gaps. Each part is now its own
line (`parts`); later parts carry `~n` in their pick id, and picking or
recolouring any part acts on the whole sequence.
- viewerHost: a prewarmed MapillaryJS viewer was never destroyed, leaking
its WebGL context. Prewarmed adapters are tracked and released on
unmount, at once when the layer went off mid-load, and per provider when
only that provider is switched off.
- nearestImage took the first record the API returned; it now takes the
closest one that passes the filter.
P2
- groundCast: the cache was cleared after a request counted its corners,
dropping ones it relied on; it is now cleared first.
- streetLevelControls: the viewer was resized on every render (every frame
while a panorama is dragged); the ResizeObserver handles size changes and
the viewer is resized once when it opens. The legend now rebuilds when its
entries change, not only their count.
- panelPositionControls: the portable-panel paths this PR added called the
CCTV viewport sync directly; they go through onPanelResized, which the
shell routes to CCTV. The "Compose camera panel controls" JSDoc is back on
_initCctvPanel.
New tests: zoom-1 columns, date-line tiles and bbox, multi-part decode and
part pick ids, nearest image, prewarmed viewer release, and the cache
ordering (fails on the old order).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
One add/add conflict, in scripts/format-scope.json: upstream's twelve new
entries and this branch's two. Both kept — 866 entries on main, 868 here,
none dropped.
Gates on the result: npm test 5397 passing, build, test:track 109/109,
changelog:check, format:check, check:boundaries, layer-token:check.
The raycast challenge on #768 showed bare-earth casting misplacing roads
that are not at grade: lines floated over freeways in trenches and steep
streets (rendered line pixels floating: Austin 8.5%, SF 27.1%, LA 43.8%),
because a ~111 m terrain grid cannot see a 30 m-wide cut or a slope between
its corners.
meshSampler.js samples the rendered Google 3D surface (scene.sampleHeight,
overlays excluded) once per ~11 m cell within 900 m of the camera, nearest
first, in idle time within a 6 ms budget; misses are retried, not latched.
groundCast.refineHeights blends it in, per point along a line:
- mesh up to 3 m above bare earth is the road: follow it, 1 m above;
- higher mesh (canopy, deck, roof): carry the road's offset from bare earth
across from the road samples on both sides (up to 8 points);
- otherwise bare earth + 2 m, as before.
Lines, cones and the marker use it; tiles touched by new samples are
redrawn one per idle slice with the no-blink swap.
Rendered line pixels on the surface / floating, before -> after:
Austin 90.0 -> 95.3% / 8.5 -> 0.7%; SF 69.0 -> 86.4% / 27.1 -> 9.1%;
LA 52.3 -> 91.0% / 43.8 -> 4.0%; Innsbruck 95.0 -> 95.7% / 0.2 -> 0.2%;
Sacramento 80.7 -> 79.3% / 3.8 -> 2.7%; buried lines stay under 1%.
Cones floating in SF 44 -> 5%, buried at LA 59 -> 28%. Main-thread long
tasks added during refinement: at most 134 ms (Austin, densest tile).
Roads on elevated decks are still drawn at ground level under the deck.
Unit tests cover the blend (trench, slope, canopy bridge, deck, bad probe),
the finer cast with meshAt, and the sampler (once per cell, nearest first,
overlays excluded, far cells skipped, misses not latched, idle when off).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
From the code review of #768:
- view.js measured the camera's height above ground with globe.getHeight,
but Google 3D hides the globe, so coverage picked its zoom from the
ellipsoidal height. Over a high city (Denver, ~1,600 m) a camera 500 m up
read 2,100 m: coverage stayed at z12/z13, whose tiles are too big to cast,
and lines stayed draped on the roofs. With the globe hidden, the bare-earth
height from the ground caster now answers; coverage re-picks its zoom when
the surface mode changes. Verified in Denver: terrain mode, z14, cast lines.
The core's surface check uses the same helper (and no longer subtracts the
2 m lift from the camera height).
- castTile rebuilt a tile even when no line could be cast (terrain proxy
down, tile too big); it now leaves such tiles as they are.
- The feature gate read Street Level's share token as a literal `0`; it now
takes it from LAYER_STATE_REGISTRY, since a rebase can reassign it.
New view.test.mjs covers the shown-globe, hidden-globe and no-sample cases.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The merge of main (c451478) appended main's new entries directly after
this branch's last entries in scripts/format-scope.json (after
streetLevel/index.test.mjs) and in two groups of
scripts/package-boundaries.json (after app/layers/streetLevel.js), without
the separating comma. Both files stopped parsing, so every CI job failed
at its first JSON read, the "Check adopted formatting" step.
With the commas back, the CI steps pass locally: doctor, format:check,
check:boundaries, layer-token:check against main, npm test (5475 pass),
build, and the Windows job's focused onboarding tests.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
src/tools/surfaces.js lists the tools a surface leaves out, and the MCP
server, the voice session's tool list and the browser catalog voice calls
all filter through it. Voice leaves out tools that answer with images, link
to the app, or repeat what its app actions already answer.
A stalled global snapshot held the request for over a minute. Each attempt
now gets 10 seconds, a timed-out or failed attempt is retried once, and a
second failure is answered from the stale cache or the regional fallback.
The OAuth token request gets the same limit.
Unknown aircraft tracks read as empty instead of a server error, aircraft
info names the aircraft and flight, the HUD caption sends the app's caption
context, and satellite pass, satellites overhead and nearby places take a
place name or point like the other tools.
The datacenter and dam layers parsed their bundled GeoJSON Lines and
built analyst records inside the Cesium-backed layer module. Move the
data URLs, the line parser, mapAnalystRecord and layerTitle unchanged to
src/sources/infrastructureData.js; the layer module imports them and
still exports mapAnalystRecord. The consumer-build pin lists the new
module.
Add find_alpr_cameras over the layer's OpenStreetMap camera extracts:
cameras in a US or Canadian area of at most 3° per side, nearest first,
with operator, manufacturer and facing direction, and the source's
attribution. Areas outside coverage or too wide are reported.
Add find_submarine_cables over the bundled TeleGeography data: cables
passing through or landing in an area, or matching a name, with landing
points and the dataset's attribution. The local MCP server's fetch reads
the bundled data files a browser would load by URL, and nothing outside
that directory.
Add get_recent_imagery. It searches the recent Harmonized Landsat and
Sentinel-2 catalog for an area, picks the image the layer would show
with the layer's own ranking, and returns it with its date, sensor and
cloud cover, falling back to the VIIRS daily overview.
Add get_weather_map, which returns the latest NOAA radar, satellite or
lightning map over an area as an image, choosing a window the image
route accepts and reporting areas outside the product's coverage, and
get_wind, which samples GFS or IFS wind at a location with the wind
layer's own grid sampling.
Add getImage() to the weather source. It reads one frame through the
bounded image route the layer already uses and returns its content type
and bytes, so code outside the layer can show the current map.
open_in_gods_eye_view accepts layers and writes them with the app's own
share-link layer codec, so the link opens with those layers on. Layer
names are limited to the registered layer ids.
layerState.js held both the share-link layer codec and the coordinator
that restores layer choices into the running app and persists them to
browser storage. Move the coordinator, unchanged, to
layerStateCoordinator.js so the codec stays a pure data module that code
outside the browser can use to write share links. The registry stays in
layerState.js, where the token tooling reads it, and the aircraft
barrel re-exports both.
Move the composition of the default tool services from the Node MCP
server into gods-eye-view/tools/services, a portable export. Given a
fetch that resolves the sources' /api paths, it builds the same services
in a browser as on the local MCP server, so other surfaces can compose
the same catalog. Queries still read only their services.
A place's bounds can reach far from the place itself; Tokyo's include
remote Pacific islands, so the middle of its box is open ocean. Keep the
geocoder's point for a named place and use it as the area's center,
while bounds still decide what lies inside the area.
Mount the local MCP server at /mcp on the development and preview
servers for clients that connect by URL. The route accepts only
requests from this machine that name a loopback host and, when an
Origin is sent, come from a loopback origin, and drains oversized
bodies so the 413 response is delivered. The provider-order regression
test now pins the route between the providers and the API fallback.
Add open_in_gods_eye_view. It builds a version 2 share link looking
straight down on an area, with altitude chosen from the area's size, on
the app address the application supplies.
Add get_traffic_flow over the traffic source's TomTom flow tiles. It
reports average speed as a share of free-flow speed, congested and
closed road length, and a breakdown by road category, stepping down a
zoom level when an area needs more tiles than the source reads at once.
Add vessels_in_area, find_vessel and get_vessel_track over the live
vessel source, and a ships section in situation_brief. Without an AIS
key the source's own message reaches clients as an unavailable feed.
Add get_weather, get_regional_brief, get_cyclones, get_fire_perimeters,
get_terrain_height, find_military_installations and get_map_features
over the application request services and the layers' sources.
situation_brief combines weather, earthquakes, fires, aircraft and
cyclones for an area by reusing those tools, marking sections that are
unavailable instead of failing. get_hud_caption returns the app's
heads-up display caption for the same overview.
Add get_bike_share over the public GBFS systems and get_transit_vehicles
over the GTFS-Realtime feeds. Each finds the systems or feeds whose
coverage reaches the area, reads up to three, and reports any that were
unavailable instead of failing the whole answer. Transit results carry
each feed's attribution and license.
Add getFeeds() to the transit source. It reads the public catalog route
the server already serves, so code outside the layer can find feeds
near a location without loading the layer.
The bike-share layer parsed station documents inside its model factory.
Move the parsers unchanged to src/sources/gbfsStations.js and have the
model import them, so code outside the layer can read GBFS stations
without constructing the layer.
Add search_places and places_nearby over the app's Google Places routes,
and plan_route over its OpenStreetMap routing. The place search service
reports when no search key is configured, so an unconfigured server is
not mistaken for an empty result, and rate limits become retry_later.
Route endpoints accept a place name or coordinates.
Add find_cctv_cameras, get_cctv_snapshot and find_radio_stations. A
snapshot returns the camera's current frame as image content with its
credit. Tool results may now carry base64 images, which the MCP adapter
sends as image content.
Add getFrame(camera) to the CCTV source. It reads one frame through the
registered frame endpoint the layer already uses and returns its content
type and bytes, so code outside the layer can show a camera's view.
Add aircraft_in_area, find_aircraft, get_aircraft_track and
get_aircraft_info over the live aircraft sources, with an optional
military feed, and next_satellite_pass and satellites_overhead over the
satellite catalog and the existing pass predictor.
Source failures from the live contract now become tool errors with the
matching code, so a rate limit reaches clients as retry_later with a
wait time. Tool schemas may use pattern.
The satellites layer parsed catalog text inside its Cesium-backed orbit
owner. Move the parser unchanged to src/sources/tle.js, add a catalog
number helper, and have the layer delegate to it so code outside the
layer can read TLE text without loading rendering.
Define tools that answer questions from the app's data once, independent
of the surface that exposes them. A tool declares its arguments and the
services it reads; applications compose tools with their own services and
optional call interceptors. Queries reuse the layers' portable source
factories, so they run wherever an application routes /api requests.
The first queries are get_earthquakes, get_active_fires and
get_recent_launches, with a shared area argument (place name, bounding
box, or point and radius) and capped lists.
gods-eye-view/tools/mcp adapts a catalog to the Model Context Protocol
with a small JSON-RPC handler and a stateless HTTP transport, adding no
dependencies. npm run mcp serves Core's tools over stdio to local clients
such as Claude Code, reading from a running app. Both exports are
portable and checked by check:boundaries; the app does not import them.
docs/TRANSLATORS.md is rewritten for the foundation state: English
shipped, and the locale-addition recipe front and center — the
one-PR-per-locale procedure (catalogs, registration, CATALOG_LOCALES,
pair behavior, parity gates). CURRENT-STATE.md documents the subsystem
(file map, resolution order, selector, gates, deferrals) and the
CHANGELOG records the foundation.
Route every application-owned presentation string through the i18n
core: t() call sites across the app/layers/ui/data/voice surfaces,
data-i18n* attributes in the src/ui/templates/*.html markup (applied at
boot by applyDocumentTranslations from src/standalone/application.js),
and the dock locale selector (runtime-rendered from the offered pair —
a single EN button while only English ships).
Rebased onto current main, where the extraction follows the module
splits: the shell wiring now lands in the owner modules
(panelChrome, visualSettings, locationNavigation, shareRestoration,
shellFeedback, applicationShell) and the voice status corpus in the
realtime* modules; scene status lives in director.js +
playbackAdapter.js.
Modules shared with the node runtime (loadingFeedback,
installationFeedback, regionalModel, keySetupCore, vessels ingestion)
stay environment-neutral English and localize at their browser render
sites (chip maps in shellFeedback/installations controls, the weather
table in cockpitBriefing, keySetupRequirementEnvVars compositions), so
the package-boundary graph stays clean; the browser boundary groups
now own the i18n modules they reach (scripts/package-boundaries.json).
Add the i18n foundation: src/i18n/locale.js (locale resolution with a
configurable GEV_DEFAULT_LOCALE / GEV_SECONDARY_LOCALE pair, guarded
preference storage, one-shot ?lang= override, <html lang>/<html dir>
metadata) and src/i18n/index.js (catalog registry, t(), Intl formatters,
applyDocumentTranslations, persistLocaleAndReload).
English is the only shipped locale in this PR: the four en catalogs
(shell/cockpit/layers/setup) are the reference and the always-shipped
fallback, the built-in pair degenerates to en-only, and English output
is byte-identical to the strings these replace.
Parity gates land with the core (catalog key/placeholder/plural-shape
parity, markup coverage of every data-i18n* attribute, repair-pass
anchors pinning en byte-identity); additional locales ship as stacked
follow-up PRs through the recipe in docs/TRANSLATORS.md.
Rebased onto current main: the client defines live in build/vite.js /
server/standalone/vite.config.js as before.
NHC stamps an advisory with its nominal time and writes it about half an
hour early, so the 5-minute future limit rejected the whole feed each
advisory cycle and the layer showed no storms. Allow 90 minutes ahead.
As more sources arrive, each one reads the same everywhere it appears: its
PROVIDERS chip, coverage lines, overview points, image cones and a single
legend swatch. The palette lives in one place, PROVIDER_COLORS in
src/layers/streetLevel/policy.js: Mapillary green (#05cb63), Panoramax
purple, Google Street View blue.
Mapillary drops its age dimming and magenta panoramas: lines and overview
points are Mapillary green, cones are green too and 360° images keep their
ring shape; the 360°/FLAT filter still separates them, and the selected
sequence stays GEV cyan.
The provider contract loses `legend`: `colors.coverage` is the source's one
colour, and the core composes the legend as one swatch per active source
(its name, its colour) followed by Selected. The chip group takes an
optional per-chip colour as a `--chip-color` CSS variable, written only
when it changes; Street Level chips show it as a swatch dot and, while on,
as the chip's border, text and a faint fill.
The feature gate checks the legend has one swatch per source plus Selected
and that the Mapillary chip carries #05cb63.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The panel changed the imagery filter and provider switches by calling the
layer directly, which bypasses the data manager's params request, the only
path saved state and share links record. A link copied after picking 360°
or a SINCE window came back with the defaults.
The controls now send those changes through
dataManager.setLayerParams('street-level', ..., { origin: 'user' }), as
the CCTV panel does; the layer applies them through its own setParams.
The feature gate's 360° step now asserts the share hash carries `0.p.p`;
it times out on the previous controls.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
fetchTile coalesced concurrent requests for one tile onto a single
upstream fetch, but that fetch ran on the first caller's signal. When the
first request was abandoned (an older pan), every joined request lost a
tile it still needed; and a joined caller that aborted stayed subscribed
until the shared fetch finished.
A flight now owns its AbortController and counts waiters, the pattern the
weather and wind providers use. Each caller's own abort rejects that caller
at once; the upstream fetch is cancelled only when the last waiter leaves,
and a later request after that starts a fresh fetch.
Regression tests (fail on the previous tiles.js, pass now): a joined
request survives the first caller aborting with one upstream request; a
joined caller that aborts leaves at once while the first still gets the
tile; both waiters leaving cancels the owned fetch.
Reported by kvnloo in review.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Brings in #744 (share-link token ledger). package.json: keep the branch's
sorted scripts and add layer-token:next and layer-token:check.
Street Level's token moves from the provisional `3` to `0`: following
CONTRIBUTING, the provisional entry was removed and
`npm run layer-token:next -- street-level` assigned `0` (free digits in
order). It is added to the registry and to
src/data/layerStateTokenReservations.json, and
`npm run layer-token:check -- --base-ref upstream/main` passes (28
published, 1 new). No link with the provisional token was ever published.
Pinned token tests move with the ledger as CONTRIBUTING asks: 29
reservations, next free digit `3`, and the competing-PR and retired-digit
scenarios shift up one free digit (0->3, 3->4) so they still test the same
rules. The README share examples and the free-digit notes in
CONTRIBUTING and CURRENT-STATE now read `0`.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Opening a photo frames it from 140 m behind using scene.sampleHeight for
the ground. Before the Google 3D tiles under the photo have loaded, that
sample can be kilometres wrong: an Austin 360 photo sampled -14,886 m and
the camera flew to -14,811 m. A sample now counts only when it is a
plausible surface height and, once the bare-earth height is known, lies
within -15/+80 m of it; otherwise the bare-earth height is used. FOLLOW's
eye height goes through the same check.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Draped ground polylines and clamped billboards land on the top of the
Google Photorealistic 3D mesh, so on a street under trees or an overpass
the coverage seemed to float at roof height.
At street zoom on Google 3D (camera below 1,400 m, back above 1,800 m)
the core switches to a `terrain` surface mode. groundCast.js samples the
existing terrain service on the ground-floor grid (0.001 deg), interpolates
bilinearly and adds 2 m; Mapillary lines become plain polylines at those
heights, and cones and the position marker stand on the street, so the
mesh hides what is behind buildings and trees. A tile draws draped first
and swaps to its cast lines once the heights arrive, keeping the old lines
until the new primitives are ready. Lookups are once per tile and aborted
when the tile is dropped; a geoid fallback counts as unknown and leaves
lines draped. Other map stacks and higher views are unchanged.
Providers get getSurface() and groundCaster in their context and an
optional setSurface(mode); getUIState() exposes `surface`. The QA gate
checks draped on Esri and terrain on Google 3D.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
npm test is the only suite CI runs, and its discovery walked src/ alone, so
server/providers/transitHistory.test.mjs has never executed since it was
added. Its 8 tests pass, so this was silent coverage loss rather than a hidden
failure: the file reads like covered code in review and never ran anywhere.
Add server/ to the searched roots, and pin the property with a test that walks
the repository itself and compares the two sets, so a tree added later without
updating the roots fails loudly instead of going quiet.
Suite goes from 5090 to 5099 tests, 0 failures: the 8 tests in that file plus
the regression test added here.
Dogfoods the convention rather than describing it: the entry for #787 arrives
as `changelog.d/787-changelog-fragments.md` and `CHANGELOG.md` is untouched.
A fragment needs the pull request number, which only exists once the pull
request is open, so it lands in a second commit. `changelog.d/README.md` says
to push first and add the fragment after, for the same reason.
Checked against the one consumer that constrains the design: assembling this
fragment moves the `DISPLAY ▸ Draw` marker in `CHANGELOG.md` by exactly the
length of the spliced entry, and the 1200-character window that
`src/annotations/drawTool.test.mjs` slices after it is byte-identical.
`CONTRIBUTING.md` asks every runtime change to add an entry at the top of
`CHANGELOG.md`, so two pull requests in flight write into the same few lines and
git has nothing to merge on. Against `main` at `81eb443`, `git merge-tree` over
the 85 open pull requests GitHub reports as conflicting that aren't mine puts
`CHANGELOG.md` in the conflict set of 41 — more than any other path — and for 6
it is the only one. `docs/CURRENT-STATE.md`, the other file rule 3 names,
conflicts for 23, and merges cleanly for 23 of the 41: edits land in the section
for the area you touched, so they only collide when the areas do.
A pull request now adds `changelog.d/<pull request>-<slug>.md` instead. Distinct
filenames never conflict, and the pull request number makes them distinct for
free. `npm run changelog` splices the directory into `CHANGELOG.md` and empties
it; `npm run changelog:check` reports the batch and changes nothing.
Fragment text is copied through unchanged apart from trailing whitespace.
`src/annotations/drawTool.test.mjs` reads `CHANGELOG.md` as prose and slices a
fixed window after a heading, so an assembler that reflowed an entry could move
text across that window. Fragments are deliberately outside
`scripts/format-scope.json` for the same reason.
The assembler refuses a batch before writing anything when a filename is not
`<pull request>-<slug>.md` or two fragments claim one pull request number, and
a unit test runs that check over the real `changelog.d/`, so a fragment the
release could not assemble fails `npm test` in the pull request that adds it
rather than at release time.
Refs #781.
`mapAnalystRecord` has always published `verticalRateMps`, but the tracked
seam — `getTrackedInfo()` / `getTrackedSubject()`, built by `_describeFlight`
— did not, so anything needing climb/descent had to reach past the public
descriptor into the raw poll record.
Both layers now map it, which matters because they store it under different
keys: the civil records write `verticalRate` (records.js), the military ones
write `verticalRateMps`. Mapping only one would leave
`resolveTrackedAircraftInfo()` handing back two different shapes, and a
consumer reading the field off a civil contact would silently read every
military contact as level flight.
Reported, never derived. `stickyNumber` seeds the field with null, so an
aircraft that has not transmitted a rate publishes null rather than a
level-flight 0 — absent is not level, and a flight-phase classifier that
cannot tell them apart calls a climbing aircraft cruise.
Tests: both layers, both halves (a reported rate arrives with its sign; an
absent one stays null). Mutation-verified — removing either field or
defaulting it to 0 turns them red.
`docs/CURRENT-STATE.md` records the descriptor field, the two record keys it
reconciles, and the reported-never-derived rule.
The proxy already restricted WHERE it would go — https, an allowlisted host, a
station_information/station_status path, no redirects, a 12-second deadline and
a 5 MB body cap — but nothing restricted HOW OFTEN, so the dev server would
relay an unbounded stream of requests at a third-party bikeshare operator on
any caller's behalf, one outbound fetch and one timeout timer each.
Admit 120 requests/minute per client address, global backstop 3x, refused with
429 and `Retry-After` before the target is even parsed. The ceiling comes from
the layer's own demand: `src/layers/bikeshare/registry.js` catalogues 15
systems, station information is cached per city after its first fetch, and
station status refreshes once per city every STATUS_POLL_MS (60s) with
in-flight requests deduplicated — about 15 requests a minute with everything
in range, so 120 is eight times a maximal session.
The method, host, path, redirect, deadline and size guards are unchanged.
Refs #264
Every distinct 5-decimal point was a permanent in-memory entry, a
permanent line in an uncapped `.gev-cache/terrain-heights.json`, and a
live upstream call. Nothing limited the rate, and coordinates were
checked for being finite rather than for being on Earth, so an
out-of-range pair was still a valid cache key.
Four bounds, each following a pattern this repo already uses:
- WGS-84 range is enforced in `parseTerrainPoints`, at the edge, so no
later stage has to. Poles and the antimeridian still parse.
- The point cache holds at most 50,000 entries and evicts oldest-first,
the shape `trackBackfillProxies` uses. Sized from measurement: this
checkout's cache file held 6,189 entries in 1.0 MB of real use, so the
ceiling is eight times that working set. Terrain does not move, so an
evicted point costs one refetch and never a wrong answer.
- A cache file above 24 MB is not read back, mirroring
`rocketLaunchesProxy`. An unmeasurable file is not treated as an
oversized one: without stat, readFile decides, exactly as before.
- The route carries an always-on limiter of 90/min per client, 270
global — the `/api/overpass` band, and an order of magnitude above
measured demand (a full test:track run drove 10 requests, peak 8/min).
The ceilings are constructor options so they can be exercised as
behaviour rather than asserted as constants; production passes none.
They are destructured in the body because `proxyErrorResponses.test.mjs`
slices these proxies to the first column-zero closing brace.
CONTRIBUTING requires runtime-behavior changes to update docs/CURRENT-STATE.md
and CHANGELOG.md in the same PR, and new env controls belong in .env.example
with the other CCTV pack controls. The README and CURRENT-STATE table diffs
are column realignment from the widened source cell.
The per-pack defaults already summed to 4,054 before WSDOT (4,304 with
it), past the documented 4,000 cap, so a default install was trimming.
The existing sum check in the DriveBC test listed packs by hand and
missed Calgary and DelDOT. Replace it with a check in
cctvCatalogCap.test.mjs that sums every exported DEFAULT_*_MAX_SOURCES,
and drop the Calgary test that pinned the cap at exactly 4,000.
CONTRIBUTING requires runtime-behavior changes to update docs/CURRENT-STATE.md
and CHANGELOG.md in the same PR. The README and CURRENT-STATE table diffs are
column realignment from the widened source cell.
A PTZ/varifocal datasheet FOV is a capability range, not the lens's
current zoom, so replacing the frustum's estimate with its wide end
presented unknown optical state as known. Parse the full horizontal
range (ignoring vertical, "combined" panoramic and lens-note figures),
carry it as specFovMinDeg/specFovMaxDeg, and substitute the datasheet
FOV only for fixed single-lens hardware. The HUD labels the range as
capability beside the estimate. This also stops the Q6000-E Mk II's
"360 combined" figure from being read as a 360-degree lens.
The loader and normalizer carried the hardware model, but the /sources
response projection and the browser catalog both whitelist fields and
dropped it, so no client consumer ever saw it. Project model and
manufacturer (manufacturer disambiguates bare model strings like "3950")
at both boundaries, and add a test that drives the real middleware and
browser catalog end to end.
WSDOT's layer metadata defines BW (Bothways) plus the diagonals
NE/SE/SW/NW alongside N/E/S/W. Diagonal cameras were being demoted to the
low-confidence id-hash fallback; map them to 45/135/225/315 degrees and
treat BW (not B) as the no-single-facing code. Add CCTV_WSDOT_ENABLED and
CCTV_WSDOT_MAX_SOURCES to .env.example with the other pack controls.
Address review feedback on #595:
- Reject frame links carrying userinfo (user:pass@) in
normalizeCataloniaImageUrl, so catalog data cannot inject credentials
into the server-side frame request on an allowlisted host.
- Fetch cameres.xml over https (verified: same 200 body, no redirect),
refuse redirects with the unread body released, and read the body
through readResponseTextCapped with a 1 MiB CATALONIA_MAX_CATALOG_BYTES
cap instead of an unbounded resp.text().
- Add regression tests for userinfo links on every allowed host, the
refused redirect, and declared and streamed oversize bodies.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Route the six proxied fetches that still buffered upstream bodies without
a limit through the shared capped readers in src/sources/httpBody.js:
FIRMS CSV, CelesTrak TLE groups, adsb.lol /v2/mil (both the OK and
relayed-status paths), TomTom flow tiles, OpenSky states/all and adsbdb
enrichment lookups. Each cap is a bound sized well above the measured
payload, not a budget.
readResponseBytesCapped gains the same optional abort signal as the text
reader, with the same cancel-on-abort plumbing and reader release, so the
TomTom tile path can share the route deadline instead of reading
arrayBuffer() unbounded. Where a route already had a deadline, the same
signal now covers the body read as well as the headers. Two polls had no
deadline at all: the adsb.lol military poll gains a 12-second one and the
OpenSky states/all exchange, including its Basic-auth retry, a 20-second
one; both miss into their existing STALE paths.
FIRMS map-key status, the OpenSky token exchange and CCTV media are left
as they are.
Co-Authored-By: Claude Opus 5 <[email protected]>
The ownership table lists every owner except the backend, though realtimeBackend.js owns the two requests, their separate transports, the abort/timeout scope around both, and client-secret expiry.
The method signatures are already documented with application construction, so link there rather than repeat them, and state only the ownership facts written nowhere: the connection is the backend's only consumer in the application, the resolved credential is opaque to it and is handed to negotiate whole, and client-secret expiry is refused inside the backend.
FOLLOW puts the globe camera at eye height inside the street-level view; on
flat imagery or terrain there is nothing to stand among. The layer now takes
the application map stack controller (attachMapStackController), subscribes
to stack switches, offers FOLLOW only while the stack is 'photoreal', disables
the button with a MAP SOURCE hint elsewhere, and stops following when the
stack leaves Google 3D. Selection uninstall is a no-op when never installed.
The QA gate checks the disabled path and, when Google 3D is available, that
leaving it stops following; its caption-timeout dump now forces one render to
tell a missed update from a render fault.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A floating window restored after a reload had no z-index, and the z ladder
renumbered from 101, so the docked DISPLAY panel (#pp-toggles, z 110) painted
over it. The ladder now starts at 111 (PANEL_Z_FLOATING_FLOOR), a restored
window is promoted like a lifted one, docking drops the promotion, and
floating panels get a CSS floor of 111. Tests fail on the old code.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
At phone width the right rail is the band between mid-screen and the credit
line, so a 16:10 viewer plus its bar and caption was cut off. Below 720 px
the viewer's height is derived from that band (clamped 110-320 px); floating
and expanded viewers reset to auto height. The QA harness checks the photo
and caption fit at 390x844, and CURRENT-STATE documents the docking rules
and phone sizing.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Only a user's own collapse docks a floating window; cockpit entry,
restores and the Cyber accordion no longer dock it or delete its saved
place, and the accordion leaves floating windows alone.
- The viewer host records an adapter as active only after its mount
succeeds, so a library that failed to load once is retried; concurrent
opens share one mount, and unmounting mid-mount leaves nothing active.
- openNearest tries every active provider; one provider's error (no key,
offline) no longer hides the others' imagery.
- The SINCE readout shows its cut-off date as soon as the committed value
matches the thumb, even while the slider keeps focus.
- A floating window that was only dragged is bounded to the viewport and
scrolls instead of running off-screen.
- getStats summarises coverage (summarizeCoverage) instead of building a
full UI snapshot every second; disable() uses credits.hideAll(); smaller
cleanups in the viewer host and sequence snapshot.
- Unit tests: panel docking and header double press, viewer-host mount
retry/sharing/cancel, coverage summary.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- The viewer opens at the top of the panel, under the header, so the photo
is on screen without scrolling; its tools (EXPAND, FIT/FILL, FOLLOW, close)
sit in a bar above the image, clear of the provider's own overlays, and the
caption wraps instead of truncating.
- No separate ON/OFF or OPEN NEAREST PHOTO buttons: the header pill is the
layer switch, and a provider chip is lit only while the layer is on (the
last lit chip switches the layer off).
- SINCE is a stepped slider (any date to the last month) whose readout names
the cut-off date and is announced via aria-valuetext.
- The panel floats and resizes (portable spec); in a resized window the
viewer takes the spare height and only the settings scroll. SHRINK after
EXPAND, collapsing, or a header double-click docks it at its default size.
- FIT/FILL tooltips say what they do.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- The rail's startup move of panels into #right-context-rail cleared every
panel's inline position, so a window restored as floating dropped back to
its rail place after a reload. Floating panels now keep their geometry.
- A header double-click never snapped a floating panel back in a real
browser: the drag handler's pointerdown preventDefault() suppresses the
mouse events a native dblclick is built from. Two presses within 400 ms and
6 px (no drag between) are now detected in pointerdown itself.
- Portable specs can ask to dock when collapsed (dockOnCollapse), and
PanelChrome exposes dockPanel(). Street Level opts in as a portable panel
(min 320 x 280) with dockOnCollapse.
- qa-panel-resize: the panel reader returns right/bottom (s/e handle points
were NaN), counts only visible rail panels like the rail does, parks the
window clear of the voice dock before the resize loop, and names the
element under the pointer when an assertion fails.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Bring in portable, resizable rail panels. The Street Level rail rules get
the same :not(.panel-floating) guard as CCTV's so the panel can float too.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Describe the provider-neutral layer, the PROVIDERS chip row, the shared
filter and its share-link options, and how a new provider registers.
Drop every mention of the removed plain-English query and Anthropic key.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Street Level is now a provider-neutral layer modelled on the iD editor's
photo overlay. The core owns what every imagery provider shares: the
enable state and a switch per provider, the imagery filter (360°/flat and
captured-since in relative days), one click handler routed by pick prefix,
one viewer host that mounts whichever provider's viewer adapter owns the
open image, the position marker, camera follow, one on-globe credit per
active provider, and share-link parameters (one bit per provider plus the
filter). Providers implement the contract in registry.js: coverage,
sequences, nearest-image lookup and a viewer adapter emitting a common
pose. Mapillary is the only registered provider; Google Street View,
KartaView and Panoramax slot in beside it.
Panel: a PROVIDERS chip row (one chip per registered provider, keyless
chips read as errors), relative-day SINCE options, a provider-neutral
caption whose link label follows the provider, and no vendor mark in the
header. The panel inner is a rail scroller like CCTV's.
Tests cover the registry, params codec, filter, pick router, UI-state
composition, the provider definition and coverage-tile decoding; the QA
harness switches the provider off and on and checks the filter.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Pure move: the layer id, panel id, CSS prefix, classes, exports and config
paths become provider-neutral (street-level, #street-level-panel, sl-);
the Mapillary-specific modules move under providers/mapillary/. Behaviour
is unchanged. The two sprite collections now register under distinct ids
instead of overwriting each other under 'mapillary'.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Street Level is imagery only from here on: coverage, sequences and the
embedded viewer. The Claude planner, map-feature and traffic-sign tiles,
the NDJSON feature stream, sprites, detection outlines and analyst records
go, along with @anthropic-ai/sdk, zod and the Anthropic key entry.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
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]>
The panel no longer shows a separate keyless call-to-action; the fieldset
gate and the layer-row tooltip already cover the missing-key case. The
planner flag is tri-state (null until status answers).
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Some catalogs publish a full product title rather than the bare model, e.g.
"AXIS Q6155-E PTZ Dome Network Camera" — which the exact model/alias match
misses. Add a fallback: after the exact match, scan for a record whose
DISTINCTIVE token (a >=4-char model/alias word containing a digit or hyphen,
so "dome"/"network"/"ptz"/"hd" never qualify) appears as a whole word in the
query. The most specific (longest) matched token wins; if two records tie on
the longest matched token the result is ambiguous and returns null, so the
lookup never guesses. Exact matches are unchanged and still take precedence.
Grow the enrichment table from the 4-model starter to the hardware the
currently integrated public catalogs actually publish, so identified
cameras render datasheet FOV cones and spec cards instead of estimates:
- King County (WA) road cameras: Cohu 3950 / 3960 (3965) / 3920 iDome
(3925), Bosch AUTODOME IP 5000i.
- Sarasota County (FL) + Sioux Falls (SD) open data: Bosch AUTODOME IP
starlight 7000i / starlight 7000 HD / dynamic 7000 HD, the VG4 AUTODOME
(500i), Bosch AUTODOME 800 (VG5-836), and Axis Q6155-E, Q6054-E,
Q6000-E Mk II, and P3707-PE.
All records are vendored verbatim from the CCTV Camera Database (CC0),
each transcribed from a manufacturer datasheet. Analog Cohu positioners
carry no fov_deg (their datasheets publish none), so they enrich the HUD
specs without a datasheet cone; every other model carries a real FOV.
Adds a test asserting the vendored table covers each integrated catalog's
published hardware strings.
When a CCTV source pack (or a seed) names its hardware `model`, resolve it
against a camera-model spec table: the coverage frustum then uses the
manufacturer's horizontal FOV instead of the pose estimate, and the HUD meta
line gains the model's resolution, night-vision, and PTZ specs. Cameras with
no identified model are unchanged — unknown never implies anything, matching
the layer's existing honesty about RAW PRIOR poses.
Spec records are vendored verbatim from the CCTV Camera Database
(https://github.com/ch-bas/cctv-camera-database, CC0-1.0), an open dataset of
12,000+ camera models transcribed from manufacturer datasheets. This lands a
small starter table plus the join + parsing helpers; source packs that publish
their hardware can grow it. Records describe camera products (optics,
resolution, night vision) only — no endpoints or access data.
- src/layers/cctv/modelSpecs.js: spec table, case-insensitive/alias lookup,
wide-end FOV parser, HUD summary formatter
- src/layers/cctv/modelSpecs.test.mjs: lookup/alias/unknown, FOV parsing
across the real fov string shapes, summary rendering
- src/layers/cctv/catalog.js: apply enrichment in both catalog builders
- src/layers/cctv/presentation.js: label a datasheet FOV, append the spec
summary, and pass fovSource + spec through the public camera state
Adds King County DOT Road Services' cameras as a keyless source pack, from
the county's public ArcGIS FeatureServer layer ("King County and WSDOT
Traffic Cameras"). 59 live cameras against the current feed.
- The `outFields` list is explicit on purpose: the layer also carries
asset-administration columns (including staff names) that must never be
fetched.
- Only live, active cameras with frames on the county's own
info.kingcounty.gov image handler are registered; the feed writes http://
and the handler serves HTTPS, so URLs are upgraded and pinned before
registration. WSDOT-owned rows are skipped so a camera never appears twice
when a statewide WSDOT pack is present; the few city-owned cameras the
county hosts (Redmond, Sammamish, Newcastle, Snoqualmie) carry their owner
as the per-camera credit, DriveBC-style.
- The layer publishes no facing: headings use the shared id-hash fallback at
low confidence, correctable with the calibration gizmo.
- Uniquely among the packs, the county publishes each camera's hardware
Manufacturer/Model (Cohu 3950/3965, Bosch AUTODOME IP 5000i, Axis
Q6135-LE, Siqura PD910). The model string is whitespace-collapsed (the
feed carries stray newlines) and carried on the source as `model`, with an
additive passthrough in normalizeSourceItem (same pattern as poseSource)
so identified-hardware consumers can use it. All 59 live cameras carry a
model value.
- Attribution per the KCGIS Center terms, registered in the attribution
popover and documented in DATA_SOURCES.md.
- Defaults: the whole catalog loads (125 cap); CCTV_KINGCOUNTY_MAX_SOURCES
changes the cap, CCTV_KINGCOUNTY_ENABLED=0 turns the pack off.
- Tests: filtering (owner/live/status/host/geometry/bbox), HTTPS upgrade,
model whitespace-collapse and passthrough, partner-city credit, env cap,
upstream-failure fallback, and normalizeSourceItem model semantics.
Adds Washington State's highway cameras as a keyless source pack, from the
Esri JSON layer WSDOT's own Travel Center map consumes
(data.wsdot.wa.gov/travelcenter/Cameras.json, ~1,700 point features).
Closes#624.
- Geometry arrives in EPSG:3857; converted to WGS84 on load and bounded to
a Washington bounding box.
- Only frames on WSDOT's own images.wsdot.wa.gov host are registered — the
catalog also lists ~70 partner cameras (Oregon DOT, City of Seattle,
lodges, airports) whose own terms apply; those are skipped rather than
proxied. 1,630 cameras survive the filters against the live feed.
- CompassDirection letters (N/E/S/W) give a high-confidence heading; the
majority `B` ("both directions") rows carry no single facing and use the
shared id-hash fallback at low confidence, with the usual two pose
personalities.
- The feed body carries Windows-1252 bytes (en-dashes in titles) with no
charset declaration, so it is decoded explicitly before parsing.
- WSDOT's layer metadata marks the feed "low volume": the 15-minute catalog
cache and per-request frame fetches respect that, frames are never stored
or redistributed, and the WSDOT credit is registered in the attribution
popover. A keyed WSDOT Traveler Information API serves the same population
for higher-volume use and is deliberately not required.
- Defaults: 250 cameras nearest Seattle and Spokane; CCTV_WSDOT_MAX_SOURCES
changes the cap, CCTV_WSDOT_ENABLED=0 turns the pack off.
- Tests: Mercator conversion against a real layer feature, official-host and
malformed-row filtering, compass mapping and both pose personalities,
cp1252 decoding, env cap, upstream-failure fallback.
Twenty layers now (weather, wind, cyclones and recent imagery landed
upstream); Street Level sits after Recent Imagery in the right rail and the
Cameras group, and the rail CSS names it beside the other panels.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
scripts/qa-mapillary.mjs drives a real browser through the rail panel,
the keyless gate, coverage, a ready-made feature plan, the viewer and its
expanded dialog at 1440×900 and 1280×800.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
MAPILLARY_CLIENT_TOKEN is a public client token and reaches the browser
through the explicit define block; ANTHROPIC_API_KEY stays server-side.
Both appear in POWER UP with links to create them.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
An ordinary collapsible GEV panel between CCTV and Context, starting
collapsed with the Mapillary mark in its strip. Body: a gated empty state
when no client token is present, plain-English query box (ASK becomes STOP
while running), streamed result chips, imagery filters with a colour
legend, and the embedded viewer whose expanded mode is a focus-trapped
dialog. All wording lives in a pure presentation module.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Vite middleware for /api/mapillary: status (capabilities, never values),
vector tiles cached in memory and on disk with the 12 MB z14 image layer
stripped in transit, an NDJSON feature stream capped at 600 tiles, MIT
sprite icons behind a grammar-guarded path, and a structured-output planner
that only activates when ANTHROPIC_API_KEY is set server-side.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Camera-driven Mapillary coverage (z0–5 overview points from orbit, z11–14
ground-clamped sequence lines below 60 km), per-sequence image cones, an
embedded MapillaryJS viewer bridge with a camera-follow mode, sprite icons
for map features, and a query engine that turns a plan into a streamed
z14 tile fan-out. Registers an on-globe CC BY-SA 4.0 credit while enabled.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
CURRENT-STATE and KNOWN-ISSUES said the panel position key is never
written; a portable panel now stores its floating window there, so both
documents describe the record and how a header double-click clears it.
CHANGELOG notes the CCTV panel's portable mode.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
The right-rail test now proves a .panel-floating panel is neither
measured, allocated nor auto-collapsed while its docked sibling still is.
scripts/qa-panel-resize.mjs drives the CCTV panel in a real browser:
lift-out by header drag, all eight handles with the opposite edge pinned,
minimum size, reload restore and double-click snap-back. Its pure helpers
sit behind a main guard so src/tooling/panelResizeQa.test.mjs can check
them without a browser. Run it with `npm run qa:panel-resize`.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
A drag spec can now be `portable`: dragging the panel by its header lifts
it out of the right rail into a fixed window, resizable from all four
edges and corners, that remembers its place and size under the existing
godsEyeView.v8.panelPos.<id> key ({ left, top, width?, height?, floating })
and snaps back on a header double-click. Only CCTV opts in here; the rail
skips .panel-floating panels when measuring and allocating, and its
placement rules in layers.css are guarded so no !important is needed.
PanelPositionControls gains an optional onPanelResized hook, threaded
through PanelChrome to a no-op on the application shell.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Pure box arithmetic for resizing a floating panel from any of the eight
compass handles: the named edges follow the pointer, the opposite edge
stays pinned, and minimum size plus the viewport margin win. Owned by the
same boundary groups as PanelPositionControls, which wires it next.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Keeps PR #595's Catalonia CCTV pack in sync with upstream's DelDOT
CCTV pack (#699) and the wind/weather doc updates from #617. The
sources.js conflict was a false positive from git's line-based diff
interleaving two structurally similar but independent loader
functions (loadCataloniaSourcesFromOpenData / loadDelDOTSourcesFromOpenData);
resolved by keeping both as complete, separate functions.
Full unit suite passes (4743/4743, 1 pre-existing Node-version skip).
- reject blank viewport-bias components before Number() reads them as 0
- give the upstream call a 10s deadline and a 256 KiB capped body read
- return fixed public text on failure instead of the upstream message
- Google's Geocoding web service refuses referrer-restricted keys, so the
browser call forced GOOGLE_MAPS_API_KEY to ship unrestricted
- add /api/google/geocode and /api/google/reverse-geocode, using the server
key like Places and Street View already do
- the browser key now needs the Map Tiles API only
Upstream added Director, Transit/GTFS-Realtime and Bhote Koshi scene work
in the same window; the only real conflict was two adjacent CHANGELOG
entries, resolved by keeping both.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Mechanical addition per CONTRIBUTING.md, following the existing per-pack
test-file entries (cctvCalgary.test.mjs, cctvNswSource.test.mjs, etc.).
Co-Authored-By: Claude Sonnet 5 <[email protected]>
One WFS/GML XML feed (~135 cameras) aggregating three publishers under a
`font` field: SCT's own highway cameras plus hotlinked Barcelona (IMI) and
Terrassa municipal cameras. Frame URLs are checked against a small host
allowlist rather than one pinned origin, and every non-SCT camera carries
its real owner as a `credit` shown beside the shared provider. The feed
carries no compass heading, so every camera loads at low heading confidence
(same as TfL and Fintraffic), and ground elevation is primed per publisher
rather than one flat prior.
SCT's own frame link needed a workaround: RenderService always 302s to a
second endpoint (TransitCamera) on a Location the upstream hardcodes as
plain http even when requested over https — refused by fetchWithinHost's
same-origin (scheme-included) redirect policy, by design (stops an upstream
steering a host-pinned request into a plaintext downgrade). Separately,
mct.gencat.cat's TLS setup fails the handshake with Node's fetch outright
(ERR_SSL_WRONG_SIGNATURE_TYPE) even though other HTTP clients complete it.
The loader builds the TransitCamera URL itself, over plain http, from
RenderService's sctidcam id, instead of registering the broken upstream
link — the only transport this host actually serves to this runtime.
Verified against the live feed and the production fetch path
(fetchCctvImageFromUpstream): all 135 cameras return real image bytes, 0
false positives. Ambient-card refresh cadence added to src/data/cctvLod.js
(3 minutes), matching the upstream update rate measured empirically (no
change inside 64s, changed within ~90-150s) — the same cadence already used
for Caltrans/TfL/Ontario/TxDOT.
CCTV_CATALONIA_MAX_SOURCES sets the cap and CCTV_CATALONIA_ENABLED=0 turns
the pack off. No overlap with any existing pack (checked catalog.js and the
git history for Spain/Catalonia/Barcelona coverage: none exists).
Verified before this commit: format, check:boundaries, unit suite (3692
tests), and build all green.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
São Paulo, Rio de Janeiro, Salvador, Recife, Porto Alegre, Brasília and
Curitiba run on the same PBSC platform as the existing Austin, Honolulu
and Chattanooga entries, so they need no new feed handling and no proxy
allowlist change — isAllowedGbfsHost already accepts *.publicbikesystem.net.
These are the layer's first stations outside the United States: 1,017
stations across the seven systems.
2026-09-13 23:01:04 -03:00
1689 changed files with 178830 additions and 27587 deletions
# networks you trust, and consider the GEV_RATELIMIT_* throttles above.
# HOST=localhost
# HOST=0.0.0.0
# Optional: comma-separated LAN hostnames to accept, in addition to localhost
# and IP addresses, for example GEV_ALLOWED_HOSTS=globe.lan,globe.internal.
# Binding HOST=0.0.0.0 alone does NOT trust every Host header; suffix and
# wildcard entries are ignored.
# GEV_ALLOWED_HOSTS=
# Optional: NASA FIRMS live active fires. SERVER-SIDE ONLY (never reaches the
# browser — the client polls the /api/firms proxy). Free key:
@@ -117,7 +158,7 @@ PORT=4173
# Optional: Live AIS vessels layer
# Browser fetch path. Keep same-origin so the private stream key stays server-side.
VITE_AIS_LIVE_API_URL=/api/ais-live
VITE_AIS_LIVE_API_URL=/api/vessels
# Client render cap. The server can keep a larger AISStream cache, but drawing
# every global vessel at once can overwhelm WebGL on laptops.
VITE_AIS_LIVE_MAX_ROWS=12000
@@ -145,7 +186,7 @@ AISSTREAM_API_KEY=
# Optional: TomTom live traffic flow (BYOK freemium — the free tier is 200,000
# Traffic Flow & Incidents tile requests per MONTH, not per day; no credit card
# required. Get a key at https://developer.tomtom.com). SERVER-SIDE ONLY:
# required. Get a key at https://my.tomtom.com/keys). SERVER-SIDE ONLY:
# the browser fetches same-origin /api/tomtom/* and never sees the key.
# Keyless fallback: the traffic layer runs its built-in simulation (white dots,
# hardcoded per-road-class speeds) — no key required for the layer to work.
@@ -169,6 +210,7 @@ AISSTREAM_API_KEY=
# CCTV_TFL_ENABLED=1 # set 0 to disable Transport for London JamCams
# CCTV_TFL_MAX_SOURCES=250 # cap cameras loaded from TfL
# CCTV_ONTARIO_ENABLED=1 # set 0 to disable Ontario 511 cameras
# ONTARIO_511_API_KEY= # free developer key: sign up at 511on.ca/my511/register, then request it at 511on.ca/developers/doc
# CCTV_ONTARIO_MAX_SOURCES=1000 # cap cameras loaded from Ontario 511
# CCTV_FINTRAFFIC_ENABLED=1 # set 0 to drop the Fintraffic (Finland) pack
# CCTV_FINTRAFFIC_MAX_SOURCES=300 # cap cameras loaded from the Fintraffic catalog
@@ -187,6 +229,33 @@ AISSTREAM_API_KEY=
# CCTV_CALGARY_ENABLED=1 # set 0 to disable the Open Calgary (Alberta) cameras
# CCTV_CALGARY_MAX_SOURCES=220 # cap cameras loaded from the Open Calgary catalog
# CCTV_CALGARY_ROWS_URL= # override the Open Calgary catalog URL
# CCTV_CATALONIA_ENABLED=1 # set 0 to disable the Servei Català de Trànsit cameras
# CCTV_CATALONIA_MAX_SOURCES=150 # cap cameras loaded from Catalonia (~130 exist today)
# CCTV_HK_ENABLED=1 # set 0 to disable the Hong Kong Transport Department cameras
# CCTV_HK_MAX_SOURCES=1100 # cap cameras loaded from Hong Kong (~1,013 exist today); min 8, max 1200
# CCTV_HK_CAMERAS_URL= # override the DATA.GOV.HK camera locations XML URL
# CCTV_KINGCOUNTY_ENABLED=1 # set 0 to disable the King County (Washington) road cameras
# CCTV_KINGCOUNTY_MAX_SOURCES=125 # cap cameras loaded from the King County catalog
# CCTV_WSDOT_ENABLED=1 # set 0 to disable the WSDOT (Washington State) highway cameras
# CCTV_WSDOT_MAX_SOURCES=250 # cap cameras loaded from the WSDOT catalog, nearest Seattle/Spokane first; min 8, max 1200
# CCTV_VEGVESEN_ENABLED=1 # set 0 to disable the Statens vegvesen (Norway) road cameras
# CCTV_VEGVESEN_MAX_SOURCES=900 # cap cameras loaded from Statens vegvesen (default loads all ~850 live; max 1000)
# CCTV_VEGVESEN_VIDEO=1 # set 0 to show stills instead of live HLS for Statens vegvesen video cameras
# CCTV_VEGVESEN_HEIGHTS=1 # set 0 to skip Kartverket ground heights for Statens vegvesen cameras (flat prior instead)
# CCTV_VEGVESEN_URL= # override the Statens vegvesen catalog URL
# CCTV_NYC_ENABLED= # off by default; set 1 only if you hold a NYC DOT data-sharing agreement ([email protected]) for the camera feed
# CCTV_NYC_MAX_SOURCES=250 # cap cameras loaded from the NYC TMC catalog (nearest borough cores first)
# CCTV_NYC_CAMERAS_URL= # override the NYC TMC catalog URL
# CCTV_TORONTO_ENABLED=1 # set 0 to disable the City of Toronto RESCU cameras
# CCTV_TORONTO_MAX_SOURCES=336 # cap cameras loaded from Toronto RESCU (clamped to 8–500)
# CCTV_TORONTO_LIST_URL= # override the Toronto RESCU camera list URL
# CCTV_LITHUANIA_ENABLED= # off by default; set 1 to load Via Lietuva / Eismoinfo road cameras (photo licence unconfirmed; see DATA_SOURCES.md)
# CCTV_LITHUANIA_MAX_SOURCES=400 # cap Lithuanian cameras; no API key required
# CCTV_FLORIDA_ENABLED= # off by default; set 1 to load FL511 (Florida) cameras (individual, personal use only; any other use, including a public deployment, needs FDOT's express written consent; see DATA_SOURCES.md)
# CCTV_FLORIDA_MAX_SOURCES=500 # cap cameras loaded from the FL511 catalog (nearest regional anchors first)
# CCTV_TAIWAN_ENABLED=1 # set 0 to skip Taiwan Freeway Bureau (MOTC) road cameras
# CCTV_TAIWAN_MAX_SOURCES=600 # cap Taiwan freeway cameras (1-2000); no API key required
# CCTV_TAIWAN_URL= # override the Freeway Bureau MOTC 2.0 CCTV.xml URL
# CCTV_MAX_SOURCES=4000 # catalog-wide ceiling, shared round-robin across packs
# CCTV_PREFER_AUSTIN=1 # prioritize Austin cameras in the registry
# CCTV_FORCE_AUSTIN= # force live default sources when a source-pack JSON is configured
@@ -194,23 +263,7 @@ AISSTREAM_API_KEY=
# Optional: comma-separated Overpass URLs; use only instances you run or pay for.
OVERPASS_UPSTREAMS=
# Optional: place-lookup endpoints. Lookups try bundled data first, then the
# endpoint set here, or the public service under its usage limits when unset.
# NOMINATIM_URL: search endpoint for last-resort place search and voice
# outlines (e.g. https://nominatim.example.org/search). A configured URL
# replaces the public service: nothing falls back to nominatim.openstreetmap.org,
# and an invalid value disables Nominatim. Unset uses the public
# nominatim.openstreetmap.org: one request at a time, 1.1 s apart, a short
# queue, cached answers (outlines 90 days, places 30 days, no-results 24 h in
# .gev-cache/nominatim) and a daily cap per install. Set it empty to disable.
# The public service's limit applies to all installs of this app together; the
# per-install cap bounds each install, not the total, so heavy or shared
# deployments should point this at their own server.
# NOMINATIM_URL=
# Public-instance requests per UTC day, outlines and search together. Not
# applied to your own NOMINATIM_URL.
# NOMINATIM_DAILY_CAP=50
# PHOTON_URL: keyless place search endpoint used by the browser
# (e.g. https://photon.example.org/api/). Unset uses photon.komoot.io; set it
# empty to disable.
# PHOTON_URL=
# Optional: who may frame the app in embed mode (?embed=1), as CSP
# frame-ancestors sources such as "https://example.com", or * for any page.
# Unset, no page may frame it. Other pages of the app can never be framed.
- The tracked-aircraft descriptor now publishes `verticalRateMps`, on both the
civil and military layers, so a consumer reading `getTrackedInfo()` no longer
has to reach past it into the raw poll record for climb/descent. Reported,
never derived: an aircraft that has not transmitted a rate reads as `null`
rather than a level-flight `0`.
- Add nine Ontario transit feeds: TTC, MiWay, Hamilton HSR, Durham Region,
YRT/Viva, Guelph, Burlington, Barrie and Kingston, each carrying its own
licence quote and attribution. A feed can now name its own modes, so Toronto's
trams read "Streetcar" while Helsinki's stay "Tram", and a feed whose route ids
are internal keys can translate them to the number a rider would say. TTC
vehicles take their mode from the fleet number first, so replacement buses on
streetcar routes read as buses and out-of-service streetcars stay streetcars.
- CCTV cameras with identified hardware show the model's datasheet specs. When a
source names a camera model found in the vendored CCTV Camera Database records
(CC0), the HUD meta line adds its resolution, night vision and PTZ capability.
A fixed single-lens model's horizontal FOV fills in when the source states no
FOV and the pose is not curated (`FOV n° (DATASHEET)`); a PTZ or varifocal
range is what the lens can do, not its current zoom, so those cameras keep the
estimate and show `SPEC FOV min–max° (CAPABILITY)` beside it (bassem chagra,
#452).
- Ship a web app manifest and raster home-screen icons, so adding the app to a
phone home screen or installing it on desktop uses the God's Eye View mark
instead of a blank tile. The icons are generated from `public/logo.svg` by
`node scripts/generate-icons.mjs` and committed, so a plain `vite build` needs
no image toolchain. The existing SVG favicon still serves the browser tab.
Manifest paths are relative to the manifest, so a build served under a path
prefix installs and launches inside that prefix.
- Cyclone advisories no longer go dark while NHC publishes an advisory ahead
of its nominal time: advisory and position times up to 90 minutes ahead of
the server clock are accepted (previously 5 minutes), so the whole feed is
not rejected for about half an hour each advisory cycle.
- Flights age out vanished aircraft again. OpenSky's routine position-less rows
no longer mark every snapshot partial, which had kept vanished and landed
aircraft at full opacity for up to five minutes; those rows retain only their
own aircraft, and a likely-landed one still leaves on the landed limit.
- Add WSDOT highway cameras (Washington State) as a keyless CCTV source pack:
WSDOT's official Travel Center camera layer, with frames registered only on
WSDOT's own image host (partner-hosted cameras such as Oregon DOT and City of
Seattle are skipped). Cameras with a single cardinal `CompassDirection` get
a high-confidence heading; `B` (both directions), `O`, and unset directions
use the shared id-hash fallback at low confidence. By default the 250
cameras nearest Seattle and Spokane load; `CCTV_WSDOT_MAX_SOURCES` sets the
cap (8–1,200) and `CCTV_WSDOT_ENABLED=0` turns the pack off (bassem chagra,
#644).
- Refuse still-image cameras on the CCTV media relay. `/api/cctv/media` is the
live-feed route, but it also relayed any registered still camera through an
ordinary fetch that followed redirects to any host, passed through whatever
content type came back and had no cap on the body, bypassing every protection
`/api/cctv/frame` applies to the same URL. Only HLS, MP4, WebM and MJPEG
feeds are relayed now; a still camera, or an unknown id, gets a JSON 404
without an upstream request and is served only through `/api/cctv/frame`.
The client already loaded stills through `/frame`, so nothing it requests
changes.
- Add Lithuania's public Via Lietuva / Eismoinfo road cameras to CCTV, with published coordinates, on-demand snapshots, attribution and bounded catalog requests. No API key is required. The pack is opt-in (`CCTV_LITHUANIA_ENABLED=1`) because its camera endpoints are the eismoinfo.lt website backend, not a service listed in Via Lietuva's open-data statement, so the licence for the photos is unconfirmed.
- Add King County road cameras (Washington State) as a keyless CCTV source
pack: one public King County DOT ArcGIS layer, fetched with an explicit field
list so its staff-name columns are never requested, and frames pinned to the
county's own image host and upgraded to HTTPS. WSDOT-owned rows are skipped,
and city-owned cameras the county hosts credit their owner. The layer
publishes no facing, so headings use the shared id-hash fallback at low
confidence. It does publish each camera's hardware, which is served on
`/api/cctv/sources` as `model` and `manufacturer`. `CCTV_KINGCOUNTY_MAX_SOURCES`
sets the cap and `CCTV_KINGCOUNTY_ENABLED=0` turns the pack off
(bassem chagra, #645).
- Add City of Toronto RESCU traffic cameras as a keyless CCTV source pack: the
~336 municipal cameras the Ontario 511 pack does not carry — downtown
arterials plus the Gardiner, DVP, Allen and Lake Shore. The list is the
City's own open data feed; frames are built from the documented
`loc####.jpg` convention and a digits-only camera number, so the frame proxy
is pinned to the portal origin by construction. The dataset publishes no
camera facing — its `D1`-`D4` fields describe static 2016 comparison images,
not the live pan-tilt-zoom frame — so headings use the shared id-hash
fallback at low confidence and are corrected with the calibration gizmo.
Carries the required Open Government Licence – Toronto attribution. Disable
with `CCTV_TORONTO_ENABLED=0`.
- Stop one retired bikeshare feed from reporting the whole layer as failed.
City feeds are fetched independently, but failure was recorded in a single
flag that only a successful *activation* cleared — and an already-loaded city
never activates again. A dead feed re-entering camera range therefore latched
LOAD FAILED permanently over a map still drawing every other city's stations.
Failure is now tracked per city: a city that fails beside others still
drawing marks the layer DEGRADED, polls that all fail over previously drawn
points read STALE without advancing freshness, and LOAD FAILED is reported
only when nothing is left to draw. A city that recovers clears its own
failure, a failure is dropped once its city leaves range or the camera rises
above the activation altitude, and late results from superseded requests are
ignored.
Repointed the three feeds whose operators had moved: Reddy Bikeshare (Buffalo)
to Social Bicycles, Tugo (Tucson) and Trinity Metro (Fort Worth) to PBSC.
Dropped CoGo (Columbus) and Broward B-cycle (Fort Lauderdale), whose systems
have wound down — CoGo's feed reports no stations and last moved a bike in
March 2025. The three repointed feeds were verified live.
- Add seven Brazilian Bike Itaú (Tembici) cities to the GBFS bikeshare registry:
São Paulo, Rio de Janeiro, Salvador, Recife, Porto Alegre, Brasília and
Curitiba (about 1,000 stations in total). These are the layer's first stations
in South America.
- Add Bike Share Toronto (1,069 stations) to the bikeshare registry. The system
publishes the same PBSC GBFS v2 feeds as the Austin, Honolulu and Chattanooga
entries, so it needs no new plumbing; a registry test now holds every city's
feed URLs against the proxy host and path allowlists, so a city the proxy
would refuse fails the suite instead of rendering an empty layer.
- GBFS v3 station names use English translations when available, then the first
usable translation or short name. Empty or malformed names no longer appear as
`[object Object]` or interrupt station loading; legacy string names remain
supported in the globe and bike-share tools. Thanks to
- Overpass and military-installation disk caches no longer serve answers from a
previously configured `OVERPASS_UPSTREAMS` instance after it is repointed.
- A Radio Browser mirror answering 204, 205 or 304, or a status outside
200–599, no longer crashes the local dev/preview server. Null-body statuses
get an empty body; anything else that cannot become a response fails that
mirror so the next one is tried.
- Snow flakes sway within a fixed range instead of drifting sideways faster the
longer the app runs; the Wind setting still scales the sway.
- The right rail ignores panels that other CSS hides, such as during an event
view, so they no longer force one-panel-at-a-time layout or collapse visible
panels. Panels the rail collapses itself are still restored.
- Coordinate searches refuse empty latitude or longitude components instead of navigating to an unintended location.
- Bing Aerial and Bing Labels request HTTPS tiles on HTTP localhost through
their ion-backed providers, retaining ion and per-tile attribution without
widening the Content Security Policy.
- Add New York City traffic cameras from the NYC DOT Traffic Management Center
as an opt-in CCTV pack. NYC DOT requires a data-sharing agreement for the
camera feed, so the pack is off unless `CCTV_NYC_ENABLED=1` is set by an
operator who holds one; otherwise no request reaches NYC DOT. NYC DOT's
requested disclaimer is in the Data attribution popover. The camera list is one keyless JSON request to
`webcams.nyctmc.org/api/cameras`; frames are stills synthesized from each
camera UUID on that same host, so a catalog field cannot steer the proxy
off-host. Offline cameras are dropped. Once enabled, the 250 cameras nearest
the five borough cores load and `CCTV_NYC_MAX_SOURCES` changes the cap. The catalog-wide ceiling is
unchanged; extra cameras share it round-robin with the other packs.
- Spanish interface (`es`), translated by @meirm (#602) and reviewed against the
current English.
- Interface localization, starting with Russian. UI text is marked with
`t()`, `tn()` (plurals) and `msg()` in code and `data-i18n*` in markup, keyed
by the English text itself; each language is one flat JSON catalog
(`src/i18n/locales/ru.json`). The language comes from `?lang=`, the language
picker (the globe button in the bottom-right corner), then the browser
language; English stays the default and the fallback. `npm run i18n`, also
run by `npm test`, fails on missing, unused or broken translations. Built on
the i18n foundation by @meirm (#601), with the Russian catalog started
from #312. It also fails on UI text that is not marked at all.
- Street Level counts unique tiles before ranking so overlapping dateline halves
in polar views do not waste slots in the 16-tile overview budget.
- Directions no longer shows a truncation notice for a route with exactly 200
turns, and a roundabout at the 200th turn keeps its exit distance.
- Weather records keep missing conditions unknown and refuse summaries without a measured temperature.
- Tool string length validation counts Unicode characters correctly, including characters outside the basic multilingual plane.
- Director timeline seeks cross the dateline along the short longitude arc.
- Keep unknown GBFS availability counts neutral instead of turning null or empty
values into zero bikes.
- Recent Imagery continues past malformed records in full catalog pages, respects the requested record limit, and reports results as truncated when that limit drops valid records.
- Regional articles retain unknown publication times when compact source dates do not identify a real calendar instant.
- Add Detroit (MoGo), Pittsburgh (POGOH), Aspen (WE-cycle) and Kona & Hilo
(HIBike) to the GBFS bikeshare registry, the layer's first stations in
Michigan.
- Treat explicitly current, complete empty vessel coverage as a successful update,
clearing obsolete contacts while retaining stale or incomplete snapshots.
- Custom Street Level provider switches survive share links and stored state
using URL-safe stable provider IDs and a shared boolean-switch codec; existing
Mapillary links keep their meaning. Explicit provider toggles reach durable
state without adopting unrelated live switch changes.
- Directions and Recent Imagery accept replaceable acquisition sources, including
route transport, imagery catalog, thumbnails, tile templates and attribution.
Explicitly removing either source leaves its layer unavailable; standalone
defaults preserve the existing providers.
- Catalog source availability no longer modifies layer objects; unavailable
tooltips use layer display names.
- Street Level with no providers uses the shared unavailable state and refuses
activation before initialization, while keeping its layer identity visible.
- Application source configuration accepts replacements and explicit removal.
Missing catalog feeds leave their layers unavailable without blocking startup
or making fallback requests; malformed supplied feeds remain configuration errors.
- Street Level providers are optional in application composition. Omitting a
Mapillary source no longer prevents startup; callers can inject a different
provider list or an empty list without changing layer identities or share links.
- Vector tile sources take their allowed tile origin from the configured
`tileJsonUrl`, so repointing only that URL at a mirror loads tiles from the
mirror or fails visibly instead of silently using OpenFreeMap. Thanks to
terrain, installation and map-feature queries, plus a situation brief, over
stdio to clients such as Claude Code, reading
from a running app. Tools are defined
once in `gods-eye-view/tools`, reuse the layers' source factories, and are
exposed through the protocol adapter in `gods-eye-view/tools/mcp`. The
catalog also covers weather maps, wind, recent imagery, submarine cables,
license plate reader cameras, datacenters and dams, the Bhote Koshi flood,
military awareness and links that open the app over an area with layers on.
Voice offers the same queries next to its app actions.
- Public Overpass instances are no longer used by default. Street Traffic
roads come from TomTom flow tiles, OpenFreeMap vector tiles, or both, chosen
@@ -182,7 +747,6 @@
geocode and admin-boundary fallback answers `region-timeout` after 3 s
instead of holding the reply.
- Transit and Directions rows repaint as soon as their data lands again:
`refreshLayerStats()` now lives on the layer lifecycle, not only on the
compatibility facade. `scripts/qa-radio.mjs` uses it instead of a private
@@ -240,34 +804,10 @@
caveat for the screen. The model is asked to answer in one short phrase.
- The weather and Recent Imagery panels can be opened by voice.
## Unreleased — voice outlines without Overpass
- Voice outlines work again without Overpass. Bundled US Census places and
Who's On First neighborhoods (lazy-loaded; `src/data/local_data/`) answer
first, then streets, grounds and pointed-at buildings from OpenFreeMap
tiles, then Nominatim for one explicit ask at a time. Traffic and outlines
share pending fetch/decode ownership and a projection-aware retained-byte
ceiling for the same immutable tile; consumer caches and request lifetimes
remain independent, and eviction or teardown retires queued idle projections.
Public-provider answers are cached and capped per
install. Public Nominatim requests now reserve their daily slot and paced
start atomically across processes sharing that installation's state file,
and fail closed when durable state is unavailable or invalid. Operator-selected
endpoints remain outside the public cap, spacing and state policy, and an
upstream cooldown is isolated to the endpoint that returned it. Anything else
stays a point marked "outline unavailable". Administrative Nominatim
outlines carry the selected record's verified country/admin-1/admin-2
identity; ambiguous, missing, contradictory or retired evidence is not
promoted into a verified level.
- Who's On First rebuilds accept only exact raw registry licence values in the
reviewed open-licence allowlist. New, unknown, restricted or noncommercial
source licences stay excluded until they are reviewed explicitly.
- New settings: `NOMINATIM_URL` (your own Nominatim instead of the public
one; empty disables it), `NOMINATIM_DAILY_CAP` (public requests per day,
default 50) and `PHOTON_URL` (browser place search; empty disables it).
## Unreleased — local receiver feeds
### Local receiver feeds
- The Local ADS-B layer also reads local 1090 MHz and 978 MHz UAT decoder
feeds: the `aircraft.json` that dump1090-fa, readsb, tar1090 or skyaware978
serves. Configure them server-side with `LOCAL_RECEIVER_FEEDS`
@@ -295,7 +835,7 @@
- Records carry `band` (`1090`/`978`) and `source` (`webusb`/`feed`).
- See `docs/LOCAL-RECEIVERS.md`.
## Unreleased — local RTL-SDR and Local ADS-B
### Local RTL-SDR and Local ADS-B
- Add a Local RTL-SDR card to the Radio panel. It connects a USB RTL-SDR in
desktop Chrome or Edge through WebUSB and receives broadcast FM (tune, seek,
@@ -320,7 +860,7 @@
- Add `@jtarrio/webrtlsdr` and `@jtarrio/signals` (Apache-2.0); see
`THIRD_PARTY_NOTICES.md`.
## Unreleased — weather review
### Weather review
- On 3D Tiles, draw a 4096×2048 detail window around the view on each
observed-weather shell except global infrared, sampled by the shell's own
@@ -408,6 +948,7 @@
Add feed provenance to analyst/view answers and HUD context while retaining existing response fields and runner ownership (Matt Van Horn, #347).
Analyst records for loaded satellites, datacenters and dams, with explicit bounded count/rank coverage (Matt Van Horn, #351).
- New Fire Perimeters layer (Events group): live NIFC WFIGS interagency
wildfire incident perimeters as ground-clamped polygons with a
containment-colored fire line, refreshed every 5 minutes from the public
@@ -586,26 +1127,26 @@ Analyst records for loaded satellites, datacenters and dams, with explicit bound
- Split application scene, controls, catalog, tools and HTML into reusable components; configure application request services and sources without changing global fetch. Preserve standalone markup and voice behavior. Explicit annotation navigation may resolve a distant named target.
## Satellite pass prediction
### Satellite pass prediction
- Bisect pass rise/set to ~0.2 s and fit peak elevation with a parabola.
- Mark passes visible from Earth-shadow and civil-twilight checks.
- Add `getNextSatellitePass(noradId, options)` for any loaded catalog satellite.
-`next_iss_pass` retains the next geometric pass and adds visibility metadata. `next_satellite_pass` adds bounded loaded-catalog name/NORAD lookup and optional visible-only filtering (Rehaan Delmotra, #451; maintainer adaptation).
## Voice component boundaries
### Voice component boundaries
- Separate voice controls, Realtime connection requests and the action runner.
- Allow compatible endpoints and server-selected models through construction options.
- Cancel pending token/SDP requests on Stop or teardown and reject expired secrets.
## Configurable geospatial services
### Configurable geospatial services
- Compose geocoding, place context and routes through independent providers.
- Allow compatible endpoint configuration without changing voice tools or annotation behavior.
- Isolate configured source caches and reject results after cancellation.
## ALPR camera locations
### ALPR camera locations
- Port Manjunath's (@manjunath22466) cyan camera badges, coral selection brackets,
gradient direction wedges and animated tactical labels into the reusable ALPR
@@ -625,32 +1166,32 @@ Analyst records for loaded satellites, datacenters and dams, with explicit bound
- Separate the request adapter, camera model, presentation, and instance lifecycle.
Source cancellation also guards late response bodies and rejects invalid query bounds.
## Release disabled infrastructure rendering
### Release disabled infrastructure rendering
- Remove built Data Center, Dam and Submarine Cable entities when their layers
are disabled, avoiding retained visualizer work and entity memory.
- Keep parsed datasets cached for re-enable; rebuild entities without refetching.
## Camera layer components
### Camera layer components
- Separate camera source requests, placement, frames, projection, cards and calibration.
- Own visibility listeners and pending initialization within each layer lifetime.
- Preserve existing camera catalogs, URL families, geometry and playback behavior.
## Traffic and bikeshare components
### Traffic and bikeshare components
- Separate traffic loading, animation, styling and lifecycle into factory-owned components.
- Give each flow source its own bounded decode cache and cancellation checks.
- Separate bikeshare registry, station requests, rendering, selection and proximity handling.
## Installation and context components
### Installation and context components
- Separate mapped-site requests, records, placement, selection and viewport lifecycle.
- Separate proximity queries, subject tracking, navigation/history, panel and direction rendering.
- Retain source and ground-floor ownership in standalone composition; reject malformed
installation snapshots and ignore failures from cancelled requests.
## Satellite and mission layer components
### Satellite and mission layer components
- Separate catalog loading, orbit calculations, display, tracking and interaction
into instance-owned satellite components.
@@ -658,20 +1199,20 @@ Analyst records for loaded satellites, datacenters and dams, with explicit bound
operations, retaining existing layer controls and satellite coordination.
- Cancel late mission source work and reject malformed launch snapshots.
## Fire layer components
### Fire layer components
- Split fire source loading, state, rendering, cards, selection and viewport work
into reusable components with application-owned scene services.
- Cancel late refreshes, retain good data after malformed responses, and preserve
selection identity without repeating a user-selection notification on refresh.
## Earthquake components
### Earthquake components
- Separate earthquake snapshot loading, record validation, and display ownership.
- Cancel pending earthquake refreshes on disable or destruction, retaining the
last good snapshot after malformed or failed refreshes.
## September 8, 2026
### September 8, 2026
Earthquake refreshes validate the complete feed and construct replacement entities before clearing the previous snapshot. Malformed rows and duplicate rendered IDs retain the last good entities, overlays, count and timestamp and report a malformed response; unknown magnitude is excluded from M2.5+ rendering.
@@ -682,8 +1223,7 @@ Launch payloads with missing records now say PAYLOAD DATA UNAVAILABLE. Missing n
This changelog records public product changes. For the authoritative description
of current runtime behavior, see [`docs/CURRENT-STATE.md`](docs/CURRENT-STATE.md).
## [Unreleased]
### Other changes
- Add ECMWF IFS model selection to Wind (#464, thanks @beneduzi), with model-scoped forecast-step caches, cancellation of replaced requests, and separate issue/valid timestamps.
@@ -703,7 +1243,7 @@ of current runtime behavior, see [`docs/CURRENT-STATE.md`](docs/CURRENT-STATE.md
- Separate submarine cable sources and rendering components, and export bundled geography lookup modules.
### Added
#### Added
- DISPLAY ▸ Draw: draw on the world by hand. Pick Area, Line or Pin, click the
vertices, double-click or press Enter to finish, label and colour it; Backspace
@@ -713,7 +1253,7 @@ of current runtime behavior, see [`docs/CURRENT-STATE.md`](docs/CURRENT-STATE.md
together. While you are drawing, the draw tool owns the pointer and no layer
selects what you click through (#235 — thanks @cora-fresh-labs).
### Fixed
#### Fixed
- Keep traffic-road bounds crossing the antimeridian monotonic and inside the
longitude range accepted by the Overpass request path, preserving the small
@@ -780,7 +1320,7 @@ of current runtime behavior, see [`docs/CURRENT-STATE.md`](docs/CURRENT-STATE.md
toggle. Camera departure cancels pending work, arrival checks the final view,
and superseded requests cannot keep a newer view loading.
### Added
#### Added
- Add MODIS NRT (Terra+Aqua, ~1 km) active fires to the FIRMS layer, sharing the
existing `FIRMS_MAP_KEY` and 30-minute cache.
@@ -908,9 +1448,9 @@ of current runtime behavior, see [`docs/CURRENT-STATE.md`](docs/CURRENT-STATE.md
- Extract shared surface keyboard handling for the welcome launcher and Provider
Settings, preserving Tab/Escape behavior and releasing the listener on teardown.
### Added
#### Added
### Security
#### Security
- The CCTV media route no longer forwards a client `Range` header to the upstream
camera host as it arrived. A single `bytes=` range is canonicalized and
@@ -941,7 +1481,7 @@ of current runtime behavior, see [`docs/CURRENT-STATE.md`](docs/CURRENT-STATE.md
- Cancel the active location lookup when its controls are disposed.
### Fixed
#### Fixed
-`DATA_SOURCES.md` states what the project does with camera frame content: a
successful upstream response is relayed as the provider served it, nothing in
@@ -1020,7 +1560,7 @@ of current runtime behavior, see [`docs/CURRENT-STATE.md`](docs/CURRENT-STATE.md
fetching, AIS records/tracks and shared request helpers; preserve existing
routes, local setup, fallback behavior and rendering.
### Added
#### Added
- **Directions layer** — keyless A→B directions without a geocoder or a
microphone (thanks @spcpza). The row's chips arm a globe click for A and B
@@ -1048,7 +1588,7 @@ of current runtime behavior, see [`docs/CURRENT-STATE.md`](docs/CURRENT-STATE.md
voice routes since launch, previously uncredited), with the OpenStreetMap
credit and the "fix the map" link the service's usage policy asks for.
### Changed
#### Changed
- The interface asks Google Fonts for only the icon glyphs it draws, instead of
the whole variable icon font, and no longer requests a second icon family that
@@ -1063,7 +1603,7 @@ of current runtime behavior, see [`docs/CURRENT-STATE.md`](docs/CURRENT-STATE.md
- Rename standalone browser startup to `src/standalone/` and add a Node-only
`gods-eye-view/build/vite` export with checked package ownership.
### Development
#### Development
- The CCTV launcher and preview-server tests resolve their temporary fixture
root through `fs.realpath`, so they pass on macOS, where the system temp
@@ -1097,7 +1637,7 @@ of current runtime behavior, see [`docs/CURRENT-STATE.md`](docs/CURRENT-STATE.md
infrastructure modules and their consumer tests. Package boundary checks keep
those exports separate from app startup and local Node services.
### Fixed
#### Fixed
- Reduce terrain-height timeouts when Re:Earth slows down. Batches are
sized against measured response latency on both browser and server to reduce
@@ -1188,7 +1728,7 @@ of current runtime behavior, see [`docs/CURRENT-STATE.md`](docs/CURRENT-STATE.md
and upstream error bodies; response statuses and cache fallback remain intact.
Includes the security fixes contributed by Tom-Neverwinter in PR #171.
### Fixed
#### Fixed
- Map Source keyboard opening retries focus until the selected tile is visible.
Leaving the disclosure, pointer interaction, or closing the tray cancels the
@@ -1230,7 +1770,7 @@ of current runtime behavior, see [`docs/CURRENT-STATE.md`](docs/CURRENT-STATE.md
that had since recovered. A miss is now cached only when every source
consulted actually returned a verdict.
### Added
#### Added
- Keyless place search. The LOCATION search box and the `fly_to_location` voice
tool now resolve place names through Photon (komoot, over OpenStreetMap) when
@@ -1250,6 +1790,13 @@ of current runtime behavior, see [`docs/CURRENT-STATE.md`](docs/CURRENT-STATE.md
to Puppeteer 25.10.0 and Sharp 0.35.4. Cesium remains on 1.138.0.
Browser QA awaits the new asynchronous executable-path lookup.
### Live CCTV video
- Live HLS video shares one decoder between the camera panel and projection,
with a DelDOT HTTPS source pack. Credit: Daniel Slay (@Danielslay86), PR #489.
- Maintainer adjustments bound sessions and downloads, remove disk/subprocess
remuxing, reject redirects, and clean up playback on switching or disabling.
## [0.1.1] — 2026-09-01 — Installation and live-data fixes
### Changed
@@ -1486,10 +2033,3 @@ represent previously published GitHub Releases.
## [0.1.0] — 2026-02-09
- Initial project version.
### Live CCTV integration candidate
- Live HLS video shares one decoder between the camera panel and projection,
with a DelDOT HTTPS source pack. Credit: Daniel Slay (@Danielslay86), PR #489.
- Maintainer adjustments bound sessions and downloads, remove disk/subprocess
remuxing, reject redirects, and clean up playback on switching or disabling.
- **Assembly lives in `src/app/`; standalone defaults live in `src/standalone/`.** UI controllers live in `src/ui/`, layer factories in `src/layers/`, portable sources in `src/sources/`, and application operations in `src/services/`. Existing `src/ui.js` and `src/data/<layer>.js` entries retain compatibility; new code belongs with its focused owner.
- **Assembly lives in `src/app/`; standalone defaults live in `src/standalone/`.** UI controllers live in `src/ui/`, layer factories in `src/layers/`, portable sources in `src/sources/`, and application operations in `src/services/`. Existing `src/data/<layer>.js` entries retain compatibility; new code belongs with its focused owner.
- Sources acquire records; renderers own Cesium resources. Import `gods-eye-view/layers/<family>/source` when only a source factory is needed. Common voice controls consume the session interface; protocol adapters own connection details.
- **Secrets stay server-side.** Anything needing a private key goes through a local proxy under `server/providers/`. The browser only ever sees the Google Maps key (which you restrict) and ephemeral tokens.
-`docs/CURRENT-STATE.md` is the authoritative runtime reference — read it first.
@@ -126,6 +126,7 @@ The highest-leverage places to jump in:
- ES modules, **2-space indent, single quotes, semicolons.**
- JSDoc on exported/public functions.
- Match the surrounding code — comment density, naming, and idiom.
- User-visible text goes through `t()`, `tn()` or `msg()`, or a `data-i18n*` attribute in markup, and `npm run i18n` must pass. See [docs/I18N.md](docs/I18N.md).
- Prefer small, reviewable commits. Conventional-commit-style prefixes (`feat:`, `fix:`, `perf:`, `docs:`) are appreciated but not required.
## Formatting and reusable components
@@ -149,7 +150,7 @@ ownership and adoption process.
1. Branch off `main`.
2. Keep `npm run build`, `npm test`, and `npm run test:track` green and avoid new console errors, plus the [feature gate](#feature-regression-gates) for the area you touched.
3. If you change runtime behavior, update `docs/CURRENT-STATE.md`and `CHANGELOG.md` in the same PR.
3. If you change runtime behavior, update `docs/CURRENT-STATE.md`in the same PR, and add your changelog entry as `changelog.d/<pr-number>-<slug>.md` instead of editing `CHANGELOG.md` — see [changelog.d/README.md](changelog.d/README.md). Distinct filenames don't conflict; a release assembles the directory into `CHANGELOG.md`. A documentation claim that needs a regression test should assert against `docs/CURRENT-STATE.md` or `README.md`, which a PR still edits directly.
4. If you add or change a data source, update [DATA_SOURCES.md](DATA_SOURCES.md) with its license and attribution. **Don't add data you don't have the right to redistribute** — fetch it at runtime instead.
5. Describe what you changed and how you verified it (screenshots welcome for anything visual).
| **NOAA nowCOAST observed weather** (`nowcoast.noaa.gov/geoserver/observations/{weather_radar,satellite}/ows`, WMS 1.1.1) | Rain radar (MRMS base reflectivity, contiguous US, about 4-minute updates) and Satellite clouds (GOES-19/18 Band 14 regional infrared, about 5-minute updates; global infrared mosaic, hourly). Keyless; layer metadata refreshed every 2 minutes, exact-time images cached for up to 24 hours | [NOAA disclaimer](https://oceanservice.noaa.gov/disclaimer.html) | "NOAA nowCOAST · NWS/OAR MRMS radar; NESDIS GOES and global satellite partners" |
| **NOAA nowCOAST lightning density** (`nowcoast.noaa.gov/geoserver/observations/lightning_detection/ows`) | Lightning density: 15-minute accumulated strike density on an approximately 8 km grid, Americas and Pacific. Keyless; layer metadata refreshed every 10 minutes, exact-time images cached for up to 24 hours | NOAA Level-5 derived product from Vaisala NLDN/GLD360; the [product description](https://ocean.weather.gov/lightning/lightning_pdd.php) permits public distribution of this density product, not of raw Vaisala detections | "NOAA/NWS nowCOAST; derived from Vaisala NLDN/GLD360" |
| **NOAA NHC / CPHC advisories** (`www.nhc.noaa.gov/CurrentStorms.json` + the `mapservices.weather.noaa.gov` tropical weather summary MapServer) | Cyclone advisories: storm positions, forecast tracks, points and cones for the Atlantic and eastern/central North Pacific. Keyless through `/api/cyclones`; cached for 5 minutes | [NWS public-data terms](https://www.weather.gov/disclaimer); no endorsement implied | "NOAA/NWS NHC / CPHC" |
| **Google Map Tiles API** (Photorealistic 3D Tiles) + Places/Geocoding | The 3D globe, voice scene context, and on-demand nearby installation search | Google Maps Platform ToS (proprietary, your own key + billing) | "Google" / "Google Maps" logo — **shown in-app**, required |
| **adsb.lol point API** | Bounded live-flight fallback when OpenSky has no usable snapshot | ODbL 1.0 | adsb.lol contributors; `api.adsb.lol/v2/lat/{lat}/lon/{lon}/dist/{radius}` |
| **adsb.lol point API** | Bounded live-flight fallback when OpenSky has no usable snapshot; GNSS Integrity layer (aircraft-reported NIC/NACp and GPS-loss integrity facts, via `/api/gnss-integrity`, 60 s server cache) | ODbL 1.0 | adsb.lol contributors; `api.adsb.lol/v2/lat/{lat}/lon/{lon}/dist/{radius}` |
| **adsbdb** (`api.adsbdb.com`) | Aircraft type, model name and registration by ICAO hex for enriched flights; airline and origin/destination airports for the tracked flight's callsign | No licence or rate limit is published for the service. Its own credits: aircraft data from PlaneBase, ICAO-to-N-number conversion by Guillaume Michel, and flight route data that "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" | "adsbdb — api.adsbdb.com" (courtesy); route data credited to David Taylor, Edinburgh, and Jim Mason, Glasgow |
| **AISStream.io** | Live vessels (AIS) | Free, beta, no formal ToS; AIS is a public broadcast | "AISStream.io" (courtesy) |
@@ -31,18 +35,20 @@ How to read this:
| **The Space Devs — Launch Library 2 v2.3** | Recent launch, payload, stage, and recovery metadata for Space Missions (30d) | [The Space Devs terms of use](https://github.com/TheSpaceDevs/Tutorials/blob/main/faqs/faq_TSD.md#terms-of-use): data may be used and shared in any form; avoid forwarding it without added value; attribution is encouraged (not mandatory). [Official API limits](https://ll.thespacedevs.com/docs/): 15 unauthenticated calls/hour; optional token | "Launch Library 2 — The Space Devs" (courtesy attribution) |
| **Esri World Imagery** (ArcGIS Online tile service) | The keyless satellite basemap — the default landing when no Google/ion credential is configured, and the "Esri Satellite" map stack | [Esri Master Agreement](https://www.esri.com/en-us/legal/terms/full-master-agreement): the public World Imagery service is usable in public-facing apps with attribution; no key is required for this classic endpoint, but Esri governs and can change access — an app at scale should review current ArcGIS Location Platform terms | "Powered by Esri — Source: Esri, Maxar, Earthstar Geographics, and the GIS User Community" (provider carries the service's own credit line) |
| **USGS** | Earthquakes | U.S. public domain | "Data courtesy of the U.S. Geological Survey" |
| **Environment and Climate Change Canada** (MSC GeoMet OGC API) | Air quality (AQHI) — Canadian station observations | [ECCC Data Servers End-use Licence](https://eccc-msc.github.io/open-data/licence/readme_en/) (open, attribution required, no endorsement implied) | "Contains information licensed under the Environment and Climate Change Canada Data Servers End-use Licence" |
| **NASA GIBS** (WMTS tiles + Worldview Snapshots) and **NASA CMR** (granule catalog) | Recent Imagery layer: HLS S30/L30 (Sentinel-2, Landsat 8/9) and VIIRS true-colour tiles draped on the map, per-day thumbnails and PNG exports; the HLS granule catalog (dates, cloud, footprints) for a selected box | U.S. public domain (NASA data); keyless, browser-direct (CORS `*`); no bulk use — one 30-day catalog query per box, tiles bounded to the box, six tile requests in flight | "We acknowledge the use of imagery provided by services from NASA's Global Imagery Browse Services (GIBS), part of NASA's Earth Science Data and Information System (ESDIS)." — [gibs.earthdata.nasa.gov](https://gibs.earthdata.nasa.gov), [HLS](https://lpdaac.usgs.gov/products/hlss30v002/) |
| **NIFC WFIGS** (Wildland Fire Interagency Geospatial Services, ArcGIS feature service) | Fire Perimeters layer: current interagency wildfire incident perimeters, containment, and incident facts | U.S. public domain (interagency wildland-fire data published through the [NIFC Open Data portal](https://data-nifc.opendata.arcgis.com/)); fetched keyless through `/api/fire-perimeters` (5-minute server cache) with `exceededTransferLimit` paging | "Wildfire perimeters: National Interagency Fire Center (WFIGS)" |
| **InciWeb** (inciweb.wildfire.gov) | Per-incident "InciWeb ↗" card links to official incident pages (narratives, evacuation/closure notices) | U.S. government public incident information. Links are matched best-effort by incident name and state, with incident page origin and update times checked on selection. Same-origin `/api/fire-perimeters/inciweb` routes cache the catalog for 1 hour and publication metadata for 30 minutes; linked pages remain InciWeb content | "Incident information: InciWeb (inciweb.wildfire.gov)" |
| **Photon** (komoot) | Keyless place search — the fallback when no Google Maps key is configured, or when Google declines the Geocoding request | Service: free public instance, [fair use](https://photon.komoot.io) (no bulk/heavy use); underlying data: **ODbL 1.0** | "Photon (komoot)" |
| **Open-Meteo** | Current weather in the cockpit Local Info page and cockpit-local dynamic atmospheric effects | [CC BY 4.0 data licence and adjacent-link attribution requirement](https://open-meteo.com/en/licence) | Linked "Weather data by Open-Meteo.com" beside the displayed local data |
| **Google News RSS** | Primary locality-matched headlines in the cockpit Regional News page | [Google News Terms of Service](https://www.google.com/intl/en_us/terms_google_news.html) restrict use to personal, noncommercial use; linked articles remain third-party publisher content and retain publisher terms | "Google News RSS" plus each article's linked publisher/domain |
| **GDELT Project DOC 2.0** | Fail-soft fallback for location-matched cockpit headlines | [GDELT Terms of Use](https://www.gdeltproject.org/about.html#termsofuse): unrestricted academic/commercial/governmental dataset use, with citation and link required; linked articles retain publisher terms | "GDELT Project" plus each article's linked publisher/domain |
| **City of Austin Open Data** | CCTV camera catalog + frames | City of Austin Open Data Terms of Use | "City of Austin, TX — data.austintexas.gov" |
| **TxDOT ITS (its.txdot.gov)** | CCTV camera catalogs + frames, Texas districts | Public TxDOT traffic camera data | "Texas Department of Transportation" (courtesy) |
| **FL511 (Florida DOT)** | CCTV camera catalog + frames, Florida statewide | FL511 content, individual personal use only; any other use needs FDOT's express written consent ([terms](https://fl511.com/privacy#disclaimer)). **Off by default**; set `CCTV_FLORIDA_ENABLED=1` to enable | "Florida DOT — FL511.com" (courtesy) |
| **City of Tallinn (ristmikud.tallinn.ee)** | CCTV camera catalog + frames, Tallinn intersections | Public City of Tallinn traffic camera data | "City of Tallinn — ristmikud.tallinn.ee" (courtesy) |
| **Transpordiamet / Tarktee** | CCTV camera catalog + frames, Estonia road-weather cameras | Public Transpordiamet / Tarktee road camera data | "Transpordiamet / Tarktee — tarktee.transpordiamet.ee" (courtesy) |
| **Stadt Warendorf webcam** | Webcam frames, Marktplatz / Historisches Rathaus (`config/cctv_sources.warendorf.json`) | Public municipal webcam data | "Stadt Warendorf — webcam.warendorf.de" (courtesy) |
@@ -54,7 +60,24 @@ How to read this:
| **Fintraffic / Digitraffic (weathercams)** | CCTV camera catalog + frames, Finland | [CC BY 4.0](https://www.digitraffic.fi/en/terms-of-service/) — attribution REQUIRED | "Fintraffic / digitraffic.fi, license CC BY 4.0" |
| **DriveBC** (Government of British Columbia) | CCTV camera catalog + frames, British Columbia highways | [Open Government Licence – British Columbia](https://www2.gov.bc.ca/gov/content/data/open-data/open-government-licence-bc) — attribution REQUIRED | "DriveBC. Contains information licensed under the Open Government Licence – British Columbia" |
| **Open Calgary** (`data.calgary.ca`) | CCTV camera catalog + frames, Calgary | [Open Government Licence – City of Calgary](https://data.calgary.ca/stories/s/Open-Calgary-Terms-of-Use/u45n-7awa) — attribution REQUIRED | "Contains information licensed under the Open Government Licence – City of Calgary" |
| **Servei Català de Trànsit** (+ Ajuntament de Barcelona, Ajuntament de Terrassa) | CCTV camera catalog + frames, Catalonia | [Llicència oberta d'ús d'informació - Catalunya](https://web.gencat.cat/ca/generalitat/dades-indicadors/dades-obertes/llicencies) — attribution REQUIRED| "Servei Català de Trànsit"; partner-supplied cameras also name their owner |
| **King County Road Services** (`services.arcgis.com`, King County GIS) | CCTV camera catalog + frames, unincorporated King County WA road network | [KCGIS Center terms](https://kingcounty.gov/en/dept/kcit/data-information-services/gis-center/about/terms-conditions-copyrights) — attribution required; public ArcGIS layer | "King County Road Services" (partner-city cameras name their owner) |
| **WSDOT** (`data.wsdot.wa.gov`) | CCTV camera catalog + frames, Washington State highways | [WSDOT travel information disclaimer](https://wsdot.wa.gov/about/policies/travel-information-disclaimer) — public "low volume" feed; catalog cached, frames on WSDOT's own image host only | "Washington State Department of Transportation (Travel Center)" (courtesy) |
| **Hong Kong Transport Department / DATA.GOV.HK** | CCTV camera catalog + frames, Hong Kong traffic snapshot cameras | [DATA.GOV.HK Terms of Use](https://data.gov.hk/en/terms-and-conditions) — attribution REQUIRED | "Hong Kong Transport Department / DATA.GOV.HK" |
| **Statens vegvesen** (catalog `ogckart-sn1.atlas.vegvesen.no`, frames `kamera.atlas.vegvesen.no`, HLS `kamera.vegvesen.no`) | CCTV camera catalog + frames + live HLS video, Norway road cameras | [NLOD 2.0](https://data.norge.no/nlod/en/2.0) — attribution REQUIRED | "Contains data under the Norwegian licence for Open Government data (NLOD) distributed by Statens vegvesen" |
| **NYC DOT Traffic Management Center** (`webcams.nyctmc.org`) | CCTV camera catalog + frames, New York City five boroughs | NYC DOT requires developers to sign a [data-sharing agreement](https://www.nyc.gov/html/dot/html/about/datafeeds.shtml) for the camera feed. **Off by default**; enable only under your own agreement | "NYC DOT Traffic Management Center" + NYC DOT's required application disclaimer (in the Data attribution popover) |
| **City of Toronto Open Data** (`opendata.toronto.ca`) | CCTV camera catalog + frames, Toronto city streets | [Open Government Licence – Toronto](https://www.toronto.ca/city-government/data-research-maps/open-data/open-data-licence/) — attribution REQUIRED | "Contains information licensed under the Open Government Licence – Toronto" |
| **GBFS (Lyft / BCycle / Social Bicycles / PBSC)** | Bikeshare availability | Per-feed (attribution-only) | Credit the operator (e.g. Austin BCycle) + its `license_url` |
| **Bike Share Toronto** (`toronto.publicbikesystem.net`) | Bikeshare station availability, Toronto | Operator GBFS feed. No licence is published: the City's open-data package declares none and the feed has no `license_url`. Attribution is given to the operator. | "Bike Share Toronto (Toronto Parking Authority)" |
| **Bike Itaú / Tembici** (`*.publicbikesystem.net`, seven Brazilian cities, Santiago and Bogotá) | Bikeshare station availability: São Paulo, Rio de Janeiro, Salvador, Recife, Porto Alegre, Brasília, Curitiba, Santiago de Chile and Bogotá | Operator GBFS feeds. No licence is published: the feeds' `system_information.json` carries no `license_url`, operator or attribution fields. Attribution is given to the operator. | "Bike Itaú (Tembici)"; "Tembici Bogotá" |
| **Bicing** (`barcelona.publicbikesystem.net`) | Bikeshare station availability, Barcelona | Operator GBFS feed (PBSC), keyless. The feed publishes no licence; the City's own open-data copy of the same station data is CC BY 4.0 but needs a token. Attribution is given to the operator. | "Bicing, Barcelona City Council (B:SM)" |
| **BiciMAD** (`madrid.publicbikesystem.net`) | Bikeshare station availability, Madrid | Operator GBFS feed (PBSC), keyless. Madrid's open-data catalogue publishes this feed (`datos.madrid.es`, dataset `900021-0-bicimad-gbfs`, owner EMT Madrid) under [EMT's open-data terms](https://mobilitylabs.emtmadrid.es/sip/terms-of-use): commercial and non-commercial reuse, with attribution REQUIRED (section 5.2). | "Powered by EMT de Madrid", linking to `https://www.emtmadrid.es` |
| **Spanish city bikes on PBSC** (`*.publicbikesystem.net`: Zaragoza, Valladolid, A Coruña, Donostia-San Sebastián, Bilbao, Rivas-Vaciamadrid) | Bikeshare station availability: Bizi Zaragoza, BIKI, BiciCoruña, Dbizi, Bilbao Bizi, BicinRivas | Operator GBFS feeds, keyless. The feeds' `system_information.json` carries no `license_url`, and no operator or city publishes a licence for them; MobilityData's catalogue lists CC BY 4.0 without naming a source. Attribution is given to the operator. | "Bizi Zaragoza"; "BIKI, AUVASA (Valladolid)"; "BiciCoruña"; "Dbizi (Donostia-San Sebastián)"; "Bilbao Bizi, Bilbao City Council"; "BicinRivas, Rivas-Vaciamadrid City Council" |
| **AMBici** (nextbike, `gbfs.nextbike.net`) | Bikeshare station availability, Barcelona metropolitan area (e-bikes) | Operator GBFS feed, keyless. `system_information.license_id` is **CC0-1.0**; no attribution required, given anyway. | "AMBici, Àrea Metropolitana de Barcelona, operated by nextbike" |
| **Oslo / Bergen / Trondheim Bysykkel** (Urban Sharing, `gbfs.urbansharing.com`) | Bikeshare availability, Oslo, Bergen and Trondheim city bikes. Keyless GBFS; the proxy sends the `Client-Identifier` header the operators ask for | [NLOD 2.0](https://data.norge.no/nlod/en/2.0) — attribution REQUIRED ([Oslo](https://oslobysykkel.no/en/open-data/realtime), [Bergen](https://bergenbysykkel.no/en/apne-data/sanntid), [Trondheim](https://trondheimbysykkel.no/en/open-data/realtime)) | "Contains data under the Norwegian licence for Open Government data (NLOD) made available by Oslo Bysykkel, Bergen Bysykkel and Trondheim Bysykkel" |
| **Kolumbus / Farte Bysykkel** (Entur Mobility, `api.entur.io/mobility/v2/gbfs`) | Bikeshare availability, Kolumbus Bysykkel (Stavanger/Nord-Jæren) and Farte Bysykkel (Skien/Porsgrunn). Keyless GBFS; the proxy sends the `ET-Client-Name` header Entur requires and is limited to Entur's GBFS paths | [NLOD](https://data.norge.no/nlod/en/2.0) per the [Entur Mobility API docs](https://developer.entur.org/pages-mobility-docs-mobility-v2) — attribution REQUIRED | "Contains data under the Norwegian licence for Open Government data (NLOD) made available by Entur (Kolumbus Bysykkel, Farte Bysykkel)" |
| **MBTA / MassDOT** (cdn.mbta.com) | Transit layer live vehicles and up to 15 minutes of recently observed positions, Greater Boston | MBTA / MassDOT open realtime vehicle data, used for the live view and up to fifteen minutes of recently observed positions; published for developer use and not against their use. | Attribution to MBTA / MassDOT (courtesy text) |
| **CapMetro** (data.texas.gov) | Transit layer live vehicles, Austin TX | CapMetro open realtime vehicle data, used for the live view; published for developer use and not against their use. | Attribution to CapMetro (courtesy text) |
| **Metro Transit** (Metropolitan Council) | Transit layer live vehicles, Minneapolis–St Paul | Metro Transit / Metropolitan Council open realtime vehicle data, used for the live view; published for developer use and not against their use. | Attribution to Metro Transit / Metropolitan Council (courtesy text) |
@@ -62,6 +85,15 @@ How to read this:
| **Entur** (api.entur.io) | Transit layer live vehicles, Norway | Entur open realtime vehicle data, used for the live view; published for developer use and not against their use. The client sends the `ET-Client-Name` identifying header. | Attribution to Entur (courtesy text) |
| **TransLink** (Queensland Government) | Transit layer live vehicles, South East Queensland | TransLink / Queensland Government open realtime vehicle data, used for the live view; published for developer use and not against their use. | Attribution to TransLink / Queensland Government (courtesy text) |
| **HSL** (realtime.hsl.fi) | Transit layer live vehicles, Helsinki | HSL (Helsinki Region Transport) open realtime vehicle data, used for the live view; published for developer use and not against their use. | Attribution to HSL (Helsinki Region Transport) (courtesy text) |
| **TTC** (bustime.ttc.ca) | Transit layer live vehicles, Toronto — surface vehicles (buses and streetcars) only | Toronto Transit Commission BusTime open realtime vehicle data, published through City of Toronto Open Data and used for the live view. The subway publishes no vehicle positions, so Lines 1/2/4 are absent rather than inferred. | Attribution to Toronto Transit Commission — City of Toronto Open Data, under the Open Government Licence – Toronto |
| **MiWay** (miapp.ca) | Transit layer live vehicles, Mississauga ON | MiWay open realtime vehicle data, published by the City of Mississauga under its Open Data Terms of Use, used for the live view. The licence is revocable and asks that the Terms of Use link travel with the data. | Attribution to MiWay — City of Mississauga Open Data, linked to the Terms of Use |
| **Hamilton HSR** (opendata.hamilton.ca) | Transit layer live vehicles, Hamilton ON | Hamilton Street Railway open realtime vehicle data, published by the City of Hamilton for commercial and non-commercial use. Not one of its 80 route ids is the number on the bus, so the route-number table in `src/data/transitFeeds.js` is derived from the City’s own published GTFS `routes.txt` under the same licence; two generations of id run at once and both are listed. | "Contains public sector Data made available under the City of Hamilton’s Open Data Licence" — wording prescribed by the licence |
| **Durham Region Transit** (drtonline.durhamregiontransit.com) | Transit layer live vehicles, Durham Region ON | Durham Region Transit open realtime vehicle data, published under the Region of Durham Open Data Licence v.1.0, which permits commercial use. | "Contains public sector information made available under The Regional Municipality of Durham's Open Data Licence" |
| **Guelph Transit** (glphtrpcloud.com) | Transit layer live vehicles, Guelph ON | Guelph Transit open realtime vehicle data, published by the City of Guelph on its open-data developer page and used for the live view. Its route ids are internal database keys, so the 33-row route-number table in `src/data/transitFeeds.js` is derived from the City’s own published GTFS `routes.txt` under the same licence. | "Contains information licensed under the Open Government Licence – City of Guelph" — wording prescribed by the licence |
| **Kingston Transit** (api.cityofkingston.ca) | Transit layer live vehicles, Kingston ON | Kingston Transit open realtime vehicle data, catalogued by the City of Kingston as open data and used for the live view. | "Contains information licensed under the Open Data Licence – City of Kingston" — wording prescribed by the licence |
| **Barrie Transit** (myridebarrie.ca) | Transit layer live vehicles, Barrie ON | Barrie Transit realtime data, published by the City of Barrie under a licence accepted by use of the data. The licence is revocable and requires no attribution. | Courtesy credit to the City of Barrie (the licence requires none, and its trademark clause is broad) |
| **YRT/Viva** (rtu.york.ca) | Transit layer live vehicles, York Region ON | York Region Transit realtime data, first-party York Region infrastructure under the YRT Open Data Licence v2.0, which permits commercial use and makes attribution optional. Viva route ids are named by colour from York Region’s published GTFS. | "Contains public transit Information made available under YRT’s Open Data Licence" — the licence’s own wording, given though not required |
| **Burlington Transit** (opendata.burlington.ca) | Transit layer live vehicles, Burlington ON | Burlington Transit realtime data under the City of Burlington Open Data Terms of Use, which permit any lawful purpose. The terms are revocable and must travel with the data, so the credit links them. Route ids are internal keys and are joined to the City’s published GTFS. | Credit to the City of Burlington plus a link to its Open Data Terms of Use, which the licence requires |
| **Radio Browser** | Geolocated internet-radio station directory and station-level tags | Public-domain directory data under PDDL 1.0; individual broadcaster stream terms apply | "Radio Browser" plus a link to the selected broadcaster |
| **Re:Earth Terrain** (Mapterhorn) | Terrain (keyless globe stacks — OSM etc. — + `/api/terrain/heights` ellipsoidal-height lookups) | Terrain mesh: CC BY 4.0; geoid: EGM2008 (NGA, public domain) | "Terrain (keyless globe stacks): Re:Earth Terrain / Mapterhorn (CC BY 4.0) / EGM2008 (NGA)" |
| **OSRM on the FOSSGIS routing servers** (`routing.openstreetmap.de`) | Street-following routes for the Directions layer and voice route annotations, via `/api/route` | [FOSSGIS routing usage policy](https://routing.openstreetmap.de/about.html): "Display the required attribution and display a link to 'fix the map'", "Use a valid user agent and, if applicable, a correct referrer", "One request per second max", "No scraping, no heavy usage". The full policy is the German [FOSSGIS Nutzungsbedingungen](https://www.fossgis.de/arbeitsgruppen/osm-server/nutzungsbedingungen/); FOSSGIS also states that the server may be embedded in your own pages but "eine gewerbliche Nutzung ist nur mit Einschränkungen erlaubt" (commercial use only with restrictions). Route data derives from OpenStreetMap (ODbL 1.0) | "Routing: OSRM on the FOSSGIS servers" + a "fix the map" link — shown in the Data attribution popover |
@@ -72,13 +104,59 @@ The optional Local ADS-B layer and the broadcast-FM receiver use radio you recei
### Notes on the live sources
- **Air quality (ECCC AQHI).** Keyless. Read from the MSC GeoMet OGC API (`api.weather.gc.ca`): the `aqhi-stations` catalog plus `aqhi-observations-realtime`. **Coverage is Canada only** — ECCC is the single registered provider, and the layer row and readout card both say so. Roughly 134 stations across ECCC's five administrative zones (Atlantic, Quebec, Ontario, Prairies & North, Pacific & Yukon). AQHI publishes hourly, so readings poll every 20 minutes and the station catalog, which changes a few times a year, is refetched only every 12 hours. Observations are requested with `latest=true`; without it the collection returns the full history, roughly 2,000 rows nationally. Readings older than six hours are dropped rather than displayed as current.
Readings are stored as `{ provider, scale, value, band }` rather than a bare number, because air-quality indices are not interchangeable: AQHI runs 1-10+ on health risk, the US AQI runs 0-500 on concentration breakpoints, and Europe's EAQI is a five-step category. Naming the index a value belongs to is what lets another network be added later without inventing a shared numeric meaning that does not exist.
AQHI is reported by ECCC as an integer from 1 to 10 with an open-ended "10+" above that. The API returns the underlying decimal (1.08, 2.32); the layer rounds to the published form rather than displaying a precision the index does not claim.
**Quebec is thinly represented**, and that is upstream rather than a filter here: the province runs its own Info-Smog program, so only a handful of Quebec stations appear in ECCC's national feed. The reporting station count also varies hour to hour as stations report or fall silent.
- **Via Lietuva / Eismoinfo (Lithuania).** Public road-camera catalogs and periodically updated still images from [Eismoinfo](https://eismoinfo.lt/), credited to Via Lietuva / eismoinfo.lt in the app. **Off by default.** The camera endpoints are the eismoinfo.lt website backend and are not among the services listed in Via Lietuva's [open-data statement](https://eismoinfo.lt/eismoinfo-backend/content?id=44), whose conditions are attribution and reuse on the same terms. Until the statement is confirmed to cover the camera photos, the pack loads only when an operator sets `CCTV_LITHUANIA_ENABLED=1`; with the flag unset, empty or any other value, the server makes no request to Eismoinfo. `eismoinfo-backend/camera-info-table` is joined by ID to `layer-static-features/VKR`. Without `lks=true`, the latter returns WGS84 `[latitude, longitude]`; the table's `x/y` instead use LKS-94 metres. Only the official HTTPS `/eismoinfo-backend/image-provider/camera/last?id=…` image endpoint is registered. Catalogs use the shared 15-minute cache; frames are fetched on demand and never bundled or archived. These are periodic snapshots, not continuous video. Camera headings are low-confidence estimates. No API key is required. `CCTV_LITHUANIA_MAX_SOURCES` defaults to 400.
- **Freeway Bureau, MOTC (Taiwan).** National freeway cameras from the Freeway Bureau's keyless [CCTV static table](https://tisvcloud.freeway.gov.tw/history/motc20/CCTV.xml) in the MOTC 2.0 standard, refreshed daily and fetched at runtime (never bundled). Freeway Bureau traffic data is published under the [Open Government Data License, Taiwan (Version 1.0)](https://data.gov.tw/license), which permits reuse, including commercial reuse, with attribution; the app credits the Freeway Bureau, MOTC, names the dataset and links the licence, as its attribution exhibit asks. The table publishes each camera's MJPEG stream on one of four official camera servers (`cctvn`, `cctvn5`, `cctvc`, `cctvs` under `freeway.gov.tw`). The frame proxy relays single images, so each published stream URL is mapped to the same server's single-JPEG path for the same camera number (`/abs2mjpg/bmjpg` → `/abs2mjpg/jpg`, `/live-view/mjpg/video.cgi` → `/live-view/jpg/image.cgi`); only those exact HTTPS pairs are registered, and the still path is not itself listed in the table. These are on-demand snapshots, not continuous video. Camera headings are low-confidence estimates. No API key is required. The table host does not answer from some networks outside Taiwan (the camera servers do); there the pack is empty, logs one warning and retries hourly. On by default; `CCTV_TAIWAN_ENABLED=0` skips the pack, and `CCTV_TAIWAN_MAX_SOURCES` defaults to 600 (bounded to 1–2000).
- **Google Maps Platform.** You supply your own API key and are bound by [Google's ToS](https://cloud.google.com/maps-platform/terms). Google Maps Content (tiles, geocodes, places) **may not be cached, stored, rehosted, or committed** — this app only ever uses it live, which is the compliant pattern. The "Google" attribution is displayed on the globe and must stay visible. Restrict your key (see [SECURITY.md](SECURITY.md)).
- **OpenSky Network.** Its license is **non-commercial**, and operational use of the REST API in a live product can require a prior written agreement with OpenSky — even for non-profit/government use. If you deploy this commercially, contact OpenSky for your own terms. The flights layer is a toggle and runs anonymously by default.
- **adsb.lol flight fallback.** When OpenSky is unavailable and no last-good OpenSky response exists, the server requests a cached, capped 250 nm adsb.lol point snapshot around the current camera subpoint. This is regional observed context, not worldwide completeness; provenance is exposed in the Flights stats/context row. Military ICAOs remain reconciled through the existing dedicated military registry rather than duplicated.
<a id="gnss-integrity"></a>
- **GNSS integrity (adsb.lol).** A separate keyless point request samples a 250 nm
circle around the screen centre, snapped once to whole degrees. This does not
reuse the Flights fallback cache. Cache lifetime is 60 s; 429 backoff is capped
at one hour and other upstream failures cool down for 15 s. Future access may
require feeder keys under the [API terms](https://github.com/adsblol/api).
Airborne decoded ADS-B v1/v2 aircraft with a known 0–60 s position age qualify,
including MLAT-positioned aircraft still broadcasting their own quality.
TIS-B/MLAT-derived NIC and NACp are ignored. GPS-loss evidence uses
`gpsOkLat/gpsOkLon` when present, otherwise the current known position.
The proxy strips identity metadata and tags its response with classifier
`gev-nic-nacp-v2`; rows carry only integrity facts.
describes the quality field origins and GPS-loss flag.
2.**GPSJAM aggregation:** score = `100 × max(0, bad − 1) / total`, with percent
bands < 2, 2–10, > 10. One bad aircraft is discounted. This is not the raw
share of bad aircraft. See the [GPSJAM FAQ](https://gpsjam.org/faq).
3.**GEV window:** 30 minutes over visited views, at least three aircraft per
0.5° cell. These bands were developed for daily totals; with fewer than ten
observations a cell can skip the middle band. Source timestamps date evidence
even through cache replays, and degraded evidence expires independently.
Medium/high cells report navigation-integrity anomalies. Interference is one
possible explanation, not a detected or located emitter. Avionics faults and
manoeuvring military trainers can also lower reported accuracy. Blank areas
can be outside coverage or lack enough observations. Attribution: adsb.lol
contributors, ODbL 1.0; GPSJAM for aggregation only.
- **adsbdb enrichment.** `/api/adsbdb` resolves an ICAO hex to aircraft type, model name and registration, and the tracked flight's callsign to an airline and an origin/destination pair. Fan-out is deliberately small because adsbdb is a free community API that publishes no quota: at most 4 requests in flight, dispatches dripped to roughly 5/s, and each key asked for at most once per session, with route lookups reserved for the tracked aircraft. The dev-server proxy caches each key for 24 hours in `.gev-cache/adsbdb.json`, negative answers included, so fresh entries survive server restarts. That gitignored cache remains local to the deployment and is not bundled with this repo. adsbdb publishes no general service license; its README says the route data may not be copied, published, or incorporated into other databases without the explicit permission of David J Taylor, Edinburgh. Deployers are responsible for obtaining any permission their use requires. Enrichment is best-effort and fail-silent — a flight that gets no answer keeps what the flight feed already gave it.
- **Launch Library 2.** `/api/launches` makes a server-side rolling-30-day query against the supported v2.3 detailed launch endpoint, caches successful responses for 15 minutes in memory and on disk, and serves the last successful response during a throttle or transient outage. Anonymous access is limited to 15 calls/hour; deployments can provide `LL2_API_TOKEN` for authenticated access. The Space Devs' published terms permit using and sharing the API data in any form, ask users not to forward it without adding value, disclaim complete accuracy, and encourage—but do not require—attribution. This app keeps a courtesy credit. Payload and stage/recovery records are shown only when supplied. Failed launches expose their source status and never receive fallback orbit geometry or a live/estimated marker. LL2 supplies launch context and event timing, not continuous ascent telemetry or live orbital state.
- **TfL JamCams.** The camera list comes from the keyless `api.tfl.gov.uk` endpoint (an optional `TFL_APP_KEY` raises its rate limit); frames come from TfL's public S3 bucket. The "Powered by TfL Open Data" attribution is required by TfL's terms and is registered in the Data attribution popover.
- **Ontario 511 CCTV.** The camera list comes from the keyless `511on.ca/api/v2/get/cameras` endpoint; frames are server-registered still images under `511on.ca/map/Cctv/`. The source is live-fetched at runtime and attributed under the Open Government Licence - Ontario.
- **Ontario 511 CCTV.** The camera list comes from the `511on.ca/api/v2/get/cameras` endpoint, which now requires a free developer key (`ONTARIO_511_API_KEY`: sign up at 511on.ca/my511/register, then request it while logged in at 511on.ca/developers/doc; throttled to 10 calls a minute). Without the key the pack is skipped. Frames are server-registered still images under `511on.ca/map/Cctv/`. The source is live-fetched at runtime and attributed under the Open Government Licence - Ontario. Access is governed by Ontario 511's Developer's Access Agreement (511on.ca/developers/resources): the data stays under the Open Government Licence - Ontario unless otherwise specified; key requests are reviewed case by case and ask for the intended use and any third parties that will receive the data; attribution by link to 511on.ca is encouraged, and the Ontario 511 logo may not be altered or used to imply endorsement (this app links and shows no logo); and the data is provided "as is", a disclaimer distributors should pass on, which the credit does.
- **DelDOT live video cameras.** The keyless `tmc.deldot.gov/json/videocamera.json` catalog supplies active cameras and official `https://video.deldot.gov/live/…/playlist.m3u8` links. The local server transiently buffers bounded MPEG-TS segments in memory and deletes them on session release or idle expiry. No recordings are retained. `CCTV_DELDOT_ENABLED=0` disables the pack. Public availability is not a blanket redistribution license; deployment beyond personal local viewing needs confirmation of applicable DelDOT terms.
- **Fintraffic weathercams.** The camera list is one keyless GeoJSON request to `tie.digitraffic.fi/api/weathercam/v1/stations` (809 stations / 2,275 presets), sent with the `Digitraffic-User: gods-eye-view` header the service asks clients to identify themselves with and with gzip; frames are stills on `weathercam.digitraffic.fi`, refreshed on each station's 600 s collection interval, which is also the client's ambient-still refresh cadence for this pack (the active camera keeps the usual 10-second refresh). One preset (a fixed camera view) is one camera; stations not `GATHERING` and presets not `inCollection` are dropped. Attribution is required by the CC BY 4.0 licence and is registered in the Data attribution popover. `CCTV_FINTRAFFIC_ENABLED=0` is the kill switch.
- **DriveBC highway cameras.** The camera list comes from the keyless `https://www.drivebc.ca/api/webcams/` endpoint served by the DriveBC.ca site ([bcgov/DriveBC.ca](https://github.com/bcgov/DriveBC.ca)); frames come from `https://www.drivebc.ca/images/<id>.jpg`, built from the numeric camera id. DataBC's [DriveBC HighwayCams](https://catalogue.data.gov.bc.ca/dataset/bc-highwaycams) catalogue entry publishes the same cameras under the Open Government Licence – British Columbia, but its CSV still lists the retired `images.drivebc.ca` frame URLs, which now return a placeholder. The licence's attribution statement is required and is registered in the Data attribution popover. By default the 250 cameras nearest Vancouver and Victoria load; `CCTV_DRIVEBC_MAX_SOURCES` changes the cap and `CCTV_DRIVEBC_ENABLED=0` turns the pack off. Some cameras in the feed are supplied by partners (TransLink, the City of Vancouver, the City of Surrey, Parks Canada / Alberta Motor Association and others); the feed's per-camera credit is carried onto the camera record and shown beside the provider in the CCTV panel.
@@ -88,16 +166,24 @@ The optional Local ADS-B layer and the broadcast-FM receiver use radio you recei
- **Warendorf webcam.** The Stadt Warendorf Marktplatz webcam, registered from the curated `config/cctv_sources.warendorf.json` (the Kreis Warendorf registration-office cameras from the contributing PR are indoor waiting rooms and were left out); only the municipal image host is proxied, frames are fetched at request time and never stored or redistributed. Disable with `CCTV_WARENDORF_ENABLED=0`.
- **Live Traffic NSW cameras.** The camera list is the keyless public `data.livetraffic.com/cameras/traffic-cam.json` feed (Transport for NSW open data, CC BY 4.0); frames are stills on `webcams.transport.nsw.gov.au`, and only that host is registered. That image host answers non-browser clients with an HTML placeholder, so the proxy identifies as a browser for that one host and as itself everywhere else. Frames are fetched at request time and never stored or redistributed. Disable with `CCTV_NSW_ENABLED=0`.
- **Open Calgary traffic cameras.** Keyless. The camera list is the public Socrata dataset `k7p9-kppz` on `data.calgary.ca`; frames are stills on the city's own `trafficcam.calgary.ca` host, and only that host is registered. Most rows publish an `http://` frame URL and the host redirects those to HTTPS, so URLs are upgraded and pinned to that origin before registration. Frames are fetched at request time and never stored or redistributed. The dataset publishes no camera facing: the `quadrant` field and the quadrant suffix on each camera name are Calgary's address grid, not a bearing, so headings use the shared id-hash fallback at low confidence and are corrected with the in-app calibration gizmo. The licence requires the attribution statement above. Disable with `CCTV_CALGARY_ENABLED=0`.
- **FL511 Florida cameras.** Off by default; set `CCTV_FLORIDA_ENABLED=1` to load them. The camera list is the keyless FL511 traffic camera layer on ArcGIS (`services.arcgis.com/.../FL511_Traffic_Cameras`), published from a Florida Division of Emergency Management account rather than by FL511 directly, and the service carries no licence text of its own. It is paged up to 2,000 rows at a time, ordered by the layer's object id and following ArcGIS's `exceededTransferLimit`, to cover the ~4,000-camera statewide set. Frames are stills on two Divas Cloud hosts (`images-dis.divas.cloud`, `snapshots.divas.cloud`); both are host-pinned and a row whose image resolves elsewhere is dropped. `CCTV_FLORIDA_MAX_SOURCES` caps the pack (default 500, anchored per region across the state). Each catalog page is read under a 2 MiB cap and redirects are refused. Frames are fetched live at request time and never stored or redistributed. FL511's terms (fl511.com/privacy#disclaimer) license its content for individual, personal use only; any other use, including a public deployment, needs FDOT's express written consent.
- **Catalonia (Servei Català de Trànsit) cameras.** The camera list is the keyless `https://www.gencat.cat/transit/opendata/cameres.xml` WFS/GML feed, fetched over https with redirects refused and the body read capped at 1 MiB, since the list decides ids, coordinates and which allowlisted frame path is proxied. It carries a `font` field naming the real publisher per camera; this pack keeps SCT's own highway cameras plus hotlinked Barcelona (IMI) and Terrassa municipal cameras (~130 total), each confirmed to return a real frame, so frame URLs are checked against a small host allowlist (`mct.gencat.cat`, `www.bcn.cat`, `emap.terrassa.cat`) rather than one pinned origin (a link carrying `user:pass@` userinfo or an explicit port, or pointing outside that host's frame directory, is rejected even on an allowed host; an SCT `sctidcam` must be a bare `.gif`/`.jpg` filename), and every non-SCT camera carries its real owner as a `credit` shown beside the shared `provider`. The feed carries no compass heading, so every camera loads at low heading confidence (same as TfL and Fintraffic). SCT's own feed link (`RenderService`) needed a workaround: it always 302s to a second endpoint (`TransitCamera`) on a Location the upstream hardcodes as plain http even when requested over https — refused by `fetchWithinHost`'s same-origin (scheme-included) redirect policy — and separately, `mct.gencat.cat`'s TLS setup fails the handshake with Node's fetch outright (`ERR_SSL_WRONG_SIGNATURE_TYPE`) even though other HTTP clients complete it. The loader therefore builds the `TransitCamera` URL itself, over plain http, from `RenderService`'s `sctidcam` id, rather than registering the (broken) upstream link — the only transport this one host actually serves to this runtime. Camera ids are built from the SCT camera file or the partner image path, not the feed's `fid` (which embeds an upstream server-start stamp and changes on every upstream restart), and rows the feed lists more than once collapse to one camera. The feed carries no camera status field, so a camera its publisher has taken offline still registers and shows the publisher's own "image unavailable" still (SCT and Barcelona both serve one with HTTP 200), the same as other packs without a status field. Frames are fetched at request time and never stored or redistributed. The feed itself states no licence; the pack relies on the Generalitat de Catalunya open-data portal's terms for this dataset (the licence and attribution above). Disable with `CCTV_CATALONIA_ENABLED=0`.
- **King County road cameras.** Keyless. The camera list is one public ArcGIS FeatureServer GeoJSON query (King County DOT Road Services, "King County and WSDOT Traffic Cameras"). The `outFields` list is explicit on purpose: the layer also carries asset-administration columns — including staff names — that are never fetched. Only live, active cameras with frames on the county's own `info.kingcounty.gov` image handler are registered (the feed writes `http://`; the handler serves HTTPS, so URLs are upgraded and pinned before registration). WSDOT-owned rows in the layer are skipped because their frames are WSDOT-hosted, outside the pinned image origin; partner-city cameras hosted on the county image handler (e.g. Newcastle) name their owner as the per-camera credit. The layer publishes no facing, so headings use the shared id-hash fallback at low confidence. Uniquely, the county publishes each camera's hardware `Manufacturer`/`Model` (Cohu 3950/3965, Bosch AUTODOME, Axis PTZ domes); both are carried on the source (and served by `/api/cctv/sources`) as `model` and `manufacturer` for identified-hardware consumers (vendor-generic strings such as `PTZ` name no model and are omitted). Frames are fetched at request time and never stored or redistributed. Disable with `CCTV_KINGCOUNTY_ENABLED=0`.
- **WSDOT highway cameras.** Keyless. The camera list is the Esri JSON layer `data.wsdot.wa.gov/travelcenter/Cameras.json` — the same layer WSDOT's own Travel Center map consumes (~1,700 point features, EPSG:3857 geometry converted to WGS84 on load; the body carries Windows-1252 bytes and is decoded as such). Only frames on WSDOT's own `images.wsdot.wa.gov` host are registered — the catalog also lists ~70 partner cameras (Oregon DOT, City of Seattle, lodges, airports) whose own terms apply, and those are skipped rather than proxied. WSDOT's layer metadata marks the feed for "low volume" use (see WSDOT's [travel information disclaimer](https://wsdot.wa.gov/about/policies/travel-information-disclaimer)). The catalog is fetched once per CCTV catalog refresh (every 15 minutes), and a camera's frame is fetched only when that camera is viewed; frames are never stored or redistributed. The live feed's `CompassDirection` is a single cardinal (N/E/S/W), `B` (both directions — most rows), `O`, or null. A single cardinal gives a high-confidence heading (the diagonals NE/SE/SW/NW are mapped too should they appear); `B`, `O`, null, and any other code carry no single facing and use the shared id-hash fallback at low confidence. By default the 250 cameras nearest Seattle and Spokane load; `CCTV_WSDOT_MAX_SOURCES` changes the cap (8–1,200) and `CCTV_WSDOT_ENABLED=0` turns the pack off. A keyed WSDOT Traveler Information API serves the same population for higher-volume use and is deliberately not required here.
- **Hong Kong Transport Department traffic snapshot cameras.** Keyless. The English camera list is the public DATA.GOV.HK XML at `static.data.gov.hk/td/traffic-snapshot-images/code/Traffic_Camera_Locations_En.xml`; frames are stills on `tdcctv.data.one.gov.hk`, and only that host is registered. Frame URLs are rebuilt from each camera key (`https://tdcctv.data.one.gov.hk/{KEY}.JPG`) rather than taken verbatim from the catalog. Stills were probed to answer directly on that origin with no redirect hop. Frames are fetched at request time and never stored or redistributed. The dataset publishes no camera facing, so headings use the shared id-hash fallback at low confidence and are corrected with the in-app calibration gizmo. By default nearly the full catalog loads (~1,013; pack cap 1,100), prioritized nearest Central, Tsim Sha Tsui, Sha Tin, Tsuen Wan, Tai Po, Fanling, Tuen Mun, Yuen Long and Tseung Kwan O so a lowered cap still spans Hong Kong Island, Kowloon and the New Territories. When the sum of packs exceeds the catalog-wide ceiling, shared round-robin keeps every lane represented. `CCTV_HK_MAX_SOURCES` (8–1200) changes the cap and `CCTV_HK_ENABLED=0` turns the pack off. The DATA.GOV.HK Terms of Use require attribution to the Government / Transport Department / DATA.GOV.HK.
- **Statens vegvesen road cameras.** Keyless. The camera list is one GeoJSON request to the OGC API Features view of the official DATEX 3.1 CCTV site table (`ogckart-sn1.atlas.vegvesen.no/ogc/features/v1/collections/datex_3_1:CctvSimple/items`), so no DATEX account is needed; frames are stills on `kamera.atlas.vegvesen.no/api/images/<cameraId>`, and only that exact path per camera is registered. Cameras the feed reports as faulty are dropped. About 135 working cameras also publish live HLS (`kamera.vegvesen.no/public/<cameraId>/manifest.m3u8`, MPEG-TS); those play as live video through the bounded in-memory HLS relay, with the still as the snapshot fallback, and only that exact manifest path per camera is registered. `CCTV_VEGVESEN_VIDEO=0` keeps every camera on stills. `orientationDescription` names the place a camera faces, not a bearing, so it goes into the label and headings use the id-hash fallback at low confidence. By default every live camera loads (~850); `CCTV_VEGVESEN_MAX_SOURCES` (8–1000) lowers the cap, keeping the cameras nearest Oslo, Bergen, Stavanger, Trondheim, Kristiansand, Bodø and Tromsø and `CCTV_VEGVESEN_ENABLED=0` turns the pack off. Frames are fetched at request time and never stored or redistributed. The NLOD attribution is registered in the Data attribution popover.
- **Kartverket ground heights for Statens vegvesen cameras.** The camera feed's points are 2D, so the loader asks Kartverket's keyless point-height API (`ws.geonorge.no/hoydedata/v1/punkt`, CC BY 4.0) for the terrain height at each camera. Requests carry 20 points: the API accepts 50, but above ~20 it answers the extra points from contour lines (`hoydekurver`, metres to ~20 m off) or not at all. Eight requests run at a time, about 36 for the whole country: a cold lookup took 6–13 s in testing (single requests 0.6–9.5 s). Answers are matched by the coordinates the API echoes and recorded only for points that request asked about. Over water the API returns the sea-floor depth (`dybdekurver` or `Havflate`), which is replaced by 0 m, the water surface; for the ~14 bridge cameras that is below the deck, but much closer than the sea floor or the flat prior. Heights are cached in memory by position for the life of the server, and points with no height (a few ferry quays and bridges, or a point a 200 answer leaves out) for 6 h, so the 15-minute catalog refresh only asks about new or moved cameras. The first failing request (a 408, 429 or 5xx, a network error, a request over 10 s, or an unreadable or oversized body) stops the lookup, with no retries, and no new lookup starts until the backoff ends (Retry-After when sent, else 30 min, longer than the refresh; bounded to 1 min–1 h). Any other 4xx marks that request's points as having no height and the lookup carries on. The whole lookup has a 30 s deadline; a lookup cut short by it resumes on the next refresh without a backoff. The lane waits at most 12 s, counted from the start of the loader, and only when more than 10% of the cameras have no answer yet; a few new cameras or expired misses are looked up in the background. A slower lookup keeps filling the cache, and the cameras it had not reached keep the flat prior until the next 15-minute refresh (about 190 of 865 on one slow first run). Only coordinates are sent, with the same User-Agent as the other CCTV requests. `CCTV_VEGVESEN_HEIGHTS=0` turns the lookup off. The Kartverket credit, with a note that heights over water are set to 0 m and heights are rounded to 0.1 m, is shown beside the Statens vegvesen entry in the Data attribution popover.
- **NYC DOT Traffic Management Center cameras.** Off by default. NYC DOT's [developer data feeds page](https://www.nyc.gov/html/dot/html/about/datafeeds.shtml) asks developers who want the camera feed to contact `[email protected]` and sign a data-sharing agreement. This project does not hold one that covers downstream installations, so the pack loads only when an operator who holds their own agreement sets `CCTV_NYC_ENABLED=1`; with the flag unset, empty or any other value, the server makes no request to NYC DOT. The endpoint needs no key. The camera list is the public `webcams.nyctmc.org/api/cameras` catalog (~980 cameras across the five boroughs); frames are stills at `webcams.nyctmc.org/api/cameras/<uuid>/image`, synthesized from a validated UUID rather than taken from the payload, and only that host is registered. Cameras the feed reports as offline are dropped. The catalog publishes no camera facing, so headings use the shared id-hash fallback at low confidence and are corrected with the in-app calibration gizmo. Once enabled, the 250 cameras nearest the borough cores load; `CCTV_NYC_MAX_SOURCES` (8–400) changes the cap. Frames are fetched at request time and never stored or redistributed. NYC DOT asks every application that uses its data to carry its disclaimer; it is registered verbatim in the Data attribution popover.
- **City of Toronto RESCU cameras.** Keyless. The camera list is the City's public `tmcearthcameras.json` on `opendata.toronto.ca` (dataset "Traffic Cameras", Transportation Services), served as JSONP and unwrapped with an anchored pattern. This is the municipal network the Ontario 511 pack does not carry: downtown arterials plus the Gardiner, DVP, Allen and Lake Shore. The list publishes no frame URL, so frame URLs are BUILT from the documented `CameraImages/loc####.jpg` convention and a strictly-digits camera number, pinning the frame proxy to the portal origin by construction; the list fetch refuses redirects and bounds the body it buffers. Frames are fetched at request time and never stored or redistributed. The dataset publishes no camera facing: the `D1`-`D4` fields are the directions of the *comparison* images (static reference stills captured in 2016 under a different path), not a bearing for the live pan-tilt-zoom frame, so headings use the shared id-hash fallback at low confidence and are corrected with the in-app calibration gizmo. A camera that is temporarily out of service still answers 200 with a byte-identical "Image Unavailable" placeholder rather than an error (about 6% of the network at any moment), so such a camera reads as healthy; the frame it shows is the agency's, not a synthesized one. The licence requires the attribution statement above. Disable with `CCTV_TORONTO_ENABLED=0`.
- **Camera frame content.** Public camera frames can contain people, vehicles and license plates, and the camera layer does nothing with them beyond putting them on the map. A successful upstream response is relayed as the provider served it: nothing in the camera pipeline blurs, redacts, enhances or restores it, and nothing there runs face, plate or object detection or otherwise recognises or classifies what is in the picture. (The ALPR layer described below maps where plate-reader cameras are, from locations contributors tagged in OpenStreetMap; it reads nothing from any camera frame.) When an upstream has no frame for a camera, what you see instead is a Google Street View still or the app's own placeholder card; the panel's source badge names which of them you are looking at, and a refresh that fails after a frame has arrived leaves the last good picture on screen rather than replacing it. The browser resamples each frame for display: onto a fixed 1920x1080 surface for the camera's projection plane in the scene, and onto a 192x108 canvas for the small ambient card. That scaling is the only change made to the picture, and the only pixels the camera layer reads back are a 64x36 downscale hashed to tell whether the picture has changed. Live streams are piped through; the still path reads the image into memory to pass it on — TxDOT wraps its JPEG in a JSON envelope, which is decoded there — and no frame is written to disk (the disk cache, `.gev-cache/`, holds feed JSON for other layers and never camera imagery). Outside that pipeline, the voice assistant is the one feature that sends imagery anywhere: when it answers a question about what is in view it may attach a downscaled screenshot of the rendered scene — including any camera plane on screen — to the request it sends to the voice model. Frames are fetched only from server-registered camera URLs — a client cannot hand the proxy a URL (see [SECURITY.md](SECURITY.md)). What the app does stays inside the line README draws: no named-person search, no face recognition, no tracking individuals.
- **Transit (GTFS-Realtime).** The proxy requests only registered operator feeds, validates redirects, bounds responses, and uses conditional requests, short snapshot caching and failure backoff. All seven registered regions are live-enabled; each can be disabled independently. Vehicles use delayed playback between reports, with report age and waiting states shown separately. Session-local selected trails are available for every feed. MBTA additionally retains up to 15 minutes of recently observed positions in the running proxy, so available history can survive a browser reload; server restart clears it. History reads never fetch upstream, and other feeds have no proxy retention. Nothing is archived to disk or collected in the background. Operator text credits are included as a courtesy and do not imply endorsement. Heights are aligned to work with Google 3D tiles; subway markers are shown at street level because these feeds do not supply depth.
- **Radio Browser.** `/api/radio/stations` discovers official API mirrors, makes bounded and coalesced healthy/geolocated HTTPS-station queries, caches the normalized public-domain directory for 45 minutes, and may serve the last good catalog for up to seven days during an outage. Refreshes must meet minimum accepted-query and station coverage before replacing a warm catalog; schema-valid responses whose rows all fail the product's health policy do not count as successful queries. A usable partial cold catalog is explicitly `DEGRADED`, and malformed or empty successful payloads are rejected atomically. Every directory and click-count request rejects redirects, validates all resolved addresses as globally routable (including reserved/documentation IPv4 and special/non-global IPv6 exclusions), and pins the TLS connection to a validated address. Only MP3/AAC non-HLS directory rows with public HTTPS stream targets are returned; favicons are intentionally omitted. Pressing play connects one browser audio element directly to the selected broadcaster and calls the directory's click counter through known-ID-only `POST /api/radio/click/:uuid`. GEV never proxies, caches, records, bundles, or redistributes audio. Radio Browser supplies station-level tags, not dependable current-song or upcoming-program metadata, so Radio filtering never claims either. Direct playback exposes the listener's IP address to the broadcaster, whose own stream terms apply.
- **Transit (GTFS-Realtime).** The proxy requests only registered operator feeds, validates redirects, bounds responses, and uses conditional requests, short snapshot caching and failure backoff. All sixteen registered regions are live-enabled; each can be disabled independently. Vehicles use delayed playback between reports, with report age and waiting states shown separately. Session-local selected trails are available for every feed. MBTA additionally retains up to 15 minutes of recently observed positions in the running proxy, so available history can survive a browser reload; server restart clears it. History reads never fetch upstream, and other feeds have no proxy retention. Nothing is archived to disk or collected in the background. Operator text credits are included as a courtesy and do not imply endorsement. Heights are aligned to work with Google 3D tiles; subway markers are shown at street level because these feeds do not supply depth. The TTC feed is BusTime, the surface system: it carries buses and streetcars and no subway at all. About four in ten of its vehicles report no route at all (unassigned or deadheading), and those are shown with the feed default rather than a route-established mode. It sends no ETag or Last-Modified, so its conditional requests fall through to the ordinary snapshot cache. Three Golden Horseshoe neighbours run alongside it — MiWay, Hamilton HSR and Durham Region Transit — each bus-only and each polled only near its own region. Durham Region Transit publishes no bearing on any vehicle, so those sprites take their heading from observed movement like any other feed that omits it. Guelph, Kingston and Barrie extend the same pattern eastward and north. Hamilton, Guelph, Burlington and YRT publish route ids that are not the number on the vehicle, so those entries carry a route-name table taken from each operator’s own published GTFS; any id missing from a table renders unchanged rather than blank. A label is never invented: YRT runs a route `9101` that appears nowhere in its published routes, so it stays as the feed sends it. Burlington’s is the one that cannot be shortcut: `351` is route 1 while `3510` is route 10, so the table is a real join rather than a trimmed prefix. GO Transit / Metrolinx is deliberately absent: its REALTIME feed requires an API key issued by manual review, which the keyless admission rule excludes (its static schedule download is keyless, but carries no vehicle positions). Barrie and YRT/Viva each invite acceptance of their licence through a form, and each licence also states that using the data is itself acceptance; this deployment relies on that clause, which is recorded in both entries. Grand River Transit in Waterloo is absent for a different reason: it answers, but its feed header was months stale and it publishes no per-vehicle timestamps, so it would render vehicles that look live and are not. Ottawa’s OC Transpo falls to the same keyless admission rule as GO Transit — its realtime feed requires an API key. London Transit and Thunder Bay Transit publish vehicle positions over plain HTTP with no HTTPS endpoint available. Every other Ontario system — Brampton, Oakville, Niagara, Milton, Windsor, Greater Sudbury, UP Express and a dozen smaller operators — publishes static GTFS schedules only, with no realtime vehicle positions to register. Between the nine included and these exclusions, every Ontario realtime vehicle-position feed catalogued in the Mobility Database is accounted for.
- **Radio Browser.** `/api/radio/stations` discovers official API mirrors, makes bounded and coalesced healthy/geolocated HTTPS-station queries, caches the normalized public-domain directory for 45 minutes, and may serve the last good catalog for up to seven days during an outage. Refreshes must meet minimum accepted-query and station coverage before replacing a warm catalog; schema-valid responses whose rows all fail the product's health policy do not count as successful queries. A usable partial cold catalog is explicitly `DEGRADED`, and non-array or empty successful payloads are rejected atomically. Individual structurally malformed rows are skipped rather than failing the whole query; the response carries their bounded `rejectedStationCount` (at most 10,000), which the client adds to its own rejected rows and reports as degraded coverage without discarding the accepted generation. Every directory and click-count request rejects redirects, validates all resolved addresses as globally routable (including reserved/documentation IPv4 and special/non-global IPv6 exclusions), and pins the TLS connection to a validated address. Only MP3/AAC non-HLS directory rows with public HTTPS stream targets are returned; favicons are intentionally omitted. Pressing play connects one browser audio element directly to the selected broadcaster and calls the directory's click counter through known-ID-only `POST /api/radio/click/:uuid`. GEV never proxies, caches, records, bundles, or redistributes audio. Radio Browser supplies station-level tags, not dependable current-song or upcoming-program metadata, so Radio filtering never claims either. Direct playback exposes the listener's IP address to the broadcaster, whose own stream terms apply.
- **Routing (OSRM / FOSSGIS).** `/api/route` proxies the public OSRM instances FOSSGIS e.V. runs for the OpenStreetMap community (car, bike, foot profiles). They are keyless and intended for fair, interactive use — the proxy rate-limits clients (60/min, 200/min total), caps route span and response size, caches each route for 10 minutes, and identifies itself in the User-Agent. Heavy or automated use must run its own OSRM. Routes are computed from OpenStreetMap data (ODbL); the Directions layer and voice routes never redistribute them. A route the router cannot find is reported as such; the Directions layer draws nothing in that case, and voice routes fall back to a clearly labeled straight line.
- **Re:Earth Terrain.** Keyless (no API key). Used two ways: (1) `src/mapStackController.js` swaps in a `Cesium.CesiumTerrainProvider` pointed at Re:Earth's `cesium-mesh/ellipsoid` quantized-mesh endpoint for globe stacks without a Cesium ion token (e.g. OSM), replacing a flat `EllipsoidTerrainProvider`; falls back to the flat provider if the endpoint can't be reached. (2) The server-side `/api/terrain/heights` proxy (disk-cached, serve-stale) resolves per-point ellipsoidal ground height for entity placement. Both are best-effort with a keyless-safe fallback (bundled EGM96 geoid math) if Re:Earth is unreachable.
- **OSRM on the FOSSGIS routing servers.** Keyless (no API key). Every route is requested by a person: clicking A and B in the Directions layer, or a voice "route from A to B". The server-side `/api/route` proxy sends an identifying `User-Agent`, pins the upstream host, accepts 2-12 coordinates, rejects any leg over 600 km or a total over 2,500 km, caps the buffered response at 8 MB, times the upstream out at 12 s, rate-limits per client, caches each route for 10 minutes, and coalesces identical requests that are in flight at the same time. Turn-by-turn maneuvers are asked for only when a caller wants them (`steps=1`), so the callers that do not read them send the smaller request. The "fix the map" link the policy asks for is in the Data attribution popover beside the OpenStreetMap credit.
- **Mapped installations.** OpenFreeMap military polygons use a bundled Overture/OSM name index; wide views show named points. Optional operator-configured Overpass and explicit Google Places searches keep their own source paths. Mapped data does not establish current activity or complete coverage.
- **Place search.** Named-place fly-to uses Google Geocoding when a Maps key is configured, then Photon (`PHOTON_URL`; unset uses the public instance, empty disables it), then `/api/geocode` (Nominatim search) when neither answers. `/api/geocode`and the voice outline route `/api/geocode/outline` share one Nominatim queue (`NOMINATIM_URL`; unset uses the public instance, empty disables it): one request at a time at least 1.1 s apart, an identifying `User-Agent` and `Referer`, a short queue that refuses a burst, a pause after 429/403/5xx answers (honouring `Retry-After` in full), at most one retry after a network failure, and a per-install daily cap on public-instance requests (`NOMINATIM_DAILY_CAP`, default 50). Processes using the same installation state file atomically reserve the next public start and daily slot; restarts share that count, spacing and any pause. Public access fails closed if the state file cannot be read, locked, written or semantically validated. This coordination does not span independent installations or state directories, and the public service's total limit still applies across all installs, so heavy or shared deployments should set their own `NOMINATIM_URL`. An operator-selected endpoint is explicitly outside the public cap, spacing and durable-state requirement; an upstream cooldown is kept with the endpoint that returned it rather than leaking across operator URLs. Answers are cached on disk (outlines 90 days, places 30 days, no-results 24 hours); failures are not cached. Outlines are looked up only for an explicit ask, after bundled data and any configured Overpass, and only results whose type suits the ask are used. Administrative outline responses expose country/admin-1/admin-2 identity only when the selected Nominatim row has a recognized, unambiguous address type consistent with its own name; numeric rank, ambiguous types, missing fields and contradictory names do not manufacture identity. It is not used for systematic or bulk queries.
- **Street and building outlines.** Voice street highlights, pointed-at buildings and grounds use the OpenFreeMap vector tiles the traffic layer already loads. Traffic and outlines share one bounded pending fetch/decode owner per immutable XYZ tile; materialized outline geometry is charged to the retained-byte ceiling and capped by coordinate count. Consumer result caches and request lifetimes remain independent, while eviction or explicit teardown cancels queued idle projections and releases their decoded owner. No Overpass request is made unless `OVERPASS_UPSTREAMS` is configured.
- **Place search.** Named-place fly-to uses Google Geocoding when a Maps key is configured, then Photon's public instance, then `/api/geocode` (Nominatim search) when neither answers. `/api/geocode`uses the shared Nominatim forward-search queue at no more than one request per second, sends an identifying `User-Agent` and `Referer`, caches answers for five minutes, shares one upstream call between identical searches already in flight, bounds the queue so a burst is refused rather than held, and drops a queued search whose caller has given up. It asks for one result per search and is not used for systematic or bulk queries.
- **Cockpit regional briefing.** `/api/regional-brief` rounds aircraft coordinates into 0.1° cache cells, caches results for five minutes, and resolves physical regions from bundled public-domain Natural Earth polygons without reverse-geocoding requests. Google News RSS is queried with the resolved locality/region first; GDELT is used only when that RSS query fails or is empty. Google's published Google News terms restrict that source to personal, noncommercial use, so commercial deployments must disable/replace it or obtain separate permission; GDELT permits commercial dataset use with citation. The Data attribution popover identifies the active headline sources; article links retain publisher attribution. Headlines are location-query matches, not verified incidents, risk rankings, or evidence that a location is safe. Empty, partial, stale, and unavailable source states remain distinct. Open-Meteo supplies current conditions independently of the news source. `WX OFF` disables cockpit weather rendering only; the Local Info briefing still fetches its source-backed weather values and displays the required linked Open-Meteo credit.
- **Dynamic weather presentation.** While cockpit mode is active, `/api/weather-effects` requests current Open-Meteo observations for the aircraft/camera location, rounds coordinates into 0.1° cache cells, caches results for five minutes, and may retain a stale observation for up to 30 minutes during a transient outage. WMO condition code selects the visual family; observed cloud cover, precipitation, visibility, wind speed, and wind direction bound its strength and motion. Missing or expired weather renders no synthetic atmospheric effect, and normal globe view never renders the weather overlay.
@@ -119,9 +205,10 @@ The [Bhote Koshi event pack](public/events/bhote-koshi-2026/README.md), under `p
| **DataSF Analysis Neighborhoods** (41 SF neighborhood polygons) | `neighborhoods/` | **PDDL 1.0** (public domain) | ✅ (no restrictions) | "City & County of San Francisco — DataSF" (courtesy — not legally required) |
| **Natural Earth countries, states and provinces** (260 admin-0/map units + 4,587 admin-1 polygons) | `natural_earth/` | **Public domain** | ✅ (no restrictions) | "Made with Natural Earth" (courtesy credit — not legally required) |
| **US Census Bureau counties** (3,235 county polygons) | `us_census_counties/` | **Public domain** | ✅ (no restrictions) | "U.S. Census Bureau" (courtesy — not legally required) |
| **US Census Bureau places** (32,629 city, town and CDP polygons) | `us_census_places/` | **Public domain** | ✅ (no restrictions) | "U.S. Census Bureau" (courtesy — not legally required) |
| **Who's On First neighborhoods** (61,755 polygons) | `wof_neighborhoods/` | Per source: CC BY, ODC-By, Open Government licences, CC0, PDDL, public domain (folder `ATTRIBUTION.md`) | ✅ (attribution) | "Neighborhood boundaries: Who's On First and contributors"; per-source credits in the folder |
| **Yale Bright Star Catalogue** (9,096 stars, sky background) | `sky/` | **Public domain** | ✅ (no restrictions) | "Yale Bright Star Catalogue (Hoffleit & Warren, via CDS)" (courtesy credit) |
| **CCTV ground heights** (3,445 cameras) | `cctv_ground_heights/` | Precomputed camera placement heights, aligned to work with Google Photorealistic 3D Tiles (folder README) | — | — |
| **CCTV Camera Database** model-enrichment extract v2.26.0 (17 camera-model records) | `src/layers/cctv/modelSpecs.js` | **CC0-1.0** (public domain dedication) | ✅ (no restrictions) | "Specs: cctv-database.com" (courtesy — not legally required) |
### ⚠️ TeleGeography is bundled but NonCommercial
@@ -147,6 +234,17 @@ commercial use with attribution and applicable database share-alike/access
obligations; it does not license the application code. Trimmed test-only tile
fixtures retain their provenance in `src/data/fixtures/README.md`.
### CCTV Camera Database (`src/layers/cctv/modelSpecs.js`)
The CCTV layer's hardware-model specs are a small verbatim extract (v2.26.0) of the
[CCTV Camera Database](https://github.com/ch-bas/cctv-camera-database), an open dataset of
camera models transcribed from manufacturer datasheets, dedicated to the public domain under
**CC0-1.0**. Only the records for hardware that the integrated public catalogs name (King
County, Sarasota County and Sioux Falls) are bundled, compiled into the module. Nothing is
fetched at runtime. The records describe camera products (optics, resolution, night vision)
only, never endpoints, stream paths or access data. CC0 requires no attribution; the CCTV HUD
credits it anyway as `SPECS: CCTV-DATABASE.COM` beside any spec line it renders.
@@ -73,7 +73,7 @@ Start with the included data sources, then add your own. Each layer is a separat
- **🔭 Satellite passes:** Ask by voice when any loaded satellite next rises over you: rise, peak and set times, and whether you can see it.
- **🔎 Analyst answers:** Count, filter and rank satellites, datacenters and dams by voice, alongside flights, ships, fires and quakes; answers say when a feed is stale or on a fallback.
- **🧭 Tilt and North Up:** Switch between straight-down and a 35° oblique, or put north at the top, while a tracked target stays centered.
- **🔍 Keyless search:** Type coordinates or a bundled city or landmark to fly there with no network request; other names use Google when configured, then Photon and Nominatim.
- **🔍 Keyless search:** Type decimal coordinates, a two-dimensional WGS84 `geo:` URI (for example, `geo:39.7392,-104.9903`), or a bundled city or landmark to fly there with no geocoder request; other names use Google when configured, then Photon and Nominatim.
- **📦 Shareable scenes:** Import, preview and share Director scenes as files or bundles, with camera anchors, authored moves and data packs.
- **🌊 Nepal flood scene:** Replay the Bhote Koshi flood: flood path, witness sources and before-and-after imagery (bundled data is non-commercial; see [DATA_SOURCES.md](DATA_SOURCES.md)).
@@ -117,6 +117,14 @@ locked dependencies, finds a free local port, and opens the app.
Version 8.2 fixes the launcher installation issue;
[details from the Pinokio maintainer](https://pinokio.co/posts/01m1m4p9xxm3qw7dnnpj2wr93g).
**Pinokio opens but the globe is very choppy?** Before treating that as a GPU
compatibility or minimum-hardware problem, copy the local server URL shown by
Pinokio and open the same URL in your normal hardware-accelerated browser. One
Pinokio 8.2.0 user reported severe stutter in Pinokio's embedded view that went
away when the same running server was opened in an external browser ([#365](https://github.com/bilawalsidhu/gods-eye-view/issues/365)).
That points to the embedded browser rather than the app server or your
hardware; it does not establish a minimum GPU requirement.
### Path 2 — Terminal / coding agent
Use **Node.js 24.x (24.14.0 or later) or 26.x**. The setup doctor warns about
@@ -145,6 +153,82 @@ See [docs/PERFORMANCE.md](docs/PERFORMANCE.md).
**macOS shortcut:**`./scripts/dev-fresh.sh` clears the Vite cache and pulls any
configured keys straight from the Keychain. It starts keyless too.
### Path 3 — Docker
No Node install needed; the container publishes the same dev server.
Open **`http://localhost:4173`** as above. Everything the app saves stays in the folder you cloned, so `docker compose down` never loses anything:
-`.env` - your API keys, whether saved from Provider Settings or typed in by hand
-`.gev-cache/` - downloaded map and satellite data, so later starts are faster
-`.gev-logs/` - logs
Back up that folder and you've backed up everything; delete `.env` to forget your keys. Source edits still hot-reload. The port is published to `127.0.0.1` only, so the default matches the native one.
The local MCP server and ChatGPT OAuth voice are not available under Docker; use an OpenAI API key for voice.
Publishing it wider is the [Sharing an instance](#-sharing-an-instance) opt-in, and it hands out **Provider Settings** too: the launcher trusts the container gateway as this machine, and a wider publish delivers everyone else from that same address. For an instance you mean to share, put the keys in `.env` by hand instead. [SECURITY.md](SECURITY.md) has the detail.
When you widen the publish, also set `GEV_KEY_SETUP_TRUSTED_PEERS=none` so the launcher trusts no gateway address.
```bash
GEV_KEY_SETUP_TRUSTED_PEERS=none docker compose up
```
Compose forwards `GEV_KEY_SETUP_TRUSTED_PEERS` and `GEV_ALLOWED_HOSTS` from your
shell or the project's `.env`; an exported shell value takes precedence. You can
also put `GEV_KEY_SETUP_TRUSTED_PEERS=none` in `.env` for a persistent setting.
Keep the `env`: bash treats `UID` as read-only, so a plain `UID=1026` in front of a command is silently ignored. If the container can't write the checkout, it says so at start. Docker Desktop on macOS and Windows maps ownership for you.
Behind a reverse proxy, put your keys in `.env` or pass them as environment variables - Provider Settings refuses proxied requests on purpose - and add the hostname you serve it on, for example `GEV_ALLOWED_HOSTS=globe.example.lan` (see [Sharing an instance](#-sharing-an-instance)).
</details>
<details>
<summary>IPv6-only networks, rebuilding, and the test suites</summary>
The launcher reads the IPv4 default gateway, so an IPv6-only Docker network
and the panel refuses every save. Put your keys in `.env` by hand there, or name
the gateway yourself with `GEV_KEY_SETUP_TRUSTED_PEERS` after reading
[SECURITY.md](SECURITY.md).
`node_modules` deliberately stays inside the image, so it survives in an
anonymous volume across restarts: after a `git pull` that changes dependencies,
recreate it with `docker compose down -v && docker compose up --build`. The
image also skips Puppeteer's Chromium download, which keeps it small but leaves
`npm run test:track` to the host — `npm run build` and `npm test` run fine in
the container.
</details>
### Then power it up — in the app, not in a file
Keys are upgrades, not prerequisites. When you want one, click the **POWER UP**
@@ -235,7 +319,17 @@ _Why cockpit mode exists: you're riding a real aircraft over real terrain — an
## 🎙️ Talk to It
> Voice needs an **OpenAI key**. Without one the entire app still runs — the mic button just reports voice is unavailable. The same key drives the **AI HUD summary**: a terse, five-word intelligence-style readout of the current view that regenerates as you move.
The standalone app offers **OpenAI** and **Gemini** in the GEV MIC provider chooser. OpenAI is the initial default; your choice is saved in this browser. To use Gemini, add `GEMINI_API_KEY` in Provider Settings or your local `.env` file, then select Gemini. `GEMINI_LIVE_MODEL` defaults to `gemini-3.8-live`. The permanent key stays on the local server, which issues a constrained, single-use session credential.
Changing provider ends the current conversation. If voice is on, the new provider starts with the same open-mic or push-to-talk mode; a held Space key is not carried over. Gemini uses the same map actions and captions, but has no OpenAI cost meter or MINI tier. When Gemini reports that its session has expired, start voice again for a new conversation. The provider chooser does not change the OpenAI-backed AI HUD summary.
> Voice can use an **OpenAI API key** or, on the same machine, an opt-in signed-in **ChatGPT/Codex OAuth** session. The API-key path remains the default. Selecting ChatGPT OAuth in Provider Settings reuses an existing local sign-in or starts the Codex browser sign-in flow and selects OAuth when it completes. The OpenAI key still drives the **AI HUD summary**: a terse, five-word intelligence-style readout of the current view that regenerates as you move.
The OAuth option is experimental. OpenAI documents ChatGPT plan usage for the [Responses API](https://developers.openai.com/siwc/token-sharing-open-source/models-and-inference); a supported third-party Realtime contract for reusing Codex tokens has not been confirmed. A successful token mint does not establish billing or support terms.
GEV reads Codex's file credentials but does not write or refresh them. It checks token shape and expiry, and reports expired sign-in as requiring re-authentication. The login button runs `codex login`, permits one pending attempt, and reports exit failures or a two-minute timeout. `CODEX_HOME` must match the directory that Codex uses. If `CODEX_AUTH_JSON` points elsewhere, sign in separately or fix that mismatch before using the button. Codex credential storage must use `auth.json`; Keychain-only and in-memory storage are not readable by GEV. On Windows, the login button may not be able to start an npm-installed Codex; run `codex login` yourself, then select OAuth. See [Codex credential storage](https://learn.chatgpt.com/docs/auth#credential-storage).
For a live voice check, run `node scripts/qa-voice-auth.mjs http://127.0.0.1:4189 oauth microphone.wav` against your local dev server. Use a WAV with three seconds of leading silence followed by a short spoken reply request. Replace `oauth` with `api-key` to check the API-key path. The check verifies audio input, a completed spoken reply, usage, resource cleanup, and reconnect. It saves screenshots and credential-free results under `.gev-logs/voice-auth/`.
Click **GEV MIC**, grant the microphone, and just talk. This is more than a voice-controlled remote:
@@ -243,12 +337,16 @@ Click **GEV MIC**, grant the microphone, and just talk. This is more than a voic
- **🎯 Entity Q&A.** Click any plane, ship, or datacenter and ask _"what's this?"_ It answers using the object's live telemetry.
- **👁️ Visual grounding.** At street level, it reads a viewport screenshot to identify legible signage and building names, and is instructed never to hallucinate labels.
- **🎬 Cinematic framing.** _"Show me the planes overhead"_ pulls the camera back, angles it, and frames the live traffic like a director.
- **🔒 Honest and secure.** The agent only confirms actions that succeeded. Your `OPENAI_API_KEY` never touches the browser; the client only gets a short-lived session token.
- **🔒 Honest and secure.** The agent only confirms actions that succeeded. Your `OPENAI_API_KEY` never touches the browser; the client only gets a short-lived session token. If you choose **USE CHATGPT OAUTH** in Provider Settings, the local server uses that sign-in only to mint the same kind of short-lived Realtime credential, and refuses the OAuth path for non-loopback clients.
Twenty-nine tools, four jobs — the commands below come straight from the product's voice test suite and tool playbook:
Thirty tools, four jobs — the commands below come straight from the product's voice test suite and tool playbook:
**🎥 Direct it** — drone-operator camera verbs:
“Rotate the globe” spins Earth to the right continuously; ask for left, a speed,
or stop. If it refuses, follow its reset-and-rotate or further-zoom guidance;
camera input stops rotation.
> 🗣️ _"Take me to Tokyo."_ · _"Orbit around this area slowly."_ · _"Draw the walking route from the Capitol to Zilker Park."_ → _"Fly the route we just drew."_ · _"Zoom out to a globe view."_
**🖊️ Annotate it** — a whiteboard over the real world:
@@ -279,27 +377,33 @@ _Ask for radio near anywhere and the globe starts broadcasting — every station
---
**💬 Or ask from Claude and Codex.** God's Eye View is also an MCP server: ask Claude Desktop, Codex or ChatGPT desktop to show a place, and the live globe opens right in the conversation. Setup takes a minute — see [docs/MCP_SETUP.md](docs/MCP_SETUP.md).
## 🛰️ What's on the Globe
Nineteen layers and map sources. **Seventeen have a keyless path.** Some offer additional capabilities with a provider key. (🟢 no key · 🟡 free key · 🔴 metered.)
Twenty layers and map sources. **Eighteen have a keyless path.** Some offer additional capabilities with a provider key. (🟢 no key · 🟡 free key · 🔴 metered.)
| 🗺️ **Map Stack** | Esri satellite imagery, Google Photorealistic 3D, OSM, plus additional ion-hosted stacks | Esri / Google / Ion / OSM | 🟢 Esri satellite + OSM · 🟡 ion-hosted Google 3D + world terrain · 🔴 direct Google + place search |
| ✈️ **Live Flights** | 11,000+ live aircraft + route history | OpenSky + adsb.lol | 🟢 (🟡 optional for more polling credits) |
| 🎖️ **Military Flights** | ADS-B military traffic in amber | adsb.lol | 🟢 |
| 🎖️ **Military Flights** | ADS-B military traffic in amber, emergency broadcasts in red | adsb.lol | 🟢 |
| 🛰️ **Satellites** | 838-object catalog, color-coded by class with a live legend — the **DENSE** chip drops in the whole Starlink shell | CelesTrak | 🟢 |
| 🌍 **Earthquakes** | Global seismic activity, last 24h | USGS | 🟢 |
| ◍ **Air quality** | Live AQHI on the published 1–10+ health-risk scale, with a Weather-panel card showing the band legend and the highest readings in view. **Canada only** — ECCC is the first registered provider | ECCC MSC GeoMet | 🟢 |
| 🚗 **Traffic** | Simulated vehicles on OSM roads. With TomTom, live flow speeds drive the simulation and congestion colors below ~8 km; individual vehicle positions are not live observations | TomTom + OSM | 🟢 simulation · 🟡 live flow speeds |
| 📹 **CCTV Mesh** | ~3,600 public cameras projected _into_ the 3D space — Austin · Texas (TxDOT) · California (Caltrans) · London (TfL) · Ontario (511) · Finland (Fintraffic) · British Columbia (DriveBC) · Estonia (Tallinn, Tarktee) · Delaware (DelDOT live video) · New South Wales (Live Traffic NSW) · Calgary. Positions are published; poses are estimated priors **you calibrate by dragging a gizmo on the camera itself** | City APIs | 🟢 |
| 📹 **CCTV Mesh** | ~4,000 public cameras (shared cap) projected _into_ the 3D space — Austin · Texas (TxDOT) · California (Caltrans) · London (TfL) · Ontario (511, free developer key) · Finland (Fintraffic) · British Columbia (DriveBC) · Estonia (Tallinn, Tarktee) · Delaware (DelDOT live video) · New South Wales (Live Traffic NSW) · Calgary · Norway (Statens vegvesen) · Toronto · King County (WA) · Washington (WSDOT) · Catalonia (Servei Català de Trànsit) · Hong Kong (Transport Department) · Taiwan (Freeway Bureau, MOTC), plus opt-in New York City (NYC DOT TMC, needs your own NYC DOT agreement), opt-in Lithuania (Via Lietuva / Eismoinfo, photo licence unconfirmed) and opt-in Florida (FL511, individual personal use only; other use needs FDOT's written consent). Positions are published; poses are estimated priors **you calibrate by dragging a gizmo on the camera itself** | City APIs | 🟢 |
| 📷 **Mapped ALPR Cameras** | License-plate-reader camera locations tagged by OpenStreetMap contributors, loaded one city-sized view at a time, with **SHOW NEAREST**. Locations and tags only: no plate data, no video | OpenStreetMap (incl. DeFlock mapping) | 🟢 |
| 📷 **Street Level** | Street-level photos behind one panel, one chip per provider: coverage on the streets, image cones, an embedded viewer with full-screen **EXPAND**. Mapillary, Google Street View, KartaView and Panoramax | Mapillary · Google · KartaView · Panoramax | 🟢 KartaView, Panoramax · 🟡 Mapillary token · 🔴 Google Maps key |
| 📻 **Radio** | Geolocated world radio with an **analog tuner** — drag the needle across up to 750 stations and the globe flies to each broadcaster | Radio Browser / broadcasters | 🟢 |
| 🚌 **Transit** | Live buses, trams, metros, trains and ferries with delayed playback between reports, selected-vehicle trails, and mode-coloured DETECT labels — Boston, Austin, Minneapolis, Helsinki, the Netherlands, Norway, South East Queensland | Operator GTFS-Realtime feeds | 🟢 |
| 🚌 **Transit** | Live buses, trams, metros, trains and ferries with delayed playback between reports, selected-vehicle trails, and mode-coloured DETECT labels — Boston, Austin, Minneapolis, Helsinki, the Netherlands, Norway, South East Queensland, and nine in Ontario: Toronto (surface vehicles; the subway publishes no positions), Mississauga, Hamilton, Durham Region, York Region, Guelph, Burlington, Barrie and Kingston | Operator GTFS-Realtime feeds | 🟢 |
| 🚲 **Bikeshare** | Live station availability | GBFS | 🟢 |
| 🧭 **Directions** | Click A and B on the globe for a street-following drive, walk or cycle route draped on the terrain with turn-by-turn steps — then FLY the camera along it. No key, no geocoder, no mic | OSRM on FOSSGIS servers (OpenStreetMap) | 🟢 |
| 🔥 **Active Fires** | Live NASA FIRMS detections, trailing 24h | NASA FIRMS | 🟡 |
| 🚀 **Space Missions** | Rolling 30-day launches with payload, stage, and recovery detail | Launch Library 2 | 🟢 (🟡 optional token raises the allowance) |
| 🚀 **Space Missions** | Rolling 30-day launches with payload, stage, and recovery detail | Launch Library 2 | 🟢 (🔴 optional paid token raises the allowance) |
| 🎖️ **Mapped Installations** | Viewport-bounded military-site context from community mapping — incomplete by nature, and labeled that way | OpenStreetMap | 🟢 |
| 🌬️ **Wind** | Animated 10 m forecast wind with an optional speed, temperature or pressure color field and a reading at map center | NOAA GFS / ECMWF IFS | 🟢 |
| 🌦️ **Observed Weather** | Rain radar (contiguous US), Satellite clouds (GOES regional or global infrared, Clouds only or Full image) and Lightning density (Americas and Pacific) on one history timeline | NOAA nowCOAST | 🟢 |
@@ -329,6 +433,16 @@ _The Space Missions layer replaying a Falcon 9 ascent — labeled `RECONSTRUCTED
---
## 📷 Street Level
> Street-level photos from any provider in one panel. This build ships **Mapillary** (a free client token), **Google Street View** (your Google Maps key), **KartaView** and **Panoramax** (no key).
**Setup.** At [mapillary.com/dashboard/developers](https://www.mapillary.com/dashboard/developers), register an application with **Read** access and copy its **Client Token** (`MLY|…`). Paste it into **POWER UP → Mapillary**, or set `MAPILLARY_CLIENT_TOKEN` in `.env` and restart. Without it the panel reads **KEY REQUIRED**. **Google Street View** uses `GOOGLE_MAPS_API_KEY` with the **Maps JavaScript API** enabled for it in Google Cloud; its chip starts off, since every panorama is billed to the key. **KartaView** and **Panoramax** need nothing: their public APIs are keyless.
Share links carry the provider switches and filters (`0.m.0` Mapillary off, `0.m.0_0.r.google-1` Street View on, `0.m.0_0.r.google-0*kartaview-1*panoramax-0` KartaView on and `0.m.0_0.r.google-0*kartaview-0*panoramax-1` Panoramax on, as the app writes them (`0.m.0_0.r.kartaview-1` and `0.m.0_0.r.panoramax-1` work too; a link that leaves out `m` keeps Mapillary, the default, lit), `0.p.p` panoramas only, `0.s.365` the last year). To add a provider, implement the contract in `src/layers/streetLevel/registry.js` and register it in `src/app/layers/streetLevel.js`.
## 🎖️ Field Missions
Once the basics click, run these:
@@ -378,11 +492,11 @@ How the globe handles live data:
```
src/
├── main.js # Bootstrap: Google 3D tiles, layer registration
| 🟡 | **Cesium ion** | 🗺️ Google Photorealistic 3D, world terrain, and additional ion-hosted imagery stacks. The free Community plan is for eligible individual, personal/non-commercial use and has quotas | [cesium.com/ion](https://cesium.com/ion) — use a public `assets:read` token and check current [pricing/eligibility](https://cesium.com/platform/cesium-ion/pricing/) |
| 🔴 | **Google Maps** | Direct Google Photorealistic 3D + Google place search ([Map Tiles API](https://developers.google.com/maps/documentation/tile)) | [Google Cloud Console](https://console.cloud.google.com/) — URL-restrict it |
| 🔴 | **OpenAI** | 🎙️ The voice experience + AI HUD summary. The mini model works; the standard model is noticeably smarter. Want Gemini or another provider behind the mic? PRs welcome | [platform.openai.com](https://platform.openai.com) — metered, see costs below |
| 🔴 | **Google Maps** | Direct Google Photorealistic 3D, Google place search and Street View ([Map Tiles API](https://developers.google.com/maps/documentation/tile); Street View also needs the Maps JavaScript API) | [Google Cloud Console](https://console.cloud.google.com/) — URL-restrict it |
| 🔴 | **OpenAI** | 🎙️ Voice + AI HUD summary through an API key. Provider Settings also offers **USE CHATGPT OAUTH** for cloud voice without removing the API-key option | [platform.openai.com](https://platform.openai.com) — API-key usage is metered, see costs below |
| 🟡 | **AISStream** | 🚢 Live global ships | [aisstream.io](https://aisstream.io) — free signup |
| 🟡 | **NASA FIRMS** | 🔥 Live active fires | [firms.modaps.eosdis.nasa.gov](https://firms.modaps.eosdis.nasa.gov/api/map_key/) — free |
| 🟡 | **TomTom** | 🚦 Live flow speeds and congestion colors for the simulated traffic layer | [developer.tomtom.com](https://developer.tomtom.com) — free tier available |
| 🟡 | **TomTom** | 🚦 Live flow speeds and congestion colors for the simulated traffic layer | [my.tomtom.com](https://my.tomtom.com/keys) — free tier available |
| 🟡 | **Mapillary** | 📷 Street Level coverage and photos. A public client token by design (see SECURITY.md) | [mapillary.com/dashboard/developers](https://www.mapillary.com/dashboard/developers) — register an app, copy its Client Token |

@@ -430,9 +545,11 @@ _What the TomTom key buys you: rush-hour density painted on the city — then di
| 🔴 | **Launch Library 2** | 🚀 Optional paid token for higher limits (🟢 no key needed for public access) | [thespacedevs.com/supportus](https://thespacedevs.com/supportus) |
Add these if you need higher polling allowances.
Leave `LL2_API_TOKEN` blank for standard public access. LL2 tokens are optional
paid Patreon upgrades for higher limits; there is no free API-key signup.
`npm run doctor` reports Node/npm readiness, the primary provider routes, and
where each configured provider was found without printing credential values.
@@ -494,7 +611,7 @@ Honest numbers, roughly, as of mid-2026 — always check the provider pricing pa
| **🟢 Most layers** | **$0, no signup.** OpenSky anon, USGS, CelesTrak, adsb.lol, city CCTV, Radio Browser, GBFS, Launch Library 2, transit feeds, OSRM routing, NOAA and ECMWF weather, OpenStreetMap ALPR mapping, bundled datasets. |
| **🟡 The free-key tier** | **$0 with a signup.** AISStream, FIRMS, TomTom, OpenSky, plus Cesium ion for eligible personal/non-commercial use. Provider quotas and eligibility still apply. |
| **🗺️ Google 3D tiles** | **Free through an eligible Cesium ion Community account within its quota; metered through a direct Google key.** Use the direct route for Google place search or commercial deployment, verify current provider terms, and set budget alerts where billing is enabled. |
| **🔴 OpenAI voice** | **The one that costs real money — so the app meters it for you.** Realtime audio runs a few cents per active minute; an evening of heavy use is single-digit dollars. A live session-spend readout sits next to the mic, with an STD/MINI model toggle, a $2 warning, and a **$5 hard cap that ends the session**. The voice context window is kept deliberately short too. |
| **🔴 OpenAI voice** | API-key sessions keep the live spend readout, STD/MINI model toggle, $2 warning, and **$5 hard cap**. ChatGPT OAuth is an explicit choice in Provider Settings. Its usage is tracked without an API-dollar estimate or spend cap. The voice context window stays deliberately short. |
Google's direct 3D route is surprisingly generous: the first 1,000 Photorealistic
3D Tiles sessions each month are currently free, and one root request supports
@@ -510,7 +627,7 @@ Everything above is the deliberately cheap baseline — enough to get a real tas
### 🔒 Sharing an instance
By default nobody else can reach your server — it binds to localhost. To share on your LAN, opt in explicitly (`npm run dev -- --host 0.0.0.0 --port 4173`, or `HOST=0.0.0.0 ./scripts/dev-fresh.sh` on macOS/Linux) — but know that ⚠️ **a LAN-visible server brokers your configured API keys to anyone who can reach it.** Set the per-IP throttles (`GEV_RATELIMIT_OPENAI_PER_MIN`, `GEV_RATELIMIT_GOOGLE_PER_MIN` — see `.env.example`) and, before anything else, **configure provider quotas, usage limits, and billing alerts**: app-level throttles are not billing caps, and a budget alert alone does not stop spending. Full threat model in [SECURITY.md](SECURITY.md).
By default nobody else can reach your server — it binds to localhost. To share on your LAN, opt in explicitly (`npm run dev -- --host 0.0.0.0 --port 4173`, or `HOST=0.0.0.0 ./scripts/dev-fresh.sh` on macOS/Linux). IP addresses work as they are; if you use a LAN hostname, add it with `GEV_ALLOWED_HOSTS=globe.lan`, since binding to `0.0.0.0` does not trust arbitrary Host headers. But know that ⚠️ **a LAN-visible server brokers your configured API keys to anyone who can reach it.** Per-IP throttles on the cost-bearing endpoints are on by default (`GEV_RATELIMIT_OPENAI_PER_MIN`, `GEV_RATELIMIT_GOOGLE_PER_MIN` — see `.env.example` to tune them), but before anything else, **configure provider quotas, usage limits, and billing alerts**: app-level throttles are not billing caps, and a budget alert alone does not stop spending. Full threat model in [SECURITY.md](SECURITY.md).
Provider Settings is disabled when the server is shared, so remote users cannot
| `OPENAI_API_KEY` | Server only | Browser fetches a short-lived **ephemeral** Realtime session token from `/api/realtime/token`; the real key never ships |
| `AISSTREAM_API_KEY` | Server only | Server holds the AISStream websocket; browser polls the same-origin `/api/ais-live` cache |
| OpenSky OAuth (`OPENSKY_CLIENT_ID/SECRET`) | Server only | Server mints + refreshes the token behind`/api/opensky` |
| `GOOGLE_MAPS_SERVER_API_KEY` (optional, #33) | Server only | Server calls Places (`/api/google/nearby-places`, `/api/google/text-search`) and the Street View fallback with this key; falls back to `GOOGLE_MAPS_API_KEY` when unset |
| ChatGPT sign-in through Codex (optional, experimental) | Codex's own `auth.json` (`CODEX_HOME`, default `~/.codex`); GEV reads it server-side and never writes or refreshes it | Only after **USE CHATGPT OAUTH** is selected in Provider Settings: the server mints the same ephemeral Realtime token with the ChatGPT access token, for loopback clients only |
| `AISSTREAM_API_KEY` | Server only | Server holds the AISStream websocket; browser polls the same-origin `/api/vessels` cache |
| OpenSky OAuth (`OPENSKY_CLIENT_ID/SECRET`) | Server only | Server mints + refreshes the token behind `/api/flights` |
| `GOOGLE_MAPS_SERVER_API_KEY` (optional, #33) | Server only | Server calls Places (`/api/google/nearby-places`, `/api/google/text-search`), Geocoding (`/api/google/geocode`, `/api/google/reverse-geocode`) and the Street View fallback with this key; falls back to `GOOGLE_MAPS_API_KEY` when unset |
### Two deliberately client-side keys — restrict them
### Three deliberately client-side keys — restrict them
These are designed to be used directly in the browser (like a Mapbox public token). They are injected into the client bundle via Vite's `define`, so they **will** be visible in browser devtools. Scope and restrict them rather than trying to hide them:
1.**Google Maps API key** — loads Photorealistic 3D Tiles directly and powers GEV place search. **Restrict it** (HTTP referrer + API restriction to the required Google APIs) in the Google Cloud Console. An unrestricted key in a public deployment can be abused and billed to you.
1.**Google Maps API key** — loads Photorealistic 3D Tiles directly and powers Street View (Maps JavaScript API; add it to the key's API restrictions). **Restrict it** (HTTP referrer + API restriction to the required Google APIs) in the Google Cloud Console. An unrestricted key in a public deployment can be abused and billed to you. Restrict it to the **Map Tiles API and Maps JavaScript API only**: a referrer restriction applies to the Maps JavaScript API and the client SDKs, not to the web services, so a key carrying Geocoding under one is refused with `API keys with referer restrictions cannot be used with this API`. Search does not need it — geocoding goes through the server proxies above (#363).
2.**Cesium ion token** (`CESIUM_ION_TOKEN`, optional — for ion-hosted Google Photorealistic 3D Tiles, Bing world imagery, and world terrain) — used as `Cesium.Ion.defaultAccessToken` client-side. Use a public **`assets:read`** token with **URL restrictions** for any hosted deployment. The Community plan has eligibility and usage limits; a public token is not a secret, but it can still consume the account's quota.
3.**Mapillary client token** (`MAPILLARY_CLIENT_TOKEN`, optional, Street Level) — used by the MapillaryJS viewer and Graph API lookups in the browser, and by the server's coverage-tile proxy, which sends it only to `tiles.mapillary.com` over HTTPS (an upstream redirect is refused and answered 502, never followed) and caches tiles in memory only. Mapillary client tokens are meant for browser use.
> The explicit browser `define` block in `build/vite.js` controls exactly what reaches the client: only these two keys. Everything else stays server-side.
> The explicit browser `define` block in `build/vite.js` controls exactly what reaches the client: only these three keys. Everything else stays server-side.
**Places and Street View never needed to be on that list** (#33): they're called from the server-side proxies in the table above, which use `GOOGLE_MAPS_SERVER_API_KEY` when it's set. Splitting it from the browser-exposed key lets each key's Google Cloud restriction actually match what it does — the browser key referrer-restricted to the APIs the client loads, the server key IP-restricted (never a referrer, since it never leaves your server) to Places + Street View Static — instead of one key that has to be either over-permissioned or broken for one of its two jobs. A single shared `GOOGLE_MAPS_API_KEY` still works if you don't split them; it just has to cover every API both sides use.
**Places, Geocoding and the Street View Static fallback never needed to be on that list** (#33, #363): they're called from the server-side proxies in the table above, which use `GOOGLE_MAPS_SERVER_API_KEY` when it's set. Splitting it from the browser-exposed key lets each key's Google Cloud restriction actually match what it does — the browser key referrer-restricted to Map Tiles + Maps JavaScript API, the server key IP-restricted (never a referrer, since it never leaves your server) to Places + Geocoding + Street View Static — instead of one key that has to be either over-permissioned or broken for one of its jobs. A single shared `GOOGLE_MAPS_API_KEY` still works if you don't split them, but then it cannot carry a referrer restriction at all: the web services refuse one, and search would be the half that breaks.
Never commit real keys. `.env` is gitignored; only `.env.example` (placeholder names) is tracked. On macOS `dev-fresh.sh` can read keys from the Keychain; plain Vite uses env vars or a local `.env`, and Pinokio uses its ignored app `ENVIRONMENT` file.
@@ -67,18 +69,42 @@ The data proxies under `server/providers/` are written so the browser cannot tur
- **No verbatim client headers upstream.** The CCTV media route is the one route that relays a request header (`Range`, for video seeking). It is parsed and canonicalized before it is forwarded: one `bytes=` range only, with every accepted form — explicit span, open-ended and suffix — bounded to the same 64 MiB ceiling the relay applies to a declared response body. Multi-range, malformed, inverted and non-`bytes` values are dropped and the request proceeds without a `Range`, as RFC 7233 §3.1 prescribes. Bounding the request bounds what is asked for: a response that declares no length — live streamed media, or a chunked body from an upstream that ignores the `Range` — has no ceiling. An upstream request the browser has stopped waiting for is cancelled rather than left running, whether the viewer leaves before the headers arrive or during the body.
- **Transit fetches registered feeds only.** `/api/transit/vehicles/<id>` resolves the id against `src/data/transitFeeds.js`; the browser never supplies a URL, and a feed that is registered but disabled does not resolve at all. Redirects are followed manually and each hop is validated against the feed's own https origin before it is requested, so an off-origin or downgraded hop is refused rather than contacted. Bodies are capped at 8 MB, the protobuf is decoded server-side under entity-count and string-length ceilings, a differential feed is refused, and snapshots live 15 s in memory with no disk cache. A per-feed admission limiter and a failure cooldown ladder bound what this process can ask of any operator.
- **Local receiver feeds are operator-configured and local-only.** `/api/local-receivers/aircraft` reads only the `aircraft.json` URLs in the server's `LOCAL_RECEIVER_FEEDS`; the browser never supplies an address. Each host must pass the shared tap address rule (`src/data/tapAddress.js`: loopback, RFC1918, `localhost`, `*.local`; no link-local, IPv6 or other names), the scheme must be http(s), the path must end in `aircraft.json`, and credentials, queries and fragments are refused. Invalid entries are logged at startup and never fetched. Reads use a 2 s timeout, refuse redirects, cap bodies at 2 MB and share one read per second; responses name each feed by band only and never carry upstream error text.
- **Validated response formats.** Proxied bodies are relayed only in the format the route serves. CCTV frames and the Street View fallback accept JPEG, PNG, GIF, WebP and AVIF only, sent under their canonical type; any other type falls through to the next fallback. Live CCTV media must match its feed kind (`video/mp4`, `video/webm`, or `multipart/x-mixed-replace` with a boundary) before it is relayed or the camera counts as healthy, and its redirects stay on the registered origin. GBFS bodies must parse as JSON and are re-serialized as `application/json; charset=utf-8`. Camera catalog URLs are pinned by parsed URL — scheme, exact host, no credentials, default port, and a normalized path inside the agency's image directory — and a redirect on a pinned host must stay inside the same directory. Every `/api/cctv` route uses the keyless cross-site checks described below, so cameras stay reachable behind a reverse proxy; a request carrying forwarding headers never reaches the key-bearing Street View fallback and gets the placeholder frame instead, as when no key is configured.
- **Consistent API response headers.** Every `/api` response, including errors, partial content, Host refusals and unknown routes, carries `Content-Security-Policy: default-src 'none'; sandbox` and `X-Content-Type-Options: nosniff` (`apiResponseHeadersPlugin` in `build/vite.js`, installed ahead of the Host check). A route that sets its own policy adds it as a further policy; it never replaces this one.
- **Response-size caps and timeouts** on proxied responses.
- **Sanitized errors** — internal error details are not echoed back to clients.
- **Coalesced OAuth refresh** and cached successful responses only (OpenSky).
- **Redacted debug logging.** The voice debug log (`.gev-logs/`, gitignored) strips API keys, bearer tokens, client secrets, and image data URLs before writing.
- **Cross-site gate on cost/log endpoints.** The cost-bearing (`/api/realtime/token`, `/api/openai/hud-summary`, `/api/google/nearby-places`, `/api/google/text-search`), quota-bearing (`/api/mapillary/*`, with a per-IP limit on tiles, and `/api/tomtom/*`, backed by its persistent daily tile budget) and log (`/api/realtime/debug-log`) endpoints refuse cross-site browser requests: a foreign or opaque `Origin`, a `Sec-Fetch-Site` other than `same-origin`/`none` (this is what blocks an `<img>`/navigation that carries no `Origin`), or any reverse-proxy/CDN forwarding header returns `403`. Non-browser loopback tools (the repo QA harnesses POST the token endpoint with no `Origin`) and the explicit `HOST=0.0.0.0` LAN opt-in keep working — the gate deliberately does not require a loopback remote address; that stricter requirement belongs only to the credential panel (`admitKeySetupRequest`). The Host check (`allowedHosts`, applied by `hostCheckPlugin` ahead of every route, since Vite's own check runs only after plugin middleware) rejects a rebinding `Host` before any handler runs, in every mode, and the gate is defense in depth behind it: the LAN opt-in keeps that check and accepts only IP addresses, `localhost` and names listed in `GEV_ALLOWED_HOSTS`. **Limit:** a name you list there is trusted like the app's own address, so list only names you control. The keyless terrain route (`/api/terrain/heights`) uses the same cross-site checks, minus the forwarding-header refusal: it guards only the per-client quota and point cache, not a key or a bill, and the map needs it to render behind a reverse proxy. The CCTV routes (`/api/cctv/*`) use the same keyless checks; their one key-bearing step, the Street View fallback, is skipped for any request with forwarding headers.
- **Full Content-Security-Policy.** The dev and preview servers send a real CSP (`script-src 'self' 'unsafe-eval' blob:` plus the YouTube, Facebook and X scripts the Nepal event's embedded media loads and the Maps JavaScript API that Street View runs on, with a matching `frame-src` for their players and posts: no inline script and no other script origin; `blob:` covers the object URLs Cesium's bundled workers bootstrap through in the built app), alongside the existing `X-Frame-Options: DENY` and `frame-ancestors 'none'`. `'unsafe-eval'` stays because Knockout, bundled inside Cesium's widgets package, resolves the global object with `eval` at load time and the globe does not initialize without it; styles permit `'unsafe-inline'` and Google Fonts because Vite's dev client injects `<style>` elements.
Allowing `maps.googleapis.com` in `script-src` also trusts its JSONP endpoints, which can bypass the script allowlist if an HTML-injection bug exists; this is an accepted limitation of loading the Maps JavaScript API, alongside the existing `'unsafe-eval'` allowance.
- **Full Content-Security-Policy.** The dev and preview servers send a real CSP (`script-src 'self' 'unsafe-eval' blob:` plus the YouTube, Facebook and X scripts the Nepal event's embedded media loads, with a matching `frame-src` for their players and posts: no inline script and no other script origin; `blob:` covers the object URLs Cesium's bundled workers bootstrap through in the built app), alongside the existing `X-Frame-Options: DENY` and `frame-ancestors 'none'`. `'unsafe-eval'` stays because Knockout, bundled inside Cesium's widgets package, resolves the global object with `eval` at load time and the globe does not initialize without it; styles permit `'unsafe-inline'` and Google Fonts because Vite's dev client injects `<style>` elements.
- **ChatGPT OAuth voice reuses the local Codex sign-in.** `/api/realtime/oauth-status`, `/api/realtime/oauth-login` and `/api/realtime/token?auth=oauth` answer only loopback connections, behind the same cross-site gate as the token endpoint; the login route also refuses a foreign `Origin`. None of them returns the access token. The login route runs `codex login` (or the executable named by `CODEX_BIN`) with an allowlisted environment that leaves out GEV's provider keys, keeps one attempt pending at a time and cancels it after two minutes or when the server stops. **Limits:** any program on this machine that can reach the server can mint Realtime sessions on the signed-in ChatGPT account, as it can with the API key; a local reverse proxy that hides the client's address defeats the loopback check; and OpenAI has not documented this use of Codex credentials for Realtime, so it may stop working or fall outside the account's terms.
## Network exposure — the operator threat model
The dev server is a **key broker**: every server-side key above is spendable by anyone who can send HTTP requests to it. That shapes the defaults:
- **Local-only by default.** `./scripts/dev-fresh.sh` (and the Vite config itself) bind to `localhost`, so only your machine can reach the server — and only local names are accepted (`allowedHosts` stays restricted, which also blunts DNS-rebinding tricks).
- **Local-only by default.** `./scripts/dev-fresh.sh` (and the Vite config itself) bind to `localhost`, so only your machine can reach the server — and only local names are accepted (`allowedHosts` stays restricted, which also blunts DNS-rebinding tricks). Binding to all interfaces keeps that restriction: IP addresses and `localhost` work, and LAN hostnames must be listed in `GEV_ALLOWED_HOSTS` (for example `GEV_ALLOWED_HOSTS=globe.lan`); suffix and wildcard entries are ignored.
- **LAN exposure is an explicit opt-in**: `HOST=0.0.0.0 ./scripts/dev-fresh.sh`. The launcher prints a prominent warning plus your LAN URL. Understand what opting in means: **every device on that network can drive the proxies and spend your OpenAI / Google / OpenSky / AISStream / TomTom / FIRMS quota** for as long as the server runs. Do this only on networks you trust.
- **App-level throttles (opt-in):** `GEV_RATELIMIT_OPENAI_PER_MIN` and `GEV_RATELIMIT_GOOGLE_PER_MIN` cap the cost-bearing endpoints per client IP per minute (over-limit requests receive a sanitized `429`). They are **per-IP, process-local, in-memory guards** — they reset on restart and are **not billing caps**.
- **App-level throttles (on by default):** `GEV_RATELIMIT_OPENAI_PER_MIN` (default `30`) and `GEV_RATELIMIT_GOOGLE_PER_MIN` (default `120`) — the values the Pinokio build already ships — cap the cost-bearing endpoints per client IP per minute (over-limit requests receive a sanitized `429`). Both defaults sit well above what the app itself generates; set either to exactly `0` to run unthrottled. They are **per-IP, process-local, in-memory guards** — they reset on restart, a caller with several addresses gets a bucket per address, and they are **not billing caps**.
- **Docker trusts one address, and only because the port is on loopback.**
Docker delivers the host's own browser to the container from the container
network's gateway, never from `127.0.0.1`, so a loopback-only Provider
Settings refuses the machine running it. `scripts/docker-start.mjs` therefore
sets `GEV_KEY_SETUP_TRUSTED_PEERS` to exactly that gateway address. The list
takes exact addresses only (no wildcards, prefixes or hostnames), is unset
under every other launcher, and relaxes none of the sharing, Host, Origin,
Content-Type or proxy-header checks. It is safe precisely because
`compose.yaml` publishes on `127.0.0.1`: **widen that mapping and every LAN
client arrives from the same gateway address, which hands them Provider
Settings too.** Treat publishing beyond loopback as the LAN opt-in above.
Compose forwards `GEV_KEY_SETUP_TRUSTED_PEERS` and `GEV_ALLOWED_HOSTS` from
the shell or project `.env` before the launcher runs. Set trusted peers to
`none` when widening the publish: it becomes an explicit empty list before
Vite loads configuration, so the gateway cannot regain Provider Settings
access. Unset or blank values preserve automatic gateway discovery.
- **Provider-side budgets are the real backstop.** For hard spend protection, configure limits where the money is: OpenAI platform usage limits, Google Cloud budget alerts + per-API quotas, and equivalent controls for any other keyed provider.
- **Pinokio LAN and Cloudflare sharing are refused.** The current supported
Pinokio release re-reads sharing state when an app registers its Open URL and
@@ -90,6 +116,15 @@ The dev server is a **key broker**: every server-side key above is spendable by
loopback only. Use a separately reviewed authentication proxy for remote
access and keep provider-side quotas as the spend backstop.
## MCP server and panel
God's Eye View's tools are also served to MCP clients: over stdio (`npm run mcp`) and at `/mcp` on the development and preview servers.
- **`/mcp` answers only direct local requests**: a loopback connection naming a loopback host on the port it reached, a browser `Origin` (when sent) from that same host, no proxy forwarding headers, and launcher sharing off. It accepts only JSON, so a web page cannot post to it cross-site. This is transport safety, not authentication: any program on your machine can use the tools, and some of them spend the same provider quotas as the app.
- **What the panel's requests can reach.** In clients that display MCP Apps, the panel loads the app through the `panel_request` tool, which requests paths on the app's server. It is marked for the panel only and requires a key that each MCP server puts in its panel page, which keeps it from clients that list tools to the model without loading the panel. That key is not access control: any MCP client can read the panel page, key included, and then reach the app's files and data routes through `panel_request`, much as its other tools reach the data. Provider Settings, credential and model endpoints, `/mcp` and the development server's internal routes are refused to every caller.
- **The panel carries the two browser keys.** The panel build (`npm run build:panel`) includes the same Google Maps key and Cesium ion token as the app, and they run inside the client's panel page, on the client's site. A key restricted by HTTP referrer must also allow that page, or the panel cannot load tiles. To keep the app's own keys narrower, build the panel with separately restricted keys in the environment of `npm run build:panel`.
- **Embed mode is not framable by default.** `?embed=1` documents keep `X-Frame-Options: DENY` and `frame-ancestors 'none'` unless `GEV_EMBED_FRAME_ANCESTORS` names the pages that may frame them; a framing page can change what the app shows. Other documents can never be framed.
## Scope & expectations
- The Vite server is a **development/preview** server. If you expose it beyond localhost, put it behind your own auth/proxy and review the bindings (see the threat model above).
- Hold rendering only for animated content (asturksever, #1281).
- Exercise current vector road sources and published camera bounds in Traffic fixture QA (asturksever, #1281).
- Establish browser-level foreground visibility for Traffic QA and reject hidden-page samples (asturksever, #1281).
- Supply CORS headers on cross-origin traffic fixtures and report browser exceptions without disabling browser security (asturksever, #1281).
- Isolate the Traffic keyless browser scenario from the keyed page’s workers and intercepted cache (asturksever, #1281).
- Restore minimized test windows before screenshots so native compositor pauses cannot stall browser acceptance (asturksever, #1281).
- Intercept only Traffic provider URLs, leaving Cesium worker and module loading untouched (asturksever, #1281).
- Settle an unfinished Traffic height correction when the last animated point leaves, so the layer stops reporting loading once its render hold is released (asturksever, #1281).
- Pause periodic network updates in hidden tabs; the overdue update on return restarts the polling schedule, so the next poll follows one interval later even when that update fails (asturksever, #1282).
- Report and guard browser visibility during Transit QA so hidden-page polling pauses cannot masquerade as playback regressions (asturksever, #1282).
- Require each live city to supply its own feed vehicles in Transit QA; retained vehicles cannot qualify another city (asturksever, #1282).
- Bound the Transit card-anchor probe by observed poll coverage instead of a fixed 20-second window (asturksever, #1282).
- Keep visible-page Transit probes focused through the browser testing API (asturksever, #1282).
- Add a real-browser regression gate for hidden-tab periodic work, explicit refresh, resume and teardown (asturksever, #1282).
- Restore minimized test windows before screenshots so native compositor pauses cannot stall browser acceptance (asturksever, #1282).
File diff suppressed because it is too large.
Load diff
Loaded 100 of 1689 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.