createApplicationTools returns drawShape({ shape, signal }): it opens Draw
for one area or line and resolves with its Polygon or LineString without
placing a mark, or null when draw mode is left, the signal aborts, a newer
request replaces it, the pointer is held elsewhere or the tool goes. The
shape controls cannot change the asked-for shape, and a shape across the
antimeridian is refused as a row's actions refuse it and drawn again. It
also returns pointerFree, so an application's own click handlers yield to a
tool holding the pointer, as the layers' do.
createApplicationTools({ drawMarkActions }) adds an application's buttons to
the rows of drawn areas and lines. An action gets the shape's id, kind, name,
row title and [lon, lat] geometry, and what it returns or resolves to is a
note under the row. The standalone shell passes none, so the open-source
build is unchanged.
The orbital Context card is gone and the Contact card stays hidden in orbit
again. Live Signals page 1 had nothing in orbit but TRACK ACQUIRED, because
orbit feeds no Contacts snapshot; it now lists what is around the
satellite, built the way the aircraft rows are, five rows with stable keys
rebuilt once a second. Rows already shown keep their places while they
still qualify, so a site passing under a low orbit is not pulled from under
the pointer.
Site rows come from the Mapped Installations layer (getNearby, asked only
as far as the reach extends) and focus their site, or release it on the
focused site's row, which reads as the subject does. A site the layer drops
during its frequent reloads stays listed for 10 s, since sites do not move.
With the layer off nothing is listed and a SITES STANDBY row says why.
Satellite rows come from the satellites layer's bounded nearest query,
measured from the satellite itself, and ride the satellite; RETURN goes
back to the one before, skipping one that has left the catalogue. While
the camera hops to a new satellite only TRACK ACQUIRED and RETURN show, so
nothing is measured from the old position. An arrow pressed while a focus
eases back takes the arrows' own clock. Site and satellite rows name their
own actions.
getNearestPositions makes one bounded pass over the shown points, keeps the
nearest few in a short sorted list and reads latitude and longitude for
those alone, leaving out dense extras, asked-for ids and classes such as
debris. A caller that wants the nearest handful no longer builds and sorts
an entry for every satellite.
getById returns a loaded installation with its drawn position, as getNearby
does, without selecting it; findByPick resolves a scene pick (a named
marker's installationId or a site entity) the way the layer's own click
selection reads it. Both return null while the layer is off.
In the orbital view a double-click on a mapped ground point went on to
Cesium's own double-click, which tracks the picked entity and so ended the
view. The Cockpit's double-click routing now takes it in orbit: it keeps or
starts the focus on that point (a point beyond the reach is left unfocused
but still not tracked). A double-click anywhere else, one a tool owns, and
every one outside orbit go on to Cesium's as before, and stopping the
Cockpit still hands Cesium's handler back.
In the satellite orbital view a single click on a mapped ground point (a
ground point of a layer that is on, such as a Mapped Installations site, a
fire or a local data pack feature) keeps the camera on it. Each frame the
look is aimed from the satellite's position to the point's Earth-fixed
position in the track-up nadir frame the arrows tilt, so the point stays
at the centre as the satellite moves. The swing onto the point, or between
points, uses the arrows' easing on a slower clock and then holds the point
exactly; reduced motion cuts.
A click elsewhere, FOCUS on the focused point, NADIR, Home or 0 ends the
focus and the view eases back to nadir; an arrow ends it and tilts on from
where the view is. The focus also ends, easing back, when the point leaves
the ±45° reach or the satellite's horizon, when its layer is turned off or
its record is dropped, and with the view. While focused the readout gives
the single off-nadir angle to the point (OFF-NADIR 23.4°), names it in a
polite live region, and the swath reads approximate as for a tilt. Clicks
still reach Cesium's own handler and never untrack the satellite; a click
a tool owns is ignored.
Under the readout, IN REACH lists the mapped points facing the satellite
that are within the reach or in the frame, nearest to nadir first and at
most 12, rebuilt once a second. A row opens the point's position, source
and OpenStreetMap tags (or its layer's properties); FOCUS focuses it,
switches to it, or ends the focus held on it, and is unavailable for a
point in frame but beyond the reach.
The Orbital view section said the zoom stops at 8x. It now covers the
16x to 128x steps for satellites with a confirmed sensor swath, capped
from live altitude, the mouse wheel, the SENSOR button, the to-scale
swath strip and readout (approximate off nadir), the imagery pre-load
ahead of the ground track, the TLE age on the identifier line, and that
the sensor values cite operator and agency pages per platform.
An imaging satellite with a known sensor can now zoom past 8x, up to 128x,
as far as the view stays at least as wide as the sensor's narrowest
standard swath at its live altitude; only swaths confirmed on a cited page
open those steps. The top step follows the altitude and window shape and
steps back when it falls below the zoom shown.
The view draws the sensor's swath to scale along the ground track with a
SENSOR SWATH readout (approximate off nadir), and a SENSOR button zooms to
exactly that width when it lies in range at nadir. From 16x the imagery
tiles the view will need 5 to 20 s ahead along the ground track are
pre-loaded at the matching level. The mouse wheel steps the zoom one notch
at a time, and the identity line shows how old the TLE is. The sensor
figures come from a new bundled pack of 46 Earth-observation platforms,
each value cited to its operator or agency, with its own data credit.
With a satellite tracked and no aircraft tracked, Cockpit can ride the
satellite. Entry needs only the Satellites layer; debris can be tracked but
not entered, and voice stays aircraft-only. Double-clicking the tracked
satellite or the entry buttons (reading ORBIT) enter it.
The camera sits on the satellite's per-frame SGP4 position and looks
straight down, turned to the ground track (north-up for a satellite with
none). The optical zoom steps 1x to 8x with a focus pull, the arrows tilt
the view up to 45 degrees off nadir, and Home or 0 returns to nadir. Off
nadir the tilt reads its total angle first and the swath readout becomes an
approximation marked with ≈. While in orbit the map leases flat Esri
imagery, the cloud pass stays off and the view opens in the optical style.
The orbital HUD names the satellite with its owner's flag (from the
flag-icons package, loaded per country) or an organisation badge, its
operator, class, NORAD and COSPAR ids and TLE epoch with the element lines
on request, altitude, orbital speed and the sub-satellite point. CURRENTLY
OVER names the country and its first-level division, or the sea, and from
8x the most prominent populated place nearby, looked up offline from the
bundled Natural Earth packs (populated places are a new pack). The
satellites layer hides the ridden satellite's own marks, dims the other
orbit rings and ignores scene clicks and Escape while held. Exit restores
the camera's field of view and the satellite's follow camera.
C no longer enters or leaves Cockpit, bridge or orbit: it is left to the
app-wide CCTV shortcut, and Escape or the exit button leave first person.
The orbital view reads a satellite's owner country, operator, COSPAR id,
TLE epoch and whether its class can be ridden. #1558 now carries only
what the class toggles read, so this brings the rest back here, where it
is first used: the classes.js helpers and the UCS pack's operator and
owner fields.
Class toggles only hid points: with Stations off the ISS's orbit ring and
its label still drew, and each catalog refresh showed the ring again. The
ring and label now follow the ISS's class, and the tracked card no longer
names a docked companion whose class is hidden.
A failed Starlink load reverted to the core catalog without applying class
visibility again, so the core catalog's own Starlink points stayed on while
the chip, legend and lists said Starlink was off, and classes.starlink kept
reading true. Both are put right on failure.
Deduplication kept only the first group's tag, so a recent imaging
satellite in both visual and resource, with no UCS record or known name,
fell to Probes. The catalog entry now keeps every group, and the
last-resort class is the first of them that names one.
SATCAT groups merged in group order, so a stale cached copy read later
overwrote a current record of the same object: a satellite SATCAT now
calls dead stayed in Imaging. A stale copy no longer overrules a current
read, from the same merge or an earlier one.
Only catalog and the tracked satellite were saved for the layer, so a class
chip lasted until the next reload and no share link carried it. The choice
is now the satellites option `classes`: one letter per class shown
(`sitpd` by default, `0` for none), with Starlink left to `catalog`, and
the layer takes the letters back through setParams. A partial request
(`{debris: false}`) hides only what it names.
Both CelesTrak routes shared one cooldown, and any failure set it: one bad
SATCAT answer made /api/celestrak return 502 "cooling down" for a minute,
failing the Starlink load and stalling every TLE group past its cache.
Throttling (403/429) still holds both routes off, since CelesTrak throttles
the client; any other failure holds off only the route that failed.
Showing only Stations and Imaging by default hid GPS, GEO comsats, Hubble
and the rest of what the layer showed before classes, for every existing
user and saved view. Every class now starts on except Starlink, which stays
off as the DENSE switch was.
The satellites layer now sorts every object into Stations, Imaging,
Telecom, Starlink, Probes or Debris, each with a chip and a live count on
the layer row. Stations and Imaging show by default; the Starlink chip is
the former DENSE catalog switch. Hidden classes keep their points, so a
toggle needs no reload, and they leave the detection overlay and the voice
position lists.
A class comes, strongest evidence first, from CelesTrak SATCAT (object type
and status, served by a new /api/celestrak-satcat/<group> proxy route and
cached for a day), the bundled UCS Satellite Database pack (operator, owner
country and purpose), name patterns for later launches, and the CelesTrak
group. The core catalog also reads the resource, weather, planet, sar and
science groups. Payloads with no evidence of life launched 20 or more years
ago are filed as debris. The UCS pack, its build script and its data credit
are added.
An application built with fewer layers no longer restores, stores or shares a
layer it does not have when a link or saved state from a build that has it
names that layer. LayerStateCoordinator drops unregistered enabled-layer ids
before normalizing, so dependent options (a tracked selection) go with them.
An application with every layer registered is unchanged.
Both reviews of the timeout found that Cesium cannot cancel a full-detail
read: each one given up kept its nine picks traversing every frame, so
stalled tiles piled up a new set every 30 s, while slow but valid answers
were dropped. Raise-only loaded reads then locked in a wrong-high height.
Back to one full-detail read at a time whose answer always applies, and
loaded reads that may adjust a river height but never take it to the sea;
a coarse probe can again lower it partway until the full read lands.
Keeps the test for another ship selected inside the double-click memory,
and corrects CURRENT-STATE: a failed read keeps the height it had.
Before the first full-detail read lands, a loaded-tile read could still
lower a river height partway (27 m to a 15 m bank). Loaded-tile reads now
only raise it; lowering is the full-detail read's to say.
That makes a full read that never lands (stalled tile requests) hold a
wrong height for the whole bridge, so one still running after 30 s is given
up for a fresh one; its late answer is dropped.
Also tests the other half of the double-click memory fix: another ship
selected within the memory is not entered through the first ship's spot.
Before the first full-detail water read lands (it can take many seconds
from cold tiles), a refresh's loaded-tile read with one low probe still
dropped a river height to the geoid. Before that landing, loaded-tile reads
may now find raised water but only a full-detail read lowers it.
CURRENT-STATE also says what the earlier fixes changed: C stays the CCTV
shortcut when the bridge cannot be entered, voice entry is refused on a
bridge, and the ridden ship's trail is hidden with its sprite.
The first press's picture of the ship survived a change of followed
subject: following an aircraft (or another ship) within 700 ms let a
double-click on the old ship's spot enter the new subject. The memory now
carries the ship's MMSI and is used only while that ship is still the one
followed.
The vessel layer draws other ships at their last AIS fix and does not
advance them, so dead-reckoning radar contacts moved the mismatch from own
ship to everyone else: a 15 kn ferry reported 90 s ago showed ~700 m ahead
of the ship out of the window. Own ship stays centred where the camera
rides; contacts go back to their last fixes, and the query drops the extra
reach and candidates (which could also cut the nearest ship in a crowd).
The bridge tests drive a hand-written layer, so nothing exercised the
vessel layer's own scene-click gate. A wire test now clicks open water with
the bridge holding the ship, then again after release.
The Contacts test that adopts Live AIS Vessels directly is renamed to say
so: it never rides a bridge.
The ship bridge rules were appended to cockpit.css, taking it to 2705
lines; they move unchanged to cockpitBridge.css, imported right after it so
the cascade is the same. cockpitBridge.css and cockpitRadar.css join the
format scope and the styles package.
The first press's picture of the track is there because a click on a
selected ship re-centres the camera. It applied to followed aircraft too,
widening their double-click hit area to wherever the aircraft was drawn
as the gesture began. Aircraft are back to the hit where they are drawn.
The sprite and card were hidden from the bridge but the selected-vessel
trail was not: at sea it ran astern from under the eye, and on a raised
river it lay buried under the water. It is now hidden while the bridge
holds its ship, keeping its history, and shown again on release.
The ground clamp read the floor at the ship's last AIS fix while the camera
rode that fix dead-reckoned up to 180 s ahead, so a quay or bridge deck at
the old position could lift the eye. Vessels now read the floor at the
dead-reckoned target, as the water read already does; aircraft are
unchanged.
The radar centred on the ship's last AIS fix while the camera rode that fix
dead-reckoned up to 180 s ahead: at 20 kn about a nautical mile apart, so a
ship already passed still showed ahead. Own ship now sits where the camera
rides and each contact is dead-reckoned to the same moment; the query reaches
past the range for ships that have since moved into it.
Ships with no MMSI each get a key of their own instead of all sharing an
empty one on the scope.
A full-detail water read that sampled nothing (tiles not loaded) fell back
to the geoid, and while a full read ran, a loaded-tile read with one coarse
low probe did the same: on the Seine either dropped the eye ~27 m under the
water until the next read. An empty read is now no answer, and once a
full-detail read has answered only full-detail reads change the height.
The read sequence numbers go: only one full-detail read runs at a time,
so a late read can never be older than one already applied.
Voice entry tracked an aircraft first, which cleared the vessel selection
and ended the bridge, and then failed because Cockpit was still active:
no bridge, no cockpit, no selected ship, and voice said entry was
unavailable. It now refuses before it touches tracking and says to exit
the bridge first.
With a vessel selected and no aircraft tracked, Cockpit took C and stopped
it even when the bridge gate was shut, so the key did nothing and CCTV no
longer toggled. C is now taken only when the bridge can be entered.
The arrow keys also stay with comboboxes, spinbuttons, trees, grids,
menubars, scrollbars and text boxes on a bridge, which move with them.
With no aircraft tracked, Cockpit can ride the selected AIS vessel. Entry
needs only Live AIS Vessels and a selected vessel; C, double-click and the
entry buttons (reading BRIDGE with a ship icon) enter it, while voice stays
aircraft-only.
The eye sits on the AIS position at a representative bridge height for the
vessel's type family, looks along true heading and moves along course over
ground. The last fix is dead-reckoned for up to 180 s between the layer's
refreshes, and on rivers and lakes the waterline is read from the rendered
surface around the ship rather than the geoid.
The bridge HUD shows speed and course over ground, a straight heading
tape, window mullions, the AIS destination as a reported voyage and a
heading-up radar of nearby ships (3 to 24 NM). The arrow keys look around,
Home or 0 looks ahead again. The vessel layer hides the ridden ship's
sprite and card, ignores scene clicks while held, and returns the camera
to the vessel's map view on exit.
Main now ships an Iceland pack that loads the full live Vegagerðin list
(about 500 views) with its CC BY 4.0 credit. The World catalog's 166
Vegagerðin rows point at the same frames under different ids, so every one
of those cameras would be drawn twice. Drop them from the World export and
its loader policy; World is now 2,230 rows and the default catalog limit
7,350 (4,000 shared slots plus 3,350 reserved).
Register Data attribution entries for Ottawa, Québec, TripCheck and MLIT,
as every other CCTV pack has. MLIT's terms ask for the source in the form
"出典:国土交通省 川の防災情報" with the page link, and ODOT asks for
"Camera courtesy of ODOT"; TripCheck partner cameras keep their owner's
name in the label.
The Houston guard test from main checked the old 4,000 default ceiling;
check the 4,000 shared slots instead, like the other pack tests.
- Label repeat-collapse uses letter lookarounds instead of ASCII-only \b, so
'Þverá Árnessýslu' is no longer joined and 'séð séð' collapses.
- 'til austurs og vesturs' and 'í norður og suður' name two facings and keep
the low-confidence pose.
- Fallback name uses the frame stem when the station number is missing.
- Tests: label and heading regressions, dedupe keeps the last row, cap clamp,
high-confidence pose, frame redirect pin; file added to format scope.
- Credit names the dataset, links the terms and states no endorsement.
Turning CCTV on picked the first catalog record, so at King's Cross it
showed a Sydney camera. enable() now picks the camera nearest the viewer.
Until someone chooses a camera, each settled move re-picks the nearest,
so a share link whose flight lands after CCTV turned on still opens
nearby.
One JSON list from gagnaveita.vegagerdin.is (about 500 camera views at roughly
170 road stations). Frames are JPEG stills in a single directory on
www.vegagerdin.is; only links inside that directory are registered, and the
media proxy pins the same directory.
The list decides ids, coordinates and which frame path is proxied, so it is
fetched over https with redirects refused and the body read capped at 2 MiB;
every rejection path releases the response body. Camera ids come from the frame
file name, which is unique per view in the live feed, and repeated frames
collapse before the cap.
The feed's view description (`Skyring`) is the label. An explicit Icelandic
facing ("til vesturs", "í suður", "séð austur") sets a high-confidence heading
(264 of 499 views); a view that states none, or two different ones, keeps the
id-hash low-confidence pose. A bare road direction ("Miklabraut austur") is not
read as a facing.
IRCA publishes road camera images under CC BY 4.0 and asks that the licensor is
acknowledged without implying endorsement; the attribution is in the Data
attribution popover and DATA_SOURCES.md, and every camera carries the licence
string. Default cap is 250 with anchors at Reykjavík and four regional centres so
a cap keeps every region. CCTV_ICELAND_ENABLED=0 turns the pack off.
Verified against the live feed through fetchCctvImageFromUpstream: 499 of 499
frames returned a JPEG, all distinct, all under 20 minutes old.
OpenAI refused every typed chat request with 400 invalid_function_parameters:
resolve_area's area-id pattern uses Unicode property classes (\p{L}), which
Chat Completions does not accept as a regex. OpenAI chat now leaves such
patterns out of the tool schemas it sends; each action still checks its
arguments against its own contract, and realtime voice keeps the patterns.
createTrafficSource({ roadModes }) lets a host offer a subset of the TomTom,
OSM and Hybrid road modes (all three by default). Without OSM the layer never
draws simulated roads, and a single mode shows no road chip.
While flow is live the Street Traffic row offers Lines (every road with flow
as a steady green/amber/red line, from the heat-line renderer) and Dots (the
moving dots), saved in layer state and share links. Defaults: Lines off,
Dots on.
Roll back a Space Missions entry whose mission index fails to load
Thanks @gekh. Maintainer review fixes (separate commits): exit cancels the layer's own camera flights again, a failed index load shows a specific toast, tests and docs.
- releaseCameraOwnership marks the camera released before stopping, so a
flight navigation asked to preserve survives; exit also releases it, so an
inactive layer never cancels another owner's flight.
- The failure toast names the mission index only when its error is from the
current Context attempt (stats.errorAt), not an earlier one.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Review fixes on top of #1578:
- Exiting Space Missions cancels the layer's own camera flights again (entry,
roster preview, mission focus); only a flight navigation started after
releaseCameraOwnership() runs on.
- A failed first index load says so: "Space Missions unavailable: the
mission index could not load. Try again later." on both the Context and
Data Layers routes, instead of a generic transition toast. Translated.
- Regression tests for both; docs and changelog describe the real feedback.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
fix(street-level): without a Mapillary key, start on a keyless source
Thanks @asturksever. Maintainer review fixes (separate commits): filter-only updates keep an explicitly chosen source, keyless Street Level QA gate updated, tests and docs.
Review fixes on top of #1601:
- Only redirect a provider the params switch on to a keyless source, so a
filter-only setParams (360°, SINCE) no longer moves an explicitly chosen
keyless Mapillary to KartaView.
- Keyless QA gate: assert the new KartaView default and that the Mapillary
chip explains its key instead of switching (it used to expect KEY REQUIRED).
- Tests: Graph-proxy keyReady, filter-only updates; shared test helper.
- README and CURRENT-STATE describe the keyless default.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
feat: add adjustable age filtering across S2 GhostMaps
Thanks @dylanmaniatakes. Maintainer review fixes (separate commits): age-window narrowing/restore consistency, card kept across same-report reloads, date-only events cover their whole UTC day, and the age choice as a labelled Event age switch.
- Keep the open card across a widened reload only when its index still
holds the same report; the server may answer a pinned version with newer
bytes.
- Narrowing to a window the loaded data already covers clears a failed
wider reload, so the row no longer stays in error.
- Label the every-age option "All" (contextual) so the six-option switch
fits the row in every locale; tooltip and info line keep "All markers".
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Review fixes on top of #1528:
- Narrowing the age window only removes markers; a product loaded with a
narrower window is reloaded when the window widens again. Fixes markers
missing after 7d -> 1d -> 7d and avoids reparsing every KMZ on narrowing.
- A wider window over the same edition keeps the open report card.
- Date-only event dates cover their whole UTC day, so 1d still shows
yesterday's reports.
- The age options are one labelled "Event age" radio switch (row
`segmented` control, as FIRMS confidence) instead of toggle chips mixed
with the product chips.
- Un-nest the per-product age test; QA compares data-source counts with the
live defaults instead of a hard-coded 2.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Publish classificationName next to the raw NHC code; unknown codes stay
unexpanded.
- Publish staleCacheFetchedAt only while the proxy serves its cache.
- Touch the shared store on clear only after this layer published, so a
feed failure no longer evicts another layer's dangling selection.
- Drop the unreachable no-storm branch and the unused return value.
- Tests: split the heading fixture into due north and stationary, cover
the unavailable and failure paths with the real store, and check that
clear/disable leave another layer's selection alone; fold the fake-store
test into the real-store one.
The data refresh is committed and its provenance (extract, timestamp, MD5,
filter) stays in the dataset README. The script needed osmium-tool and could
only re-verify while Geofabrik still listed that extract, so it goes with its
test.
A failed flow tile now carries the server's reason. A hosted deployment that limits each account answers USAGE_LIMIT_REACHED or RATE_LIMITED, which the traffic panel labels as the monthly allowance or a rate limit; a 503 with another reason reads as the traffic service being unavailable, not a missing key. The local proxy's no_key and daily budget labels are unchanged.
From 400 km, a tilt to -5° that a zoom out x2.5 caught 70 frames in eased down from the camera's old pitch at the new range. That put the camera below the place's horizon: the first frame's crosshair hit the ground 186 km from London, and London stayed hidden for about 11 frames. The re-solve also left the pivot pitch at 0°, so a zoom in before the next frame computed an infinite clearance floor, was refused, and stayed at 1,000 km.
The re-solved pivot pitch now stops at the place's horizon limit, or where the pivot already was. A camera that the zoom leaves past that limit steps down to it with the zoom and holds it, and the zoom still reports that angle. A zoom in straight after brings the range back to 400 km.
The voice runner subscribes to camera handoffs as it is created. The Cyber sonar test builds its runner on a StyleManager made from the prototype, which has no navigation owner, so the facade's delegation threw and both Node jobs failed. The facade now returns nothing there, like its other optional owners; the real shell builds its navigation owner before voice creates a runner.
One error was chosen before each failure's delay was known. An antimeridian
box 400 hid the other box's 503 Retry-After; a grid tile 400 with Retry-After
set a cooldown through the snapshot metadata; a short Retry-After on one tile
undercut a sibling 5xx's default backoff; and a huge Retry-After made the
deadline Infinity.
Every failed request now keeps its status and Retry-After. One function gives
each failure's delay: 4xx other than 429 none, 429 and 5xx their Retry-After
or the default backoff. The layer waits for the longest, and shows the
failure that set it. Retry-After is capped at one day, the proxy's own
daily-budget deadline, and non-finite values are ignored. Each publish of
one acquisition derives the default backoff from the backoff before it
started, so repeated tile publishes no longer double it.
From 400 km, a tilt to -5° that a zoom out x2.5 caught at -8.0005° reported -8°, and the next frame jumped to -9.407° and ended there. The zoom kept the camera's pitch as the goal when it was already past the new range's horizon limit, but at the same pivot pitch the longer range looks down more steeply, so the next lookAt dropped the camera 1.4° at once.
The zoom now re-solves the pivot pitch so the new range keeps the camera's pitch, and aims the tilt at the asked angle or the new limit, even when that limit is below where the camera stands: the tilt eases down to it. The zoom's reply names that end, and the camera reaches it with no frame moving more than 0.1°.
A partial failure reset the layer's cooldown, so a camera move retried at
once. Acartia's error was discarded, an absent X-Upstream-Status hid the
HTTP 503 (the proxy's daily-budget reply), and a grid schema error masked
the observation 503. Each failed source now keeps its status and
Retry-After, the HTTP status counts when no upstream status is sent, and the
layer waits for the longest 429/5xx cooldown among them. The shown error is
the one that explains the cooldown.
The held-pointer guard was one flag, so with two fingers down, lifting one cleared it and the recovery turned the view down under the other (pitch -42° to -90°). It now tracks each pressed pointer by id and defers until the last is released.
A release outside the window that the page never sees could leave the recovery deferred for good. A lost window focus, a hidden page or a lost pointer capture now releases every held pointer, and a pointer that moves with no button down is released.
The trim that fitted the tilt wording under the Realtime prompt ceiling dropped the -45° default from pitchDeg, so the model no longer read why a bare tilt goes oblique. pitchDeg reads 'Absolute; -45 default, -89 top-down.' again, and the direction description becomes 'Pan/rotate/tilt steps; orbit/rotate_globe default right.' to pay for it: the Overpass prompt measures 55,499, 1 under the 55,500 ceiling. The schema pin and the measured-size note follow.
A pivot range rebuilt from the camera's position came back a hair over the 1,000 km cutoff when the view sat exactly on it, so a tilt from 1,000 km ran in place and lifted the crosshair onto the sky. The cutoff now allows 1 m of slack.
A zoom during a tilt to an angle changes the range the pivot holds, and with it the place's horizon limit, but the tilt kept its old goal and its reply its old angle: from 400 km, a tilt to -5° zoomed out x2.5 reported -5° with no note and ended at -9.4°. The zoom now re-judges the goal at the new range, eases the tilt from where it is to it, and its result names the angle the tilt now ends at, with a note.
/grid/{z}/{x}/{y}.grid.json is documented in the v1 and v2 API docs as
Grid Tiles UTFGrid; only its per-cell records are unspecified, which is
what the schema guard covers. DATA_SOURCES.md now also notes that
Acartia's README asks API users to create an account and an API key,
although /sightings/current answers without one today.
An iNaturalist rejection discarded a successful Acartia reply, and the
antimeridian Promise.all lost both halves when one failed. Each box now
settles independently; the failed part is reported on
sources.inaturalist (error and failed boxes), the layer keeps that
area's last iNaturalist rows and shows DEGRADED with retry timing, and
the update fails outright only when every requested source failed.
The card's "Acartia" link was built from the contributor-supplied
photo_url, so any https site could appear labelled as Acartia. Acartia
has no documented per-sighting page, so the source link is now always
the Acartia site, on the server projection and again in the card.
The separate "Open on Acartia" fallback link is no longer needed.
A quiet or fully filtered feed raised invalid_snapshot, so the proxy
answered 502 with a 15 s cooldown and re-polled about four times a
minute. Only a non-array payload is malformed now; an empty projection
caches for the normal ten minutes and clears earlier rows.
Holding a drag still for half a second on a view that already showed no surface looked like a camera at rest, so the recovery turned the view down under the user's hand. The recovery now defers while a pointer pressed on the canvas is held, and runs once it is released, as for a flight or a playing scene.
The pre-check picked the in-place look from args.direction alone, while the tilt itself turns down for any direction but up. A tilt sent as left or right at the frame limit was checked as a turn left or right, refused as losing the globe, though it turns down toward the planet. A rotate sent as up or down was checked as a look up or down, though it turns right.
A tilt's look now comes from its goal pitch against the current pitch (tiltLook), and the tick tilts in place through tiltInPlaceWithinFrame(cam, m, goalPitch). Neither depends on the direction word, so a tilt to an absolute angle, or a tilt with no direction, can use them as they are.
Gemini runs one turn's calls one after another. By the time "pan left" ran beside "fly to Xyzzyville", the lookup had already found nothing, so there was no flight to wait on and the pan moved the old view with ok: true. OpenAI's parallel calls refused the same reply.
The runner now remembers, for the latest reply that sent a voice fly-to, whether one moved the camera and whether one found nothing. A tilt, orbit, pan or rotate of that reply with no flight left to wait on is refused when its fly-to found nothing and none moved. The reply is Gemini's turn, passed as a per-turn reply object, or OpenAI's authorityGroup. A motion in a later reply, or beside a fly-to of the same reply that did land, runs as before.
A motion queued behind a voice fly-to checked only that its reply was current and that the user had not touched the map. A location-panel search, share link or file import stamps a deferred navigation without stopping the flight, so the flight landed, the motion took navigation, and the search's reassert then failed: the search was silently dropped while the camera kept orbiting.
The runner now counts camera handoffs from subscribeCameraHandoff, less the ones voice fly-tos make themselves, and each voice flight records that count when it starts. A motion whose flight saw another navigation since then is not run and says so. Voice fly-tos' own stamps do not count, so a later fly-to in the same reply, including a lookup that finds nothing, is still followed as before. The test harness now publishes handoffs and stamps its voice lookups, as the shell does.
From 550 to 1,000 km of range the camera's -5° limit lies past the place's own horizon. The swing ran into its 150 m clearance, let go of the pivot and tilted the rest in place, which lifted the crosshair onto the sky (28 to 63 km up from 600 to 900 km), and a return to top-down then landed hundreds of km from the place.
The swing now hands over to an in-place tilt only where the clearance is what stops it (within about 17 km). Farther out it stops with the camera 0.5° above the place's horizon, still pivoting: a tilt to an angle reports the angle it reached with a note, a continuous tilt stops there, and a further tilt up is refused as at the limit. Tests check that the crosshair stays on the place, frame by frame for a continuous tilt, and that top-down returns over it.
With main merged, the Overpass prompt measured 55,549 against the 55,500 ceiling. The pitchDeg and direction descriptions now read 'Absolute; -89 top-down.' and 'Needed for pan/rotate/tilt steps', bringing it to 55,497. The schema pin and the measured-size note follow.
Riding an aircraft now adopts its layer the way a direct selection does, and
Contacts gives up a dependency the user adopts, so ending Contacts mid-session
(one of its layers off, or Contacts itself) no longer turns off the ridden
layer and ends the Cockpit. When Contacts goes off, Live Signals drops the
rows built from its snapshot instead of leaving stale distances marked
CURRENT, and the feed-off warning is no longer duplicated.
Recent community wildlife observations, as a new Wildlife group in the
layer panel.
- Server: keyless iNaturalist proxy (v2 observations with only the
projected fields; undocumented v1 UTFGrid density tiles, decoded on the
server) sharing one paced queue (burst 4, ~1/s, 60/min, 8,000/day budget,
up to four in flight), in-memory caches and upstream-only cooldowns;
Acartia Salish Sea marine-mammal feed rounded to 0.01 degrees with
comments dropped.
- Layer: grid counts drawn as a soft view-ranked density glow; real sampled
sightings as taxon badges at their true positions with ambient photo
thumbnails and labels; obscured records stay privacy cells; a clicked
sighting stays pinned through camera moves and refreshes.
- Anchored photo card per selection with the single iNaturalist link;
attribution in the layer row and the Data attribution popover.
- Filters (CONFIRMED, taxa presets) round-trip in share links with token 9.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Name each Rio feed in its credit instead of repeating the licence, drop the
deprecation history from the bus note, list Rio among the feeds with a
route-name table, and shorten the route-table header and test comments.
foldPlaceName drops a leading "the", so "The Hague" over upstate New
York accepted Hague, NY as an exact nearby match. Split the script-aware
fold into foldPlaceText and use it for keyless toponyms; foldPlaceName
keeps its behaviour for the packs and street matchers.
Also: changelog fragment credits #1585, folding test pins the Latin
letters NFD does not decompose (ß, ø, ł), and CURRENT-STATE notes that
non-Latin names match only when spelled with their marks.
Both SMTR feeds now publish Position.speed in m/s. Compared with how far
vehicles moved between reports on 2026-11-05, the published values ran
3.1x (buses) and 3.6x (BRT) below the observed speed, so the km/h
conversion divided real speeds by 3.6. Drop speedUnit and the conversion.
A Director hold longer than the settle check's five-minute bound used
to expire the deferred check, so a run that ended on a stars-only shot
after a long hold stayed there until the next move. While a flight or a
playing scene defers the check, its bound no longer counts down; the
flight or scene bounds the deferral itself.
Also correct the trackball note in CURRENT-STATE: Cesium's tilt tests
the screen centre, so any tilt (drag or touch) with the centre on the
stars between 1,000 and 7,500 km now tilts about the planet's edge or
does nothing, wherever it starts. Rotation drags that start on the
planet are unchanged.
A check skipped because a flight or a playing Director scene was in
progress used to be dropped, so a run that ended or held on a stars-only
shot stayed there until the next camera move. Keep watching instead:
the settle check retries once the flight or scene ends, within its
existing five-minute bound, and a moveEnd that arrives while it is
deferred keeps the check armed rather than stopping it.
Also test that the free-look limit never raises Cesium's trackball
height and that dispose keeps a value set after install, note in its
JSDoc the enableLook case it cannot tell apart, and document the
trackball change and the retry in CURRENT-STATE and the changelog.
The launches layer swallowed every fetch error in its update, so the
manager treated a 429 or timeout on /api/launches as a successful start.
Context then committed the isolating mode with no missions: the other
layers stayed off, unrelated enables were refused, the camera stayed at
the full-globe view and the roster kept reading "loading".
Until the first index loads, the update now reports the failure. The
manager fails the enable closed and both entry routes roll back to the
pre-entry layers. The camera pose captured at entry is restored when the
mode is left before any index loaded, and the roster shows a failed
state instead of loading. A valid empty index still enters, and a
refresh failure after a loaded index keeps its records.
Below Cesium's trackball height (7,500 km by default) a drag that starts
on the stars turns the camera in place, whatever enableLook says, which
was the drag path to a view with no planet in it. Lower that height to
the free-look height with the free-look limit, never raising it, and
restore it on dispose. Above 1,000 km such a drag now orbits the globe,
as it already did above 7,500 km, at a similar rate, and a tilt drag
from the stars there tilts about the planet's edge or does nothing
instead of turning the camera. Drags that start on the planet are
unchanged.
Review follow-ups for the globe-in-view recovery and voice limits.
- Leave a camera flight in progress and a playing Director scene, holds
included, to their own framing. The recovery is now installed with the
scene tools, which own the Director, and reads its running state.
- Reuse a voice turn's half-degree proof only from the position and
direction it was proven at, so a release that moves the camera checks
the turn again.
- Share one free-globe-camera check between the recovery, the voice
turn limit and the globe rotation.
- Keep an enableLook setting other code makes after the free-look limit
is installed, instead of writing back the install-time value.
- Name the nudging test for what it asserts, the 1% floor, and document
that a view losing its surface without a camera move waits for the
next one.
- A tilt's angle, limits and "unchanged" check use the camera's own
pitch. At regional range the pivot pitch is up to about 8 degrees
shallower, so "tilt to 5 degrees" from 900 km ended at -12.9 degrees.
The swing now solves for the pivot pitch that gives the camera angle.
- A zoom out during a pivoting tilt stops at the 1,000 km pivot limit
instead of the orbit's 5,000 km, and a zoom in that reaches the
clearance is applied before the pivot ends.
- An angle the camera already holds still takes navigation, so it stops
a running motion and releases the follow camera.
- A motion queued behind a fly-to moves to a newer fly-to as soon as it
begins, so a stalled lookup no longer holds it past a flight that
landed. Wait listeners are removed when a wait ends, and a lookup that
found nothing points straight at the flight that moved.
- A fly-to that stops a running motion names it (stoppedMotion).
- A tilt left or right is refused, and the pitchDeg description says
steps use direction.
SMTR's realtime feeds carry internal route keys, so a Rio card read
"Route O0731AAN0A" where riders know line SN731. Join both feeds to the
route_short_name in SMTR's static GTFS routes.txt (CC BY 4.0, same
publisher and terms). The keys cannot be parsed: O0014AAA0A is LECD135.
A 2026-11-04 snapshot found all 282 bus and 21 BRT routes in service in
the table; an unknown key still falls through raw.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Cockpit entry no longer needs Contacts or both flight feeds: a civilian
aircraft needs Live Flights, a military one Military Flights. Contacts and
every other layer can be turned on or off before or during a session, and
turning off the ridden layer ends it. A Context mode change in flight still
blocks entry.
When the other air feed is off, Live Signals and the Contact card say
FLIGHTS FEED OFF or MILITARY FEED OFF · PARTIAL PICTURE instead of
presenting a partial picture as complete. Voice refuses entry with
subject-layer-off rather than contacts-inactive, and its tool description
says Contacts is not required.
Double-clicking the followed aircraft enters Cockpit through the same gate.
The Cockpit takes the scene's double-click before Cesium's stock handler,
which would otherwise untrack a followed aircraft whose model is a
primitive. The entry also shows under the Intel HUD readout, sharing the
Contacts button's face. A test keeps the classes the Cockpit adds to the
body from matching another element's rule.
Arabic catalog strings carry optional marks ("تتبّع"), but people type
without them, so "تتبع" found no suggestion. Fold the marks out of both
the draft and the candidates before comparing.
- Host test switches AI Provider from chat Settings through the real
provider commands and checks the new mount keeps chat's context, frame,
Back to chat and focus on the provider choice.
- Drop the open and minimized branches of the carried presentation: the
provider choice lives only in Settings, so chat is never open then.
- One showChatSettings helper for the gear and the restore, and one
isStoppable helper for the live-or-error check in chat and the panel close.
Split the Cockpit camera update into the aircraft's own front half and a
shared placeCamera that reads the eye's forward and up offsets, pitch and
ground clearance from a profile. The aircraft profile carries the existing
constants, so the aircraft view is unchanged; other subjects can now supply
their own eye.
Resolves the DATA_SOURCES.md conflict by keeping main's rewritten Transit
and Radio bullets and restating the count for this branch: eighteen feeds
covering seventeen regions (the two Rio feeds share one region).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Switching AI Provider in Settings opened from chat rebuilt the voice
control, so Settings fell back to voice and the dock went idle, widening
the wings. The switch now carries chat's Settings, or chat itself, to the
new provider as it already carries the input mode; the conversation still
starts fresh and focus stays on the provider choice.
After a voice error, opening chat or closing the voice panel left the dock
red on ERROR, because both stopped only an active session. They now also
end a session left in error, which returns the dock to idle.
The quick commands "Reset to globe view" and "Show earthquakes" reached t()
through a .map(), so the catalog check never saw them and every language
showed them in English. They are now marked with msg() and translated.
Review of the 71 strings from #1507 in all eleven languages:
- zh-Hant is regenerated from zh-CN with OpenCC s2tw, as its guide says;
40 entries had Taiwan vocabulary instead.
- ar "Dock to the right" names the left side, where RTL puts that rail.
- ja "What am I looking at?" no longer asks what the assistant sees.
- Provider Settings, dock, provider, live and map use each glossary's terms.
- de drops the du-form; ja drops spaces between Japanese and Latin text.
- Calques, register and the example commands read naturally.
A poll that returned the same stale snapshot dropped STALE from the
Contacts cohort until the next answer, because the in-flight loading flag
outranked stale and the source revision ignored the provider name. A
source that has answered now reads its status without the loading flag;
one that has never answered (AIS first connect) still reads as loading,
so it shows no STALE before any snapshot exists.
A new voice fly-to now records the last flight that moved as the one it replaced, landed or not; only a still-flying one passes on its manual-input count. A slow lookup cancelled by a direct flight that landed, followed by a lookup that found nothing, used to drop the queued motion, and a cancelled direct flight in the same place let it fall back to an older landing.
A motion queued behind a voice fly-to acts on the last flight that moved the camera. A later lookup that was not found, threw, or was cancelled once a newer voice fly-to had begun moved nothing, so the motion looks past it rather than refusing or running early and cancelling the flight still in the air. It never acts at a flight older than the one it started waiting on. A lookup cancelled while still the latest voice flight (a search, a share link, Cockpit) still refuses.
The top-down no-op now reports the camera's real pitch.
A motion queued behind a voice fly-to now follows a later fly-to in the same reply whether the earlier one arrived, was cancelled or failed. A geocode that loses navigation reports failed, so the motion used to be dropped; and a flight left flying while the next place is looked up could land and run the motion there, taking navigation and cancelling the newer flight. The deadline, abort and stale-reply checks still end the wait.
pitchDeg -90 or -89 from a top-down view is now unchanged rather than a 1° tilt up, and the tilt tests' pitch loops are bounded.
Asking from the chat composer to open Voice Settings or Voice Preview
closed chat around the focused input, leaving focus on a hidden control.
Focus held in chat now moves to the requested view's tab, as the panel's
own tabs move it; focus anywhere else stays put.
Three 40 ms actions in one round outlast a 50 ms deadline unless each
action re-arms it, so removing that re-arm now fails the test for both
providers.
The conversation is kept to its newest 60 messages, so once full it no
longer grows as a message is sent, and a reader who had scrolled up was
left behind by their own request. The view now spots a send by the newest
user message's id instead of the list's length.
The attach menu promised up to 10 KB per file, but the attachments share
one 10 KB budget, so two 6 KB files were refused. The hint now says the
limit covers them all, in every locale.
With Voice Preview off the voice panel never opens on its own, and chat
Settings hid the voice preferences, so the Preview toggle, inactivity
timeout and model tier could not be reached by pointer. Chat Settings now
show them below the chat options, still in chat's frame and without the
voice tabs.
set_panel_open for voice-settings or voice-preview reported success while
chat's open state kept the voice panel hidden, minimized chat included. A
successful explicit show now closes chat, which keeps the typed session,
its running turn and its messages; reopening chat restores them.
The streamed oversized body was invalid JSON, so the loader returned
nothing whether or not the byte cap applied. Pad a valid catalog with a
usable camera instead, so only the cap can refuse it.
TranStar's Content Usage terms prohibit copying, distribution and
external framing of its images without TxDOT's prior written
permission. The opt-in text only said the pack was off unless
CCTV_HOUSTON_ENABLED=1, without saying what an operator needs before
setting it. Say "enable only if you hold TxDOT's written permission" in
.env.example, the README, DATA_SOURCES.md, CURRENT-STATE and the
changelog, matching the NYC and Florida packs, and fix the column of the
.env.example comment.
The houston-cctv credit now reads "(Houston, when enabled)" and links
the Content Usage terms, like the Florida credit. Where the docs said
frames are "never stored or redistributed", they now say what happens:
frames are relayed on demand, buffered only per request, sent with
no-store and not archived.
TranStar's `dir` field is the side of the freeway a camera sits on, not
the way it faces. TranStar's own map script (translateCameraDirection)
shows it as "Traffic closest to the camera is traveling": South is
eastbound, North westbound, West southbound and anything else
northbound. The live catalog agrees: I-10 cameras are North/South and
I-45 cameras East/West.
Treating `dir` as a high-confidence facing pointed most Houston cameras
across or away from the road they watch, and dropped the "estimated"
marking. Map it to the travel direction instead (S 90, N 270, W 180,
E 0) as a low-confidence prior with the low-confidence pose, and keep
the id-hash fallback for None or unknown values.
npm run changelog consumes changelog.d fragments at release, so a test that reads the fragment would fail with ENOENT afterwards. CURRENT-STATE is permanent.
FAA marks line breaks in TIMESOFUSE with a literal %, which 56 live
restricted-area cards showed verbatim ("SAT - SUN; %OTHER TIMES BY NOTAM").
Read each marker as a '; ' separator, absorbing a semicolon already before
it.
The changelog fragment named share-link token 6 (sea marks); the layer
reserves 8. CURRENT-STATE said voice could toggle and query the layer and
listed it among the analyst record sources, but it is in VOICE_OFF_LAYERS.
Say voice is off for now, and that the layer covers FAA prohibited and
restricted special-use airspace only, not the UAS Facility Map / LAANC grids
or TFRs. A test pins these claims to the token ledger and the voice manifest.
A foreign Origin or Sec-Fetch-Site: cross-site request was answered and
counted against the client's 60-a-minute quota, and could start a refresh.
Run the keyless same-site checks the other map routes use before rate
limiting and cache acquisition; reverse-proxy forwarding headers stay
accepted. List the route with the keyless map routes in SECURITY.md.
The refresh signature left out the polygons, and these rows carry no
timestamp that moves with the geometry, so an amended boundary with the same
attributes kept the old shape and card anchor until the layer was destroyed.
Add a deterministic hash of each zone's rings to the signature.
The source dropped the proxy's stale flag and fetch time, so the layer row
read nominal and freshly updated through an outage, and the proxy served its
last snapshot indefinitely. Carry stale and fetchedAt through the source as
Fire Perimeters does, date a stale snapshot by its fetch, and stop serving a
snapshot more than 24 hours old.
An upstream answer whose zones all fail validation normalized to an empty
list, and paging that stopped at the page limit while the service still
flagged more records was accepted as complete. Both were cached as a fresh
snapshot for an hour and replaced the last good one. Treat them as failed
refreshes so stale-on-error keeps serving the previous snapshot.
A still-tagged feature without exportable geometry already keeps its old
line. A newly tagged one had nowhere to go and was dropped silently; it is
now listed as skipped with a warning, so a refresh never loses a site or
hides a broken one.
From the whole-globe view the camera could end up facing empty space with
only stars on screen. Cesium's free look (Shift+drag) turns the camera in
place, and from 18,000 km the planet's edge is only about 15 degrees from
nadir, so a few drags turned it out of frame. The rolled pose was then
saved into the share hash, so a reload or shared link opened on stars too.
- Allow free look only below 1,000 km. Orbit, tilt and zoom are unchanged.
- Measure how much of the frame shows the planet's surface by casting a
32x18 grid of view rays at the WGS84 ellipsoid. While the scope mask is
on (the default) only the rays inside its keyhole count, since the mask
hides or dims the rest of the frame.
- When a camera move ends above 1,000 km with no ray on the surface, turn
straight down at the same position and heading. Limb glow and a sliver
up to about one grid cell thick can count as out of view. The end of a
move comes from Cesium's moveEnd and from a check of its own that
samples the pose every 250 ms after moveStart and acts once it has held
for half a second: at some of these poses the camera drifts by a
rounding error every frame, so Cesium never sees it stop and never
raises moveEnd. The zoom-out guard from #1522 keeps wheel and pinch
zoom-outs in view; this catches the other ways a move can end looking
away, such as a saved or shared view that did, or a
drag that Cesium turns into an in-place look below its 7,500 km
trackball height (one that starts on the stars, or a tilt drag with the
screen centre off the planet). Views with more surface, tracked or
locked cameras and the 2D and Columbus modes are left alone.
- Voice tilt and in-place turns of a free 3D globe camera above 1,000 km
stop when about a tenth of the frame still shows the planet (or about as
much as it showed when it started, two grid cells of sampling noise
allowed), never below about 1% of the visible frame; a view that already
shows less only keeps or gains what it has. A turn that cannot go half a
degree further without crossing that limit is refused with a reason
instead of reported as done, and a turn it accepts turns at least that
half degree. A turn that keeps or gains surface is not refused, even
after the window has been resized, except that near the limit, where the
planet's edge crosses the sample grid, sampling wobble can occasionally
refuse one (most often above about 15,000 km and on phone or ultrawide
frames); nothing then turns away from the planet. A turn that starts
where the last voice turn left the camera keeps that turn's limit, or a
higher one if the planet now fills more, so repeating the request does
not take any more; once anything else has moved or turned the camera, a
turn is measured from what is on screen. A request made while the camera
is tracked is judged by the pose voice's release of the follow camera
will leave, before that release, so a refused turn keeps the tracking.
The frame measure and the look-step limit live in src/globeInView.js,
shared by the viewer and the voice camera verbs.
Main now offers osm_query, and names it in the prompt, only when Overpass
is configured. Typed chat follows the same switch on both providers: its
prompt, the tools it offers and the calls it accepts.
Lifting one finger of a pinch removed the document listeners, so the
remaining finger's drag, which Cesium carries on with, stopped counting.
A gesture now ends only when its last pointer lifts or no touches are
left. And a gesture whose end never reaches the page (the window loses
focus mid-drag, or the page is hidden) left the listeners attached, so
a later unrelated drag counted as camera input; a blur or hidden page
now stops following it and drops its pending input.
Only canvas events refreshed the 1.5 s input window, and a touch only on
touchstart. Cesium follows a drag that leaves the canvas at document
level (in Firefox, and wherever there are no pointer events), so a long
gesture kept moving the camera after the window closed, and the pulses
rested mid-gesture. From a pointer or touch start on the canvas until
its end or cancel, moves at document level now count too; the listeners
go when the gesture ends or the layer detaches.
The cockpit turns Cesium's camera controls off and moves the camera on
its own, so a click, drag or wheel on the canvas moved nothing yet still
counted as the user handling the map: the cockpit's next move woke a
nine-second pulse burst. Input now counts only while the camera controls
take it, and a gesture still pending when another owner takes the
camera is dropped.
Cesium keeps every material type in a global cache for the page's
lifetime. The pulse disc's fabric had no type, so each new animated
visual (a subject switch, a reselect, reduced motion turned off) added a
GUID-named entry that was never removed. Name the type, and give each
material built from the cached template its own uniform objects back.
Brings #1518 up to date with main. The only conflict was
scripts/format-scope.json, where main restored a block of entries
(#1527) next to this branch's addition. Resolved as the union of both
sides: every entry from main and from this branch is kept, the
duplicate rateLimitKeys line is dropped, and every listed path exists.
Against main, this branch now adds only
src/layers/awareness/rangeRing.test.mjs to the list.
Re-pin the Realtime tool payload hash for the merged tree (voice geometry plus pitchDeg). To fit main's 55,500 prompt ceiling with Overpass on, the pitchDeg schema drops its -90..90 range (the runner already clamps to -89..-5, now pinned by a test) and its description is shorter. The Overpass prompt measures 55,492.
At or within about 1e-7 degrees of a pole the latitude/longitude
destination formula cancels out, and the 128-point ring collapsed to
three distinct points joined by chords of about 500 km. Step along the
great circle in 3D from the centre's up, north and east unit vectors
instead. Results at ordinary latitudes and across the antimeridian are
unchanged.
The budget test measures the full instructions plus every tool. At this
head that is 55,210, ~290 under the 55,500 ceiling; the note said 55,370,
which that measurement does not produce.
/api/osm/features keyed its rate limit and its one-search-per-client slot
by the full peer address, so an IPv6 host could rotate addresses inside
its /64 to take both global slots and slip the 12/min limit. It now uses
clientNetworkKey, as /api/overpass does.
Picks up acd87e6c: the keyless cross-site gate on /api/osm/features
(582bfce6) and main at 93804d5d. SECURITY.md, the format-scope list and
the schema-test comments take the branch's side; the place-search budget
wording from the timeout fix is kept.
While the camera tracks an entity or looks at a point, Cesium measures
zoom distance from the target, not the Earth. Raising the ceiling to the
camera's altitude then lifted it with every outward step, so a tracked
camera could zoom out without limit. The guard now follows the altitude
only for an untracked 3D camera and otherwise keeps its 40,000 km
ceiling.
The action under way when a reply stops is answered as cancelled, since it
may have taken effect, and only later calls as skipped; a stop during a
round's first action keeps that round too. The closing note no longer
claims the actions succeeded. Tests cover the refusal body read cap and
the release of an unread refusal body.
Picks up #1527, which restores the 97 format-scope entries an earlier merge
dropped; this branch had kept the same 97. The resolved list is main's plus
this PR's 12 test and helper files, 1,222 paths, all present.
The heatmap now takes over above 4,500 km instead of 3,000 km, with
the cross-fade between 3,600 and 5,400 km. The markers band caps at
40,000 detections, which covers the ~40,000 in the padded view over
Africa at the low end of the fade, so weak fires still stay as markers.
To keep that rebuild near its old cost, markers outside the close band
read the anchor already built for each fire instead of the ground floor
cache: only the close band warms floors, so the lookup there always
missed. A 40,000-marker rebuild now costs ~55 ms, down from ~120 ms.
Review follow-ups for the sonar pulses.
Wake on user input only: a camera move wakes the pulses only within
1.5 s of a pointer, wheel or touch on the Cesium canvas, where its
camera controller listens. Tracking, the cockpit view, an orbit or a
fly-to move the camera on every frame and no longer keep the pulses
running. Keys don't count: the camera controller reads none.
Below the disc: with the camera under 2.5 km the pulses rest
completely, with no disc, ring flash, echoes or frame requests.
Echo brightness: echoes scale by the contact bracket's drawn alpha,
which includes the Cyber sonar sweep, rounded to the nearest band with
no floor, so an echo is never brighter than its bracket.
Disc follow: the pulse disc follows the subject by a model-matrix
rotation about the Earth's centre and is rebuilt only every 20 km
instead of every 500 m.
Tests: a cap on burst length; teardown through disable(); the echo's
spread and alpha on the canvas; echoes under the Cyber sonar never
brighter than their bracket; waking by pointer, wheel, touch and drag
but not by keys, app camera moves or stale input; the rest below the
disc; and tighter asserts. CURRENT-STATE notes that terrain above
2.5 km still cuts through the disc and ring.
Each resolve() that raced the caller's signal added an abort listener and
never removed it, so a long-lived signal collected one listener per
completed lookup. The listener is now removed when the call settles and
is not added at all to a signal that is already aborted.
Without a limit parameter the route read null, Number(null) is 0, and the
clamp turned that into a one-feature list. An absent or empty limit now
gets the documented default of 500.
resolve_area compared the spoken name with the unit's name literally, so
"State of Texas" (admin1) and "USA" or "UK" (country) were refused with
AREA_IDENTITY_MISMATCH although the bundled matcher found exactly those
units. Both identity checks, the bundled one and the outline ladder's, now
also accept the prefix forms and same-unit country aliases the matcher
reads, at the level they imply, so the level check still applies.
"Great Britain", "Britain", "America" and "Korea" still need their own
names, and the natural-region check stays literal.
The guard's own tests pass even if createApplicationControls stops
installing it. This checks the ceiling and frame listeners are in place
after construction and gone after teardown.
Above 300 km the guard treated any shrinking pinch distance as an outward
zoom, so a two-finger tilt whose fingers drifted slightly closer had its
pitch overwritten. A two-finger gesture now counts as a zoom only when the
spacing changes more than the fingers' midpoint moves.
The guard reads its input in preRender, which Cesium only fires on frames
that draw. In request-render mode, wheel input at the ceiling doesn't move
the camera, so nothing drew and the input was applied later by an
unrelated frame. The guard now requests a frame through the render
governor whenever it has unread input.
Cesium clamps any zoom that starts above maximumZoomDistance to that
limit, in either direction, so a share link or flight that left the
camera above 40,000 km jumped to the ceiling on the first wheel tick.
While the camera is above the ceiling, the guard now uses its current
height as the limit and lowers it as the camera descends.
osm_query said "At least N" when the server capped its list, but handed
the places to the analyst engine with truncated:false, so "which of those
have emergency?" answered as if the list were whole. The handoff now
carries the cap and its caveat, and follow-ups (and their follow-ups)
stay incomplete with an unknown total.
The 750–3,000 km markers band now draws up to 20,000 detections instead
of 2,500. At the 2,400 km end of the cross-fade the padded view over
Africa holds ~21,000 fires, and the old cap kept only those above
~17 MW, so most heat spots faded into empty ground as the heatmap went
out. A rebuild of that many costs ~60 ms, and only in the densest views.
The voice answer waited 25 s for /api/osm/features, while the server let
Overpass run for 25 s and itself waited up to 40 s per endpoint and 45 s
in all. When the browser gave up first it closed the request, the route
cancelled the upstream query, and nothing was cached, so the OSM_TIMEOUT
retry started the same query from scratch and timed out again.
The server now tells Overpass 20 s, waits 22 s per endpoint and 22 s in
all, which ends inside the client's 25 s. A test pins that ordering.
A reply stopped partway through a round now keeps the actions that ran,
answering the calls it never reached as skipped, so the history stays
paired for both providers. A failed typed turn no longer resets the
pointer, referents and follow-up memory, since its conversation continues.
The server now tells a rejected key (401, or Gemini's 400 with reason
API_KEY_INVALID, matched exactly), denied access (403) and an account out
of credit (OpenAI's 429 insufficient_quota) apart; each cools down for a
minute and gets its own message in the chat.
Merge commit 34906c9a resolved the end-of-list conflict in scripts/format-scope.json with the branch's side, which dropped the 97 entries main had appended since Oct 26. Those files all still exist; 64 of them are tests that runtime discovery does not cover, so format:check had stopped checking them.
Each entry goes back at its position on main before the merge. All 1680 files pass npm run format:check.
A zoom in during a pivoting tilt stops at the range that keeps the camera 150 m above the pivot at its current pitch. A zoom that moves the range by less than half a metre (at the clearance, the 80 m floor or the 5,000 km cap) now answers not zoomed instead of ok, and a tilt reports tiltRangeAdjusted rather than orbitRadiusAdjusted.
#1308 moved /api/overpass to the keyless cross-site gate: foreign origins
and cross-site fetches are still refused, but forwarding headers are not,
so the map works behind a reverse proxy. Voice place search spends the same
operator Overpass, so it now uses the same gate; with the stricter one,
place search failed behind a proxy while the map kept working. The prompt
lint note records the merged size, 55,370 with an operator Overpass.
Picks up #1308 (Overpass admission), #114 (Sea Marks and Ski Pistes),
#1206 (radio country coverage) and #1435. Resolutions:
- SECURITY.md: main's rewritten cross-site paragraph, with voice place
search listed beside /api/overpass among the keyless map routes.
- scripts/format-scope.json: both sides. A merge on main dropped 97 entries
(being restored there); keeping them, this PR's 12 and main's 2 new ones
keeps every file in format:check. All 1,222 paths exist.
- Translation catalogs: main's two new keys and this PR's 47, no overlap.
- Layer count 36 (33 + OSM Places + Sea Marks + Ski Pistes); both schema
pins re-derived, the first-run length is exactly 30,919 + 292 + 107.
The far-zoom heatmap now uses the ½× grid (4096 × 2048, ≈10 km per
pixel, one-pixel blur), so a lone fire's spot matches its marker at the
3,000 km hand-off. The spot size slider and its share option are gone.
Markers and heatmap are both kept built at every zoom and cross-fade by
camera height between 2,400 and 3,600 km on every frame, so a fast zoom
never shows a beat with neither. The heatmap fades by imagery alpha:
hiding a Cesium imagery layer unmaps it from the globe tiles, and showing
it again re-mapped them late, which was the gap at the band edge. Under
the markers the heatmap is built 150 ms after the data or filter changes.
Typed chat sessions no longer end on the voice inactivity timeout, and a
failed or stopped reply keeps the conversation: Stop cancels only the
reply in progress, and map actions that already ran stay in the model's
context. Each upstream request and map action gets its own deadline, old
tool results shrink instead of the history overflowing, and step limits,
a missing or rejected key, the OAuth lock and throttling each get their
own message.
- Server: OpenAI chat is off under GEV_PREFER_CODEX_OAUTH; chat throttles
default to the provider's throttle; replies carry API_KEY_MISSING,
API_KEY_REJECTED, CODEX_OAUTH_REQUIRED and RATE_LIMITED codes; upstream
Retry-After passes through, and a rejected key (Gemini's invalid-key 400
included) cools down for a minute
- Pinokio forwards the chat throttle settings
- UI: the reply reveal publishes only the message list; the border orbit
runs only while busy; layers attach with a button or Enter; the log is
aria-busy while a reply types out; auto-scroll respects a reader who
scrolled up; Escape closes the attach menu; the chat gear says Settings;
a note says chat bills the OpenAI key when voice uses ChatGPT sign-in
- Layout does less work while panel height animates; unused blurs removed
- Gemini voice start ends chat; the voice preview clears on every new
session
- Tests for the route gates, client limits, attachments and the fixes
The detection overlay asked the echo field for every visible flight,
military aircraft and vessel on every frame while a subject was
selected, projecting each to the ground before learning the pulses were
resting. It now checks awarenessEchoActive once per frame and builds no
echo paths at rest, and awarenessEchoAt checks the level before the
projection.
A detectionHost test with six echo-eligible flights checks that a
resting frame asks for the level fewer times than there are contacts
and paints no echo; it fails on the per-contact path.
The pulse and echo policy doc comments are attached to their constants
again, and one long comment line is re-wrapped.
Review fixes for #1506.
- Ottawa: use the documented camera list and the city's Traffic Camera
Images API. The pack is skipped with no request unless
OTTAWA_CAMERA_CERTIFICATE is set; the certificate is added only at
fetch time inside the pinned /opendata/ path, and each camera is
fetched at most once a minute through the shared frame cache.
- Québec 511 video is opt-in (CCTV_QUEBEC_VIDEO_ENABLED=1) and loads
only after the viewer presses Play; selection, auto-hop, scenes and
voice no longer contact Québec 511. Credit splits locations (MTQ,
CC BY 4.0) from video (Québec 511). Interrupted play() is no longer
reported as an unavailable source; the per-frame listener runs only
while a Québec camera is selected and the canvas rect is cached.
- Catalog cap: reserved Québec and World slots come only from capacity
above the 4,000 shared slots, so a lower CCTV_MAX_SOURCES thins every
pack instead of starving the existing ones. dev-fresh and
.env.example default to 7,516.
- Throttled frames (World, Ottawa): inside the refresh floor with no
cached frame the proxy serves the placeholder, never Street View; a
failed fetch backs off for a minute; the cache is bounded by bytes.
- MJPEG first-frame extraction runs only for sources registered as
MJPEG (Taiwan Highway Bureau), caps a frame at 2 MB, reads in linear
time, accepts a CRLF preamble and a boundary that already starts with
"--", and ends a part without Content-Length at the next delimiter.
THB hosts are registered in the redirect pin list.
- Tests: World loader policy, Québec loader and opt-in, proxy cooldown
without Street View, Ottawa signing, cap reservations at 7,516, 4,000
and 48, overlay Play gate, refresh, visibility and teardown. Guard
tests check the 4,000 shared slots again.
- Remove the unused standalone Québec preview module and public page.
- Docs: DATA_SOURCES, SECURITY, CURRENT-STATE, CCTV_INTEGRATION, README,
.env.example, changelog fragment renamed to its full scope.
The range ring was a Cesium entity, and entities are always picked, so
a click or tap on its edge became an unowned hit: vessel selection read
it as empty space and pickContact stopped its touch search there. A
contact under the 250 km edge was hard to select.
The ring is now a polyline primitive with allowPicking false, in the
same PolylineOutline material and 4 px width the entity used, so it
draws the same. The flash while pulses run writes the material's colour
alphas from the pulses' per-frame update; under reduced motion the
colours stay at full and no listener is added. A subject that moves
250 m rebuilds the primitive and keeps the material, so a flash carries
on across the move. Viewers without a usable primitive collection
(headless tools, stubs) still get a static entity.
The ring's geometry options come from awarenessRingGeometryOptions,
tested directly, and the tests now check that the awareness visual adds
no entity and that every primitive it adds is unpickable, with and
without reduced motion.
- A motion waiting on a voice fly-to follows a later fly-to in the
same reply that cancels it, within one deadline, instead of
reporting that its flight did not arrive. A stale reply stops
following.
- The wait is 15 s, down from 30 s: it holds the tool output, and on
Gemini the later tools in the turn.
- Beyond 1,000 km of range a tilt happens in place instead of
swinging thousands of km about the crosshair point.
- The tilt-nudge limits read the pivot pitch the motion starts from,
including for a voice tilt from a tracked view, whose follow camera
is released before it moves.
- move_camera's argument checks run before the flight wait, so a
malformed call is refused at once.
- Tests: t.after cleanup in the fly-then-motion loop, the pitchDeg
pin asserts the field before removing it, and the handoff-order
pin follows the extracted argument checks.
Remaining review follow-ups for the sonar pulses.
No self-echo: pulses are born 5 km out (startFraction of the radius),
but contacts nearer than that, the subject included, still lit on every
pulse birth. They now never echo. Just past the birth ring, an echo is
scaled with the front's own fade-in, so a bracket no longer flashes
before a visible front reaches it. The ring flash at the full radius is
unchanged.
Older Safari: the reduced-motion listener falls back to the media
query's addListener / removeListener when addEventListener is missing,
as other modules here already do.
Echo wiring test: a detectionHost test now paints through the real
detection lane with an echo field set, and checks the echo stroke takes
the echo colour on the default style and each style's line colour under
CRT, NVG and FLIR. CURRENT-STATE notes the 5 km no-echo zone.
Tightening the AREAS rule dropped "to outline it (draw=true)", and on Gemini
"Outline the state of Texas" stopped passing draw: none of 5 runs set it, so
the model drew the state again with annotate_map. With the clause back all 4
runs called resolve_area with draw. The prompt is 55,263 characters with an
operator Overpass, ~240 under the 55,500 ceiling.
Review follow-up for the sonar pulses (the non-blocking items).
Unpickable pulses: the three pulse lines were polyline entities, which
Cesium always picks, so near-miss and touch picks around them became
unowned hits. They are now drawn by the pulse disc's shader, which
already drew the gradient and is built with allowPicking false. Each
line keeps the polyline outline material's look: 3 CSS px wide with a
1 px dark outline in the ring's colours, measured from the screen-space
gradient of the disc's radius times czm_pixelRatio, as Cesium's contour
material does. Without derivatives only the gradient band is drawn.
Disc follow: with the lines on the disc, it is rebuilt after the subject
moves 500 m instead of 2 km, so the pulses stay concentric with the
ring. A resting disc is hidden and not rebuilt; it catches up when the
pulses wake.
Camera below the disc: the disc sits 2.5 km above the ellipsoid with
faceForward, so from a lower camera it swept across the sky. It is now
hidden while the camera is below that height.
Echo palette: echo brackets keep the pulse blue on the default
detection style and take the style's line colour under NVG, FLIR and
CRT.
Echo test: the detection renderer's echo maths moves into
awarenessEchoBracket and awarenessEchoColor in src/awarenessEcho.js,
tested directly instead of running a regex slice of detection.js
through vm, and the one-sided spread assert is now two-sided.
The positions reuse parameter of awarenessRangeRingPositions and the
pulse segment count had no callers left and are removed.
From review:
- Card lines the actions write in English (area basis, part counts, merged
duplicates, OSM caveats, the imagery window) are shown in the UI
language through the analyst card's note path; the model still gets the
English. One OSM place reads "1 hospital".
- Same-named clarification choices show their region code (WA, DC).
- OSM Places shows as unavailable once the server says no Overpass is
configured, and its points are depth-tested past 2.5 km so they never
show through the globe.
- find_imagery puts the operator's sources and mode back when a box is
refused, and a voice search cancelled mid-flight leaves "Search
cancelled" instead of an empty strip.
- The README lists OSM Places with the other Utilities.
From review:
- The browser client for /api/osm/features gets its own tests (request
shape, abort, network failure, coded refusals), and a 200 whose body is
not JSON is now a failure instead of zero places.
- An end-to-end test runs resolve_area, osm_query and an analyst follow-up
through the real runner and checks the follow-up keeps the area; it fails
with the wiring reverted.
- The Realtime token test pins Overpass off and compares the full tool
schemas again instead of names taken from the environment.
From review: a row inside a large area pays a ray cast over every edge, so
20,000 rows against a 6,000-vertex outline made ~200 ms long tasks. The scan
now also yields once ~8 ms have passed, checked every 256 rows. An ambiguous
region refusal also tells the model the way back: resolve_area with the
picked candidateId, then analyst_query over that area.
From review:
- An answer the view picked ("Springfield" over Illinois) is reused or
shared only within the same ~2° view cell, so asking again elsewhere no
longer gets the first state's area.
- Admin names compare with accents folded on Latin letters only: "Quebec"
matches "Québec" and "Cordoba" asks between Argentina and Spain, while a
Thai or Devanagari mark still tells names apart.
- A Natural Earth name used by several features is one place with every
part when they touch (the Great Rift Valley, the Lau Group across 180°)
and a question when they are far apart (Flores); natural places can now
be candidates.
- The AREAS rule and the area tool descriptions are tighter, leaving a
~270-character margin under the 55,500 prompt ceiling, and resolve_area
no longer names osm_query when that tool is not offered.
From review:
- /api/osm/features now refuses cross-site browser requests like the other
quota-bearing routes, so a page elsewhere cannot spend the operator's
Overpass. SECURITY.md lists the route, its limits and its disk cache.
- One client runs one search at a time and two run at once in all, instead
of one global slot any single client could hold.
- A search gets 45 s across every configured endpoint in turn, answered 504
when it runs out.
- A cache write no longer waits on the disk prune; prune() waits until the
cache is idle for callers that need it.
- A zoom during a pivoting tilt now scales the range the tilt holds,
as it already did for orbit, instead of being undone on the next
frame while reporting success. The zoom limits no longer move a view
the wrong way: a zoom in leaves a view closer than 80 m in place, and
a zoom out leaves an orbit beyond 5,000 km where it is.
- A grab, touch or wheel on the map while a voice fly-to is in flight
drops the motion queued behind it. Cesium lands the flight anyway,
so the orbit or tilt would otherwise take the camera back. Chained
fly-tos in one reply keep the earliest snapshot.
- A pivot tilt toward the horizon stops lowering the camera at 150 m
above the pivot and tilts the rest in place, so a close view no
longer ends inside tiles or a hillside.
- pitchDeg is an absolute angle. A direction that contradicts it is
refused, judged from the pitch the swing starts at, and the tool
description says so.
Cesium zooms a shallow view out by backing along the cursor ray with
heading and pitch held fixed, and GEV set no zoom ceiling. Past ~700 km
the limb drops below the fixed pitch, the steps grow with height, and
the camera flies off into space.
Cap user zoom at 40,000 km (whole-Earth view; flights are not limited)
and, above 300 km during a user wheel, pinch or right-drag zoom-out,
ease the pitch down to at least 8 degrees below the Earth's limb. The
clamp yields to disabled inputs, tracked transforms and camera flights.
Review follow-up for the sonar pulses.
Bounded repaint: the pulses used to ask for a frame every 80 ms from a
setInterval for as long as a subject was shown, so a parked view
repainted at 12.5 fps indefinitely. They now run for 9 s (three ring
flashes) after a selection or a camera move, fading in and out over
1.2 s, then rest: the ring stands on screen at full strength, the pulse
lines and gradient disc are hidden, echoes stop, and nothing asks for
frames. A moving subject does not wake them, since a flying subject
moves on every refresh. The lifecycle's existing camera pose signature
wakes them.
No beat with the Cyber sonar: the setInterval is replaced by a
postRender-armed setTimeout on CYBER_SONAR_RENDER_INTERVAL_MS, as in
cyberSonarScene. Every drawn frame, whoever asked for it, re-arms the
request 80 ms later, so requests never land inside the detection
lane's 80 ms gate.
Reduced motion mid-session: a change listener on the media query
rebuilds the visual, and the same-subject refresh path rebuilds when
the preference no longer matches. The listener is removed in
clearVisual.
Readable edge: restOpacity goes from 0 to 0.4, so the 250 km ring stays
visible between flashes, in screenshots and for low-vision viewers.
The echo field takes an optional level so echoes rest with the pulses.
CURRENT-STATE and the changelog fragment describe the bounded run and
the dimmed ring.
Tests: render requests stop after the bound and stay stopped for a
minute; a camera move seen by the lifecycle wakes them for another
bounded run; a foreign frame postpones the next request to 80 ms after
it; reduced motion flipped by event and by a later refresh; the ring
never drops below restOpacity; echoes scale with the level; the level
fades in, holds, fades out and keeps its value when woken.
The Cockpit Contact counts named fixed providers (OpenSky, adsb.lol,
AISStream) while the Layers panel and Contacts named the provider the last
snapshot reported, and the vessels layer kept that name out of getStats(),
so Contacts always said AISStream. Each cohort now carries the provider and
whether it answered as a fallback or from a stale snapshot, and both views
show it the way the Layers row does ("adsb.lol · FALLBACK").
Fixes HAL-721.
A radius the user gives always draws a buffer, but the enclosing outline
was still looked up first (up to 6 s and a guarded Nominatim request) and
then thrown away. It is no longer requested. The slow-outline test now
checks the default 250 m fallback it was meant to cover.
The outline rungs recorded only the credit kind, so the voice area code
never saw 'openfreemap' or 'nominatim' and stored every such outline as
the Overpass rung, even with no Overpass configured. Voice geometry tagged
the source originally; the merge with main kept main's untagged rungs and
its test pinning the absence. The tag is restored and the test now asserts
it for both sources.
Three resolver fixes from checking the review focus:
- A natural region named like a country or state no longer hides it.
"India" was the peninsula, "Japan" an island group, "Florida" the
peninsula and "Punjab" a desert, so bare "Punjab" never asked which one.
The region still answers when asked for by level, and when no unit truly
has the name ("Great Britain" finds the United Kingdom only by alias).
- Same-named units get their own candidate: "Washington" offered WA and DC
with one id, so either choice was DC and a DC lookup overwrote a stored
Washington State. Ids now carry the region code, and choices show it.
- A bundled unit that only shares the name ("Azad Kashmir" for "Kashmir")
no longer ends the search; the outline ladder still looks, and the
mismatch is kept if it finds nothing. Retired zones stay refused.
The /api/osm/features cache key was the query alone, kept for a day, so after
an operator repointed OVERPASS_UPSTREAMS the old instance's answers were still
served. The key now includes the configured-upstream tag, as main's other
Overpass caches check it, so a new instance is always asked.
osm_query handed its places to the analyst with no presentation, so "which of
those have emergency?" after "hospitals in Kathmandu" was answered as "anywhere
in the loaded data". The handoff now carries the scope the places were found
in ("inside Kathmandu" with its areaId, or "in view"), as an analyst count over
that area would.
Every name ending in "Zone" was kept without a level and refused as a retired
administrative identity. That is right for Nepal, which replaced its zones in
2015, but zones are current units elsewhere: Ethiopia's "Gamo Zone" was
refused. The selector now keeps the verified level unless display_name places
the zone in Nepal or names no country, and the client no longer reads "Zone"
as a retired level when the source verified one.
The maintainer workflow asks runtime changes for a changelog.d/<pr>-<slug>.md
fragment and no edit to CHANGELOG.md. The three entries move unchanged, with
their credit, into changelog.d/846-voice-geometry.md.
The cancelled and failed branches ran before the say-line branch, so a
cancelled panel change no longer said "Cockpit exited; the voice panel was
left as you last set it", and AREA_TIMEOUT / OSM_TIMEOUT got "tell the user it
failed" instead of the tool description's "retry once". A cancelled or failed
result with a say line now has it spoken, and those two timeouts are retried
once. Both providers share this instruction.
"Fly to Paris and orbit it" reaches the app as two sibling tool calls,
which OpenAI runs at once. With a place that needs a lookup, the orbit
started over the old view and the late flight then replaced it; with a
known place, pan and rotate cancelled the flight already in the air.
Orbit, pan and rotate now wait for a pending voice flight the same way
tilt does: up to 30 s, and not run (saying so) if the flight is
cancelled, finds nothing or times out. Stop and rotate_globe do not
wait. Orbit keeps its own arming for flights the voice runner does not
track.
"Fly to London and tilt the camera" reaches the app as two sibling tool
calls, and OpenAI runs them concurrently. The tilt's navigation step
cancelled the flight already in the air, so the camera tilted where it
was; when the place still needed a lookup, the late flight replaced the
tilt and London came out top-down.
The voice runner now tracks its latest flight. A tilt sent while that
flight is still looking up or flying waits for it to land (up to 30 s),
then tilts at the destination. If the flight is cancelled, finds nothing
or times out, the tilt is not run and says so.
A voice fly-to frames a city top-down, at pitch -90. The tilt clamp
rejected any step that left the pitch below -89, so the first frame of
"tilt up" stopped the motion and "tilt down" was refused: from the view
voice lands on, no tilt ever moved the camera. A bare "tilt the camera"
also failed because tilt required a direction.
- Clamp tilt only in the direction of travel, so -90 can tilt up.
- Swing the camera about the ground under the crosshair, so the place
stays centred and the heading is kept. A tracked view still tilts in
place, and a pivot tilt yields when tracking takes the camera.
- Add an optional move_camera pitchDeg for "tilt 45 degrees"; a tilt
with neither angle nor direction settles at -45 (oblique view).
- Re-derive the voice tool schema pins for the new argument and wording,
and keep the Realtime prompt under its 52,000-character budget.
Picks up the Radio Next/Previous fix (#1474) and the Codespaces docs
(#1515). The one conflict is the Radio cycle assertion in
gevActions.test.mjs: main's new fallback flag is kept in this branch's
layout.
Pulses: three dim rings grow from the Global Context subject to the
250 km range ring at constant speed, one every 3 s, each taking 9 s.
Each fades in over its first 8% of travel, then fades as
(1 - travel)^1.5 to about a third of its 60% peak, so it arrives at the
ring dim but visible. They are 3 px in the ring's colours, as polylines
with CallbackProperty positions and colours that share the ring's
centre object, so they follow a moving subject.
Ring: while pulses run, the 250 km ring rests unseen and flashes to full
strength as each pulse reaches it, fading over 1.8 s. Without pulses
(reduced motion, or a scene with no primitives or preRender hook) it
stays on screen as before.
Gradient: behind each pulse a radial band in the same blue fades inward
over 35% of the radius. One flat, translucent, unpickable 250 km disc
draws it with a small Fabric material. A preRender listener writes the
pulse radii and opacities into two vec4 uniforms each frame. The disc
(0.25 degree granularity, ~3.5 ms to build) is rebuilt only after the
subject moves 2 km.
Echoes: as a pulse front crosses a flight, military aircraft or AIS
vessel, the detection overlay draws a second bracket in the pulse blue
around that contact's own bracket, spreading outward as it fades over
1.4 s. Awareness publishes the field (centre, radius, pulse timing,
echo styling) through the new src/awarenessEcho.js. The overlay reads
it, so no layer's icon or colour is touched. Mapped installations have
no detection bracket, so they do not echo.
Rendering cost: pulses never hold continuous rendering. They ask for a
frame every 80 ms, the Cyber sonar's parked cadence, so a parked
Contacts view repaints at ~12.5 fps while a subject is shown. This
changes the "parked Contacts stays quiet" behaviour on purpose.
clearVisual stops the timer and removes the disc, the listener and the
echo field.
awarenessRangeRingPositions can now reuse a result array, and passes the
WGS84 ellipsoid explicitly so a reused Cartesian never lands in the
ellipsoid argument. Tuning lives in AWARENESS_RANGE_PULSE,
AWARENESS_RANGE_ECHO and AWARENESS_RANGE_RING_STYLE. src/awarenessEcho.js
is registered in the awareness-layer, application-components and
application-layer-construction packages.
Tests cover:
- the pulse curve and calm rate;
- the ring flash once per arrival, and the static ring without pulses;
- the 80 ms frame cadence and its stop;
- the partial-scene fallback;
- the gradient disc: unpickable, uniform values, rebuild and cleanup;
- per-frame evaluation, and the echo field following the subject;
- awarenessEcho strength, rim wrap, layer filter and altitude handling;
- the detection renderer's echo-bracket band and outward spread.
The Global Context range ring was a Cesium ellipse outline at 1 px in
rgba(98,181,255,0.72). WebGL ignores larger ellipse outline widths, so it
could not be thickened, and it vanished over desert and snow and under the
FLIR, NVG and CRT styles.
Draw it as a polyline instead: a 4 px line with a 2 px Context-blue core
inside a 1 px dark halo on each side (PolylineOutlineMaterialProperty).
The halo gives luminance contrast on light and dark ground, which the
monochrome styles keep. The ring sits at the same 2.5 km height and
follows the subject on the same 250 m threshold. Positions come from a
new awarenessRangeRingPositions helper that stays within 50 m of 250 km.
Style values live in AWARENESS_RANGE_RING_STYLE (awareness/policy.js).
Adds rangeRing.test.mjs (geometry at equator, Sahara, Greenland and the
antimeridian; contrast; entity shape; follow threshold) and a
CURRENT-STATE note.
A ½×–4× slider (log2 axis, 5 % steps, default 1×) under the intensity
filter sets how large far-zoom heat spots are. It scales the grid, not
the blur: 2× is a 1024 × 512 grid with the same one-pixel blur, so each
spot grows exactly and weak lone fires don't fade as they widen. A change
rebuilds only the heatmap. Share links keep it as w.s (percent, 50–400).
The layer row now takes a list of sliders.
Dropping each detection whole into its nearest pixel snapped spots by up
to half a pixel and left weak lone fires as squares or plus signs once
thresholded. Share each detection's weight between the four nearest pixel
centres instead (linear binning, Wand 1994): spots sit at the true
position and blur round, at the same build cost.
"Over {area}" put a country name straight after a preposition, which drops
the article Italian, Portuguese, Spanish and German need ("Sopra Italia",
"Über Schweiz"). Use the label form French and the "In {area}" line already
use. zh-Hant now says 服務器 like the rest of its catalog.
- keep inserted place names clear of prepositions and articles they
would need in fr, pt, ru and uk
- use each catalog's own terms: punto de interés, trazar contorno,
Buscando, 加載/匹配/搜索 and full-width colons in ja
- list the Italian "Area" as intentionally the same as English
- strip control and format characters from OSM tag values, keeping the
zero-width (non-)joiners Persian and Indic names spell with, and cut
them at 120 code points so a name cannot steer or flood the model
- adopt the nine new geometry test files in format-scope.json; five of
them change by formatting only
Both stack parents (#837, #842) are on main, so the voice geometry branch
now builds on main directly. Conflict resolutions keep main's behaviour and
add the geometry work on top: osm_query gating on every provider, restored
admin identity for Nominatim outlines, merged analyst follow-up memory, and
the cancelled Globe View reset instruction ahead of the generic cancellation.
Between real time and an hour a second there was nothing to watch a few
minutes of weather or a launch play out at. The speed menu now offers a
minute a second as its second choice.
The zoomed track now drags: the handle still drags the time, while the
track around it drags the view along (or the time, with all of it shown),
a tap goes to the time under it, two fingers pinch it, and a sideways swipe
or a shift-wheel drags it too, the same with a mouse, a trackpad or a
finger. A view dragged off the handle stays put until the time moves.
Play moved in steps: the world clock told of its time four times a second,
so the handle and the globe jumped, and play stopped at every weather frame
while it drew, loaded ahead or not. The clock now tells of its time each
animation frame, and play no longer waits on the frame it is at: that
frame loads as play goes on, and play holds only at the next frame while
the one before it still loads. Weather loads its next frame ahead while the
world plays too, tells its listeners of frames as they change rather than
every frame, and the sun and moon move on each minute of world time.
Resolve the CURRENT-STATE.md layer table by keeping main's realigned
table and adding this layer's row. Main added an Arabic locale, so
translate the 12 layer strings and list ar.json in the drone-airspace
package.
Play holds while a layer loads the frame for its time, and the bar's only
sign of it was a faint pulse, so a stopped handle read as a stall. The
bubble now shows a small spinner beside the time while play is held. It
comes on once a hold lasts long enough to notice and stays through a run of
slow frames, so neither a quick frame nor the gaps between frames flash it.
With layers of very different reach on together, a month of launches and
a few hours of lightning, the short-range ticks were crushed at the end of
the one shared track. Scroll or pinch on the track now zooms it around the
handle, plus and minus zoom from the keyboard, and zero or a double-click
shows it all again. Everything still plays together; zooming only changes
what the track shows, and the view follows the handle as it plays, steps or
is left behind by now. A scrub's live edge goes by the stretch shown, so a
zoomed track can land minutes from now, and a double-click or pinch puts
back the time its first press moved.
At 10 km per pixel each fire was a marker-sized spot that faded into the
imagery from the globe view. A 2048 px grid with the same one-pixel blur
makes every spot exactly twice as wide (~60 km for a lone weak fire),
still separate, and the build is three times faster.
The heat raster was 2048 px wide with a 3 px blur, so every fire spread
over ~200 km and the far-zoom map read as clouds next to the markers.
Double the grid to ~10 km per pixel and blur by one pixel: a lone fire
is now a spot about as wide as its marker at the 3,000 km hand-off.
Play no longer runs ahead of the data. The world clock gains a wait, which
holds continuous play where it is while a layer loads what the time needs,
and a gate, which keeps play from passing a time a layer has yet to load.
Weather gates play at its next frame and waits there until the frame shows,
so every frame is seen however fast play runs; it waits no more than eight
seconds on a frame that never comes, and takes a wait only when its frames
change. Play pressed while a frame loads waits for it too. A scrub still
drops what loads and moves on.
The bar loses its long date and the dates at its ends: the time rides in a
bubble over the handle, which pulses while play waits, and the bar runs
left to right in every language. Play starts at real time.
A speed menu beside play offers real time, an hour, a day or a week a
second, a day a second to start with. A speed chosen while playing applies
at once; chosen while weather's own replay steps its frames, it takes
playback over from where it is. Play starts again from the top only at live
or at the very end, so a pause just before the end plays on. On phones the
menu sits beside the track, a full-size target.
Cesium sets no limit, so zooming out left the globe a speck among the stars.
On the globe a hand now stops zooming out where the globe spans 30% of the
screen's height, which follows the screen's shape as the app's own
whole-globe view does. A camera already past it, after a flight or a turned
screen, stops there rather than jumping in at the next zoom. Flat maps keep
zooming out unlimited.
Chat sits next to the mic in the command dock. Replies are silent and never
request the microphone; the first message connects with the selected provider
and runs the same map actions as voice through each provider's text API
(OpenAI OPENAI_CHAT_MODEL, Gemini GEMINI_CHAT_MODEL via generateContent, which
never starts Live). Only one mode runs at a time.
- Chat panel rendered by the voice control presenter: conversation, recent and
quick-command suggestions, Up/Down history, attachments (map-layer
references or small UTF-8 text files, sent only with the message)
- Chat and voice panels sit above the dock or dock into the right rail's
column; minimized bars sit centred above the dock, and height animates on
swaps
- A centred panel steps down to its bar for the Location and Visual Presets
trays without moving focus, rests above an open tray, and docks right while
expanded over one; it returns to the centre once the trays close
- Minimized voice previews the last sentence from each side; the empty Voice
Preview suggests things to say
- Settings opened from chat keep chat's frame and show the shared AI provider
and quick commands; voice Settings show Back to voice
- Closing chat ends a typed session; the voice panel's close turns voice off
- The dock widens its voice control for the chat button and restyles its
wings, which show icons only at 900px and below; assistant panels share the
left panels' glass, and Cyber keeps its own surfaces
- Server text-chat handlers with body, time and per-IP limits
(GEV_RATELIMIT_*_CHAT_PER_MIN), documented in SECURITY.md; tests and all
eleven locales
Co-authored-by: Darren Dignam <[email protected]>
Away from live, a launch shows from its lift-off on: its marker, label,
paths and orbit stay hidden while the world time is before it, so scrubbing
or playing brings the launches up in the order they flew. The selected
mission stays whenever it flies, and live shows every launch as before.
Play on the timeline runs the whole track in about a minute, from its start
when at live, and runs the clock itself past weather's own replay. It goes
live when it reaches now, or holds at an end that lies ahead of now. Going
live puts the rate back to 1.
The heat scale is set by the hottest regions, so most of the world sits
at the low end. The ember edge was a dark red at 35% opacity and nearly
vanished into forest and savanna imagery: over the Americas the spots
were there but hard to see. The edge is now a bright red starting at
58% opacity, which reads at least as well as the old blue did.
About 90% of detections are under 20 MW, so the old blue and teal
stops painted most of the map in cold colors that do not read as fire.
The ramp now runs ember, red, flame, orange, gold and white-hot at the
same thresholds, each stop lighter than the one below, so the brightest
marks are the strongest fires. The heatmap uses the same colors.
Main moved the layer rows to Vue, so the fire confidence switch and
minimum-FRP slider now render through RowSwitch and RowSlider on the
kit's UiSegmented and UiSlider, with the same classes as before. The
fire filter strings are translated into Arabic.
The timeline shows whenever a layer with time to show is on, live or not.
Its track runs over the time those layers cover, reaching to now, with a
tick for each launch or frame and a handle to drag; arrows step between the
ticks, and a scrub to now goes live. The bar sits along the bottom of the
screen, and the dock and everything anchored above it rise by its height;
clean view, recording and the cockpit put it away with the dock.
Layers whose data has a time of its own follow the page's world clock from
the start; live feeds such as flights, military, vessels and transit stay
live. Launches register the span of the launches loaded, so turning them on
brings up the timeline. Each timed layer registers its coverage once and
reports none while off, telling the clock after each step has settled. The
launch feed keeps its own window, and an orbit ring is drawn again once the
world time has moved far from when it was drawn.
The bar now needs more than the world clock being away from live: a layer
that moves with the clock must be on with time to show. In Core that is
weather radar, satellite or lightning with frames; a host adds layers by
following them. Turning the last one off hides the bar; turning it back
on brings it back.
The world clock reports this as `supported` in its state. Weather products
register their frames as coverage one by one, suspended or not, so the way
back to live stays on screen with reduced motion. Weather refreshes and
satellite catalog changes tell the clock their coverage may have changed.
A coverage that throws counts as none, and the clock reads its state once
per notification round.
A bar above the command dock, like a video player's: play and pause on
the left, the world time's date and time in UTC beside it, and Live to
return. It stays hidden while live, so Core looks as before until weather
history is scrubbed or replayed, a host moves the clock, or a link made
away from live is opened. Adds the pause and play_arrow glyphs and the
strings in every catalog.
Field 28, worldTime, holds the world time of a link made away from live:
zigzag seconds since 2026-01-01, like field 8, but it may lie ahead of the
link's creation (a forecast moment). Opening such a link pauses the world
clock at that time; a link without it leaves the clock alone, so a page
opens live and Back or Forward does not stop a replay. Times no JS Date
can hold are dropped.
Core told time eight ways: Date.now() in most layers, JulianDate.now() for
flights and military, Cesium's clock frozen at page load for the sky, the
weather clock for observed imagery. They now read one clock,
src/time/worldClock.js, exported as ./time.
Live is a mode, not a timestamp: while live the clock is the wall clock,
so every layer draws as before. Away from live only the layers that follow
the clock move with it. The weather products follow it, as they followed
the weather transport; a host opts others in with follow(id).
- The weather clock takes its time from the world clock and drives its
replay, stepping its frames as before.
- Satellites, launches, flights and military (still 30 s and 15 s behind),
transit, GNSS, Recent Imagery, street level, the HUD and the celestial
ring read worldTimeFor(id). Cadences stay on the wall clock.
- Cesium's sky (stars, sun, moon, atmosphere, lighting) draws at the world
time each frame instead of the moment the page loaded.
- Weather frames and satellites declare coverage() for a host's timeline.
A one_side line that doubles back within 5 m of itself, such as a
turnaround drawn as one line, matched its own return leg and took a
two-way half share with no partner. Flow index pieces now carry their
source line, and a radiusFor of 0 leaves a piece out of nearest(), so
pairing skips the line's own pieces.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
TomTom draws a two-way street whose directions differ as two one_side
lines on one centreline, and each got a one-way road's full share of
simulated vehicles, so the street drew twice the dots of the same street as
one full-coverage line or a two-way OSM road. A one_side line with an
opposite-direction one_side line within 5 m of at least two of three points
along it now gets a two-way direction's half share. On the decoded Austin
flow fixture every one_side line is paired, and road length weighted by
direction share falls from 553 to 417 km. Pairing a 16-tile area takes
about 41 ms per road pass. Refs #1455.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Register Rio's two keyless GTFS-Realtime VehiclePositions feeds from the
city transport secretariat (SMTR): municipal buses (SPPO) and the
Mobi-Rio BRT, the Transit layer's first Latin American region.
Both feeds publish Position.speed in km/h instead of m/s. A new optional
registry field, speedUnit: 'kmh', makes buildTransitSnapshot convert it
once, so every speedMps consumer stays correct and other feeds are
untouched.
Document the source in DATA_SOURCES.md and update the region lists in
README.md and docs/CURRENT-STATE.md.
Refs #1480
Co-Authored-By: Claude Opus 5.5 <[email protected]>
uk.json: keep both sides' new keys. The new it and pt catalogs get the fire
filter strings; "≥ {value} MW" (it, pt) and "Nominal+" (pt) stay as in
English, like es.
Heat was a detection count, so raising the minimum-FRP slider removed most
detections and the hot spots faded to blue even where strong fires still
burned. Each detection now adds its FRP in megawatts (0.5 MW floor), so weak
fires carry little heat and filtering them out keeps strong-fire areas hot.
On the live payload, hot spots that stay yellow or hotter at >=100 MW rise
from 7% to 17% and their median heat from 0.10 to 0.38.
Locale conflicts (es, ru, zh-CN): keep both sides' new keys. The new uk
and zh-Hant catalogs get the fire filter strings; zh-Hant keeps
"≥ {value} MW" as in English, like zh-CN.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The global and regional LOD bands drape the heat raster on the active
imagery host and draw no rectangle entities; the 2/1 degree aggregation now
only feeds the "N FIRES" cards, whose accent is the strongest fire's FRP
color. Markers below ~750 km hide the heatmap, as do disable and destroy.
A stale raster is rebuilt immediately on first paint and with a 150 ms
trailing debounce afterwards, so a slider drag does not re-encode on every
step. The shell hands the layer its imagery host, so the heatmap follows
globe and 3D-tiles map stacks.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Detections are binned on a 2048x1024 equirectangular grid weighted
1 + log10(1 + FRP), Gaussian-blurred (longitude wraps), normalized on a log
scale against the 99.5th percentile and mapped blue -> red with an alpha
ramp that is fully transparent below the floor. heatImagery drapes the bytes
as one full-globe SingleTileImageryProvider on the active imagery host.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The ellipsoidal horizon test hid ground below the ellipsoid (low-geoid
coasts, the Dead Sea) from a camera near it, so it is now tested at 0 m.
With the globe hidden the ground is tested as the vertical from 0 to
9,000 m against the frustum planes, in place of a 4.5 km sphere that let
in much unseen ground.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
An unsampled base sits at ellipsoid height 0. Over high terrain seen at an
angle that point projects below the screen while the ground above it is in
view, so the gate skipped ground the camera could see and never sampled it.
The gate now tests loaded globe terrain when there is some, and otherwise
the whole span of heights a sample can accept (0 to 9,000 m).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
scene.sampleHeight reads only surface rendered for the current view. For a
data centre or dam base outside the frustum or over the horizon it read
through the Earth (about -6,344 km over Ashburn on Esri), failed the 9,000 m
check and retried every 2 s indefinitely, one offscreen render per record.
Over Ashburn that held rest frames at 58-589 ms. Unseen bases are now
skipped without being stamped, so each is sampled on the first walk that
sees it, and they no longer arm the retry render.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Coloring each far-zoom cell with the ramp stop of its strongest detection
turned a burning region into a blue / teal / lime mosaic: neighbouring
cells flipped hue on one outlier and the cold stops did not read as fire.
Cells say where it burns, so they go back to the heat-normalized score
(density, night share, max FRP) on the warm end of the ramp — lime, amber,
red — with the cell card accent matching. Single detections keep the full
six-stop FRP ramp.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
The switch had no visible word saying what ALL / NOMINAL+ / HIGH refer to,
and "Min FRP" assumed the reader knows the acronym. The segmented row
descriptor gains an optional visible `label` (a caption above the group);
the fires row uses "Confidence" and "Fire intensity (FRP)", and the slider
value reads "≥ N MW" so the threshold is still clear. Locales updated.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Describe the six-stop FRP ramp, the slider / confidence switch / counter,
the single filter point, the share-token options and the tool arguments in
docs/CURRENT-STATE.md, and add changelog.d/1426-fire-intensity-filter.md as
CONTRIBUTING asks for runtime changes.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Move the share-link token to 6 (3 is gnss-integrity), build altitude
ranges from LOWER/UPPER VAL, UOM and CODE, mark card and legend strings
for translation, rename the layer to Prohibited & Restricted Airspace,
move the changelog entry to changelog.d, and keep the layer voice-off
while the Realtime prompt is at its budget.
Color every FIRMS detection by fire radiative power on the Portolan
cold → hot ramp (blue · teal · lime · amber · orange · red at
0 · 5 · 20 · 50 · 100 · 300 MW), replacing the three warm stops that
made a 2 MW and a 40 MW fire look alike. Heat cells, glow sprites and
card accents all read the same ramp (src/data/firmsIntensity.js).
Add a minimum-FRP log slider whose track is painted with the ramp (so
it doubles as the legend), an All / Nominal+ / High confidence switch
and an "N of M shown" counter to the Active Fires row. The filter runs
once in ingestion.applyFilter(), so markers, cards, heat cells, the
context store, the top-fires query and the analyst records all see one
set. Default is 0 MW / All: many of the strongest fires are rated low.
Persist both options in the layer state (token w: minFrp `f`,
confidence `c`), including the short share codec, and give the agent's
get_active_fires tool matching min_frp_mw / confidence arguments.
Layer panel gains generic `slider` and `segmented` row descriptors
(src/ui/rowFilterControls.js). New strings in es / ru / zh-CN.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Adds MIC-7522-Z30B, MIC-7502-Z30B and MIC-7130-PB4 (MIC IP starlight 7000
HD) from the CCTV Camera Database v2.29.0 model-enrichment extract. Colour
siblings (B/G/W) share one datasheet, so they and the register's x wildcard
(MIC-7522-Z30x) are aliases of one record. The MIC datasheets state FOV
without an axis, so these records add specs but no horizontal FOV range.
Refreshes the dataset size in the module header and DATA_SOURCES.md.
TranStar's disclaimer prohibits copying, distribution and external framing
of specific images without TxDOT's prior written permission, so the pack
no longer loads by default. It now uses envOptIn and loads only when
CCTV_HOUSTON_ENABLED=1, the same pattern as NYC DOT and Via Lietuva.
- DATA_SOURCES.md: replace the licence column with the disclaimer's terms
and a link, and document the opt-in
- Ambient stills for Houston refresh every 3 minutes, matching TranStar's
stated snapshot cadence
- README, CHANGELOG, CURRENT-STATE and .env.example describe the pack as
opt-in; the README camera count no longer includes Houston
- Tests cover the default-off and =0 cases and the opted-in catalog path
Register the keyless TranStar snapshot catalog as a live CCTV source,
synthesize frame URLs from validated numeric camera ids, and keep the
nearest cameras to Houston's metro cores under the existing catalog ceiling.
Read and write files as UTF-8 with LF line endings so the script and
--verify work on Windows, where the default codec is cp1252 (thanks to
quanticsoul4772 for the report). Geofabrik keeps only its newest dated
extract, so --extract now defaults to the current one and accepts a dated
name for --verify while it is listed; the dataset README says so. Downloads
time out instead of hanging.
Show US prohibited and restricted special-use airspace from the keyless
FAA SUA FeatureServer as an off-by-default Infrastructure layer, with
share-link token 3 and an informational-only disclaimer.
Add a maintainer script that merges a pinned, MD5-verified Geofabrik Norway
extract (osmium tags-filter telecom/building=data_center) into the bundled
datacenter layer. Only features inside the Geofabrik Norway boundary are
touched; unchanged features keep their original line.
norway-261005 (OSM 2026-10-05) adds Troll Housing (w1551029155) and updates
tags on four existing features. Provenance is recorded in the dataset README
and the bundled count moves from 4,351 to 4,352.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- turn named places and mapped landmarks into reusable bounded areas
- count loaded records inside resolved, drawn and annotated geometry
- find recent imagery for an area, the view or the pointer snapshot
- keep osm place search behind an operator-configured overpass endpoint
- preserve cancellation, ownership and follow-up memory across every action
- cover geometry, provider lifecycle, cache isolation and voice routing
The fleet tick dead-reckoned, floored and distance-tested every contact on
every ~80 ms tick, then hid the ones behind the horizon. The worldwide feed
carries ~13k contacts; zoomed in, well under 1% are on screen, so nearly all
of that work produced a hidden billboard.
A contact that is already hidden and still behind the horizon at its last
position now re-reckons only on its turn in a staggered one-in-12-ticks
rotation (about once a second, HIDDEN_CONTACT_DR_TICKS). Anything that
position puts in view, and any contact with a fleet model, takes the full
pass immediately, so on-screen behaviour is unchanged. Hidden-inclusive
proximity queries (getNearby includeHidden, getAllPositions) read positions
at most about a second old, well inside the 30 s poll's uncertainty.
Measured in Chrome on a Radeon 780M, ~12.9k contacts, 56 on screen over
Colombia at 4 km, same view before and after:
fleet tick 7.66 -> 2.03 ms/frame, longest tick 50.9 -> ~16 ms
frame 20.6 -> 14.0 ms, 42.9 -> 57.8 FPS
Teleporting the camera to Madrid showed 975 contacts within 100 ms.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
# OAuth in Provider Settings; missing or expired Codex sign-in then fails closed.
# GEV_PREFER_CODEX_OAUTH=true
OPENAI_REALTIME_MODEL=gpt-realtime-2
# Cheaper voice tier, used when the GEV MIC panel's MINI toggle is on.
@@ -211,6 +218,7 @@ AISSTREAM_API_KEY=
# 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
# OTTAWA_CAMERA_CERTIFICATE= # City of Ottawa Traffic Camera Images API certificate (free; request it via the form on traffic.ottawa.ca 'About the data'); without it the Ottawa pack is skipped
# 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
@@ -231,6 +239,8 @@ AISSTREAM_API_KEY=
# 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_ICELAND_ENABLED=1 # set 0 to disable the Vegagerðin (Iceland) road cameras
# CCTV_ICELAND_MAX_SOURCES=250 # cap cameras loaded from Iceland (~500 views 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
@@ -256,13 +266,22 @@ AISSTREAM_API_KEY=
# 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_HOUSTON_ENABLED= # off by default; set 1 to load Houston TranStar cameras only if you hold TxDOT's written permission (TranStar's Content Usage terms prohibit copying, distributing or framing its images without it; see DATA_SOURCES.md)
# CCTV_HOUSTON_MAX_SOURCES=250 # cap cameras loaded from the Houston TranStar catalog (nearest metro cores first)
# CCTV_HOUSTON_CAMERAS_URL= # override the Houston TranStar catalog URL
# CCTV_A22_ENABLED=1 # set 0 to disable the Autostrada del Brennero (A22, Italy) webcams
# CCTV_A22_URL= # override the Open Data Hub A22 webcam catalog URL
# CCTV_MAX_SOURCES=4000 # catalog-wide ceiling, shared round-robin across packs
# CCTV_OTTAWA_ENABLED=1 # set 0 to skip City of Ottawa cameras (also needs OTTAWA_CAMERA_CERTIFICATE)
# CCTV_OTTAWA_MAX_SOURCES=250 # cap Ottawa cameras (8-500, nearest downtown first)
# CCTV_QUEBEC_ENABLED=1 # set 0 to skip the Québec camera locations (MTQ, Montréal, scenic links)
# CCTV_QUEBEC_VIDEO_ENABLED= # off by default; set 1 to let viewers play Québec 511 clips in their browser after pressing Play (511 terms of use unconfirmed; see DATA_SOURCES.md)
# CCTV_WORLD_CAMERAS_ENABLED=1 # set 0 to skip the Oregon and MLIT (Japan) still cameras
# CCTV_MAX_SOURCES=7350 # catalog-wide ceiling: 4,000 slots shared round-robin, the rest reserved for Québec and World
# CCTV_PREFER_AUSTIN=1 # prioritize Austin cameras in the registry
# CCTV_FORCE_AUSTIN= # force live default sources when a source-pack JSON is configured
# Optional: comma-separated Overpass URLs; use only instances you run or pay for.
# Also enables voice place search (osm_query, "hospitals in Kathmandu").
OVERPASS_UPSTREAMS=
# Optional: place-lookup endpoints. Lookups try bundled data first, then the
@@ -295,3 +314,14 @@ OVERPASS_UPSTREAMS=
# 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.
# GEV_EMBED_FRAME_ANCESTORS=
# Gemini typed chat uses generateContent; voice continues to use Gemini Live.
GEMINI_CHAT_MODEL=gemini-3.5-flash-lite
# Typed chat requests (/api/gemini/chat) per IP per minute. Unset, it takes
# GEV_RATELIMIT_GEMINI_PER_MIN's cap (default 30); 0 disables it.
# GEV_RATELIMIT_GEMINI_CHAT_PER_MIN=30
# Optional keyless Radio Browser coverage: up to 8 ISO alpha-2 countries.
# Each reserves up to 40 stations within the existing 750-station directory.
# Restart after changing. Leave blank for the existing global selection.
@@ -128,7 +128,7 @@ prints anything; never fix that by renaming or deleting PR A's published token.
The highest-leverage places to jump in:
- **🌆 Add a CCTV source pack.** Austin is the reference camera source. Adding another city means a clean public camera catalog with coordinates, attribution, and server-registered frame URLs (the proxy only fetches registered URLs — never client-supplied ones, see [SECURITY.md](SECURITY.md)). City packs are the best first lane.
- **🌆 Add a CCTV source pack.** Austin is the reference camera source. Follow the [CCTV integration workflow](docs/CCTV_INTEGRATION.md) for source review, provider registration, proxy security, tests, and attribution. The proxy only fetches server-registered URLs — never client-supplied ones; see [SECURITY.md](SECURITY.md).
- **🛰️ Add or improve a data layer.** Layer factories live in `src/layers/<family>/`, with source, record, controller and renderer owners implementing the layer interface (`init/enable/disable/update/destroy/getStats`, optional `getDetectableObjects`/`getStats`). Use an existing layer as a template, and see [Adding a new layer](#adding-a-new-layer) for what the PR needs.
- **🎙️ Extend voice control.** Voice arguments are defined in `src/voice/actionSchemas.js`, with server-side descriptions in `server/providers/openai/tools.js` and client-side execution in `src/voice/gevActions.js`. Keep the tool surface tight and the responses honest (confirm only what actually happened).
- **🎨 Add a visual style.** Styles are GLSL post-process shaders in `src/styles/`.
| **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) |
| **CelesTrak** | Satellite TLEs (SGP4) | US-government-origin data, no license; citation requested | "CelesTrak (celestrak.org), Dr. T.S. Kelso" |
| **CelesTrak** | Satellite TLEs (SGP4) and SATCAT records (object type, operational status, owner) that file each satellite under a class | US-government-origin data, no license; citation requested | "CelesTrak (celestrak.org), Dr. T.S. Kelso" |
| **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" |
@@ -40,6 +40,9 @@ How to read this:
| **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)" |
| **FAA Special Use Airspace** (`services6.arcgis.com` FeatureServer `Special_Use_Airspace`) | Prohibited & Restricted Airspace layer: US prohibited (P) and restricted (R) special-use airspace polygons (not the UAS Facility Map or UAS flight restrictions), altitude limits from the `*_VAL`/`*_UOM`/`*_CODE` fields, times of use, and controlling agency | U.S. public domain (FAA aeronautical data published through the [FAA AIS Open Data portal](https://adds-faa.opendata.arcgis.com/)); fetched keyless through `/api/drone-airspace` (1-hour server cache). Informational only — not a substitute for official UAS flight-planning tools | "Prohibited & restricted airspace: FAA Special Use Airspace" |
| **iNaturalist API** (`api.inaturalist.org/v2/observations`, `/v1/grid/{z}/{x}/{y}.grid.json`) | Wildlife Sightings layer: verifiable wildlife observed within the last 48 h (whales 7 d), by observation time, in the current view, with taxon, observation time, quality grade and eligible photo | Per-record licences; observations default CC BY-NC. Photos CC0/BY/BY-SA/BY-NC/BY-NC-SA shown; ND and all-rights-reserved photos link-only. Keyless via `/api/inaturalist/observations` (5-min cache; global sample 10 min; stale ≤ 1 h; 60 requests/min and 8,000/day upstream caps). **NonCommercial content:** commercial redistributors must filter to CC0/BY/BY-SA or remove the layer. No AI training on this content | "Wildlife sightings: iNaturalist contributors (licences per record; photos credited on each card)" |
| **Acartia** (`acartia.io/api/v1/sightings/current`) | Wildlife Sightings layer: Salish Sea marine mammal reports with validated species, observation time, reporting provider and trust flag; WHALES includes only the frozen cetacean list | [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/), each provider credited per sighting. Keyless via `/api/acartia/sightings` (10-min cache; stale ≤ 24 h). Positions rounded to 0.01° on the server; free-text comments excluded. Marine-conservation use; Be Whale Wise | "Wildlife sightings: Acartia (CC BY 4.0; each provider credited per sighting) · Be Whale Wise" |
| **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 |
| **Icelandic Road and Coastal Administration (Vegagerðin, IRCA)** | CCTV camera catalog + frames, Iceland road cameras | [CC BY 4.0](https://www.vegagerdin.is/vegagerdin/gagnasafn/vefthjonustur/terms-and-conditions) (IRCA terms of use, Act 45/2018) — attribution REQUIRED; no suggestion of IRCA endorsement; IRCA and State logos are excluded; real-time data is provided as is | "Icelandic Road and Coastal Administration (Vegagerðin)", CC BY 4.0 |
| **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" |
| **Webcam Romania — Giroc** (`webcamromania.ro`, `w4.webcamromania.ro`) | Optional Giroc camera metadata, JPEG snapshot and live HLS video | [Provider terms](https://webcamromania.ro/termeni-si-conditii/): video belongs to the original camera operator; no open video redistribution licence asserted. The [camera page](https://webcamromania.ro/webcam-orase/webcam-giroc/) offers an embed. Local opt-in via `CCTV_SOURCES_FILE`; media is fetched at runtime, never bundled. | "Webcam Romania · webcamromania.ro"; source link and rights notice in the Data attribution popover |
| **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" |
| **Houston TranStar** (`traffic.houstontranstar.org`, frames `www.houstontranstar.org`) | CCTV camera catalog + frames, Greater Houston | No licence. The [TranStar disclaimer](https://traffic.houstontranstar.org/disclaimer.aspx) (Content Usage) prohibits copying or distribution, and external framing or linking to specific images, without TxDOT's prior written permission. **Off by default**; enable only if you hold TxDOT's written permission | "Houston TranStar" (courtesy) |
| **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)" |
@@ -96,6 +102,7 @@ How to read this:
| **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 |
| **SMTR Rio de Janeiro** (dados.mobilidade.rio) | Transit layer live vehicles, Rio de Janeiro: municipal buses (SPPO) and the Mobi-Rio BRT, as two feeds | CC BY 4.0, stated on both data.rio catalog pages. Keyless GTFS-Realtime published by the Secretaria Municipal de Transportes, labelled beta, with a gateway limit of 5 requests a second and 60 a minute. Line names come from SMTR’s GTFS `routes.txt` (dados.mobilidade.rio/gtfs/schedule), under the same licence. | Attribution to SMTR / Prefeitura da Cidade do Rio de Janeiro under CC BY 4.0 |
| **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)" |
@@ -117,7 +124,8 @@ The optional Local ADS-B layer and the broadcast-FM receiver use radio you recei
**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).
- **Freeway Bureau and Highway Bureau, MOTC (Taiwan).** National freeway cameras use the Freeway Bureau’s keyless [MOTC 2.0 CCTV table](https://tisvcloud.freeway.gov.tw/history/motc20/CCTV.xml), refreshed daily and fetched at runtime (never bundled). Freeway Bureau data follows the [Open Government Data License, Taiwan v1.0](https://data.gov.tw/license); the app credits the Bureau, names the dataset and links the licence. If that catalog is unreachable and no last-good source list exists, the app falls back to the Highway Bureau’s free [provincial-road CCTV XML feed](https://thbapp.thb.gov.tw/opendata/), with attribution as requested by the Bureau. Its MJPEG streams are accepted only from cctv-ss01 through cctv-ss08.thb.gov.tw over HTTPS; the frame proxy extracts one bounded JPEG per request. This fallback covers provincial roads, not national freeways. Neither feed requires an API key. CCTV_TAIWAN_ENABLED=0 disables both; CCTV_TAIWAN_MAX_SOURCES defaults to 600 (bounded to 1–2000).
- **Québec CCTV (MTQ / Québec 511, Montréal and scenic links).** `public/quebec-cameras.json` contains 680 MTQ positions from the official georeferenced WFS (CC BY 4.0), 426 Montréal camera locations derived from OSM (ODbL 1.0), and 14 scenic records with per-row provenance and accuracy. The MTQ video is **opt-in** (`CCTV_QUEBEC_VIDEO_ENABLED=1`): the CC BY 4.0 licence covers the camera locations only, and Québec 511's terms for embedding its clips on another site are unconfirmed, so by default MTQ cameras link to the Québec 511 viewer page instead. With the flag set, the 680 MTQ cameras play the operator's five-second MP4 clip directly in the viewer's browser from `www.quebec511.info/Carte/Fenetres/camera.ashx?id=<id>&format=mp4`, only after the viewer presses Play, so Québec 511 sees the viewer's IP address; Québec 511 refreshes those clips every few minutes. A second browser request supplies an approximate map still. The app does not proxy, store, or read video pixels, and no camera images are bundled. The selected-camera credit names both sources: locations MTQ under CC BY 4.0, video Québec 511. Montréal and scenic entries have provider-page links rather than direct frame URLs; their source-page terms apply. The video remains hosted by Québec 511 under its service terms. No API key is required; `CCTV_QUEBEC_ENABLED=0` disables the pack. Confirm Québec 511's terms of use before setting `CCTV_QUEBEC_VIDEO_ENABLED=1` on a public deployment.
- **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)).
- **Sea Marks and Ski Pistes (#10).** Two keyless raster overlay layers under Infrastructure, OpenSeaMap sea marks and OpenSnowMap ski pistes, requested tile by tile as the camera views them, with no prefetch, bulk download or caching on our side (OpenSnowMap forbids bulk tile download without prior agreement). They drape on the globe on the OSM and Bing stacks and on the Google Photorealistic tileset on Google 3D (see `src/maps/imageryHost.js`). OpenRailwayMap and OpenInfraMap, also requested in #10, are not included: OpenRailwayMap's tile server only answers requests whose Referer is its own site, and OpenInfraMap serves vector tiles that need a vector rendering pipeline.
@@ -165,6 +173,8 @@ The optional Local ADS-B layer and the broadcast-FM receiver use radio you recei
- **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.
- **City of Ottawa traffic cameras.** Camera images come from the city's [Traffic Camera Images API](https://traffic.ottawa.ca/en/traffic-map-data-lists-and-resources/about-the-data), which requires an access certificate requested through the city's registration form and allows one certificate and user id to fetch a camera at most once every 60 seconds. Set `OTTAWA_CAMERA_CERTIFICATE`; without it the pack is skipped and makes no request. The camera list is the documented keyless `traffic.ottawa.ca/map/service/camera` JSON (camera number, English and French names, coordinates, owner). Frames are `traffic.ottawa.ca/opendata/camera?c=<number>`; the server adds the certificate and the user id `gev` at fetch time, so no catalog response carries the key, and serves each camera from one shared in-memory frame per minute. MTO cameras carried on the city feed keep their owner as a per-camera credit. The feed publishes no bearing, so headings use the shared id-hash fallback at low confidence, and no status field, so a camera the city takes offline still registers. Frames are never stored on disk. The city states no redistribution licence for the images; the certificate request asks how they will be used, so describe the deployment there. `CCTV_OTTAWA_MAX_SOURCES` caps the pack (default 250, nearest downtown) and `CCTV_OTTAWA_ENABLED=0` turns it off.
- **World cams image subset.** The normal CCTV catalog adds 1,176 Oregon TripCheck stills and 1,054 MLIT river cameras from the curated `config/cctv_sources.world-image-cameras.json` export; disable it with `CCTV_WORLD_CAMERAS_ENABLED=0`. The export's Vegagerðin rows are left out because the Iceland pack already loads those cameras. The server validates and pins each image URL to its provider path, fetches a frame only when a camera is viewed, and never stores or redistributes it. [ODOT allows businesses, private citizens and organizations to use TripCheck images and asks for credit](https://www.tripcheck.com/Pages/Frequently-Asked-Questions) ("Camera courtesy of ODOT"); TripCheck also carries partner cameras run by Oregon counties and cities, Caltrans and WSDOT, which keep their owner's name in the camera label. MLIT's [site terms](https://www.river.go.jp/kawabou/kwb_apend/html/caution.html) follow the Government of Japan Standard Terms of Use 2.0 (CC BY 4.0 compatible, commercial use allowed, source credit required) and ask tools not to collect data periodically, pointing regular collection to the river information data service; frames are therefore fetched only on view and refresh at most every ten minutes per camera. Both credits are registered in the Data attribution popover.
- **TxDOT ITS cameras.** The catalog comes from the keyless `its.txdot.gov/its/DistrictIts/GetCctvStatusListByDistrict` endpoint, one call per district. Frames come from the per-camera `GetCctvSnapshotByIcdId` endpoint, which answers JSON carrying a base64 JPEG rather than an image body; the proxy decodes it only for responses from the official TxDOT origin and validates the JPEG header before serving. Only cameras reporting `Device Online` are registered. Districts default to Austin and San Antonio (`CCTV_TXDOT_DISTRICTS`; 25 codes exist statewide); `CCTV_TXDOT_MAX_SOURCES` caps the pack and `CCTV_TXDOT_ENABLED=0` turns it off. Frames are fetched live at request time and are never stored or redistributed.
- **Tallinn ristmikud.** Intersection camera stills are fetched live from `ristmikud.tallinn.ee` (`/last/camNNN.jpg`). The curated catalog (`config/cctv_sources.tallinn.json`) supplies coordinates and heading priors; only server-registered ristmikud HTTPS URLs are proxied. Frames are fetched at request time and never stored or redistributed. Disable with `CCTV_TALLINN_ENABLED=0`.
- **Tarktee / Transpordiamet.** Estonia road-weather camera locations and current JPEG URLs come from keyless DATEX2 endpoints on `tarktee.transpordiamet.ee` (`/api/v1/datex/roadCameraLocations` + `roadCameraImages`). Frame URLs rotate with each capture; the 15-minute CCTV source cache refreshes them. Only `https://tarktee.transpordiamet.ee/images/…` URLs are registered. Disable with `CCTV_TARKTEE_ENABLED=0`.
@@ -173,6 +183,7 @@ The optional Local ADS-B layer and the broadcast-FM receiver use radio you recei
- **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`.
- **Iceland (Vegagerðin / IRCA) cameras.** The camera list is the keyless `https://gagnaveita.vegagerdin.is/api/vefmyndavelar2014_1` JSON feed (about 500 camera views at roughly 170 stations), fetched over https with redirects refused and the body read capped at 2 MiB, since the list decides ids, coordinates and which frame path is proxied. Frames are JPEG stills in `https://www.vegagerdin.is/vgdata/vefmyndavelar/`; only links inside that directory are registered (userinfo, explicit ports and encoded path separators are rejected). Camera ids come from the frame file name. The feed's `Skyring` view description is the camera label, and a stated facing ("til vesturs", "í suður", "séð austur") sets a high-confidence heading; a view that states none, or two different ones ("niður á veg", a bare road direction), takes the id-hash low-confidence pose. The feed has no elevation, so cameras carry a flat prior that the client's ground snap corrects. IRCA publishes road camera images under CC BY 4.0 and requires the licensor to be acknowledged without implying endorsement; the pack shows the attribution above (licensor, dataset, CC BY 4.0, terms link and no-endorsement note) in the Data attribution popover, and every camera record carries the licence string. IRCA describes the images as fetched several times per hour (a live frame's `Last-Modified` was about four minutes old on 2026-11-06) and the real-time data as provided as is, without warranty. Frames are fetched at request time and never stored or redistributed. Disable with `CCTV_ICELAND_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.
@@ -180,17 +191,25 @@ The optional Local ADS-B layer and the broadcast-FM receiver use radio you recei
- **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`.
- **Houston TranStar cameras.** Keyless. The camera list is the public `traffic.houstontranstar.org/data/layers/cctvSnapshots_json.js` catalog (~1,000 cameras across Greater Houston); frames are stills at `www.houstontranstar.org/snapshots/cctv/<id>.jpg`, synthesized from a validated numeric id rather than taken from the payload, and only that host is registered. Cameras whose catalog id is `0` (a different, non-snapshot class) and cameras outside Greater Houston are dropped. TranStar's `dir` field is the side of the freeway the camera sits on, not the way it faces: TranStar's own map shows it as "Traffic closest to the camera is traveling …" (South → eastbound, North → westbound, West → southbound, East → northbound). That travel direction (90°, 270°, 180°, 0°) seeds the heading as a low-confidence prior with the low-confidence pose; `None` or an unknown value uses the shared id-hash fallback at low confidence. Headings are corrected with the in-app calibration gizmo. Off by default. TranStar's [Content Usage terms](https://traffic.houstontranstar.org/disclaimer.aspx) prohibit copying or distributing the site's information, and external framing or linking to specific images, without TxDOT's prior written permission; its FAQ grants permission-free use only for the RSS feeds. This project does not hold that permission for downstream installations, so the pack loads only when an operator who holds TxDOT's written permission sets `CCTV_HOUSTON_ENABLED=1`; with the flag unset, empty or any other value, the server makes no request to TranStar. Once enabled, it loads the 250 cameras nearest downtown, Uptown, the Medical Center, IAH and Galveston; `CCTV_HOUSTON_MAX_SOURCES` (8–400) changes the cap. Snapshots update about every 3 minutes, which is also the ambient still cadence. Frames are fetched from TranStar and relayed to the browser on demand, buffered only transiently per request, sent with `Cache-Control: no-store`, and not archived.
- **Autostrada del Brennero (A22) webcams.** Keyless. The camera list is one request to the Open Data Hub tourism API (`tourism.api.opendatahub.com/v1/WebcamInfo?source=a22`), run by NOI Techpark in South Tyrol, which republishes Autobrennero's own list of 13 motorway webcams with coordinates and kilometre markers. Frames are the operator's stills on `www.autobrennero.it/WebCamImg/km<N>.jpg`, refreshed about once a minute, and only the exact image for each camera's kilometre is registered. The feed names no bearing, so headings use the id-hash fallback at low confidence. The road climbs from about 20 m in the Po valley to about 1,370 m at the Brenner Pass, so each camera's ground elevation comes from a per-kilometre table sampled from the Copernicus GLO-90 DEM (via Open-Meteo's elevation API); a camera not in the table uses a 200 m prior. The Open Data Hub tags every record CC0 (`ClosedData: false`, Autobrennero as licence holder), but that tag is the aggregator's metadata and Autobrennero publishes no terms of its own for the images, so they are credited to the operator as a courtesy and are not redistributed. Frames are fetched at request time and never stored. Disable with `CCTV_A22_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 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. Individualstructurally 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.
- **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 eighteen registered feeds, covering seventeen 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, YRT and Rio’s SMTR 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 country coverage (optional).** Set `RADIO_PRIORITY_COUNTRIES=IN,PL` in the local `.env` and restart to include stations beyond the global click charts. The operator may choose up to eight unique ISO alpha-2 country codes; no key is needed. Each country adds one bounded query (at most 200 rows) and reserves up to 40 unique, usable stations inside the existing 750-station catalog. Country seeds precede specialist-tag seeds and global popularity fill; unused slots return to the shared pool. This is a coverage preference, not an exhaustive national directory or a guarantee of stations in every city. Countries with no eligible HTTPS MP3/AAC geolocated stations remain empty. An unset list preserves global selection. Country requests share the directory cache and three-worker pool; failed country queries are reported in coverage without blocking a healthy global refresh. Failed/degraded refreshes back off for one minute, with at most three mirrors per query.
- **Radio Browser.** In every installation, including when `RADIO_PRIORITY_COUNTRIES` is unset, each request tries at most three mirror candidates (later fallback candidates are skipped), and failed or partial catalog refreshes back off for 60 seconds. A cold-start failure therefore continues returning 503 during that delay even if upstream recovers. `/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; broad/specialist responses whose rows all fail the product's health policy do not count as successful queries. Optional priority-country outcomes are advisory: valid, in-limit responses with no eligible stations succeed as empty results, while failed or malformed country queries report unsuccessful coverage without degrading the global catalog. A usable partial cold catalog is explicitly `DEGRADED`, and non-array or empty broad/specialist 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.
- **FAA Special Use Airspace.** Keyless. `/api/drone-airspace` queries the public FAA AIS FeatureServer for prohibited (P) and restricted (R) zones only — not MOAs, TFRs or other SUA types — with simplified geometry, a body cap, paging, a one-hour cache and stale-on-error. The layer is US coverage, informational only, and is not a substitute for B4UFLY, LAANC or a preflight briefing. Data is fetched at request time and never stored or redistributed. US government public domain; courtesy attribution is registered in the Data attribution popover.
- **iNaturalist.** Keyless. `/api/inaturalist/observations` returns cached random view samples (newest first globally), at most 200 per request. Recency uses observation time: the v1 `d1` filter and a rolling 48 h cutoff (whales 7 d); date-only rows qualify when their day starts within the window. Observation caches last 5 min (global 10 min), with stale fallback ≤1 h. `/api/inaturalist/grid` supplies density counts, cached 15 min. Both share a paced queue capped at 60/min and 8,000/day; only request counters persist in `.gev-cache/`. The server caches projected records, never raw responses. Private records are excluded; obscured coordinates become 0.2° cells. Observer identity is retained only for photo attribution. Eligible photos load directly from `inaturalist-open-data.s3.amazonaws.com`, with creator credit on ambient cards and creator/licence on selection. Per-record licences and noncommercial restrictions apply as listed above. Attribution appears in the layer row and Data attribution popover. Coverage follows community observations, not a census; Acartia adds Salish Sea marine mammals.
- **iNaturalist grid (cell schema risk).** `/api/inaturalist/grid` shares observation-date, taxa, quality, pacing and budget rules with observations. A strict 64×64 UTFGrid schema guard fails closed; only logical-cell keys, counts and coordinates survive projection. Raw tiles, observation identifiers and free text are not retained. Projected tiles use a 400-entry LRU, 15-minute freshness and at most one hour of stale fallback. Views request at most six tiles; cached and adjacent-zoom coverage publishes before network waits, independently of observation pages. Cells supply a noninteractive density glow ranked within the view, from teal to amber with mono/CRT support. Every sighting badge comes from a real observation at its public position. Counts describe observations, not a population census. The endpoint is documented in the v1 and v2 API docs as “Grid Tiles UTFGrid” (`/grid/{z}/{x}/{y}.grid.json`), but its per-cell records are not specified; schema drift disables grids until revalidated. See `docs/KNOWN-ISSUES.md`.
- **Acartia.** Keyless. `/api/acartia/sightings` reads the current 7-day marine-mammal feed for ALL, MAMMALS and WHALES views intersecting 45.5–50.5° N, 126–121° W in the Salish Sea. WHALES accepts recognized cetaceans; other identified marine mammals appear under ALL/MAMMALS. The server rounds positions to 0.01° and drops comments before caching projected records (10 min fresh, stale ≤24 h); raw responses are not retained. Selected text cards credit the reporting provider under CC BY 4.0 and show Be Whale Wise guidance. `/sightings/current` answers without a key today, but Acartia's README asks API users to create an account and generate an API key on the [Acartia dashboard](https://acartia.io/) and to follow its [community guidelines](https://acartia.io/register). Attribution also appears in the layer row and Data attribution popover. Opportunistic sightings, not a census.
- **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, empty and invalid values disable it; an operator must explicitly select an authorized endpoint): 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 hold one lock through the actual fetch invocation and recording the next public start, with the daily slot persisted before invocation; restarts share that count, spacing and any pause. An unfinished dispatch marker fails closed until operator recovery; it is never automatically reclaimed. 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 does not establish an application-wide public-service allowance or authorize public use. An operator-selected non-public endpoint is 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. Named grounds require a returned name matching the selected place, except that an ask whose head noun names a kind of grounds (a school, university or college, hospital, cemetery, stadium or zoo) may use the part of the smallest unnamed tile area of that class that holds the resolved place; a differently named area never answers. Other anonymous landcover polygons cannot establish identity, so unsupported grounds stay unavailable rather than borrowing a nearby POI name.
- **Voice areas and place search.** Voice areas (`resolve_area`) use the same outline sources as voice outlines. Voice place search (`osm_query`, `/api/osm/features`) runs only on an operator-configured Overpass: a fixed list of kinds, a bounded box and result count, one query at a time, answers cached for a day. It never uses Nominatim.
- **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.
@@ -204,20 +223,25 @@ The [Bhote Koshi event pack](public/events/bhote-koshi-2026/README.md), under `p
| **World camera image subset** (2,230 locations) | `config/cctv_sources.world-image-cameras.json` | Mixed: ODOT TripCheck public images (ODOT permits use with credit) and MLIT site content under terms compatible with CC BY 4.0. Frames are not bundled or persisted; the server holds a bounded in-memory cache. | Per-source terms apply; NZTA Journey Planner web-cache rows are excluded; Vegagerðin rows are served by the Iceland pack instead | Oregon Department of Transportation (Camera courtesy of ODOT) and Japan Ministry of Land, Infrastructure, Transport and Tourism (出典:国土交通省 川の防災情報) |
| **Imaging sensor pack** (46 Earth-observation platforms: swath, resolution) | `imaging_sensors/` | Individual specification facts from operator and agency pages, cited per entry | ✅ (facts only) | "Imaging sensor swaths: operator and agency specifications" |
| **Natural Earth physical regions** (1,046 land + 292 marine named polygons) | `natural_earth/` | **Public domain** | ✅ (no restrictions) | "Made with Natural Earth" (courtesy credit — not legally required) |
| **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,441 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) |
@@ -245,13 +269,16 @@ 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
The CCTV layer's hardware-model specs are a small extract (v2.26.0, plus the Bosch MIC
records from v2.29.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
**CC0-1.0**. Only records for hardware named by the integrated public catalogs (King County,
Sarasota County and Sioux Falls) or by the City of Amsterdam register proposed in
[#251](https://github.com/bilawalsidhu/gods-eye-view/pull/251) are bundled, compiled into the
module. Nothing is fetched at runtime. The MIC records keep their specification fields; their
titles are shortened to model codes and colour aliases are added. 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.
### Bundled OpenStreetMap data
@@ -269,6 +296,14 @@ transaction quota. Requires a free `FIRMS_MAP_KEY`
(https://firms.modaps.eosdis.nasa.gov/api/map_key/); the layer is empty without it.
The former bundled 2026-05-25 snapshot was removed 2026-07-16.
### UCS Satellite Database (`ucs_satellites/`)
Operator, owner country and purpose for active satellites, keyed by NORAD number, from the Union of Concerned Scientists Satellite Database release **UCS-Satellite-Database 5-1-2023** (data current through 2023-05-01; https://www.ucsusa.org/satellite_database). The satellites layer joins it to CelesTrak positions and SATCAT records to file each satellite as a station, imaging, telecom, Starlink, probe or debris object, and to name its operator and owner. Section 3 of the UCS database manual: "The Database is free and its use is unrestricted. We request that its use be acknowledged and referenced in written materials. References should include the version of the Database that was used, which is indicated by the name of the Excel file, and a link to or URL for the webpage www.ucsusa.org/satellite_database." The app acknowledges it in the Data attribution popover. UCS has paused updates since this release; Starlink rows are dropped (Starlink is classed by name). Build: `scripts/build-ucs-satellites.py`; curation in the folder README.
### Imaging sensor pack (`imaging_sensors/`)
Swath widths and ground resolutions for 46 Earth-observation platforms (Landsat, Sentinel, MODIS, VIIRS, GOES, Himawari, Gaofen, CBERS, RADARSAT, TerraSAR-X, COSMO-SkyMed, ALOS, KOMPSAT, SPOT, Pléiades, WorldView, SkySat, PlanetScope, ICEYE, Capella, Umbra and others), compiled 2026-11-04 from the operator and agency pages cited per entry. The orbital view draws each satellite's sensor swath to scale and caps its zoom at that swath. Facts only; no text or images copied. A swath is confirmed on an operator, agency (ESA's eoPortal included) or academic page; resellers are not cited. The CEOS MIM Database (CC BY 4.0) is not yet used. Sources and unverified values: the folder README.
### Natural Earth physical regions (`natural_earth/`)
Curated from the **Natural Earth 10m physical vectors** (https://www.naturalearthdata.com/ —
@@ -288,6 +323,16 @@ https://www.naturalearthdata.com/about/terms-of-use/). We credit anyway: "Made w
Earth". Registration in the in-app `dataCredits.js` attribution list ships with the resolver
@@ -362,9 +362,11 @@ _Why cockpit mode exists: you're riding a real aircraft over real terrain — an
## 🎙️ Talk to It
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.
The standalone app offers **OpenAI** and **Gemini** under **AI Provider** in Settings, one choice shared by voice and chat. 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.
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 AI Provider setting does not change the OpenAI-backed AI HUD summary.
Next to the mic, **CHAT** opens a typed chat with silent replies. It uses the selected provider and the same map actions, and never asks for the microphone. OpenAI chat needs an API key (`OPENAI_CHAT_MODEL`), so it is off while `GEV_PREFER_CODEX_OAUTH` pins OpenAI to ChatGPT OAuth; Gemini chat uses a separate text model (`GEMINI_CHAT_MODEL`). Chat and voice panels share the left panels' glass surface and sit above the dock or dock to the right.
> 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.
@@ -424,7 +426,7 @@ _Ask for radio near anywhere and the globe starts broadcasting — every station
## 🛰️ What's on the Globe
Twenty layers and map sources. **Eighteen have a keyless path.** Some offer additional capabilities with a provider key. (🟢 no key · 🟡 free key · 🔴 metered.)
Twenty-two layers and map sources. **Twenty have a keyless path.** Some offer additional capabilities with a provider key. (🟢 no key · 🟡 free key · 🔴 metered.)
| 🛰️ **Satellites** | 838-object catalog, color-coded by class with a live legend — the **DENSE** chip drops in the whole Starlink shell | CelesTrak | 🟢 |
| 🛰️ **Satellites** | About 1,300 objects in six classes (Stations, Imaging, Telecom, Starlink, Probes, Debris) with a toggle and a live count each; Starlink loads its whole 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** | ~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) · Italy (A22 Brennero motorway), 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 | 🟢 |
| 📹 **CCTV Mesh** | up to 7,350 camera locations (4,000 shared round-robin slots, plus room for 1,120 Québec and 2,230 World image cameras) projected _into_ the 3D space — Austin · Texas (TxDOT) · California (Caltrans) · London (TfL) · Ontario (511, free developer key) · Ottawa (City of Ottawa, free access certificate) · 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) · Iceland (Vegagerðin) · Hong Kong (Transport Department) · Taiwan (Freeway Bureau, MOTC) · Québec (MTQ locations with Québec 511 links, Montréal locations, regional webcams) · Oregon (TripCheck) · Japan (MLIT river cameras) · Italy (A22 Brennero motorway), plus opt-in New York City (NYC DOT TMC, needs your own NYC DOT agreement), opt-in Lithuania (Via Lietuva / Eismoinfo, photo licence unconfirmed), opt-in Québec 511 in-browser video (embedding terms unconfirmed), opt-in Florida (FL511, individual personal use only; other use needs FDOT's written consent) and opt-in Houston (TranStar, enable only with TxDOT's written permission per TranStar's Content Usage terms). Positions are published or attributed; 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, 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 | 🟢 |
| 🚌 **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, Rio de Janeiro (buses and BRT), 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) | 🟢 |
| ⌖ **OSM Places** | Voice place search ("hospitals in Kathmandu") from OpenStreetMap, drawn as numbered points that follow-up questions can filter. Needs your own Overpass server (`OVERPASS_UPSTREAMS`); off otherwise |
| 🔥 **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 paid token raises the allowance) |
| 🎖️ **Mapped Installations** | Viewport-bounded military-site context from community mapping — incomplete by nature, and labeled that way | OpenStreetMap | 🟢 |
| 🚫 **Prohibited & Restricted Airspace** | FAA prohibited (P) and restricted (R) special-use airspace as ground polygons, with name, altitude range and times of use on select. Not the UAS Facility Map or UAS flight restrictions; informational only, not for flight planning | FAA SUA | 🟢 |
| 🦉 **Wildlife Sightings** | Wildlife observed within 48 h (whales 7 d): density glow, taxon badges and anchored photo cards with creator/licence credit. Salish Sea marine mammals via Acartia; sensitive locations shown as 0.2° cells. Not a census | iNaturalist, Acartia | 🟢 |
| 🌬️ **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 | 🟢 |
| 🌀 **Cyclones** | Advisory positions, forecast tracks and uncertainty cones for the Atlantic and eastern/central North Pacific | NOAA NHC / CPHC | 🟢 |
**Weather, keyless.** **Wind** animates 10 m forecast flow from NOAA GFS or ECMWF IFS, with an optional color field and a reading at map center. **Rain radar**, **Satellite clouds** and **Lightning density** are observations on one history timeline in the **WEATHER** panel (step back, **Play**, **Latest**); **Cyclone advisories** draw NHC and CPHC positions, forecast tracks and cones. On Google 3D Tiles the observed layers float above the city as translucent shells, sharpened around your view.
In the app, **Data Layers** groups them as Movement, Cameras, Infrastructure, Events, Weather and Audio; Directions sits in the separate **Utilities** panel with Draw and Import.
In the app, **Data Layers** groups them as Movement, Cameras, Infrastructure, Events, Weather and Audio; Directions and OSM Places sit in the separate **Utilities** panel with Draw and Import.
**The basemap ladder — what each tier buys you:**
@@ -468,7 +473,7 @@ In the app, **Data Layers** groups them as Movement, Cameras, Infrastructure, Ev
_The Space Missions layer replaying a Falcon 9 ascent — labeled `RECONSTRUCTED ESTIMATE`, scrubbable 0.25×–4×._
**Also on the globe:** neighborhood overlays · an optional cockpit WX cloud effect. **Bundled static infrastructure:** Datacenters (4,351), Dams (704), and Submarine Cables (712).
**Also on the globe:** neighborhood overlays · an optional cockpit WX cloud effect. **Bundled static infrastructure:** Datacenters (4,352), Dams (704), and Submarine Cables (712).

@@ -480,9 +485,9 @@ _The Space Missions layer replaying a Falcon 9 ascent — labeled `RECONSTRUCTED
> 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.
**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 Street Level starts on **KartaView** instead, and the Mapillary chip says which key to add. **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`.
@@ -634,7 +639,7 @@ and panel. DelDOT uses its official HTTPS HLS catalog links. Disable that
pack with `CCTV_DELDOT_ENABLED=0`.
The server allows two concurrent sessions. Each retains at most 12 segments
and 24 MiB in memory; individual downloads are capped at 4 MiB with a ten
and 24 MiB in memory; individual downloads are capped at 8 MiB with a ten
second deadline. There are no segment files or ffmpeg processes. Redirects,
off-origin references, encrypted playlists and non-MPEG-TS segments are refused.
Each decoder has its own client lease (at most eight per session), including
@@ -69,22 +69,25 @@ browser-key fallback for existing single-key setups.
The data proxies under `server/providers/` are written so the browser cannot turn the server into an open relay:
- **No arbitrary-URL fetching.** The CCTV frame proxy fetches only server-registered camera/frame URLs — clients cannot pass an upstream URL to fetch (SSRF mitigation). Other proxies target fixed upstream hosts.
- **Live HLS is bounded and origin-pinned.** Registered HTTP(S) HLS sources use a Node puller that rejects redirects and off-origin playlist references, bounds playlists and segment bodies while streaming, and aborts timed-out or released sessions. At most two sessions retain 12 segments/24 MiB each, plus bounded download buffers. Stores are memory-only and idle sessions expire after 15 seconds. RTMP/ffmpeg execution is not enabled. Locally configured source URLs remain an operator trust boundary.
- **Live HLS is bounded and origin-pinned.** Registered HTTP(S) HLS sources use a Node puller that rejects redirects and off-origin playlist references, bounds playlists to 256 KiB and segment bodies to 8 MiB while streaming, and aborts timed-out or released sessions. At most two sessions retain 12 segments/24 MiB each, plus bounded download buffers. Stores are memory-only and idle sessions expire after 15 seconds. RTMP/ffmpeg execution is not enabled. Locally configured source URLs remain an operator trust boundary.
- **Radio is not an audio relay.** `/api/radio/stations` contacts only allowlisted Radio Browser HTTPS hosts and paths, rejects redirects, rejects any hostname with a loopback/private/link-local/metadata/non-public A or AAAA result, and pins each TLS connection to a validated address. It returns normalized public HTTPS stream URLs; `/api/radio/click/:uuid` applies the same destination policy and accepts only station IDs from the current bounded catalog. The browser then connects directly to the broadcaster after an explicit playback action, so the broadcaster sees the listener's IP address. GEV never proxies, caches, records, or redistributes audio.
- **Québec 511 video plays in the browser, opt-in and after Play.** With `CCTV_QUEBEC_VIDEO_ENABLED=1`, `/api/cctv/sources` lists, for each MTQ camera, one URL built server-side from its numeric id on `https://www.quebec511.info/Carte/Fenetres/camera.ashx`; the overlay accepts no other host, path or format. The browser loads that clip only after the viewer presses Play for the selected camera, so Québec 511 sees the viewer's IP address, and the panel says so before Play. Selecting a camera by click, auto-hop, a scene or voice never loads it, and switching cameras stops playback. GEV never proxies, caches or reads the video's pixels. With the flag unset (the default) the browser makes no request to Québec 511.
- **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.
- **Voice place search runs only on the operator's Overpass.** `/api/osm/features` answers only when `OVERPASS_UPSTREAMS` is set and never falls back to a public instance. The client sends a curated preset id, a box, a mode and a limit; the server writes and quotes the Overpass query itself, so no client or model text reaches it. Boxes are capped (36 deg² for a count, 4 deg² for a listing), as are the feature limit, the Overpass timeout and memory, and the response bytes read. Each client (IPv6 clients keyed by /64, as on`/api/overpass`) runs one search at a time and two run at once in all, under a per-client rate limit (12/min) and a global one (30/min); a search gets 22 s across every configured endpoint in turn (Overpass itself is told 20 s), which ends before the voice answer stops waiting at 25 s. The route sits behind the cross-site gate below. Answers are cached in memory and on disk (`.gev-cache/osm-features/`, at most 400 entries and 256 MB, kept a day), keyed to the configured instance so a repointed `OVERPASS_UPSTREAMS` never serves the old one's answers; a malformed answer is never cached.
- **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. The one exception is a source registered as an MJPEG still (the Taiwan Highway Bureau feeds): its `multipart/x-mixed-replace` answer is read only up to its first JPEG part, capped at 2 MB, and sent as `image/jpeg`; a multipart answer from any other camera falls through. A camera whose API needs an operator credential (the City of Ottawa certificate, `OTTAWA_CAMERA_CERTIFICATE`) has a keyless catalog URL; the frame proxy adds the credential only at fetch time, only for a URL inside that camera's pinned path, so `/api/cctv/sources` and `/api/cctv/stream` never carry it. 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 map routes (`/api/terrain/heights` and `/api/overpass`) use the same cross-site checks, minus the forwarding-header refusal: they guard only per-client quotas and caches, not a key or a bill, and the map needs both to render behind a reverse proxy. `/api/overpass` also admits at most 300 POSTs a minute per peer (IPv6 peers keyed by /64) and 900 a minute across all remote peers before it reads a body. A direct loopback request — a loopback socket with none of the forwarding headers `hasProxySignals` checks (`Forwarded`, `Via`, `X-Forwarded-For`, `X-Forwarded-Host`, `X-Forwarded-Port`, `X-Forwarded-Proto`, `X-Real-IP`, `CF-Connecting-IP`, `CF-Ray`) — draws on its own per-peer admission window, so remote peers cannot exhaust the owner's admission. A reverse proxy on the same machine must add at least one of those headers (Caddy and Cloudflare do by default; nginx needs `proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;`, plus `proxy_set_header Host $http_host;` and `proxy_set_header X-Forwarded-Proto $scheme;` so the `Origin` check matches), or its traffic is indistinguishable from the owner's and shares the owner's window. With one, its requests are then counted as remote, in one bucket per proxy socket — the forwarded client address is never trusted as identity — so all users behind one proxy share 300/min. The isolation needs a loopback socket: under the default Docker bridge network every host-side client, the owner included, arrives from the container gateway and is one remote peer. **Limit:** this isolates admission only. The upstream budget (90 requests/min per client key, 300/min across all clients) is shared, so once remote traffic has used the global upstream budget, an owner request that misses the cache can still get `429`, or `503` when remote misses hold every upstream concurrency slot; cache hits are unaffected. 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.
- **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 map routes (`/api/terrain/heights`, `/api/drone-airspace`, `/api/inaturalist/observations`, `/api/acartia/sightings`, `/api/overpass` and voice place search's `/api/osm/features`, which spends the same operator Overpass) use the same cross-site checks, minus the forwarding-header refusal: they guard only per-client quotas and caches, not a key or a bill, and the map and voice need them to work behind a reverse proxy. `/api/overpass` also admits at most 300 POSTs a minute per peer (IPv6 peers keyed by /64) and 900 a minute across all remote peers before it reads a body. A direct loopback request — a loopback socket with none of the forwarding headers `hasProxySignals` checks (`Forwarded`, `Via`, `X-Forwarded-For`, `X-Forwarded-Host`, `X-Forwarded-Port`, `X-Forwarded-Proto`, `X-Real-IP`, `CF-Connecting-IP`, `CF-Ray`) — draws on its own per-peer admission window, so remote peers cannot exhaust the owner's admission. A reverse proxy on the same machine must add at least one of those headers (Caddy and Cloudflare do by default; nginx needs `proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;`, plus `proxy_set_header Host $http_host;` and `proxy_set_header X-Forwarded-Proto $scheme;` so the `Origin` check matches), or its traffic is indistinguishable from the owner's and shares the owner's window. With one, its requests are then counted as remote, in one bucket per proxy socket — the forwarded client address is never trusted as identity — so all users behind one proxy share 300/min. The isolation needs a loopback socket: under the default Docker bridge network every host-side client, the owner included, arrives from the container gateway and is one remote peer. **Limit:** this isolates admission only. The upstream budget (90 requests/min per client key, 300/min across all clients) is shared, so once remote traffic has used the global upstream budget, an owner request that misses the cache can still get `429`, or `503` when remote misses hold every upstream concurrency slot; cache hits are unaffected. 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.
- **Typed chat spends the provider keys.** `/api/openai/chat` and `/api/gemini/chat` send the conversation to OpenAI Chat Completions or Gemini `generateContent` with the server's `OPENAI_API_KEY` or `GEMINI_API_KEY`. They require a same-site `Origin` (the Gemini request check), cap request bodies at 512 KB, refuse upstream redirects, cut the upstream call off after 60 s and return only sanitized errors. OpenAI chat can only bill `OPENAI_API_KEY`, so while `GEV_PREFER_CODEX_OAUTH` pins OpenAI to ChatGPT OAuth it refuses with `503` (`CODEX_OAUTH_REQUIRED`) rather than spend the key. After the provider refuses the key or account (a `401`, a `403`, Gemini's `400``API_KEY_INVALID` or OpenAI's `429``insufficient_quota`), a route stops sending that key upstream for a minute, or until the key changes.
- **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
@@ -93,7 +96,7 @@ The dev server is a **key broker**: every server-side key above is spendable by
- **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 (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**.
- **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`). Typed chat keeps its own per-IP window, set by `GEV_RATELIMIT_OPENAI_CHAT_PER_MIN` and `GEV_RATELIMIT_GEMINI_CHAT_PER_MIN`; left unset, each takes its provider's resolved cap (`GEV_RATELIMIT_OPENAI_PER_MIN`, or `GEV_RATELIMIT_GEMINI_PER_MIN`, default `30`), so lowering a provider throttle lowers chat too, while an explicit chat value, including `0`, wins. 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
"license":"Public camera: https://webcamromania.ro/webcam-orase/webcam-giroc/ . Provider terms: https://webcamromania.ro/termeni-si-conditii/ . Video rights remain with the camera operator. Coordinates corrected by the contributor; direction, height, elevation and field of view are uncalibrated estimates."
| `server/standalone/` | Environment, local settings writes and server composition |
See [application construction](APPLICATION.md) and the
[infrastructure contract](INFRASTRUCTURE-LAYERS.md) for construction interfaces, and the [renderer contract](RENDERER-CONTRACT.md) for what a renderer provides.
[infrastructure contract](INFRASTRUCTURE-LAYERS.md) for construction interfaces.
## Import direction gates
@@ -164,7 +163,8 @@ local composition mounts them in the same order. Importing the entry performs
no acquisition; factory calls create independent cache state.
`gods-eye-view/sources/space` owns only fixed upstream URL construction: the
CelesTrak group/TLE query and Launch Library's recent 30-day detailed feed.
CelesTrak group/TLE and SATCAT record queries and Launch Library's recent
30-day detailed feed.
Callers supply the group or end date and own validation, credentials, transport,
response limits and cache policy. The boundary gate checks this portable entry
separately from the Node providers. Satellite rendering and launch replay remain
"description":"A real-time intelligence console for planet Earth — photorealistic 3D globe, live aircraft/ships/satellites/earthquakes/CCTV, and hands-free voice control. Runs in a browser.",
"description":"A real-time intelligence console for planet Earth \u2014 photorealistic 3D globe, live aircraft/ships/satellites/earthquakes/CCTV, and hands-free voice control. Runs in a browser.",
{"source":"Revos addons/Qc cams/quebec_all_cameras.geojson","count":1120,"cameras":[{"id":"4103","kind":"traffic","name":"Route 117 à la jonction de la route 101 et à la hauteur du carrefour giratoire (Rouyn-Noranda)","nameEn":"Junction of route 117 and route 101 and at roundabout (Rouyn-Noranda)","category":"Highway traffic","operator":"Ministere des Transports et de la Mobilite durable (MTQ) - Quebec 511","region":"Abitibi-Témiscamingue","lat":48.26461,"lon":-79.01808,"accuracyM":0,"positionSource":"MTQ georeferenced point (WFS, EPSG:4326, 5 decimals)","viewerUrl":"https://www.quebec511.info/Carte/Fenetres/FenetreVideo.html?id=4103","notes":"Live since 2025/11/26"},{"id":"3820","kind":"traffic","name":"Aut. 20 à la hauteur de la montée Industrielle-et-Commerciale","nameEn":"Aut. 20 at montée Industrielle-et-Commerciale","category":"Highway traffic","operator":"Ministere des Transports et de la Mobilite durable (MTQ) - Quebec 511","region":"Bas-Saint-Laurent—Gaspésie—Îles-de-la-Madeleine","lat":48.43993,"lon":-68.47784,"accuracyM":0,"positionSource":"MTQ georeferenced point (WFS, EPSG:4326, 5 decimals)","viewerUrl":"https://www.quebec511.info/Carte/Fenetres/FenetreVideo.html?id=3820","notes":"Live since 2018/12/12"},{"id":"3821","kind":"traffic","name":"Montée Industrielle-et-Commerciale à Rimouski","nameEn":"Montée Industrielle-et-Commerciale at Rimouski","category":"Highway traffic","operator":"Ministere des Transports et de la Mobilite durable (MTQ) - Quebec 511","region":"Bas-Saint-Laurent—Gaspésie—Îles-de-la-Madeleine","lat":48.44639,"lon":-68.48528,"accuracyM":0,"positionSource":"MTQ georeferenced point (WFS, EPSG:4326, 5 decimals)","viewerUrl":"https://www.quebec511.info/Carte/Fenetres/FenetreVideo.html?id=3821","notes":"Live since 2018/12/12"},{"id":"3822","kind":"traffic","name":"Route 132 à la hauteur du Bic à la jonction de l'Aut. 20","nameEn":"Route 132 at Bic at the junction of Aut. 20","category":"Highway traffic","operator":"Ministere des Transports et de la Mobilite durable (MTQ) - Quebec 511","region":"Bas-Saint-Laurent—Gaspésie—Îles-de-la-Madeleine","lat":48.3775,"lon":-68.68694,"accuracyM":0,"positionSource":"MTQ georeferenced point (WFS, EPSG:4326, 5 decimals)","viewerUrl":"https://www.quebec511.info/Carte/Fenetres/FenetreVideo.html?id=3822","notes":"Live since 2018/12/12"},{"id":"3834","kind":"traffic","name":"Route 132 à la hauteur de Saint-Fabien","nameEn":"Route 132 at Saint-Fabien","category":"Highway traffic","operator":"Ministere des Transports et de la Mobilite durable (MTQ) - Quebec 511","region":"Bas-Saint-Laurent—Gaspésie—Îles-de-la-Madeleine","lat":48.32927,"lon":-68.80262,"accuracyM":0,"positionSource":"MTQ georeferenced point (WFS, EPSG:4326, 5 decimals)","viewerUrl":"https://www.quebec511.info/Carte/Fenetres/FenetreVideo.html?id=3834","notes":"Live since 2019/02/07"},{"id":"3835","kind":"traffic","name":"Route 132 à la hauteur de Cap-Chat (secteur Capucins)","nameEn":"Route 132 at Cap-Chat (Capucins sector)","category":"Highway traffic","operator":"Ministere des Transports et de la Mobilite durable (MTQ) - Quebec 511","region":"Bas-Saint-Laurent—Gaspésie—Îles-de-la-Madeleine","lat":49.04153,"lon":-66.86847,"accuracyM":0,"positionSource":"MTQ georeferenced point (WFS, EPSG:4326, 5 decimals)","viewerUrl":"https://www.quebec511.info/Carte/Fenetres/FenetreVideo.html?id=3835","notes":"Live since 2019/02/07"},{"id":"3856","kind":"traffic","name":"Route 132 à l'est de Gros-Morne","nameEn":"Route 132, east of Gros-Morne","category":"Highway traffic","operator":"Ministere des Transports et de la Mobilite durable (MTQ) - Quebec 511","region":"Bas-Saint-Laurent—Gaspésie—Îles-de-la-Madeleine","lat":49.25302,"lon":-65.48355,"accuracyM":0,"positionSource":"MTQ georeferenced point (WFS, EPSG:4326, 5 decimals)","viewerUrl":"https://www.quebec511.info/Carte/Fenetres/FenetreVideo.html?id=3856","notes":"Live since 2019/12/06"},{"id":"3836","kind":"traffic","name":"Route 132 à la hauteur de la baie du Petit Pabos","nameEn":"Route 132 at baie du Petit Pabos","category":"Highway traffic","operator":"Ministere des Transports et de la Mobilite durable (MTQ) - Quebec 511","region":"Bas-Saint-Laurent—Gaspésie—Îles-de-la-Madeleine","lat":48.37216,"lon":-64.58866,"accuracyM":0,"positionSource":"MTQ georeferenced point (WFS, EPSG:4326, 5 decimals)","viewerUrl":"https://www.quebec511.info/Carte/Fenetres/FenetreVideo.html?id=3836","notes":"Live since 2019/02/07"},{"id":"3837","kind":"traffic","name":"Route 132 à la hauteur de Bonaventure","nameEn":"Route 132 at Bonaventure","category":"Highway traffic","operator":"Ministere des Transports et de la Mobilite durable (MTQ) - Quebec 511","region":"Bas-Saint-Laurent—Gaspésie—Îles-de-la-Madeleine","lat":48.03448,"lon":-65.46953,"accuracyM":0,"positionSource":"MTQ georeferenced point (WFS, EPSG:4326, 5 decimals)","viewerUrl":"https://www.quebec511.info/Carte/Fenetres/FenetreVideo.html?id=3837","Line truncated
Loaded 100 of 1062 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.