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.
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.
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]>
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]>
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.
Keep the new 502, but store it in the 60 s cache as the proxy already does
for 404/429: /tracks/all costs OpenSky credits, so an oversized track must
not be re-downloaded on every reselect. Update the regression test and
credit the fix in CHANGELOG.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Adds MODIS NRT to Active Fires and a keyless Fire Perimeters layer (NIFC WFIGS) with
InciWeb incident links, served through a capped, cached /api/fire-perimeters proxy.
Co-authored-by: Gustavo Beneduzi <[email protected]>
Co-authored-by: James Cooke <[email protected]>
Adds a Local ADS-B layer fed by your own receivers: a browser WebUSB
RTL-SDR (with FM listening) and local 1090/978 MHz decoder feeds
(LOCAL_RECEIVER_FEEDS, local and private addresses only). Local aircraft
draw in magenta with class, silhouettes, 3D models, trails and a position
sanity filter; the click card names the band and input that heard them.
Builds on Sameh Khamis's SDR prototype and the initial GEV port by
Mazeyar Moeini Feizabadi (#134); the decoder-feed design follows the
notes on #57. Closes#57.
Co-authored-by: Sameh Khamis <[email protected]>
Co-authored-by: Mazeyar Moeini Feizabadi <[email protected]>
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]>
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]>
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]>
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).
Wind (NOAA GFS and ECMWF IFS: animated particles, colour field, a reading at map centre), observed weather (NOAA MRMS radar, GOES and NESDIS infrared clouds, lightning density) on one shared history timeline, NHC and CPHC cyclone advisories with tracks and cones, and a WEATHER panel in the right rail. On 3D Tiles the observed products render as raised shells with a sharper window around the view.
Co-authored-by: Gustavo Beneduzi <[email protected]>
Co-authored-by: Daniel Slay <[email protected]>
Co-authored-by: Rehaan Delmotra <[email protected]>
Co-authored-by: Cursor Agent <[email protected]>
The macOS credential hardener strips ACLs with `chmod -N`, spawning
`chmod` by bare name. On machines where Nix home-manager or Homebrew
coreutils sit ahead of /bin on PATH, that resolves to GNU chmod, which
has no `-N` flag and exits non-zero. The hardener treated that as a
failure and refused every save from the Provider Settings panel with
"could not restrict the credential file to your account; nothing was
saved".
Spawn /bin/chmod by absolute path instead, matching how the Windows
branch already resolves its native tools without consulting PATH, and
add a regression test that pins the absolute path.
- 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
The media relay only deadlined the wait for upstream response headers. Once
headers arrived the timer stopped, so an upstream that answered and then sent
nothing held both the client response and the upstream socket open for as long
as the camera host allowed. The declared Content-Length ceiling was the only
body bound, and a chunked or length-less body had none.
A 30-second idle deadline now measures the gap between upstream chunks and
tears the stream down through the existing release path. Because a paused pipe
also stops upstream bytes arriving, the deadline reschedules itself whenever
the response is still waiting to drain, so a viewer on a slow link is not read
as a dead camera. Live feeds that keep delivering frames are unaffected.
TOMTOM_DAILY_TILE_BUDGET defaulted to 40,000 per day against a free
tier that is granted per MONTH, not per day. TomTom currently allows
200,000 Traffic Flow & Incidents tile requests per month
(https://docs.tomtom.com/pricing/), so the default exhausted a user's
entire monthly allowance in five days and left the traffic layer dead
for the rest of the billing period.
The docs contradicted themselves on this. traffic.js described the
free tier as "~50k/day", while .env.example cited the correct 200K
per month two lines below quoting the same 40,000 default. 50k/day
would be 1.5M/month, 7.5x the actual allowance.
Sets the default to 6,000/day. A 31-day month lands at 186,000,
inside the allowance with headroom. Corrects the stale figures in
traffic.js, .env.example and DATA_SOURCES.md.
This remains an application-side safety ceiling, not a billing cap,
and the surrounding text still says so. TomTom's free tier requires
no card, so overrunning it cuts the layer off rather than generating
a charge — but a layer that dies three weeks into every month is
still worth avoiding by default.
Co-Authored-By: Claude Opus 5 <[email protected]>
ITU-R M.1371 encodes speed over ground in 0.1-knot units and course over
ground in 0.1-degree units, reserving the top code of each field for "not
available": SOG 1023 reaches us as 102.3 knots and COG 3600 as 360 degrees.
Both are finite, so numberValue passed them through as ordinary readings and
every consumer — vessel cards, the HUD, voice analyst queries — treated a
missing measurement as a real one.
Observed on a live AISStream feed of 3,062 vessels: exactly 3 records at
102.3 knots with no reading anywhere between 40 and 102.3, and 404 records
at exactly 360 degrees. One Dutch rescue boat moored at its station carried
both sentinels at once, so an analyst summary reported a maximum speed of
102.3 knots — faster than any vessel afloat.
Filter both at the store, where AISStream messages are decoded, so the
sentinels never enter the cache. Heading already had this treatment through
normalizedHeading with its own separate sentinel of 511.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
The comment claimed "Keep the detail in the log" while the line it introduces
carries none, so the next reader would have read the omission as an oversight
and added the errno back.
Detail-free is the deliberate choice: a media-fetch failure names the camera's
upstream host, and these lines get pasted into issues. Say that, and say why
the catch at the bottom of this file may still log its error text.
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]>
Supersedes #218 by @spcpza, ported onto the current component layout with fixes from review.
Verified before merge: format, package boundaries, unit suite, build, and the browser tracking suite; a browser harness for the change where one applies; adversarial review applied.
Contributor: @spcpza. His original commit is the base of this branch, and his own re-port onto main (2026-09-15, `5b69011`) was consulted while finishing the height work. That port reads every vehicle's surface from the app's shared ground-floor system (`cachedGroundFloor` / `warmGroundFloor` / `GROUND_FLOOR_LIFT_M`) rather than sampling the scene per layer, keeps a vehicle hidden until its cell answers, warms a bounded batch per poll on a timer instead of per frame, and renders at the ellipsoid above the altitude gate. That is the sounder primitive — one floor system for the app, with the rooftop-versus-street question already settled against a datum-correct DEM prior — and this branch's height work was rebuilt on it, replacing a bespoke cell cache, ground prior and rooftop rule written here. Credit for that direction is his.
Future work (not in this PR), both written up in docs/CURRENT-STATE.md:
- Readable route names. The realtime feeds carry only `route_id`; `route_short_name` / `route_long_name` live in each operator's static GTFS `routes.txt`, and headsigns in `trips.txt`. Doing it properly wants a server-side metadata loader beside the transit proxy with fixed registered static sources, bounded indexes built once and persisted, a slow conditional refresh, and a versioned route dictionary the browser caches. Shipped here: ids are never rewritten, and a cut one carries an ellipsis instead of reading as a typo.
- Below-ground rendering. Subway vehicles are drawn on the surface directly above themselves, because the feeds carry no depth and a train nobody can see is not worth drawing; the card says so plainly. Real treatment wants OSM tunnel geometry per city and a ghosted sprite tethered to the surface. The owner recorded it and left it.
Round four and after (2026-09-15). The owner tested round three under FLIR and NVG, and then asked for the design to be taken further: elegant in performance, beautiful on camera, trails with a cache for public transport, Cesium-native, and a core other moving layers can adopt. Codex Astra reviewed the plan (it confirmed the speed fault: a glide started from the drawn position at receipt time but spanned the gap between report times, so with MBTA reporting every ~10 s against a 15 s poll every second glide ran at 1.5x; it rejected a queued-segment patch), then wrote the design, then implemented it in commit-sized passes with adversarial review between them and real-browser harness runs by the integrator.
What this PR now contains beyond the contributor's original port:
- Delayed playback in a contact-agnostic core (`src/data/contactPlayback.js`): each vehicle keeps a typed, bounded fix history and a display clock that runs at one second per second through it, a feed-derived lag behind real time, holding at its newest fix rather than extrapolating and never sprinting to catch up. Marker, course, motion word, card and trail all read the same displayed sample. The clock has an explicit time-offset and seek API so a rewind is a later feature, not a rewrite; flights and vessels are untouched.
- A selected-vehicle trail (`src/data/contactTrailRenderer.js`, `src/layers/transit/trails.js`): the retained history drawn with batched Cesium primitives, clipped at display time so the head is exactly the sprite, faded by age, in the mode colour (white under sensor presets), on the same surface path as the marker.
- Bounded proxy-side history (`server/providers/transitHistory.js`): fifteen minutes and at most 128 fixes per vehicle with hard byte ceilings, MBTA only for now (opt-in per feed), served by a small endpoint that never fetches upstream, so a selected trail survives a browser reload while the server keeps running.
- Scene work bounded to what is visible and moving, an occluder, cached DETECT candidates, holds on the render loop only while something visible moves.
- Sensor presets: white-hot haloed sprites under NVG/FLIR/noir (black-hot inverts), saturated mode colours under CRT, per-mode bracket tiers in every theme, the selected card keeping its mode colour.
- A discriminating browser harness (`scripts/qa-transit.mjs`) that samples postRender frames, reads the real card anchor, and measures fleet budgets at 800 and 3,000 vehicles.
Not built, recorded as follow-ups: curated route display names (lowest priority per the owner), fleet-wide tails, route snapping, a rewind UI, adoption of the playback core by flights and vessels.
Verified at merge (2026-09-15 morning): unit suite 3,896 passing, build clean, format and boundary gates clean, tracking regression 109/109, browser harness keyless 200 pass / 0 fail (13 sensor checks unexercised because the live view offered fewer than six isolated sprites; every judged sprite passed), keyed on Google 3D tiles 198 pass with the per-frame allocation gate as the only failure. Follow-ups recorded: the keyed build's playback sampler still allocates per frame (CPU and frame time stay within budget; the 3,000-vehicle metro view sits at a 27.6 ms frame interval against a 25 ms budget), the shared detection overlay's per-paint allocation, friendlier route names, fleet-wide tails, a rewind UI, and adopting the playback core for vessels and flights.
Co-authored-by: spcpza <[email protected]>