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.
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.
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]>
- 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]>
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]>
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]>
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]>
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 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]>
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]>
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]>
- 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]>
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
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.
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.
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]>
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]>
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]>
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]>
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]>
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.
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.
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.
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.
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.