Cesium stops its render loop after a render error and offers only a
panel that dismisses itself, and a lost WebGL context never draws again,
so the globe froze or went black while the controls kept responding.
The scene chrome now shows an alert dialog with the error. Retry restarts
the render loop and keeps the notice until a frame renders without an
error; after a lost context it reloads the page, whose address keeps the
view. Reset view opens the app root without the restored hash or share
path. An inline conversation panel never reloads its host page. The
application viewer turns off Cesium's own panel; other callers keep it.
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.
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.
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.
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.
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.
#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.
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.
The two Update-branch merges resolved the locale conflicts by keeping
this branch's side, which dropped nine strings main had added (Utilities
panel, drawing colours, cockpit exit). Rebuild each catalog as main's copy
plus Sea Marks and Ski Pistes, and re-pin the voice tool schema length and
digests on top of main's Utilities target.
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.
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.
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.
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.
One provenanceLine() builds STATE | text | LAYERS for the HUD and
get_hud_caption. hasWord is one lookaround regex, capAtWord one grapheme
pass. Feed-state helpers are imported from layerSnapshot.js directly, and the
kept-whole sanitizer cases are one table.
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.
A dial click shorter than the device start-up let suspend() run on a still-suspended context, so the later resume() left it running with no nodes. Cover paused, connecting and voice-fallback releases in radioNextState tests.
"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.
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.
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.
A building word ending the ask ('… Chapel', '… Library', '… Hall') now
passes the last-word rule only when the result is one a building ask
would accept, so a campus, zoo or district whose name merely starts the
ask (Duke University, Old Town) no longer answers it.
- A building word ("Burj Khalifa Tower", "Sagrada Familia Church") passes
the last-word rule on a building-like result; enclosures stay out.
- The anchor chooses only among the best-named outlines, so a worse name
never wins by containment alone.
- An ask without a quadrant takes only the quadrant of the matching street
nearest the point; a leading quadrant ("SE Main St") may go unsaid too.
- On an unbuffered tile edge a fragment shows as an edge along the
boundary, so a whole building touching it at a corner is not cut.
- Drop two orphaned comments and correct the place-name key wording.
Opening Utilities while Data Layers is open no longer forgets that Contact
was folded for the first, and closing one of them leaves Contact folded
while the other is still open. Enter and Esc in a drawn shape's name keep
focus in the field instead of dropping it to the page. The draw tool's
destroy test now checks it stops following the board, and the Draw gate's
amber-count comment sits on its function again.
- One place-name key for server and tiles, so "St. Paul" and "Saint Paul"
agree in Nominatim name checks.
- Tile street names: a lone letter is a direction only beside another name
word ("E Street" is not "East Street"), and a trailing quadrant ("E St NW")
may go unsaid in the ask.
- A building cut at the tile edge that is nearer the point than any whole
one leaves the point instead of a farther neighbour standing in; an edge
without a tile buffer counts as a cut, so a cut area never answers grounds.
- A batch building install re-judges only results that installed; a late
timeout during caption conversion keeps the pending point drawn; a failed
plain recaption keeps the old mark addressable.
- An interrupted Nominatim dispatch is reclaimed by the next lock owner ten
minutes after the start its marker names; node:sqlite loads on first use
without its experimental warning.
- PHOTON_URL accepts only https, which the page's connect-src allows.
- Shared OpenFreeMap tiles decode each layer when a consumer first selects
it; outline selection reuses the shared ring area and simplification, and
ground probes the shared ring centroid; the grounds word list lives once.
qa-draw-tool drives the rows, waits on a mark's state instead of fixed
sleeps, checks that fly-to centres the line, and compares two screenshots to
show a hidden Line's pixels go and come back. Its clicks and pixel clip stay
clear of the Utilities panel beside the left rail. qa-layer-panel checks
that Directions returns to the Utilities list after a reassembly.
Utilities stays in Cockpit for Directions, but only Data Layers folded the
Cockpit context rail away when opened, set its top below the open panel and
took Cyber's Cockpit size. Utilities now does all three, and Contact
expanding closes it as it closes Data Layers.
Draw and Import used to be reachable by voice through DISPLAY. set_panel_open
now takes 'utilities-panel', and "utilities", "draw", "drawing" and "import"
reach it. This adds one value to the Realtime tool schema, so the pinned
payload digest, enum list and serialized length are re-derived with a note.
The show_data_layers_menu test now finds a row only inside the list that
really holds it, so dropping #utilities-toggles from the lookup fails it.
Answers the review of bdbbc3d7 and the IC8 pass after it.
A shape hidden from Draw's list was only set to alpha 0, so the line's
flowing stroke, which keeps its own colour, stayed painted. Now:
- the world renderer's sync() sets every entity of a hidden mark to
show = false, and FlowMaterialProperty follows the mark's alpha;
- the screen renderer does not project a hidden callout, so it is not drawn
and cannot push a visible label aside;
- hiding acts at once, even before a mark's first frame, and with every
mark hidden the annotations render hold is released after one last frame;
- fly_route skips a hidden route.
Hand-drawn shapes are never merged as duplicates: every one is drawn on
purpose and unnamed until its row names it, so a second pin dropped near
the first was dropped, or replaced the first when its colour differed. A
restyle the renderer cannot draw now also syncs the renderer.
Tests cover each: renderer show/hide and flow alpha, the screen skip, the
render hold and its last frame, a show with no fade-in, hidden routes, two
nearby pins, and the restyle rollback.
main moved the panels to Vue views and a one-open-panel rail, and removed
the two templates this branch edited. This merge carries the branch onto
that architecture; it adds no behaviour of its own beyond the port.
- The Utilities panel is a RailPanel in LayerPanelsView with its own rail
item (handyman). LayerPanel renders the Directions rows into its list with
LayerRows, and falls back to Data Layers without it.
- Draw leaves DisplayPanel for DrawControls, DrawMarkList and DrawSwatches,
which render what the draw tool publishes to the display store and call
its actions, as GeoImport does. drawMarks.js words the rows; the DOM-built
drawMarkList.js is gone. A row's fly-to is an immediate navigation like
Import's ('drawing' gets its Cockpit refusal), the name field keeps a
draft through v-field-value, and the colours are one radio group with a
single tab stop.
- UiSegmented draws an option's icon, named by its label, for the shape
buttons.
- The catalogs take main's languages with this branch's strings, Arabic
included, and the tests follow the views to their new homes.
A street ribbon that holds neither its ring mean nor the anchor is centred
on the street's nearest point when the street runs within 100 m.
Nominatim outline selection ranks an exact name, then one holding every
asked word, then an overlap, and refuses an overlap whose name lacks the
ask's last word unless that word is in the result's address or type, so
"Central Park Zoo" is no longer Central Park. The server caches every
accepted outline and answers each ask with the one holding its resolved
anchor (sent as alat/alon), read across the antimeridian.
A duplicate mark that fails to clean up no longer tears down the new
outline it duplicates.
Each overlay is now its own Infrastructure row instead of a chip on one
shared row: sea-marks (token 6) and ski-pistes (token 7), both
enabled-only, built by createOsmOverlayLayers() from one descriptor
list. Drops the option group and its share-codec defaults, gives each
layer its own name, voice entry and CURRENT-STATE row, and shifts the
token-ledger fixtures to the next free digit 8.
Main moved on since this PR opened; the overlay layer is ported to the
current contracts:
- Layer-state token 6 (j belongs to transit; new single-character
tokens are digits) with a reservation-ledger row, and the option owner
renamed to the layer id so the compact share codec can encode it.
- Constructed per catalog by createOsmOverlaysLayer() in
constructCatalog.js; src/standalone/data.js no longer registers layers.
- Drapes through maps/imageryHost.js, so the overlays show on Google 3D
(tileset imagery) as well as the globe stacks, and follow map-stack
switches. Imagery layers are created on enable and destroyed on
disable/destroy.
- Translated chip labels and layer name in every locale, LAYER_NAME_KEYS,
Infrastructure panel group, voice manifest entry.
- Attribution: OpenSeaMap tiles CC BY-SA 2.0, OpenSnowMap credit as
required, OSM ODbL credit for both; bulk-download note in DATA_SOURCES.
- CURRENT-STATE row, changelog fragment, share-codec absent defaults,
updated token-ledger fixtures and voice schema pins.
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]>
A release that only selects a station now stops the looping noise source and suspends its AudioContext. The qa-radio dial scenarios start from an active stream, or expect a silent release from a stopped Radio.
Han and kana have no word edges, so a CJK forbidden word now counts anywhere
in the reply. The proxy no longer counts words: the model reads Rue de Rivoli
as one, so only the 80-character word-boundary cap remains.
Also restore src/voice/gevActions.test.mjs, reformatted by mistake in 818d2c82.
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.
- The proxy keeps combining marks, in-word apostrophes and name dots
(ar vowel signs, fr d'Orléans, ja ・), normalizes to NFC, caps every
summary in characters and cuts at a word boundary.
- feedState is nullable instead of a `none` token, and the summary has at
most five words: "exactly five" made the model pad short views with
"no data" or "unavailable".
- The HUD caches only the model's words and adds the current warning on
every paint; an RTL locale gets a leading right-to-left mark.
- The echo check also rejects an all-Latin reply in a non-Latin language,
translated layer ids and, with every feed healthy, the UNAVAILABLE word;
places are matched by word and one-word layer names are skipped in
Latin-script locales.
- The warning tag names the worst layers first; a partial analyst card
shows one partial chip.
Releasing the tuning dial always started the chosen station, plus tuning
static, even when nothing was playing. It now follows the Previous/Next
rule: the release plays only if a stream was loading, buffering or playing
when the drag began; otherwise it just selects the station.
The privacy note no longer lists finishing tuning as a way to connect and
now names voice instead, in all locales. Docs, changelog and comments are
tightened to match.
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.
main moved the Radio markup into Vue views and rebuilt the UI tests on
stores. Resolved:
- src/ui/templates/context.html: deleted in main; the new privacy note now
lives in src/ui/views/panels/RadioPanel.vue.
- src/ui/radioControls.test.mjs: main's store fixture; the Next/Previous
autoplay test drives store.act('cycleRadio').
- src/voice/gevRealtime.test.mjs: main's createTestVoiceUi; the stop and
select handoff cases stay one table-driven test.
- src/data/radioNextState.test.mjs reads the published gev-radio view.
- src/i18n/locales/ar.json: the new note translated for the Arabic locale.
- _relocalize() now drops the received reply and aborts one in flight, so
the next tick asks in the new language instead of keeping old-language
text under the new-language state tag.
- The English-echo check also allows state words inside a place label
(Estación Loading Dock).
- Reword the non-English instruction so it reads for every language name.
- Add the two new test files to the format scope.
The catalog stored the designator on satrec.intldesg for
internationalDesignatorYear, while the active-TLE lookup parsed the same
columns with tleLineLaunchYear. Each catalog entry now keeps launchYear from
tleLineLaunchYear, which moves to src/sources/tle.js beside the other TLE
field readers, and internationalDesignatorYear goes.
A blank designator now reads as an unknown year in both lookups; the
active-TLE lookup read it as 2000.
The traffic timing test starts a Vite server rooted at the package, which
wrote its dependency cache to node_modules/.vite and kept writing after the
server closed. A host running Core's suite then finds files inside Core and
its check refuses them. The server now uses a temporary cache directory,
removed afterwards, and skips dependency discovery, which the SSR-loaded
modules never need.
The HUD place-lookup changelog note credits its PR, #1459.
Directions, the Contacts marker and panel, placed annotation labels, the
drawn-line measure, the Cockpit and formatRouteDistance each chose the unit
and precision on their own, and most chose before rounding: #1450, #1433 and
formatDistanceText or kilometreDigits from src/data/distanceFormat.js, which
cut after rounding. Each keeps its own metre step and rendering.
That also fixes the 10 km cut the Contacts marker and panel, the drawn-line
measure and the Cockpit still had (9,960 m read 10.0 km). The helper returns
kilometres already rounded, so the plain marker and the localized panel no
longer disagree on half-tenths such as 1,450 m.
A negative annotation distance now leaves the label without a distance, and
a drawn line with a coordinate that is not a number shows no measure, rather
than printing 0 m or NaN km. The HUD altitude tag and the spoken view height
keep their own unit choice.
UiRail takes `collapsible`: while no panel on its side is open, each item
shows its icon and label as a pill, and once one opens the labels fold away
to the icon rail, making room for the panel. The tip is not shown while the
label is on screen; the button keeps its accessible name and the visible
label is hidden from assistive technology. The side panel rails turn it on.
Each rail is now placed by its screen-side edge, at the rail clearance, so a
rail that widens grows toward the middle of the screen; collapsed, it sits
6px outside its stack as before.
Since the UI moved to single-file components, every unit-test process
compiled the views it imports from scratch, and the loader ran off-thread
(module.register), adding a round trip to every module a test imports,
including the ones that are not components. Core's unit tests took about
twice as long, and a host running Core's suite on smaller runners passed
its CI time limit.
register-vue now installs synchronous hooks on the loading thread
(module.registerHooks). Compiled components are kept in the system temp
directory, keyed by the file's path and text, the Vue version and the
loader's own code, so each one compiles once across test processes; the
compiler itself loads only on a cache miss. Cached modules run as code, so
the directory is used only when it is a real directory owned by the current
user and writable by no one else; on Windows it lives in the user's local
app data instead of a temp directory that may be shared. The cache never
sits inside the package tree, and GEV_VUE_CACHE=0 compiles every time.
On four cores the full suite went from 441 s to 240 s from a cold cache.
The transit, ALPR, vessel, CCTV and flight/military context fixes each
wrapped 359.5° to north with an inline Math.round(x) % 360. They now call
roundHeadingDegrees in src/data/headingDegrees.js, which also wraps negative
and multi-turn bearings. normalizeHeading moves there with it, and
cockpitMath re-exports it so existing imports keep working. It now adds a
turn only to a negative remainder, so a bearing just under half a degree
keeps its precision.
The awareness, Local ADS-B, cockpit wind, cockpit heading and Street Level
readouts use the same helper. The Cockpit's TRACK ACQUIRED signal rounded
without wrapping and could read COURSE 360°; it now uses
formatHeadingReadout, which shows --- for a track that is not finite.
The panel now renders from the CCTV store, so the meta line is no longer
written to a DOM node by _renderCctvState. Assert on presentCctvPanel's
meta text instead.
The application package boundary owns src/i18n/locales/ar.json beside the
other catalogs, and the style.css shim test checks that the shim holds only
import statements rather than counting its lines, so adding rtl.css to it
does not read as rules.
Selects pad the chevron's side with padding-inline-end, as do the Radio
filter, the SDR gain select and the voice timeout select, which set their
own padding; rtl.css moves the chevron to the left, where an Arabic field's
text ends. A rail legend's range keeps left-to-right order under its colour
ramp, which always runs low to high from left to right.
Main moved the UI to Vue views and Pinia stores, so the Arabic changes to
the old DOM controllers now live in their views: the rail legend range
comes from formatScaleRange (keeping units a source supplies as given),
the voice timeout options are worded by Intl unit formatting in
VoiceControl, and the scene editor's JSON fields stay left to right in
SceneDialog. Main's CCTV status line, launch panel and calibration values
keep the Arabic isolates and Western-digit formatting.
isolate() and keepCodesWithValues() read the locale through getLocale(),
which follows the page's vue-i18n instance, and the Intl formatters take
intlLocale(getLocale()). A language switch now applies in place, and
applyDocumentLanguage sets <html dir> on every switch, so the page mirrors
without a reload.
The icon rails and their tips are placed with logical properties, so they
stay on the outer side of the mirrored stacks; Cyber's new stack offsets,
padding and edge lines are logical too, and rtl.css moves Cyber's right
stack to its new rail clearance. Main removed the Context mini radio, so its
right-to-left rules and test go.
The Arabic catalog gains main's new strings and drops the ones main no
longer uses; the other catalogs keep main's new keys and the branch's
landing labels.
With the panel rails, the left stack's red edge line stood alone beside
the rail whenever no panel was open. It now shows only while a left panel
is open, and the right stack gains the mirrored line for its open panel.
The right stack takes the line and its gap without moving its panels, so
its rail sits outside the line like the left one.
A tall panel moves its stack to the focus or tail layout, and the rail
inside the stack went with it, so the Data Layers and Authoring buttons
jumped up and the right rail followed. Each layout pass now offsets the
rail back to the resting top, with the same transition as the stack, and
the right rail lines up with the left rail's buttons. Cyber pins the left
stack's top, so it flags that and the rail takes no offset.
Choosing a language in the picker now applies it at once. The choice is
stored and ?lang is kept in step through replaceState, and the picker keeps
focus.
Code that writes translated text itself re-presents it on a switch:
onLocaleChange() lets an owner listen, removed at teardown, and status
writers hand over a function of the locale (voice cards, scene director,
SDR, draw hints, imagery split, map sources, Bhote Koshi media) so the
words are rebuilt rather than re-sent. The HUD, cockpit, radio, CCTV,
launch, awareness and credit texts follow the same hook, and
applyDocumentTranslations can be applied again without undoing text code
wrote since.
The unsaved-work registry and persistLocaleAndReload existed only to guard
the reload, so they go, with their catalog entries.
Every data-i18n, data-i18n-title, data-i18n-aria-label and
data-i18n-placeholder marker in the Vue templates becomes a t() call with
the key the boot pass used to look up, contexts included. The three labels
the shell rewrote at boot (Visual Presets, Location and the standby
description) render through t() too, and that rewrite is gone. With the
locale in vue-i18n, view text now follows a locale change, and markup
under v-if or v-for no longer comes back in English when it re-renders.
Rendered text and attributes are unchanged in every locale. The boot pass
still translates static page markup such as the loading screen and a host's
own markup, and npm run i18n rejects data-i18n* in a Vue template.
The application views install one vue-i18n instance that holds the active
locale and the catalogs. src/i18n/index.js stays free of Vue: bindLocale()
keeps its locale in the instance's ref, so t(), tn() and the Intl formatters
read it from there and a view that translates while rendering renders again
when the locale changes. A tag written straight to the ref is normalized.
Message formatting, catalogs and call sites are unchanged, and switching
language still reloads the page.
The instance outlives the views: unmounting one no longer disposes it. The
browser build defines vue-i18n's feature flags and drops its message
compiler, which Core does not use.
The Vue and Pinia migration's entries move out of CHANGELOG.md into
changelog.d/0000-vue-and-pinia.md, newest first, as the contributing guide
asks. 0000 stands in for the pull request number until it is opened. The
migration plan's cleanup step now records where it stands: the template
expansion is gone, the pattern is required for new UI, the page-level
markup tests render the real views, the attribute translation pass waits
for vue-i18n, and a host panel registration API waits for its first host.
CONTRIBUTING gains a Building UI section: new UX elements are Vue
components rendering a Pinia store their owner publishes to, portable
owners stay framework-free behind a publish option, controls come from
src/ui/kit first, controllers don't bind the DOM by hand (Teleport where a
view renders elsewhere; body classes, focus, same-task measured positions
and painted surfaces stay imperative), text goes through t()/tn() with
msg() for stored English, and tests mount components. The README and the
architecture summary drop the stale vanilla-JS claim, the UI ownership doc
points to the guide, and the migration doc's UiPanel note names
v-panel-state as what draws a panel's collapse.
The Cockpit display portal publishes the Display popover's slots to the
gev-display store while Cockpit is on, and the Display panel teleports its
HUD, Detection, Parameters and 3D groups into them and back, each only
while its own slot is published. The portal no longer moves the groups:
it keeps each surface's scroll position, puts back focus the move
dropped, and lays the surfaces out once Vue has moved them. The popover's
uses-shared-display-controls class renders from the store, and the right
rail no longer re-homes the parameters panel the markup already places.
PanelChrome, its collapse-preference restore and the phone shell read a
panel's disclosure state back from the gev-panel-chrome store
(_shownPanel), falling back to the markup's classes before the panel is
first presented, instead of reading the classes v-panel-state draws. The
share link and the phone shell share one rule for a panel the person keeps
closed (prefersCollapsed). The rails, the accordion and the hover
disclosure still read the classes, which the directive draws in the same
task as their layout.
The Cockpit layout pass publishes the utility strip's fit to the
gev-cockpit store: the HUD's --cockpit-utility-* lengths, whether only the
expanded control fits, and whether a collapsed control beside an expanded
one is hidden from assistive technology. CockpitView renders the lengths
on the HUD and CockpitUtilityControls the strip's class and each control's
aria-hidden, which now also follows a popover opened between passes. Focus
leaves a control before the strip hides it. Exit publishes no layout,
which also clears the expanded height, class and aria-hidden the old exit
left behind. The briefing and Contact cards' positions stay imperative:
the same pass measures them straight after writing them.
VisualSettings publishes the active style's parameter rows to the display
store, and the Display panel renders them with the shared row component;
a moved slider goes back through the store's setStyleParameter action.
The readout snaps a value to its step as the browser does, half-steps up
and an off-grid maximum down. A preset that resets the shown style's
uniforms republishes its rows, input for another style's parameters is
ignored, and a stopped owner takes its rows with it unless a newer owner
has published since. createStyleParameters is deprecated and builds its
rows the same way.
RecordingControls publishes the safe frame a recording composes in, 16:9
or 9:16 (null while none records), to the new recording store, and the
scene chrome renders the overlay's active and ratio classes from it
instead of the controls writing them. Any frame but 9:16, even none named,
records in 16:9 as before.
LocationControls presents whether the search field is open and whether a
lookup runs, and the dock renders its expanded and searching classes and
calls toggleSearch(). The lookup, the navigation's settle, Escape and the
location tray's collapse reach the field through the controls
(setSearching, settleSearch, closeSearch) instead of writing its classes.
LocationControls no longer takes searchToggle, LocationSearch takes
setSearching and NavigationController takes settleSearch in place of
searchInput.
The top-center and cockpit reset buttons call the location controls'
new reset() through the globe actions store, and render a reset under
way from it, instead of the location controls binding their clicks and
the navigation writing their labels. The store keeps each owner's reset
under way like its clear, and controls replaced mid-reset keep it on the
buttons. LocationControls no longer takes resetButtons.
The camera orientation controls publish whether the map is tilted and the
camera's whole-degree heading instead of writing the buttons' attributes,
and answer tilt and north-up through the globe actions store. The new
GlobeCompass renders the heading, so a rotating camera re-renders only the
compass. The store drops an unchanged camera state and keeps each clear
owner's busy state, so the buttons show the shell that answers them: a
shell replaced by a newer one, or disposed mid-clear, leaves the live
shell's state alone, and an older shell that answers again shows its own.
createOwners gains owns() and ownerOf().
The top-center bar is now GlobeActions.vue. The shell publishes whether a
clear is under way and answers the clear and share buttons through the
globe actions store, instead of binding listeners and writing attributes on
the buttons. A shell disposed mid-clear resets the busy state, so the next
shell's bar is not left busy. bindClearLayersControl stays exported and is
deprecated.
The Contact and briefing cards' headers repeated the same frame: the title
grid over a hairline, the accent kicker and the square action buttons. It
lives in cockpit-card-header, cockpit-card-kicker and cockpit-card-actions
now, beside each card's own classes, which keep only their spacing and
sizes; the cyber theme targets the shared classes. The computed styles of
every header element are unchanged in the default and cyber themes,
expanded and collapsed.
UiSlider merges a caller's own input, change and blur handlers with its
own, leaves the input alone on a null model, and gains unstyled for a
range drawn as the browser's own. The rail card setting, observation
timeline, imagery opacity, Street Level date and Bhote Koshi split and
progress ranges now render through it. The timeline and the setting
ranges put the handle back once their owner has answered, since a drag
that ends where the slider last rendered leaves its model unchanged.
The radio tuner keeps its own input: its owner moves the handle back to
the selected station without changing the slot.
UiBadge takes a tone and carries Core's gev-status-badge class, with
the density variants and colours left to its callers; the CCTV source and
calibration badges and the rail card badges render through it. UiEmpty takes
a title and hint, with the title as the line itself, since Core's
empty states are one line; the Scene shot list, mission roster and imagery
strip use it.
UiSelect renders the bare native select with v-model, options and
disabled. Options that carry their own attributes come through the slot, a
caller's own change handler still reaches the select, and hold never
rewrites a focused select and takes the model back on blur. Without hold, a
select takes its model back once its change has rendered, so a pick its
owner refuses goes back, as the raw value binding did.
The HUD layout, draw colour, RTL-SDR gain, radio station tag, voice
provider, voice timeout, scene and CCTV camera selects now render through
it.
UiChip is a non-interactive label chip and renders the voice
card's chips. The pressable chips and pills are pressed toggles, so the
rail card chips, map source chips and city and place pills render through
UiButton with pressedClass, keeping their classes, attributes and the
classes other code adds. The pills now report aria-pressed.
UiButton gains pressedClass, Core's class on a pressed button. The pressed
classes toggle through classList, so classes other code adds survive a
press, a caller's class change keeps them, a renamed pressedClass leaves
the button, and server-rendered markup carries them.
The Display, SDR, Radio and Cockpit enable, Cockpit weather and briefing,
mission replay, Street Level, TR-3B, overlay, voice and CCTV toggles now
render through it with the same ids, classes and aria-pressed values.
UiSlider takes v-model, min, max, step and disabled
over Core's native range input, emits numbers, and with `hold` never
rewrites a focused handle and takes the model back on blur. It is a
functional render function, so the portable style parameter rows use it
too. The Display, Radio, RTL-SDR, Cockpit, mission replay and style
parameter sliders render with it; server-rendered markup keeps their
default values.
RadioTuner binds the slider's pointer, key and value events in its
template and calls RadioControls through the radio store; the drag,
its slot and the pointer capture stay with RadioControls, unchanged.
The Radio panel's and Cockpit's Enable, launcher, station tag,
transport and volume controls call RadioControls through the radio
store instead of DOM listeners. A refused station tag gets the kept tag
back from act(), which now returns what the answering action returned.
The Cockpit's station steps keep rotate: false. The unused element
fields go from the shell.
SdrCard calls LocalSdrControls through radio store actions instead of
DOM listeners, and renders its gain options from the receiver's own
R820T gain steps with page-language labels. The display and radio
stores share one owner list (createOwners): the newest connected owner
with an action answers it.
Street Level's fit and panorama filters and Recent Imagery's mode render
through the kit's UiSegmented, and their owners answer the choice
through the Street Level store (setPano, setRenderMode) and the
readout's onSetMode. The imperative segmentedControl.js adapter and its
test go. UiSegmented moves focus at once on a key, a radio group from
the option holding focus and an arrows group from the selection, and
asks for a change only when the key lands somewhere new.
The file import no longer looks up or listens on the Import button, the
picker or the list, and no longer restores focus itself. It connects
importGeoFiles, toggleGeoImport, flyToGeoImport and removeGeoImport to
the display store. A new GeoImport view owns the button, the picker, the
rows and the hint: it opens the picker once the import has presented
itself, sends the pick and the row buttons' actions, and hands keyboard
focus on with v-focus-safe-children before a focused row goes, to the
next row's same button, else the previous row's, else Import.
The draw tool no longer looks up or listens on its controls. It connects
toggleDraw, setDrawShape, setDrawColor, setDrawLabel and clearDraw to the
display store, which now holds several owners and lets the newest owner
of an action answer it, and it publishes the colour and label beside the
shape, state and hint. The Draw panel renders the shape choice with
UiSegmented and binds the label and colour to what the tool publishes.
The label field's own Enter listener goes: the session's capture-phase
key handler already finishes the shape from that field.
The Display panel, the dock's style buttons and the clean-view exit call
the display store's actions, and DisplayBindings connects their handlers
to it, replacing the bindDisplayControls listeners. The detection
allocation and 3D model coverage choices render through a new kit
UiSegmented, which takes v-model, options, label,
stacked and disabled, and adds a keyboard policy, item classes and
per-option attributes.
Shell element fields and facade accessors that only the old listeners
read are removed. The package boundaries now own UiSegmented and the
UiPanel kit files, and the format scope drops the deleted test.
The Authoring panel's and shot list's buttons, the CCTV panel's buttons and
Street Level's nearest button are the kit's UiButton, keeping their classes:
danger buttons take the alert tone and primary ones the primary variant. The
CCTV on/off, coverage, auto-hop, projection and adjust toggles, which showed
their state only by colour, now carry aria-pressed too.
The Data, Authoring, Weather, Recent Imagery, Context, CCTV, Street Level
and Radio panels each wrote the same frame by hand. They are now one kit
component, UiPanel, with a title, badge/actions/default slots and
v-model:collapsed, drawing Core's panel markup and classes beside the kit's;
props cover the differences (sideways, glow, scroll, railScroller, tag,
toggleWords). RailPanel binds it to the panel chrome store: v-panel-state
writes its state classes at once, and its button's clicks reach PanelChrome.
UiPanelToggle is the collapse button UiPanel and PanelCollapseButton share.
UiPanel's collapsed drives the button and ui-panel--collapsed
only; the owner hides the body by writing the root's collapsed class, since
the rails measure a panel in the task that opens it. PanelCollapseButton
takes startCollapsed instead of initial and initialTitle, so a button shows
its state's glyph and words before startup presents the panel. Titles render
through t() instead of data-i18n, and the unused "Expand panel" string is
pruned. v-panel-state redraws the presented state after a re-render, in
case a root's class binding ever changes.
Cockpit presence now carries contextCollapsed and signalCollapsed.
setContextCollapsed and setSignalCollapsed update presence and republish
it only when the value changes, instead of writing the dataset and the
toggle attributes. CockpitContext and CockpitBriefing bind data-collapsed
and their toggles' aria-expanded, aria-label, title and icon from
presence. The Cockpit utility QA script waits a frame for Vue to render
before it reads the toggles.
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]>
A panel's collapsed, dock-pinned, dock-pinned-top, layout-auto-collapsed
and accordion-collapsed classes are drawn by its root's v-panel-state from
the gev-panel-chrome store. PanelChrome, its restore, the layout
controller, both rails and the side accordion present each panel's state
there instead of writing its classes; the rails call onCollapse(panel,
{collapsed, auto}). The directive applies a change in a synchronous
watcher, since the rails measure a panel in the task that opens or closes
it.
Destroying the chrome inside Cockpit now puts back the panels Cockpit's
entry closed while it can still present them. Without a shell, the voice
tools' set_panel_open reports that there is no panel to open. QA scripts
present the states they set instead of writing the classes.
PanelChrome no longer writes the panels' collapse buttons, the command
dock tray toggles' names or the dock pins' pressed state. It presents each
panel's open and pinned state to the new gev-panel-chrome store, and the
buttons, now views, call it there: PanelCollapseButton for the panel
headers, bound toggles and pins in the command dock. Clicks take effect
once the chrome is initialized; a new owner starts from its own states.
bindPanelDisclosure binds only a panel's Escape and refuses the retired
buttons and onChange arguments. UiButton joins the component kit.
SceneControls no longer takes element handles or writes the Scene panel's
button states, PLAY label, status line, progress bar or runtime chip: it
presents them to the gev-scenes store, and the panel's buttons, scene
select and IMPORT chooser call its actions there. The chooser opens only
while an owner presents the panel and is cleared once its import settles.
On destroy the panel keeps its last status and buttons, without shots or
the runtime chip, as before.
A refreshed manifest could lose its credit in two ways. A frame still
loading when the refresh arrived had no credit yet, so the refresh was
skipped and the frame installed with the credit of the snapshot it was
staged from. And a host switch restaged the shown frame, then re-applied
the refreshed manifest captured when the restage began, overwriting any
newer refresh that arrived while it loaded.
Each frame now remembers the latest refreshed manifest of its product,
whether or not its imagery is installed yet, and takes its credit from it,
falling back to the snapshot it was staged from. That snapshot still
supplies the time and bounds. A restage passes the newest refreshed
manifest to the new frame up front instead of re-applying a captured one
afterwards, and a refresh that arrives while a frame loads is kept for a
later restage.
initFirstRunExperience no longer writes the launcher card's classes,
attributes and text, wires its listeners or removes it: it presents the
card's visibility, busy state, status line, copy and scroll fade to the
gev-first-run store and connects the controls the welcome view's choices,
DISMISS and "Don't show this again" call. A refused save hands the tick
back to the view to revert. The launcher still reads the rendered card for
its hit test, keyboard trap, focus and overflow measurement.
The owner-chosen copy moves to src/firstRunCopy.js, shared by both. The
source-text tests give way to component tests, and the first-run mutation
QA follows the new code (67/67 caught).
A weather source can describe its own products (credit, units, coverage,
legend) through an optional `display` object in each product manifest:
source, credit, units, coverage, info, legend and extent. The weather
layer uses each valid field in the row summary, legend, info text,
coverage navigation, outside-coverage status, layer stats and imagery
credit, and keeps today's built-in value for any field that is absent or
invalid. Units and legend describe one scale, so they apply only as a
valid pair; otherwise both keep their built-in values, and the radar
info heading names the units in use.
Text is single-line and length-capped. It rejects control characters
(U+0000-U+001F, U+007F-U+009F), line and paragraph separators, and
invisible or format characters: soft hyphen (U+00AD), combining grapheme
joiner (U+034F), Arabic letter mark (U+061C), Mongolian vowel separator
(U+180E), zero-width and direction marks (U+200B-U+200F), bidi embedding
and override (U+202A-U+202E), word joiner, invisible operators and bidi
isolates (U+2060-U+2069), variation selectors (U+FE00-U+FE0F), the
byte-order mark (U+FEFF) and tag characters (U+E0000-U+E007F).
Legends need 2-16 stops whose labels are plain decimal numbers or short
text and whose colours are #rrggbb. A number label must read back
exactly as the legend shows it: within the safe integer range, with no
leading zeros and no digits lost to rounding. Extents must be finite, in
range and non-empty. Supplied source and units text is data: the layer
marks it so the panels show it untranslated, while built-in values are
translated as before.
The imagery credit is escaped as plain text and follows a refreshed
manifest even when the shown frame stays the same. A refresh changes
only the credit: the shown frame keeps the snapshot it was staged from,
so a later host switch restages it with its own time and bounds and the
refreshed credit. Manifests without `display`, including the bundled
nowCOAST ones, render exactly as before.
IntelHUD no longer builds #intel-hud with innerHTML or writes its readouts
by id: it publishes its readouts, typing summary, REC blink, visibility,
layout variant and style colors through its publish option to the gev-hud
store, and the intel-hud application view renders them. The corners render
from mount so the panel layout controllers register them as obstacles.
The vessel layer presents the selected vessel's readout through
src/hudReadouts.js, which IntelHUD follows, instead of writing into the
HUD. The Cyber sonar state answers a HUD toggle from IntelHUD's visibility
rather than the class the view renders on its next update.
The awareness layer presents the compass ring, heading, cardinal labels and
contact bearing markers through subscribeDirectionOverlay(); the application
shell publishes them to the gev-awareness-direction store and the scene
chrome renders them in the viewer's container.
The world overlay presents its focusable mirror of painted cards through
subscribeWorldOverlayActions() and answers activateWorldOverlayAction();
the application shell publishes the list to a store and the scene chrome
renders the buttons and the status, announcing a repeated focus again.
The module stays free of Vue for the portable transit entry, and its
canvases remain painting surfaces.
The screen annotation renderer keeps each mark's SVG parts as attribute
records and presents the board to a store after every rendered frame; a
view teleports it to the page and hands back the callout text and mark
groups the renderer measures. The tracked-entity fade is measured once
the frame is in the document, removed marks fade out before they leave,
and the styles it injected move to annotations.css.
The celestial ring presents its overlay to a store and the scene chrome
renders it in the globe container, handing back the two canvases the
ring paints. The application shell passes the store in so the keyhole
helpers other packages import stay free of Vue. Without its canvases the
ring still tracks whether the globe is framed and turns itself off.
The Bhote Koshi layer keeps its panel controls as plain records and
presents them to a store; the context rail renders the panel as its last
child and marks itself while it is on. Controls are bound to the enable
that published them, and an unchanged frame is not sent again.
The launches layer keeps the replay state, projection and wording and
presents the replay vehicle overlay through subscribeReplayOverlay(),
sending nothing for an unchanged frame, so the portable launches entry
stays free of Vue. The application shell publishes each presentation to
the gev-mission-replay store and the scene chrome teleports the vehicle,
orbit dot and callout into the globe container with the same markup,
classes, mode, transform, rotation and sonar opacities. Hiding keeps the
mode, place and text, as before.
createWindInspectionMarker() moves to src/layers/wind/inspectionMarker.js,
so the portable ./tools entry that reads wind/inspection.js stays free of
Vue. It keeps the frame listener and projection and presents where the
inspected point falls to the gev-wind-inspection store after each
rendered frame, publishing nothing for an unchanged frame; the scene
chrome teleports the marker into the wind layer's container with the
same class, inline style, hidden state and position. The application
guide lists everything leaving out the scene chrome now leaves out.
PointerReticle keeps the turn's timers and tool tracking and presents
where the user pointed to the gev-pointer-reticle store, one entry per
reticle; the command dock teleports each to the body with the same
class, aria-hidden, hidden state, target and position. The voice QA
reads the card and reticle after Vue's flush, and the application guide
says leaving out the command dock leaves out the voice control and its
reticle.
createFrameRateMonitor() keeps the key, frame count and interval and
publishes the readout to the gev-status store; the scene chrome renders
it after the subtitle with the same class, hidden state and title. A
page without the title bar still gets no monitor, and a monitor only
updates or withdraws the readout the store shows.
The data-pack presentations keep the Cesium resources, object URLs and
pick targets and present each pack's attribution card, with its media
player, to the gev-data-packs store; the DataPacks view teleports one
panel per set of presentations into the viewer's container with the same
markup and inline styles, and stops a removed card's player as it goes.
The scene interactions owner keeps the pick handler and the interaction
session and presents the panel to the gev-scene-interactions store; the
SceneInteractions view teleports it into the viewer's container with the
same markup and inline styles. Each activation keys its own panel, each
button carries the owner it was rendered for, and only the owner the
store shows may change or remove it.
LocationControls offers the indicator with showOrbit(active) in its
presented tray instead of building it on the body, and the scene chrome
teleports it there. Orbit toggles, including the voice switch, light it
through the controls.
ShellFeedback and the scene director show toasts through createToast(),
each on its own timer as before, and the scene chrome renders the toast.
Every show rewrites the text, so a repeated message is announced again.
ShellFeedback publishes the loading status and the road network chip, and
CctvControls the camera grid chip, to the gev-status store; the scene
chrome renders them. SplitFlapText plays the departure-board cascade that
splitFlap.js plans, writing only the label's one text node, and rests when
its owner is destroyed. setSplitFlapText and disposeSplitFlap are gone.
createImagerySplit keeps the divider's value, drag, keyboard, CSS position
and scene split, and publishes its line to the imagery split store.
ImagerySplitView, in the scene chrome, teleports each line to the body, so
recent imagery and the Bhote Koshi event no longer build divider DOM.
The dock renders the chips from the map source controls' published view,
so renderMapStackChips, syncMapStackChips and MAP_STACK_CHIP_CLASS have no
callers. Their tests now pin the chip models; the dock test keeps the
rendered chips' button type and native enablement.
createVoiceControl now returns a presenter instead of building the
control with innerHTML: the session, cost, input, card and panel owners
write its state by part and listen for interactions, and VoiceControl.vue
renders it in the command dock, between the location bar and the visual
presets as before. Focus moves wait for the render; the waveform's bars
publish apart from the control and only when they change; the minutes
field keeps a draft until its owner writes it. Owner tests use a real
presenter on their own store, and view tests render the control.
The Vue views apply the gev-action-button and gev-status-badge classes
themselves, so the DOM builders have no users left. The docs describe the
views instead.
createSceneDialog keeps its builder but publishes the dialog's lines,
fields, buttons and status to a scene sharing store; SceneDialog.vue,
teleported to the body from the scene panels view, draws the same modal
with the same styles and opens it with Cancel focused. The EDIT DETAILS /
SHARE SCENE bar is drawn by that view while sharing offers it. The status
directive is shared with Provider Settings, and the two QA scripts that
drove the old DOM read the new dialog.
initKeySetup keeps the setup requests, the ChatGPT OAuth flow, the pasted
field values and the dialog's keyboard, and publishes the POWER UP chip,
the dialog, its key rows and the status line to the gev-provider-settings
store. ProviderSettingsView renders them and sends clicks and typing to
the connected owner; in Cyber the chip is teleported into the top
toolbar. A successful save renders the rows afresh so no field keeps a
key, and the status line announces a repeated message again. The voice
action opens and closes the dialog through the same controls.
LocalePicker renders the existing picker model with the same markup and
listbox behaviour; ProviderSettingsView shows it while the shell publishes
the active language and its choice handler to the gev-locale store. A host
passing localePicker: false publishes nothing. A choice reaches the shell
after the list is closed on screen, and every opening keeps the active
option in view.
index.html now lists each view's host and the loading screen itself, so
the gev:template expansion plugin, its view list, the loading-screen
template and the ./build/html export are gone. createApplication takes
omitViews for views an application leaves out; their hosts are removed,
and a view an earlier call rendered is unmounted.
The FIRMS test that checks a real all-addresses connect failure relied on
the system resolving localhost to both ::1 and 127.0.0.1. On a host whose
localhost resolves only to 127.0.0.1, fetch makes a single attempt, gets a
plain ECONNREFUSED instead of an AggregateError, and the test fails.
Mock dns.lookup for that fetch to return both loopback addresses, so the
real fetch still attempts both families and the error shape check stays
deterministic. Assert the mock was used, so a runtime that stops resolving
through dns.lookup fails loudly instead of silently testing something else.
The Cockpit controller publishes which surfaces are shown (the entry
button and TR-3B chip, the HUD with its exit and reset buttons and
briefing card, and the Contact card) and the views bind them. Focus moves
to the exit button, and back to the entry, after Vue renders them.
Layout scheduling counts the published presence as visible, so entry
still lays out the cards. The Display portal restores only focus its move
dropped. The Missions layer no longer hides the Contact card directly.
Every scrolling surface carries the kit's .ui-scroll, which
draws the thin themed scrollbar from --scroll-thumb and --scroll-track; the
Context panel and others drew the platform's light scrollbar because the
page declares no dark color-scheme. The per-panel scrollbar rules go; Cyber
sets the two tokens and Cockpit's HUD windows keep their accent tint.
Selects draw .gev-field's own chevron instead of the platform arrow, the
voice selects join .gev-field, number fields keep dark spinners, and the
Street Level range uses the shared slider in its green.
RadioTuner renders the band label, readouts, slider and needle from the
Radio store. RadioControls keeps the drag and its slot and publishes what
the tuner shows; the component lays out the tick tape against the dial's
measured width, drawing no ticks before it has one, and lays out the
snapped needle before it drops the drag class so the needle does not
animate from where the drag let go.
The Display and Radio launchers, their popovers and the compact Radio
transport, Enable button and volume now render from the Radio store.
RadioControls owns the disclosure state; Cockpit Escape and the Display
reveal read that state instead of the DOM, and the Enable toggle guards
re-presses synchronously. The compact transport reuses the panel's
transport state and adds only its glyph and title.
The Cockpit controller no longer reconciles the live signal rows by hand.
publishCockpitSignals turns its signal items into rows, keyed by the
signal and its action target as before, with repeats of a key numbered,
and publishes them to the gev-cockpit store; a new CockpitSignalList
component renders them with a keyed v-for inside the briefing card.
Rows reorder several times a second, and a browser blurs any element it
moves. The new v-focus-safe-children directive turns Vue's move of the
row holding focus into moves of the rows around it, reaching the same
order while the focused row stays connected, and before the focused row
is removed it hands focus to the shown briefing page's tab, or the
collapse toggle, as the reconciler did. Selection stays with the
controller's click listener.
The focus test now runs the real components over happy-dom and counts
any move or removal of the focused row as a loss, including across 60
random reorders. The Cockpit QA script publishes and waits a frame
before reading the rows.
The Cockpit controller no longer writes the HUD's instruments, the flight
plan card, the visor's destination arrow or the vision selector. It keeps
them as plain state, starting from cockpitInstrumentState.js, and
publishes copies to the gev-cockpit store: the instruments on every HUD
update, the route only when it changed, the vision selector when the mode
changes. CockpitView renders the callsign, aircraft meta and emergency
mark, speed and altitude rims and ticks, compass tape, clock, position,
HUD layer, destination arrow and vision selector; a new CockpitRoute
component renders the flight plan card inside the briefing card.
The odometer readouts become CockpitRollingValue components. Their
throttle and digit roll move unchanged from setCockpitRollingValue into
cockpitRollingValue.js: every accepted reading restamps the throttle, a
change inside it is dropped, and each accepted change draws fresh glyphs
so the roll animation restarts. A rim tick with no reading still hides
where it stood, and a hidden arrow keeps its last angle and label.
In a dev build the HUD's Vue patch costs about 0.5 ms per update (1.4 ms
at 4x CPU throttle), against 0.4 ms (2.1 ms) for the DOM writes it
replaces, at about 10 updates a second.
The catalog now passes the gnss-integrity source's windowMs to the layer.
A missing, non-finite or non-positive value keeps the 30-minute default.
The row text now names the window the layer uses instead of a fixed 30 min.
The Cockpit controller no longer writes the Contact card's scope,
navigation state, cohort counts, nearest contact, bearing, uncertainty,
update time or weather toggle: it keeps them as plain state, starting
from cockpitContextState.js, and publishes a copy to the gev-cockpit
store, and a new CockpitContext component renders it. A lost contact
still holds its last readout. The weather toggle flips from that state,
not the button's attribute. The card's visibility, collapse and layout
stay with the controller. The controller's publishBrief option becomes
a publish object, one entry per Cockpit view.
The Cockpit controller no longer writes the briefing card's pages, cycle
toggle, regional news and local conditions: it keeps them as plain state,
starting from cockpitBriefPages.js, and publishes a copy to a new
gev-cockpit store. A new CockpitBriefing component, lifted verbatim from
the Cockpit view, renders it. News rows are keyed by story, so a focused
link never turns into another story on refresh. The controls keep the
controller's listeners; the route card and the signal list keep their
owners' bindings for now. The boot-time kicker translation is gone, as
the card translates its own text.
The phone shell no longer builds its tab bar and backdrop or writes their
aria-expanded, aria-hidden and hidden state: it publishes its tabs,
whether it is on and the open sheet to the gev-display store, and a new
PhoneTabBar component in the dock teleports them to the end of the page
from the first publish until the shell is destroyed, so they still follow
the scene's credits. The shell takes tab and backdrop clicks from the
document and returns focus to a tab once the tab bar shows it closed; a
reopen or destroy cancels a pending return.
LocationControls (./ui/location) no longer builds the city and landmark
pills or writes the mini status and the search error: it presents the tray
and hands it to its publish option, so the entry stays Vue-free. The shell
publishes it to a new gev-location store and connects the controls, and
the dock renders it and calls chooseCity() and choosePoi(). The landmark
row still expands a frame after its pills render. The search field, its
toggle and the reset buttons keep their listeners. The location QA harness
reads the rendered elements.
createMapSourceControls() (./ui/maps) no longer builds the chip row or
writes the status badge: it presents both and hands them to its publish
option, so the entry stays Vue-free. The shell publishes them to the
gev-display store and connects the controls, and the dock renders the
chips and calls select() for an available one. renderMapStackChips and
syncMapStackChips are deprecated. The map source QA harnesses wait for
the dock to render and no longer read the removed element refs.
VisualSettings no longer toggles the style buttons' active class or writes
the dock's style mini status and the ACTIVE STYLE corner. It publishes the
active style and its label to the gev-display store; CommandDockView and
SceneChromeView render them. The buttons keep their owner's listeners, and
the initial label renders through t().
The launches layer no longer builds the roster or the selected mission
panel as HTML strings in #space-mission-panel-host. It presents both as a
plain view and publishes it through subscribePanel(); MissionRoster and
MissionDetail render it from the gev-context store and call the layer's
panel commands. Replay controls are presented the same way.
The roster keeps keyboard focus on the same mission across refreshes, or
continues beyond the list when the mission departs, and the row under the
pointer takes the preview back on its next movement. Lines a null view
rebuilds are translated through t(). Disposing the shell clears the
Contacts and Space Missions views.
The awareness layer presents its panel as a plain view and publishes it through subscribePanel() instead of writing markup into #military-awareness-panel. AwarenessPanel renders it from the gev-context store, calls the layer's navigation, and keeps focus on the same control by identity across live repaints, continuing at the note when that control leaves or is disabled.
ContextControls presents the selected mode, the transition busy state and the nearby sites search through a publish option, and ContextView renders them from the gev-context store. ./ui/context stays free of Vue. The Radio QA reads the search button's aria-busy, the state it actually carries.
LocalSdrControls publishes the card to the radio store and SdrCard renders it. The new v-field-value directive never overwrites a focused field, including one a render disables.
RadioPanel, lifted from ContextView, renders the layer state, Enable, the tag filter, station card, transport, volume and playback line from the gev-radio store that RadioControls publishes to. The tuner, the local SDR card and the Cockpit radio controls keep their element bindings. Enable's busy mark and the dragged volume go through the owner so a render never leaves them stale, and the markup's tag option stands until the categories first change. ./ui/radio now imports Vue and Pinia.
The file import publishes its rows and hint to the gev-display store and DisplayPanel renders them, keyed by file, so a toggled row keeps its controls. Keyboard focus is given back once the list has rendered: a removed row's next row, else the previous one, else the Import button; a second change before the render keeps the first target, and focus moved elsewhere meanwhile stays there.
Restore shouldHideCollapsedRightPanels, syncScopeControls and the fadeSliders field, skipping sliders already bound by name. Note the API changes in the changelog.
A desktop side panel hides once collapsed, so Escape hands focus to its visible rail item instead of the hidden disclosure. Rail selection reads the panel's collapsed class rather than the store, which follows it a microtask late.
Re-apply the published scene to the select after every option render, as the imperative renderer did. The scene and visual-input QA scripts click a held shot label after disposal and wait a frame for the bloom row.
The picker's options are data: CctvControls computes the cameras in view
and the closing note for those left out, keeps a focused picker's list and
prepends a camera activated under it, and publishes the options, disabled
state and selection. CctvPicker renders them keyed by camera id and
applies the selection once the options are in place; it updates only when
the controls publish, as the old picker did on layer notifications. A
picker about to be disabled no longer counts as focused.
This completes the CCTV panel: everything it shows is rendered from the
gev-cctv store.
The frame lifecycle keeps its request state in an object instead of the
image's dataset and publishes it; the panel binds the image's src, class
and data attributes and the wrapper's loading and has-frame classes from
it, so preload tokens, kept pixels on a failed refresh and clearing on a
camera change are unchanged. The source badge is the pure
presentCctvSourceBadge(), the live switch and the typed summary are
published too. The video surface still draws into the panel's canvas.
presentCctvCalibration() joins the panel view: ADJUST, SAVE CAL, RESET CAL
and the seven pose chips render from the gev-cctv store. The inline editor
is store state: starting an edit publishes its field, starting value and
finish(), and the CctvCalValue chip renders the number input. The draft is
set once on mount so a refresh while typing keeps it; each edit gets its
own input. Commit and cancel rules, the captured base pose and the camera
guards stay with CctvControls.
formField.js had no other user and is removed.
CctvControls presents the switch, navigation and toggle buttons, the CAL
chip and the meta line with a pure presentCctvPanel() and publishes them
to the gev-cctv store; CctvPanel renders them. The controls keep their
listeners on the same elements, and still write the picker, frame, source
badge, calibration readout and summary, which carry no bindings.
A page that imports ui/cctv with its own markup no longer has these
elements painted for it, as with Street Level.
The panel's markup moves, verbatim, into StreetLevelPanel, which renders
the presentation StreetLevelControls publishes to the gev-street-level
store, with the EXPAND state. The controls keep their listeners, full
screen and popover handling, focus moves, resizing and transitions. The
SINCE readout follows the thumb while dragging and shows the recorded
window once the thumb is there; the thumb never moves under the user's
hand. Text the panel now renders itself drops its data-i18n markers.
Panels lifted out of a view live in src/ui/views/panels/, kept out of
Prettier like the views.
A failed assert.equal or deepEqual prints both operands, and a happy-dom
node reaches the whole page and window: printing one exhausted memory and
took the machine down. assertSameNode() compares identity and fails with
a short description of each node; every node comparison in the happy-dom
tests uses it.
createRecentImageryPanel() computes the readout from the layer snapshot
and renders RecentImageryReadout and its day cards synchronously; its
delegated listeners stay on the stable root and the mode buttons keep their
segmented control. Rail card actions take optional pressed and reserved
states, replacing the owner's patches after render. Thumbnails, focus,
scroll preservation, the DETAILS reveal and export behave as before, and
nothing paints after teardown.
createWeatherPanel() computes the readout it shows (forecast and event
cards, the observed group's scope, the history timeline state and observed
cards) and renders WeatherReadout synchronously into the panel body, so a
newly opened card is still revealed after measuring. The hand-built hosts
and the separate card and timeline builders are gone; markup, open-card
choice, first appearance and teardown are unchanged.
The observed-history transport is RailTimeline in src/ui/rail/, rendered
by createRailTimeline() with the same update and destroy API. Drag preview,
the coalesced 150 ms commit, release, cancel and blur behave as before; the
slider keeps the ticks it is dragged over, an update from inside a preview
cannot move the handle back, and readouts follow the handle within the same
task. utcClock moves to its own module and is still exported here.
The rail cards, card blocks, settings, chips and row lists are Vue
components in src/ui/rail/. createRailCards() and createRailCardBlocks()
keep their update and destroy API and patch the components synchronously
with render(), so Weather and Recent imagery are unchanged. Controls of a
removed block dispatch nothing, a released range keeps its value until the
owner's next update, and range values are written only when they differ.
Weather's remount state is keyed by its panel. Tests of rendered parts run
on happy-dom.
SceneShotList and SceneOptions render the project from a gev-scenes
store instead of scenePresentation building rows with createElement.
SceneControls stays the owner: it publishes the director's state and
attaches the row actions, and a late destroy cannot detach a newer
owner. Rows keep their identity across selection; a rename keeps its
typed text across republishing and ends with its row. The select still
changes its choice only when its options are rendered.
Vue keeps whitespace in every component, so Prettier formats single-file
components with strict whitespace sensitivity, and the test loader parses
templates with the same options as the browser build. The i18n check
reads view text without its interpolations, whose t() calls are scanned
with the sources.
The scene chrome, cockpit, Display controls, command dock, layer panels,
Context rail, welcome card and Provider Settings move verbatim into
src/ui/views and mount into their hosts before the page is translated and
wired. Each view renders the same elements, attributes and text as its
template; Vue keeps whitespace between elements as one space, and no view
markup sits under a white-space: pre rule.
Template asset URLs stay literal, view files are left out of Prettier so
the markup is not reflowed, and the Node component loader moves to build/
and is exported as build/register-vue for host test runners. Hosts that do
not initialize Core's locale pass localePicker: false instead of editing
the markup.
Templates listed in build/applicationViews.js expand to mount hosts, and
mountApplicationViews() renders them before translation and wiring. Markup
tests read the page through a server-side render helper, the i18n scanner
reads view templates, and Vue preserves whitespace so views keep the exact
DOM. No template is converted yet.
Each side stack hosts a Vue rail listing its panels; selecting one opens it
through PanelChrome. The shell owns one Vue runtime and Pinia instance, and a
panel store mirrors each side's panels from the DOM. The rail hides with its
stack, in Cockpit, clean view and recording, and on phones. Panels without a
catalog entry get a generic item, and Data Layers leaves the rail while the
f shortcut hides it.
A panel a feature opens (Street Level's viewer, a selected radio station)
now becomes its side's open panel instead of being collapsed again by the
next layout pass. Cockpit keeps showing its collapsed Data Layers strip,
which Cyber's Cockpit layout aligns to, and Cyber's focus layout no longer
re-shows collapsed left panels. Both sidebars share one accordion helper.
The Cyber accordion now applies to both sidebars and every HUD layout:
opening a side panel collapses its peers, the layout normalizes restored
state to one owner, and collapsed panels are hidden on desktop. Phones keep
showing several panels per sheet.
A building or grounds outline from tiles now needs the tile holding the
anchor: when that tile fails and only neighbours load, no neighbouring
building or area stands in. Nominatim name tags holding several
";"-separated names match each name. Document recovery from a stuck
public-Nominatim dispatch marker, correct the phone credit comment,
drive the slow-device queue test with a mocked performance clock, and
credit GitHub handles in the changelog.
On phones the credit line kept white-space: nowrap, so map plus outline credits ran past the right edge and cut off the Data attribution link. Cesium's credit block is absolutely positioned inside #cesium-credits, so the box needs a real width for the credits to wrap: give it the screen width divided by its 0.8 scale, overriding the dock's width: auto !important the way the phone offset already does. Desktop keeps its one-line rule.
Recheck the caller's signal right after the capability probe on every path, so an ask cleared while the probe is pending starts no outline or last-resort search request, whether the probe says configured, unknown or not configured.
Add GET /api/geocode/status ({configured}), like /api/overpass/status. The outline client and the last-resort place search ask it once per page and send no lookup a server without a Nominatim endpoint would refuse, so a default install no longer logs a 503 on its first named landmark or grounds ask. An older server or a failed probe leaves the capability unknown and lookups proceed as before.
- Skip the model when the view has layers but no place, street or nearby
label: with layers alone it invented a place.
- The English-echo check treats only Latin letters as part of a word, so
ids flush against kana or Han and hyphenated names are caught; it skips
nominal, state words a locale keeps in English and names inside place
labels.
- Outside English, per-layer state ids are left out of the request.
- The ja/zh cap steps back to the start of a Latin word, not the last space.
- Fold the CJK cap cases into hudSummaryResponse.test.mjs.
- join street pieces at up to 3 m again (inclusive), as before the shared stitcher
- share one default outline tile source across outline rung instances
- cover Japanese and Thai street names that differ only by a mark
- restore main's text for formatting-only CURRENT-STATE hunks
The server built center from Number(row.lat/lon), which reads a missing value as 0,0, and the client never forwarded it, so the rung's second ordering point was always undefined. Order parts by the request point only.
The rail titles took the dock's rem type inside fixed 176px launchers, so a
larger browser font cut collapsed titles such as Recent Imagery short. Cap
the rail type at its 16px-root size.
The compact type for translated titles now skips Chinese and Japanese,
which fit at full size and keep their 9px floor, and reaches only the rail's
own launchers, so Radio inside Context keeps its type.
Tests now check the launcher divider, gap, toggle margin and flex-shrink
rules by their declarations, not only their selectors.
The shape list rebuilt every row on each board change. A name being typed was
lost when a voice mark landed or expired, and a click right after typing only
committed the name, because the button under the pointer was replaced. Rows
are now updated in place, a re-entrant render is deferred, picking a colour or
Esc returns focus to the swatch, and keys on the list no longer reach Draw.
Fly-to on a line across the antimeridian flew to longitude 0. A drawn mark
and a voice mark are no longer deduplicated into one another, so a voice TTL
cannot expire a drawn pin, and a new pin on a hidden one is drawn.
The engine tests now check effects (renderer calls, alpha, notifications on
clear and expiry, the framed centre), not only return values.
- remove the unused Nominatim reserve(); spacing tests now measure the
actual transport start across processes
- start each queued building's 2 s ground-sampling budget when it
reaches the head of the queue, and request a frame on promotion
- accept Nominatim and grounds outlines that cross the antimeridian
- let an unnamed school, university, hospital, cemetery, stadium or zoo
area answer a grounds ask whose head noun names that kind, using only
the part that holds the point; compare grounds names symmetrically
- charge shared OpenFreeMap consumers their own slice and keep the
charged budget at the previous 64 MiB
- share one place-name fold, point-in-polygon, line stitching and
nextTask helper instead of duplicates
- exclude Who's On First sources licensed only as "CC BY (equivalent)"
or "CC0 (equivalent)"; rebuild the pack from the pinned archives
- whole-word neighbourhood alias guard; correct the outline size-limit
comment
- restore DATA_SOURCES.md table formatting, keeping only real edits
- test behaviour instead of private engine fields
- Add A22_IMAGE_ORIGIN to the pinned frame directories, so a same-host
redirect from www.autobrennero.it/WebCamImg/ cannot leave that directory,
as for the other directory-pinned packs.
- Give Autostrada del Brennero a 3-minute ambient refresh in cctvLod.js,
like the other highway packs, instead of the 5-minute default.
- DATA_SOURCES: the Open Data Hub now tags every A22 record CC0; note that
this is the aggregator's metadata, not Autobrennero's own terms.
- Tests for the redirect pin and the refresh cadence.
Resolve conflicts with the NYC, Toronto, Lithuania, Florida, Taiwan and
Kartverket work on main. The CHANGELOG entry moves to
changelog.d/956-a22-brennero-webcams.md, as main now asks.
- fetchIpv4 builds its Response inside the https response callback, outside
the promise. A 204, 205 or 304, a status above 599 or an invalid reason
phrase made the Response constructor throw there, an uncaught exception
that stops the dev server. Null-body statuses now get a null body and
anything else that cannot become a Response rejects, as the Radio Browser
transport does.
- A non-OK FIRMS or mapkey-status response cancels its body so the IPv4
socket closes at once instead of at the deadline.
- Tests: the crash cases (fail on the old code), a source check that every
upstream request goes through fetchFirms and that fetchSource's body read
shares its deadline (fails if either call reverts to fetch), a real-fetch
contract test for the address-selection error shape, and an explicit skip
in the IPv4-only socket test where the host cannot tell the families apart.
- Changelog entry moves to changelog.d/1078-firms-ipv4-fallback.md per
CONTRIBUTING; CURRENT-STATE says which timeouts are retried and that the
retry does not follow redirects.
Resolve fetchSource onto main's shared deadline: one AbortSignal covers the
fetchFirms request and the capped body read. The CHANGELOG entry moves to a
changelog.d fragment in the next commit.
reconcile voice outlines with main f13c60c, retaining German/Japanese,
Operator HUD retirement and Context/Radio cleanup, named-building
containment, courtyard-hole, non-Latin identity, attribution, cancellation
and antimeridian repairs
keep outline callouts above the open voice preview when an above-panel
slot fits without colliding with another callout,
and clip the compact dock location label on narrow screens
Collapsed Recent Imagery (English) and Data Layers (Portuguese) pushed
their toggles out of the card. Collapsed rail launchers now drop the
divider and pack closer, translated collapsed titles use compact type,
and any title that still does not fit ends in an ellipsis.
The hover now matches the dock trays' hover rather than their pinned
look, only on pointers that can hover, and recolours only the hovered
panel's own header, so Radio and body labels keep their colours.
The cyber theme keeps main's gutters and Display toggle size, the dock
and rails share --dock-label-* tokens, and the dimmed open Context title
rule is gone.
Restore the two fixes the previous commit moved out:
- A tuner release onto a station the directory dropped mid-drag stops the
stream the drag paused, publishing one state with the gesture's origin,
so the button reads PLAY and a voice handoff is cancelled.
- The transport buttons offer Pause while a stream connects, matching
what a click does.
Their tests now use the shared Radio layer harness.
- Selections take the playback fallback as a `playFallback` option and
set it after releasing a stale stream, instead of callers arming state
that the release then had to save and restore.
- Move the tuner `station-unavailable` release and the Pause label while
a stream connects out of this branch: both were wrong on main already
and go in a separate change.
- Share the Radio layer test setup in src/testSupport/radioLayerHarness.mjs
and fold overlapping Next/Previous tests (13 to 7).
- One-line comments and a two-line changelog entry.
Second follow-up review fixes:
- Japanese and Chinese summaries are capped at 20 characters only, without
the five-token cut first, and the cap steps back to a space instead of
splitting a Latin word.
- The HUD request sends localized layer names in feedProvenance too and
leaves the English narration note out outside English. A non-English reply
that still echoes a feed-state id or a translated English layer name falls
back to the deterministic line.
- A camera move during typing shows the deterministic line, not a fragment.
- A view with no place labels and no enabled layer does not ask the model.
- Behaviour tests on IntelHUD replace the regex checks of hud.js; docs and
the instruction wording updated.
Follow-up review fixes:
- The HUD line is STATE | text | LAYERS for the AI summary and the
fallback line, so the narrow minimal and cyber layouts show both the
warning and the summary. get_hud_caption builds its caption the same
way. The unused "{state} {layers}" key is pruned from the catalogs.
- The 20-character cap applies only when the request locale is Japanese
or Chinese; other languages keep five words around a Han place name.
- enabledLayers names are localized too, and the instructions name layers
only from enabledLayerLabels, never from feedProvenance or its note.
- The keyless test restores GEV_RATELIMIT_OPENAI_PER_MIN again; comments
and docs updated.
main added de, fr, it, ja and pt with the old "after you press play"
note; give them the updated wording. main also removed the compact
Context Radio player, so the Next/Previous tests stop checking its
button.
Review follow-ups for HAL-722. A Next or Previous that only selects a
station now leaves nothing of the previous stream behind:
- Selecting another station without playing stops a paused or failed
stream, so the panel, Stop, voice summaries and the audio element agree
on "stopped". A fallback the caller armed for the new selection is kept.
- A silent UI step arms no broadcaster fallback, and a user Play of the
selected station drops one left by a cancelled voice step. Voice steps
pass `fallback: true` because voice starts the prepared playback later.
- A user step to another station emits a `select` playback control, and
the realtime voice handoff treats it like Pause and Stop.
- A tuner release onto a station the directory no longer has stops the
stream the drag paused, publishing one state with the gesture's origin,
so the button reads PLAY and a voice handoff is cancelled.
- The transport buttons offer Pause while a stream connects, matching
what a click does.
- The privacy note names every action that connects to a broadcaster
(play, finishing tuning, choosing a station on the globe) in all
locales.
The Next/Previous layer tests live in src/data/radioNextState.test.mjs.
Review fixes for the HUD summary language change:
- The translated feed-state tag now leads the one-line summary and the
fallback line, so the readout's ellipsis cannot hide it.
- get_hud_caption checks the reply's feedState, returns it and appends the
state tag; a reply naming another state keeps only the place.
- The model gets the layer names the user sees (localizeLayerName).
- Japanese and Chinese summaries are capped at 20 characters; pt, zh-CN and
zh-Hant get explicit language names.
- A partial view is narrated as incomplete coverage, not as not live.
- provenanceTag and hudSummaryMatchesProvenance move to layerSnapshot.js so
the tools package can use them without browser globals.
- docs/CURRENT-STATE.md, changelog and comments updated; redundant tests
trimmed.
The HUD sends its locale with the summary context; the proxy names that
language in the instructions and the input, English when missing or
unsupported. The model returns {summary, feedState}; the HUD shows the
summary only when feedState matches its own state for a view that is not
nominal, and appends its own translated state tag.
partial joins the shared feed-state table between fallback and nominal,
so one partial layer no longer reads off and partial plus nominal no
longer reads nominal.
The CCTV layer kept every loaded camera live: each view change touched every
icon, every notification rebuilt every camera's state, and the picker listed
the whole catalog. With the default 4,000 cameras, a continental drag ran at
152-179 fps with 18-24 frames over 30 ms (worst 112 ms). It now runs at
235 fps with none. The layer now:
- shows icons only on or near the screen, and when zoomed out keeps one camera
per world-anchored cell (healthy feeds first), so panning keeps the same
cameras;
- grounds and draws coverage only for the cameras in view, the active camera
and its coverage neighbours, retrying a camera at most twice per view regime;
- reruns the view pass while the camera moves, paced by its own cost, and
projects with one matrix per pass instead of a Cesium call per camera;
- keeps the billboard buffer-usage hint fixed, so showing or hiding a few
icons no longer rewrites the vertex data of every billboard;
- lists the cameras in view (active camera first) in the picker, closing with
the count of cameras outside the view, and builds the full camera list in
the UI state only when it is read.
Adds the Authoring labels and tooltips to the de, fr, it, ja, pt and uk
catalogs that arrived from main, and prunes the retired SCENES-era
entries there. The file row's tighter padding and letter spacing keep
one long translated word per line in the narrow Cyber panel
(IMPORTIEREN, ESECUZIONE, ІМПОРТУВАТИ, EJECUCIÓN).
- PLAY and NEXT continue into the following scenes, so they stay
enabled while any scene in the project has a shot, not only the
selected one.
- Renaming a shot drops the number and ellipsis so the editor is no
longer hidden; qa-scene-controls checks a long rename stays visible.
- PLAY, NEXT, EDIT DETAILS and SHARE SCENE tooltips describe what the
buttons do, translated in every shipped language.
- The primary actions and the file row keep a hover state.
- Docs: the EXPORT rename in DIRECTOR-SHARING and the playback button
states in CURRENT-STATE are accurate.
- Test the voice "authoring" alias through set_panel_open.
main gained de, fr, it, ja, pt and uk catalogs that still carried the 12
strings this change retires, so their catalog-matches-code tests failed
once the branch met main.
The entry now lives in changelog.d/1471-context-panel-cleanup.md with
credit, as CONTRIBUTING asks, and CHANGELOG.md is back to main.
CURRENT-STATE no longer lists a compact Radio power control and scopes
the readability description to what Cyber keeps. The responsive glow
comment no longer cites the removed Radio popover, and qa-radio's
pick-buffer comment is back above settleRadioFrames.
Four test fixtures stop stubbing the removed launcher sync, and
PanelChrome drops the _syncRailPanelCollapseButton pass-through, wiring
the layout controller straight to _syncPanelCollapseButton.
Japanese in the language picker: the full catalog, counters, unit words joined
to their numbers (5分前, 1時間5分), lower-case HUD units as in Chinese, and the
CJK label rules plus strict line breaking for Japanese. Based on translations
by @abiyeenzo (#913).
Co-authored-by: Enzo Abyie <[email protected]>
German in the language picker: the full catalog, neutral infinitive wording,
one/other plurals and the de style guide. Directions keep German nouns
capitalized, and layer names break at word hyphens but not inside codes.
Based on translations by @abiyeenzo (#908).
Co-authored-by: Enzo Abyie <[email protected]>
French in the language picker: the full catalog, French typography, one/many/
other plurals and the fr style guide. Voice plan steps join through a catalog
string so French spaces its colon, and with nine languages the picker shows
its search field. Based on the translation by @abiyeenzo (#907), checked
against @meirm's French catalog (#312).
Co-authored-by: Enzo Abyie <[email protected]>
Co-authored-by: Meir Michanie <[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.
The merge from main kept this branch's side of four conflicts, which dropped the Snow PR's changes: tests expected Snow again, the share-link table lost its retired mark, and the voice pins lost the Snow restore. Keep both retirements, and re-derive the full-payload digest for both enum removals.
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.
Operator was the only layout that displayed the top banner bar. Its markup, styles and keep-out selectors are removed, and the Cyber style test now checks that none remain.
The it, pt and uk catalogs merged from main still carried the two Snow
button strings, which the catalog tests report as stale. They are
pruned with npm run i18n -- --prune.
A Scene Director shot saved with Snow now normalizes to Normal when the
project loads, so the shot list shows NORMAL rather than an untranslated
SNOW for a shot that plays on Normal. The README's style keys read 1-6.
Brazilian Portuguese in the language picker: 2732 strings, Portuguese plural
forms and the pt style guide. The CCTV summary line names its calibration,
source and view in the page language, markup can give text a context with
data-i18n-context, and the cyclone source errors are translated. Based on
translations by @abiyeenzo (#909).
Co-authored-by: Enzo Abyie <[email protected]>
Italian in the language picker: 2730 strings, Italian plural forms, labels
shortened to fit the panels, and the it style guide. Based on the translation
by @abiyeenzo (#912).
Co-authored-by: Enzo Abyie <[email protected]>
The Next and Previous buttons cycled stations with the layer's default
autoplay, so a first press started buffering a broadcaster stream even
though the panel says audio connects only after Play. Stepping now only
selects until a stream is loading, buffering or playing; once one is,
playback follows the new station so the old broadcaster does not keep
playing under the new label.
The compact Next fallback scenario in qa-radio.mjs started from a stopped
stream and relied on the old behaviour, so it now starts from a playing
station.
Fixes HAL-722
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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]>
Ukrainian in the language picker: 2730 strings, four plural forms, Cyrillic
HUD units and the uk style guide. Based on the Ukrainian translation by
@meirm (#312).
Co-authored-by: Meir Michanie <[email protected]>
Co-authored-by: Meir Michanie <[email protected]>
CI failed at format:check: the format scope still listed the deleted
panelChrome.radioOwnership test, and panelRails.test.mjs needed Prettier
after its glow list shrank.
The new Context title colour outranked Cyber's red panel titles, and the
collapsed Context launcher came out brighter than its siblings; the rule
now applies only to the expanded panel outside Cyber. Cyber's layout-focus
standby card had relied on the old dashed border and centring, so it
drew a 3px frame with its text off the radar art; it now names its own
1px frame, padding and centring. Cyber also drops the shared segmented
track, which drew a third frame around its already framed mode buttons.
Tests now pin the Context header to its title and collapse control, the
segmented control, 10px type, the missing halo and the scoped title
colour, plus the Cyber standby and track overrides. Each fails on the
previous CSS or markup.
qa-radio enables Radio and starts the first Play from the visible
controls, expanding Context and Radio the way a person must, then puts
the panels back. The Escape test and its mutation describe what they
still protect. CURRENT-STATE no longer describes the removed Radio icon
or compact player.
Each shape drawn by hand gets a row under Draw, as Import's files do: its
colour swatch, its name, and show/hide, fly-to and remove. A shape is named
in its row, and its swatch opens the five colours there, so labelling and
colouring happen after a shape is drawn and can be seen, instead of in a
field filled in beforehand. Area, Line and Pin become icon buttons, named
for screen readers and tooltips, and the colour for the next shape is a row
of swatches instead of a dropdown.
The annotation engine gains per-mark remove, setHidden, restyle and frame,
and onChange so the list follows the board. A hidden mark stays on the board
at alpha 0, a renamed line keeps its length, and fly-to frames a line by its
whole path. Voice marks are not listed.
The Draw, form-field and segmented-control gates drive the new controls;
swatches are checked as chips, not segments.
The step headings make the panel taller, so the left stack squeezed the
shot list down to one row at about 1000px of window height. Its minimum
now holds two rows; the panel scrolls when space is short.
Voice show_data_layers_menu opens the panel that holds the row, so asking
for Directions opens UTILITIES and reports that panel, not a hidden row
under Data Layers. In Cockpit the panel stays with Directions alone, as
Data Layers does; only Draw and Import step aside there.
Draw's options sit flat under its name instead of keeping the card that
hung under a DISPLAY button, and its Shape label no longer takes DISPLAY's
label column in other languages. Cyber marks Draw on with its red edge.
LayerPanel routing, the voice panel choice, and the panel's order, mode
and phone-sheet CSS gain tests that fail on the earlier code. The form-field
and segmented-control gates open Utilities for Draw's controls, measuring
Scenes and Utilities in turn since the left rail fits one tall panel.
README, CONTRIBUTING, the PR template, CURRENT-STATE and comments now say
UTILITIES for Draw and Import and list the Audio group, and two files
outside the format scope lose an unrelated reflow.
- Keep the documented 72px / flex-grow 1 action sizing and the 360px
panel width; the larger type applies to the Authoring panel only, so
CCTV and Street Level buttons are unchanged.
- Scope the primary and file-row styles to the panel so Cyber's shared
button surface no longer overrides them.
- Shorten the scene PLAY in Spanish and Russian so it does not break
mid-word, and fix two translation inconsistencies.
- Map the spoken name "authoring" to the panel.
- Clear the has-shots marker when the controls are destroyed.
- Test the idle button states through published changes, the PLAY
translation context, and where the sharing row is placed.
createView threw on style 'snow', so an embed host or a
show_in_gods_eye_view call holding a view from before Snow was retired
was refused outright, camera and layers included. viewFromParams read
style=snow as the recipient's own style, while the app opened the same
link on Normal.
Both now read a retired style as Normal; truly unknown names still
throw. The retired list moves to a browser-free module shared by the
view API, the share codec and visual settings.
The panel reads AUTHORING and groups its controls under SCENE, SHOTS,
PREVIEW and SAVE & SHARE headings, with the EDIT DETAILS / SHARE SCENE
row moved beside EXPORT / IMPORT. Text is larger with more contrast,
shots are numbered, every action has a tooltip and the status line is a
live region. Outside playback, buttons that would do nothing are
disabled. New labels are translated in every shipped language; PLAY
uses its own catalog context apart from the radio's PLAY.
qa-radio cycles Minimal, off, on, then Tactical, so the HUD toggle must
restore a non-default layout, and expects that sequence instead of the
removed Operator step.
The share-link test pins the exact hudVariant codes, so dropping or
moving operator's reserved code fails it. A live IntelHUD test checks
that a retired Operator layout falls back to Tactical, and the Cyber
test stub themes from the normalized value.
The share-link table marks operator as retired, and the zh-CN glossary
drops the removed string.
The Context header carried a Radio icon that opened a second, compact
Radio player on top of the panel, duplicating the Radio section that
already lives inside Context. Remove the icon, the compact player and
everything that kept the two in step. Radio stays in Context as its own
section with one Enable control; Cockpit's compact Radio is unchanged.
Context itself was hard to read: 7-8 px labels at low contrast, a dashed
placeholder box, and a glow halo. The two modes are now one segmented
control, labels and guidance are 10 px at higher contrast, the standby
hint is plain text, section titles use --text-secondary, and the panel
draws no glow.
Unused translations are pruned, tests and QA harnesses that pinned the
compact player are updated or retired, and CURRENT-STATE and the
changelog describe the new header.
Snow is no longer offered anywhere a style can be chosen: the Visual
Presets tray, keyboard shortcut 7, the Cockpit vision carousel, and the
set_visual_style voice action. The shader and its labels go with it;
weather snowfall and the SNOW weather condition are unaffected.
Share links keep working. Snow's enum code (6) stays reserved, so a
compact or long-form link that names it opens on Normal and drops the
Snow shader parameters without reporting them as unknown. A Scene
Director shot saved with Snow plays on Normal too. Snow's frozen share
defaults matched Normal's apart from those parameters, so no other
setting in an old link changes meaning.
The legacy voice-schema pins restore Snow before hashing, so the
unchanged pins still prove nothing else moved; the complete Realtime
payload digest is re-derived.
Mirror the dock trays and their pin, the compact Radio window and the
phone top-bar actions in right-to-left; keep the Cyber cockpit's Data
Layers stack on the left edge. Isolate the Space Missions counter,
landing coordinates and CCTV summary parts, format their numbers with
the locale's numerals, decide pack shot titles by the marker binding,
and place Bhote Koshi media cards between the panels on either side.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
UTILITIES becomes its own panel in the left column, after Data Layers and
CCTV and above Scenes. The layer panel renders the Directions row into the
panel's list instead of Data Layers, and Draw and Import move there from
DISPLAY with their ids unchanged. Radio, the other row under the old
Utilities heading, stays in Data Layers under "Audio".
The panel collapses and remembers its state like the other left panels; share
links do not carry it. It hides in clean view, recording and Cockpit, takes
the Cyber left-rail treatment, and sits collapsed in the phone's Layers sheet.
Voice focuses the Directions row in its new list. The Directions, Draw,
Import and form-field gates open UTILITIES to reach their controls.
Localize a bootstrap shot title without a source pack only when the
Nepal pack marker binds that title to the shot's id, so an operator
shot renamed to a bootstrap title in the built-in scene stays as typed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Localize a legacy Nepal base shot only when the applied pack marker binds
that title to this exact shot id, so a user shot renamed to a built-in
title stays as typed in every locale.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A pack marker with shotBindings now vouches only for the shot bound to a
legacy bootstrap title, so a user shot typed with the same title keeps its
text instead of showing the localized built-in name.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Localize a pack shot title only for the shot the pack marker binds to that
title, so an operator's shot named like a pack shot shows as typed. Markers
saved without shotBindings keep the title-only check.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Japanese has no letter case, so the HUD summary keeps units as the
catalog writes them (13 km), matching the cockpit and the ja HUD CSS.
The ja '{value} KM', '{value} KM/H' and '{value} KM/S' strings are now
lower-case like zh-CN.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Display > HUD > Layout and the set_hud voice action now offer Tactical,
Minimal and Cyber. The Operator option, its CSS and its QA coverage are
removed, and the unused "Operator" translations are pruned.
Share-link codes are append-only, so operator keeps its hudVariant code.
A link, scene or saved state that names it restores the default Tactical
layout. The voice schema pins keep their legacy digests by restoring the
retired value before comparing; the full-payload digest is re-derived.
Every rail panel title uses the command dock's label type and colour,
collapsed or open, instead of three sizes at 30% opacity. Panels answer
hover as the dock trays do, with a blue edge, a soft glow and an accent
title. In the default theme each panel's title and collapse button sit
19px from its edge; Context's toggle no longer shrinks beside the radio
button. The cyber theme keeps its own colours and insets.
Conflicts in es, ru, zh-CN and zh-Hant catalogs: both sides appended keys
at the end (cyclone errors from this branch, routing refusals from main);
kept both.
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]>
The set_layer_visibility layerId hint listed aliases such as "dams" for
local-dams and "weather radar" for weather-radar, which the id in the
enum already says. Leaving those out, and a layer with no other alias,
cuts the Realtime prompt from 52,000 to 51,695 characters. Aliases the id
does not spell (aqhi, hurricanes, clouds) stay, and the runner still
accepts every alias. Refs #1363.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The test passed URL.pathname to sharp, which is /D:/... on Windows, so the
faces were not found and npm test failed on every Windows clone.
fileURLToPath gives a path that works on every platform.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The HUD builds place context for its AI summary whenever the camera
settles, and each build paid for up to four reverse geocodes plus a Nearby
Places search, even when no OpenAI key was configured to use them.
- Ask GET /api/openai/hud-summary/status once; with no summary configured,
or after an unconfigured reply, make no place lookups for the page.
- Reverse-geocode at most two points: the view center and one viewport
point.
- Reuse a lookup until the view center moves a quarter of the visible
width, the camera's distance to it doubles or halves, or the camera
crosses a lookup height. The reused points are asked again, so only a
timeout, failure or empty answer spends.
- Repaint the last AI reply when a small pan leaves the context unchanged.
Cached-only hosts and voice context lookups are unchanged.
Apply the accepted findings from the fourth three-model French review:
« Fermez d'abord cet outil » in the pointer-busy notices, the colon form
« Outil actif : {name} » for the directions notice, « FOV CONSTRUCTEUR »,
« RAPPORT » for the USGS trigger chip, « CONTEXTE GLOBAL DÉSACTIVÉ », and
wording fixes for the radio, recent imagery, bundle preview and draw label
strings. The guide gains glossary rows for the datasheet FOV and for
rapport versus relevé.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Node.js 24.14.0 carries CVE-2026-111497 ("Node2Shell"), an out-of-bounds
write in the bundled Brotli decoder that can lead to remote code
execution, plus several high-severity DNS, TLS and URL-parsing fixes.
24.22.1 is the first patched 24.x release.
- Dockerfile: node:24.22.1-slim, digest re-pinned to the multi-arch index
- CI: 24.22.1 in the verify matrix and the other two jobs; the
allocation-gate requirement now keys on 24.22.1
- Docs: contributor setup and the dependency baseline name 24.22.1
The engines floor and the setup doctor are unchanged.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
At 900px and narrower the Location and Visual Presets wings shrink to
--dock-side-compact (4.25rem), open or closed, but their text labels kept
their full width. Between 721px and 900px, where the desktop dock is still
shown, LOCATION and VISUAL PRESETS spilled over the idle voice control, and
the LOCATION label took taps on the left half of the mic button. The label
clip that existed was scoped to 620px, which the phone shell (720px) always
covers, so it never applied to a visible dock.
In that band the labels are now font-size 0 and lose their chevrons, so
each wing shows only its icon. The label text is unchanged, so the toggles'
"Expand/Collapse ..." aria-label and title, which panelChrome builds from
it, keep the full name. The Cyber HUD layout sets a wider
--dock-side-compact that fits its labels, so it keeps them. The
unreachable 620px clip is removed.
Found in GEV launch-readiness QA (HAL-728).
Catalog: refresh/updates wording, received SDR aircraft, playing states,
move-start button, impersonal layer note, live-data descriptions,
arrival steps, retry timing, tracking spelling, military chip, dollar
amounts, label: value statuses, upstream source, globe map, stage
recovery, comparison signs as words, ellipses and earlier round-2 fixes.
Code: FIRMS cell cards use the plural entry for their count (FIRES
pruned), translated locator pin titles are isolated in RTL, legend
stops keep their sign, voice timeout minutes agree with the number,
CCTV badges are translated, first-run and referenced-item alignment.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Apply the accepted findings from the third three-model French review:
tool-name agreement (« Outil actif : {name} »), elision before ADS-B,
« GORGE AVAL », « CLASSIQUES », « EN SURVOL », one name for the VIIRS
daily overview, U+202F thousands separator, and wording fixes for the
scene editor, directions, radio tuner and status strings.
The CCTV HUD summary now shows its CAL badge, source kind and view band
through existing catalog entries, so French reads « CAL ESTIM. INITIALE ·
SRC AMONT · CONTEXTE RUE »; English output and unknown codes are
unchanged. presentation.js matches the pt branch's round-3 version byte
for byte, so the two branches merge cleanly.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Catalog fixes from the third review round: booster flight count
({count}回目の飛行), 晴れ for PARTLY CLOUDY, 配信元 for upstream,
部分的 for the PARTIAL feed state, または between provider names,
POS kept as a HUD code, product names in their own case (YouTube,
Facebook, Radio Browser, ChatGPT OAuth, Googleストリートビュー), Esc,
bracketed button names, en-dash ranges, 土砂ダム湖 for both lake
labels and smaller wording fixes. Guide: range rule and new glossary
rows.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Three-model review (astra, sol, fable), round 3: 17 of 18 findings
applied (some in own wording, one in part); "Missões espac." rejected.
- pt: layer cards «{label}: camada ativada/desativada», pointer and
Directions toasts name «A ferramenta "{owner}"», counters after a
colon where the count can be 1, traffic «sem dados de fluxo» and
«Fluxo: {percent}% cob», «limite de requisições atingido», FECHAR,
ANT., PREDEF. VISUAIS, quoted "Nível da rua", infinitive tooltips,
and more (see docs/translations/pt.md).
- CCTV summary line localizes its CAL, SRC and CONTEXT values through
existing catalog keys (all locales); codes stay underneath.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Catalog fixes from the second review round (squawk 7600/7700 names,
transit occupancy, radio category, cyclone advisory wording, Orbital Watch,
validation and hint texts), cockpit distances keep "km" in Japanese as in
Chinese, the CCTV context line uses the translated view band, and the
Japanese layer status line no longer splits "1分以内".
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A request with any point the proxy cannot answer gets 502, and the page
used to put every point of that chunk on its geoid, including the ones the
proxy had real (cached or stale) heights for. During an upstream 429 pause
that drops cached ground under every chunk that also holds a new point.
The 502 body now carries `partial`: the request's real heights in order,
null for points without one, left out when there are none. The terrain
request service attaches it to the error it throws, and the page caches
those heights and uses its geoid only for the rest. `get_terrain_height`
still reports the failure as before.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- The trailing visibility frame uses the layer's injectable runtime timer and
is cancelled by resetState as well as disable and destroy.
- An empty-feed regular pass records the camera pose, so later in-cadence
frames do not keep scheduling trailing frames.
- New renderGovernor.test.mjs: a parked camera schedules nothing, a stopped
camera gets exactly one trailing frame and no self-wake, a forced pass
requests its frame, destroy cancels the timer.
- The CHANGELOG entry moves to changelog.d/871-vessels-idle-render.md; docs
say the hold also covers vessels dimmed under a live target.
stopScene now supersedes seeks through _bumpSceneSeekGeneration; the
bhoteKoshiLocator Stop test calls stopScene on a plain object, so it
needs the method too. Without it four tests throw at the PR head.
- Superseding a seek aborts only the seek's own layer reconcile, so a
loadShot that is later rejected (shot-not-found) no longer cancels an
in-flight LOAD. Stop still aborts any LOAD.
- A refused seek rolls back to each layer's effective intent, so a manual
toggle still in flight is kept.
- A cancelled seek leaves layers to whatever superseded it, as a cancelled
LOAD does; only refusal rolls back.
- Move the _seekLoadedShot JSDoc back onto its method.
- Meaning fixes: the radio link opens the broadcaster's website
(SENDER-WEBSITE), a straight lane is «geradeaus führend», not the middle
one, IDLE is INAKTIV (BEREIT is READY), the Gemini errors name the send
queue, traffic percent is live coverage (Live-Abdeckung), the voice radius
is measured from the Bildmitte, the AIS voice error names the Live-Schiffe
layer, and the Bhote landslide is «Erdrutsch» throughout (not «Abbruch»).
- Number-neutral status lines for feed and layer lists, and a payload count
line that also reads correctly for +1.
- Retry wording follows the glossary: «neue Versuche in größeren Abständen».
- Weather history lines are shorter and fit the collapsed row: «Frame fehlt;
letzter Stand bleibt erhalten», «Kein Frame in den {gap} min bis {time}»
(the gap only looks back from the target time).
- Shorter search placeholder «Ort suchen...», which was clipped.
- Smaller wording: Himmelsring, Lagebild-HUD, SCHLEPPVERBAND, Minimum Fuel
(Treibstoff knapp), Bloom / Leuchten, DOWNRANGE GESCHÄTZT, Places-Kontext,
MELDUNG, Rückflug zur Weltübersicht, REALE BILDDATEN and others.
- Glossary and routing notes updated.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The route proxy also refuses with 'routing busy — too many routes at once'
(429) and 'route proxy error'; both showed in English in every locale.
Mark them in ROUTING_REFUSALS and add es, ru, zh-CN and zh-Hant entries.
errorText is now cleared on a new connection, a mode switch and stop(), so
it never outlives the error it describes. Tests cover the two refusals,
'Routing failed: rate limited' (pattern order) and the stop() reset.
When several candidates fit a "place, region" query, take a settlement or
administrative area over a POI of the same name, so a cafe called "Paris"
in Lyon cannot win "Paris, France" from the nearby pass.
Tests: region queries without a camera bias, whole-word and every-word
region matching, city and county regions, the nearby whole-name answer when
the unbiased request fails, and the settlement preference. The new tests sit
with the other Paris, Texas tests. The station fixture uses its real city.
CURRENT-STATE now describes the order the code applies.
A Retry-After longer than the 10 s retry budget used to sleep the whole
budget out and then give up, so the proxy's 429 pause started 10 s late
and every new-point poll in that window still went upstream and logged.
The retry loop now stops at once in that case.
Tests: the retry loop no longer sleeps for an unreachable Retry-After;
fully cached points, including stale ones, are served during the pause
with no upstream call; a 503 does not pause; a shorter concurrent
Retry-After never cuts the pause short. CURRENT-STATE now says which
requests are still served and which log.
Three-model review (astra, sol, fable), round 2: 29 of 30 findings
applied, one of them in part.
- pt: FIRMS «mais recente há», CCTV «cruzamentos», degraded radio
directory, «{layers}: ativação recente» for any layer list, «Sem
imagem», NIVELADO, AJUSTA-SE, quoted Provider Settings, V/S kept on
the ADS-B card, no-break space in «(24 h)» / «(30 d)», and more
(see docs/translations/pt.md).
- Malformed cyclone snapshot is now a catalog message (all locales).
- Markup supports data-i18n-context; the radio STATION TAG option uses
"radio tag|All", so it no longer flips gender when the list loads.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Three-model review (astra, sol, Fable), round 1:
- Meaning fixes: post-tropical cyclone, partly cloudy, upload-date basis,
ground track (対地針路), spec FOV, voice credentials, Activate → 押す,
bikeshare counts, NHC/CPHC empty state, absence caveat, regional-news
empty state, route steps (件の案内), minimum fuel, microphone access.
- Consistent terms: 範囲 for WINDOW, カバー範囲 for coverage, 封じ込め率
for fire containment, サイクロン for storms, 交通流 for TomTom flow,
シグナル tab, インポート / エクスポート for projects and presets,
射撃場 for the RANGE class, か所 for sites, full-width colons.
- Short forms where the re-shoot showed a wrap: エクスポート (Scenes),
提供元で操作 (embeds), 該当施設 (Contacts).
- cjk.css: the Recent Imagery slot line wraps at its separators in
Japanese, never inside プレビュー.
- Glossary in docs/translations/ja.md updated to match.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Findings from the astra, sol and fable reviews of the Arabic screenshots:
- Wording: forecast valid time is «عند», not an expiry; acres are آكر, not
feddans; live layers are «تتبّع … المباشر» (not "direct flights"); the
straight-ahead lane, turboprop, call sign, reconcile, proximity, retrying,
count agreement (colon form, fixed counts, ي for compact day ages) and
smaller fixes. Guide and glossary updated.
- Right to left: the scene editor's JSON fields are LTR; canvas map cards
draw with LTR geometry and an Arabic face; whiteboard callouts anchor
inside their cards; HDG/FOV keep their values; the cyclone timestamp no
longer splits its date; bare-number counters read 01 / 25, translated
ones say «من».
- Built-in shot titles of the Nepal shot pack's bootstrap shots localize
(the applied pack vouches for them), in every language.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Three-model review (astra, sol, fable): 42 findings accepted, 3 deferred
(unmarked cyclone errors and the Chinese locale descriptors need new keys
in every catalog). The USGS callout now reads as an action (APRI LA FONTE)
because it opens the report. Layer, tool and card-title sentences agree with
any feature name («Il livello {layer} è attivo», «Lo strumento {name}»,
«{label}: attivato»). The store write error is grammatical for both
values. RADO/BILANCIATO agree with DENSO. Also fixes the meaning of
viewpoint, fire incident, trigger record, transit report, FIRMS cell age,
drizzle, widebody and fast-jet classes; Dissolvenza and CH replace the odd
abbreviations; cause/hint dashes become colons.
Fixes the Nepal scene showing shots 01-08 in English in every locale:
displayShotTitle() now also trusts the bootstrap keyframes of the append
recipe that upgrades a built-in scene (src/scenes/recipes.js, with a test).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Meaning fixes: playback connects to the broadcaster when it starts, browser
keys are restricted at the provider, clearing data layers switches them off
(ausschalten, not löschen), cockpit inputs are Quellen, Electronic is
Elektronische Musik, AIS types name vessels (SEGLER, LOTSENBOOT,
SANITÄTSDIENST, SCHNELLSCHIFF), contiguous US excludes Alaska/Hawaii,
wind and weather readings are model values, nearest frames are nearest in
time, cloud imagery and the brightness filter say what they are.
- NHC/CPHC advisories are Lageberichte, not Warnungen; cyclone strings use
Wirbelsturm throughout. Glossary updated.
- Grammar: " oder " keeps its space as a join separator, "{x} konnte nicht
sauber beendet werden", Then: no longer doubles Dann, scene save reasons
continue the sentence in lower case, neutral "Warten auf".
- Spacing between numbers and units in the remaining nine readouts.
- Reroute tooltip: German mode nouns keep their capital (Auto, Fahrrad).
- Built-in Nepal shot titles localize in every language: the legacy scene
upgrade rebuilds its base shots without sourcePackId, so the pack's own
shots in the scene now vouch for them.
- Layout: the layer status column no longer shrinks under a wide chip, and
a mixed-case compound (ALPR-Kameras) may break at its hyphen; only capital
codes (ADS-B, RTL-SDR) stay unbroken. WIRD AKTIVIERT overlapped the name.
The Bhote step meta line may take a third line.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Three-model review (astra, sol, fable): 51 of 52 findings applied, two
of them in part.
- pt: AQHI bands as health risk, NAVIO-TANQUE, CANDIDATO DO GOOGLE
PLACES, Provider Settings agreement, retry/feed-down wording,
NOTÍCIAS DA ÁREA (fits the briefing kicker), detection modes in the
masculine, GS and REC kept on the HUD, EXPORTAR PREDEF., prepositions
before times, and more (see docs/translations/pt.md).
- Cyclone request timeout is now a catalog message (all locales).
- Built-in Nepal base shots written by the legacy pack bootstrap now
show their localized titles: displayShotTitle also trusts the legacy
keyframes of an append pack the scene has applied.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Fix meaning errors flagged by the three-model review: downed aircraft
(Аварія літака), fast jet, the straight-ahead lane, road traffic,
pilot vessel, nearby contact counts, the low-accuracy score.
- Terms: CRS/TRK → ШК (шляховий кут), ascent → виведення, booster
flight → ПОЛІТ, session → сеанс, sequence → послідовність, place
search → пошук місця, CCTV pitch → нахил, mapped sites →
нанесені обʼєкти everywhere; glossary updated.
- Grammar: case agreement in the radio aria label and the key-store
message, «прицілу», у/в euphony, payload count after a colon,
FIRES as ПОЖ. so any count reads correctly.
- The cockpit current-style button widens to fit ЗВИЧАЙНИЙ; other
languages keep the 58px minimum.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Three-model review (astra, sol, fable): 53 findings accepted, 14 rejected
or deferred. Fixes the meaning of "Downed aircraft", "Compilation", the
satellite ground-distance caveat and the browser-key restriction note;
agreement for Full, Provider Settings and Space Missions; fork directions
that compose with every modifier; a radio aria label that reads as a
phrase; imperative tooltips. Shortens the labels that clipped: ESP. A/B,
search placeholder, QUOTA · FT, PREC, Esri imagery note.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The voice context records for a selected flight or military contact
rounded the track without wrapping, so a contact on 359.5° or more was
described to the assistant as heading "360°". Both now wrap after
rounding: 359.6° reads "0°", 359.4° still reads "359°".
Apply the accepted findings from the three-model French review: ground
track reads « route », wildfire containment « circonscrit », downrange
« distance au sol », tankers « navire-citerne », plus wording fixes for
lanes, timestamps, wind sector, radio, CCTV and voice strings.
Shots rebuilt by a pack's legacy bootstrap carry no sourcePackId, so the
first Nepal shot titles stayed English; the scene's applied-pack marker
now vouches for the pack's required titles.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Layer rows read «не оновлювалося» for a feed that never updated, not
«ніколи».
- The dock voice toggle label fits on one line (ПЕРЕМК.), the cockpit
briefing title (НОВИНИ РЕГІОНУ) no longer truncates, Celestial is «Небо».
- No-break spaces keep «24 год», «30 дн» and coordinate abbreviations
(пн. ш., зх. д.) on one line.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The Global Context panel and its buttons set ui-monospace through the `font`
shorthand, so Arabic fell back to a fixed-pitch face with letters spaced
apart. In Arabic they use the Arabic-aware mono stack.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The mission orbit lookup reads each loaded satellite's launch year from its
international designator, but satellite.js never parses that field, so the
year check never ran and a new payload could take an older namesake's orbit
and position. The loaded catalog now keeps the designator from the TLE line.
- "never" in a layer row reads لم يُحدَّث (not updated), not أبدًا.
- Space Missions (30 يومًا) and Earthquakes (24 ساعة) keep their
parenthesis together with a no-break space instead of wrapping inside it.
- VS BASEMAP is مقابل الخريطة, short enough for its segment.
- The voice pointer label THIS is هذا; two leftover حي (live) become مباشر.
- Latin provider names in Street Level hints lose their «» quotes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A container that only holds a translate="no" span (a provider name in the
Global Context lines) kept its tracking and tore its own Arabic text into
separate glyphs. Only the Latin span itself keeps its letter-spacing now.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Labels that ran out of their buttons: EXPORT / EXPORT A stay English
(EXPORTIEREN was cut to EXPORTI…), the imagery mode reads VGL. KARTE,
the scene run log PROTOKOLL (it broke as LAUFPROTOKOL/L), the dock voice
status SPRACHE N. V., and the Esri imagery note fits its line.
- The cyclone layer is Sturmwarnungen (no hyphenated break in the layer
list); Erdbeben (24 h) matches the other hour readouts; retry countdowns
get a space before s; the Mapillary hint names the Straßenebene layer.
- The cyclone source's timeout and malformed-snapshot errors were plain
English in every language: mark them like the wind source does and
translate them in de, es, ru, zh-CN and zh-Hant.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Shorter layer and segment labels that wrapped or broke mid-word in the
narrow panels: ALPRカメラ, 宇宙ミッション 30日, 地図と比較, 対象.
- Window readouts put the value after the label (空/海ウィンドウ 250 km),
so a unit never runs into Japanese text.
- 情勢図 for the GhostMaps intel picture; Googleストリートビュー joined like
any Latin product name in the guide.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
cjk.css now covers Japanese as well as Chinese: tight tracking, the 9px
floor on dock voice labels and layer buttons, whole travel-mode and preset
labels, catalog-case units in the HUD and the taller Recent Imagery cards.
Japanese also gets strict line breaking (no line starts with ー or a small
kana) and unbroken segment labels (すべて, 平面). Chinese rendering is
unchanged: :is(:lang(zh), :lang(ja)) keeps the :lang(zh) specificity.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- The cockpit news kicker no longer cuts off: regional news reads «notícias
locais», the cycle button «CICLO DESLIG.», the subtitle «ÚLTIMAS NOTÍCIAS
DO LOCAL».
- Contact header fits in two lines («AR/MAR 250 KM · INSTALAÇÕES: ATÉ
100 KM»); ground speed reads KT like the rest of the HUD.
- A space between a number and s/h/d («nova tentativa em 30 s», «há 5 h»),
and «aproxime-se de uma rua» for zoom-to-street hints.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Shorter wording where the French text ran out of its button, chip or
collapsed card at 1600×1100: the DATA LAYERS launcher (COUCHES), Recent
Imagery buttons and hints (CETTE VUE, VS CARTE, EXP. A), the ENABLING /
DISABLING layer chip, the voice detail line and the location search
placeholder. Also the CCTV range readout gets a space before m, the radio
privacy note reads naturally, and the cockpit air/sea window is shorter.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
From the screenshot review: the Recent Imagery buttons (SELEZIONA, USA
VISTA, VS MAPPA BASE) no longer clip or wrap, the layer state chip reads
AVVIO / ARRESTO so a layer name keeps two lines, the cockpit contact header
fits two lines (ARIA/MARE, SITI), and the Bhote Koshi play buttons fit on one
line (AVVIA). The post-event year follows the label, as the reference one does.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Fixes from the Arabic screenshot review:
- t() isolates every interpolated value in RTL locales (U+2068…U+2069), so
a time (10/28 09:00 UTC), a code or an English name keeps its order inside
an Arabic sentence; coordinates (30.326° ش) are isolated too. No-op in
left-to-right languages.
- The cockpit position readout reads right to left again now that its
coordinates are isolated; gauges, tapes and roll counters stay LTR.
- The start window's arrows point along the reading direction; FM seek and
the glyph-only mini transport stay left to right, the worded PREV / PLAY /
NEXT row follows the page; the Bhote Koshi clock stays a number.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The live flow status derived its wording from the rounded coverage
percent, so 1 matched dot among 300 rounded to 0% and read
"SIMULATED ... (no matches)", and 1 simulated dot among 300 rounded to
100% and dropped "Unmatched: simulated". Coverage now stays within 1-99%
whenever both matched and simulated dots are shown.
Placed route, arrow and drawn-line labels picked metres before rounding
to ten metres, so anything from 995 m to 1 km read "1000 m", even where
the draw hint for the same line read "1.0 km". The label now rounds
first, then picks the unit, and does the same at the 10 km cut, where
9,960 m read "10.0 km".
The draw hint picked m², ha or km² from the raw area and then rounded,
so 9,999.5 m² up to a hectare read "10000 m²" and about 999,500 m² up
to a square kilometre read "100.0 ha". It now rounds first, as the line
measure already does.
dump1090-fa and readsb file an aircraft's vertical rate under geom_rate
instead of baro_rate when its source is GNSS. The decoder-feed adapter
read only baro_rate, so those aircraft showed "V/S —" on the Local ADS-B
card. It now falls back to geom_rate; a sent baro_rate still wins.
Moving the replay-speed slider during the pre-launch hold or countdown
rescaled the time left to liftoff, so 0.25x turned T-8 into T-32 and 4x
into T-2. The speed change now skips a replay that has not lifted off;
mission time already flown is still preserved as before.
routeDistanceText picked one decimal while the raw kilometres were under
10, then rounded them for display, so 9.95 km or more read "10.0 km".
It now picks the unit from the rounded tenth, as it already does at the
metre/kilometre boundary.
The selected ALPR card rounded a mapped camera:direction without
wrapping, so a camera facing 359.5° or more read "DIRECTION 360°". The
rounded bearing now wraps to 0°.
TeleGeography stores 17 cables (2Africa, Echo, Hawaiki...) as several
features under one id, and the tool made a row per feature, so "Echo"
read "3 submarine cables" and area counts ran high. Rows are now unique
by cable id, and a cable still matches when any of its parts does.
Replay Ascent schedules liftoff 15 s out: a 5 s tile-preparation hold,
then a 10 s countdown. The hold ended 5 s before liftoff instead of 10,
so the replay sat on the pad for 10 s and the countdown began at T-5.
The hold now ends at T-10, and the countdown runs T-10 to T-1.
A station whose feed publishes no capacity froze its capacity at the
first status poll's bikes + docks, or at the placeholder 15 when that
poll had no row for it, so later cards read "10 avail · 20 docks · 15
cap" and coloured by the wrong ratio. Each poll now resolves it afresh.
The selected-vehicle card rounded the operator's reported bearing without
wrapping, so a GTFS-Realtime vehicle heading 359.5° or more read
"reported hdg 360°". The card now wraps after rounding: 359.6° reads
"reported hdg 0°", 359.4° still reads "reported hdg 359°".
The CCTV HUD heading token and the camera panel's meta line rounded the
bearing without wrapping, so a camera facing 359.5° or more (a feed
bearing, or a heading calibrated just west of north) read "HDG 360°".
Both now wrap after rounding: 359.6° reads "HDG 0°", 359.4° "HDG 359°".
- docs/translations/ar.md: style, Western digits and direction, units,
the six plural forms and the glossary.
- TRANSLATORS.md and CURRENT-STATE.md list Arabic and say how a
right-to-left language mirrors the chrome.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- The title, style indicator and both rails swap sides in RTL through logical
properties (inset-inline-*, margin-inline-*, border-inline-start); LTR
computes the same values. The Display/Context rail keeps its physical
`right` for the credit-clearance model and swaps in rtl.css above 720px.
- rtl.css: no letter-spacing on Arabic (it tears joined letters apart);
Noto Sans Arabic leads the Arabic font stacks so words are not set in a
fixed-pitch fallback; the intel HUD and cockpit instruments stay a
left-to-right instrument overlay; media transport and the radio dial stay
left to right; feed headlines and station names take their own direction;
the moved rail's arrows flip.
- isolate(): a quantity in RTL text (2.7 كم, -18.0°) is wrapped in
first-strong isolates so its digits do not join a neighbouring Latin run.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Register fr: catalog in CATALOGS, LOCALES and LOCALE_NAMES (Français);
any fr-* browser tag (fr-CA, fr-BE, fr-CH) maps to it.
- package-boundaries.json lists fr.json next to the other catalogs.
- Picker and locale tests cover the new row and its local names.
- docs/translations/fr.md: style, typography, plurals and glossary;
TRANSLATORS.md and CURRENT-STATE.md list French as shipped.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Translate the remaining 1,865 strings: aviation, maritime (AIS), weather
and remote-sensing terms, radio, routing, scenes, voice and setup.
- Review the 862 entries from #907: LIVE becomes DIRECT / en direct, stale
becomes obsolète, FIRMS detections are feux actifs, replay is rejeu, the
AIS ship types use the French terms (PILOTINE, SERVITUDE, POLICE), keys
stay Space / Esc / Enter as docs/TRANSLATORS.md asks, and NEXT/PREVIOUS
chips match PREV (SUIV. / PRÉC.).
- French typography: narrow no-break space before : ; ? ! and %, inside
« », and … for ellipsis; plural entries get the many form (exact
millions, "de").
- same-as-english.json: codes, units, genre and product names that French
spells the same as English.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Register ja: CATALOGS, LOCALES, LOCALE_NAMES (日本語) and the package
boundaries; ja and ja-JP tags select it.
- Japanese unit words follow the number directly, as in the catalog:
formatQuantity writes 5分 and 1時間5分 (km and kt keep the space), and
formatRelativeTime drops the space CLDR puts in 5 分前.
- Tests: locale tags, picker order, Intl output, durations and the
ramp/exit/lane wording in Japanese.
- docs/translations/ja.md: style, units, plurals and glossary; ja added to
TRANSLATORS.md and CURRENT-STATE.md.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Translate the 1,875 strings #913 did not cover (2,727 in all), for a
tactical HUD: aviation, maritime, weather and imagery terms, counters
next to numbers (機, 隻, 基, 件), year-first dates.
- Review the contributor entries: full-width punctuation ():? and …,
no space between Japanese and Latin, one katakana form per term
(ブラウザー, ビューアー), and fixes where the meaning was off:
NO PLACE LEFT BEHIND, LEVEL (horizon), CURATED / RAW PRIOR (CCTV
calibration), STALE (更新遅延, not 古い), rocket stage (段), SAR vessels,
T minus, POWER UP.
- List the codes, units and product names that stay as in English.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Translate the remaining 1,895 strings for the current UI (Bhote Koshi
event, weather, imagery, street level, directions, SDR, voice, scenes,
import) and review meirm's 832 entries from #312: 134 edited for
Ukrainian HUD terms (КУРС/ПЕЛ./ШЛЯХ/ПРИЗН., Л/П, ОНЛАЙН for LIVE,
Cyrillic units КМ, ВУЗ, ФТ, МВт), vessel types, number agreement and
the ʼ apostrophe throughout.
- Register uk: catalog, LOCALES, LOCALE_NAMES (Українська), package
boundaries, same-as-English codes; picker and plural tests cover the
four Ukrainian forms.
- docs/translations/uk.md: style, plurals, units and glossary.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Add ar to LOCALES (picker name العربية) and RTL_LOCALES, so <html dir>
turns rtl; any ar-* tag maps to it.
- Intl formatters get ar-u-nu-latn: numbers and dates keep Western digits
like the catalog, whatever the browser's ICU picks for Arabic regions.
- HUD quantities keep a space before an Arabic unit (2069.0 كم), as in
Chinese, instead of running digits into the word.
- Codes and product names that stay Latin go to same-as-english.json.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Translate the 1,875 strings #915 did not cover (2,727 in all), with all
six CLDR plural forms where tn() asks for them.
- Review pass on the #915 entries: LIVE reads مباشر (not حي, "alive"),
STALE reads متقادم, ON/OFF pair as مفعّل/متوقف; distances and speeds use
Arabic units (كم، م، قدم، عقدة) instead of Latin KM/FT/KTS; HUD stays HUD;
quotes are «» and a few clumsy count phrasings are rebuilt.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Translate the remaining 1,868 strings in a neutral infinitive style (no
Sie/du), with German compounds, decimal commas and CLDR one/other plurals.
- Review the #908 entries: knots are kn everywhere, the avionics codes BRG,
CRS and DEST stay English like HDG and SPD, expand/collapse is
ausklappen/einklappen, the radio toggle reads EIN/AUS, density SPARSE is
DÜNN, the Thermal style is Wärmebild, German quotes in the shot prompt.
- Register de: catalog, LOCALES, LOCALE_NAMES (Deutsch), package boundaries,
same-as-english (codes, product names, music genres, units).
- Tests: picker order and keyboard model with Deutsch, de/de-AT/de_CH
normalization, browser de-DE resolves to German.
- docs/translations/de.md: style, plurals, units and glossary; TRANSLATORS.md
and CURRENT-STATE.md list German.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Translate the remaining 1,868 strings (pt-BR, as in #909) and review the
contributor entries: HUD codes BRG/SPD/RANGE stay as in the other
languages, sentence-case layer and scene names, Space key named as on the
keyboard, short PREV/NEXT/ON/OFF chips, and the CLDR many form (exact
millions take «de») on every plural.
- Register pt: any pt tag (pt-BR, pt-PT) maps to it; the picker shows
«Português (Brasil)».
- Codes, units and product names identical to English go to
same-as-english.json.
- docs/translations/pt.md: style, plurals, units and glossary.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Register it: catalog in index.js, LOCALES and LOCALE_NAMES (Italiano);
it-IT, it-CH and other it tags select it.
- List it.json with the other catalogs in package-boundaries.json.
- docs/translations/it.md: style, plurals and glossary; Italian in the
shipped lists of TRANSLATORS.md and CURRENT-STATE.md.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Translate the 1,868 strings #912 does not cover: directions, weather and
wind, cyclones, Recent Imagery, Street Level, GNSS, the Bhote Koshi
reconstruction, scenes and sharing, local SDR, voice and key setup.
- Add the Italian `many` plural form (exact millions) to every plural entry.
- Review the #912 entries: fix RIMORCHIATORE, POWER UP (ACCENDI), DENSO,
minutes as "min", sentence case for layer and scene names, keyboard keys
kept in English (Space, Enter), and the SIG/NOTIZIE/LOCALE tab names.
- List codes, product names and words Italian spells as English in
same-as-english.json.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The German locale by abiyeenzo (#908), converted from its namespaced JS
catalogs to the English-keyed de.json: 859 entries whose English still
matches the current UI.
The French locale by abiyeenzo (#907), converted from its namespaced JS
catalogs to the English-keyed fr.json: 862 entries whose English still
matches the current UI.
The Brazilian Portuguese locale by abiyeenzo (#909), converted from its
namespaced JS catalogs to the English-keyed pt.json: 859 entries whose
English still matches the current UI.
The Arabic locale by abiyeenzo (#915), converted from its namespaced JS
catalogs to the English-keyed ar.json: 852 entries whose English still
matches the current UI.
The Ukrainian locale by meirm (#312), converted from its namespaced JS
catalogs to the English-keyed uk.json: 832 entries whose English still
matches the current UI.
The Japanese locale by abiyeenzo (#913), converted from its namespaced JS
catalogs to the English-keyed ja.json: 852 entries whose English still
matches the current UI.
The Italian locale by abiyeenzo (#912), converted from its namespaced JS
catalogs to the English-keyed it.json: 859 entries whose English still
matches the current UI.
Correct 年曆史 → 年歷史, 下遊 → 下游, 並道 → 併道 and 發佈 → 發布,
and list these fixes for future reconversions from zh-CN.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Rebuild the zh-Hant catalog from zh-CN with OpenCC s2tw glyph
conversion, so wording and terminology match zh-CN and only the
script differs. Drop Taiwan-specific vocabulary and wording notes;
zh-TW/HK/MO browser tags still select zh-Hant.
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]>
use bundled place boundaries and shared OpenFreeMap tiles before a guarded Nominatim fallback
preserve multipart shapes, holes and cancellation,
and keep pending annotations visible while ground sampling finishes across frames
keep non-Latin name distinctions when selecting provider results,
and retain an outline anchor only when it lies in filled geometry
The /api/military-installations endpoint was the last keyless proxy without
the admitSameSite gate that protects terrain, CCTV, traffic, places, and
Celestrak. A cross-site <img>, foreign Origin, opaque origin, or
reverse-proxy-forwarded request now receives 403 before any Overpass query.
Same-origin app requests and bare tool requests continue to work.
Adds 4 integration tests (cross-site refused, same-origin admitted,
bare tool admitted, proxied refused) following the terrainHeightsBounds
test pattern.
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.
readsb reports an aircraft's vertical rate as geom_rate instead of baro_rate
when its source is GNSS. The military normaliser read only baro_rate, so about
one in six live /v2/mil contacts had a null vertical rate in analyst and
tracked-aircraft data. Fall back to geom_rate, as the adsb.lol flights
fallback already does; a sent baro_rate still wins.
The on-globe Contacts markers picked metres before rounding, so a contact
999.5 m or more away read "1000 m" while the Contacts panel read
"1.0 km". The marker label now rounds first, as the panel does.
select_nearest_aircraft passes altitudeM: null for an airborne contact
without a barometric altitude (military rows from adsb.lol can lack
alt_baro). The speech builder coerced it with Number(), and Number(null)
is 0, so the reply said "..., 0 feet." and the voice card showed
"Altitude 0 feet". Read the altitude only when it is a finite number,
so an unknown one is left out as the builder already intends.
Vessel cards and the AIS HUD rounded the direction without wrapping, so
a vessel reporting no heading and a course over ground of 359.5° or more
read "360°" on its cards and "HDG: 360DEG" in the HUD. The rounded
direction now wraps to 0°.
The Street Level viewer caption rounded the photo's bearing without
wrapping, so a view facing 359.5° or more read "360°". The rounded
bearing now wraps to 0°.
readsb omits lat/lon for a contact heard without a current position; about a
third of every /v2/mil response is such rows. They made every military
snapshot partial, so a contact that left the feed was retained at full
opacity for five minutes instead of fading and leaving on the missed-poll
limits. A row with a hex and no lat/lon now leaves the snapshot complete and
is listed in retainIds, as OpenSky's null-position rows already are.
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]>
Main now listens to camera.moveStart in IntelHUD's constructor (#1384), so
the fake camera in src/hudPageVisibility.test.mjs needs that event too, as
hudAltitudeDatum.test.mjs already has. No change to the fix itself.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The style shaders and visualPresets imported msg() from src/i18n/index.js,
which loads every locale catalog. The share-view codec reads the styles'
defaults, so a server bundling the codec carried about 0.9 MB of
translations it never uses. They now import the catalog-free marker, as
other portable modules do; the codec's bundle drops from about 1 MB to
130 KB.
A location search that found nothing or failed now keeps its query, the open field and focus (text selected), and shows a persistent role=alert message under the search row instead of a toast. Typing, a new search or closing the emptied field clears it. HAL-727.
* feat(share): export the share-view codec for host servers
Add `@gods-eye-view/core/share/view-codec` (src/share/viewCodec.js) as a
package export with its own boundary group. The codec is pure (no DOM, no
Cesium, no external dependencies), so a host's server can decode a
/share/<code> link, for example to describe the view in link previews.
* feat(share): export the share-link address helpers
Add `@gods-eye-view/core/share/link-path` (src/share/shareLinkPath.js), so a
host's server reads /share/<code> addresses the way the app does.
* feat(share): add compact versioned share-view codec
Add a standalone codec for `#s=<base64url>` share links: a version byte
followed by protobuf-style tag-length-value fields. Settings equal to
their default are omitted and refilled from the same table on decode;
unknown fields are skipped by wire type, so new fields never break
older apps. Views are a place (camera) or a thing (an aircraft,
military aircraft, vessel or satellite to follow, with camera offset
and last-known fix); an unknown kind falls back to the camera.
The decoder bounds its input and rejects malformed payloads with a
reason instead of throwing. A converter maps the long form's parsed
state to and from the codec state; the long form is unchanged.
* fix(share): tighten codec decoding after review
Unknown detection mode and allocation names now fall back to their
defaults instead of reaching the lenient long-form migration. Future
panel values and style-parameter tokens are skipped rather than applied
or rejected, and layer options are accepted only under their owner
layer. Wrap longitudes before scaling, normalize negative zero, reuse
the layer-state tracking ID grammar, and align the format doc with the
decoder.
* feat(share): let a host share the link its own way
ShareLinkManager.buildLink() returns the current share link without
copying it, and copyLink() now copies exactly that. The share button
passes the link to an optional services.shareLink(url), which resolves
to 'shared', 'copied', 'cancelled' or false; 'cancelled' shows no toast
and 'shared' shows the new "Link shared" (es, ru, zh-CN). Without the
service the button copies as before.
* feat(share): open and make short /share/<code> links
The share button now makes `<app>/share/<code>` links with the compact
share-view code, and the app opens them at start-up through the same
restore pipeline as long links. Once the app next writes the address, the
share path is replaced (no new history entry) by the app's own address and
the live long-form hash, so a reload follows the live view. A host can pass
the code as `initialShareCode` instead. A code that fails to decode restores
nothing and shows "This share link can't be opened".
The code gains a trailing CRC-8 check byte, so any single mistyped
character is rejected instead of opening a different view. Thing views no
longer store a camera: it is derived from the entity's last-known fix and
the follow offset on a sphere, the entity kind is implied by the id's
field, and the followed entity's layer bit is implied; an aircraft followed
from the app's own camera is a 42-character code. The `#s=` hash form is
gone.
While the camera follows an aircraft, military aircraft or satellite, the
link is a thing view with the camera's offset and the entity's position.
Opening one turns the layer on and follows the entity through the existing
shared-follow restore, then swings the camera round to the shared offset; a
vessel view selects the vessel once the vessel feed has it. If the entity
is not reporting within its window, the view stays at its last-known fix
and says so. The panel and style-parameter tables now come from the codec,
and a host share hook that returns a plain value or throws still gets a
toast.
* fix(share): encode style settings against the style's own look
Settings a style applies when selected (bloom, sharpen, HUD, detection,
celestial ring and the style's shader parameters) now default, per field,
to what that style applies in a fresh session, read from the tables the
app applies (the first-load baseline, the style presets and the shader
defaults). The encoder compares against the view's style and the decoder
fills omitted settings from the decoded style, so a view that keeps its
style's look costs only its camera and style. The previous defaults did not
match a fresh session (sharpen, HUD and detection were off), so every link
paid for them, and a style's own shader parameters were always written.
A test pins the per-style defaults as frozen v1 meanings. A followed
aircraft in Noir is now 44 characters and the real long-link example 39.
* docs: changelog for #1418
* fix(layers): make Clear All clear the saved and shared layer selection
After Clear All every layer control was off, but the share URL could keep
listing layers (for example l=e.f) and a reload switched them back on.
The URL and local preferences are written from the committed layer
state, which only explicit visibility changes update. Clear All issued an
explicit OFF only for layers it found on at the start of the batch and
whose turn was not superseded. A layer already off for a passive reason
(isolated by Contacts or another context mode, or released by Contacts'
own teardown while the batch ran) never received one, so it stayed in the
committed state. Clear All also discards the context snapshot that would
have restored those layers, leaving the screen and the URL out of step.
Clear All now publishes the set of layers still on once its batch
settles, and the layer-state coordinator commits that selection: every
saved layer not in it is dropped and its passive restore is cancelled,
while a layer a newer explicit request turned back on is kept. A context
restore interrupted by Clear All no longer brings its snapshot back after
Contacts settles.
* docs: changelog for #1417
* fix(share): restore a share link opened by hash navigation
Opening a different share link in an already-open tab (pasting it into
the address bar, following a link, or Back/Forward) changed the URL but
left the camera, visual style, map, panels and layers on the old view
until a reload.
The share manager now listens for hashchange and popstate. A new hash
that parses as a share and differs from the last hash the app read or
wrote runs the start-up restore pipeline: lane authority is reserved, URL
writes are held (including a pending debounced write of the old view),
the camera restore takes a new navigation generation, and the layer
coordinator replaces any in-flight layer restore with the new share's
layers through the passive share origin. A newer navigation supersedes an
older restore end to end; only the newest one releases URL writes. The
app's own replaceState writes never restore. Listeners are removed on
destroy.
Returning to the Normal style and resetting an already-active preset now
also apply when a share is restored after start-up, so the result matches
a fresh load of the same link.
* docs: changelog for #1416
Add the shared Voice Preview and Settings panel with browser-local preferences, inactivity controls and visible cost warnings.
Contribution by @manjunath22466.
- fmtArr prints reveal-err and flags an angle violation on a timeout result.
- Regression: a late hidden reveal frame records no angle and stays retryable.
- README/SECURITY: $host drops a non-default port, so recommend $http_host.
- requestAdmission tests: say why a new admission test must use freshClock.
- observeArrivalRotation reads the sample before the deadline check and keeps
a late frame's angle on the timeout result; arrivalAngleViolated then makes
it final. Regressions: wrong late reveal and wrong late settle, each followed
by a clean second attempt, still fail; a clean late frame stays retryable.
- Overpass admission tests: a monotonic per-test clock removes order
dependence on the module-level limiters; the proxy test covers ::1 and
::ffff:127.0.0.1 sockets; a new test pins the proxied: upstream bucket.
- Retry edges (wrong on-time reveal, maxAttempts, inclusive 10° limit) and a
check that every name the track harness imports is exported.
- Docs: nginx needs Host, X-Forwarded-Proto and X-Forwarded-For; Docker bridge
networking makes every host client one remote peer; list the exact
forwarding headers; owner misses can also see 503.
- /api/overpass: a loopback socket with forwarding headers (a local reverse
proxy) is admitted from the remote pool under a socket-keyed proxied: bucket,
never the owner's direct-loopback window; the upstream limiter uses the same
key. Regression: owner cache hits survive an exhausted proxy bucket.
- Arrival check: a reveal timeout keeps the settle angle it measured; the new
observeArrivalRotationWithRetry retries only inconclusive timeouts, so a
real violation cannot be retried into a pass. Regression: wrong settle angle
-> reveal timeout -> clean second attempt still fails.
- SECURITY.md, README, changelog and CURRENT-STATE: scope the guarantee to
admission isolation, name the forwarding headers a local proxy must send,
and state that the upstream budget stays shared.
Once the TomTom tile budget is spent, or TomTom fails, the proxy serves
cached tiles marked STALE-BUDGET / STALE-ERROR. flowSource works out
`stale`, but the layer fetched flow through fetchFlowForBounds, which
returns only the segments, so getStats() and the sync chip still read
"LIVE · Roads: …". #1239 fixed the get_traffic_flow tool the same way; the
layer now uses fetchFlowDetail too, reports stale and oldestTileAt in its
stats, and the chip reads "STALE · Roads: <source> · Flow: cached TomTom
tiles". A source without fetchFlowDetail keeps the previous behaviour.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
When WFIGS was unreachable the proxy served its last snapshot, marked stale
(X-Data-Stale, body.stale), with no age limit, and the client source kept
only the rows. The layer then read healthy with a fresh update time and
get_fire_perimeters reported the perimeters as current; three days into an
outage it still said "1 mapped wildfire perimeter in the requested box."
The source now offers getSnapshotDetail ({rows, stale, fetchedAt}); the
layer reports stale with the snapshot's fetch time, the tool adds stale,
retrieved_at and "(data may be stale)", and the proxy serves a stale
snapshot for at most 24 hours, as the GNSS proxy caps its own.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
* chore: remove four modules nothing in the app uses
src/app/sourceSlot.js re-exported src/sources/sourceSlot.js, which every
caller imports directly. src/data/flowTiles.js and src/data/issPass.js were
reached only by their own tests. src/data/localGeojson.js wrapped
localGeojsonCore.js with the app's services, which the catalog already
passes itself. The local GeoJSON layer tests now build layers the way the
catalog does, so they keep covering localGeojsonCore.js; only the test of
the wrapper's own default host goes. Boundary lists, format scope and docs
drop the removed paths.
* docs: changelog for #1403
Review follow-ups: the upstream error now carries its HTTP status and the
pause checks error.status instead of the message text; concurrent 429s
extend the pause with Math.max rather than a shorter Retry-After cutting a
longer one short; the per-chunk re-check says it is for multi-chunk
requests; the first 429 test runs its backoff on mocked timers.
CURRENT-STATE describes the pause.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A query with a comma counted as "place, region", so a nearby place whose
own name has a comma ("Bed, Bath & Beyond") no longer won the nearby pass
and the unbiased pass could fly to a namesake far away. Its whole-name
match is now kept as the last choice, after the region checks of both
passes. CURRENT-STATE describes the place-region rule.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A name that only started with the place word also matched, so from Lyon
"Paris, France" picked "Paris Gare de Lyon" in the biased pass, where main
fell through to the unbiased answer and reached Paris. Only an exact name
matches now. Adds a test for the UK alias.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A rate-limited upstream got asked again by every page request for new
points (about once a second while the camera moves), each with its own
retries, which kept the limit in force and logged "refresh incomplete" each
time. After a 429 the proxy now leaves the upstream alone for its
Retry-After (at most 10 minutes), or 60 s when it sends none, serving
cached points meanwhile. The warning that starts the pause says
"(HTTP 429; pausing upstream for N s)"; requests refused during the pause
don't log.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
From Denver, 'Paris, Texas' flew to a pub in Budapest at rooftop range. The
camera-biased Photon answer listed the town first, but the pub's name equals
the whole query, and the biased path matched names only. A query with a
region now takes a candidate named for the place whose city, county, state,
country or country code (or USA/UK for US/GB) contains every word of the
region, in both the biased and the unbiased pass; with no such candidate
the existing name rules decide. A query with no region after its comma
keeps the biased name match.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
get_active_fires joined FIRMS acq_date and the unpadded acq_time, so a
detection at 00:45 UTC came back as "2026-01-01 45". The row now uses
acquisitionMsUtc, which pads the time, and reports an ISO UTC timestamp
like the other tool times.
Compare declared visibility with the manager's effective intent, and treat
in-flight transitions as stale so a toggle or superseded LOAD rollback that
is still settling cannot slip past the check. Reconcile only those layers
inside the lightweight seek instead of rebuilding the shot, and report a
refusal without resetting the seek position or camera.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Document visual evidence, category and data licensing requirements for new layers, using the changelog fragment workflow.
Contribution by @asturksever.
formatCockpitWindDirection rounded the bearing and printed that number, so wind from 359.5° or more read "N · 360°". Take the rounded degrees modulo 360 so north reads 0°.
formatMeasure checked the raw length against 1000 but printed it rounded, so a drawn line of 999.5 m or more read "1000 m" in the hint. Compare the rounded metres so it moves to kilometres.
Discard stale HUD summaries when camera movement starts and allow cancelled requests to retry for unchanged place context.
Contribution by @quanticsoul4772.
The Contacts panel checked the raw meters against 1000 but printed them rounded, so 999.5 m or more read "1000 m". Compare the rounded meters so the distance moves to kilometres.
routeDistanceText checked the raw meters against 1000 but printed them rounded to tens, so 995 m or more read "1000 m". Compare the rounded value so it moves to kilometres.
routeDurationText checked the raw seconds against 60 but printed them rounded, so a leg of 59.5 s or more read "60 s". Compare the rounded seconds so it moves to minutes.
Report stale traffic tiles and their fetch time, and release pending freshness metadata when tile requests fail or are cancelled.
Contribution by @quanticsoul4772.
Support spaced hemisphere coordinates while rejecting malformed numeric fragments and ambiguous component assignments. Include regression tests and runtime documentation.
Contribution by @rudycelekli.
Main now has #1282, which pauses periodic layer polling in a hidden page
with its own catch-up and listener cleanup, so this PR drops its duplicate
lifecycle.js change and test in favor of main's.
Review fixes:
- data.js passes panelAwareVisibility(document) (new in app/embed.js) as
the lifecycle's visibility target, so an inline panel whose host reports
it hidden keeps polling. The HUD's 250 ms readout follows the same
document.hidden && !isEmbeddedInline() rule as tools.js.
- Tests: embed.test.mjs (layers poll in an inline panel reported hidden,
pause without the flag) and hudPageVisibility.test.mjs (HUD readout).
Both use mock timers and clean up in finally.
- changelog.d/1344-inline-panel-visibility.md and a note in
docs/CURRENT-STATE.md.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Read OpenSky OAuth credentials from OPENSKY_CREDENTIALS_FILE and stop sending retired Basic authentication. Update setup diagnostics and guidance.
Keep credential-file warnings free of parser excerpts and filesystem error details, cover both failure paths with a regression test, and use a launcher-compatible absolute path example.
Original contribution by quanticsoul4772, with credential-warning and documentation follow-up by Sameh Khamis.
Co-authored-by: quanticsoul4772 <[email protected]>
Co-authored-by: Sameh Khamis <[email protected]>
Wrap longitude differences in annotation proximity checks and nearby-place distances so points across the antimeridian remain close.
Original contribution by quanticsoul4772.
Co-authored-by: quanticsoul4772 <[email protected]>
add the shared Voice Preview and Settings panel with browser-local preview
and inactivity preferences, keyboard controls and responsive positioning
preserve main’s Rotation and Context behavior, clarify panel routing and
refresh schema pins within the unchanged prompt limit
A hidden tab shows nobody the result, so polled layers (flights, quakes,
weather...) skip their refresh while the page is hidden and catch up with
one refresh when it is shown again. The lifecycle takes an optional
pageVisibility service ({ isHidden, subscribe }) so src/data/lifecycle.js
stays free of browser globals; src/app/data.js supplies the document-backed
one. Without the service, refreshes run as before. Every place that stopped
a layer's interval now goes through one helper that also drops the
catch-up listener.
The HUD's 250 ms camera readout also skips hidden tabs.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Add a complete Taiwan Traditional Chinese catalog, locale selection,
Chinese readout formatting, translation notes and regression coverage.
Seed translations from #724 and #652 and adapt the zh-CN catalog.
Co-authored-by: caxer <[email protected]>
Co-authored-by: tbdavid2019 <[email protected]>
Co-authored-by: Enzo Abyie <[email protected]>
Co-authored-by: EDIT-Aeterna <[email protected]>
* feat(street-level): export the KartaView and Panoramax providers
A host that composes Street Level with its own provider list can already
import the Mapillary and Google Street View providers; KartaView and Panoramax
were reachable only through the default list, which needs a Mapillary source.
Exporting them lets a host offer the keyless providers without Mapillary.
* chore: changelog entry for the Street Level provider exports
* feat(street-level): send Mapillary Graph lookups to a source endpoint
A Mapillary source can take a graphEndpoint, a Graph API base that adds
the token itself. Its sequence lookups go there through the source's
transport, and the photo viewer uses MapillaryJS's stock GraphDataProvider
at that endpoint, so a host that proxies Mapillary needs no client token in
the page. Without it, behaviour is unchanged: direct Graph API calls with
the client token.
The proxy now skips malformed upstream rows individually but counts
them, capped at 10,000, in the catalog metadata and the /stations
envelope (rejectedStationCount). The count never fails health policy,
so an otherwise healthy generation is still accepted. The client
validates the field, adds its own rejected rows after admission, and
reports degraded coverage when either side skipped rows. Adds an
end-to-end proxy-to-client test and aligns DATA_SOURCES.md and
CURRENT-STATE.md.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Clear _rejectedStationCount alongside _degraded so a destroyed and
re-initialized layer does not report the previous catalog's count.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A caller that joins a cold reservation no longer inherits another client's
RATE_LIMITED result. Once the rejected reservation is released it retries:
it either owns the next reservation and is admitted on its own quota, or joins
an admitted fetch. Adds a two-client regression (exhausted owner, available
joiner, exhausted joiner coalescing onto the admitted fetch).
Also drops the duplicate memory publication for a disk hit in the handler;
preflight already sets it, the handler only trims.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- The DOCTYPE scan now jumps over whole comments and processing
instructions found outside a declaration string literal, so a quote or
bracket inside them no longer leaves the scanner stuck before the root.
- Root namespace repair walks the root's attributes in order, so
declaration-like text inside a quoted value can no longer fill `seen`
and cause the real declaration to be dropped as a duplicate.
- Regression tests for both, plus linear-time cases in the isolated
deadline test.
- CURRENT-STATE: describe the scanner as a boundary check and leave the
rest of the XML grammar to DOMParser.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- CelesTrak allowlist includes `active`, which Space Missions'
getActiveTle() requests; unknown groups still answer 400.
- The CelesTrak gate uses admitSameSiteKeyless so the documented reverse
proxy keeps working while foreign/opaque Origins and cross-site
Sec-Fetch-Site are still refused with 403.
- A provider failure never shortens an existing cooldown (CelesTrak and
Launch Library keep the later deadline).
- Tests: real getActiveTle() through the middleware, proxied app request
vs unrelated Origin, concurrent failures finishing in both orders.
- docs/CURRENT-STATE.md: eight groups, keyless gate, non-shortening cooldown.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The standalone military source no longer has getTrack, so the MCP
get_aircraft_track tool threw a TypeError and MCP advised a retry. It
now returns an `unsupported` ToolError when the selected feed has no
getTrack, while custom sources that supply history still work. Tests
cover the default composition through the catalog and MCP.
Also drop the unused epoch import and update the CURRENT-STATE track
sections to describe local trails and 503 history_unavailable.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A move inside the 120 ms hover throttle window was dropped, so a pointer
that stopped right after a pick kept a stale cursor (pointer over empty
space, or default over a mission) until the next move. Keep a copy of the
last suppressed position (Cesium reuses the movement object) and run one
trailing check when the window ends. The check rechecks hover eligibility
and is cancelled by reset, disable, camera motion, and destroy.
Tests cover a stopped pointer in both directions without another event,
the copied position, one timer at a time, cancellation, and the
eligibility recheck.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
An overdue refresh on return to a visible tab advanced the due time but kept
the old setInterval phase, so the next phase tick was skipped and the gap
could approach two intervals (also after a failed resume refresh). The
catch-up now re-arms the interval at the new deadline. Interval ticks no
longer compare against the due time, since the phase and the deadline share
one origin. Overlap prevention and teardown are unchanged, and a stale
listener cannot re-arm a stopped loop.
Tests cover the next poll one interval after a between-ticks resume, with a
successful and a failed catch-up refresh.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A closure that removes the last animated points mid height correction
released the render hold, and animate() then returned before easeHeights()
could run, leaving _heightPending (and getStats().loading) true. Settle
every retained road on its corrected heights before the hold is released.
Adds a regression combining height correction, a late closure and an
empty population.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
At 720px and narrower the panels live in the phone shell's tab sheets, so the
camera, scene and map-source tray harnesses open the matching tab (from the
keyboard where they check focus rings) before measuring, wait for the shell to
follow a viewport change, measure the container that actually scrolls, and run
the pin and compact-wing checks at 800px where the narrow desktop dock still
exists.
At 720px and narrower the globe stays full screen and a bottom tab bar (Layers,
Cameras, Context, Search, More) opens one bottom sheet at a time, presenting the
existing rail and dock panels in place. Sheets borrow panels without persisting
or sharing them and put back what they changed. Cockpit, recording and clean
view close the sheet; scene playback keeps STOP reachable. Focus moves into the
sheet and back to its tab, the required attribution stays visible above the
voice control and any sheet, and the page uses safe-area insets. Wider
viewports are unchanged.
Adapted from the phone tab-shell proposed in #203 by @blearymoth.
Once the cache was full, every write sorted all 50,000 entries to find the
oldest. A Map keeps insertion order, so keep the map in fetch-time order
and delete from the front: the resolver re-inserts each point it refetches
(an overwrite would leave it in its old place), and the disk load sorts
once, since a file written before this change, or a migrated raw-batch
cache, is not in age order.
Suggested by asturksever on #975.
A stale point that is refetched becomes the newest entry, and the cache
file's order is not its age order. The ceiling must still evict the
oldest fetch. Passes on the current sort-based eviction.
The README source tree now lists src/ui/ instead of the removed
src/ui.js. docs/CODE-BOUNDARIES.md names src/ui/applicationShell.js as
the owner of panel layout, persistence and share restoration, and
docs/CURRENT-STATE.md places GLOBAL_POST_DEFAULTS in
src/ui/visualPresets.js. QA script comments and test assertion messages
that still named ui.js are updated the same way.
An ESLint no-unused-vars scan over src, server, scripts, tools, build
and pinokio, repeated until it is stable, reported these.
Runtime modules keep every call and drop only the unread binding:
- src/ui/cockpitInput.js: the unread result of enter()/exit().
- src/layers/launches/rendering.js: the unread entity returned by
entities.add().
- src/layers/radio/rendering.js: the unread map key in the horizon scan,
which now iterates the map's values.
QA scripts and tools drop an unused helper, two unused in-page module
imports, unused counters and destructured fields, and an unread wait
result. Tests drop source files read into variables that no assertion
uses, redundant StyleManager imports (the shell-source helper already
imports it), and unused destructured fields.
The one remaining report is the `_` loop variable in src/tools/schema.js,
which counts iterations and is intentional.
QA harnesses import some modules in the page by path, for example
import('/src/data/radio.js'). Those calls name exactly which bindings
they use, and every other export of these modules has no importer:
- src/data/radio.js: 20 aliases of radio layer methods and two
re-exported altitude constants. The radio layer keeps the methods.
- src/layers/transit/movement.js: tuning constants and course helpers
used only inside the module lose `export`; FIX_HISTORY_MAX,
DISPLAY_LAG_MIN_MS, DISPLAY_LAG_RESET_MS, FIX_NOISE_M, STOPPED_FIXES
and courseBetween, which nothing reads, are deleted.
- src/overlays/worldOverlay.js, src/splitFlap.js,
src/data/contactPlayback.js and src/voice/cloudVoiceAuth.js: helpers
and constants used only locally lose `export`.
- src/data/local_data/dams/dams.geojson held the same 704 features as
dams.geojsonl, feature for feature. Only the JSONL file is loaded
(src/sources/infrastructureData.js), so the GeoJSON copy is dropped
and the dataset README no longer mentions it.
- public/models/ship.glb is not loaded anywhere: no module, template,
stylesheet or script names it. Its row in public/models/README.md
goes with it.
Together these remove 960,345 bytes of tracked data.
Knip, configured with the application, package exports, scripts, QA
harnesses, workers, tools and tests as entry points, reports exports
that no module imports. Each was checked by hand against string-based
and namespace imports.
- Symbols that are still used inside their own module lose only the
`export` keyword (176 constants and helpers).
- Unused re-exports are removed from barrel modules (flight, military,
vessel, CCTV, directions, transit history, local ADS-B and street-level
policy modules).
- Declarations that nothing uses at all are deleted, among them
AIS_WATCHDOG_STATUSES, applyFilter, isRadioCountryCode, compactText,
DENSITY_STOPS, the unused CCTV endpoint constants, and the SDR band
constants.
- Default exports that nothing imports are removed. The ones in
src/data/earthquakes.js, src/data/telegeographySubmarineCables.js,
src/data/wind.js and src/data/bhoteKoshiLocator.js each constructed a
layer instance at import time that nothing used; those instances are
no longer created.
Every export of a package.json entry stays, as does TOMTOM_CREDIT,
which the traffic layer reads from the credits namespace it is given.
Delete three modules that no entry point, package export, test or script
imports:
- src/ui.js, a StyleManager subclass that pre-selected the standalone
catalog. Application controls are built through src/app/controls.js
and src/standalone/controls.js.
- src/standalone/ui.js, a bare re-export of StyleManager.
- src/data/localLayers.js, a list of default infrastructure, cable and
FIRMS layer instances. The catalog builds these layers itself.
With src/ui.js gone, the import-direction check keeps a single
compatibility exception (server/providers/local.js to
server/standalone/key-setup.js), and its test now asserts that a
src/ui.js importing standalone code is rejected. The boundary and
format registries and the docs that named these files are updated.
Remove locals that are assigned but never read: two unused
JulianDate.now() calls in the flights and military nearby queries, an
unread record lookup in the military snapshot renderer, unused axes from
the CCTV gizmo, an unused vessel state alias, and two unused ALPR
presentation handles. Each removed expression is side-effect free.
Drop module-local helpers that nothing calls (linkAbort, summarize,
stringOption, summarizeEntity and the polygonCenter, propertyObject and
unwrapProperties helpers only it used, CALLOUT_DOT_SIZE_PX) and unused
import specifiers in runtime modules, tests, QA scripts and tools.
None of the removed bindings is exported. Re-exports such as the ALPR
policy constants and every package export are unchanged.
CAMERA_PRESETS and flyToPreset have no callers. Startup uses flyToAustin,
which stays, and city navigation uses flyToPresetLocation from
src/locations.js. Neither removed symbol is a package export.
i18n: Simplified Chinese (zh-CN)
Add a complete Simplified Chinese translation and language picker support.
Improve Chinese typography, date formatting, and localized readouts.
Include translation guidelines and regression tests.
Based on translations by abiyeenzo (#914) and EDIT-Aeterna (#979).
Co-authored-by: German Khokhlov <[email protected]>
Co-authored-by: Enzo Abyie <[email protected]>
Co-authored-by: EDIT-Aeterna <[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.
A fresh full review of the catalog (52 changes in 48 entries):
- Age readouts that render as bare durations got their missing 前
(LIVE · updated {age}, POSITION {age}).
- Unified the rate-limit family on 已被限流 (12 entries had 已限流,
正在限制请求频率 or 受到频率限制).
- Unified photorealistic on 照片级逼真 (planet strings, 3D Tiles).
- Round-1 stragglers: 括标 → 括号标记, FLIGHT/VESSEL WINDOW →
监视窗口, mapped-ALPR loading strings, plain Talk → 脱口秀, the
hardcoded Current style twin.
- Collocations: 与…同源, 不超过…上限, 单击放置顶点, 随语音分享.
- Mainland usage and consistency polish: 气象高度, 地面风, 标签页,
已获取航迹 (acquire ≠ lock), 缩放级别 6, 发布时间, 独立视角,
视野中心, 播报当前视野, and similar one-word fixes.
Kept deliberately: the 图像 (image) vs 影像 (imagery) distinction,
which mirrors the English and is consistent across the catalog.
Review of all 2,677 zh-CN entries (scripted checks + full meaning
review) confirmed the catalog fully Simplified and mechanically clean.
83 fixes in two passes:
Meaning and terms — PARTLY CLOUDY lost 'partly' (多云 → 晴间多云);
AIR/SEA WINDOW said scheduled flights (航班 → 空/海监视窗口); Mapped
ALPR Cameras collided with ALPR Cameras; globe terms unified on 地球
(球体地图/球体影像); later/earlier observation buttons read as future
actions (稍后/早前 → 较晚/较早); 从未叠加… misread as 'never'; 风流线
→ 风场流线; 访问飞行器 → 来访航天器; 目击 → 目击者; downrange is
落点距离 not 航程.
Polish — 加载 over 载入; 预警 for advisories; {amount}K/M/B 美元;
空载; 窗口 over 视窗; 分享的 {subject} matches its siblings; missing
CJK↔Latin spaces restored around {store} and {mode}; 拦截点击,
已被限流, 精选, 逼真世界, and other one-word mainland-usage fixes.
The router reads an absolute-form target's path without resolving dot
segments, so a target such as http://host/api/../x still reaches /api
middleware. Take that path the same way when deciding whether the API
response headers apply.
The Street View fallback now treats any forwarding header as forwarded, even
an empty one, before it spends the key.
The URL parser already turns backslashes into separators, so only encoded
separators need refusing; a pin prefix without a trailing slash is treated as
a directory.
The CCTV routes now use the keyless cross-site checks, which refuse cross-site
browser requests but accept forwarding headers, so cameras work behind the
documented reverse proxy. The Street View fallback is the one step that spends
a key; a request with forwarding headers skips it and gets the placeholder
frame, as when no key is configured.
The router mounts /api without regard to letter case, accepts a '.' as well
as '/' after the prefix, and routes absolute-form request targets, so the
API response headers were missing on those spellings. Match the same way, on
both the raw and the parsed path. The test now runs real dev and preview
servers.
Camera catalog image URLs were pinned by string prefix, so a URL that only
began with the agency's directory before dot segments or an encoded separator
were resolved still counted as inside it. The pin now parses the URL and
requires the same scheme, the exact host, no credentials, the default port and
a normalized path inside the pinned directory. Redirects on a pinned host must
stay inside the same directory.
Document the response format checks and API response headers alongside the
existing server hardening notes.
CCTV frames and the Street View fallback accept only JPEG, PNG, GIF, WebP and
AVIF, parsed from the media type essence and sent under the canonical type;
any other type is cancelled and falls through to the next fallback as a
failed upstream does. Live media must match its feed kind (video/mp4,
video/webm, or multipart/x-mixed-replace with a boundary) before the camera
is reported healthy or any byte is relayed, and its redirects now stay on the
registered origin like frames. Every /api/cctv route uses the same-site gate.
The GBFS proxy relays only bodies that parse as JSON, re-serialized under a
fixed application/json type.
Every /api response, including errors, partial responses, Host refusals and
unknown routes, now carries a sandboxing Content-Security-Policy and
X-Content-Type-Options: nosniff on the dev and preview servers. Vite's server
and preview headers only reach the documents and assets it serves itself, so
a small plugin sets them ahead of the Host check. A route's own policy is sent
as an additional policy rather than replacing it.
A headless SwiftShader run showed a 926 ms first frame on the military probe,
which no wall-clock budget can accept: the 1 s idle refresh may already have
run inside it. Raise moveEnd and wait one frame before each attempt so the
settle frame is measured against a pass that just ran, and treat a timeout
(never a wrong angle) as inconclusive with one more attempt. Document why the
per-phase budget stays at half the refresh interval.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Address gekh's review of 69a87fe2:
- P1: /api/overpass now uses admitSameSiteKeyless, like /api/terrain/heights,
so an instance behind the README's reverse proxy keeps its Overpass-backed
layers; cross-site shapes are still refused and forwarding headers no
longer launder them. SECURITY.md, the gate header and CURRENT-STATE say so.
- P2: admission keys IPv6 peers by /64 and folds IPv4-mapped peers
(clientNetworkKey), and loopback has its own 300/min window outside the
900/min global, so a few LAN peers cannot lock the owner's browser out.
- P2 (track): the settle and reveal frames each get their own 500 ms budget,
measured from moveEnd and from the settle frame respectively, so a ~300 ms
SwiftShader frame no longer trips the reveal deadline while both stay
inside the 1 s idle refresh.
- Smaller: drop the dead settings-block check and stale probe comment in
query.js, clamp whitespace-tolerant [ timeout : N ], and add tests for the
global window, the /64 keying, loopback headroom, proxied admission and
cache hits spending admission but not upstream budget.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Merge origin/main and retain both format-scope additions.
Give StarrySky a separate black environment cube map and release it on destroy.
Guard anime updates when StarrySky is absent, require a visible Normal sky
before anime QA, and compare a lit airplane against black and white controls.
Document the sky background contract and its verification.
Co-Authored-By: GPT-6 (Codex) <[email protected]>
Preserve Chinese unit casing and spacing in the HUD and cockpit, localize FIRMS cache ages, CCTV health and radio counts, and keep imagery resolution and route modes readable. Add regression coverage for localized output and unchanged English.
Co-Authored-By: Codex (GPT-6) <[email protected]>
Correct weather, imagery, scene, voice, and status wording after the Chinese UI review. Align the translation glossary with the revised terminology.
Co-Authored-By: Codex (GPT-6) <[email protected]>
Enzo's commit email is not linked to his GitHub account, so credit his #914
translation here as well.
Co-authored-by: Enzo Abyie <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Translate the remaining 910 strings and review the whole catalog (meaning,
terms, punctuation, label length) against the screens.
- Register zh-CN: any zh tag maps to it except Traditional ones (Hant, TW,
HK, MO), which keep English until a Traditional catalog ships.
- Hemisphere letters get their own context so the HUD sector line reads
21.33N, not a compass word; the Bhote Koshi imagery dates follow
year-first languages (2021年10月16日).
- cjk.css: tight tracking for Chinese text and a 9px floor on the dock voice
labels and layer buttons; Latin HUD readouts keep their look.
- docs/translations/zh-CN.md: style, plurals and glossary.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The Simplified Chinese localization by EDIT-Aeterna (#979), converted to the
English-keyed catalog: 823 more entries for UI text that #914 did not cover.
The Simplified Chinese locale by abiyeenzo (#914), converted from its
namespaced JS catalogs to the English-keyed zh-CN.json: 891 entries whose
English still matches the current UI.
When the model upstream fails, the wind proxy keeps serving its last
run, however old, with manifest.stale set. The wind layer shows that,
but get_wind ignored it and gave the old run's wind as current.
get_wind now returns stale and adds "(forecast may be stale)" to its
summary, the way get_weather and the space tools flag stale data.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- taiwan.js: a failed download serves the last good table (up to 24 h)
and is not retried for an hour. The table host does not answer from
some networks outside Taiwan, and each attempt held the shared catalog
refresh for the ~10 s connect timeout.
- Own server/providers/cctv/taiwan.js in package-boundaries.json
(check:boundaries failed).
- Decode numeric character references; name falls back to the CCTVID
when a row has no road details.
- Credit names the dataset and links the OGDL Taiwan licence.
- Document CCTV_TAIWAN_URL; list Taiwan in the CURRENT-STATE source table.
- Tests: isolate catalog env, reset loader state, cover the byte cap,
failed fetch, backoff, last-good fallback, cap clamping, redirect with
a valid body, name fallback, numeric entities and UUID camera ids.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Move the Hong Kong CCTV changelog entry into changelog.d/ (main now takes
fragments instead of CHANGELOG.md edits) and document CCTV_HK_ENABLED,
CCTV_HK_MAX_SOURCES and CCTV_HK_CAMERAS_URL beside the other packs.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Fetch keyless DATA.GOV.HK camera locations at runtime and pin stills to
tdcctv.data.one.gov.hk with required attribution. Default cap 1,100 covers
the ~1,013-camera TD catalog, prioritized across Hong Kong Island, Kowloon
and the New Territories.
Co-authored-by: Cursor <[email protected]>
The slim-down in #974 dropped --sl-text-small, which .sl-since-note still
used, so with KartaView lit "KartaView: by upload date" rendered on its
own line larger than the value. The note now sits beside the value in the
same mono type and letter spacing, a size smaller (0.5rem), as
"(by upload date)". One source is lit at a time, so the provider name is
dropped. es/ru catalogs, tests, README, CURRENT-STATE and the registry
JSDoc follow.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Google 3D: the OPEN button re-measures a ground whose tiles are still
loading (a few times, a second apart) and on a map change, not only
after the next camera move.
- Mapillary: two mounts on one host during the first download share one
build instead of leaking a second WebGL viewer.
- Retired floating-panel keys (v8 panelPos, panelFloatHintShown) are
dropped from localStorage; dead inline-position resets, the drag JSDoc
and the dead qa-ui-disposal observer branch are gone.
- Docs: z ladder, panel chrome, position storage and the fixture gate's
actual coverage; changelog fragment for #974 (old .gev-cache/mapillary
can be deleted).
- ru/es: the layer is named as everywhere else in the UI.
- Fixtures and test fakes: unused exports removed; a fake credit removal
of an absent credit no longer drops another one; the tile test reset
frees upstream slots.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The far-plane comment said z = w (the code puts z just under w), the Milky
Way constant is a brightness, not an opacity, and the CDS acknowledgement
lives in the Data attribution popover, not DATA_SOURCES.md.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- qa-anime-sky keeps only the Milky Way case; the satellites and keyless
cases checked an anime shader this branch no longer changes.
- Drop qa-sky-sun-moon.mjs: the viewer-options unit test covers it.
- VisualEffects sets the sky's visibility directly: the viewer always
carries a StarrySky and never outlives the effects.
- Drop the unused star intensity knob, the malformed-row tolerance for
our own pack, the test-only Transforms parameter and a private-name test.
- DATA_SOURCES points to sky/README.md instead of repeating it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Scene reads skyBox._cubeMap as the environment map that lights models, in
the TEME, flipY layout of Cesium's own sky box; the ICRF Milky Way would
light them rotated. Rename the field so models keep Cesium's default.
Test the anime fade on a real StarrySky and that the viewer gets the sky
as an option (skyBox: false would drop the Sun and Moon). Correct three
comments on the star limit and the Milky Way levels.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The fine 0.4 degree blur kept the residual glow of unresolved stars as
mottled patches at the band's edges. Blend towards a 2 degree blur where
the coarse copy is faint (smoothstep over 0.5-8 % of the range), so the
core keeps its dust lanes and the faint sky goes smooth. Faces 85 -> 62 kB.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Resolve conflicts with #1234: keep redirect: 'manual' on both DATEX
fetches and keep main's 30 s TARKTEE_FETCH_TIMEOUT_MS. Drop this
branch's 15 s constant and its import, which auto-merged into a
duplicate export. Keep both sets of Estonia tests and drop the 15 s
claim from the changelog fragment.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
page.waitForFunction polls on requestAnimationFrame by default. In a
minimised or hidden headful window rAF stops while timers keep running, so
each 120 s slice only checked the predicate when it started: a drain that
finished in the last slice was recorded as a FAIL on the sign-off path.
- waitForPredicate replaces waitForFunctionSliced and the other three
waitForFunction calls: one short page.evaluate per tick (1 s for the
drain, 250 ms elsewhere), a monotonic performance.now() deadline, retry
across a context swap, and a throwing predicate fails the run instead of
silently spending the budget.
- One in-page window.__qaRenderFrames(count, capMs) helper serves both the
gizmo-drag and the sibling-owner frame waits.
- The sibling pick counts as a stalled renderer only at 0 of 3 frames, so a
slow SwiftShader frame still reports a real miss; the record flattens and
prints the frame count once.
- Review screenshots log and continue when a capture fails.
- Launch with pipe: true (no localhost CDP port) and protocolTimeout
300_000 like the other harnesses; drop the occlusion flag that puppeteer
already passes by default.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
When the sibling-owner pick misses after fewer than 3 rendered frames, the
cause is a stalled renderer, not a pick regression. Record it as a failure
on the headful sign-off path and as inconclusive under headless GL, with
"renderer stalled: N/3 frames" in the detail. A missed pick after 3 frames
still fails in both modes. Also corrects the catalog-size comment (~3,700).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The headful drain budget is uncapped (camCount * 800 + 30000 ms, ~50 min
at N≈3,700), but each page.waitForFunction is one CDP call that puppeteer
aborts after its 180 s default protocolTimeout. The .catch then recorded
"timed out waiting for drain on the real-GPU sign-off path" after ~3 min.
- Poll the drain in <=120 s slices (waitForFunctionSliced) so no single
CDP call outlives protocolTimeout, whatever the catalog size.
- Launch with protocolTimeout 240 s, headroom above the largest slice and
the 60 s fixed waits.
- Bound the sibling-owner 3-frame postRender wait at 10 s and report the
frame count, so a renderer that stops producing frames fails with
evidence instead of hanging the evaluate.
- List --disable-backgrounding-occluded-windows explicitly. Puppeteer
25.10 already passes it by default, so this documents the headful
contract rather than changing behaviour.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Conflicts: the Street Level and Gemini voice strings both appended to the
es/ru catalogs, and both sides' modules to package-boundaries.json — kept
both. scripts/qa-panel-resize.mjs stays deleted (floating panels removed).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Away from the galactic plane the ESO panorama keeps a mottled glow of
unresolved stars and dust. With the black level at the darkest quarter of
the sky and a hard cut, it showed as grey clouds with black holes around
the globe. The black level now sits at the median sky and eases in with a
soft knee on luminance, keeping each pixel's hue: high galactic latitudes
are black, the band keeps its glow. Faces shrink from 247 to 85 KB.
The changelog fragment also notes the anime fade and the new asset weight.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- VisualEffects.destroy() puts skyBox.milkyWayVisibility back to 1, as it
already restores the borrowed bloom stage, so a surviving viewer does not
keep a faded sky.
- Rename scripts/qa-anime-space-mask.mjs to qa-anime-sky.mjs: rewrite the
header for the current approach, drop --space-expr and --repo, import
puppeteer as a package, reject unknown cases and count the milkyway case
as reached only while the anime stage is on.
- Put both sky QA scripts under format:check.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
in push-to-talk, answer a Gemini tool call that arrives with no input, reply
or tool result outstanding with a failure instead of running it, and refuse
every other call in the reply it starts, including one after unprompted
speech; the reply stays tracked so the next input retires it and its late
boundary cannot close the user's turn
in push-to-talk, a caption that arrives after its turn ended no longer opens
a reply, so the next request is not retired with its tool calls discarded
open mic cannot tell either case from a new reply and stays unchanged
check packet ownership after the narration callback before queuing tools,
and raise the per-connection call history to 4096 IDs
move the pointer turn event, context item and announcement into
pointerContext so the OpenAI and Gemini controllers share them, and drop the
Gemini token route's copy of the shared exact-Origin comparison
- The selected sequence is drawn past the per-tile caps again, and its
highlight takes every loaded part, drawn or not: in a dense tile a
clicked Mapillary or Panoramax line kept its cones but lost its line.
- photoViewer tries :fullscreen and :popover-open one at a time, each in
its own try: Safari before 16.4 parses neither, and the fallback threw
on every flat KartaView or Panoramax photo.
- A lit Street View off Google 3D says it needs the Google 3D map again,
instead of "every provider is switched off".
- Chips carry their provider colour again (--chip-color, as on main).
Each comes with the test that reproduced it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The far-plane mask in the anime shader read the post-process depth, which
cannot tell the sky from far geometry: with a layer that clamps the near
plane to 0.1 m (satellites) the whole Earth lost its cel bands, keyless
terrain above the ellipsoid horizon did too, and the atmosphere, Sun and
Moon stopped banding.
Restore the anime shader from main and fade the Milky Way glow with the
style instead: VisualEffects sets skyBox.milkyWayVisibility to
1 - anime intensity. scripts/qa-anime-space-mask.mjs checks all three
cases headless and that the anime sky shows no bands.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Seven tests built terrainHeightsProxy() with no cachePath, so the proxy fell
back to this checkout's .gev-cache/terrain-heights.json. A point the app had
already cached there answered a request the test expected to reach the
stubbed upstream: after running the dev server near Austin, the cross-site
test failed with 1 !== 2.
isolatedCachePath(t) gives each test a mkdtemp cache file removed in
t.after, as the over-large cache test already did; that test now uses it too.
Fixes#1242.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BNTtHAa65nobzJ7jp2Ltr4
On CI's SwiftShader the ground-clamped cones take a few frames after a
sequence is selected; a single pick search found none.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Default backoff is 30 min, longer than the 15-minute catalog refresh,
so a failing Kartverket costs one attempt per backoff, not per refresh.
- A point a 200 answer leaves out is cached as no height (6 h TTL)
instead of being asked again, and waited on, every refresh.
- Only 408, 429 and 5xx stop the lookup; any other 4xx marks that
request's points as no height and the lookup carries on.
- Each request has its own 10 s timeout inside the shared 30 s deadline;
a lookup cut short only by the deadline resumes without a backoff.
- The lane waits only on a cold cache (>10% of cameras missing); a few
new cameras or expired misses are looked up in the background.
- applyVegvesenGroundHeights skips cameras without finite coordinates,
so it really never throws.
- clearKartverketHeightCache bumps a generation, so a lookup started
before a clear cannot write to the cache, backoff or pending slot.
- Credit and DATA_SOURCES note the modification (water set to 0 m,
rounded to 0.1 m) as CC BY 4.0 asks; docs match the stop rules.
- Tests for the deadline being shared, per-request timeouts, 503,
oversized and non-JSON bodies, 4xx, left-out points, Retry-After end
to end, overlapping refreshes, the warm-cache wait, bad coordinates,
the cache-clear generation and the loader's wait budget.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- A background catalog refresh that rejects now keeps the old catalog for
another TTL, as an empty refresh already did, so a refresh that keeps
failing is retried once per TTL instead of on every request.
- Trim notes are keyed by the source list they describe, and /sources asks
for the list it served, so a request answered during a refresh never pairs
the old cameras with the new list's notes.
- Tests: a failed background refresh (one warning, no unhandled rejection,
no retry before the TTL, trim notes stay with their list) and an empty
background refresh (old catalog kept, no upstream fetch before the TTL).
The race assertion's message now names the failure; its timer is cleared.
- The Tarktee test no longer imports the new constant, so on main it fails
on its own assertion instead of taking the whole file down.
- precompute-cctv-heights waits 60 s for a cold catalog, past Tarktee's 30 s.
- CURRENT-STATE notes the 30 s bound and the retry spacing.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Assert the key never reaches the camera records sent to the browser or
the "Loaded" log line.
- Count fetches in the keyless test instead of relying on assert.fail
inside the mock, which the loader's catch would swallow; a
whitespace-only key is treated as missing.
- Cover the key hint for 400/401/403 and its absence for 429/500/503,
and log the failure as one string without a trailing empty argument.
- Add _resetOntarioKeyWarningForTest so the logged-once check no longer
depends on test order.
- Read CCTV_ONTARIO_MAX_SOURCES from the same env as the key, and keep
console output out of the suite.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
refreshCctvFocusStyles passes cameraColorFor as baseColorFor, but only
refreshCoverageStyles was tested. The new rendering.test.mjs runs the
focus pass through createRendering with stubbed focus services and
checks that an idle video camera fades from violet, a still from cyan
and the active camera from amber. It fails if the focus callback goes
back to the old active/idle check. Also adds the missing semicolon on
the policy.js import in cctv.test.mjs.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Drive drawProjectionFrame with a fake runtime and a recording canvas:
a 640x480 still draws at its fitted rectangle over the matte, the same
pixels at the same size skip the redraw, and the same pixels at a new
aspect redraw at the new fit. The last case fails on the pixel-only
signature; the first fails if drawImage stretches to the canvas again.
Expected rectangles now read the canvas size from policy.js.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Pass the custom sky box to the Viewer instead of `skyBox: false`, which
also stopped CesiumWidget from creating the Sun and the Moon. Guarded by
scripts/qa-sky-sun-moon.mjs.
- Outside the IAU 2006 tables (before 1975, after ~2050) the preload
resolves without data; remember that span and stay on TEME instead of
preloading and rendering every frame.
- The anime style no longer cel-shades the far plane, where four bands
turned the faint Milky Way into blotches.
- Paint the planets out after the star-removing opening and with the
ring median, so no brighter plateau is left; faces rebuilt.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The parity test compared getWorldPacket with voice, which now calls
getWorldPacket. Compare both against a fixed payload captured from main's
get_current_view_state before the extraction, key order included, and
cover a non-finite now.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A default parameter only covers undefined, so now: null aged every feed
against the epoch and reported stale data as just now.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Four open PRs prepended their notes at the top of CURRENT-STATE.md, so
merging any one of them made the others conflict. Place this note in its
own section instead.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- State FL511's terms the same way everywhere: individual, personal use
only, and any other use (a public deployment included) needs FDOT's
express written consent. .env.example, README, dataCredits, the source
licence string, the catalog comment and the changelog fragment said only
"individual, non-commercial use".
- Page the ArcGIS catalog on exceededTransferLimit alone and advance the
offset by the rows each page returned, so a lower server-side max
record count no longer cuts the catalog short at the first short page.
- Order pages by OBJECTID_1 (this layer's object id; a plain OBJECTID is
rejected with HTTP 400) so offset paging is stable.
- Raise the page ceiling to 40 and warn when it is hit.
- Tests: short pages with exceededTransferLimit keep paging, orderByFields
is sent, and a service that ignores resultOffset stops at the ceiling.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The globe renders in the viewer's own browser, so a codespace does not
remove the need for local graphics. Retitle Path 4 accordingly.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
aircraft_in_area stats summarized altitude over airborne aircraft but
speed over every match, so parked aircraft pulled the mean speed toward
zero. Both figures now use the same airborne subset, and the on-ground
fixture is stationary so the test can tell the difference.
Reserved AIS ship-type codes 100 to 255 now read as unknown, as type 0
does, instead of showing up in the breakdown as their own type.
Adds tests for an area where every aircraft is on the ground and for
find_vessel type and destination normalization, and notes in the
changelog fragment that every vessel answer is now normalized.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
runError doubled as the failure flag, so a phase that threw a falsy
value was still masked by a final release error (ianblenke's nit).
Track failure with a boolean and cover it with a test. Make the
changelog fragment a wrapped bullet like the other fragments.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
#686 moved its changelog entry from CHANGELOG.md into
changelog.d/686-terrain-heights-bounds.md. Take #686's side so this
branch no longer edits CHANGELOG.md directly and no longer conflicts
with #686.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Attach the background-refresh warning where the refresh starts, so a
failed refresh logs once instead of once per request served meanwhile.
The test now checks that requests during a refresh share it, and turns
the hang off before releasing the fetches so a retrying loader can't
stall it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Mapillary was never in DATA_CREDITS (since #768), and Google Street View,
KartaView and Panoramax came without entries. HAL-530.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Read ~1,800 national freeway cameras at runtime from the Freeway Bureau's
keyless MOTC 2.0 CCTV.xml table. Each published MJPEG stream maps to the
same camera server's single-JPEG path; only those exact HTTPS pairs on the
four freeway.gov.tw camera servers are registered. On by default,
CCTV_TAIWAN_ENABLED=0 skips it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Main gained three imagery providers (Google Street View, KartaView,
Panoramax), shared line-coverage, cone and photo-viewer modules, one source
lit at a time and the translated interface. Their shared modules still
carried what this branch removes, so the merge ports them onto it:
- lineCoverage.js is this branch's coverage.js made provider-neutral
(draped lines near the ground only, the selection drawn over them, key
and rate-limit blocking); sequenceCones.js is sequences.js the same way.
Mapillary, KartaView and Panoramax all build on them; Mapillary moves
back to providers/mapillary/ beside the others.
- No ground casting, mesh sampling, surface modes, FOLLOW, legend, prewarm,
orbit overview points (Panoramax's density dots included), stale-tile
swaps or water-filled point budget come back.
- Kept from main because the new providers need them: requiresMapStack and
attachMapStackController (Street View on Google 3D only), ground clicks,
openNearest with an optional provider nearestImage, openImage {frame},
reportError, the view-cone marker (now an SVG glyph), filters /
coverageLabel / sinceBasis, OPEN STREET VIEW, keyAlternatives and i18n.
- photoViewer.js treats the viewer as expanded when its wrap is full screen
or the iPhone popover, as EXPAND now is.
- The fixture browser gate keeps its 8 steps and adds one each for
KartaView and Panoramax; catalogs, docs, boundaries and format scope
follow.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Cesium's default Tycho-2 sky box is six 1024² JPEGs stretched 3–6× around
the whole-globe view, so space read as an over-compressed JPEG (HAL-627).
The scene now draws its own sky in the Scene#skyBox slot: the 9,096 stars
of the Yale Bright Star Catalogue as point sprites sized in CSS pixels
(magnitude → width and opacity, B−V → colour), over a soft, star-free
Milky Way cube map built from ESO/S. Brunier's panorama (CC BY 4.0). Both
layers share one ICRF→Earth-fixed rotation at the scene clock, with the
TEME rotation filling in until Cesium's IAU 2006 tables load.
Assets total 482 KB, built by scripts/build-bright-stars.mjs and
scripts/build-milky-way.mjs. Frame time and the idle render governor are
unchanged.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
An expired catalog made every /api/cctv request (camera list and frames)
wait for the full refresh, which lasts as long as the slowest of the live
packs: Tarktee's DATEX feeds took 2 to 16.6 s across ten requests. With
Tarktee's timeout raised to 30 s, that wait could reach 30 s at each
15-minute refresh. The cached list is now returned while a single-flight
refresh replaces it in the background; only a first load, with nothing
to serve, waits. A background refresh that throws is logged.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The Estonian Transpordiamet feeds answered in 2 to 16.6 s across ten
requests (120 and 160 KB), and both must arrive for any camera to load,
so the shared 15 s catalog timeout dropped the whole pack on about a
third of loads ("Tarktee DATEX download error: The operation was aborted
due to timeout"), for the 15 minutes until the next catalog refresh.
TARKTEE_FETCH_TIMEOUT_MS gives the two requests 30 s; other packs keep
15 s.
Live: 175 cameras in 3.3 s.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Reject non-numeric and out-of-range feed coordinates before they reach the
map or bike-share queries. Preserve numeric strings, aliases and zero.
Cover malformed records at the parser and public query boundaries.
Signed-off-by: Bortlesboat <[email protected]>
Review fixes for #1228: CURRENT-STATE.md and the constants comment no
longer call Ontario 511 keyless. DATA_SOURCES.md notes Ontario 511's
Developer's Access Agreement (511on.ca/developers/resources): the data
stays under the Open Government Licence - Ontario, key requests are
reviewed case by case, attribution by link is encouraged and the logo
rules don't apply because the app shows none, and the "as is" disclaimer
should be passed on, which the credit now does.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A wrong or revoked key logged only "download failed: 400"; a 400, 401 or
403 now adds "(check ONTARIO_511_API_KEY)". The README's CCTV row listed
Ontario among the default cameras with no mention of the key it now
needs.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
511on.ca/developers is a 404. Sign up at 511on.ca/my511/register, then
request the key while logged in at 511on.ca/developers/doc (both checked).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The Ontario 511 camera list answered 400 "Invalid Key" on every catalog load
("[CCTV] Ontario 511 camera download failed: 400"), so the pack loaded
nothing. 511on.ca's developer docs now say the API "Requires a developer
key" (free, from 511on.ca/developers; 10 calls a minute). The loader reads
ONTARIO_511_API_KEY and sends it as `key`; without it the pack is skipped
with one log line instead of a failed request on every load. The key is
never logged. .env.example and DATA_SOURCES no longer call the endpoint
keyless.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The card's fire pattern is written by hand against the producers' template,
so a format change upstream would silently show English. Both locale tests
now pass the label the layer actually produced through shownEntityLabel()
under ru, and set the locale inside the try that resets it.
Main now takes pending entries as changelog.d/<pr>-<slug>.md
files (#787), so the entry leaves CHANGELOG.md for changelog.d/686-terrain-heights-bounds.md.
AIS sends ship types as ITU-R M.1371 codes, so vessels_in_area grouped
"70", "74" and "79" as three types, and type: "tanker" matched nothing on
feeds that send codes. Codes now read as the family the vessel cards already
show (CARGO, TANKER, TUG), for rows, group_by and the type filter. The code
table moves to src/data/vesselTypes.js, which has no i18n import, so the
server-side tools can use it; vesselLabels re-exports it.
A destination with no letters ("0", "---") now counts as unknown.
Live, Singapore: "most by type: CARGO (3), TANKER (3), PASSENGER (1)"; "how
many tankers near Singapore" found 3.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ICAO flight-plan Item 7 caps aircraft identification at seven characters, so
the flight number after the designator is one to four characters, not five.
Philippine RP-C1234 and Cuban CU-T1240 registrations read like a designator
plus a flight number once the hyphen is dropped; they now count as unknown.
Cubana's CUB123 still counts as CUB.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
OpenSky, the default civil feed, reports no operator, so group_by: operator
said "no operator reported". aircraft_in_area now also takes
group_by: 'airline', read from the ICAO designator in each callsign: three
letters followed by a flight number that starts with a digit (SWA1234,
UAL12A). Registrations used as callsigns (N123AB) count as unknown. Rows are
unchanged; the designator is derived for the breakdown only.
Live, Texas: "785 aircraft ...; most by airline: AAL (89), SWA (65), UAL (52);
283 unknown."
Co-Authored-By: Claude Opus 5.5 <[email protected]>
World Packet #880 removes the duplicate tracking table from gevActions; the Cockpit refusal source-scan test should inspect the actual exported source of truth instead of assuming the previous file position. Fix failing Node 24 exact-head CI without duplicating runtime definitions.
Reuse one context/contacts/tracking owner in voice and World Packet. Compare voice outputs after stripping presentation fields; remove the unconsumed fingerprint API pending an actual caller. Document the exported packet boundary and reject null options safely.
Review guidance: asturksever on origin #880; Windows parity diagnosis by quanticsoul4772. Existing source ownership retained.
Reconcile #880 with current main. Delegate the view-state builder while keeping enabled-only layers, disabledLayerCount, provenance, context semantics, and existing voice behavior. Add parity and roster-read regressions; retain package export, boundaries and changelog fragment.
Preserve original GEV voice, layer and packet contributor credit.
Carry reviewed #816 onto current main without conflicts; preserve i18n updates, replace obsolete changelog edit with a fragment, and add a constructor-wiring regression alongside the owner and rail tests.
Credit: raysung-2026 (report), asturksever (review), quanticsoul4772 (Windows diagnosis).
Review follow-up on #975. The shared same-site gate refuses any forwarding header, which is right for routes that spend a key, but terrain is keyless and needed for rendering: behind the reverse proxy the README documents, every /api/terrain/heights chunk got 403 and fell back to the geoid. Terrain now uses admitSameSiteKeyless, which keeps the Sec-Fetch-Site and exact-Origin checks (comparing Origin against a single X-Forwarded-Proto scheme) but does not refuse forwarding headers. The shared admitSameSite gate is unchanged for every other route. The changelog entry moves to changelog.d/975-terrain-cross-site-gate.md.
Co-authored-by: daikaginza <[email protected]>
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.
The fire and camera labels in the context store, getStrongestFire() and
getSelectedInfo() were translated at the source, so the voice model got
"Пожар · FRP 12 МВт" or "Cámara ALPR" in track_entity,
get_entity_context and pointer_context while every other entity name in
those payloads is English. Keep them English, as before the interface
was translated; the voice card translates them where it shows them.
The pointer chip, its live-region line and the get_entity_context card
title now translate the English entity labels the FIRMS and ALPR layers
register ("Fire · FRP {frp} MW", "ALPR camera"). Other names, such as
callsigns and place names, show as they came.
summarizeAwarenessCohort() translated `reason`, which is retained in the
awareness context snapshot. Mark the four reasons with msg() instead; the
awareness panel already shows them through translateKnown().
The Local ADS-B layer copied the SDR controller's `message`, which is
Radio card text in the UI language, into its `error` stat. The controller
now publishes an English `errorText` next to the translated `message` for
every receiver error, and the layer reports that. A receiver is any
object with getState(); one that does not publish errorText still shows
its error from `message`, as before. The Radio card text is unchanged,
and the Layers panel still shows the error translated.
The Directions row errors (pointer blocked, no route, routing timed out,
no response from the routing service) and the route request failures
("Routing failed: {error}", "{error} — try again in a moment",
"Routing unavailable (HTTP {status})", the rate-limit and fallback
texts) were translated inside getStats(), so the layer snapshot that
voice tools and the analyst read carried Spanish or Russian text.
Fixed texts are marked with msg() and the interpolated ones are built
in English. The Layers panel translates them where it shows them,
through a ROUTING_ERROR_PATTERNS block in statusText.js that the
proxy's refusal texts move into. English reads as before. In other
locales two things differ slightly: a proxy text inside "Routing
failed: X" that is not one of the known refusals is now looked up in
the catalog instead of shown as given, and the "— try again in a
moment" shape is recognised on every layer's error, not only on
Directions.
Move the changelog entry into changelog.d/1056-area-tool-breakdowns.md
(upstream's one-file-per-PR rule, #787) and leave CHANGELOG.md as on main.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
AIS type 0 means "not available" (ITU-R M.1371), but the source keeps it
as '0', so group_by: 'type' listed "0" as a type and understated
unknown_count. vessels_in_area rows now report it as null, and a
regression fixture mixes type 0 with Cargo and Tanker.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
keep typed and tool-follow-up responses attached to their original requests
reject duplicate events before they affect newer work
preserve newer input and Radio handoffs during old callback cleanup
integrate the tested main snapshot without dropping existing history
main now rejects out-of-range coordinates at the proxy itself (19fcc00b), the
same check this branch added. main's version is kept, and the changelog entry
no longer claims that part; the cache cap, file ceiling and rate limit remain.
Preserve upstream changes, restore the reviewed Radio ownership regression,
and retain reporter raysung-2026 credit and #804 provenance.
Reconciles upstream main 81cf437 and PR #816 head 702ecc3; no force push.
Keep this branch's CHANGELOG entry above upstream's; upstream main no longer lists the GBFS station-name entry an earlier merge carried here.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Resolve the CHANGELOG.md conflict by keeping both new entries: this
branch's group_by/stats entry above main's GBFS v3 station-name entry.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Draped lines build in a worker after their tile decoded: on a slow CI runner
the single pick scan ran before the line existed and the gate failed at
"a sequence line across the middle of the view". The scan now retries for up
to 30 s.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Coverage lines are 2.5 px wide and there is no hover cue any more: with the
default 3 px pick window a click just beside a line selected nothing.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Review of #974 (asturksever):
- After a 429 the other tiles of the view, and the fetches queued behind the
first, still went to Mapillary. The proxy now answers 429 itself until the
wait is over, scoped to the token that was refused.
- A Retry-After sent as an HTTP date read as 60 s; it is now the seconds
left, with the same 10-minute cap.
- A body that failed to trim (up to the 48 MiB cap) could fill the 120-entry
cache; anything over 4 MiB is served but not kept.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
main grew the Street Level provider registry while this branch removed it
(#982–#985: injected providers, an empty composition, the `r` switch field).
The registry, params, uiState and pick router come back as main has them and
the slimmed layer sits behind the provider contract:
- The core (index.js) drives providers as on main, without surface modes,
ground casting, FOLLOW, prewarm and nearest-image lookups. A provider's key
status is asked with a 10 s timeout and asked again on a switch-on while no
key is known.
- Mapillary is mapillaryProvider.js over the slim coverage, sequences and
viewer modules, which read the filter and the active flag from the provider
context.
- The viewer host mounts one provider adapter at a time, no prewarm.
- The panel has a chip per provider again (the pill switches the layer);
no legend, EXPAND stays the Fullscreen API.
- The contract drops what no provider implements: groundCaster, meshSampler,
getSurface/setSurface, nearestImage, prewarm, reportError, the legend and
surface fields of the UI snapshot.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Associate expanded cable and landing entities through a copied source index,
retain one reference stem per feature, and cover duplicate IDs, bundled
segments and frozen provider inputs with real Cesium regressions.
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.
Node's network-family auto-selection gives each resolved address a short
attempt timeout. On hosts where IPv6 is unreachable, a slow IPv4 handshake
is abandoned for the IPv6 address, which fails at once, and the request
reports only `fetch failed`. Retry such failures once with a single IPv4
connection within the same deadline, and log the per-address connection
errors (never the keyed URL). Node 26 wraps the AggregateError one level
deeper, so both shapes are recognised.
Fixes#68
Retain upstream changes to the panel controller, formatting scope, changelog and docs. Preserve the #804 ownership fix and its rail restoration regression (original report: @raysung-2026).
Close the overlay when adaptive layout restores an auto-collapsed Context directly. Regression covers the reachable rail reflow path; changelog credits the original report by raysung-2026 (#804).
Voice leaves aircraft_in_area and vessels_in_area out, so the changelog no
longer suggests spoken questions gain breakdowns. It also stops leading with
operators, which the default civil feed never reports.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
OpenSky, the default civil feed, reports no operator or type code, so a
breakdown by either put every aircraft under unknown while the summary said
nothing. The summary now says "no operator reported" in that case, and
otherwise appends the unknown count after the largest groups.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
aircraft_in_area and vessels_in_area return at most 200 rows, so a client
asked which operators dominate an area, or the average altitude there, could
only estimate from a truncated sample. Both tools now take an optional
group_by (aircraft: operator, origin_country, type_code; ships: type,
destination) and stats (airborne altitude and speed for aircraft, speed for
ships). Each is computed over every match before rows are capped. Breakdowns
list the ten largest groups plus other and unknown counts, and the summary
names the three largest.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Follow-up to #686 requested on #923. The route's per-client limiter keys on the socket address, which for a cross-site <img> is the visitor's own browser, so any web page could fill the bounded cache with junk keys and spend the visitor's 90/min quota, leaving the app's own terrain lookups on 429. Gate the route with admitSameSite before the limiter, like the Places routes.
Co-authored-by: daikaginza <[email protected]>
- No pointer cursor over coverage lines (no other layer has one); a click
test replaces the hover tests.
- No legend: one source, one colour.
- The on-globe credit lives in index.js; credits.js is gone.
- Billboard glyphs are SVG data URLs with the same geometry, so tests need
no canvas stub.
- The tile cache keeps the 120 most recently used tiles, same 24 h TTL,
instead of a byte budget; listTileLayers was test-only.
- providers/mapillary/ is flattened into the layer and its policy merged
into one policy.js; the source's method list is a literal in the catalog.
- One fakeCesiumViewer for the layer tests.
- The fixture gate keeps the steps the unit tests do not cover: coverage and
credit, a rejected key, line -> cone -> photo, close, a keyless page.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
No markup carries a drag handle, so nothing called it: panel position
controls keep only collapsed state and the layout-reset notice.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- A failed or missing key status is asked again on the next switch-on, and
the check gives up after 10 s instead of holding LOADING forever.
- Waiting for the key status reads as LOADING while the layer is on.
- Esc shrinks the photo in full screen too, before the panel's own Esc.
- Destroying the panel with the iPhone popover open leaves no popover behind.
- An empty view says whether the filter emptied it.
- A freed upstream tile slot passes straight to the oldest waiter.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- visibleBbox: no view-rectangle fallback or grid option; its one caller
always ranges the rays
- tilesForBbox returns the tiles; normalizeBbox takes arrays only
- viewerHost: no try/catch around adapter calls that catch for themselves
- coverage: counts come from the drawn sequences; no sequenceCount export
- one keyStatusLabel for the pill and the layer list
- no constructor source check, double-init guard or selectSequence /
clearSequence on the layer
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A client drops the tiles a camera move leaves behind; their fetches no
longer run on or hold the six upstream slots ahead of the tiles in view.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A release with a modifier held left the LEFT_DOWN/LEFT_UP flag stuck, and
right, middle or modified drags picked every frame. Track pointer buttons
instead, as main did, and pick a still pointer again on moveEnd.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Tiles a camera move leaves behind keep downloading after the client drops
them; the cap bounds their memory and quota, as main's proxy did.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The panel connected before the shell had its data manager, so it never
heard switch-on requests and a user switch-on left it collapsed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Three regressions from the slimming, found in review:
- Clean View, recording and the cockpit hid the old expanded viewer with
CSS; native fullscreen sits above all of that, so the photo stayed over the
globe (and in recordings). While expanded, the panel now watches the
<body> classes of those modes and leaves full screen when one starts.
- iPhone Safari has no element fullscreen, so EXPAND had disappeared there.
Where requestFullscreen is missing but popovers exist, EXPAND opens the
viewer as a top-layer popover sized to the viewport (it stays in the
panel's DOM; the popover attribute is set only while open, since a closed
popover is hidden). Checked in Chrome: the popover fills the viewport over
the panel's backdrop filter.
- Without the provider chips, a layer that was on with Mapillary switched off
by a share link or tool (`0.m.0`) showed ON and drew nothing. The pill now
reads OFF with a hint, and pressing it brings Mapillary back without
switching the layer off.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Two regressions from the slimming, found in review:
- Google 3D hides the globe, so groundUnderCamera returned null once the
ground caster was gone: in high cities (Denver ~1,600 m) the camera read
kilometres too high, the coverage zoom came out coarse and the visible box
slid kilometres ahead. With the globe hidden (or not yet loaded) the
ground now comes from the rendered surface (scene.sampleHeight), checked
for plausibility and ignored when above the camera. The plausibility check
is shared with photo framing (plausibleSurfaceHeight, sampledSurfaceHeight).
- Tiles were ranked from the camera's ground point alone, so a tilted view
from a few kilometres up (z13) could miss the tile at the centre of the
screen. They are ranked from viewFocus: halfway between that point and the
ground at the screen centre (or the nadir when the centre ray misses).
A test pins the 3 km / 70° case: the nadir ranking leaves the centre tile
out, the focus ranking loads it.
Also ignore .playwright-mcp/: its browser console logs can carry tokens.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The browser gate is one pass over the remaining flows: rail strip and
expand, coverage with fixtures, the ON/OFF pill, 360° and SINCE filters,
KEY REJECTED, nearest photo with caption, × close, phone fit, keyless
KEY REQUIRED on a second page and no page errors. Panel controls are
pressed through the DOM; pick-point search, line/cone clicks, the
framing-cancel, reload/share and status-route probes, the second
viewport and pointer-settling retries are gone (unit tests cover them).
Tests that pinned constants, copy, CSS or source text, or repeated
another test, are removed or merged. CSS loses an unused variable and
duplicate rules. Docs describe the slimmed feature; unused exports and
the layer's dead setCoverageFilter/refreshCoverage surface are removed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Drop the disk tile cache (.gev-cache/mapillary/tiles, background sweep,
size cap, legacy retrim), the server-side 401/403/429 holds, the upstream
concurrency semaphore and per-waiter abort bookkeeping. The client already
pauses on 429 and stops on a rejected key.
Keep: server-side token, {configured}-only status, strict coverage path
validation (z11-14), image-layer trimming, same-site gate and per-IP limit,
HTTPS redirect pinning to the tile origin, the streamed size cap, the error
mapping the client reads, a 24 h byte-budget LRU memory cache, in-flight
coalescing via the shared coalesceProxyRequest, and browser Cache-Control.
Routes and constants fold into mapillary.js and mapillary/tiles.js.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The layer now owns its Mapillary parts directly: the provider registry,
pick router, multi-provider UI summary, params codec helper and provider
context are gone. createStreetLevelLayer({ source }) builds coverage,
sequences, the nearest-image lookup and the MapillaryJS viewer itself; a
single providerOn switch keeps the share link's `mapillary` option (off
draws nothing), and getParams/setParams keep { mapillary, pano, sinceDays }.
No z0-5 overview points: above 60 km nothing is drawn and the panel says to
zoom in; the proxy now serves only coverage z11-14. Tiles come from the
visible ground within range of the camera, ranked from the ground under it
(no line-of-sight ranking or near-range padding). MapillaryJS loads on the
first photo; viewer prewarm is removed. Key status, a rejected key and a
rate limit share one `blocked` state on coverage.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
FOLLOW is gone: no setFollow, follow state, map-stack availability or
handoff listener. Opening a photo still frames it with one flight through
the application's deferred navigation (begin/reassert), so a slow photo
never overrides newer navigation and tracking is released as before.
Ground casting is gone: groundCast.js, meshSampler.js, the surface mode,
the draped-to-cast swap and remesh. Coverage lines are always draped
GroundPolylinePrimitives (ClassificationType.BOTH); cones and the marker
clamp to the ground and keep the horizon cull. The terrain/meshFloor/ground
services and meshFloor.visibleTilesetLoaded are no longer needed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
EXPAND now uses the browser's Fullscreen API on the viewer element, which
stays in the panel; the hand-built modal (re-parenting to <body>, inert
siblings, focus trap, document-level Esc/Tab, Clean View/recording/cockpit
hide rules) is gone. The button is hidden where the API is missing.
The FOLLOW button and the PROVIDERS chip row are removed. The header pill is
the only layer switch and re-enables a provider switched off elsewhere; under
KEY REQUIRED the error line says which key to add. Chip colour support in
chipGroup, dead CSS and tests pinning the removed behaviour go with them.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
PR #768 bundled a portable-panel windowing system with the Street Level
layer. Remove it so CCTV and Street Level are ordinary docked right-rail
panels again, as on main before the merge.
Removed:
- header drag to float, 8-way edge/corner resize, double-press snap-back,
dockOnCollapse, command-dock clearance, float hint toast, floating z floor,
persisted floating geometry and ResizeObserver re-clamping
(panelPositionControls.js restored to its pre-#768 version)
- src/ui/panelResize.js and its tests, panelDock.test.mjs
- onPanelResized/dockPanel wiring in PanelChrome, the application shell and
StreetLevelControls (EXPAND/SHRINK no longer docks)
- :not(.panel-floating) guards and .panel-floating/.panel-portable/resize CSS
- qa:panel-resize, qa-panel-resize.mjs, qa-panelDrag.mjs, their tests, the
CI "Portable panel gate" step, QA_FAIL_ON_RETRY, and the Street Level
gate's float/resize/dock steps
- floating-panel docs in README, CHANGELOG, CURRENT-STATE, KNOWN-ISSUES
Kept: the Street Level panel as a docked rail panel (share/cockpit/rail
lists, layout controller, rail and Cyber CSS, EXPAND viewer), and the
independent right-rail alignment fix (settledLeftRailTop, 9dde7b4).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Closesbilawalsidhu/gods-eye-view#155. The first Italian cameras: the 13
webcams along the A22 from the Brenner Pass to Modena.
The camera list is one keyless request to the Open Data Hub (NOI Techpark)
tourism API, which republishes Autobrennero's own list with coordinates
and kilometre markers. Frames are the operator's stills, refreshed about
once a minute, and only each camera's own WebCamImg/km<N>.jpg is
registered. The catalog fetch refuses redirects and is size-capped.
The road climbs from ~20 m in the Po valley to ~1,370 m at the Brenner
Pass, so ground elevation comes from a per-kilometre table sampled from
the Copernicus GLO-90 DEM instead of one flat prior. Headings use the
id-hash fallback at low confidence. CCTV_A22_ENABLED=0 disables the pack.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Send 20 points per request, not 50: above ~20 Kartverket answers the
extra points from contour lines (hoydekurver) or not at all. At 20,
nearly every camera resolves from dtm1, so the null-retry pass is gone.
Eight requests in flight keep a cold lookup ~6 s.
- Cache misses: positions with no height are not asked again for 6 h.
- Transport failures: the first 429, 5xx or network error stops the
lookup with no retries, and no lookup starts until the backoff ends
(Retry-After when sent, else 10 min; bounded to 1 min-1 h).
- Budget the 12 s wait from the start of the loader, catalog fetch
included.
- Record answers only for the points each request asked about.
- Send the CCTV User-Agent; link CC BY 4.0 in the credit.
- Docs: bridge cameras sit at the water surface, below the deck.
- Tests: batching, 8-in-flight cap, request count on failure, the shared
deadline, Retry-After bounds, null TTL, backoff, the -20 m bound and
each water marker on its own.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The Vegvesen camera feed is 2D, so every camera shared one flat 150 m
ground prior and mountain-pass cameras could sit hundreds of metres below
the terrain where neither the point-height prior nor the ground snap
resolves. Each camera now gets its terrain height from Kartverket's
keyless point-height API (CC BY 4.0): ~840 of ~845 cameras.
- 50 points per request, six in flight; points a full batch answers null
for are retried once in batches of 10 (the API drops points from large
batches).
- Sea-floor depths (dybdekurver / Havflate) under bridges and quays
count as the water surface, 0 m.
- Heights are cached by position, so the 15-minute refresh only asks
about new or moved cameras. A catalog load waits at most 12 s; a slower
lookup keeps filling the cache for the next refresh.
- CCTV_VEGVESEN_HEIGHTS=0 turns it off; Kartverket is credited beside
Statens vegvesen in the attribution popover.
Follow-up to #929.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- 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
Vessels move only when a poll lands, yet enabling the layer held the
continuous render loop for its lifetime, so a parked vessels-only scene
rendered every frame. The layer now:
- requests a frame for every forced visibility pass (new data, selection);
- leaves one trailing frame when the camera moved inside the 800 ms
visibility cadence, so the view the camera stopped on is evaluated. It is
keyed to camera motion alone: the pass's own label publication requests a
frame, and scheduling on that looped at ~1.2 Hz;
- holds 'ais-vessels' only while focus emphasis is animating.
The 80 ms focus pass, which runs while anything is tracked, also skips hidden
sprites whose focus state is at rest (new spriteFocusAtRest) and whose alpha
is already 1: advancing them changes nothing, and with a 12k worldwide feed
they are most of the fleet.
Measured: a parked vessels-only scene (headless SwiftShader) went from
continuous mode and 28 frames in 5 s to idle and 0; horizon visibility had no
mismatches after the camera moved and settled. The focus pass over 12,000
vessels (11,353 hidden) went from ~4.0 ms to ~1.2 ms.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Every distinct 5-decimal point was a permanent in-memory entry, a
permanent line in an uncapped `.gev-cache/terrain-heights.json`, and a
live upstream call. Nothing limited the rate, and coordinates were
checked for being finite rather than for being on Earth, so an
out-of-range pair was still a valid cache key.
Four bounds, each following a pattern this repo already uses:
- WGS-84 range is enforced in `parseTerrainPoints`, at the edge, so no
later stage has to. Poles and the antimeridian still parse.
- The point cache holds at most 50,000 entries and evicts oldest-first,
the shape `trackBackfillProxies` uses. Sized from measurement: this
checkout's cache file held 6,189 entries in 1.0 MB of real use, so the
ceiling is eight times that working set. Terrain does not move, so an
evicted point costs one refetch and never a wrong answer.
- A cache file above 24 MB is not read back, mirroring
`rocketLaunchesProxy`. An unmeasurable file is not treated as an
oversized one: without stat, readFile decides, exactly as before.
- The route carries an always-on limiter of 90/min per client, 270
global — the `/api/overpass` band, and an order of magnitude above
measured demand (a full test:track run drove 10 requests, peak 8/min).
The ceilings are constructor options so they can be exercised as
behaviour rather than asserted as constants; production passes none.
They are destructured in the body because `proxyErrorResponses.test.mjs`
slices these proxies to the first column-zero closing brace.
Two independently toggleable open-data raster overlays — OpenSeaMap
sea marks (buoys, lighthouses, ports) and OpenSnowMap ski pistes —
drawn as Cesium.ImageryLayers directly on the Cesium globe surface.
Same "small array of descriptors" shape as MAP_STACKS in
mapStackController.js.
Visible on the osm/bing-aerial/bing-labels map stacks and invisible
(not broken — the layer's stats say so in plain text) on the default
photoreal stack, since Google Photorealistic 3D Tiles render with the
globe hidden.
OpenRailwayMap and OpenInfraMap, also requested in #10, are
deliberately NOT included in this pass: OpenRailwayMap's public tile
server enforces a Referer allowlist scoped to its own site (confirmed
by direct request — every other Referer, including none, 403s), and
OpenInfraMap serves MapLibre GL vector tiles, not raster XYZ — that
would need a genuine vector-tile rendering pipeline, not a
Cesium.ImageryLayer drop-in. Worth its own dedicated pass.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
# CCTV_TFL_ENABLED=1 # set 0 to disable Transport for London JamCams
# 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
# 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
@@ -221,6 +238,9 @@ 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_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
# CCTV_KINGCOUNTY_ENABLED=1 # set 0 to disable the King County (Washington) road cameras
# CCTV_KINGCOUNTY_MAX_SOURCES=125 # cap cameras loaded from the King County catalog
# CCTV_WSDOT_ENABLED=1 # set 0 to disable the WSDOT (Washington State) highway cameras
@@ -228,6 +248,7 @@ AISSTREAM_API_KEY=
# CCTV_VEGVESEN_ENABLED=1 # set 0 to disable the Statens vegvesen (Norway) road cameras
# CCTV_VEGVESEN_MAX_SOURCES=900 # cap cameras loaded from Statens vegvesen (default loads all ~850 live; max 1000)
# CCTV_VEGVESEN_VIDEO=1 # set 0 to show stills instead of live HLS for Statens vegvesen video cameras
# CCTV_VEGVESEN_HEIGHTS=1 # set 0 to skip Kartverket ground heights for Statens vegvesen cameras (flat prior instead)
# CCTV_VEGVESEN_URL= # override the Statens vegvesen catalog URL
# CCTV_NYC_ENABLED= # off by default; set 1 only if you hold a NYC DOT data-sharing agreement ([email protected]) for the camera feed
# CCTV_NYC_MAX_SOURCES=250 # cap cameras loaded from the NYC TMC catalog (nearest borough cores first)
@@ -237,14 +258,62 @@ AISSTREAM_API_KEY=
# CCTV_TORONTO_LIST_URL= # override the Toronto RESCU camera list URL
# CCTV_LITHUANIA_ENABLED= # off by default; set 1 to load Via Lietuva / Eismoinfo road cameras (photo licence unconfirmed; see DATA_SOURCES.md)
# CCTV_LITHUANIA_MAX_SOURCES=400 # cap Lithuanian cameras; no API key required
# CCTV_FLORIDA_ENABLED= # off by default; set 1 to load FL511 (Florida) cameras (individual, personal use only; any other use, including a public deployment, needs FDOT's express written consent; see DATA_SOURCES.md)
# CCTV_FLORIDA_MAX_SOURCES=500 # cap cameras loaded from the FL511 catalog (nearest regional anchors first)
# 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_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
# endpoint set here. Nominatim stays off unless NOMINATIM_URL is set; Photon
# uses its public service when PHOTON_URL is unset.
# NOMINATIM_URL: search endpoint for last-resort place search and voice
# outlines (e.g. https://nominatim.example.org/search). A configured URL
# selects the endpoint explicitly. Unset, empty and invalid values disable
# Nominatim; nothing falls back to nominatim.openstreetmap.org. Use an endpoint
# you operate or are authorized to use. Answers are cached (outlines 90 days,
# places 30 days, no-results 24 h in .gev-cache/nominatim).
# Explicit public-service configuration retains per-install pacing and caps;
# these do not establish an application-wide allowance or authorize public use.
# NOMINATIM_URL=
# Public-instance requests per UTC day, outlines and search together. Not
# applied to your own NOMINATIM_URL.
# NOMINATIM_DAILY_CAP=50
# Public-instance pacing is shared through .gev-cache/nominatim/state.json. If
# the server stopped mid-request (or its disk filled), it leaves a
# state.json.dispatch.* marker and refuses public lookups ("unfinished durable
# Nominatim dispatch") for ten minutes rather than risk breaking the spacing;
# the next lookup after that removes the marker and resumes with fresh spacing.
# PHOTON_URL: keyless place search endpoint used by the browser
# (e.g. https://photon.example.org/api/). Unset uses photon.komoot.io; set it
# empty to disable. Only https endpoints are accepted (the page's
# Content-Security-Policy allows no plain-http origin); any other value disables it.
# PHOTON_URL=
# Optional: who may frame the app in embed mode (?embed=1), as CSP
# 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.
and Yemen products and the newest incident drop) from
github.com/s2underground/GhostMaps, with one chip per product and report
cards on click. `/api/ghostmaps` watches the repository's branch head every
15 s and pushes each new commit to open pages, which reload only the
products that changed (Black Pine Defense).
- Clear a temporary GhostMaps upstream error and notify open pages when the
repository becomes reachable again, including when no new edition was published.
- Release packed-image resources when a load is cancelled or fails, respect
injected or omitted GhostMaps sources, and wait for background cache warm-up
before removing provider test fixtures.
- Include Simplified Chinese translations and keep the combined voice prompt
within its existing size limit while retaining all spoken aliases.
- A Street Level provider whose `zoomFor` still answers `{zoom, kind}`, the
shape before the core went slim, draws its lines again instead of a silent
blank; an overview level or any answer that is not a whole zoom shows the
zoom-in hint (asturksever, #1288).
- Give ordinary sliders a shared native control and consistent track, thumb,
value, focus and disabled styling while preserving feature behavior and
specialized controls (mazy1998, #1287).
- The Street Level SINCE note sits on the value's line, in its type and a
size smaller: with KartaView lit it reads ANY DATE (by upload date). It
had lost its size variable in the Street Level slim-down and rendered
larger than the value, on a line of its own (asturksever, #1286).
- Keep aircraft route labels on the scheduled airport leg instead of accepting
its distant great-circle extensions, preserving generous endpoint tolerance
across the signed-angle branch cut (rudycelekli, #1284).
- Report cached CelesTrak satellite records as stale in the Data Layers row,
preserving separate core and dense catalog provenance through refresh,
removal and lifecycle cleanup (rudycelekli, #1283).
- Pause periodic network updates in hidden tabs; the overdue update on return restarts the polling schedule, so the next poll follows one interval later even when that update fails (asturksever, #1282).
- Report and guard browser visibility during Transit QA so hidden-page polling pauses cannot masquerade as playback regressions (asturksever, #1282).
- Require each live city to supply its own feed vehicles in Transit QA; retained vehicles cannot qualify another city (asturksever, #1282).
- Bound the Transit card-anchor probe by observed poll coverage instead of a fixed 20-second window (asturksever, #1282).
- Keep visible-page Transit probes focused through the browser testing API (asturksever, #1282).
- Add a real-browser regression gate for hidden-tab periodic work, explicit refresh, resume and teardown (asturksever, #1282).
- Restore minimized test windows before screenshots so native compositor pauses cannot stall browser acceptance (asturksever, #1282).
- Hold rendering only for animated content (asturksever, #1281).
- Exercise current vector road sources and published camera bounds in Traffic fixture QA (asturksever, #1281).
- Establish browser-level foreground visibility for Traffic QA and reject hidden-page samples (asturksever, #1281).
- Supply CORS headers on cross-origin traffic fixtures and report browser exceptions without disabling browser security (asturksever, #1281).
- Isolate the Traffic keyless browser scenario from the keyed page’s workers and intercepted cache (asturksever, #1281).
- Restore minimized test windows before screenshots so native compositor pauses cannot stall browser acceptance (asturksever, #1281).
- Intercept only Traffic provider URLs, leaving Cesium worker and module loading untouched (asturksever, #1281).
- Settle an unfinished Traffic height correction when the last animated point leaves, so the layer stops reporting loading once its render hold is released (asturksever, #1281).
- Throttle mission hover picks with a trailing check for the last pointer position, and clear stale cursors (asturksever, #1279).
- Reserve singleflight before disk cache reads (asturksever, #1278).
- Bound each height request to one upstream chunk (asturksever, #1276).
- Preserve enable during first-load replacement (asturksever, #1275).
- Release disabled weather camera listeners and make teardown resource leaks fail the browser probe (asturksever, #1275).
- Keep visible-page weather teardown probes focused through the browser testing API (asturksever, #1275).
- Scan the kml root delimiter in bounded time (asturksever, #1274).
- Retire unavailable history and keep local trails (asturksever, #1273).
- Normalize geocoder bias across the antimeridian (asturksever, #1272).
- Reject inherited style keys during restore (asturksever, #1271).
- Respect provider cooldowns and bound tle groups (asturksever, #1270).
- Coalesce refreshes and cool down opensky failures (asturksever, #1269).
- Normalize station tags and retain valid mixed rows; skipped malformed rows still report degraded coverage (asturksever, #1268).
- Correct Radio QA push-to-talk setup to target the mounted voice controller (asturksever, #1268).
- Honor Docker Compose trusted-peer and allowed-host settings from the shell
or project `.env`, and let `GEV_KEY_SETUP_TRUSTED_PEERS=none` disable gateway
access to Provider Settings before Vite starts (asturksever, #1265).
- Prevent malformed Overpass filters from blocking the local server, and reject
cross-site or excessive query requests before reading their bodies
(asturksever, #1264; fixes #1248).
- Release unread responses from the optional Google-token probe so keyless
browser validation can settle; the Overpass gate recognizes only that probe's
expected 404 as an unavailable capability (asturksever, #1264).
- Make tracking arrival checks respect the aircraft update cadence while checking
orientation on the first reveal, avoiding false failures at faster render
rates (asturksever, #1264).
- The terrain-heights bounds tests no longer read the checkout's
`.gev-cache/terrain-heights.json`. Each test gets a cache file of its own, so
`npm test` no longer fails after the dev server has cached terrain near
Austin (KnottyDyes, #1244).
- Replace the blocky JPEG sky around the globe with the 9,096 stars of the
Yale Bright Star Catalogue, drawn as sharp points where they are at the
scene time, over a soft Milky Way from ESO/S. Brunier (CC BY 4.0). The Sun
and Moon stay; the anime style hides the Milky Way. Assets: 296 kB (gekh,
#1243).
-`get_traffic_flow` flags stale cached TomTom tiles and reports the oldest
tile fetch time (`stale`, `oldest_tile_at`), with pending freshness metadata
released on failed or cancelled tile reads (quanticsoul4772, #1239).
- Add Taiwan's national freeway cameras from the Freeway Bureau, MOTC to CCTV:
~1,800 cameras read at runtime from the official MOTC 2.0 open-data table,
with on-demand snapshots and attribution. No API key is required; set
`CCTV_TAIWAN_ENABLED=0` to skip it (419vive, #1237).
- Require a geocoded containment anchor for ambiguous analyst region names,
preserve boundary provenance and cancellation safety, and retain exact-name
precedence for natural outlines (manjunath22466, #1235).
- Serve the camera catalog while it refreshes. An expired catalog made every
camera request wait for the slowest pack to reload; it is now answered at
once while one background refresh runs, so only the first load waits. Estonia's
slow Tarktee feeds get 30 s instead of 15 s, so the pack no longer drops out
on about a third of loads (quanticsoul4772, #1234).
- Harden the Tarktee (Estonia) DATEX2 camera catalog fetches: refuse redirects,
cancel both response streams when either response fails or overflows the 4 MB
byte cap, and deduplicate camera IDs before per-pack prioritization
(yaka0007, #1233).
- Skip bike-share stations with missing, blank or invalid coordinates instead
of placing them at coerced zero coordinates or accepting out-of-range values.
Explicit zero coordinates and numeric strings remain supported (Bortlesboat, #1232).
- OpenSky sign-in matches what OpenSky accepts since 2026-03-18. The server
now reads `OPENSKY_CREDENTIALS_FILE` (the `credentials.json` OpenSky's
account page downloads) on every platform, not only through the macOS
launcher, and no longer sends retired username/password Basic auth:
`OPENSKY_AUTH_MODE=basic` loads anonymously with one warning saying what to
set instead (quanticsoul4772, #1229).
- Credential-file warnings omit parser excerpts and filesystem error details;
the path example also works with the macOS launchers.
- Load Ontario 511 cameras again. 511on.ca's camera endpoint now requires a
free developer key; set `ONTARIO_511_API_KEY` to enable the pack, which is
otherwise skipped with one log line saying where to get a key
(quanticsoul4772, #1228).
- The first-run card can be closed on a phone or tablet. "ESC to dismiss" is
now a button that closes the card the same way ESC does; touch screens read
DISMISS, and phones no longer hide it, so choosing a mission is not the only
way out (KnottyDyes, #1226).
- Add continuous globe rotation with direction and speed controls and a globe-fit
check before starting (manjunath22466, #1225).
- Open God's Eye View from Pinokio in the system browser using the supported
`popout` menu option. The application's framing policy is unchanged
(NukeThemAII, #1222).
- Avoid starting or queuing a reply when the local channel rejects a typed OpenAI
command, while preserving an earlier accepted deferred input
(Manjunath, #1220).
- Keep NASA FIRMS fire and ALPR camera labels English in voice tool results
(`track_entity`, `get_entity_context`, pointer context); the voice card still
shows them in the interface language (daikaginza, #1207).
- Report actual outcomes when voice enables or disables CCTV through the shared
layer lifecycle, including failed and cancelled changes (manjunath22466,
#1205).
- Draw CCTV stills at their source aspect on the monitor plane instead of
stretching them to the 16:9 canvas. Most DOT cameras publish 4:3 frames, so
every still was widened; they are now centered and pillarboxed on the dark
the placeholder already ends on (Danielslay86, #1201).
- Color CCTV cameras with a video feed violet. Cameras with an HLS, MP4 or
WebM feed were the same cyan as stills, so a DelDOT or Statens vegvesen
video camera could only be found by opening it. Stills stay cyan and the
active camera stays amber, and focus de-emphasis fades the violet the same
way (Danielslay86, #1200).
- Keep Space Missions marked STALE when Launch Library 2 falls back to cached
launch data, and return to NOMINAL after a fresh response without discarding
retained missions (rudycelekli, #1199).
- Share Launch Library request cache keys across installations refreshing in the
same UTC minute, identify the app to the provider, and explain that standard
space-missions access needs no key. Optional higher-rate LL2 tokens are paid
upgrades; existing local cache and server-only token handling are unchanged
(rudycelekli, #1197).
- Add nine Spanish city bike systems to Bike Share and `get_bike_share`: BiciMAD
(Madrid), Valenbisi (Valencia), Bizi Zaragoza, Sevici (Seville), BIKI
(Valladolid), BiciCoruña, Dbizi (Donostia-San Sebastián), Bilbao Bizi and
BicinRivas, about 1,840 stations. Station names read underscores as word
breaks, and station capacity counts docks rather than PBSC's geofenced area
(ulihhhh, #1190).
- Keep missing launch-pad coordinates unknown so launches use the location
fallback or stay off the map instead of appearing at 0°N 0°E. Missing stage
flight numbers no longer show "FLIGHT 0"
(Rudy Celekli, @rudycelekli, #1186).
- Flights' ground floor works across the antimeridian. The viewer-distance
check behind the 150 km floor radius and its copy in the photoreal mesh-floor
sampler subtracted longitudes without wrapping, so two points 21 km apart
across 180° measured about 38,000 km and low or grounded aircraft just across
the line from the viewer were never floored. A contact's display corridor
walked a segment across 180° the long way round, spending its cell budget on
ground behind the contact, and a contact on the seam lost its cell hysteresis.
SEARCH NEARBY SITES on a view across 180° searched Google Places around
longitude 0, on the far side of the globe, with a radius sized from the view's
height alone; it now searches the view (Aditya, @Rohilalala, #1173).
- Keep missing CCTV numbers unknown instead of reading them as zero. `Number`
turns null, blank strings and booleans into 0 or 1, and the camera source
list sends an unknown number as JSON null, so a source-pack camera that left
out its pose reached the browser as zeros clamped to each field's floor —
facing north, pitch −2°, FOV 20°, range 120 m, mount 6 m and ground 0 m —
instead of the defaults (id-hash heading, −17°, 74°, 700 m, 24 m and the
city's ground); a null or blank position placed the camera at 0,0. The
server's `toFiniteNumber` and the browser's `safeNumber` now treat absent,
blank and boolean values as missing. The same conversion also stops a
Tallinn row with a null heading reading as a confident north-facing camera,
a Caltrans camera with a blank elevation losing its 150 m ground prior, and
a TfL or Catalonia camera with a null or blank coordinate being kept at 0,0
or on the zero meridian (omerbberk, #1172).
- Director shot actions land on the requested shot. A seek to a shot's start
could round a hair below it and select the end of the previous shot for some
shot counts and durations (Aditya, @Rohilalala, #1171).
- Fire-perimeter queries handle outlines and holes crossing the antimeridian:
areas enclosed near ±180° are included, areas at the opposite meridian are
excluded, and returned perimeter coordinates stay near the dateline. Incident
cards use the same circular longitude mean and compare multipart outline
areas along short longitude edges, keeping the card on the largest polygon
(Arianna Method, @ariannamethod, #1142).
- Scenes keep the "Scene not saved" status when a capture, update, append or
import cannot be saved, and an unreadable saved project no longer overwrites
the recovery checkpoint. `startScene()` resolves to its documented result,
`afterShotId` works without an explicit scene, and `scenesRun` counts the
distinct scenes queued (djabhi31, #1135).
- Label camera-tool Street View fallbacks as historical Google imagery rather
than the current camera view; keep unknown frame provenance explicit
(rudycelekli, #1118).
- Release superseded Google Street View open waits and suppress metadata
requests whose viewer was unmounted while the service loaded (rudycelekli,
#1117).
- Exclude aircraft without a reported altitude from requested altitude bands;
unfiltered aircraft queries still retain them (rudycelekli, #1115).
- A failed Esri Satellite or keyless terrain load still shows OSM or flat terrain
at once, but no longer lasts the whole session: while the fallback is on
screen, the real provider is retried in the background (30 seconds, doubling
to a 10-minute cap) and replaces it live once it loads. Map switches never
wait for a retry (Aditya, @Rohilalala, #1111).
- Skip TLE sets whose line 1 and line 2 carry different catalog numbers, so a
spliced catalog cannot pair one satellite's elements with another's; the next
valid set still parses (VG31OP, #1101).
- Overpass data whose element tags contain a failure phrase ("timed out",
"runtime error", "too many requests" and similar, e.g. a place named "Timed
Out Escape Rooms") is returned as data instead of being refused and putting
the endpoint on cooldown; upstream failures are read from the remark (Aditya,
@Rohilalala, #1097).
- Military aircraft keep their last heading and speed when an adsb.lol row omits
`track` or `gs`, instead of turning north and stopping; a grounded row without
a speed still reads 0 (Aditya, @Rohilalala, #1096).
- Update DOMPurify to 3.4.17, Sharp to 0.35.5 and source-map-js to 1.2.2 for
GHSA-p98j-92pf-mc4p, GHSA-6688-9rhm-gjv2, GHSA-wq5f-xc86-pv6w and
GHSA-68fv-2mgg-jv7q; `npm audit` now reports no vulnerabilities (NurstarK,
#1094).
- Location search accepts two-dimensional WGS84 `geo:` URIs offline, with
the existing coordinate framing and label. Unsupported URI fields are passed
to the remaining search providers unchanged (cj-vana, #1071).
- Keyless standalone startup answers an unavailable Google tile-token endpoint
with uncached 204 instead of a browser-visible 404. Existing providers take
precedence, and unrelated API routes behave as before. (dvilrgamerz, #1066)
- On Windows, `npm run doctor` and the Pinokio install no longer print Node's
DEP0190 warning ("Passing args to a child process with shell option true can
lead to security vulnerabilities"): npm.cmd's fixed subcommand is now part of
the shell command string instead of an argument array (quanticsoul4772, #1057).
-`aircraft_in_area` and `vessels_in_area` take `group_by` and `stats`, so an
MCP client can ask which airlines, countries, aircraft types or ship types
dominate an area, or the average altitude or speed, over every match instead
of estimating from the returned rows. Airlines come from the ICAO designator
in each callsign (SWA1234 counts as SWA), so the breakdown works on feeds
that report no operator; operator and type are counted only where the feed
reports them. Ship types count by family, so AIS codes 70 to 79 are all
cargo; type 0 ("not available") and the reserved codes 100 to 255 count as
unknown. Altitude and speed stats cover airborne aircraft only, so parked
aircraft do not pull the means down. Every vessel answer, `find_vessel`
included, now reads AIS ship-type codes as their family and a destination
with no letters as unknown (quanticsoul4772, #1056).
- Add Hong Kong Transport Department traffic snapshot cameras as a keyless CCTV
source pack: the public DATA.GOV.HK English locations XML, frames pinned to
`tdcctv.data.one.gov.hk`, with DATA.GOV.HK Terms of Use attribution. By
default nearly the full TD catalog (~1,013 cameras; cap 1,100) loads,
prioritized across Hong Kong Island, Kowloon and the New Territories. Headings
use the shared id-hash fallback at low confidence. `CCTV_HK_MAX_SOURCES` sets
the cap (8–1,200) and `CCTV_HK_ENABLED=0` turns the pack off (JCY2515, #990).
- Cross-site pages can no longer fill the terrain-height cache or spend the
visitor's own `/api/terrain/heights` rate-limit quota through the visitor's
browser. The route refuses a cross-site `Sec-Fetch-Site` or a foreign or
opaque `Origin` before rate limiting, cache access or upstream work. Unlike
the cost-bearing routes it still answers same-origin requests that arrive
through a reverse proxy, so terrain keeps rendering behind nginx, Caddy or
Cloudflare instead of falling back to the geoid (Sunil56224972, daikaginza,
#975).
- Street Level is rebuilt around one small provider registry: Mapillary,
KartaView, Panoramax and Google Street View share one coverage drawer and
one photo viewer, one source lit at a time. Floating and docked panels,
FOLLOW, surface modes, the legend and the Mapillary proxy's disk cache are
gone; `.gev-cache/mapillary/` from earlier runs is no longer read and can be
deleted (gekh, #974).
- Cross-site browser requests can no longer spend the TomTom traffic-tile
budget through `/api/tomtom/*`. The shared same-site gate runs before status,
cache, and upstream work; same-origin app requests and non-browser local
callers remain supported (Sunil56224972, #973).
- Recent Imagery: opening DETAILS scrolls its card into view when it opens below
the fold. The panel used to restore the previous scroll position right after
the reveal, so the card stayed out of sight (Ian Blenke, #970).
- Harden Vite's filesystem deny list for local-only artifacts: repo `dist/`,
`.gev-logs/`, and `.gev-cache/` are denied without blocking dependency
`dist/` directories, and checkout paths containing glob metacharacters are
escaped before building the root-anchored pattern (blowing-a-raspberry, #948).
- Give each Statens vegvesen camera its own ground height from Kartverket's
keyless point-height API (CC BY 4.0) instead of one flat 150 m prior, so
mountain-pass cameras no longer sit hundreds of metres below the terrain
where neither the point-height prior nor the ground snap resolves. About 850
of ~855 cameras get a height; the rest keep the old prior. Requests carry 20
points, the most the API answers from its 1 m terrain model, eight at a
time; a cold lookup takes about 6–13 s. Cameras over water use the water
surface rather than the sea-floor depth the API returns there; for bridge
cameras that is still below the deck. Heights are cached for good and misses
for 6 h, so a refresh only asks about new or moved cameras. After a
failing request (a 408, 429 or 5xx, a network error, a request over 10 s or
an unreadable body) the lookup stops and backs off (Retry-After, else
30 min, longer than the refresh). The lane waits at most 12 s from its
start, and only on a cold cache; a slower first lookup leaves the cameras it
had not reached on the flat prior until the next 15-minute refresh, and
keeps filling the cache meanwhile.
`CCTV_VEGVESEN_HEIGHTS=0` turns it off. Follow-up to #929 (trygvehhh, #945).
- Extract a reusable model-free World Packet from the existing voice
`get_current_view_state` owner without restoring disabled layers to the
model context. Keep the enabled-only payload, disabled-layer count, source
provenance, action result and view-state contract; read the layer roster once
per packet; voice retains the speech/card envelope at its own boundary.
Credits: original GEV voice/layer contributors and #880 implementation.
- Add optional Gemini Live voice to the standalone app, with a provider chooser,
server-held API key, microphone streaming, shared map actions and captions.
Switching providers ends the old conversation and preserves the input mode
(Manjunath, #872).
- Close the compact Radio disclosure when an adaptive tactical right-rail
restore directly expands Context, before Space Missions can be covered.
A repeated Context-open request also clears stale disclosure state.
Real PanelChrome constructor wiring and the rail reflow path are covered
by focused regressions. Credits: kvnloo (#816), raysung-2026's original
#804 report, asturksever's review, and quanticsoul4772's independent
Windows diagnosis.
- Changelog entries are now one file per pull request. A change adds
`changelog.d/<pull request>-<slug>.md` instead of editing the top of
`CHANGELOG.md`, which two pull requests in flight could not share — it was the
most frequent merge conflict in the open queue, and for six of them the only
one. `npm run changelog` splices the directory into `CHANGELOG.md` at release
time and empties it; `npm run changelog:check` reports the batch and changes
nothing. Fragment text is copied through unchanged (daikaginza, #787).
- Read satellite catalog numbers written in Alpha-5, the TLE spelling of
numbers above 99999: a letter for the first two digits, skipping I and O
(`A0000` is 100000, `Z9999` is 339999). The satellites layer converted such a
field to `NaN`, so every Alpha-5 satellite shared one catalog entry and all
but the first were dropped as duplicates; the satellite tools read the same
field as no number, so such a satellite could not be found by it. The layer
and the tools now decode the field in `src/sources/tle.js`, and reject a
malformed one (a sign, decimal point, hex, embedded space or stray letter)
instead of reading part of it as a number. This is Alpha-5 TLE
compatibility: CelesTrak's TLE feed carries no satellite above 99999, which
needs the OMM feed (#1191) (Honorius Neogy, #767, fixes #751).
- The dev server no longer watches `.gev-cache` and `.gev-logs`. Providers write
cache files and logs there while the app runs, and none of it is source, so
the watcher now skips it; this guards against the DNS-lookup delays heavy
cache writes have caused, though no difference was measured on current main
(monwiss, #761).
- Bound what a caller can grow through `/api/terrain/heights`. Every distinct
5-decimal point was a permanent in-memory entry, a permanent line in an
uncapped `.gev-cache/terrain-heights.json`, and a live upstream call, with
nothing limiting the rate. The point cache now holds at most 50,000 entries
and evicts oldest-first, a cache file above
24 MB is not read back, and the route is limited to 90 requests/minute per
client (270 global). Evicting a point costs one refetch, never a wrong
answer, since terrain does not move. Ordinary use is unaffected: a full
`test:track` run drives 10 requests, peaking at 8/min (daikaginza, #686).
- Add FL511 (Florida DOT) traffic cameras as an opt-in CCTV source pack:
the statewide ArcGIS catalog, read a page at a time under a byte cap with
redirects refused, and frames pinned to FL511's two image hosts over HTTPS.
FL511's terms limit its content to individual, personal use, and any other
use (a public deployment included) needs FDOT's express written consent, so
the pack stays off unless `CCTV_FLORIDA_ENABLED=1`. Headings come from the
catalog's direction codes, and camera ids come from the image channel, since
the catalog's ID column repeats across regions. `CCTV_FLORIDA_MAX_SOURCES`
sets the cap (Danielslay86, #605).
- The client's great-circle distances now come from one shared
- Street Level: Panoramax is the fourth provider, keyless. Light its chip
(orange; off by default, one provider at a time): from orbit dots show
where the federated commons has pictures, and below about 60 km its
sequences draw as lines from `api.panoramax.xyz` vector tiles. A click on a
(orange; off by default, one provider at a time): below about 60 km the
federated commons' sequences draw as lines from `api.panoramax.xyz` vector tiles. A click on a
line opens that sequence's picture nearest the click without moving the
camera, flat or 360°, with cones along the sequence around it and ‹› /
← → following the picture's own links. The 360° and since filters apply,
@@ -377,10 +1070,10 @@
Maps JavaScript API) and click a street, or press OPEN STREET VIEW, to
open Google's own panorama in
the panel, with its arrows, the address and month in the caption, a link
to Google Maps and FOLLOW. Share links carry it in the provider switches
to Google Maps. Share links carry it in the provider switches
(`r.google-1`). One provider is lit at a time (lighting one switches the
other off), Mapillary by default since it is free. Street View's panoramas
are all 360°, so the IMAGERY and SINCE filters and the legend apply to
are all 360°, so the IMAGERY and SINCE filters apply to
Mapillary only and hide while GOOGLE STREET VIEW is lit; its globe marker
is a Google-blue view cone.
@@ -419,21 +1112,16 @@
proposed diagnosis in #927, and [kvnloo](https://github.com/kvnloo) for identifying
the repair race. Windows/Claude Desktop Cowork confirmation remains outstanding.
- Street Level: a street-level imagery layer modelled on the iD editor's photo
overlay, with Mapillary as its first provider (free client token, CC BY-SA 4.0
imagery). One right-rail panel holds a chip per provider, shared 360°/flat and
captured-since filters, and an embedded viewer with EXPAND, FIT/FILL and
FOLLOW (Google 3D only). Coverage lines and image cones are drawn from orbit
down to a single street; on Google 3D they sit on the bare earth, refined
against the rendered mesh near the camera, so trees and buildings hide them.
Share links carry the provider switches, the filters and the panel's collapsed
state. A server proxy caches Mapillary coverage tiles (24 h, 1 GiB on disk),
refuses cross-site requests, rate-limits per IP and backs off when Mapillary
rejects the token or rate-limits.
- The CCTV and Street Level panels are portable: drag the header to float the
panel, resize it from any edge or corner, and double-click the header to dock
it again. The position persists across reloads.
- Street Level: street-level imagery from providers, Mapillary first (free
client token, CC BY-SA 4.0). A right-rail panel holds a chip per provider,
360°/flat and captured-since filters and an embedded viewer with EXPAND and
FIT/FILL. Coverage lines (below 60 km) and image cones drape on terrain and
Google 3D tiles; opening a photo frames it with one camera flight. Share
links carry the provider switches, the filters and the panel's collapsed
state. A server proxy adds the token to Mapillary coverage tiles, trims and
caches them for 24 h, refuses cross-site requests and redirects, and limits
load per IP and upstream. The panel reads KEY REQUIRED without a token and
KEY REJECTED when Mapillary refuses it.
- Add Norway's city bikes to the Bike Share layer: Oslo, Bergen and
Trondheim Bysykkel (Urban Sharing), Kolumbus Bysykkel in Stavanger and
@@ -504,6 +1192,28 @@
needs only the Map Tiles API and the Maps JavaScript API (Street View). Keyless sessions and the Photon/Nominatim
fallback order are unchanged.
- Voice can switch on every shipped data layer (the two scene-driven
Bhote Koshi layers stay scene-only), including transit, Recent
Imagery, wind, the weather layers, cyclones and mapped installations, and
understands their spoken names ("buses", "hurricanes", "military bases").
One voice manifest (`src/voice/layerManifest.js`) generates the tool enums
and aliases, and a unit test fails when a new layer has no entry.
-`analyst_query` counts up to 250,000 loaded records per layer instead of the
first 2,000, and says when a layer went past that (`complete: false`). It
also answers over bikeshare stations, transit vehicles, rocket launches,
cyclones, ALPR cameras, mapped installations, local ADS-B aircraft and fire perimeters; fire
records carry their sensor (VIIRS or MODIS).
- Unknown fields, operators, units, value types, scope kinds and invalid
centres are refused with the allowed values instead of answering zero or widening to anywhere. A
layer that is off, still loading or unavailable is reported as such, not
as a count of zero; a partly answerable query is marked `partial`.
Distance sorts honour `sortDir`, a written unit such as `altitudeM(ft)`
converts, and large rankings no longer sort the whole set.
- Results carry `lat`/`lon` for each item and a `display` object with the
scope, the feed window (earthquakes and fires cover the last 24 h) and any
caveat for the screen. The model is asked to answer in one short phrase.
- The weather and Recent Imagery panels can be opened by voice.
## [0.2.1] — 2026-10-02 — God's Eye View through Your Agent (with security fixes and other features)
- Ship each bundled data pack once. The region, marine, admin-boundary,
@@ -786,32 +1496,6 @@
(`src/ui/imagerySplit.js`, `src/maps/imageryComparison.js`), and
`MapSourceController.subscribe()` reports every settled map switch.
## Unreleased — voice reaches every layer; honest analyst answers
- Voice can switch on every shipped data layer (the two scene-driven
Bhote Koshi layers stay scene-only), including transit, Recent
Imagery, wind, the weather layers, cyclones and mapped installations, and
understands their spoken names ("buses", "hurricanes", "military bases").
One voice manifest (`src/voice/layerManifest.js`) generates the tool enums
and aliases, and a unit test fails when a new layer has no entry.
-`analyst_query` counts up to 250,000 loaded records per layer instead of the
first 2,000, and says when a layer went past that (`complete: false`). It
also answers over bikeshare stations, transit vehicles, rocket launches,
cyclones, ALPR cameras, mapped installations, local ADS-B aircraft and fire perimeters; fire
records carry their sensor (VIIRS or MODIS).
- Unknown fields, operators, units, value types, scope kinds and invalid
centres are refused with the allowed values instead of answering zero or widening to anywhere. A
layer that is off, still loading or unavailable is reported as such, not
as a count of zero; a partly answerable query is marked `partial`.
Distance sorts honour `sortDir`, a written unit such as `altitudeM(ft)`
converts, and large rankings no longer sort the whole set.
- Results carry `lat`/`lon` for each item and a `display` object with the
scope, the feed window (earthquakes and fires cover the last 24 h) and any
caveat for the screen. The model is asked to answer in one short phrase.
- The weather and Recent Imagery panels can be opened by voice.
## Unreleased — local receiver feeds
### Local receiver feeds
- The Local ADS-B layer also reads local 1090 MHz and 978 MHz UAT decoder
@@ -954,7 +1638,6 @@
Add feed provenance to analyst/view answers and HUD context while retaining existing response fields and runner ownership (Matt Van Horn, #347).
Analyst records for loaded satellites, datacenters and dams, with explicit bounded count/rank coverage (Matt Van Horn, #351).
- New Fire Perimeters layer (Events group): live NIFC WFIGS interagency
wildfire incident perimeters as ground-clamped polygons with a
containment-colored fire line, refreshed every 5 minutes from the public
@@ -1231,6 +1914,7 @@ of current runtime behavior, see [`docs/CURRENT-STATE.md`](docs/CURRENT-STATE.md
### Other changes
- Add ECMWF IFS model selection to Wind (#464, thanks @beneduzi), with model-scoped forecast-step caches, cancellation of replaced requests, and separate issue/valid timestamps.
- Add bounded Director feature actions with accessible controls, explicit camera/layer admission and cancellation; restore pack geometry on same-shot seek. Preserve existing scenes and content attribution.
@@ -2039,3 +2723,4 @@ represent previously published GitHub Releases.
./scripts/dev-fresh.sh # or: npm run dev (keys are optional)
@@ -27,6 +29,25 @@ through Pinokio; the terminal path above remains the contributor path.
Open `http://localhost:4173`. Before sending a PR run `npm run build`, `npm test`, and `npm run test:track` (dev server must be up) — **all three must stay green.**
### Test prerequisites
- **`npm test`** — unit tests. Runs standalone; no dev server or credentials
required. Exit code is the gate.
- **`npm run test:track`** — the 3D flight-tracking regression harness. It
drives the **real app** in headless Chromium, so it **requires a running
dev server** (default `http://localhost:4173`). To target another instance,
run `npm run test:track -- --url <app-url>` (or
`node scripts/track-regression.mjs --url <app-url>`). It does not depend on
live OpenSky / adsb.lol / AISStream (those are optional or rate-limited
locally); it installs a persistent `fetch` shim that returns synthetic
aircraft in the exact upstream payload shapes the layers parse.
- **`npm run build`** — production Vite build. No server or credentials
required; it validates that the client bundle compiles cleanly.
Each feature also has its own headless gate; see
[Feature regression gates](#feature-regression-gates) below. Docs and changelog
rules are under [Pull requests](#pull-requests).
## Checking a built app locally
Run `npm run build` followed by `npm run preview`. Preview serves the built
@@ -54,9 +75,8 @@ head -40 scripts/qa-radio.mjs # what this runnable gate proves and how to run i
Runnable gate entrypoints document what they assert and how to invoke them.
Most need a dev server; some also need a specific provider key or port. Follow
the entrypoint's header rather than assuming every matching file accepts the
same arguments. Five have an `npm run` alias: `qa:map-source-tray`,
`qa:panel-resize`, `qa:street-level`, `qa:street-level:fixtures` and
`qa:transit`.
same arguments. Four have an `npm run` alias: `qa:map-source-tray`,
If you aren't sure which gate covers your change, search `docs/CURRENT-STATE.md`
for the feature: it names the gate for many of them, and it's the authoritative
@@ -66,11 +86,10 @@ gate for the feature you changed.
> **CI covers only two of these for you.** The workflow runs the setup policy
> checks, the formatting and package-boundary checks, the unit suite and the
> production build, plus a Windows onboarding job, and against the built preview
> it runs `qa:street-level:fixtures` and `qa:panel-resize`. It does not run
> `npm run test:track` or any other `qa-*.mjs` gate — those need a live dev
> server and a browser. Include the applicable local run in your PR's
> validation evidence.
> production build, plus a Windows onboarding job. It runs neither
> `npm run test:track` nor any `qa-*.mjs` gate except the Street Level fixture
> gate (`street-level-browser` job) — they need a live dev server and a
> browser. Include the applicable local run in your PR's validation evidence.
## Share-link layer tokens
@@ -110,15 +129,15 @@ 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 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.
- **🛰️ 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/`.
- **🐛 Fix bugs / improve the first-run experience.** See [docs/KNOWN-ISSUES.md](docs/KNOWN-ISSUES.md).
- **Assembly lives in `src/app/`; standalone defaults live in `src/standalone/`.** UI controllers live in `src/ui/`, layer factories in `src/layers/`, portable sources in `src/sources/`, and application operations in `src/services/`. Existing `src/ui.js` and `src/data/<layer>.js` entries retain compatibility; new code belongs with its focused owner.
- **Vue 3 + Pinia for the interface,** [CesiumJS](https://cesium.com/platform/cesiumjs/) for the globe and [Vite](https://vitejs.dev/) to build. See [Building UI](#building-ui).
- **Assembly lives in `src/app/`; standalone defaults live in `src/standalone/`.** UI controllers live in `src/ui/`, layer factories in `src/layers/`, portable sources in `src/sources/`, and application operations in `src/services/`. Existing `src/data/<layer>.js` entries retain compatibility; new code belongs with its focused owner.
- Sources acquire records; renderers own Cesium resources. Import `gods-eye-view/layers/<family>/source` when only a source factory is needed. Common voice controls consume the session interface; protocol adapters own connection details.
- **Secrets stay server-side.** Anything needing a private key goes through a local proxy under `server/providers/`. The browser only ever sees the Google Maps key (which you restrict) and ephemeral tokens.
-`docs/CURRENT-STATE.md` is the authoritative runtime reference — read it first.
@@ -128,9 +147,65 @@ The highest-leverage places to jump in:
- ES modules, **2-space indent, single quotes, semicolons.**
- JSDoc on exported/public functions.
- Match the surrounding code — comment density, naming, and idiom.
- User-visible text goes through `t()`,`tn()` or`msg()`, or a `data-i18n*` attribute in markup, and`npm run i18n` must pass. See [docs/I18N.md](docs/I18N.md).
- User-visible text goes through `t()` or`tn()`, with`msg()` marking English that is stored and translated later. Components call `t()` in their templates, so their text follows the locale; `data-i18n*` attributes are only for static page markup such as the loading screen, and `npm run i18n` rejects them in a template.`npm run i18n` must pass. See [docs/I18N.md](docs/I18N.md).
- Prefer small, reviewable commits. Conventional-commit-style prefixes (`feat:`, `fix:`, `perf:`, `docs:`) are appreciated but not required.
## Building UI
New UX elements are Vue components whose state lives in a Pinia store, and so
are existing ones when a change converts them:
- **Views are Vue components.** Application views live in `src/ui/views/` and
the components they place in `src/ui/views/panels/`; a component that
belongs to one feature sits beside its owner (`src/ui/rail/`,
`src/ui/weather/`). A view renders what its store holds and forwards
clicks and input to the store; it holds no application logic.
- **State lives in a Pinia store** in `src/ui/stores/`. The owner presents a
plain view object and publishes it to the store; the view renders it. User
actions return to the owner through handlers it connects to the store,
which replace an earlier owner's. Where several owners answer different
actions of one store, use `createOwners()` from `src/ui/stores/owners.js`:
`act(name, ...args)` calls the newest connected owner that handles `name`,
and the owner calls `detach(token)` when it stops.
- **Portable owners stay framework-free.** An owner in a portable export
imports neither Vue nor Pinia: it takes a `publish` option or offers a
`subscribe…()`, and the application shell connects it to the store.
`npm run check:boundaries` enforces this.
- **Reuse the kit first.** `src/ui/kit/` has one component per control:
`UiButton` (including pressed toggles and pressable chips), `UiSelect`,
`UiSlider`, `UiSegmented`, `UiChip` (a label, not a control), `UiBadge`,
`UiEmpty`, `UiIcon`, `UiTooltip`, `UiPanel` and `UiRail`. Use these before
writing a control. When the kit lacks something, extend a kit component or
add one that follows the kit's names, props, events and class names (see
[Component kit](docs/VUE-MIGRATION.md#component-kit)), rather than
building a one-off.
- **No hand-written DOM binding.** Don't build elements or write text,
classes or attributes from a controller to show UI state; render it from
the store. New controls take input through the store rather than through
listeners the owner adds. To render outside the view's host (the body, the
globe container, or a slot another surface opens), use `<Teleport>` to
`body`, a fixed id or a container the owner publishes; don't move nodes by
hand. What stays imperative: classes on `<body>` and `<head>`, which sit
outside Vue (`cockpit-mode`, `ui-clean-view`); focus moves; positions a
layout pass measures in the same task that writes them; and surfaces an
owner paints or plays, such as the Cesium canvas, overlay canvases and live
video. A few existing surfaces keep listeners or bindings that
[docs/VUE-MIGRATION.md](docs/VUE-MIGRATION.md) records; keep those
contracts when you change them, and don't copy them into new UI.
- **Text** goes through `t()` or `tn()` where the template renders it.
English kept in a constant or a store is marked with `msg()` and passed to
`t()` when it renders, since a `t()` at module load would fix the English.
- **Test components, not markup strings.** Mount the component over
happy-dom with `@vue/test-utils` or `createApp` and assert what it renders
from its store (see `src/testSupport/vueDom.mjs`, `src/ui/kit/*.test.mjs`
and `src/ui/views/intelHudView.test.mjs`). Run one such file with
nodes to `assert.equal`/`deepEqual`; compare them with `assertSameNode()`
from `src/testSupport/domAssert.mjs`.
[docs/VUE-MIGRATION.md](docs/VUE-MIGRATION.md) records how each surface moved
and what stays imperative, and why.
## Formatting and reusable components
Run `npm run format` before submitting changes, then `npm run format:check`
@@ -155,6 +230,34 @@ ownership and adoption process.
3. If you change runtime behavior, update `docs/CURRENT-STATE.md` in the same PR, and add your changelog entry as `changelog.d/<pr-number>-<slug>.md` instead of editing `CHANGELOG.md` — see [changelog.d/README.md](changelog.d/README.md). Distinct filenames don't conflict; a release assembles the directory into `CHANGELOG.md`. A documentation claim that needs a regression test should assert against `docs/CURRENT-STATE.md` or `README.md`, which a PR still edits directly.
4. If you add or change a data source, update [DATA_SOURCES.md](DATA_SOURCES.md) with its license and attribution. **Don't add data you don't have the right to redistribute** — fetch it at runtime instead.
5. Describe what you changed and how you verified it (screenshots welcome for anything visual).
6. If you add a new layer, fill in the new-layer section of the PR template. See [Adding a new layer](#adding-a-new-layer).
## Adding a new layer
A new layer changes what everyone sees on the globe, so maintainers need to see
it working and know where its data comes from before reviewing the code. PRs
that add a layer must include all four of these in the description. The PR
template has a section for them.
1.**Screenshots.** At least one of the layer on the globe, plus its info card
or panel if it has one.
2.**A screen recording**, preferably 15 to 30 seconds: turn the layer on, move
around, and open a record. Drag an `.mp4` or `.mov` into the PR description.
GitHub accepts videos up to 10 MB on free accounts, so link to a larger file
instead of attaching it.
3.**The proposed category.** Say which layer-panel group the layer belongs in.
The groups are defined in `PANEL_GROUPS` in `src/ui/layerPanel.js` (today:
Movement, Cameras, Infrastructure, Events, Weather and Audio, plus Utilities,
whose rows render in the separate UTILITIES panel). A layer that isn't
listed there falls under "Other layers". If none of the groups
fits, propose a new one and explain why.
4.**The data source and licence.** Name the provider and endpoint, link its
licence or terms, give the attribution shown in the app, and say whether it
needs a key and whether the data is fetched at runtime or bundled. Add the
same details to [DATA_SOURCES.md](DATA_SOURCES.md) and register the credit
in `src/data/dataCredits.js`.
Maintainers may hold a layer PR until all four are there.
| **NOAA nowCOAST observed weather** (`nowcoast.noaa.gov/geoserver/observations/{weather_radar,satellite}/ows`, WMS 1.1.1) | Rain radar (MRMS base reflectivity, contiguous US, about 4-minute updates) and Satellite clouds (GOES-19/18 Band 14 regional infrared, about 5-minute updates; global infrared mosaic, hourly). Keyless; layer metadata refreshed every 2 minutes, exact-time images cached for up to 24 hours | [NOAA disclaimer](https://oceanservice.noaa.gov/disclaimer.html) | "NOAA nowCOAST · NWS/OAR MRMS radar; NESDIS GOES and global satellite partners" |
| **NOAA nowCOAST lightning density** (`nowcoast.noaa.gov/geoserver/observations/lightning_detection/ows`) | Lightning density: 15-minute accumulated strike density on an approximately 8 km grid, Americas and Pacific. Keyless; layer metadata refreshed every 10 minutes, exact-time images cached for up to 24 hours | NOAA Level-5 derived product from Vaisala NLDN/GLD360; the [product description](https://ocean.weather.gov/lightning/lightning_pdd.php) permits public distribution of this density product, not of raw Vaisala detections | "NOAA/NWS nowCOAST; derived from Vaisala NLDN/GLD360" |
| **NOAA NHC / CPHC advisories** (`www.nhc.noaa.gov/CurrentStorms.json` + the `mapservices.weather.noaa.gov` tropical weather summary MapServer) | Cyclone advisories: storm positions, forecast tracks, points and cones for the Atlantic and eastern/central North Pacific. Keyless through `/api/cyclones`; cached for 5 minutes | [NWS public-data terms](https://www.weather.gov/disclaimer); no endorsement implied | "NOAA/NWS NHC / CPHC" |
| **Google Map Tiles API** (Photorealistic 3D Tiles) + Places/Geocoding | The 3D globe, voice scene context, and on-demand nearby installation search | Google Maps Platform ToS (proprietary, your own key + billing) | "Google" / "Google Maps" logo — **shown in-app**, required |
| **Environment and Climate Change Canada** (MSC GeoMet OGC API) | Air quality (AQHI) — Canadian station observations | [ECCC Data Servers End-use Licence](https://eccc-msc.github.io/open-data/licence/readme_en/) (open, attribution required, no endorsement implied) | "Contains information licensed under the Environment and Climate Change Canada Data Servers End-use Licence" |
| **NASA GIBS** (WMTS tiles + Worldview Snapshots) and **NASA CMR** (granule catalog) | Recent Imagery layer: HLS S30/L30 (Sentinel-2, Landsat 8/9) and VIIRS true-colour tiles draped on the map, per-day thumbnails and PNG exports; the HLS granule catalog (dates, cloud, footprints) for a selected box | U.S. public domain (NASA data); keyless, browser-direct (CORS `*`); no bulk use — one 30-day catalog query per box, tiles bounded to the box, six tile requests in flight | "We acknowledge the use of imagery provided by services from NASA's Global Imagery Browse Services (GIBS), part of NASA's Earth Science Data and Information System (ESDIS)." — [gibs.earthdata.nasa.gov](https://gibs.earthdata.nasa.gov), [HLS](https://lpdaac.usgs.gov/products/hlss30v002/) |
| **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" |
| **GDELT Project DOC 2.0** | Fail-soft fallback for location-matched cockpit headlines | [GDELT Terms of Use](https://www.gdeltproject.org/about.html#termsofuse): unrestricted academic/commercial/governmental dataset use, with citation and link required; linked articles retain publisher terms | "GDELT Project" plus each article's linked publisher/domain |
| **City of Austin Open Data** | CCTV camera catalog + frames | City of Austin Open Data Terms of Use | "City of Austin, TX — data.austintexas.gov" |
| **TxDOT ITS (its.txdot.gov)** | CCTV camera catalogs + frames, Texas districts | Public TxDOT traffic camera data | "Texas Department of Transportation" (courtesy) |
| **FL511 (Florida DOT)** | CCTV camera catalog + frames, Florida statewide | FL511 content, individual personal use only; any other use needs FDOT's express written consent ([terms](https://fl511.com/privacy#disclaimer)). **Off by default**; set `CCTV_FLORIDA_ENABLED=1` to enable | "Florida DOT — FL511.com" (courtesy) |
| **City of Tallinn (ristmikud.tallinn.ee)** | CCTV camera catalog + frames, Tallinn intersections | Public City of Tallinn traffic camera data | "City of Tallinn — ristmikud.tallinn.ee" (courtesy) |
| **Transpordiamet / Tarktee** | CCTV camera catalog + frames, Estonia road-weather cameras | Public Transpordiamet / Tarktee road camera data | "Transpordiamet / Tarktee — tarktee.transpordiamet.ee" (courtesy) |
| **Stadt Warendorf webcam** | Webcam frames, Marktplatz / Historisches Rathaus (`config/cctv_sources.warendorf.json`) | Public municipal webcam data | "Stadt Warendorf — webcam.warendorf.de" (courtesy) |
@@ -62,9 +65,13 @@ How to read this:
| **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 |
| **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" |
| **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)" |
| **Bike Itaú / Tembici** (`*.publicbikesystem.net`, seven Brazilian cities, Santiago and Bogotá) | Bikeshare station availability: São Paulo, Rio de Janeiro, Salvador, Recife, Porto Alegre, Brasília, Curitiba, Santiago de Chile and Bogotá | Operator GBFS feeds. No licence is published: the feeds' `system_information.json` carries no `license_url`, operator or attribution fields. Attribution is given to the operator. | "Bike Itaú (Tembici)"; "Tembici Bogotá" |
@@ -93,6 +100,8 @@ How to read this:
| **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 |
| **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)" |
| **OSRM on the FOSSGIS routing servers** (`routing.openstreetmap.de`) | Street-following routes for the Directions layer and voice route annotations, via `/api/route` | [FOSSGIS routing usage policy](https://routing.openstreetmap.de/about.html): "Display the required attribution and display a link to 'fix the map'", "Use a valid user agent and, if applicable, a correct referrer", "One request per second max", "No scraping, no heavy usage". The full policy is the German [FOSSGIS Nutzungsbedingungen](https://www.fossgis.de/arbeitsgruppen/osm-server/nutzungsbedingungen/); FOSSGIS also states that the server may be embedded in your own pages but "eine gewerbliche Nutzung ist nur mit Einschränkungen erlaubt" (commercial use only with restrictions). Route data derives from OpenStreetMap (ODbL 1.0) | "Routing: OSRM on the FOSSGIS servers" + a "fix the map" link — shown in the Data attribution popover |
### Local receiver input (not fetched from a service)
@@ -110,8 +119,10 @@ 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).
- **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.
- **OpenSky Network.** Its license is **non-commercial**, and operational use of the REST API in a live product can require a prior written agreement with OpenSky — even for non-profit/government use. If you deploy this commercially, contact OpenSky for your own terms. The flights layer is a toggle and runs anonymously by default.
- **adsb.lol flight fallback.** When OpenSky is unavailable and no last-good OpenSky response exists, the server requests a cached, capped 250 nm adsb.lol point snapshot around the current camera subpoint. This is regional observed context, not worldwide completeness; provenance is exposed in the Flights stats/context row. Military ICAOs remain reconciled through the existing dedicated military registry rather than duplicated.
<a id="gnss-integrity"></a>
@@ -152,7 +163,7 @@ The optional Local ADS-B layer and the broadcast-FM receiver use radio you recei
- **adsbdb enrichment.** `/api/adsbdb` resolves an ICAO hex to aircraft type, model name and registration, and the tracked flight's callsign to an airline and an origin/destination pair. Fan-out is deliberately small because adsbdb is a free community API that publishes no quota: at most 4 requests in flight, dispatches dripped to roughly 5/s, and each key asked for at most once per session, with route lookups reserved for the tracked aircraft. The dev-server proxy caches each key for 24 hours in `.gev-cache/adsbdb.json`, negative answers included, so fresh entries survive server restarts. That gitignored cache remains local to the deployment and is not bundled with this repo. adsbdb publishes no general service license; its README says the route data may not be copied, published, or incorporated into other databases without the explicit permission of David J Taylor, Edinburgh. Deployers are responsible for obtaining any permission their use requires. Enrichment is best-effort and fail-silent — a flight that gets no answer keeps what the flight feed already gave it.
- **Launch Library 2.** `/api/launches` makes a server-side rolling-30-day query against the supported v2.3 detailed launch endpoint, caches successful responses for 15 minutes in memory and on disk, and serves the last successful response during a throttle or transient outage. Anonymous access is limited to 15 calls/hour; deployments can provide `LL2_API_TOKEN` for authenticated access. The Space Devs' published terms permit using and sharing the API data in any form, ask users not to forward it without adding value, disclaim complete accuracy, and encourage—but do not require—attribution. This app keeps a courtesy credit. Payload and stage/recovery records are shown only when supplied. Failed launches expose their source status and never receive fallback orbit geometry or a live/estimated marker. LL2 supplies launch context and event timing, not continuous ascent telemetry or live orbital state.
- **TfL JamCams.** The camera list comes from the keyless `api.tfl.gov.uk` endpoint (an optional `TFL_APP_KEY` raises its rate limit); frames come from TfL's public S3 bucket. The "Powered by TfL Open Data" attribution is required by TfL's terms and is registered in the Data attribution popover.
- **Ontario 511 CCTV.** The camera list comes from the keyless `511on.ca/api/v2/get/cameras` endpoint; frames are server-registered still images under `511on.ca/map/Cctv/`. The source is live-fetched at runtime and attributed under the Open Government Licence - Ontario.
- **Ontario 511 CCTV.** The camera list comes from the `511on.ca/api/v2/get/cameras` endpoint, which now requires a free developer key (`ONTARIO_511_API_KEY`: sign up at 511on.ca/my511/register, then request it while logged in at 511on.ca/developers/doc; throttled to 10 calls a minute). Without the key the pack is skipped. Frames are server-registered still images under `511on.ca/map/Cctv/`. The source is live-fetched at runtime and attributed under the Open Government Licence - Ontario. Access is governed by Ontario 511's Developer's Access Agreement (511on.ca/developers/resources): the data stays under the Open Government Licence - Ontario unless otherwise specified; key requests are reviewed case by case and ask for the intended use and any third parties that will receive the data; attribution by link to 511on.ca is encouraged, and the Ontario 511 logo may not be altered or used to imply endorsement (this app links and shows no logo); and the data is provided "as is", a disclaimer distributors should pass on, which the credit does.
- **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.
@@ -162,21 +173,31 @@ The optional Local ADS-B layer and the broadcast-FM receiver use radio you recei
- **Warendorf webcam.** The Stadt Warendorf Marktplatz webcam, registered from the curated `config/cctv_sources.warendorf.json` (the Kreis Warendorf registration-office cameras from the contributing PR are indoor waiting rooms and were left out); only the municipal image host is proxied, frames are fetched at request time and never stored or redistributed. Disable with `CCTV_WARENDORF_ENABLED=0`.
- **Live Traffic NSW cameras.** The camera list is the keyless public `data.livetraffic.com/cameras/traffic-cam.json` feed (Transport for NSW open data, CC BY 4.0); frames are stills on `webcams.transport.nsw.gov.au`, and only that host is registered. That image host answers non-browser clients with an HTML placeholder, so the proxy identifies as a browser for that one host and as itself everywhere else. Frames are fetched at request time and never stored or redistributed. Disable with `CCTV_NSW_ENABLED=0`.
- **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`.
- **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.
- **Statens vegvesen road cameras.** Keyless. The camera list is one GeoJSON request to the OGC API Features view of the official DATEX 3.1 CCTV site table (`ogckart-sn1.atlas.vegvesen.no/ogc/features/v1/collections/datex_3_1:CctvSimple/items`), so no DATEX account is needed; frames are stills on `kamera.atlas.vegvesen.no/api/images/<cameraId>`, and only that exact path per camera is registered. Cameras the feed reports as faulty are dropped. About 135 working cameras also publish live HLS (`kamera.vegvesen.no/public/<cameraId>/manifest.m3u8`, MPEG-TS); those play as live video through the bounded in-memory HLS relay, with the still as the snapshot fallback, and only that exact manifest path per camera is registered. `CCTV_VEGVESEN_VIDEO=0` keeps every camera on stills. `orientationDescription` names the place a camera faces, not a bearing, so it goes into the label and headings use the id-hash fallback at low confidence. By default every live camera loads (~850); `CCTV_VEGVESEN_MAX_SOURCES` (8–1000) lowers the cap, keeping the cameras nearest Oslo, Bergen, Stavanger, Trondheim, Kristiansand, Bodø and Tromsø and `CCTV_VEGVESEN_ENABLED=0` turns the pack off. Frames are fetched at request time and never stored or redistributed. The NLOD attribution is registered in the Data attribution popover.
- **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 malformed or empty successful payloads are rejected atomically. 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.
- **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.
- **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's public instance, then `/api/geocode` (Nominatim search) when neither answers. `/api/geocode`uses the shared Nominatim forward-search queue at no more than one request per second, sends an identifying `User-Agent` and `Referer`, caches answers for five minutes, shares one upstream call between identical searches already in flight, bounds the queue so a burst is refused rather than held, and drops a queued search whose caller has given up. It asks for one result per search and is not used for systematic or bulk queries.
- **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.
@@ -198,8 +219,12 @@ The [Bhote Koshi event pack](public/events/bhote-koshi-2026/README.md), under `p
| **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) |
@@ -61,7 +61,7 @@ Start with the included data sources, then add your own. Each layer is a separat
- **🎯 Click-to-track anything:** Camera locks on, draws a fading trail, surfaces full metadata — and a tracked fire or vessel hands you off to the nearest live camera in one click.
- **🖊️ Voice whiteboard:** Speak annotations onto the world — real boundary polygons, marks, and routes.
- **🛫 3D hangar:** Real per-class aircraft models — 787, ATR-72, Citation, Bell 206, MQ-9 — and a tracked contact swaps from glyph to 3D model as you close in.
- **🎨 Reskin reality:** GLSL sensor looks over the normal globe — CRT, NVG, FLIR/thermal, Noir, Snow.
- **🎨 Reskin reality:** GLSL sensor looks over the normal globe — CRT, NVG, FLIR/thermal, Noir.
- **🟩 Detection overlay:** Screen-space bounding boxes and IDs on everything in view.
- **🎖️ Military HUD:** Tactical heads-up display with intelligence-style telemetry.
- **🌐 Global Context:** Stage the full situational picture with one switch — and get your exact view back when you leave.
@@ -89,7 +89,7 @@ Start with the included data sources, then add your own. Each layer is a separat
## ⚡ Quick Start
**Start without an account or API keys.**Both paths open the same app with
**Local setup needs no account or API keys.**The app opens with
Esri satellite imagery and keyless terrain. OSM is the fallback if Esri is
unreachable. Flights, military traffic, satellites, earthquakes, public
cameras, radio, and launches are available without keys.
Keep the `env`: bash treats `UID` as read-only, so a plain `UID=1026` in front of a command is silently ignored. If the container can't write the checkout, it says so at start. Docker Desktop on macOS and Windows maps ownership for you.
Behind a reverse proxy, put your keys in `.env` or pass them as environment variables - Provider Settings refuses proxied requests on purpose - and add the hostname you serve it on, for example `GEV_ALLOWED_HOSTS=globe.example.lan` (see [Sharing an instance](#-sharing-an-instance)).
On SELinux hosts (Fedora, RHEL), compose mounts the checkout with `:z` so the container may read it; Docker may relabel the checkout for container access, which can take a moment on a large checkout. Without that, the start fails with `Cannot find module '/app/scripts/docker-start.mjs'`.
Behind a reverse proxy, put your keys in `.env` or pass them as environment variables - Provider Settings refuses proxied requests on purpose - and add the hostname you serve it on, for example `GEV_ALLOWED_HOSTS=globe.example.lan` (see [Sharing an instance](#-sharing-an-instance)). The map routes compare the browser's `Origin` with the `Host` the server sees, so the proxy must pass the original `Host` and, when it terminates TLS, `X-Forwarded-Proto`. Caddy does both by default; for nginx add `proxy_set_header Host $http_host;`, `proxy_set_header X-Forwarded-Proto $scheme;` and `proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;`. On an install outside Docker, that forwarding header also rate-limits the proxy's users apart from your own browser. Under the default Docker bridge network every client on the host, your browser included, reaches the container from its gateway address and shares one Overpass bucket.
</details>
@@ -219,10 +234,48 @@ the container.
</details>
### Path 4 — GitHub Codespaces (cloud, no local install)
Codespaces requires a GitHub account. Overpass queries and OpenAI voice token
requests return HTTP 403 through its forwarded HTTPS URL, even with API keys
in `.env` and `GEV_ALLOWED_HOSTS` configured. Use a local setup for these features.
The codespace runs the dev server in a cloud workspace, so Node and the dependencies
never touch your machine. The globe still renders in your own browser, so it
needs the same graphics there as any other path. GitHub Free includes 120
core-hours a month with no card on file — about 60 hours on the default 2-core
machine. Org and Enterprise plans include no free quota; current terms and
quotas apply.
1. Fork this repository, then open **your fork** on GitHub.
2. Click the green **Code** button, the **Codespaces** tab, then **Create codespace on main**.
3. In the codespace terminal:
```bash
nvm install 24 && nvm use 24
npm ci
npm run doctor
GEV_ALLOWED_HOSTS="$CODESPACE_NAME-4173.$GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN" npm run dev
```
4. Open the **Ports** tab, find `4173`, and click the globe icon to open it in
the browser. The port is private by default, so the URL asks for your GitHub
login. Keep it private: a public port lets anyone use the app and the keys
in your `.env`.
`GEV_ALLOWED_HOSTS` there is required, not optional. The dev server answers only
the hostnames it trusts, and a forwarded port arrives on
`$CODESPACE_NAME-4173.$GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN`. Without it the
page is `Blocked request. This host is not allowed.` Leading-dot wildcards are
ignored on purpose, so pass that exact name.
⚠️ **A codespace is reachable from the internet.** Treat it as the
wider-publish case in [Sharing an instance](#-sharing-an-instance). Provider
Settings is unavailable on the forwarded URL. Never commit your workspace
`.env`. [SECURITY.md](SECURITY.md) has the threat model.
### Then power it up — in the app, not in a file
Keys are upgrades, not prerequisites. When you want one, click the **POWER UP**
chip in the bottom-right corner: Provider Settings lists every supported key,
For a local setup, keys are upgrades, not prerequisites. When you want one,
click the **POWER UP** chip in the bottom-right corner: Provider Settings lists every supported key,
what it switches on, and where to get it. Paste, hit **SAVE KEYS**, and the app
restarts itself with the new capability on. Once everything is configured the
chip reads **POWERED UP** — and if a compact layout hides it, `?setup=1`
@@ -280,14 +333,14 @@ Choose a first-run mission, or try these in order. The GIFs show Google Photorea

6.**Switch the optics.** Tap `1`–`7` — CRT, NVG, FLIR — and the whole live planet re-renders through a different sensor.
6. **Switch the optics.** Tap `1`–`6` — CRT, NVG, FLIR — and the whole live planet re-renders through a different sensor.

7. **Talk to it** _(needs an OpenAI key)_: _"Take me to LAX and select the nearest airborne aircraft."_
8. **Come home.** Hit **Reset Globe** — or just say _"zoom out to a globe view."_
@@ -309,6 +362,12 @@ _Why cockpit mode exists: you're riding a real aircraft over real terrain — an
## 🎙️ Talk to It
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 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.
The OAuth option is experimental. OpenAI documents ChatGPT plan usage for the [Responses API](https://developers.openai.com/siwc/token-sharing-open-source/models-and-inference); a supported third-party Realtime contract for reusing Codex tokens has not been confirmed. A successful token mint does not establish billing or support terms.
@@ -329,13 +388,17 @@ Thirty tools, four jobs — the commands below come straight from the product's
**🎥 Direct it** — drone-operator camera verbs:
“Rotate the globe” spins Earth to the right continuously; ask for left, a speed,
or stop. If it refuses, follow its reset-and-rotate or further-zoom guidance;
camera input stops rotation.
> 🗣️ _"Take me to Tokyo."_ · _"Orbit around this area slowly."_ · _"Draw the walking route from the Capitol to Zilker Park."_ → _"Fly the route we just drew."_ · _"Zoom out to a globe view."_
**🖊️ Annotate it** — a whiteboard over the real world:
> 🗣️ _"Outline the state of Texas."_ · _"Annotate the Texas State Capitol and its grounds"_ — it draws the **actual enclosing boundary**, not a circle. · _"How far is the Eiffel Tower from the Louvre?"_ — a connector arrow appears and it speaks the distance. Everything persists until you say _"clear the map."_
**✍️ Or draw it yourself** — DISPLAY ▸ **Draw**: pick Area, Line or Pin, click the vertices on the real world, double-click to finish, label it. Same whiteboard, same persistence, no microphone needed.
**✍️ Or draw it yourself** — UTILITIES ▸ **Draw**: pick Area, Line or Pin and a colour, click the vertices on the real world, double-click to finish, then name, recolour, hide or fly to each shape from its row. Same whiteboard, same persistence, no microphone needed.

@@ -363,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-one layers and map sources. **Nineteen have a keyless path.** Some offer additional capabilities with a provider key. (🟢 no key · 🟡 free key · 🔴 metered.)
@@ -377,23 +440,25 @@ Twenty layers and map sources. **Eighteen have a keyless path.** Some offer addi
| 🌍 **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) · 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), plus opt-in New York City (NYC DOT TMC, needs your own NYC DOT agreement) and opt-in Lithuania (Via Lietuva / Eismoinfo, photo licence unconfirmed). Positions are published; poses are estimated priors **you calibrate by dragging a gizmo on the camera itself** | City APIs | 🟢 |
| 📹 **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), 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; 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 and **FOLLOW**. Mapillary, Google Street View, KartaView and Panoramax | Mapillary · Google · KartaView · Panoramax | 🟢 KartaView, Panoramax · 🟡 Mapillary token · 🔴 Google Maps key |
| 📷 **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 | 🟢 |
| 🚲 **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 token raises the allowance) |
| 🚀 **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 | 🟢 |
| 🌬️ **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 Utilities.
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:**
@@ -421,9 +486,9 @@ _The Space Missions layer replaying a Falcon 9 ascent — labeled `RECONSTRUCTED
**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.
Drag the panel header to float the panel, and resize it from any edge. Double-click the header or collapse it to put it back in the rail. 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`.
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`.
## 🎖️ Field Missions
@@ -469,12 +534,12 @@ How the globe handles live data:
- **Sits on the real ground.** Entity heights are aligned to work with Google 3D tiles, so aircraft park on aprons and cameras stand on street corners instead of floating.
- **Caching and request budgets.** An OpenSky credit governor, a TomTom daily tile budget, and disk-cached TLEs reduce repeated requests. These controls do not replace provider quotas or billing controls.
- **Server-side credentials.** Every API that touches a private key (OpenAI, AISStream, OpenSky OAuth, camera frames) is brokered through a hardened server-side proxy with SSRF protection, response caps, and sanitized errors. The only keys the browser sees are Google Maps and Cesium ion (restrict both at the provider).
- **No framework.** Vanilla JavaScript, **CesiumJS**, and **Vite** — plus **Google Photorealistic 3D Tiles** for the planet and the **OpenAI Realtime API** for voice. Fast to read, fast to hack on.
- **A small stack.** **Vue 3** and **Pinia** for the interface, **CesiumJS** for the globe and **Vite** to build it — plus **Google Photorealistic 3D Tiles** for the planet and the **OpenAI Realtime API** for voice. Fast to read, fast to hack on.
```
src/
├── main.js # Bootstrap: Google 3D tiles, layer registration
God's Eye View is a local-first client for **public** data. It is built for exploration, demos, and learning — not as a hardened production service. This document explains the security model so you can run it safely and report issues responsibly.
Vite's development file server also denies `.gev-logs/`, `.gev-cache/`, and
the package checkout's root `dist/` directory. Dependency `dist/` directories
remain available. This filesystem deny list does not filter `public/` assets or
preview output; never put private artifacts in a public asset directory.
## Reporting a vulnerability
Please report security issues **privately** — do not open a public issue for anything exploitable.
@@ -29,7 +34,7 @@ These are designed to be used directly in the browser (like a Mapbox public toke
1.**Google Maps API key** — loads Photorealistic 3D Tiles directly and powers Street View (Maps JavaScript API; add it to the key's API restrictions). **Restrict it** (HTTP referrer + API restriction to the required Google APIs) in the Google Cloud Console. An unrestricted key in a public deployment can be abused and billed to you. Restrict it to the **Map Tiles API and Maps JavaScript API only**: a referrer restriction applies to the Maps JavaScript API and the client SDKs, not to the web services, so a key carrying Geocoding under one is refused with `API keys with referer restrictions cannot be used with this API`. Search does not need it — geocoding goes through the server proxies above (#363).
2.**Cesium ion token** (`CESIUM_ION_TOKEN`, optional — for ion-hosted Google Photorealistic 3D Tiles, Bing world imagery, and world terrain) — used as `Cesium.Ion.defaultAccessToken` client-side. Use a public **`assets:read`** token with **URL restrictions** for any hosted deployment. The Community plan has eligibility and usage limits; a public token is not a secret, but it can still consume the account's quota.
3.**Mapillary client token** (`MAPILLARY_CLIENT_TOKEN`, optional, Street Level) — used by the MapillaryJS viewer and Graph API lookups in the browser, and by the server's coverage-tile proxy. Mapillary client tokens are meant for browser use.
3.**Mapillary client token** (`MAPILLARY_CLIENT_TOKEN`, optional, Street Level) — used by the MapillaryJS viewer and Graph API lookups in the browser, and by the server's coverage-tile proxy, which sends it only to `tiles.mapillary.com` over HTTPS (an upstream redirect is refused and answered 502, never followed) and caches tiles in memory only. Mapillary client tokens are meant for browser use.
> The explicit browser `define` block in `build/vite.js` controls exactly what reaches the client: only these three keys. Everything else stays server-side.
@@ -69,15 +74,19 @@ The data proxies under `server/providers/` are written so the browser cannot tur
- **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.
- **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. 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.
- **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/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
@@ -86,7 +95,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
@@ -98,6 +107,11 @@ The dev server is a **key broker**: every server-side key above is spendable by
`compose.yaml` publishes on `127.0.0.1`: **widen that mapping and every LAN
client arrives from the same gateway address, which hands them Provider
Settings too.** Treat publishing beyond loopback as the LAN opt-in above.
Compose forwards `GEV_KEY_SETUP_TRUSTED_PEERS` and `GEV_ALLOWED_HOSTS` from
the shell or project `.env` before the launcher runs. Set trusted peers to
`none` when widening the publish: it becomes an explicit empty list before
Vite loads configuration, so the gateway cannot regain Provider Settings
access. Unset or blank values preserve automatic gateway discovery.
- **Provider-side budgets are the real backstop.** For hard spend protection, configure limits where the money is: OpenAI platform usage limits, Google Cloud budget alerts + per-API quotas, and equivalent controls for any other keyed provider.
- **Pinokio LAN and Cloudflare sharing are refused.** The current supported
Pinokio release re-reads sharing state when an app registers its Open URL and
- Street Traffic gives the two TomTom `one_side` lines of one two-way street
a two-way direction's half share of simulated vehicles each, so the street
is no longer drawn twice as dense as the same street from OpenStreetMap
(quanticsoul4772, #1495).
Loaded 100 of 2071 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.