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