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 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.
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:
- 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]>
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]>
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]>
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 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]>
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]>
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]>
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 /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]>
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.
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.
keep contacts counts, rows, scope and provenance on one retained snapshot
report capped cohorts as lower bounds
separate sibling tool cancellation from newer user turns
align track_entity validation across runtime and benchmark paths
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]>
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.
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.