Keep both sides at the end of the de, fr, it, ja, pt and zh-Hant locale files, where main's scene authoring strings landed next to this layer's.
578 KiB
God's Eye View Current State
Docker Compose mounts the checkout with the shared SELinux :z label so
containers can read it on enforcing SELinux hosts. The separate image-owned
node_modules volume and loopback-only port publishing remain unchanged.
The HUD's AI summary line looks up place names for the view only when the
summary can use them. The page asks GET /api/openai/hud-summary/status once;
a server without an OpenAI key answers { configured: false }, and the HUD then
makes no reverse-geocode or Nearby Places request until the page reloads. A
server without the route is asked once, and an unconfigured summary reply
stops the lookups the same way. Each lookup reverse-geocodes at most two
points: the view center alone below 10 km, the center and one viewport point
up to 750 km, and two viewport points up to 3000 km; Nearby Places is still
added below 25 km. A lookup is reused until the view center moves a quarter of
the visible width, the camera's distance to the center doubles or halves, or
the camera crosses one of those heights. The width is measured from the
camera's distance to the view center, so terrain and tilt scale it. A reused
lookup asks again only for what it did not get (a timeout, a failure, an empty
answer); everything else is a cache hit. A small pan over a reused lookup
repaints the last AI reply instead of asking for a new one. A host passing
cachedOnly keeps reading every cached sample. Voice context lookups are
unchanged.
The HUD's LAT/LON degrees-minutes-seconds readout rounds the coordinate once, to
hundredths of a second, and then splits it. A value within 0.005" of the next
whole minute carries into the minutes and degrees, so -122.35 reads
122°21'00.00"W rather than 122°20'60.00"W. The field widths and hemisphere
letters are unchanged.
Aircraft detection candidates reuse normalized display text until callsign or registration changes, including in-place enrichment. Each candidate resolves its fleet model once per collection. Positions, model ownership and tracked-aircraft anchors remain live on every collection; contact admission is unchanged.
Civil aircraft share one delayed render timestamp per fleet update. Extrapolated fixes reuse their local coordinate frame until their coordinates change, including in-place ground-height corrections. Update cadence, contact counts, interpolation, turn rates and tracked-aircraft motion are unchanged. Cached frames are weakly keyed by each position fix and do not retain retired history.
Touch-first devices use three times the default Cesium zoom sensitivity, so pinching traverses globe and street scales with fewer gestures. Cesium retains its distance-aware zoom, collision handling and minimum/maximum distance limits. Fine-pointer desktop mouse and trackpad sensitivity is unchanged.
Analyst region queries resolve unique Natural Earth names offline without geocoding, so Gold Coast and Ivory Coast name the West African natural regions. Duplicate names require a geocoded containment anchor; when several entries for the same name contain that anchor, the lookup stays unresolved rather than selecting the largest entry. Natural outlines try exact names before aliases and broader suffix variants. Follow-ups retain boundary provenance, and cancelled lookups preserve the previous analyst cohort and boundary source. Navigation search keeps a successful geocode, with no search or view-based region recovery.
Manager-owned periodic network updates pause while the document is hidden. Returning to a visible tab runs one overdue update per enabled layer without overlapping an active refresh and restarts that layer's polling schedule from the return, so the next poll follows one interval later whether or not the catch-up update succeeds; explicit refresh requests remain available. Transit also defers camera-driven feed admission while hidden and rechecks coverage on return. Visibility listeners are released with their polling intervals.
Terrain height requests accept at most 64 points, matching the browser service's existing batch size and one bounded upstream chunk. Larger requests return HTTP 413 before disk-cache loading or upstream work; malformed coordinates remain 400. The browser continues to split larger workloads into sequential 64-point requests.
Traffic holds continuous rendering only while enabled with animated dots or a pulsing jam heat-line. Empty and high-altitude views release the hold. A failed load leaves no hold only when no animated content is retained: if an earlier load succeeded, ingestion keeps its animated points and the hold stays. Population admission reacquires the hold and resets animation time. Clear, disable, and destroy also release it. Retained fading dots keep animation alive until they are removed. When the last point leaves mid height correction, the correction is settled at once, before the hold is released, so it cannot keep the layer reporting loading.
Shared distance checks use src/geoDistance.js with { lat, lon } points and
the IUGG mean spherical Earth radius, 6371.0088 km. greatCircleKm is
canonical; greatCircleMeters is its exact metre conversion. Missing,
non-finite, and out-of-range points (latitude outside ±90, longitude outside
±180) return Infinity, so threshold and nearest-item consumers exclude invalid
positions. Older exported haversineKm entry points remain as compatibility
wrappers only. src/data/approxDistance.js, the equirectangular
viewer-proximity approximation, is deliberately separate, as are intentional
local angular, projection, ellipsoidal prefilter, and small-viewport
approximations. Four great-circle copies remain for a follow-up:
src/data/adminBoundaries.js and src/tools/area.js pass unwrapped longitudes
across the antimeridian, so they cannot use the strict ±180 check as-is, and
src/sources/adsbRecords.js and server/providers/common/geo.js keep their own
implementations.
Prohibited & Restricted Airspace — FAA special-use airspace
The Infrastructure group includes Prohibited & Restricted Airspace (drone-airspace,
share-link token 6), off by default. /api/drone-airspace pages the public
FAA Special Use Airspace FeatureServer for TYPE_CODE P (prohibited) and R
(restricted), simplifies geometry, caches for an hour, and serves the last
good snapshot as stale when a refresh fails. The globe draws ground-clamped
outlines with a translucent fill: red for prohibited, orange for restricted.
Selecting a zone shows name, an altitude range built from the
LOWER_*/UPPER_* value, unit and reference fields (for example SFC to 5,000 ft MSL or 20,000 ft MSL to Unlimited), times of use and controlling
agency,
plus a notice that the layer is informational only and not a substitute for
official UAS flight-planning tools. Pick ownership is unregistered so sibling
layer cards still work over a zone. Voice can toggle the layer and query the
loaded records. The layer covers P and R zones only, not the FAA UAS Facility
Map or National Security UAS Flight Restrictions; the label and legend say so.
See the live-layers table below.
Vessel sources can report healthy empty coverage using a current, complete snapshot with zero raw rows, a healthy transport and a positive last-message time. This clears obsolete contacts and settles startup without manufacturing position timestamps. Stale, incomplete and malformed empty results retain the existing recovery behavior.
Every /api response on the dev and preview servers carries
Content-Security-Policy: default-src 'none'; sandbox and
X-Content-Type-Options: nosniff. apiResponseHeadersPlugin sets them ahead of
the Host check and matches /api the way the router mounts it (any letter case, a
/ or . boundary, absolute-form targets), so errors, partial responses, Host
refusals and unknown routes are covered too; a route's own policy is sent as an
additional policy, never in place of it. The app document's policy is unchanged.
The GBFS proxy relays only bodies that parse as JSON, re-serialized as
application/json; charset=utf-8; anything else answers 502.
Catalog source contracts and their consuming layers are declared in
src/app/sourceComposition.js. Omitted or null acquisition sources preserve layer
registration and serialization, report unavailable, and are refused by the existing
visibility lifecycle before initialization. Availability is held in an explicit
catalog lookup supplied to the lifecycle, without modifying layer instances;
unavailable reasons use the layer display name. Invalid supplied methods still fail
construction. createStandaloneApplication and createStandaloneCatalog accept
sources overrides; unspecified entries retain
standalone defaults, and null entries explicitly remove a source. Street Level
continues to accept an explicit provider list. An empty Street Level provider
list uses the same unavailable lifecycle and disabled layer toggle. Directions (getRoute) and
Recent Imagery (searchHls, getThumbnail, getTileTemplate) use the same source
configuration boundary. Standalone selects their default adapters; layers retain
route normalization, imagery selection and rendering. Recent Imagery adapters
consume the existing HLS/VIIRS product schema and provide their own tile attribution.
The source factories are exported at layers/directions/source and
layers/recent-imagery/source. Configuration is applied at construction time;
this does not introduce live source replacement.
Application catalog composition accepts an optional streetLevelProviders list.
An explicit list replaces the default Mapillary provider; an empty list removes
all imagery providers. Without that option, Mapillary is registered only when
sources.mapillary is supplied. An absent source leaves Street Level registered
with a truthful no-provider state, preserving layer controls and share tokens.
Malformed selected sources and provider definitions still fail validation.
An explicit provider list bypasses the unused Mapillary source, including its
validation. Layer share identities and the legacy Mapillary m option remain
stable. Custom provider switches use the r option (example-1*panoramax-0),
with URL-safe separators and a shared layer-neutral boolean-switch codec, sorted by provider ID rather
than registration order, and round-trip through links and stored state. A receiver
applies only switches for its registered providers; absent custom switches leave
current defaults unchanged. Explicit provider toggles persist only their requested
switch IDs through the state coordinator; unrelated live values remain transient.
The custom switch field is bounded to 256 characters;
provider registration rejects compositions that cannot fit rather than truncating
identities. A malformed or duplicate entry invalidates the complete custom-switch
field. Existing whole-link size limits still apply.
God's Eye View in conversations — October 2, 2026
Tool answers that can be shown in God's Eye View include a view: camera, layers,
style, map, marks and an aircraft or satellite to follow, written in the
share-link format (gods-eye-view/view). show_in_gods_eye_view shows a view as live
God's Eye View inside clients that display MCP Apps, and as a link everywhere.
The panel runs the app's panel build (npm run build:panel, served at
/panel/) and loads it, with its data, through the MCP server, so it works
in Claude Desktop and in the Codex and ChatGPT desktop apps with a local
server and no HTTPS. ?embed=1 shows the app as the globe alone and takes
new views from the page that frames it; framing is off unless
GEV_EMBED_FRAME_ANCESTORS allows the framing page. MCP leads with the tools that find
what to show. See tools and the MCP server.
Stdio processes from one install share the panel key in
.gev-cache/mcp-panel-key, resolved from the checkout rather than the client's
working directory. First creation is atomic; malformed-file repairs select one
winner and concurrent processes re-read it. A busy repair waits at most two
seconds before falling back. If the cache cannot be read/written or repair remains
busy, stderr reports the fallback to a per-process key; cross-process panels can
still fail in that mode. An abandoned .repair-lock is left intact: after
confirming no repair process is running, remove it to allow repair on restart.
HTTP MCP servers retain per-server keys. This key is a panel routing guard, not
authentication. Large-response continuations remain process-local and require
consistent tool-call routing. Two-process reproduction is covered locally;
Windows/Claude Desktop Cowork confirmation remains outstanding (#927).
Tools and local MCP server — October 1, 2026
Tool argument string lengths follow JSON Schema Unicode code-point counts, so supplementary characters count once rather than as two UTF-16 units.
Query timestamps outside the representable Date range are reported as null, preserving the rest of the result row.
gods-eye-view/tools defines queries that answer questions from the app's data,
and gods-eye-view/tools/mcp exposes a composed catalog over the Model Context
Protocol. npm run mcp serves Core's tools over stdio to a local MCP client,
reading from a running app's /api routes (default http://localhost:4173).
The development and preview servers also serve the tools over HTTP at /mcp,
accepting only direct local requests: a loopback host on the port reached,
an Origin (when sent) from that same host, no proxy forwarding headers, and
launcher sharing off.
Queries cover earthquakes, active fires, recent launches, aircraft (in an
area, by identifier, tracks, and type and route lookups), ships (in an area,
by identifier, and tracks), satellites (next
pass over a point, and those overhead now), public cameras (including a
camera's current image), license plate reader cameras, radio stations, place search, routing, bike-share
stations, transit vehicles, road traffic flow, weather, weather map images
(radar, satellite, lightning), wind, the most recent satellite image of an
area, submarine cables, datacenters and dams, the Bhote Koshi flood event pack, regional briefs, tropical cyclones, fire
perimeters, terrain height, military installations and map features, plus a
combined situation brief, military awareness around a point, the app's heads-up display caption, and a link that
opens the app over an area. Tools reuse the layers' portable source factories, take a
shared area argument (place name, bounding box, or point and radius) and cap
lists at 25 rows by default. aircraft_in_area and vessels_in_area can also
count every match by a field (group_by: airline, read from the ICAO
designator in the callsign, or operator, country or type for aircraft; type or
destination for ships; the ten largest groups, then other and unknown counts;
the summary says when a feed reports none of the field) and summarize airborne
altitude and speed (stats: count, min, median, mean, max).
Both cover all matches, not just the rows returned; voice leaves these two
tools out, so they reach MCP clients only. Voice offers the same queries next to its app
actions: the session lists them, and the browser runs them through the same
catalog, loaded on first use. See tools and the MCP server.
Aircraft altitude-band queries require a reported barometric or geometric altitude
for each match, so a grounded contact that reports none is left out of a band too.
Queries without an altitude band retain unknown-altitude contacts.
Catalog calls check their cancellation signal immediately before executing a tool. Already-cancelled queries do not start; cancellation after execution starts continues to follow the existing tool and caller behavior.
Fire-perimeter queries test polygon interiors and holes on continuous unwrapped
longitudes, matching the short-edge convention used for boundary intersections.
An outline crossing ±180° contains nearby boxes and radius areas without also
containing their counterparts near Greenwich. Returned perimeter coordinates
use a circular mean for longitude, including multipart outlines. Incident cards
also use a circular longitude mean over the largest outer ring (excluding its
repeated closing vertex). Their relative-area comparison follows short longitude
edges so a small dateline-crossing part cannot outweigh a larger ordinary part.
Regression coverage lives in src/tools/area.test.mjs,
src/tools/queries/environment.test.mjs and
src/layers/perimeters/cards.test.mjs.
Scope display controls
Display's native Scope checkbox defaults ON with feather at 100%, adjustable
from 0–200%. OFF disables the slider while retaining its value. HUD layout
changes preserve both settings; share links and scenes restore them. The
historical missing-scf fallback remains 35%; explicit 0% keeps the hard edge.
With Scope OFF, Tactical and Minimal add text-sized black backplates at 70% opacity behind telemetry, vertical metadata and scene chrome. Original HUD text color, glow and opacity are preserved. Scope OFF brightens the tagline and Active Style label with the shared secondary text color. The title plate covers the eye and title without moving the title or tagline. Scope ON retains the original HUD presentation; Cyber retains its existing skin.
node scripts/qa-scope-hud.mjs checks native input, range, layout persistence,
restoration and both Scope presentations. QA_SCREENSHOT_DIR saves screenshots.
HUD layouts — October 29, 2026
Display > HUD > Layout offers Tactical, Minimal and Cyber; the set_hud voice
action accepts the same three. The Operator layout is retired. Share links
keep its enum code so older links still parse, and a link, scene or saved state
that names Operator restores the default Tactical layout. Operator was the
only layout that showed the HUD's top banner bar, so its markup and styles are
removed too.
Cyber HUD — September 30, 2026
Display > HUD > Layout includes Cyber, also available through the HUD voice action and shared visual state. An explicit first transition into Cyber selects FLIR with Ironbow 0.42. An explicit switch back to another HUD layout restores the visual preset used before Cyber; restored links and scenes remain authoritative. The skin uses shared red/slate panel treatments in map and cockpit, with compact 200px collapsed controls and wider expanded panels. Other HUD layouts retain their existing presentation. Chamfered panel expand/collapse controls use an inset keyboard-focus outline and red hover border in both map and cockpit, so clipping does not hide the highlight.
Cyber's map-side right rail opens one panel at a time while keeping collapsed launchers available. Display, CCTV and Context scroll their contents inside fixed headers and decorative frames. The narrow-screen rail remains scrollable to reach each panel. Radio retains the shared nested Context player without relocating playback controls on theme changes.
Voice help/error popups and Location/Visual Presets pins extend beyond their Cyber frames without being clipped: the shapes decorate non-interactive pseudo-elements. The lower-left telemetry card leaves room for attribution's full logo row. New panels can use the shared surface contract for theme tokens, rail input and a fixed header with a bounded scroll body. The contract is intentionally future-facing and opt-in; its first production adopter will land after this change. Existing owners still control disclosure, persistence and placement, while small-screen rail popup clipping and other integration limits remain documented in the contract.
On desktop, Cyber raises the left rail so it clears the lower coordinate card.
layoutRightPanelRail reads that rendered top as its measured baseTop, which
keeps the right rail aligned. Cockpit retains its separate rail placement for
its visor layout.
Sonar has one contact-highlighting method. Native Cesium points, billboards and labels are treated in GPU draw commands without replacing their positions, identities or pick commands. Model and canvas/DOM overlays retain their own rendering paths. Sonar OFF stops the sweep while retaining Cyber's neutral contact treatment. Reduced motion disables the animated sweep; cockpit and hidden HUD states do not apply the map treatment. If the native shader is not supported, native contacts remain available and Display reports the limitation. No CSS scene-dimming fallback or effect selector is exposed.
The Sonar controls adjust rings, range, power, opacity and sector. Sweep opacity affects detection painting rather than label cohort admission. Theme exit and renderer teardown release the derived GPU resources and restore owned model colors without replacing newer layer-owned color changes.
The set_cyber_sonar voice action adjusts the same controls without switching
themes or enabling omitted settings. State readback distinguishes configured
settings from active map/contact effects. Sonar settings are session-only and
return to their defaults on reload. Sliders accept integer values matching the
action's ranges.
Wind appears in the Weather group before Utilities. The surface-weather prototype uses keyless NOAA GFS or ECMWF IFS forecasts on an approximately 1° display grid. It defaults to 10 m wind trails over the existing basemap. Speed shading is an explicit choice; earlier v2 links retain their original speed-shading meaning. Weather rows contain only the toggle and source/meta line. Configuration lives in each WEATHER card's Settings section: model, No color field, Speed, Pressure, Temperature, wind units, Pause/Resume and Inspect center for wind; coverage, image treatment and opacity controls for observed products. Cyclone cards carry a Storms section whose entries select and fly to the advisory, plus the official advisory link. Settings and Storms default closed; disclosure choices last for the page session.
Inspect center adds an open Reading section to the wind card with captured coordinates, interpolated wind speed and meteorological direction, selected scalar, model, valid time and freshness. Clear reading removes the sample and map marker. Model/field changes or refresh also clear them. Unit chips reformat the captured sample without resampling or moving its marker. There is no separate floating reading surface. The card shows forecast issue and valid times; the observed-history timeline labels the oldest and newest available ticks. Animation moves through a fixed forecast; it does not advance forecast time.
Temperature is air temperature at 2 m in °C; pressure is mean sea-level pressure in hPa. Optional companion fields come from the same model run/forecast as the wind. A missing or invalid companion leaves usable wind visible and identifies the selected field as unavailable. The color texture drapes the globe basemap; on photorealistic 3D Tiles the same 360×181 raster is a raised shell 5 km above the ellipsoid. With GPU rendering, the color field fades out below ~1,200 km camera height and is hidden at or below 200 km. Globe imagery keeps the smooth per-frame fade. On 3D Tiles, the shell alpha is quantized to 0.1 steps and updated only on camera move end, installation or rehome. GPU wind curves follow the sampled forecast field. Their 12 km display lift is a rendering aid; the source remains 10 m wind, not a forecast at the displayed height or a street-level observation.
The renderer owns field installation, scalar imagery and the animation lifecycle. It bakes at most 7,200 curves (1,200 below 700 px) with at most 33 geographic points each on the CPU when installing a wind field, then advances a GPU phase along those curves without CPU projection of every point on every frame. Regional 30° batches are culled against the horizon and view frustum each frame, including when the globe is hidden; curves fade below 60 km and disappear at 15 km. Animation stops when no batch is visible and resumes on camera events. A canvas renderer remains available as a fallback. Pause and reduced-motion mode show a static flow view without an idle animation loop; hidden tabs suspend animation, and disable/destruction releases owned rendering resources and subscriptions. The globe relief helper is enabled with Wind and released when Wind is disabled. It uses terrain vertex normals for view-directed shading, falling back to global globe-curvature shading when normals are unavailable; the fallback does not show local hillshade. Neither mode adds elevation data or represents sunlight. Relief declines to replace an existing globe material, restores the prior empty material only while still owning it, and leaves later owners intact. Source acquisition retains deadlines, body budgets, disconnect cancellation, per-model/field singleflight caching and one-minute failure backoff. Application catalog construction owns each instance.
Wind animates one forecast without advancing forecast time. Separate Weather
observation layers provide radar and satellite history; none claims measured cloud volume.
Weather imagery releases its camera move listener on disable and registers it once on re-enable. Disabling also clears active imagery work.
A staged weather frame with loaded tiles and no pending requests rechecks
readiness on a single 150 ms wake timer instead of requesting every render.
The existing readiness conditions and two settled frames still govern promotion;
completion, cancellation, replacement and teardown clear the wake timer.
Weather imagery drapes onto the globe basemap. On photorealistic 3D Tiles each
observed product is instead a raised, translucent shell: one rectangle over the
product bounds at a fixed height (global infrared 5.5 km, regional infrared
5.8 km, radar 6.2 km, lightning 6.6 km) showing one full-extent image per frame.
Draping onto 3D Tiles was limited by Cesium's per-primitive texture budget and
rebuilt every tile's coverage on each change; shells avoid both and show at any
camera height. Shells draw first in the opaque pass, in height order,
alpha-blended without depth writes: lightning draws over the other products,
other map content draws over the shells, and shells are not pickable. A
map-source change tears down one renderer and restages the shown observation on
the other. A map without an imagery host pauses history while metadata refresh
continues, then resumes when a host returns.
Globe tiled products use 256 px tiles to maximum level 6. The tile proxy accepts
size=256 (default), 512 or 1024 and keys cached bytes by size, retaining the
24-hour immutable response and eight upstream slots. The image proxy serves every
product as one whole-extent PNG at its advertised bounds, up to 4096×2048 for
radar, regional infrared and lightning and 2048×1024 for global infrared
(size=W×H, default the maximum), capped at 16 MiB and cached by product, time
and size. NOAA returned each of these sizes from a single request in a live
check; the WMS capabilities advertise no size maximum. Devices whose texture
limit is below the product size request halved images.
On 3D Tiles, each shell except global infrared also draws a detail window: the
shell's own surface samples a second, 4096×2048 image of the area around the
view inside that window, so the two images share one mesh and never blend. On
camera move end and host switch the window is centred on the camera's ground
footprint, max(2 × its longitude span, 6°) wide and half as tall, on a 0.5° grid
inside the product bounds. It is used only while narrower than half the
product's longitude extent and while the view overlaps the product; it moves
only when the view centre leaves its inner half or the span changes by more
than 50 %. A window applies once its image has drawn; until then the
full-extent image covers it. A new frame swaps the full-extent image first and
keeps the previous detail image until the new one decodes, over at most one
newer frame; a moved window hides until its image arrives. Global
infrared contrast depends on the requested extent, so it keeps one image. The
image proxy accepts bbox=west,south,east,north for every product: inside the
product bounds, 2:1 within 1 %, rounded to 0.25°, up to 4096×2048 (the
default), cached by product, time, size and bbox. Diagnostics report
shell.detail (bbox, size, ready, enabled).
Clouds only applies a soft brightness ramp to decoded pixels once, using
Cesium's sRGB-to-linear conversion (channel ** 2.2) and smoothstep from 0.40
to 0.70. The old 0.55 threshold is the ramp midpoint. RGB and source alpha are
preserved in Full image mode; both modes use the chosen layer opacity. This
is a display filter, not a cloud mask. The cloud-filter slider is a continuous
0–100% strength: 0% is Full, 100% is the existing Clouds only key, and
intermediate values blend their alpha. The value commits on slider release to
avoid restaging for every pointer event. Status refreshes retain the uncommitted
drag preview; changed model values or disabled controls supersede it.
Satellite share links carry the filter
strength; legacy f/a values keep their Clouds only / Full meanings. Soft and
Vivid remain layer-opacity presets (40%/70%); history remains transient and links
open latest.
Global infrared fetches one capped 4 MiB, 2048×1024 mosaic per frame and decodes
and processes it before staging. Globe hosts crop that canvas into 256 px tiles
to maximum level 3 on a geographic 2×1 root grid bounded to the manifest extent,
avoiding request-dependent contrast seams.
On globe hosts, regional infrared retains network tiles and processes each
decoded tile once; shells process each whole frame once.
Exact-time tile and image responses are immutable for 24 hours; manifests and
errors remain uncached. Each globe renderer retains up to 6 processed global
mosaics in a least-recently-used cache keyed by observation time and infrared
mode (up to 48 MiB of canvas pixels). Each shell keeps decoded full-extent and
detail images in one least-recently-used cache bounded at 128 MiB, keyed by
time, mode and window: the shown frame and the warmed next frame, each
full-extent and detail; nothing older survives them. Disable clears the caches.
Cache hits skip
fetch/decode and report mosaic.cached: true with zero decode time.
During playback, a successful frame warms the next advertised observation,
wrapping at the end. Global imagery warms a decoded frame and shells warm the
next frame and its detail window; globe tiled
products fetch at most eight level-0/1 tiles intersecting the view and product
bounds. Prefetch is best effort, has a deadline, and cancels on frame
replacement, pause, suspension or clear. Diagnostics expose the renderer
(host), mosaic count and active prefetch state.
Browser cache reuse and scrub-back latency still require browser verification.
On globe hosts a replacement becomes visible before the previous layer retires
on the next rendered frame; a shell keeps its previous texture until the new
image is uploaded. Failed acquisition or staging retains the previous
observation. Imagery ordering skips already ordered layers. Diagnostics expose
the infrared mode and mosaic fetch/decode state.
Throttled weather requests (429/503) get at most three retries per tile within
a frame, independent of other tiles; successful requests reset only their own
counter, and closing a frame clears its retry state.
Mapped.earth's public bundles informed the rendering study; no code or assets
were reused, and the study found no application licence granting reuse. Native
hardware GPU behavior remains unverified; software-rendered checks do not
establish native GPU performance or compatibility.
The right-rail WEATHER panel owns active-product readouts, coverage badges, legends and the single observed-history timeline. The timeline appears with an active observed layer and at least two union times; wind alone does not show it. Dragging previews UTC and age locally and coalesces clock commits at 150 ms; release commits immediately. Latest selects the newest frame per product, and history labels the actual displayed frame as synced or nearest. Missing-frame messages use each product's eligibility gap (30 minutes, or three hours for the global mosaic). Wind shows forecast valid and issue times and does not follow history. Satellite clouds uses Clouds only / Full controls, a continuous cloud filter slider and Soft/Vivid opacity presets in its right-rail settings.
Left weather rows retain only the toggle and one status line with a source tooltip. Configuration and the cyclone Storms list live in the cards, as do the cyclone advisory, position time, wind, pressure, geometry status and the official advisory link. The panel hides when empty, auto-expands once per page session and then preserves the user's collapsed state. Status lines reserve their space and refreshes are coalesced per frame. Generic keyed rail cards and the native rail timeline preserve DOM nodes and avoid identical writes; the weather panel owns descriptor mapping. Inspection emphasizes the chosen scalar and marks the exact sampled location; the passive marker follows that snapshot, respects globe occlusion and disappears on dismissal, field/model changes, disable or teardown. Clean view and recording mode hide weather presentation with the other controls. Scalar changes reuse native wind geometry when the model, issue/valid time, grid and U/V values are identical; new or revised wind still rebuilds. Source and renderer clocks remain independent of the shared Cesium clock.
Weather also offers default-off lightning density and cyclone advisories. Lightning uses NOAA/NWS nowCOAST's public derived 15-minute density product from Vaisala NLDN/GLD360, with ten-minute metadata refresh and exact source times. Coverage is the Americas/Pacific (110°E across the dateline to 0°, 25°S–80°N), not worldwide detections; units are strikes/km²/min ×10³. It is neither raw GLM flashes nor a live strike counter. Weather imagery orders scalar context below satellite, radar and lightning consistently across enable and history order.
Cyclones combine fixed NHC/CPHC status and summary GIS endpoints through a bounded
same-origin provider. The status endpoint lacks browser CORS; each installation
fetches keyless public sources at runtime, with no centralized ingestion service.
The five-minute snapshot covers Atlantic and eastern/central North Pacific only.
Classification labels follow NHC status codes: PC is a potential tropical
cyclone, PTC a post-tropical cyclone, and STS/STD a subtropical
storm/depression. Unknown codes remain visible unchanged.
Current positions, advisory issue time and position time stay distinct. NHC may
release an advisory before its nominal time (routinely up to about 30 minutes,
rarely about an hour), so advisory issue and position times up to and including
90 minutes ahead of the server clock are accepted; 90 minutes is this app's
tolerance, not an NHC limit. Later, non-canonical, or more than 12-hour-old
times still reject the status snapshot. Tracks,
forecast lead-hour points and cones render only when all source parts match the
status advisory; a newer status shows its position with geometry pending rather
than relabeling older geometry. The cone represents forecast center uncertainty,
not storm size or the full hazard area. Successful empty and unavailable states
are distinct. One owned native Cesium data source preserves polygon holes and
geographic seams and releases on disable; forecast animation is not implied.
NHC movement speed is converted from miles per hour to rounded knots by the
proxy; advisory wind intensity is already in knots and is passed through.
Cyclone markers, labels, tracks and cones are hidden beyond the horizon on every map source.
Consult the linked official advisory for safety decisions.
Voice and HUD snapshots reuse the existing feedState classifier. Analyst follow-ups retain their original data provenance; current-view results append provenance without replacing legacy fields. HUD context and deterministic telemetry include non-nominal feed state.
Analyst records come on demand from each layer's getAnalystRecords: flights, military, local ADS-B, vessels, satellites, launches, earthquakes, FIRMS (VIIRS and MODIS, with a sensor field), fire perimeters, drone restrictions, cyclones, transit, bikeshare, ALPR cameras, mapped installations, datacenters and dams. analyst_query counts the whole loaded set (up to 250,000 records per layer; past that coverage.records reports returned, total and truncated: true and the result carries complete: false). Ranking keeps a top-k and scans in 20,000-row slices that yield to the page and stop when superseded. It refuses unknown fields, operators, units that do not convert (altitudeM(ft) converts), wrong-typed values, invalid centres, unknown scope kinds and sort fields with code and allowed, refuses a follow-up with no previous answer (NO_RESULT_CONTEXT) or naming other layers than that answer (FOLLOW_UP_MISMATCH), and tells LAYER_OFF, NOT_READY and FEED_UNAVAILABLE apart from a real zero; when only some layers can answer the result is partial with unanswered ids. The Contacts-window path applies the same filters, ranking and item shape to one immutable panel snapshot. Each aircraft layer retains at most 20,000 rows; reaching that bound sets complete: false and the spoken/card count becomes “At least”, never an exact claim. A superseded query never replaces follow-up memory, while sibling analyst calls from one assistant response do not cancel one another. Items carry lat/lon; the feed window (earthquakes and FIRMS: last 24 h), the precise scope and any caveat sit in display, which is on-screen detail rather than something the model recites. "In view" is still a radius around the view centre, not the visible footprint. Satellite distance is ground distance.
Keyless terrain tiles retry when Re:Earth throttles them. The browser fetches
terrain.reearth.land/cesium-mesh/ellipsoid/{z}/{x}/{y}.terrain directly; no
proxy in this repository sees those requests (/api/terrain/heights is the
separate point-height endpoint), and the upstream edge answers zoom-out bursts
with HTTP 429 under load. src/maps/terrain.js hands Cesium a Resource
whose retry policy (src/maps/terrainRetry.js) every derived tile fetch
inherits: 429, 502, 503 and 504 replies retry up to three times, 750 ms
doubling to a 15 s cap, with every retry waiting behind one shared cooldown
plus up to 1.5 s of jitter, and Retry-After extending the cooldown when the
upstream exposes it. One console warning is logged per cooldown window. Other
failures keep Cesium's handling and the flat EllipsoidTerrainProvider
fallback; while that flat terrain is on a shown globe the map controller
retries Re:Earth in the background (30 s, doubling to a 10-minute cap, each
attempt abandoned after 60 s) and installs the real terrain without waiting
for a map switch or an imagery retry. Behind a 3D tileset it neither retries
nor installs; the next globe activation does.
scripts/qa-terrain-429.mjs reproduces the burst against a keyless
dev server, counts real throttles with their headers as evidence, synthesises
throttles with --inject N when the upstream is not throttling, and fails if
a throttled tile never loads.
AIS encodes speed over ground in 0.1-knot units and course over ground in 0.1-degree units, reserving the top code of each field for "not available", so those reports arrive as 102.3 knots and 360 degrees. Both are stored as unknown, and vessel cards, the HUD and analyst queries show a missing measurement rather than a reading. Heading keeps its existing separate check for its own sentinel. Genuine readings are unchanged, including a stopped vessel's zero and the highest speed and course the fields can encode.
For an already tracked vessel, AISStream static messages update name, type, destination and IMO without changing its position, motion, fix timestamps, position freshness or trail. Non-empty static values replace earlier values; omitted, empty or unavailable fields retain known metadata across Class B's separate reports and in the cache used by the next position update. Position reports still prefer their own metadata name. Invalid Class B parts are ignored; absent fields prefer the latest live value over the static cache.
Native <select> controls declare a dark colour scheme and explicit option
colours, so the browser-painted option list matches the panel it drops out of.
The scheme is declared on the controls rather than on the document root, so
hand-tuned scrollbar and text-input skins are unchanged. Applies to the HUD
layout, Authoring, CCTV camera, Radio filter and Draw colour menus.
Vessel snapshot completeness is separate from freshness. A current snapshot with rejected or duplicate records shows PARTIAL with accepted/received counts; stale or unknown freshness and transport failures retain their warnings. Partial snapshots still retain missing contacts within the existing age and row limits. A complete successful snapshot clears the partial indicator.
Director imports now open a non-mutating preview before Apply. EDIT DETAILS authors validated anchor, camera, pack and interaction drafts; SHARE SCENE exports a selected scene or a bounded bundle of explicitly chosen pack files. Bundled bytes remain in memory until replacement/teardown and require reimport after a page reload. Stop releases rendering while retaining replay bytes. Cancellation and stale drafts cannot replace newer project state. See authoring and sharing.
Director version-6 documents add bounded, scene-local feature actions. Settled LOAD/seek exposes keyboard-accessible text/source cards, anchor focus, explicit shot transitions and admitted layer state changes. Pointer claims take priority; Stop/replacement/teardown cancel pending actions and release handlers and UI. Same-shot seeks rebuild selected packs and restore declared layer baselines. See scene actions. Existing scenes/assets and credit remain.
Director version-5 documents add scene-local data-pack manifests and per-shot selection. Registered sources acquire bounded, cancellable assets separately from GeoJSON, PNG and manually played media presentation. Storage paths, attribution and geographic placement remain distinct; Stop/replacement releases resources. Import acquires nothing. See data packs. Existing scene content, assets and credit remain unchanged.
The asset directory source rejects with the caller's abort reason if cancellation arrives during any read, including the final one, or while the reader is being released.
Director version-4 documents add named geographic anchors and optional explicit camera moves with easing, duration and holds. Playback and seek share one coordinate sampler; navigation/manual input revokes authored motion and pending holds. Anchor heights and explicit endpoints require the WGS84 ellipsoid reference. Legacy projects retain their ordinary flights, content and edits. Seeking between ordinary camera poses uses the shortest longitude arc across the dateline while preserving both authored endpoint longitudes, and turns heading and roll the short way for any authored angles. A seek to a shot's start, as a shot action makes, selects that shot rather than the end of the previous one. Same-shot seeking restores declared layer visibility after a manual toggle, including one still in flight, by reconciling only the changed layers through the cancellable layer admission path. Refused restoration reports a failed seek and keeps the last settled position. Undeclared operator layers remain untouched, and the shot is not rebuilt. See camera directions.
Realtime voice composes separate connection, response/tool, Radio handoff, input and audio-meter, cost, viewport-context and diagnostic owners. The existing controller exports and session/backend contracts remain available. Tool protocol, response wording, push-to-talk timing, model preferences and Radio confirmation order are preserved. Stopped connections, stale offers and delayed captures cannot affect a replacement session; audio meters release failed initialization and reject revoked frames. Delayed action results and post-capture continuations cannot resume a stopped conversation or send output into a replacement. See voice ownership.
The opt-in voice panel shares Preview and Voice Settings, with browser-local
Preview and inactivity preferences. Voice-requested timeout changes apply directly
without a confirmation question; results distinguish a live change from a saved
browser preference. set_voice_inactivity_timeout accepts integer minutes from 1–60,
or 0 for None. Named voice-settings and voice-preview panel targets exit
Cockpit before opening the requested voice UI. control_cockpit also accepts
voicePanel: "preview" | "settings" for an explicit exit-and-show request.
Newer manual choices, Stop and provider replacement prevent an older operation
from revealing its UI. Ambiguous requests to open settings ask whether the user
means Voice Settings or Provider Settings before opening either panel. Named
Voice Settings applies directly; Provider Settings opens the existing local
provider dialog when available. Visual Presets targets the visual controls tray,
while Scenes (the Authoring panel) targets saved scenes. Selecting Preview does not enable a disabled
Preview preference. Unverified speech captions use the localized Recent
speech label; ordinary user captions retain YOU. Caption attribution does not
control tool dispatch.
Voice inactivity waits for local input, output and actions to become quiet. A matching OpenAI response that explicitly completes without audio can restore idle eligibility; completion of an audio response does not prove playback has drained. Missing or ambiguous output evidence remains conservative. Gemini retires its per-audio observer wrappers on close, replacement or session end, so repeated Stop/Start does not retain old audio instances until the panel closes. The panel keeps the model tier and regular cost estimate in Voice Settings; warning, capped and unknown cost states also appear in the dock heading when Settings is closed. The existing hard cost cap still stops the session and announces the reason.
Automatic Voice Preview uses output from the active mounted provider, including late replies that may answer an older request. Manual panel choices remain until genuine local input rearms automatic presentation. Preview stays off when disabled, remains hidden in Cockpit, and retires on Stop or provider replacement. Display events do not grant command authority.
Contacts nearest-aircraft follow-ups retain the snapshot's distance basis even after the camera moves. An explicit new scope replaces that basis. Provider instructions require an analyst query for requested lists and use the requested radius rather than substituting the panel's fixed window. Analyst speech carries stale/degraded feed status and does not speak an authoritative unavailable count. The existing NOTES disclosure deduplicates coverage/source caveats and retains their full text in at most six rows, grouping analyst overflow in the final row; ordinary tool-card limits are unchanged.
Voice speech: src/voice/speech.js adds { say, display, referents?, schedule? }
to layer toggles, fly_to_location, frame_overhead, select_nearest_aircraft,
get_entity_context, annotate_map and analyst_query. Analyst speech and the card
share one deterministic count/scope headline; lower bounds use "At least" and
partial answers name the unanswered layers. get_current_view_state lists enabled layers only.
FIRMS and ALPR entity labels remain English in shared context and tool data;
voice-card titles and pointer presentation translate those known labels at display
time, while arbitrary names and callsigns remain unchanged.
The model-free gods-eye-view/agent/world-packet export exposes
getWorldPacket({ viewer, styleManager, dataManager, sceneDirector }) to
non-voice consumers. It reads the same enabled-only state and feed
provenance before voice adds speech/card fields, with no new network requests
or mutations.
The voice card nested in the mic control (voiceCard.js) shows captions, plan
steps and the result. resultDisplay.js is the one adapter from a result to
the card: builder results show as built, and analyst results show a title
with their scope, window and caveats. Caveats are on the card; the model speaks
one only when it changes the answer, and always says lower bounds and partial
answers. narration.js speaks one
code-authored line for the current step of a tool still running at 1.5 s
(out of band, never after the result). Tools that report steps: layer
toggles, place lookups, select_nearest_aircraft and annotate_map (finding,
outlining). Other tools get one "still working" line at 8 s. The
earcon plays in push-to-talk only. A claimed Space hold while the assistant
speaks is a barge-in (WebRTC cancel + audio clear), refused while Radio holds
the speaker (mayVoiceClaimSpeaker()). Progress lines stay outside the reply
lifecycle and Radio handoff. turn.span debug records carry generation and
playback latency, silence, preamble and word-count figures. Caption
transcription (OPENAI_REALTIME_TRANSCRIBE_MODEL, default on) is metered into
the session cost and cap in API-key sessions; ChatGPT OAuth sessions count
captions without pricing them. The debug log omits transcripts, text, tool arguments
and complete tool-result bodies (including serialized function outputs) unless
GEV_VOICE_LOG_CONTENT=1.
Tool names, call IDs, protocol event metadata and usage remain available.
Numbered analyst targets resolve from current layer records, including layers
without pick/context lookup; numbered annotations retain their resolved coordinates.
Requested analyst lists and rankings may speak up to three returned items and
values in result order alongside the count and its caveats. A rejected current
action emits a failure result to settle its card; cancelled turns remain silent.
Coverage grading validates calls before accepting an expected capability match.
The voice manifest includes Street Level visibility toggles; its imagery has no
analyst query or entity-context records.
Point and ask: pointerContext.js reads what the cursor is on. A Space hold
is the pointing gesture: its keydown snapshot (released with the key) is sent
as an inert pointer_context item with the target, with reticle and chip.
Open-mic speech and typed turns count only a pointer that moved on the map in
the last 5 s and send only the kind (pointing:"aircraft"/"ground"); the chip appears when a tool
resolves it. Losing the pointer sends one target:"none" item. deixis.js resolves 'pointer' in
track_entity/fly_to_location queries, get_entity_context scope, analyst_query
scope.kind and annotate_map targets, plus referent:n from the latest
numbered result (referents.js, cleared at session end). A new result set (an analyst count) replaces
the numbered list even when it is empty. Only the five rows numbered on the
card are resolvable; -1 means the fifth/last visible row. track_entity
accepts either a named query or a referent, while an empty target is rejected. A reticle marks the
point during the turn and the voice card shows "THIS"/"HERE"; both hide in
Clean-UI and recording. A ground-only pointer lookup at local/city scale sends a ringed 512 px crop in
the one retained image slot, dropped if the camera moved or a newer capture or
turn started. The pointer path makes at most two bounded fresh-frame requests so
a slow render loop can recover without accepting a stale frame; a missing camera
identity, expired snapshot, invalid geometry, black frame or oversized payload
still fails closed. screenX/screenY against the crop are mapped back to the canvas. Harness: qa-voice-routing.mjs --layer deixis,
--layer deixis-live, --layer behavior --deixis-only.
The application shell composes focused state owners for navigation, destination lookup/orbit, Cockpit, visual settings, panel layout, aircraft display and layer bindings. Keyboard/display subscriptions have a separate lifetime; existing layer, scene and voice methods remain available through a delegation facade. Disposal settles pending globe resets and removes camera-entry listeners before asynchronous restoration; replacing a manager detaches its old Directions services. See UI ownership.
Director's ordered shot runner is exported independently of rendering, UI, storage and scene content. The existing scene controller supplies an adapter for visual/layer state, camera travel, media holds and release. Playback cancellation, preview cleanup, saved projects and all scene assets/attribution are preserved. Pure authored-time/seek calculations and a playback clock now own timing, hold deadlines and subscriptions. Stop immediately clears timers and settles holds; stale tick callbacks cannot publish into a replacement. Registered pack rules own presentation overrides and Nepal map fallback without changing saved shots or content. Scene-document validation and legacy migration now have separate owners. Invalid imports preserve the current project; unreadable saved projects are protected from fallback writes. Version 6 is the export format, with zero pitch, low camera heights, scope and detection edits preserved. See the document contract; see Director for camera, pack, action and sharing support.
Search framing and annotations request semantic map features from an explicit source. The Overpass adapter owns bounded queries, member/tag decoding and request deadlines; callers retain candidate ranking, outline caching, deferred retries and scene placement. Empty, unavailable, transient and throttled outcomes remain distinct; a data reply is refused only for its remark, never for text in element tags. Traffic sources return road records and installation sources return mapped records with freshness/saturation metadata, so their layers no longer decode upstream elements. ALPR already normalizes its records in the source. Traffic uses OpenFreeMap roads with optional matched TomTom congestion; ALPR uses an hourly OpenStreetMap extract. Keyless installations use military-area tiles. Detailed feature queries require operator-configured Overpass.
Nepal media preloads survive repeated camera-flight updates, but Stop, event disable and replacement remove abandoned frames and revoke pending Facebook sessions. Fallback evidence-card clicks respect the shared drawing-tool pointer lease.
In the Pinokio browser shell, Nepal provider embeds and hidden preloads are disabled before creating frames or loading provider SDKs, because the shell can redirect iframe navigation to an external browser. Existing source-linked cards remain available, and Open Original is an explicit action. Optional approved local clips still use the existing fallback; the public pack bundles no witness clips. Chrome and Safari retain embeds even when visiting a Pinokio-launched server. This compatibility fallback does not repair Pinokio's navigation handler.
Nepal flood lines classify the active terrain or photoreal surface, and the surge marker follows that surface. Geographic route coordinates are unchanged; dynamic lines do not rely on unsupported depth-failure materials. Later media-only shots keep their completed source-path history throughout arrival and card reveal.
Scene Stop, replacement, seek, and teardown revoke the Upper Valley locator's delayed camera approach and orbit while preserving its visible callout. A revoked timer or camera callback cannot start another flight or interfere with a newer shot.
Nepal scene ownership follows the parameters prepared by the public layer manager before activation, without depending on enable-time origin metadata. Enabling a scene beat preserves its selected map and camera; passive restoration does not schedule standalone playback.
Animated scene cards sample their optional scale, opacity and leader callbacks once per projected frame. A failed callback suppresses that frame and can recover on the next one; ordinary layers keep the shared projection path without animation evaluation.
Nepal Flood Incident is accessed through Authoring. Its flood and locator components remain registered for scene playback but are hidden from the Data Layers menu. Closing the event stops the active scene, including pending loads and media. The default catalog does not install a separate reconstruction scene; existing saved scenes remain untouched.
Double-click a shot name to edit it inline. Enter or moving focus saves a nonempty name, Escape cancels, and saved names persist with the scene project.
The Nepal overview route and lake-shot trail share the river-centerline starting point nearest Debris-Dammed Lake. No earlier upstream section is included; evidence pins remain separate from the schematic river geometry.
The event panel omits the WITNESS shortcut. Shot media, source attribution, Open Original links and corridor navigation remain available.
The Mailung Bazzar, Dandaguan YouTube embed follows the provider's playback clock through 0:07, then the scene advances after its 0.65-second media exit. The controller reconciles provider state and source time when the API attaches and while waiting for playback, including missed PLAYING notifications and an already-ended clip. Startup is bounded to five seconds, and stalled playback has a bounded timeout; unavailable or blocked media cannot strand the scene. When the host deliberately suppresses provider playback and there is no approved local clip, the source card instead uses the authored 7.65-second dwell before the next shot. The authored estimate replaces older saved holds at runtime without rewriting them. Autoplay requires transient Director ownership from Play Scene or Play Shot; LOAD, restoration and seek remain passive. Stop, replacement and teardown revoke that ownership and pending playback callbacks.
Persistent media-led Nepal shots preserve their completed upstream flood path during camera travel. The next reach still reveals after arrival; direct loads reconstruct the prefix and backward replay trims downstream history.
Repeated map-stack requests retain the live imagery layer when its provider is unchanged. Scene handoffs keep loaded tiles while real source changes, switch cancellation, and tile-error fallbacks retain their existing behavior.
Nepal Flood Incident is available in Authoring, with a 25-shot sequence, geographic labels, an event panel, scene scrubbing and shot playback. Comparison shots use Esri beneath Vantor imagery. Other Nepal shots use Google 3D when its tileset is available and fall back to Esri otherwise; without an ion token, the existing map stack uses Re:Earth terrain. Saved shots retain their authored camera and map preferences across keyed and keyless runtimes. The bundled pack contains historical 2021 and post-event 2026 imagery, not an isolated before/after measurement; the animated flood path is schematic, not a hazard model. Witness media opens or embeds original sources without bundled clips. See public/events/bhote-koshi-2026/README.md for provenance and non-commercial data terms.
Transit snapshot and selected-history reads use an explicit source interface. The standalone source selects the existing routes; rendering, playback and selection retain their existing owners. A portable request service owns registered-feed admission, bounded decoding, cache/revalidation, backoff and history; development and preview use the same thin adapter. Compatibility exports and source attribution are preserved.
Place search accepts an explicit Nominatim provider with independently configured search and reverse endpoints. The default offline/Google/Photon/local-fallback order is unchanged when no provider is selected. Provider adapters share normalized coordinates, viewport framing and reverse labels; roads and boundary geometry remain separate services. Portable capped-response and Overpass lexical helpers are exported independently of the Node server.
Reference feed construction is exported through sources/reference; the cable source also has a dedicated layers/submarine-cables/source entry. Standalone catalog compatibility remains available. Source choices, data and attribution are unchanged.
Source factories have dedicated layers/<family>/source exports. ALPR and earthquake record normalization and CCTV source policy no longer pull rendering into source consumers. Catalog construction and voice feed reads use their focused owners; compatibility entries remain available. Settings filesystem hardening lives under server/standalone/. Import-direction checks complement export ownership checks; source behavior, settings policy and rendering are unchanged.
Voice controls bind to a protocol-independent session factory. The default WebRTC adapter preserves the existing Realtime connection, push-to-talk, cost controls and radio handoff. Session subscriptions expose state, transcript, action call/progress/result, interruption, completion and turn-metrics events; stopping or replacing a session cancels pending actions. An active session's normal Stop publishes one idle transition, including adapters that emit their own terminal state; a restart triggered by that transition keeps ownership of its new startup state. Alternate adapters can use the same controls and action runner.
The standalone app offers OpenAI and Gemini Live through an explicit provider chooser. Gemini keeps its permanent API key on the local server and mints a short-lived, single-use credential with server-owned instructions and the standalone action catalog. Its native WebSocket adapter supports open microphone and push-to-talk input, streaming audio, captions and the existing action runner. In push-to-talk, a tool call that arrives with no input, reply or tool result outstanding is answered with a failure and never runs, and neither does any other call in the reply it starts; open-microphone turns start on Gemini's server-side activity detection, which reports no boundary to check. Switching providers ends the old conversation and releases its microphone and connection; only the input mode carries over. Gemini uses bounded shutdown draining and explicit manual restart, without automatic reconnection or a promise of uninterrupted session duration. Hosts that do not opt into the provider facade retain their existing default or custom adapter.
Voice action argument schemas have one portable owner under src/voice/actionSchemas.js. The Realtime provider builds 30 tools using separate description-only metadata. Description customization cannot replace argument types, enum values or required fields.
createGevActionRunner routes control_cctv enable/disable through the shared
layer visibility lifecycle. Results distinguish successful changes from
manager-reported failures or cancellation and report enabled state and lifecycle
uncertainty. A caller-supplied abort signal reaches the manager; a late abort
preserves an already committed change. Missing-layer errors and other CCTV
actions retain their existing behavior. The exported controlCctv helper refuses direct
enable/disable requests and directs callers to set_layer_visibility.
src/voice/layerManifest.js declares each catalog layer's voice capabilities: spoken aliases, toggle, in-view context, track, and analyst query fields with type and unit, or noQuery with the reason. The set_layer_visibility, show_data_layers_menu, get_entity_context and analyst_query layer enums, the runner's alias table and the analyst field hints are generated from it. src/voice/layerManifest.test.mjs fails when a catalog layer is neither listed nor named in VOICE_OFF_LAYERS (today only the two scene-driven Bhote Koshi layers). Global Context is listed with toggle: false; set_context_mode reaches it. set_panel_open also opens weather-panel and recent-imagery-panel.
Portable source exports provide Radio Browser station normalization, CCTV feed types and regional records independently of HTTP middleware. Radio normalization accepts an explicit URL policy; the standalone directory retains its existing HTTPS rules. Tile coordinate validation accepts explicit zoom bounds with unchanged traffic defaults. Cache, request and rendering owners remain unchanged.
Vessels separate plain, stable MMSI records and source acquisition from scene resources. The renderer owns geometry and billboards; cards, picking and trails resolve those resources through it. Partial-feed retention, selected-vessel pinning, first-position grace, source time and sea-surface placement are unchanged.
Military flights also separate portable records and source acquisition from Cesium snapshot application. The metadata owner retains aviation feet, source fix times, sticky fields and partial-feed retention. A row without track or speed holds the last known value, as civil records do; a grounded row without speed reads 0. Rendering retains ground model ownership, motion history, military registration and tracking cleanup.
Civil flights separate source acquisition, portable record reconciliation and scene application. The record owner keeps sticky metadata, geoid cache and bounded missing-poll state; ingestion owns cancellation, source freshness and backoff. The snapshot renderer owns Cartesian history, billboard/model changes and follow transitions. Aviation altitude remains separate from render height; partial retention, military suppression and ground-sampling policies are unchanged.
The UI facade composes separate owners for navigation authority, share restoration, visual settings and panel chrome. Navigation owns request generations and follow handoff; restoration owns its coordinator, deferred notices, gestures and timer; visual settings own style/detection overrides and sensor cleanup; panel chrome owns disclosure and Cockpit panel snapshots. Each takes explicit operations and reads instead of the complete shell. Existing controls, share formats and one-application-per-page behavior are preserved.
Layer lifecycle transactions are owned by data/lifecycle, which has no panel,
render-governor or detection imports. Application composition mounts a separate
layer presentation owner that reacts to status, accepted visibility/parameter
changes and non-throwing updates. Rejected partial updates still invalidate the
rendered detection set. The legacy manager retains panel helper compatibility;
registration sealing, restoration order and failed-transition rollback remain
unchanged.
Polled layer refreshes and the HUD's camera readout pause while the page is
hidden. An app embedded inline in a panel keeps both running, because the
panel's host can report the page as hidden while it is on screen. This is the
same rule the render loop follows (keepPanelRendering in app/embed.js).
Application scene setup owns request services, terrain/floor caches and annotation lookup state. Controls, layers and voice share those owners, and cancellation clears caches and removes map-stack listeners. Standalone setup supplies the existing service clients. HUD summaries, regional brief, cockpit weather, location framing and annotation boundaries receive their service instances explicitly. Compatibility entrypoints retain defaults for direct callers; normal startup does not change global service slots. Analyst follow-up memory belongs to its voice runner and ends with the voice session: stopping voice invalidates writes still in flight and forgets the last answer, the pointer and the numbered list; removing voice disposes the runner.
Analyst result memory is committed only after any Contacts-window projection finishes and the call is still current. Cancelled or superseded queries cannot return success or replace follow-up memory; Contacts follow-ups filter the same cohort that was shown, and partial/unanswered coverage remains attached when a remembered result is narrowed. The narrowed answer retains the remembered scope label and detail, including the Contacts-window attribution, unless the follow-up supplies a new scope.
Application startup constructs fresh layer instances from explicit source objects. Standalone composition selects the existing providers. Controls, launch orbit lookups, Contacts voice answers and ISS pass queries use the registered catalog. Aircraft classification is catalog-owned and cancelled with application teardown; scene engines still support one application per page. Compatibility data entrypoints remain for direct callers, but normal startup does not configure global sources.
Application data setup receives an ordered layer catalog and serialization metadata. Controls use the same instances. The standalone composition selects the existing default catalog, and restoration starts only after all registrations are sealed.
The sources/firms-csv package export exposes existing FIRMS CSV parsing and
UTC acquisition-window helpers without Node middleware or rendering dependencies.
The standalone FIRMS provider, source defaults and parsing behavior are unchanged.
CLI tools and development launchers accept GEV_PROJECT_ROOT to select the project environment, dependencies and output directory. Their default remains this checkout; bundled tool resources and source checks stay relative to the script installation. The setup doctor also accepts an explicit rootDir.
Application composition now uses shared scene/control/catalog/tool constructors and allowlisted HTML component templates. The standalone entry supplies settings and the default sources. Terrain, boundaries, weather, regional context and summaries use configurable services; renderer/action owners remain page-scoped. Annotation proximity guards remain on by default and allow distant targets only for explicit navigation.
Voice controls compose a supplied action runner and connection controller. Realtime token and SDP requests live in a configurable backend, with independent transports and cancellation through response parsing. Stop and application teardown abort pending connections; reconnect requests a new client secret. Microphone, radio handoff, tool schemas and default model behavior are preserved.
Coordinate search accepts whitespace-separated decimal degrees with prefix or
suffix hemisphere letters, including 40.7128 N 74.0060 W. Components require
an actual separator and an unambiguous hemisphere assignment; malformed numeric
fragments and scientific notation fall through to geocoders instead of becoming
exact coordinate results.
Geospatial lookups are composed through src/search: forward/reverse geocoding,
text/nearby search and routing use configurable providers/endpoints. Annotation,
HUD and voice consumers share the configured service. Existing Google/Photon
selection, route/direct-line honesty and tool schemas are preserved. Node
Google, OSRM and Nominatim adapters accept trusted construction-time configuration;
request parameters cannot choose arbitrary upstream URLs. See APPLICATION.md.
Every ALPR native marker is repositioned on map-source changes, including canvas-owned saved appearances, while retaining its entity identity.
ALPR floor preparation and clamped markers share a per-cell queue, with at most one 64-cell terrain batch active per layer. Disabling or destroying the layer cancels its terrain consumer and discards queued cells. Repeated moves inside an outstanding query retain the displayed markers until the new snapshot is ready.
The optional ALPR Cameras layer shows community-mapped OpenStreetMap locations,
not camera footage or plate records. City-scale acquisition uses an hourly
OpenStreetMap US/Canada extract, with available operator/brand/bearing
attributes. The extract carries full attributes from z9 (z0-8 are geometry-only
heat tiles); each view reads the finest zoom from z12 down to z9 that fits 16
tiles, so a 30 km whole-city view loads about 1,000 Austin cameras and a
neighbourhood reads z12 (about 2 m position precision). Each camera keeps the
finest position seen, so zooming out never nudges a marker. The fetched tiles'
bounds are the accepted coverage: later views inside it (and precise enough for
it) reuse the records at once, without a request or debounce, and a still camera
sees one marker update per move. Missing verification dates remain unknown; OSM edit dates do not
stand in for verification. Outside coverage the row says "No ALPR data for this
area". Reads cap tiles at 16 per country, concurrency at four per country,
responses at 4 MiB, decoded cache at 64 entries/24 MiB per country with a one-hour
TTL, and accepted records at 6,000; at most 1,500 cameras in view render,
nearest the view centre first, and more than that reads "Coverage limited".
Partial coverage remains explicit.
Its ./layers/alpr entry exposes a per-instance layer factory and bounded source
adapter. Sources return normalized records, stale/coverage flags and their own
attribution; rendering does not parse tile payloads. The standalone
registration supplies selection, picking, terrain and
render services. Selected cards and Data attribution identify OpenStreetMap.
Layer state and the existing voice layer tools include alpr-cameras.
The row count explicitly says nearby: loaded records can be outside the
screen. Cyan camera badges use Manjunath's camera artwork; selection switches to a
larger coral badge with corner brackets and an animated tactical card. Numeric
mapped bearings draw illustrative 90 m direction wedges (not measured coverage).
The shared overlay paints gradients for at most 64 nearby cameras, promoting the
selected camera; farther cameras keep native Cesium badges without wedges.
Native ground-clamped wedges (a ground polyline plus a classification polygon per
camera) cost about 40 ms per frame for 200 downtown cameras on photoreal tiles
and were removed. Badges sit on the cached ground floor (validated mesh or DEM,
+1.5 m) rather than ground clamping, which re-sampled every badge whenever a
tile streamed in; floors resolve before new badges appear (bounded wait), and a
badge whose cell has not resolved clamps until it does, then moves in place.
Overlay anchors start on the same floor and write marker properties only on
change. Once the camera is still and tiles have settled, each overlay anchor is
replaced once by the rendered surface under it (two depth samples per paint,
accepted only within -30/+40 m of the floor cell), so badges do not float or
sink on slopes; wedge corners follow their own floor cells instead of staying
level with the apex. Unknown bearings never acquire a wedge. The row reads
N nearby · M on screen; when every loaded camera is outside the view it adds
"None on screen — nearby cameras are outside the view" instead of looking
unloaded. scripts/qa-alpr-journey.mjs drives an Austin journey with ALPR off,
then on (cold and warm), and asserts time to cameras, marker stability while
still, and frame time within 10% of ALPR off. It also checks rendered frames:
marker pixels in the recorded clip against the ALPR-off pass, and, at the end
of every hold, the screen against the same scene after forcing frames, so a
state change without a render request fails (--self-test-drop-renders proves
it does). Asset URLs resolve relative to
the reusable layer, and the caller supplies overlay and map-stack services.
The ALPR row uses the same header layout as other data layers. SHOW NEAREST
frames and selects the nearest loaded camera; it is
disabled with no loaded cameras or while following a tracked entity. This action
chooses from the current source snapshot and uses rendered surface height from
3D tiles or globe terrain when available.
Coverage follows a bounded neighborhood around the screen-center ground point,
with a radius of twice the camera-to-ground range (at least 1 km), rather than
extending to the horizon. Sky, dateline-crossing and wider-than-city views do not
query. Movement within an accepted or pending query reuses it. Markers retain
their Cesium identity while their geometry is unchanged; refreshes preserve
selection without replaying a click event.
Data Centers, Dams and Submarine Cables release their built Cesium data sources and record references when disabled. Parsed datasets remain cached for the layer lifetime, so re-enable rebuilds entities without downloading or parsing again; this can take longer than simply revealing hidden entities. Destruction clears the parsed cache as well. A disable during loading cannot leave a completed build hidden in the scene.
CCTV exposes a factory through ./layers/cctv. Catalog and health requests,
frame/media URLs, camera records, ground placement, geometry queues, playback,
projection, cards, calibration and interaction have separate components. The
standalone entry supplies application-owned scene, ground and activation services.
Each layer owns its state and visibility listener; destruction cancels source
reads and pending initialization. Malformed health responses retain prior health.
Existing catalog fallback, camera poses, frame pacing and coverage controls remain.
CCTV frames and the Street View fallback accept only JPEG, PNG, GIF, WebP and
AVIF, sent under the canonical type; any other type falls through to the next
fallback. Live media must match its feed kind (video/mp4, video/webm, or
multipart/x-mixed-replace with a boundary) before health reports it connected,
and follows redirects only on the camera's origin. Catalog image URLs, and
redirects on those hosts, are pinned by parsed URL to the agency's image
directory. Every /api/cctv route refuses cross-site browser requests but
accepts forwarding headers, so cameras work behind a reverse proxy; a forwarded
request skips the key-bearing Street View fallback and gets the placeholder.
The CCTV layer draws only the cameras on or near the screen. Zoomed out, it keeps one camera per ground cell sized to the view, preferring a healthy feed, and the cells are anchored to the ground so a pan keeps the same cameras. The active camera always shows. Ground placement and coverage geometry load only for the cameras in view, with at most two attempts per camera and view regime. The view pass reruns while the camera moves, paced by its own cost. The camera picker lists the cameras in view, active camera first, and closes with the count of cameras outside the view.
Traffic owns its rendered-surface observer only while enabled, removes it on disable/destroy, and caches surface identity without allocating on unchanged frames.
Traffic and bikeshare expose factories through ./layers/traffic and
./layers/bikeshare. Traffic separates road requests, ingestion, animation,
flow matching, styling, viewport lifecycle and development timing. Each source
owns its decoded flow cache; cancelled bodies cannot refill it. Bikeshare
separates its city registry, station source, parsing, rendering, selection and
proximity lifecycle. Existing standalone sources, live/simulated labels, road
budgets, station availability and polling behavior are retained.
Installations and proximity context now expose ./layers/installations and
./layers/awareness factories. Each owns its records, selection, presentation,
navigation and lifecycle. The standalone entries supply existing scene operations
and the aircraft/vessel/installation instances used for proximity queries.
Installation requests use a bounded source adapter; explicit nearby-place search
remains separate from ordinary mapped-site loading. Invalid snapshots retain the
previous display, and cancelled requests cannot publish a later failure state.
Viewport limits, saturation retry, placement, query caps and controls are retained.
Satellite and mission layers expose separate factories through ./layers/satellites
and ./layers/launches. Each owns its catalog, render state, tracking, interaction
and teardown. Satellite core and dense catalogs retain upstream stale-cache
provenance only for admitted records; duplicate or invalid entries cannot taint
a fresh winning record. Dense removal clears only its own provenance, and a
fresh core rebuild or lifecycle reset replaces the corresponding ownership.
Source adapters retain the existing CelesTrak and Launch Library
endpoints; standalone entries provide scene services and the satellite dependency.
The mission factory separates paths, replay, camera, panel, overlays and placement.
Launch Library pad coordinates retain unknown values: missing fields use the
reported location coordinates when available, and launches without a known pad
position stay off the map. Genuine zero coordinates remain valid. Undisclosed
stage coordinates, downrange distance and flight numbers remain unknown; the
mission panel omits the flight-number label when no number was reported.
Pending mission requests abort on disable/destruction and malformed launch
snapshots preserve the last accepted display. A Launch Library stale-cache fallback
retains its missions but reports STALE in Data Layers; a fresh snapshot clears that
provenance. Hiding retained missions does not relabel them as fresh, and teardown
clears the instance state. Injected getLaunches-only sources remain supported.
Catalog groups, propagation cadence,
tracking intent, replay timing, controls and source attribution remain unchanged.
The fire layer now exposes ./layers/firms: an instance factory with separate
snapshot requests, records, rendering, cards, selection, viewport scheduling and
terrain-anchor batching. The standalone entry supplies the existing FIRMS source
and scene services. Disable and teardown cancel pending refreshes; malformed
snapshots preserve the last good display. Refresh restoration keeps selection
identity without announcing a new user selection. Source/label, LOD thresholds,
card limits, fire identities, altitude placement and analyst records are retained.
Earthquake rendering is exposed through ./layers/earthquakes. The layer owns
its entities and request lifecycle; the application supplies the overlay host
and snapshot source. The standalone adapter keeps the existing USGS daily feed,
M2.5+ filtering, static discs, magnitude labels and analyst records. Disabling or
destroying the layer cancels pending work and ignores late results.
Vessel components and sources
src/data/aisLiveVessels.js assembles createVesselLayer from the
./layers/vessels package entry. The factory owns feed lifecycle, keyed records,
rendering, selection, trails and cards in separate components. The standalone
entry supplies its AISStream source, scene services and existing row limits.
Incomplete observations retain missing keyed vessels for at most five minutes since their last accepted receipt, within the renderer's row budget (plus the existing selected-contact pin). Complete observations keep the existing removal policy. Empty or failed observations preserve warm data with the existing feed health warning and first-connect grace period. Source timestamps remain source timestamps; receipt does not turn an unknown epoch into a fresh update.
A refreshed record keeps its identity while updating its history reference. Selection changes, disable and destruction cancel pending trail requests; late responses cannot refill a cleared or replacement trail. Heading and course, sea-surface placement, click ownership and card selection policy are unchanged.
Military-flight components and aircraft mechanics
gods-eye-view/layers/military exports createMilitaryFlightLayer. It uses the
same normalized observation contract as civil flights, with separate military
classification, styling, model and tracking policy. Each instance owns its
contacts, history, scratch objects, model loads and cancellation lifetime.
Applications supply the existing scene services and resolve model asset URLs;
src/data/militaryFlights.js keeps the standalone layer API and adsb.lol source.
A source may retain a bounded stale-status reason; the standalone cached-feed
behavior remains unchanged.
gods-eye-view/aircraft exports the existing shared classification, icon,
metadata, motion, altitude, model-anchor, proximity and selection calculations.
It also exports createMilitaryRegistry, an explicitly constructed owner for
known military identities and active-layer transitions. Its optional background
poll consumes the optional getIdentities capability, falling back to normalized
positioned records when a source has no identity-only capability. The adsb.lol
adapter preserves known identities without requiring positions; those entries
still cannot enter the renderer. Source replacement
and disposal abort pending work and clear retained identities; construction
starts no network request. Both standalone aircraft layers use one registry.
Civil-flight components
gods-eye-view/layers/flights exports createCivilFlightLayer. Each instance
owns its contacts, histories, model collections, scratch objects and lifecycle.
State, ingestion, enrichment, motion/floor interpolation, rendering, tracking and
queries live in separate files under src/layers/flights. The standalone
src/data/flights.js assembles the existing OpenSky source and scene services.
Applications supply the existing floor/snap, geoid, picking, sprite, camera, trail, focus, readout, context, aircraft presentation and render services. The factory never constructs a second application registry. Configure a source before initialization; replacing it while initialized is rejected. Model loads use the supplied asset resolver, including the preload path. Enrichment receives an abort signal and cannot update a later lifecycle after destruction. Existing camera, terrain floor, trail, selection and measured model-size policies remain.
The tracked-aircraft descriptor both flight layers hand to the cockpit carries
verticalRateMps beside velocityMps and track. Civil records store the rate as
verticalRate and military records as verticalRateMps; the query seam maps both to
one name, so resolveTrackedAircraftInfo returns a single shape for either layer. The
rate is reported, never derived: a blank poll keeps the last transmitted value and an
absent rate stays null rather than reading as level flight.
Transponder emergency highlighting
src/data/aircraftEmergency.js derives one status per poll from the Mode A
squawk (7500 unlawful interference, 7600 radio failure, 7700 general emergency)
and the readsb ADS-B emergency/priority field where present. Higher severity
wins, with ADS-B taking a tie. GEV treats minfuel and lifeguard as priorities;
general, unlawful, nordo and downed are emergency statuses.
Civil and military fleet billboards, 3D models and Cockpit dots turn red for emergencies and repaint when the broadcast changes. Priorities retain normal colors; tracked map colors keep their existing selection treatment. The Cockpit meta line names the squawk or ADS-B status (only emergencies are red), replacing COURSE ALIGNED while a status is present.
Analyst and tracked voice results describe broadcasts, not confirmed incidents.
Fields include squawk, emergency, emergencyKind, emergencySource and
emergencyBroadcast. lastContactAtMs is the last known transponder contact,
not the arrival time of this status; missing contact times retain known contact.
Civil fallback preserves status alongside unchanged OpenSky vectors. One poll
omitting ADS-B status may retain the previous status when the squawk is unchanged;
explicit clears and changed squawks take effect immediately. The next omitted
poll expires that retention. scripts/qa-emergency-squawk.mjs checks billboard
and model transitions, Cockpit HUD, clearing and tracking release.
Browser live-source observations
Flights, Military Flights and AIS Vessels obtain snapshots and optional history
through gods-eye-view/sources/live. The standalone adapters use the existing
same-origin routes. Aircraft observations distinguish barometric metres from
WGS84 ellipsoid metres and retain source position/contact epochs; vessel records
retain separate heading/course and sea-surface datum. History is a best-effort
addition to the locally accumulated trail, never a promise of complete coverage.
Snapshot coverage, completeness and freshness are separate fields. A partially
admitted aircraft snapshot retains absent contacts for up to five minutes before
the usual missed-poll eviction. An OpenSky row with an ICAO24 address but null
longitude and latitude is a contact without a position, not a malformed row: it
leaves the snapshot complete and is listed in retainIds, so civil flights
retain that aircraft under the same five-minute bound, unless it is likely
landed, while other absent aircraft fade and leave on the missed-poll limits. A
nonempty snapshot with no positioned rows is still malformed. Military readsb
admission is unchanged. Unknown snapshot times remain unknown in stats. An invalid nonempty
snapshot retains the previous display. Empty vessel refreshes retain the existing
first-connect grace and warm-data behavior. Known source failures have bounded
messages; arbitrary HTTP response bodies are not surfaced as diagnostics.
Sources receive cancellation signals and check them after body parsing. The
layer's existing lifecycle and selection guards continue to reject late work.
State and action outcomes
Share preferences, place lookups and Scene playback expose immutable snapshots and disposable subscriptions. Share settings drive URL updates; lookup outcomes drive Location labels and busy/error feedback. Superseded or disposed lookups cannot publish accepted destinations. Each completion carries its own request identity so an older completion cannot clear the current search indicator.
Scene controls consume playback state and editing outcomes from the director.
Progress updates carry a small playback snapshot and preserve shot-row identity;
editing outcomes include a copy of the affected scene or shot. Subscriptions
start with current state, isolate listener failures and stop on disposal.
gods-eye-view/scenes exports the same director used by the standalone app.
UI shell and component ownership
The standalone entry composes the UI with the application's existing layer, terrain, navigation and rendering operations. The shell receives those instances and assembles the controls. Panel layout scheduling, loading notices, recording presentation and DOM lookup have focused owners. Disposal revokes queued presentation and listeners before asynchronous Context restoration, restores the recording HUD and releases status decoration without replacing accessible text. The ordinary control snapshot includes the current 3D model toggle and mode.
style.css imports component styles in their original cascade order. Scene,
share, HUD and layer engines retain their existing behavior and entry points.
Scene control ownership
Scene controls own creation/deletion prompts, panel listeners, shot rows, playback/recording presentation and keyboard cancellation. The director supplies project reads and explicit editing/playback actions while retaining persistence, camera and layer sequencing. Shot selection updates the highlight without replacing the row, preserving native double-click rename. Replacing rows revokes their old listeners. Disposal stops controls immediately; late file and failed-action completions cannot update removed presentation.
The panel is titled Authoring and is laid out in four labelled steps: SCENE
(picker on its own row, NEW SCENE, DELETE), SHOTS (+ CAPTURE SHOT, UPDATE SHOT,
the numbered shot list and a select/rename hint), PREVIEW (PLAY, STOP, NEXT) and
SAVE & SHARE (EXPORT, IMPORT, RUN LOG, then the EDIT DETAILS / SHARE SCENE row).
Every button in the step rows has a tooltip, and the status line is a polite
live region. Outside playback a button that would do nothing is disabled:
DELETE and CAPTURE SHOT without a scene, UPDATE SHOT without a selected shot,
PLAY and NEXT when no scene in the project has a shot (both continue into the
following scenes). During playback the authoring and file actions stay disabled
and STOP is enabled, as before; RUN LOG follows the last run, and EDIT DETAILS /
SHARE SCENE keep their own checks. PLAY uses the scene|PLAY catalog entry,
apart from the radio's PLAY.
Director clock listeners, interaction observers and scene release failures no longer leave the director inconsistent, and a phase error is kept when cleanup also fails.
Cockpit component ownership
Cockpit presentation is separated from its camera/controller behavior. Existing layer, terrain and rendering operations are supplied by composition, retaining the same tracked identity, ground acquisition, motion correction and cadence. The Display portal owns group anchors, focus/scroll restoration and listeners. Superseded portal frames cannot repaint old state or steal focus after disposal; retained Cockpit actions cannot restart a disposed controller. Input, subscriptions and queued panel work stop before asynchronous layer restoration; final camera and portal cleanup follows that restoration.
Context coordination
Context controls own Contacts/Space Missions state, entry and exit transactions, layer snapshots and restoration. The application supplies the existing manager, installations search and camera/panel actions. Tab listeners and pending presentation work stop during disposal; layer restoration retains its existing compensation and latest-intent rules. Clear Selected Layers shares this owner, so an older restore cannot replay over a newer Clear action.
Camera panel ownership
CCTV controls receive the existing camera port and explicit application actions. Frame loading, calibration editing and status display have separate modules; providers, camera placement and navigation policy retain their existing owners. Changing cameras invalidates old image callbacks and cancels an unfinished calibration edit. Failed refreshes preserve settled pixels for the same camera, while a newly selected camera never shows the prior camera's image. Disposal releases listeners, subscriptions, image handlers, summary timers and chip-hide timers.
UI disposal
UI disposal releases the CCTV subscription, command-dock observer, legacy drag resize observer and window resize listener. These remain independent of the Location component and are included in whole-UI browser teardown acceptance.
Radio controls
Radio panel, compact controls and tuner presentation have a dedicated owner. It receives playback, layer actions and layout callbacks; station ingestion, marker placement, audio playback and camera policy stay with their existing owners. Tuner calculations have a pure entry, with existing layer exports preserved. Destruction removes listeners and state subscriptions before ending tuning; a delayed Enable result cannot reveal or refocus removed controls.
NYC DOT TMC CCTV source pack
The CCTV catalog can add New York City traffic cameras from the NYC DOT
Traffic Management Center. NYC DOT requires developers to sign a data-sharing
agreement for the camera feed, so this is the one opt-in pack: it loads only
when CCTV_NYC_ENABLED=1, and with the flag unset the server makes no NYC
request. NYC DOT's requested disclaimer is in the Data attribution popover.
When enabled, the server reads the keyless
camera list at https://webcams.nyctmc.org/api/cameras and keeps cameras the
feed marks online. Frame URLs are built from each camera UUID under
https://webcams.nyctmc.org/api/cameras/<uuid>/image; they are never taken
from the payload. There is no heading in the feed, so gizmos use the shared
id-hash fallback at low confidence.
Once enabled, the 250 cameras nearest the five borough cores load.
CCTV_NYC_MAX_SOURCES sets the pack cap (8–400). The catalog-wide ceiling stays at 4,000 and is shared
round-robin: no region is dropped outright, but when the packs together exceed
the ceiling, adding a pack trims cameras from the largest packs.
City of Toronto RESCU CCTV source pack
The CCTV catalog adds the City of Toronto RESCU traffic cameras — downtown
arterials plus the Gardiner, DVP, Allen and Lake Shore, which the Ontario 511
pack does not cover. The server reads the keyless camera list the City
publishes on its open data portal (tmcearthcameras.json, served as JSONP).
The list has no URL field: frame URLs are built from each validated, digits-only
camera number on the fixed origin
https://opendata.toronto.ca/transportation/tmc/rescucameraimages/CameraImages/.
The feed has no compass data, so headings are low-confidence priors. Some
cameras return a 200 "Image Unavailable" placeholder frame instead of an error.
By default all ~336 cameras load, nearest downtown first.
CCTV_TORONTO_MAX_SOURCES sets the pack cap (8–500) and
CCTV_TORONTO_ENABLED=0 turns the pack off. The pack shares the 4,000
catalog-wide ceiling round-robin with the other packs.
Lithuania CCTV source pack
The CCTV catalog can include Via Lietuva / Eismoinfo road cameras. The pack is
off unless CCTV_LITHUANIA_ENABLED=1, because its camera endpoints are the
eismoinfo.lt website backend rather than a service listed in Via Lietuva's
open-data statement, so the licence for the photos is unconfirmed.
Two keyless public endpoints provide current snapshot URLs and WGS84 camera
locations; records are joined by ID, with invalid coordinates and off-origin
image URLs rejected. Catalog requests refuse redirects, have a 15-second
deadline and a 4 MiB response cap, and use the existing shared 15-minute cache
and fair catalog allocation. An unavailable source contributes an empty pack.
Frames use the registered-image proxy and retain Via Lietuva attribution.
The images are periodically updated road-condition photographs, not continuous
video; camera poses use the existing low-confidence heading prior.
CCTV_LITHUANIA_MAX_SOURCES sets its cap (default 400, bounded to 1–1000).
Taiwan CCTV source pack
The CCTV catalog includes Taiwan national freeway cameras from the Freeway
Bureau, MOTC. One keyless request reads the daily MOTC 2.0 CCTV.xml table
(~1,800 cameras); the request refuses redirects, has a 15-second deadline and a
6 MiB response cap, and uses the shared 15-minute cache and fair catalog
allocation. A failed download serves the last good table (up to a day old),
or an empty pack, and is not retried for an hour: the catalog host does not
answer from some networks outside Taiwan, and without the backoff every refresh
there would wait out the connect timeout. The table
publishes MJPEG stream URLs; each is mapped to the same camera server's
single-JPEG path for the same camera number, and only those exact HTTPS pairs
on the four freeway.gov.tw camera servers are registered with the frame
proxy. Camera keys come from the server and camera number, because published
CCTV IDs contain Chinese place names and a few repeat across cameras. Poses use
the low-confidence heading prior. CCTV_TAIWAN_ENABLED=0 disables the pack;
CCTV_TAIWAN_MAX_SOURCES sets its cap (default 600, bounded to 1–2000), and
CCTV_TAIWAN_URL overrides the table URL.
Gate: src/data/cctvTaiwan.test.mjs.
DriveBC CCTV source pack
The CCTV catalog adds DriveBC highway cameras for British Columbia alongside the
Austin, Caltrans and TfL packs. The server reads the keyless camera list that
DriveBC.ca serves at https://www.drivebc.ca/api/webcams/ and keeps cameras that
are switched on and published. Frame URLs are built from each numeric camera id
under https://www.drivebc.ca/images/; they are never taken from the payload.
DriveBC's eight compass orientations become high-confidence heading priors, and
its elevation (metres above sea level) seeds the ground height.
By default the 250 cameras nearest downtown Vancouver and Victoria load.
CCTV_DRIVEBC_MAX_SOURCES sets the pack cap (8–1200) and
CCTV_DRIVEBC_ENABLED=0 turns the pack off. The default catalog cap rises from
900 to 1,050, the sum of the default pack caps, so no default camera is dropped.
The Open Government Licence – British Columbia attribution is registered in the
Data attribution popover.
Location control ownership
City/POI rows, search/reset bindings, location readouts and the orbit indicator have a dedicated component with explicit navigation actions. A separate lookup controller cancels superseded searches and checks camera authority before flight and result presentation. Existing search providers and camera handoff policy are preserved. Replacing or closing a POI row cancels its pending expansion frame; destruction releases listeners, pending searches and the orbit indicator.
Contacts and mapped-site search feedback filter subject-window installation matches by surface distance from the current subject position, within the stated 100 km radius; the retained tile-fetch square is only a loading window.
Search arrivals (location bar and voice) end above the rendered surface.
Precise places without a detailed outline used to frame a 250 m landmark shot
from a sea-level target, which left Camp Mabry about 1 m above the 171 m mesh
and a Denver coordinate underground. Search now resolves the application's
ground-floor elevation (bounded wait, same ellipsoidal datum) before framing and
warms the cell; after landing, src/cameraGroundGuard.js samples the rendered
surface under the target and the camera once tiles stream in and lifts the eye
to 120 m clearance, holding heading and pitch. Navigation ownership changes (Director, tracking, keyboard/UI controls and direct camera flights), layer/app teardown, and pointer/wheel gestures cancel the check and remove its listeners. scripts/qa-context-arrivals.mjs types Camp Mabry
and a Denver coordinate and requires at least 60 m clearance.
Layer panel ownership
Layer rows, feed feedback, counts, focus-preserving chips and toggle listeners are owned by a renderer-free panel component. The layer manager supplies current snapshots, lifecycle actions and row descriptors. Remount and teardown remove listeners and row subscriptions; obsolete completions do not repaint old rows. The clear control presents busy state while its existing action owns the transaction.
Sky background
src/sky/starrySky.js fills Cesium's Scene#skyBox slot with 9,096 Yale
Bright Star Catalogue stars and a soft ESO/S. Brunier Milky Way. Both layers
rotate from ICRF/J2000 into the Earth-fixed frame at the scene clock, with
a TEME fallback while Cesium's IAU 2006 tables load. Bundled assets and
their provenance are documented in src/data/local_data/sky/README.md.
Anime fades out the Milky Way with its style intensity to avoid cel-shaded bands; the catalogue stars remain. A separate black 1×1 cube map preserves model environment lighting independently of the visible sky. The adapter is validated against Cesium 1.138.0 and must be revalidated on upgrades.
scripts/qa-anime-sky.mjs checks asset readiness, visible Normal sky and
Anime band coverage against a Vite dev server.
scripts/qa-sky-model-lighting.mjs compares a lit airplane.glb against
black and white environment maps in isolated scenes.
Model atmosphere on Apple Metal
Cesium's per-vertex model atmosphere is kept out of the pipeline on devices
whose driver cannot link it. AtmosphereStageVS binds shader out parameters
directly to varyings, which ANGLE's Metal backend rejects at link time, tearing
down the render loop on iPadOS and iOS. ModelSceneGraph.configurePipeline
attaches that stage only when fog.enabled && fog.renderable, so the viewer
clears scene.fog.renderable and leaves fog.enabled — and the fog density
that drives 3D Tiles screen-space-error scaling — intact. Sky atmosphere and the
ground-atmosphere fragment path route through locals and are unaffected.
Affected devices lose distance fog on 3D tiles and on globe basemaps (Esri,
Bing, keyless), since the globe fog shader uses the same renderable flag.
The probe releases its throwaway WebGL2 context immediately.
Detection is a WebGL2 link probe of the same out-parameter/varying pattern, so a future driver fix restores the effect with no code change; iOS/iPadOS platform detection is the backstop for when no probe context can be created. Applied during viewer construction, before any tile builds a draw command.
Map Source control ownership
Map Source controls own chip listeners, source-state subscriptions and selection feedback. Loading remains with the supplied map controller. The active chip follows the source actually displayed, including fallback; obsolete completions cannot overwrite a newer selection. Refresh and destruction revoke old listeners and destruction suppresses late UI updates. Available choices and setup behavior remain unchanged.
Visual effects ownership
VisualEffects owns style-stage creation, crossfades, animation scheduling, bloom
and sharpen. Display supplies actions and renders settings; effect execution has
no DOM dependency. Existing presets and the 500 ms transition remain unchanged.
Stopping the controller revokes animation before asynchronous UI teardown; final
destruction removes its stages and restores the borrowed bloom configuration.
Snow is no longer a visual preset. A share link that names it, in either the
long or compact form, opens on Normal and drops the Snow shader parameters. A
view passed to embed mode or show_in_gods_eye_view, and a saved scene shot,
that names Snow also open on Normal.
Sharpen runs only when enabled with intensity above 0.001. At slider zero its
stage is disabled; raising the slider resumes it when sharpen is enabled.
Display control ownership
Display button, selector and slider listeners have a single destroyable owner. The application supplies actions and retains effect settings, restore claims and rendering. Native keyboard editing, model modes and current defaults are preserved. Destroying the UI removes these listeners before asynchronous teardown.
Ordinary sliders share the native range component in ui/slider.js and the
gev-quantitative-slider surface in ui/styles/slider.css. Display, shader
parameters, volume, replay speed, weather settings/history, imagery opacity and
Street View's date filter use the same rail, thumb, value styling, keyboard focus and disabled state.
Generated controls use createSlider; static templates apply the same class.
Feature owners retain bounds, steps, formatted values, input/change timing and
listener cleanup. The radio tuning dial, imagery comparison divider and event
comparison/progression controls keep their specialized presentation and behavior.
Shared control CSS ownership
Shared styles own ordinary control surfaces and interaction states; feature CSS
owns surrounding layout and context-specific sizing. CCTV's calibration editor
width lives in ui/styles/cctv.css, while the shared inline field variant owns
only density and spinner treatment. Radio supplies its field palette and disabled
opacity through the --gev-field-* variables, retaining compact typography and
arrow space locally. Cyber supplies shared-field palette variables explicitly;
its legacy native-field palette rules exclude .gev-field. These ownership
boundaries preserve the current Tactical/Cyber appearance and native behavior.
Ordinary status badges
CCTV source/frame labels, calibration provenance and Weather coverage labels use
ui/statusBadge.js and the gev-status-badge CSS surface. The listener-free
adapter accepts already-translated label and hidden values; pill, box and plain
appearances describe shape/density only. Static CCTV markup uses the same classes
as generated rail-card badges. Repeated identical updates do not rewrite text or
replace nodes. Empty coverage remains hidden.
Feature owners retain the status rules, machine-state attributes, translations
and announcement policy. CCTV frame acquisition (idle/loading/error/ready)
remains separate from provider status: retained pixels do not imply a healthy
feed. Calibration still distinguishes manually saved, catalog-curated and raw
prior poses; unsaved edits override the label until save/reset. Coverage labels
remain geographic scope, not health claims. Existing colors and the source
badge's polite live region are preserved; coverage and calibration do not gain
live regions. Interactive layer toggles, progress indicators and world overlays
retain their specialized ownership. Vue can reuse the view contract and CSS while
replacing DOM construction. Browser acceptance:
QA_BASE_URL=http://127.0.0.1:4189 node scripts/qa-status-badges.mjs.
Ordinary action buttons
Scene panel actions, shot LOAD/DEL actions, the EDIT DETAILS / SHARE SCENE
bar and one-shot CCTV actions use ui/actionButton.js and the shared
gev-action-button surface in ui/styles/action-button.css. Static templates
apply the same classes as generated buttons. The native builder accepts translated
text, default/danger variants, default/compact sizes and disabled/accessibility
properties; it installs no listeners and owns no application state. Feature
controllers retain callbacks, enable/disable rules, confirmation prompts,
persistence and teardown. The smaller shot actions retain their existing sizing
and hover treatment. Feature CSS still owns panel layout and translated wrapping.
CCTV enable, coverage, auto-hop, projection and adjustment toggles, editable
calibration-value chips and the scene authoring dialogs retain their specialized
controls. Existing scene-btn hooks remain compatible with Street Level and
legacy toggles. The shared CSS, explicit input/action boundary and behavior tests
can carry to a Vue view while Vue replaces native DOM construction.
Browser acceptance: QA_BASE_URL=http://127.0.0.1:4179 node scripts/qa-action-buttons.mjs;
qa-scene-controls.mjs also covers real Scene authoring/playback with native Space capture.
Segmented selectors
Segmented selectors use the controlled DOM adapter in ui/segmentedControl.js
and the shared gev-segmented / gev-segment surface. Model coverage, label
allocation, drawing shape, Street View fit/panorama filters and imagery mode keep
their feature-owned values, actions and persistence. Painting a restored value
does not emit a change or move focus. Existing keyboard policies remain explicit:
Street View uses arrows/Home/End with one tab stop, imagery mode uses arrows from
its controlled selection, and Display keeps its existing native button behavior.
Pure value navigation is separate from DOM rendering for reuse by a future view.
Segments keep their word minimum width: compact inline groups such as FIT/FILL
and ALL/360°/FLAT must not break short labels mid-word. Collapsed Weather follows
the same exclusive-rail hiding rule as the other readouts.
The imagery mode row reserves 26px for the 24px buttons and group borders so
the complete button and inset keyboard-focus outline remain visible.
Browser acceptance: QA_BASE_URL=http://127.0.0.1:4173 node scripts/qa-segmented-controls.mjs.
Application shortcuts and shader parameter controls
ui/input supplies the bubbling application shortcut listener and generated
shader-parameter rows. Number/style keys, H/O/V/F/D/C actions, native form-control
typing and Escape behavior retain their existing mappings. Capture-phase
surfaces continue to arbitrate their own keyboard events first.
The UI facade retains shader values, share-restore authority, render requests, search dismissal, visibility and Cockpit portal policy. Parameter rows preserve labels, bounds, steps and precision. Rebuilding rows removes their previous listeners; disposal removes shortcuts and parameter listeners synchronously before asynchronous application teardown.
Adaptive panel rail layout
ui/layout supplies the left/right rail layout passes, natural-height
measurement and pure corridor/allocation helpers. The UI facade passes live DOM
nodes, obstacle nodes, HUD presentation, preferred panels and callbacks. It
retains observers, frame scheduling, saved/share preferences and Cockpit portals.
The left rail keeps its measured collapsed heights and obstacle-safe corridor;
the right rail follows its top baseline and preserves Display's scroll owner.
Automatic collapse remains presentation only, prioritizes the latest explicit
panel, and honors keyboard focus on the right. Mobile layout still releases
desktop allocation styles. Stable Display allocation avoids unnecessary style
writes. This extraction does not change panel positions or layout defaults.
The right rail skips panels hidden by other CSS (computed display: none), but
keeps the collapsed panels its own exclusive presentation hides, so automatic
collapse can still be undone.
Phone layout
At 720px and narrower, src/ui/mobileShell.js keeps the globe full screen and
shows a bottom tab bar: Layers (Data Layers, with Authoring collapsed below),
Cameras (CCTV and Street Level), Context (Global Context, plus Weather and
Recent Imagery when shown), Search (Location) and More (Visual Presets and Map
Source above Display). A tab opens one bottom sheet; the same tab, the backdrop
or Escape closes it, and focus moves into the sheet and back to its tab. A
downward swipe of at least 64px on non-control sheet content also closes it,
when every scroll container under the finger starts at the top. Ordinary
vertical scrolling, horizontal gestures, controls and multitouch stay native;
a cancelled gesture or a sheet/viewport change cannot dismiss another sheet. Panels
are not moved in the DOM: styles/mobile-shell.css presents the rails and dock
as the sheet, and the dock's Location and Presets trays render inline. The voice
control floats above the tab bar while no sheet is open.
The phone logo is 58px wide (about 39px high), aligned with the top controls. The shared contact picker preserves exact picks. For a coarse primary pointer that misses onto the map surface, it searches at most eight hits in a 14px square and accepts the first registered layer hit, stopping at non-map objects. Civil and military aircraft and vessels share this rule; fine pointers stay exact.
Manually opening Context with a tracked civilian or military aircraft enters the existing Contacts mode when Context is off. Contacts adopts that aircraft without restarting its tracking; an active or entering Space Missions mode is left alone. Programmatic sheet changes and closing the tab do not activate a mode.
Sheets borrow panels. Opening one expands its panels without persisting them or
claiming a user choice, and share links keep reporting each borrowed panel's
state from before the sheet; closing puts back what opening changed, while a
panel the user changed inside the sheet stays as they left it. Escape closes the
sheet before a panel's own Escape handling, and the dock's hover auto-close does
not apply inside a sheet.
Cockpit, recording and clean view close the sheet and hide the tab bar; scene
playback keeps both so its STOP control in the Authoring panel stays reachable. The
right rail neither hides collapsed siblings nor applies Cyber's one-panel
accordion at these widths, since the sheet chooses what shows. The Cesium and Google attribution stays visible above the
voice control, and above an open sheet. The page draws under the notch and
system bars (viewport-fit=cover) and the shell's chrome uses safe-area insets.
Wider viewports are unchanged, and crossing to them closes any open sheet.
Surface keyboard lifecycle
Surface Tab cycling excludes controls with a negative tabindex, native disabled state or inert ancestry; ordinary visible enabled controls retain their order.
ui/surfaces owns the capture-phase keyboard listener, Tab cycling and return
focus shared by the first-run launcher and Provider Settings. Each caller
activates it while open and deactivates it on dismissal; destruction releases
keyboard ownership without moving focus. Reopening captures the current opener.
The launcher retains its hit-test/exclusive-surface arbitration and dismissal
preferences. Provider Settings retains its existing visibility and save policy.
Initial focus, transitions and DOM content remain with each screen. This
component does not add modal semantics or make the map inert.
Panel disclosure lifecycle
Panel collapse buttons, nested Escape handling and dock hover/focus timing now
use ui/panels. The component receives existing DOM elements and callbacks for
state changes and content focus. Panel layout, saved state, share restoration,
Location draft cleanup and Map Source selection remain with their existing
callers. Listener and timer cleanup is synchronous when controls are replaced
or disposed, preventing old hover or focus work from changing a later view.
Military feed cooldown and loading guidance
The adsb.lol military proxy reuses its last response during upstream 429/5xx failures and observes Retry-After, bounded to 5–120 seconds (defaults: 30 seconds for rate limits, 15 seconds for server errors). A failure with no cached data still reports an error. Cached responses carry their age; the browser preserves observation timestamps and marks fallback data stale instead of inventing fresh positions or reporting a failed load. Fresh responses clear that stale state.
Mapped-installation zoom guidance appears in the layer row without counting as a global loading failure. Guidance statuses do not suppress independent refresh errors.
Places and CCTV request bounds
With a Google key configured, nearby and text search reject missing, blank,
non-numeric and out-of-range coordinates before the per-IP limiter and upstream
request. Text search also requires a nonblank query. Keyless requests retain
their configured: false response.
The cost-bearing proxies are throttled per client IP without configuration:
/api/realtime/token and /api/openai/hud-summary share 30 requests per
minute per IP, /api/google/nearby-places and /api/google/text-search share
120 — the caps the Pinokio build already sets, so the packaged app is
unaffected. GEV_RATELIMIT_OPENAI_PER_MIN and GEV_RATELIMIT_GOOGLE_PER_MIN override
those; exactly 0 disables the limiter, while a value that cannot be read as a
number falls back to the default rather than to unlimited. Over-limit requests
receive a sanitized 429 with Retry-After: 5 and never reach the provider.
The client key is the socket peer address only — a forwarded-for header is not
trusted, so a proxied deployment shares one bucket per upstream hop.
CCTV media waits at most 15 seconds for upstream response headers and returns
504 on timeout. Its timer stops when headers arrive, so live bodies can continue
streaming; a separate 30-second idle deadline then bounds the gap between
upstream chunks and releases a body that has gone silent. A response that is
still waiting to drain reschedules that deadline, so a slow client does not read
as a stalled upstream. Error responses are cancelled. Buffered snapshots have a
16 MiB streaming cap; an oversized image remains an upstream miss and uses the
normal fallback chain. The existing declared media size ceiling remains 64 MiB.
/api/cctv/media relays live feeds (HLS, MP4, WebM, MJPEG) only; a still-image
camera is refused there with a JSON 404 and is served solely through
/api/cctv/frame and its same-origin, image/*-only, size-capped fetch.
GBFS upstream bounds
Station information is normalized in src/sources/gbfsStations.js for both the
globe layer and get_bike_share. Stations with missing, blank, nonnumeric or
out-of-range coordinates (latitude outside ±90, longitude outside ±180) are
skipped. Numbers and nonblank numeric strings are accepted, including genuine
zero and the latitude/longitude aliases; other JSON types are rejected.
Names accept legacy strings or GBFS v3 Localized
String arrays regardless of the reported feed version. Nonempty English text
(including regional en-* tags, case-insensitive) takes precedence, then the first
usable translation. An unusable name falls back to short_name with the same
rules; if neither supplies text, the station remains available with an empty name
(the globe uses its station-ID label and the tool returns null). A run of underscores,
with the spaces around it, reads as one word break and the ends are trimmed:
JCDecaux's Spanish contracts publish _PLAZA_MUSICO_LOPEZ_CHAVARRI. Other spacing
stays as published. Capacity counts docks: PBSC adds the bikes a station accepts
in its geofenced area to capacity and reports that part as geofenced_capacity
(Bilbao Bizi: 839 = 39 docks + 800), so the layer, its colour ratio and
get_bike_share read capacity − geofenced_capacity when the extension is present. Malformed
translation entries do not interrupt loading of the other stations. This covers
station names, not complete GBFS v3 support.
scripts/qa-gbfs-names.mjs checks synthetic localized names through the real layer,
station selection, keyboard dismissal, mobile cards and bike-share tool against
either a dev or preview server (--url selects the target).
The station reader addresses the proxy as /api/gbfs/<encoded feed URL>; the
feed address is read from the path, and a query-string form is refused with a 400. GBFS refuses upstream redirects and enforces its 5 MiB response cap while
streaming. The 12-second deadline includes reading the body, and rejected or
stalled downloads are cancelled. Development and preview use the same handler.
Every upstream request carries Client-Identifier: gods-eye-view, which Urban
Sharing asks of clients of the Oslo, Bergen and Trondheim Bysykkel feeds
(gbfs.urbansharing.com); other operators ignore it. Requests to Entur
(api.entur.io, Kolumbus and Farte Bysykkel) also carry
ET-Client-Name: gods-eye-view-bikeshare. Entur serves its feeds without a
.json suffix, so the endpoint check accepts station_information and
station_status with or without it. Hosts that front more than GBFS are kept to
their GBFS tree: api.entur.io to /mobility/v2/gbfs/ and api.cyclocity.fr
(Bysykkel Lillestrøm) to /contracts/.
Upstream body bounds
FIRMS CSV, CelesTrak TLE groups, adsb.lol military responses, TomTom flow
tiles, OpenSky states/all and adsbdb lookups now read their bodies through the
shared streaming readers in src/sources/httpBody.js, so a declared or observed body
over the cap is refused with RESPONSE_TOO_LARGE and cancelled rather than
buffered. TomTom flow tiles use readResponseBytesCapped alongside
GTFS-Realtime. Each read takes the route's abort signal, so a body still
streaming when the deadline fires is cancelled. Caps are bounds, not budgets, sized well
above measured payloads: FIRMS 64 MiB per satellite source, CelesTrak 16 MiB
per group, OpenSky states/all 8 MiB, adsb.lol /v2/mil 8 MiB, TomTom tiles
8 MiB and adsbdb lookups 1 MiB. Where a route already had a deadline (FIRMS
60 s, CelesTrak 20 s, TomTom 15 s, adsbdb 8 s, and each 10 s OpenSky
states/all attempt) it already bounded the body read, because aborting a
fetch signal also aborts its body; the capped reader takes the same signal.
The adsb.lol military poll, which had no deadline, now waits at most 12
seconds for headers and body together. An oversized OpenSky snapshot is not
retried, and an OpenSky 429 starts its cooldown from the response headers and
cancels the body unread. A rejected or timed-out body is an upstream miss and
follows each route's existing error and cached-data paths. FIRMS map-key
status, the OpenSky token exchange and CCTV media keep their current handling.
The route also bounds request frequency: 120 requests/minute per client
address, with a global backstop of three times that. A refused request answers
429 with Retry-After and Cache-Control: no-store and performs no outbound
fetch. The bikeshare layer's own demand is about 15 requests a minute with
every catalogued system in range, so an ordinary session never meets the limit;
the method, host and path guards run unchanged.
Updated: September 21, 2026
Remaining local service modules
Overpass query validation, geometry simplification, disk caching and upstream transport now have separate modules; military-installation search reuses that transport. Regional briefing combines separate place, news and weather sources, while weather effects uses only the weather source. GDELT compact dates that do not name a real calendar instant keep the article with an unknown publication time. Existing process-scoped caches, rate limits, stale fallbacks and route ordering are preserved.
Local voice has separate HUD-summary, debug-log and Realtime-token handlers,
with tool definitions and instructions in dedicated files. The factory accepts
an optional annotationGuidance paragraph; its default instructions and all 28
tool definitions remain unchanged. sourceRoot resolves debug logs against the
application directory. Standalone key setup accepts the same directory option
for its environment store and preserves boot provenance, loopback/origin guards,
atomic credential writes and development-only registration.
Node-only package entries expose Overpass, military installations, regional services, local voice and standalone key setup. Importing them starts no network acquisition. Browser layer lifecycle, rendering and voice execution stay in their existing modules.
Web app install icons
public/manifest.webmanifest declares the installable app: standalone display,
#0a0a0f background and theme, and 192/512 icons plus a 512 maskable variant.
index.html links the manifest, a 180px apple-touch-icon and a 32px PNG
favicon; the existing SVG favicon still serves the browser tab. Safari ignores an
SVG apple-touch-icon and substitutes a page screenshot, so the raster is what
makes an installed icon appear at all.
Manifest start_url, scope and icon paths are relative to the manifest, and
Vite rewrites the index.html links under base, so a build served from a path
prefix installs and launches inside that prefix.
node scripts/generate-icons.mjs renders every raster from public/logo.svg and
writes public/icons/. Output is committed, so building needs no image
toolchain; re-run the script after changing the logo. Ordinary icons inset the
mark to 78% of the canvas and the maskable variant to 54%, keeping it inside the
circle Android may crop to. src/tooling/webAppIcons.test.mjs checks the links,
the manifest fields and that each committed PNG is square at its declared size.
It also builds the install links under base: '/example/' and asserts every
emitted link and manifest URL stays inside that base.
Local build preview
After npm run build, npm run preview serves the built app with the same data
provider routes as development, including aircraft, satellites, terrain, traffic,
FIRMS, GBFS, Overpass and CCTV/media. Unmatched /api requests return a JSON 404
in both modes instead of the application HTML. Browser routes retain SPA fallback.
Credential editing (/api/setup/* and Provider Settings) is development-only;
preview returns JSON 404 for those endpoints. Server credentials come from the
local environment; browser keys are captured at build time. Rebuild after changing
a browser key. Preview is for local build verification, not a production server.
CCTV and radio provider modules
CCTV direction text resolves travel tokens first, then intercardinals, then bare cardinals when enabled. Only conflicting headings within the highest matching tier remain unknown; street quadrants and lower-tier words do not cancel an explicit travel heading.
CCTV catalog acquisition, source normalization and frame/media delivery now live
in separate modules. Each CCTV factory owns its catalog and health state; its
sourceRoot option resolves relative source files against the application root.
Radio Browser station normalization, restricted outbound transport and directory
caching are separate modules. The pinned transport gives a mirror's 204, 205 or
304 reply an empty body and fails any reply outside 200–599 without throwing
outside its promise, so an unusual mirror status cannot crash the server.
Node-only package entries expose both provider factories. Routes, payloads,
fallback behavior and existing dev/preview hook registration are preserved;
browser rendering is unchanged.
The portable frame source retains the proxy's known X-CCTV-Source kind. Camera
snapshot tools label Street View fallback imagery as historical and credit Google,
while upstream frames retain their current-view label and catalog attribution.
Synthetic placeholders are unavailable as snapshots. Injected frame sources may
supply sourceKind; missing or unknown provenance keeps current_view null and
does not claim the image is current. The current_view field distinguishes camera-feed
provenance from historical fallback imagery; it does not measure capture time or
image freshness.
CCTV missing numeric fields
CCTV numbers are read as missing when they are absent, null, blank or boolean,
on the server (toFiniteNumber, every pack and the file/env source packs) and
in the browser (safeNumber). /api/cctv/sources sends an unknown number as
JSON null; the browser then uses the bundled seed camera's value when one
shares the id, else the field's default — id-hash heading, pitch −17°, FOV 74°
(which a matched datasheet can still replace), range 700 m, mount 24 m, the
city's ground elevation — rather than reading zero and clamping it to the
field's floor. A camera with a missing latitude or longitude and no seed
position is skipped by the browser catalog instead of being placed at 0,0.
Real zeros, including "0", are kept.
Terrain, traffic, fire and bike-share provider modules
Local composition now imports separate Node modules for Re:Earth heights,
TomTom flow tiles, NASA FIRMS detections and GBFS station feeds. Existing routes,
plugin order, server-key selection, validation, disk caches, budgets, retries
and stale/error responses remain unchanged. Each has a Node-only package entry
under gods-eye-view/server/providers/. Portable terrain mechanics, traffic tile
math and GBFS source rules are available under gods-eye-view/sources/.
The TomTom /api/tomtom/* routes run the shared same-site admission gate before
status, cache or upstream work, refusing cross-site browser requests while
preserving same-origin app requests and non-browser local callers. The browser
layers and their rendering remain in their existing modules.
Past the daily tile budget, or while TomTom fails, the proxy answers with its
last copy of a tile (x-tomtom-cache: STALE-BUDGET / STALE-ERROR) however old
it is, and every tile carries x-tomtom-fetched-at, the proxy fetch time
(or the persisted cache file time after restart). The flow source keeps that mark with each decoded tile, so
get_traffic_flow returns stale and oldest_tile_at and says when traffic
may be out of date instead of presenting cached tiles as live. Pending freshness
metadata is released when body reads fail, are cancelled, or exceed their size
limit, as well as when a tile decodes or the source resets.
GBFS null or empty availability counts retain an unknown value through station
parsing and rendering; they use the neutral station color rather than reporting
an empty dock. Explicit zero and caller-supplied numeric fallbacks remain valid.
Terrain height cache bound
The /api/terrain/heights route refuses cross-site browser requests before
rate limiting, cache access or upstream work: a Sec-Fetch-Site other than
same-origin/none (an <img> on another page), or a foreign or opaque
Origin, gets 403. The per-client limiter keys on the socket address, which
for such a request is the visitor's own browser, so without the gate any page
could spend the visitor's 90/min quota and leave the app's own lookups on
429. Unlike the cost-bearing routes, the terrain gate does not refuse
reverse-proxy forwarding headers: terrain is keyless and needed for rendering,
and a 403 there would send every chunk to the geoid fallback for an instance
served behind the documented reverse proxy. Same-origin app requests,
non-browser local callers and proxied same-origin requests keep working.
The terrain proxy parser rejects finite longitudes outside [-180, 180] and latitudes outside [-90, 90] for every client before upstream resolution. One out-of-range point rejects the whole batch; inclusive WGS84 bounds remain valid.
The client terrain-height resolver's in-memory cache is bounded at 20 000
entries (DEFAULT_MAX_CACHE_ENTRIES, overridable per instance through
createTerrainHeights({ maxCacheEntries })). Coordinate rounding makes repeated
visits to one place share a key but does not bound how many distinct places a
session visits, and the cache previously had no eviction, so a long session
retained every coordinate it ever resolved. Eviction is least-recently-used:
a consumer read, including a warm hit in a batch lookup, promotes its entry, so
the cell being watched is not dropped for having been resolved early. The bound sits far above a city-scale session, so
normal use never evicts and issues no additional proxy requests. A batch's
results are assembled from what that call resolved or found warm, so eviction
during a large batch cannot report one of its points as unresolved. Re:Earth and
geoid-fallback entry semantics, the fallback cooldown and the abort-time flush
are unchanged.
Landmark annotation identity
When a landmark geocode contains only address components, annotations retain its requested name for outline matching. A city or neighborhood address no longer replaces the landmark's identity. Without a canonical feature name, outline candidates must contain the geocoded anchor or closely match the requested name; otherwise the annotation stays at its geocoded point. Genuine feature-name components and existing administrative/monument matching retain their established behavior.
Satellite and launch provider modules
server/providers/space/ owns the CelesTrak TLE and Launch Library 2 Node
proxies. Their routes, six-hour/15-minute caches, disk storage, stale fallback,
request coalescing and optional LL2 server token retain existing behavior.
The Node-only gods-eye-view/server/providers/space export supplies factories;
sources/space supplies fixed upstream URL builders with no I/O or environment
access. Callers retain validation, transport and response policy.
LL2 recent-launch requests use UTC minute-aligned bounds over the same 30-day window, so installations refreshing in the same minute share an upstream cache key. The upper bound trails the current time by less than one minute. Requests identify the app and repository through User-Agent; the existing 15-minute local cache, stale fallback and optional server-only Authorization remain unchanged. Provider Settings identifies LL2 tokens as optional paid higher-rate upgrades: standard public access needs no key, and its field can remain blank.
The shared TLE text parser skips incomplete named sets and resumes at the next complete set. A missing orbital line does not discard the later satellites in the catalog. A set whose two lines carry different catalog numbers, or a malformed catalog number, is skipped the same way, so a spliced catalog cannot pair one satellite's elements with another's. Satellite layers and next-pass tools use the same parser.
Build configuration and local provider boundaries
vite.config.js delegates to server/standalone/vite.config.js, which loads
this checkout's environment and constructs the local providers in their existing
order. server/providers/local.js is the composition and compatibility entry;
provider families own their middleware and process state in focused modules.
Provider URLs, key selection, cache behavior, setup restrictions and routes are
unchanged.
gods-eye-view/build/vite is a Node-only export for explicit browser build
settings: Cesium assets, caller-supplied plugins, browser key defines, server
binding and document/credential protections. It never reads an environment file
or constructs providers. The standalone caller owns those choices.
Browser startup now lives in src/standalone/. That directory contains only
browser code; Node configuration remains under server/. Application lifecycle
and viewer export paths are unchanged.
Application startup and shutdown
The standalone entry now composes scene setup, controls, layer registration and tools through the reusable application lifecycle. Map defaults, layer order, share restoration, voice setup and the running debug handle retain their behavior. The welcome card still waits for restoration and the loading-cover transition.
Startup failure cleans up acquired resources. Explicit application destruction aborts construction, cancels pending playback/annotations and delayed welcome UI, then releases controls, layers and the viewer. Destruction is terminal; the standalone page must be reloaded to start again. The exported lifecycle and viewer helpers do not import the standalone entry or discover configuration. See application construction.
Scoped formatting and package checks
npm run format and npm run format:check operate on the explicit adopted-file
list. npm run check:boundaries checks the dependency graph of all
current package exports in their declared browser or Node runtime; infrastructure owns its three implementation modules
and takes Cesium from the consumer. CI runs both checks on Linux and Windows.
The standalone app, layer behavior and public export paths remain unchanged.
See component ownership and adoption.
Google browser and server keys
Local Places nearby/text search and the CCTV Street View fallback prefer
GOOGLE_MAPS_SERVER_API_KEY, falling back to GOOGLE_MAPS_API_KEY when the
server key is blank or absent. Only the browser key is injected into client
code. POWER UP presents one Google Maps entry for the browser key. The optional
server key is configured manually in the same ignored root .env, or Pinokio's
ignored pinokio/ENVIRONMENT; it is omitted from Provider Settings and its
missing-key count. Existing server keys and the single-key fallback remain
supported. .env.example and pinokio/_ENVIRONMENT document both entries.
The Street View headings tool uses the same server-first selection after
resolving environment overrides per variable; its explicit --key wins.
In standalone development and preview servers, a missing Google tile-token
provider returns an uncached 204 No Content for GET /api/google/tiles-token
(and HEAD) instead of a browser-visible 404. The fallback is registered after
real token providers and before the unknown-API handler, so existing Google
browser keys, provider-issued tokens, and unrelated API routes remain unchanged.
Infrastructure marker visibility
Datacenter/dam registration uses fresh reusable factories with the application's existing context, overlay and render functions. The scoped package exports do not import standalone application globals; see the infrastructure interface for lifecycle and asset requirements.
Local GeoJSON layers coalesce concurrent enables into one load. Disabling while loading keeps the result hidden; destruction aborts the fetch and rejects late parse/add results. Destruction and failed setup remove owned context records, so replacing a layer cannot retain stale entities or listeners.
Datacenters and dams retain their full datasets while limiting active marker stems per layer: 80 at camera heights of 3,000 km or above, 200 from 200 km, and 420 below 200 km. Selection favors existing label priority and nearby features, retains stable choices across small camera movements, and refreshes during continuous motion. Disable/re-enable resets selection cleanly. Close-up stem sizing uses camera-to-feature distance without a 5 km minimum. Unchanged overlay entries do not request another frame; moved positions, changed membership and re-enabling still publish. Ground sampling waits for visible globe tiles to settle and uses valid loaded terrain as a floor while retaining rooftop and below-sea-level elevations. A hidden globe does not constrain photoreal geometry, and retry work remains bounded. Already sampled nearby markers follow higher settled terrain on existing bounded walks, without additional GPU samples or timers, when close-up tile detail refines the floor. The submarine-cable renderer is unchanged. These limits bound active stem work; they do not reduce the materialized entity count or establish an FPS gain.
Keyboard interaction and focus
- Enter on the map-source disclosure opens immediately. A short Space press opens it on key release. Either route focuses the selected source once visible; a bounded retry handles delayed opening transitions, and closing the tray or moving focus elsewhere cancels the handoff.
- Keyboard focus rings are a global interface state and stay visible when a button is active or selected. Visual Styles, Location suboptions, Context and mission actions, Cockpit utilities, native fields, and sliders use the same visible-ring contract.
- A short Space press on a focused control keeps its native key-release action. If it remains held for 500 ms, focus is checked again, the control is blurred, and push-to-talk starts; releasing a claimed hold cannot activate the old control. The same hold works on the map and page background. Text-entry controls remain protected.
- The Location disclosure is reachable with Tab or Shift+Tab and shows a keyboard focus ring. Its city, point-of-interest, search-toggle, and search field controls show inset rings, including selected items. Enter toggles its tray immediately, while Space does so on release; either route makes the revealed controls immediately reachable by Tab; Escape closes it and clears any unfinished search. Escape from a tray control returns focus to the disclosure; Escape on the disclosure clears focus after closing.
- Data Layers ON/OFF buttons show a visible keyboard focus ring without changing their enabled state or feed-status presentation.
- Display controls and shader-parameter sliders show keyboard focus in both the map panel and Cockpit Display. Arrow keys retain native range adjustment. The enabled CCTV camera dropdown shows keyboard focus as well.
- Tab reaches both Contacts and Space Missions in sequence in every Context state. Left/Right arrows also switch them; the focused tab has a distinct ring even when selected.
- Keyboard focus on a Space Missions roster item uses the same temporary globe rotation and mission-marker highlight as pointer hover. Focus alone does not select the mission; Enter or Space performs selection. Keyboard and pointer preview ownership remain independent when the pointer is parked over the list.
- Radio power controls in the full and Cockpit surfaces, Search Nearby Sites, and Clear Selected Layers remain focusable while lifecycle work is in progress. They announce busy/disabled semantics and ignore repeat activation until the operation settles, so async work cannot drop the keyboard ring.
- Normal-mode Contacts results preserve the focused action or contact by stable identity while live counts, distance order, and pages refresh. If that contact departs or rotates off the visible page, focus moves to a stable continuation point on the named explanatory note at the end of the list and remains there through later repaints. The following Tab proceeds beyond the results instead of restarting at Contacts or silently focusing another contact.
- Cockpit Live Signals updates existing contact buttons without replacing or disconnecting the focused one. Tab can continue through the briefing footer to Display and Radio. If the focused contact leaves the list, focus moves once to the current briefing tab; later refreshes do not reclaim it.
- Cockpit-only Display and Radio launcher icons show a complete inset keyboard ring in their collapsed and expanded states.
- Escape collapses the nearest expanded panel containing focus before any containing panel acts. Closing from panel content returns focus to that panel's disclosure; closing from the disclosure itself clears focus so the collapsed button does not retain its ring. This includes standard panels, nested Parameters, Cockpit Contact and Live Signals, and Cockpit Display/Radio utilities.
- The required bottom-left Data attribution control is a named popup button in the Tab order. Enter opens immediately and Space opens on release, then the credit lightbox focuses Close; Close, Escape, or backdrop dismissal restores focus to Data attribution.
CCTV launcher and proxy failure responses
scripts/dev-cctv.sh delegates startup to scripts/dev-fresh.sh. It retains
its Austin source file, Austin preference, 36-camera Austin limit, and 48-camera
total limit, with environment overrides. Keys are optional; credential loading
and names-only provider provenance follow the normal launcher. The default
binding is localhost. An explicit HOST=0.0.0.0 uses the same LAN warning as
normal startup.
The CelesTrak, Launch Library, terrain-height, and ADSBDB middleware return fixed messages for unexpected failures. Launch Library retains its upstream HTTP failure status and no-store policy but does not forward the upstream body. Existing successful, in-flight, missing-data, and stale-cache behavior remains in place. Failure diagnostics identify the service and, for Launch Library, HTTP status without printing raw exception text or upstream bodies.
Map Source keyboard focus
Keyboard opening focuses the selected map-source tile, falling back to the first only when no source is selected. A bounded retry handles delayed tray visibility. Moving focus away, pointer interaction, closing the tray, or disposing the UI cancels pending work; a later reopening cannot inherit an old focus request.
September 8, 2026
Earthquake refreshes validate the complete feed and construct replacement entities before clearing the previous snapshot. Malformed rows and duplicate rendered IDs retain the last good entities, overlays, count and timestamp and report a malformed response; unknown magnitude is excluded from M2.5+ rendering.
Non-object or array-valued properties reject the response instead of being treated as an unknown magnitude.
Launch payloads with missing records now say PAYLOAD DATA UNAVAILABLE. Missing names use Unnamed payload; absent or invalid mass stays unknown instead of appearing as 0 KG.
Updated: September 23, 2026
Aircraft and vessel server modules
OpenSky credential-file failures emit a fixed warning once per process; neither JSON parser excerpts nor filesystem error messages are logged. The environment example uses an absolute path compatible with the macOS launchers.
Standalone aircraft routes now live in server/providers/aircraft/: OpenSky
state vectors and regional fallback, adsb.lol military positions, ADSBDB
enrichment and track backfill. Vessel routes and websocket/watchdog setup live
in server/providers/vessels/ais-live.js; AIS records and recent tracks live
in ais-store.js. Common response caps, request coalescing and query parsing
have their own modules. server/providers/local.js composes these with the
remaining providers and retains existing named compatibility exports.
gods-eye-view/server/providers/live is a Node-only entry for the existing
plugins and shared request helpers. Importing it starts no sockets or timers.
The existing aircraft normalizer is separately available through the portable
gods-eye-view/sources/adsb-lol export. Provider URLs, local credentials, cache
policy, fallback behavior, response shapes and rendering remain unchanged.
Control names for assistive technology
Scope, Bloom, Sharpen and location search have explicit accessible names. Generated style sliders use the same name as their visible parameter label. The first-run suppression checkbox keeps its native wrapping label, so its accessible name remains "Don't show this again". Control behavior is unchanged.
FIRMS source status
The FIRMS CSV parser rejects impossible acquisition calendar dates before the proxy filters records to the trailing 24 hours, while retaining valid UTC acquisition times.
The get_active_fires tool reports acquisition times as UTC ISO timestamps,
including zero-padded hours and minutes; missing or invalid dates and times
remain null, including impossible calendar dates from a custom source.
The FIRMS proxy records source success after appending its rows. If aggregation throws, that source reports failure without a contradictory success entry. The existing per-record append continues to support large feeds; sequential fetching, trailing-24-hour filtering and partial-success caching are unchanged.
FIRMS requests retry once over IPv4 when every resolved address fails to
connect. Node's network-family auto-selection gives each address a short
attempt timeout, so on hosts where IPv6 is unreachable a slow IPv4 handshake
was abandoned and the request reported only fetch failed (#68). The retry
makes a single IPv4 connection within the same deadline and the same body
cap, and does not follow redirects. HTTP errors, an expired request deadline
and DNS failures are not retried; a per-address connect timeout, which Node 26
reports as ConnectTimeoutError, is. A retry answered with a null-body status
(204, 205, 304) or a status above 599 fails that source instead of crashing
the server, and a non-OK response releases its connection at once. FIRMS
warnings include the per-address connection errors and never the request URL,
which embeds the MAP_KEY.
Installations and map-source guidance
- Keyless mapped installations use OpenFreeMap
landusemilitary polygons, merge touching parcels and tile fragments into one "Military area" marker; tiles carry no site names. The marker uses an area-weighted centroid, moved inside a footprint when necessary. Source-feature aliases retain ids across pans (4096-entry cap); grouping admits at most 2048 fragments per view and reports saturation if that cap is reached. Tile-buffer geometry is clipped to each core and artificial clipping edges never become outlines. Fills, boundary polylines and markers clamp to the visible globe or photoreal mesh. Wide views start at z10, local views at z12, stepping down no further than z9 to fit the 16-tile view cap: OpenMapTiles carrieslanduse=militaryonly from z9, so a view too wide for 16 z9 tiles is a "Zoom in" state, never an empty area. Camera changes debounce for 180 ms, with a final moveEnd check; identical windows reuse the active load. Disabling or returning Contacts to passive mode clears its anchor; passive refreshes cannot reclaim it. Background repaints refresh context records without publishing a new selection. Installation tiles decode only thelanduselayer. Reloads keep the Cesium entities of sites whose geometry is unchanged, so outlines and fills do not blink. While Contacts has a subject, the layer loads a window around it instead of the camera view (100 km half-width at mid latitudes, shrinking so it fits 16 z9 tiles), moves it only after the subject travels 20 km, and ignores cameramoveEnd; clearing the subject returns to the viewport. The row status names what was found ("3 mapped sites within 100 km of the contact", "No mapped sites in view") and tile failures read "Map tiles temporarily unavailable". Google Places search/classification stays separate and unchanged. Configured Overpass can still supply named sites; a not-configured miss switches to tiles once. Retryable tile/configured-upstream failures retain the existing 30–240 second backoff, extended when Retry-After requires it. The top status and Contacts row explain the outage and scheduled countdown; an active retry says "Retrying mapped sites" and successful recovery clears the previous error. Known upstream rate limits, timeouts, and query failures are distinguished without exposing raw server errors. Failures from other loading layers retain precedence. - Click a selected installation again or click elsewhere on the map to clear its selection. Clearing the installation does not clear another layer's newly selected contact, and refreshes do not revive the cleared site.
- Installation ways and relations without an explicit center use the midpoint of finite, ordered bounds spanning at most 10 degrees per axis. Explicit coordinates and centers retain precedence; invalid bounds are dropped.
- Visual-style buttons describe their simulated effects on hover. Unavailable map-source tooltips and toasts share provider guidance: missing credentials point to Provider Settings, while a configured Google 3D route that fails points to restrictions, quota, or connectivity. These hints do not expose keys.
2026-08-23 — first-run mission launcher (
src/firstRunExperience.js,#first-run-launcher, styles at the tail ofstyle.css). After startup settles, a fresh session gets one card offering Live Contacts · Space Missions · Environmental · Explore manually. No layer and no optional API call happens until a tile is clicked. The right-hand DISPLAY rail (pp-toggles) now starts collapsed on a first run rather than expanded — a stored collapse state still wins, as before.The ENVIRONMENTAL tile is quakes AND fires — live USGS earthquakes plus NASA FIRMS active fires (
layerIds: ['earthquakes', 'local-firms']), with the tile subcopy naming both. The launcher optimizes for the fully configured experience: it does not trim what it offers down to the lowest-configured install. The mission does not branch on whether a key is present — everyone gets the same tile.Keyless, the honest surface is the layer row, which reads
UNAVAILABLE · NASA FIRMS · LIVE · KEY REQUIRED. Its control also names the key: hovering it, and its accessible name, readNeeds FIRMS_MAP_KEY — add it in Provider Settings. A layer declares which key it needs as a key-registry id and reportsstats.keyRequiredwhile that key is absent; the panel builds the text from the pair, and produces none for a layer that needs no key, already holds one, or names a key the registry does not recognise. The earthquakes half still delivers in full. The shared loading reducer now treats an explicitly declared missing optional key as a configured terminal state rather than a failed multi-layer mission, so the global chip completes without showingLOAD FAILED. A genuine lifecycle or fetch failure still retains failure priority.Acceptance changed with that ruling (2026-08-23).
qa-firstrunno longer asserts "a keyless Environmental never shows a failure chip" — that stopped being a launch requirement when the tile went back to promising both feeds. The Environmental section now branches on the observed key state and says which branch it took: KEYED asserts both datasets actually arrive and that no LOAD FAILED banner appears while the mission runs; KEYLESS asserts the layer-row honesty (KEY REQUIRED), the quakes half loading, and that the deliberate missing-key state never becomes a global failure.An INFRASTRUCTURE tile is deliberately absent. It was built, playtested, and cut: one click enabling
local-datacenters+local-dams+telegeography-submarine-cablesputs ~5,700 entities on a full-earth view and the frame rate goes with them. The layers are unchanged and still reachable by hand and by voice ("infrastructure mode" is still mapped). Do not re-add the tile before the bundled-infra globe-LOD declutter lands — that is the real fix, and it is post-launch work.Show policy — it is NOT one-shot. Precedence, highest first: a share link never sees it →
?welcome=0suppresses →?welcome=1replays (past both suppressions, for demos/support) → the durablelocalStorage['gev:first-run-mission:v1'] === 'suppressed', written only by the "Don't show this again" checkbox → the per-sessionsessionStorage['gev:first-run-mission-session:v1'] === 'dismissed', written by every close path (mission, Explore, ESC, the footer dismiss button). So it returns each fresh browser session until the visitor ticks the box; clearing storage un-ticks it, which is accepted. Both stores fail open — an unreadable store still shows the launcher rather than silently swallowing first launch.The footer dismiss button is the keyboardless ESC. Phones and tablets have no ESC key, so the footer hint is a
data-first-run-dismissbutton that calls the samedismiss()as ESC. It stays visible at every width (phones used to hide it, leaving a mission as the only way out) and reads DISMISS on a(hover: none) and (pointer: coarse)screen, "ESC to dismiss" otherwise. Its 44px touch target is a::beforeoutside the layout, so the footer row keeps its one-line height.What a mission may persist (do not "simplify" this). Layer enablement is durable in this app (
gev:layer-state:v2, written byLayerStateCoordinator._commitExplicitonly for originuser/voice/tool). A mission enables its own layers atorigin: 'user'— durable, exactly as clicking those rows is, because picking the mission is that choice. The two Context missions also expand the Context panel, as the visible tabs do; the globe missions open no panel. Everything else is off limits: detection mode/density,gev:detection-allocation:v1, 3D models, feather, and above all_detectionUserOverridden— setting that flag means "the operator hand-edited detection" and would silently disable the CRT/NVG/FLIR auto-preset contract for the session. The full table is a comment block in the module and is pinned bysrc/firstRunExperience.test.mjs.Voice is instruction-only. Both globe missions are expressible with shipped tools (
set_layer_visibility's enum already carrieslocal-datacenters,local-dams,telegeography-submarine-cables,local-firms,earthquakes;zoom_to_globesupplies the camera), soGEV_REALTIME_TOOLSis byte-identical tomainand pinned by sha256 in the unit suite. One instruction paragraph invite.config.jsteaches the phrase mapping; deleting it is the complete rollback.ESC arbitration — three rules, do not collapse them into one. (1) The launcher yields: a MutationObserver watches
bodyfor the surfaces that take the screen (cockpit-mode,scene-playback-mode,recording-mode,ui-clean-view—EXCLUSIVE_SURFACE_CLASSES, kept in step with the CSS hide rule by a unit pin), and session-dismisses rather than contesting the key; if one is already up at init it waits instead of appearing over it. (2) A surface can take the screen with no class to watch — the Cesium attribution lightbox is atz-index: 200against the card's175, which left the launcher measurable (getClientRects()non-empty) and buried, so ESC dismissed a card nobody could see and burned the session flag.isTopmost()therefore also hit-tests the card's own centre withelementFromPoint; any overlay, classed or not, disarms the handler. Every inconclusive answer counts as uncovered, so the guard can never be why ESC stops working. (3) A small control that claims only the key (a disclosure, a popover) is not something to yield to: whoever handles ESC first callspreventDefault()andstopImmediatePropagation(), and the launcher skipsdefaultPreventedevents.stopPropagation()alone does not stop later listeners on the samedocument— that is exactly how a disclosure that called it alone made one key close the disclosure and dismiss the launcher. Cockpit's utility disclosures (Display, Radio) claim the key the immediate way.Accepted: a surface class that never clears means no launcher for that page load, with no timeout. None of the four classes is restored at startup, so an already-blocked init is an error path, while a long recording or clean-view session is ordinary — a "reveal anyway" timer would trade a benign no-show for the card punching through a recording in progress. The no-show is benign: the handler is inert, no session flag is written, the observer still reveals the card if the class clears, and it returns next session either way.
Blocked storage un-ticks the box. "Don't show this again" is a claim about the future, so a refused
setItemreverts the checkbox and says so in the status line instead of showing a saved preference that was never saved.Gates:
node scripts/qa-firstrun.mjs --url <app>(in-app checks across eight independent sections) plus its--teethnegative control, which removes the launcher and requires EVERY launcher-dependent section to go red — it always exits non-zero,1meaning the control is healthy and2meaning it is not. Plus the unit pins above.
2026-10-24 — a turned-off layer's memory is released: Cesium 1.138 keeps the draw commands past each frame's count in the opaque and other per-pass arrays (
View.frustumCommandsList) and in the offscreen pick view, so a turned-off layer's entities stayed reachable (Data Centers kept 116 MiB).src/sceneCommandRelease.js, installed insrc/app/tools.js, requests one frame throughgovernorRequestRenderafter every layer disable and then trims those arrays. It reads private Cesium fields;src/sceneCommandRelease.test.mjspins them and fails if Cesium renames them or changes which passes it truncates.
2026-08-08 — performance waves 1+2: the app idles via an explicit render governor (
src/renderGovernor.js— hold/release from every per-frame animator; discrete mutators callgovernorRequestRender). Any NEW per-frame visual animation MUST register a hold; any new discrete scene mutation MUST request a frame —scripts/qa-perf.mjsis the gate. The circular scope is an explicit canvas (src/scopeMask.js, DISPLAY-rail SCOPE toggle + FEATHER slider, hash keyssc/scf) — it is NOT the six zero-intensity style stages anymore (those are disabled; see the history note in_initStages). Hidden tabs stop the render loop. AIS vessels no longer hold the loop while enabled (2026-09-30): vessels move only when a poll lands, so the layer requests a frame for every forced pass (new data, selection), leaves one trailing frame when the camera moved inside the 800 ms visibility cadence, and holdsais-vesselsonly while focus emphasis animates or any vessel stays dimmed under a live focus target. The trailing frame goes through the layer's injectable runtime clock and is cancelled on disable, destroy and reset (renderGovernor.test.mjspins the no-self-wake invariant). The 80 ms focus pass also skips hidden sprites whose focus state is at rest (spriteFocusAtRest), since advancing them changes nothing; with a 12k worldwide feed that is most of the fleet. The scope's OUTSIDE terminus is altitude-adaptive (2026-08-17): 0.94 at/above 10 Mm, so faint stars survive in the corners of a TRUE full-globe view, fading quickly (smoothstep) to fully opaque by 7 Mm — every working altitude below that is solid black, because there the same 6% bleed reads as smeared geometry. FEATHER is unaffected by the terminus ramp and there is NO new slider (its own default later moved 35 → 0 on 2026-08-22, 0 → 8 on 2026-08-23, and 8 → 11 at the 2026-08-24 final lock; see Current Global Post Defaults); hash keyscepins the terminus and is clamped to 94..100 on BOTH parse and write (out-of-range clamps into the band; absent or non-numeric = the adaptive default), so a shared link can neither freeze the ramp by accident nor carry an unsupported sub-94 terminus. Repaints are gated on a quantized 0.005 alpha step, so a full 20 Mm→ground descent costs 12 canvas repaints (the alpha span sets that, not the altitude span) and a parked camera costs zero. That quantization also means the PAINTED value plateaus at each end of the band: measured, the painted terminus stays 0.94 from the top of the band down to ~9.63 Mm (first step), and is solid black from ~7.37 Mm (last step) rather than exactly at the 10 Mm / 7 Mm clamp heights. SCOPE OFF costs less still: no height sampling and no canvas work after the single clear on the disable transition. The hard-crop (FEATHER 0) path honors the same terminus.
This is the current runtime/source-of-truth snapshot for the project.
Important
Delta since the July-2 body below (the detailed sections are still accurate for everything they describe; these landed after):
- "Never answered yet" is a THIRD state, distinct from empty (2026-08-23):
sourceStateinsrc/data/militaryAwareness.jstreats a dependency that is busy AND has never produced an answer (loading === true && !lastUpdate) as unavailable, so the Contacts panel prints?and voice says "unknown". This is deliberately NOT "busy": a source that has answered once keeps its real count through every later refresh poll. It is a CONTRACT over the whole dependency list rather than a fix for one layer, and the dependencies reach it by different routes:
- AIS vessels is its reachable producer.
enable()/update()both resolve as soon as the first/api/vesselspoll answers, so the manager's lifecycle settles toenabled— but until the server-side socket delivers a position,firstConnectPhasestays'loading'andgetStats()reportsloading: true,lastUpdate: null, count 0, and an UNDEFINED status. Without the predicate that window prints an all-clear0.- Mapped installations never reaches that window.
militaryInstallations.enable()is synchronous and the manager awaitsupdate(), which owns the first mapped-data fetch, so the lifecycle staysenablingfor the whole fetch and the pre-existingenablingbranch covers it. Confirmed live on :4272 across a held 17 s first fetch (34 samples,enablingthroughout, panel non-numeric) and across a failing one. Itsstatus: 'idle'is not what saves it — the lifecycle is; the module has noloadingstatus at all. Any new dependency that can be slow must reportloadingandlastUpdatehonestly for the contract to hold, and must be pinned against the shape its own module really returns — a fixture that invents a status the module cannot emit guards nothing (that is exactly how a hole here survived a green suite).- A held ground snap is dropped when measured ground contradicts it (2026-08-23): the WARM hold described below answers on DISTANCE travelled, which is only a proxy for whether the value still describes the ground. A contact can taxi ~200 m onto a different surface INSIDE the 250 m bound, and since a miss preserves the hold, nothing would ever correct the burial. So
heldSnapM(src/data/groundSnap.js) also consultscachedMeshFloorat the contact's CURRENT position: a measured floor more thanHELD_SNAP_CONTRADICTION_M(5 m) ABOVE the held value drops the hold and the contact is COLD again, back to the floored 2D billboard. The rule is ONE-SIDED on purpose — it targets BURIAL. A cell reading BELOW a real sample is the floor chain's expected under-read (one-shot latch over ~111 m, neighbours lean lowest,displayFloorHeightMonly ever raises), not evidence of anything, and a two-sided cut was disproved on the track rig by a planted cell 66.7 m below a real sample at the same spot. Accepted residual, stated with its condition: a DOWNHILL taxi floats, and the only correction on this path is a SUCCESSFUL resample — nothing guarantees one. On the OSM fallbacksampleHeightmisses forever, so the float persists for as long as the contact stays inside the 250 m bound, with no timer beside it and no vertical cap of its own beyond whatever the ground drops within that radius. It is accepted because it errs UPWARD and stays visible. Three boundaries are load-bearing — that one direction, MESH cells only (cachedGroundFloor's DEM fallback is a DIFFERENT surface; the skin/DEM spread is whatMESH_FLOOR_BELOW/ABOVE_PRIOR_Mbudget 15 m / 80 m for) and the contact's OWN cell only (a neighbour ~111 m away may be a terminal roof —neighborFloorMleans lowest for that reason — and must never be borrowed to DISCARD a real measurement). Do not widen either without re-arguing both.- The trail's acceptance bar is visual (2026-08-23): the trail terminates roughly BACK-CENTRE on the aircraft; MINOR hull overlap is acceptable; there is no conspicuous top, bottom or lateral protrusion; it is stable across headings; and a parked aircraft draws no moving head segment. That is the bar a future change is judged against — NOT sub-metre precision. The pins below are tighter than the bar on purpose, because a measurable property is what a test can hold, but a pin's tolerance is not the product requirement and tightening one is not an improvement to the picture. Measured on live traffic at the shipped transform, the airliner anchor sits 24.09 m aft (70 % of the model's rendered envelope, so inside it), 2.22 m below centre (6 %), and 2e-9 m off the centreline.
- The tracked trail attaches to REAL HULL, aft and below (2026-08-23):
MODEL_TRAIL_ANCHOR_NATIVE(src/data/modelVisualAnchor.js) holds, per GLB and in RAW glTF coordinates (see the transform-chain entry below), the point of the hull's CENTRELINE PROFILE (its y = 0 slice) closest to the aft-belly AABB corner. It is NOT the corner: a bounding-box corner is empty space, 4.80 m off the nearest triangle onairplane.glband 6.14 m onjet.glb, so the trail ended in mid air beside the aircraft. The head segment is drawn from a point behind the aircraft to this anchor, so the anchor must stay aft (83–96 % of each aft extreme) or the segment enters at the tail and stops inside the fuselage.modelScale.test.mjsreads the POSITION BUFFERS — real vertices and triangles, because accessor min/max cannot tell a corner from a surface — and re-derives on-hull, aft, lowest-at-its-station, on-centreline, and the construction itself. Re-measure, never re-guess.- The trail anchor rides CESIUM'S transform chain, not a hand-rolled one (2026-08-23 regression fix):
MODEL_TRAIL_ANCHOR_NATIVEstores RAW glTF coordinates andmodelAnchorWorld()assemblesmodelMatrix × components.transform × axisCorrection— the same chainModelSceneGraphrenders with, built from Cesium's own exportedAxis.Y_UP_TO_Z_UP/Axis.Z_UP_TO_X_UPand the model instance's own root transform. The anchors were previously PRE-CONVERTED by a single glTF Y-up → Z-up step ([x,y,z]→[x,−z,y]) and multiplied straight bymodelMatrix; that is half the correction (the defaults also applyZ_UP_TO_X_UP, the complete mapping being raw[x,y,z]→[z,x,y]), and an aircraft's longitudinal axis is raw glTF X, so the aft offset landed on the RENDERED model's LATERAL axis.modelMatrixcarries the heading, so both frames rotated together and the trail terminated a fuselage-length to one SIDE, swapping sides with the course. Measured live in each aircraft's own (aft, cross, up) frame: civil airliner (0.00, −81.47, −7.50) before, (81.47, 0.00, −7.50) after; rotorcraft (0.00, −73.67, −16.33) → (73.67, 0.00, −16.33). Never pre-convert an anchor — one transform, Cesium's, or a second hand-maintained convention drifts again. The pin sweeps headings 0/45/90/180/270/315 across every shipped asset plus a hovering rotorcraft and asserts NO lateral component, and it derives its reference axes from the ENU frame and the heading rather than through the function under test: the first version routed them throughmodelAnchorWorldand passed against the very bug it was written for, because a wrong transform rotates the anchor and its reference frame together.- A stationary contact draws no trail head, and the head end NEVER gives (2026-08-23): grounded tracking starts a trail unconditionally, and on a contact that has not moved the last body point sits where the aircraft is — so the head segment became a line from inside the model out to its own anchor, through half the fuselage.
trailHeadStart()decides where that segment starts being drawn: nothing while the last body point is no further from the model centre than the ANCHOR'S OWN STATION (every millimetre would be drawn forward of the attachment point, into the fuselage — a parked contact sits at exactly zero), the whole segment once it has cleared the model's rendered ENVELOPE, and between them the drawn start slides along the segment so the visible length grows CONTINUOUSLY from zero. Three earlier cuts are recorded because their shape matters: testing segment LENGTH against the radius HID real trail (58 m aft ofairplane.glb's 34.41 m envelope gives a 33.98 m segment, suppressed, though ~23.6 m of it is open air); CLIPPING at the envelope and keeping only what lay outside stopped a moving trail visibly SHORT of the aircraft, since a bounding sphere encloses a lot of empty space around a slender airframe; and a BOOLEAN containment test flashed 10.33 m of trail on and off across 2 cm of travel at the boundary, and again on any fix that fell back inside. The END is never cut — that end is the whole point.radiusMcarriescomputedScale, so the verdict is the same at every camera distance, and any contact that has moved more than its own size (every airborne one: 30 s of flight is kilometres) gets bit-identical geometry to the containment rule this replaces.- The AIR bracket alpha floor SCALES with the OUTSIDE slider (2026-08-23):
aircraftBracketAlphaFloor(src/data/detectionPolicy.js) is piecewise linear through 0 → 0, the default → 0.35, and 100 % → 1.0. The default value reproduces the previously shipped flat 0.35 exactly at every keyhole alpha, so the approved bracket look is unchanged; a flat floor made every reachable stop below 35 % paint identically.AIRCRAFT_BRACKET_FLOOR_ANCHORmirrorsKEYHOLE_OUTSIDE_OPACITY_DEFAULT(kept Cesium-free on purpose), the two are pinned together, and the anchor MOVES WITH THE DEFAULT — both are0.01since the 2026-08-24 final lock (0.03on 08-23,0.05before). That pin is the tripwire for a default move: the mapping is pinned to bracket BRIGHTNESS, not slider position. Thedetection-opacity-sliderstepis 1 so low values are reachable — the mapping was always continuous from 0, but at the previous step of 5 the entire sub-default range was one stop wide. Both the markup and the ordering of 1–5 % are pinned indetectionPolicy.test.mjs.- A grounded contact HOLDS its floor through a terrain outage (2026-08-21): when the Re:Earth proxy fails, the floor cells a grounded contact stands on never warm, and the un-clamped render height for a contact reporting no altitude at all is the GEOID — ~150 m below the ground at an inland field. Both steps now hold instead. At POLL time
geoidSurfaceLastResortM()(src/data/renderAltitude.js) withholds the geoid guess from any contact that already has arenderAltitudeM, so the sentinel path holds that height; the guess is reserved for a genuine first sighting. At DISPLAY time_heldDisplayFloorM()(src/data/flights.js) answers with the contact's own last resolved floor — valid withinHELD_FLOOR_MAX_DRIFT_KM(1 km, one rollout's worth of travel) of the cell that supplied it — and otherwise with a resolved ADJACENT cell vianeighborFloorM(), which takes the LOWEST of at leastNEIGHBOR_FLOOR_MIN_SAMPLES(2) resolved neighbours and otherwise refuses. An earlier cut leaned HIGH, reasoning from "never below the visible surface"; that principle is about a contact's OWN measured ground and it inverts for a BORROWED cell, as playtesting confirmed — planes floating at terminal gates. The errors are not symmetric: too LOW is inert (displayFloorHeightMonly ever raises, so an under-reading floor simply does not lift, bounded by one cell of grade), while too HIGH invents a position bounded by BUILDING height and a parked contact holds it — measured at 29.5 m of permanent float from a lone roof neighbour. A plane at a gate is on the apron, never on the roof. The honest residual is the mirror image: on a genuine slope the lowest neighbour under-reads, so the clamp lifts a little less than it could, which shows up as no lift rather than a wrong one and is corrected as soon as the contact's own cell warms. Both tiers are validated measurements out of the shared floor cache; with neither available the position passes through untouched, exactly as before. Adjacent-cell probes are throttled per contact (500 ms) and rationed no further: a probe is eight synchronousMapreads with no I/O — every DEM request is driven bywarmGroundFloorfrom the poll loop, bounded there — and 200 synchronised all-cold contacts probing on the same tick measure 1.0 ms median / 1.2 ms worst on the all-cold workload (a noisier ad-hoc run of the same workload peaked at 2.1 ms), 1.4% of one 80 ms fleet tick (scripts/qa-floorhold-probe-cost.mjs). A global per-tick budget with a fairness queue was built over that and DELETED: it protected single- digit milliseconds and produced two starvation defects. Nothing can starve because there is no shared resource to be starved of. A floor that moves DOWN under a contact standing on a BORROWED one is APPROACHED exponentially (FLOOR_EASE_TAU_MS, 360 ms — ~20% of the remaining gap per fleet tick, hard-capped atFLOOR_EASE_MAX_STEPso a delayed or stalled tick cannot close more) from the value currently displayed, rather than interpolated from a fixed anchor over a fixed duration. The target moves: a second, lower neighbour can warm mid-approach, and re-evaluating a fixed anchor against a moved target jumps by the eased fraction of the change (measured at 100 m in one tick). Approaching from the displayed value has no such seam. Rises are always taken whole, including mid-approach, since an eased rise is time spent under the mesh; a change between two resolved floors keeps its existing timing. The hold state is retired the moment a contact stops being a grounded billboard (airborne or model-owned), but the floor itself is PARKED as a rehydration seed rather than destroyed: deleting it outright let anon_groundflap through a rotation cold-start the contact under the runway (observed with VIR138M at JFK). A seed is a memory, not a reading, and three bounds keep it honest.HELD_FLOOR_MAX_DRIFT_KMrefuses it more than a kilometre from where it was measured.FLOOR_SEED_GRACE_MS(90 s, three polls) expires it on wall-clock age, judged BOTH while the contact is away and AGAIN at the moment it re-grounds — a contact that makes no calls in between (off the poll on a cruise, outside the corridor radius, tab hidden) never reaches the first check, and an earlier cut that only had that one reused a floor parked 198 s earlier. And the seed ranks BELOW the neighbour tier: two freshly resolved adjacent cells overrule it, which is what stops a contact re-grounding half a kilometre away from floating on the field it left (measured before that rule: a 200 m seed held over a 100 m/105 m neighbourhood, 100 m in the air). So a short hop back onto the same apron inside the grace window and the drift bound DOES reuse its floor — deliberately, and only while nothing fresh contradicts it. What starts clean is a genuine departure. A model→billboard handoff retires and rehydrates by exactly the same rules. What none of this fixes is the flap tick ITSELF: the display clamp passes airborne positions through by design (an airborne height is the fix-time clamp's job), and at a sea-level field the airborne fix IS baro + geoid N, ~4 m under the runway. That is an accepted one-tick transition residual, andscripts/qa-floorhold-staircase.mjs§F1 counts every tick so it stays visible: 1 of 23 ticks below the runway, all of it that airborne tick, 0 of 22 grounded ticks — against 12 of 22 grounded and not recovering before the seed existed. A third tier that read the rendered mesh where no DEM existed to validate it was built and REMOVED — measured against a real GPU with the proxy down, it recorded a coarse-LOD 20.6 m for ground that is really ~122 m;tilesLoadedgoes true while coarse tiles are what is loaded, so without a DEM prior there is nothing to tell a surface from a mis-hit. Gates:scripts/qa-floorhold-mutations.mjs(22 named defects, each reverted individually and required to go red) andscripts/qa-floor-hold.mjs(live, real GPU — the proxy is failed mid-run and the contact is measured againstscene.sampleHeight). This floor-hold path currently applies toflights.js;militaryFlights.jsdoes not use it.- Screen picks are validated before conversion (2026-08-21): anything that comes back from
scene.pickPosition()must clearisPickedWorldPosition()(src/data/scenePick.js) before it is converted to a Cartographic. The guard is a magnitude BAND — 6,000,000 m to 1,000,000,000 m, plus finite components — not a null check, because a depth read over empty sky can return a Cartesian that Cesium mishandles three different ways: non-finite throwsDeveloperError: normalized result is not a number, exactly(0,0,0)returns undefined, and a near-center value such as(500,0,0)converts SILENTLY into a point 6,378 km underground that reverse-geocodes as 0°, 0°. The floor sits ~346 km below the smallest real surface magnitude (WGS84 polar radius 6,356,752 m); the ceiling is ~24× geostationary, so no real contact is rejected. A degenerate pick is a MISSED pick: the cascade ingetViewTargetCartesian()andpickWorldFromScreen()falls through topickEllipsoidand then the globe ray, and callers receive the samenullthey already handle for a miss — there is no new sentinel. Two consumers had no owner for a throw and were hardened to match: the moveEnd view-target prewarm runs insiderequestIdleCallback(now catches and reports once per viewer atconsole.debug), andIntelHUD._updateSummary()awaits its context INSIDE a guard, because every caller invokes it asvoid this._updateSummary(...)and a rejection there is ownerless. Gate:scripts/qa-view-target-prewarm.mjs.- Scene playback ownership + reconcile (2026-08-20): five corrections to
src/scenes/director.js, with the pure decisions insrc/scenes/scenePolicy.js. (1) A shot reconciles ONLY the layers it declares. The old walk forced every undeclared layer off, so a recipe authored against the original four layers tore down CCTV, vessels and fires with no restore pass. Operator captures snapshot the whole registry, so those still reconcile in full. (2) Playback claims the camera throughrunImmediateNavigation('scene', …)instead of a barecamera.flyTo, so it releases the follow camera, respects the navigation policy and lets Cockpit refuse before anything mutates. (3) Playback NEVER re-establishes tracking.SCENE_TRACKING_PARAM_KEYS(selectedFlightsTrackingId,selectedMilitaryTrackingId,selectedSatTrackingId) are stripped on the way to the layer — a shot captured while following a contact would otherwise hand the camera straight back to the follow loop it was just taken from, recreating the two-writer jitter. The keys stay in the STORED capture; only the apply path drops them. A unit pin sweeps every layer'sgetParams()for the whole selection naming family (SCENE_SELECTION_PARAM_PATTERN—selected…/tracked…,…TrackingId,…Mmsi/…Norad/…Icao), and each match must be on the strip list or onSCENE_KEPT_SELECTION_PARAM_KEYS; a name liketrackedVesselMmsitherefore cannot slip through by not matching the older spelling. CCTV'sselectedCameraIdis the one recorded keep: it raises a monitor plane and never writesviewer.trackedEntityor the camera. (4) An isolating Context mode is exited before a shot's layers apply. Space Missions refuses every enable outside its replay bundle, so with it left dirty all five recipes were refused (or, for Orbital Watch, composed over a replay they never declared). The verdict is read off the shared guard (contextLayerEnableBlockReason), not a mode name, so a future isolating mode is covered — the probe idSCENE_EXCLUSIVITY_PROBE_LAYER_IDis reserved by a test against the real layer registry. The exit itself is the ordinarysetContextMode('off')exact-restore path and is deliberately NOT abortable: leaving the mode IS the restore to the operator's pre-mode state, and tearing that transaction in half would strand Context. A refusedsetEnabledis surfaced in the status line and ashot_layers_refusedtelemetry event instead of being reported as success. Mixed accepted / refused layers are reported honestly, not rolled back — a shot whose enables partly failed leaves the layers that did move in place. (5) Cancellation cancels the work, not just the next step. Checking a flag after anawaitonly stops what has not started, so both awaited operations are now themselves cancellable. Layers: the run/LOAD owns anAbortControllerwhose signal is passed to everysetEnabled, and STOP, supersession, and run teardown abort it — the data manager rolls an aborted enable back through the module's owndisable(), so no layer is left on carrying stale params. Visuals:styleManager.applyVisualState(state, { isCurrent })gates BOTH halves of the map-stack switch, which is its only suspension point. The switch is a mutation, not just a wait, andmapStackControllerinvalidates a switch only when anothersetStack()arrives — a winning state that omitsmapStack(every normalized shot does) never issues one, so a stale switch would otherwise stand on the globe. So: an already-superseded caller never starts the switch, and one superseded during it puts the globe back to the stack the winner inherited — but only whilegetSwitchGeneration()shows no newer switch has claimed it, because a newer switch is a live intent that must not be stomped. The shader-uniform commit after the await keeps its own gate. Precisely: a stale LOAD's synchronous prelude (style/bloom/HUD) can still have landed before it was superseded, and is then overwritten by the newer state; what cannot survive is its map stack, its uniform commit, its layers, or its camera flight. Post-awaitflag checks remain as a backstop. Known remainder: run cleanup still waits on a suspended visual/map operation, becausesetRecordingMode(false)is not idempotent (a second call restores HUDautorather than the operator's saved mode) — worst case is recording chrome staying up until the promise settles, which then restores correctly.- Ambient contact labels (2026-08-20): detection callsign callouts paint on the shared normal-blend world-overlay canvas as their own host lane (
detection-callouts) — NOT on the screen-blended sensor surface.screencan only lighten, so a dark backing plate drawn there is a no-op over sunlit ground and the text dissolves into the imagery. Brackets, the scanline wash and the mode banner stay on the sensor surface; the callout lane registers in the samedetectionslot, so callouts keep their z-position beneath every ordinary overlay card and beneath the tracked readout. Plate fills come from per-themecalloutPlate/calloutPlateSpacetokens (~63% / ~73% of the tracked card'sCARD_PLATE_ALPHA), resolved once per style change; space-tier contacts take the heavier one. Rows are pooled, and an empty field must clear the replay buffer or the final callsigns strand on the canvas. Do not move callouts back onto the sensor surface, and do not merge the plate tokens intolabelBg— that token is the scanline wash and shifting it retunes the sensor texture. Plates are additionally backdrop-selective (2026-08-21):skyBackdropFactor()(src/data/iconOrientation.js, exact scaled-space ellipsoid silhouette) feathers the plate FILL ALPHA toSKY_PLATE_SCALE(0.18×) for labels above the horizon — sky backdrops read as near-bare text, terrain backdrops keep the full plate — across a smoothstep band ofHORIZON_FEATHER_RAD(~1.09°/side). Only the plate alpha feathers; text, tier accents, leaders, brackets, and the tracked readout card are untouched. The test is TWO-REGIME, and only the first regime is a ray test (2026-08-22). Above the ellipsoid,1means the view ray genuinely misses the planet and the function is the exact complement of the occluder. At and below the ellipsoid the horizon is EYE LEVEL — the local geodetic horizontal plane through the camera — and that is NOT a ray-miss test: from a −18 m camera the ray to a contact 900 m up crosses the ellipsoid and still reads sky, deliberately, because the ellipsoid it crosses is not a surface anyone can see. It is reached by clamping the tangent cone's half-angle at 90°, the continuous limit of the same formula (the horizon dip goes to zero at the surface), and is the convention Cesium'sEllipsoidalOccluderalready uses there, so the two stay sign-consistent. This is not an edge case: coastal airports sit at NEGATIVE ellipsoid height (JFK ramp ≈ −30 m, geoid ≈ −34 m), so a ground-level cockpit is genuinely inside the ellipsoid and must not be treated as degenerate — doing so put a full plate behind every label on an empty sky. Fail-closed now covers only unanswerable input (null, zero-length ray, non-finite camera, camera at the planet's centre). Note the pairing: from such a camera the occluder culls every contact BELOW eye level before detection sees it, so ground-backed labels only reappear once the camera clears the ellipsoid. Rendered proof:scripts/qa-cockpit-plates.mjs.- Required attribution has two named keep-out rules (2026-08-20): the Google/Cesium credit line must stay visible in every state, and below 900px two surfaces used to paint over it — the command dock's popover tray (any width ≤900px) and the right context rail, which goes edge-to-edge below 720px and covered the credit with every dock panel closed. Both now yield; the credit itself never moves, shrinks, or hides. The clearance is not a single constant:
#command-dockis anchored at2vhdown to 721px and re-anchors to a flat8pxat 720px while#cesium-creditskeeps its2vhbase, so anything reasoning "the 2vh terms cancel" is only true in the 721–900px band.src/creditAttribution.test.mjsis a fail-closed cascade model: it flattensstyle.css, resolves each anchor by importance → specificity → source order, evaluates a 14×11 viewport grid, and fails loudly on any construct it cannot resolve (!important,inset/marginshorthands, unvetted custom properties, unparsable or nested media queries, or an unrecognized selector positioning one of these elements). Extend the model rather than working around it — a silent skip here ships a ToS violation.- Dock tray stacking is decided by ID count (2026-08-20): the pinned-tray selectors
#command-dock.dock-has-two-pinned-trays …carry one ID against five classes, so any narrow-width override written with two IDs outranks them and the upper tray silently losesvar(--dock-lower-pinned-height), landing on its pinned sibling. The ≤900px and ≤720px overrides therefore name the panel (#location-bar/#control-panel) to reach (2,5,0). Adding a new tray rule means checking it against the pinned variants, not just against the base rule.- LOCATION mini-status is data-only today (2026-08-20): the collapsed readout now follows a free-text geocode search as well as preset pills (
src/locationStatus.jsowns the copy for both), and every other camera destination invalidates the searched label —_stampNavigationcovers voice/reset/takeover/selection, and scene playback calls the publicclearSearchedLocation()per shot.#command-dockstill hides.location-mini-statuswithdisplay: none !important, so none of this is on screen; adisplay:nonesubtree is also out of the accessibility tree, so nothing is announced. Unhiding it is a separate product decision.- The Street Traffic sync chip shows one percentage (2026-08-20): the settled confirmation flash carries the layer's coverage figure and NO progress number.
reduceTrafficSyncFeedbackreturns an emptyprogressTextonce the sync lands and#traffic-sync-progress:emptycollapses the slot; the renderer writes the empty value rather than guarding on truthiness, or the busy...strands beside the settled label.#active-style-namehas exactly one writer (2026-08-20): the style-name mapping insetStyle. Location, search, and scene paths report where the camera is through the LOCATION surfaces, never the style slot.- Share-link selected-subject Follow (2026-08-20): a copied v2 link adds an ephemeral
atepoch-seconds field; ordinary live hash updates omit it. A shared Flights, Military Flights, or Satellites selection restores only after the base destination camera, ordinary layer restoration, and a new destination-scoped source refresh settle. The source module—not lifecycle success or the UI—owns the final presence decision: Flights and Military use the exact accepted snapshot, while Satellites waits for the applicable dense catalog and treats a partial CelesTrak catalog as unable to prove absence. A found subject starts the normal moving Follow at its current position regardless of link age. An authoritatively missing subject isexpiredonly when copy age is strictly greater than 90 seconds for Flights, 45 seconds for Military, or 5 minutes for Satellites; equality, missing or malformed time, and other non-found cases are unavailable. Feed/catalog failure has its own feed-unavailable message. These warnings use the universal top-center status banner and its standard failure dwell, beginning only after the shared-view startup cover clears. If teardown or disable invalidates an in-flight refresh, any selected-subject restore waiting behind it settles as cancelled instead of remaining pending. Terminal non-found cleanup compare-clears only the exact passive ID in memory and the live URL, never recipient local storage. A newer explicit selection, visibility request, destination, pointer gesture, wheel gesture, destroy, or source cancellation wins and suppresses late Follow/status work without cancelling unrelated shared layer visibility or options. Radio station selection remains outside the share payload; Radio restores only its allowlisted filter and volume.- AIS feed watchdog (2026-08-18): feed liveness is judged by DATA, not socket state — AISStream can complete the handshake and then deliver nothing forever.
/api/vesselsreportslive | stale | reconnecting | down | auth-failed(plus the unchangedmissing-key/unsupported) withsilentForMs,reconnectAttemptandnextAttemptAt. Silence is REPORTED at 120s and ACTED ON at 300s; recovery walks a 5s/15s/60s/300s ladder and then stops at a terminaldownwith a slow 15-min retry running behind it (a retry never flips the chip back to "connecting" — only real data clearsdown). Liveness credit requires a frame that arrived on a still-owned socket AND decoded into a real AIS record: handshakes, malformed frames and error envelopes are never liveness, and orphan frames are dropped entirely. Failures are CLASSIFIED — auth rejections (error envelope, HTTP 401/403) are terminal with an hourly probe and an actionable chip, 429 honoursRetry-Afterand otherwise enters at the slowest rung, and only genuine transport faults use the ladder; worst case is single-digit connection attempts per hour in every class. Degraded states stay visible in the chip even while cached vessels are still drawn. Locked invariants — do not "fix" these: teardown isws.terminate(), neverclose()(the built-in WebSocket has no hard-abort and itsclose()never completes against a black-holed peer, leaking the single per-key connection); socket generations are monotonic for the module lifetime and never reused across a dispose, and every socket-map mutation is identity-checked (otherwise a pre-disposal close event orphans a post-disposal socket and two connections race for the one slot); durations use a monotonic clock, wall time only for display. Policy is a pure state machine (src/data/aisWatchdog.js) returning actions; the socket lifecycle issrc/data/aisStreamAdapter.js, tested directly with mock sockets; the transport assumption is pinned insrc/data/aisWatchdogTransport.test.mjs.- Honest live AIS health: the vessel layer treats socket connection, first message receipt, raw payload rows, and accepted vessel positions as separate stages. Each enabled session owns one 30-second first-connect grace: an open or connecting socket with no accepted position reads
LOADING, and polls do not restart that deadline. The first accepted position ends the grace and establishes freshness; expiry, missing credentials, rejected transport, or another definitive failure readsUNAVAILABLE. Disable/re-enable starts a new isolated session. A socket with no received message or no usable positions does not advancelastUpdateor replace warm accepted vessels; warm selection and trail state remain visible as stale/degraded. Late responses from disabled, destroyed, or replaced layer requests cannot mutate or finalize the current lifecycle. Layer stats expose transport status, message time, and raw/accepted row counts for diagnosis.- Vessel/fire camera transfer: clicking an actionable AIS vessel sprite or painted card selects that MMSI and requests one close oblique camera transfer; re-clicking the selected vessel refocuses it. FIRMS detection sprites and actionable detection cards do the same using a refetch-stable identity that includes position, acquisition time, and source satellite. Aggregate fire cells remain non-actionable. Painted actionable cards are also mirrored into a named, focusable assistive-control list that exposes selected state and announces focus only after the backing record accepts activation. Global FIRMS cards reject far-side cells before filling the bounded overlay cohort; the shared overlay still owns final horizon culling. The UI validates world-focus requests before releasing tracking, refuses Cockpit-owned moves before any camera mutation, and releases follow owners before accepted flights. Sibling-owned picks win without clearing the vessel/fire selection or issuing a competing camera command. Deferred geocoding stamps intent but retains the current owner until a valid destination resolves; immediately before flight it rechecks shared navigation authority. Newer destinations, voice
move_camera,fly_route, overhead framing, strongest-fire focus, vessel/aircraft/satellite tracking, reset, Cockpit entry, or teardown make older work and its UI completion inert. Teardown removes immediate camera-entry listeners before its first asynchronous restoration step and refuses any new immediate or deferred navigation after disposal begins.- Loading, reset, and Display completion: every registered layer exposes one normalized manager loading contract. Enable and disable feedback remains lifecycle-authoritative, while manager-owned periodic updates publish refreshing, failure, and recovery without replacing a producer's more specific error or availability state. The shared presentation is delayed to avoid flashes, visible outside the rails, and retained in Cockpit. One continuous overlapping load interval retains the strongest terminal outcome (
failed, thencancelled, thencomplete) until every participant settles, so a later success cannot mask an earlier failure. A participating producer's terminal error, unavailable status, or key-required state also outranks generic completion without requiring a separate manager failure event; AIS first-connect expiry therefore ends asLOAD FAILED, notLOAD COMPLETE. Slow disable work is labeled as turning live data off rather than as a completed load. Street Traffic's dedicated sync chip shows genuine work and one bounded completion; steady TomTom coverage, including 0%, does not keep it open. Mapped Installations reports its bounded camera-driven requests to the same shared surface, but full-globezoom-inguidance is not presented as loading. Terminal completion, cancellation, and failure labels are centered in that surface without an empty detail slot. The circularRESET GLOBEaction sits beside the top-center share control in map view; Cockpit hides the complete action group and provides a cockpit-styledRESETbesideEXIT COCKPIT. Both resets share one route with the voice action, release continuous/POI/Cockpit/entity and Space Mission camera ownership, and returns to the 18,000 km globe frame. Reset preserves the selected Contact while invalidating delayed automatic refocus work; the normal ContextFOCUSaction is the explicit route back to that same flight or vessel after the globe view settles. Location navigation uses the same selection-preserving camera handoff once a city, landmark, coordinates, or search destination resolves; failed searches leave the current camera owner untouched, and Context Focus can return to the preserved Contact. Focus also restores the selected aircraft's canonical follow frame after a manual zoom-away. Visual presets retain the order Normal, CRT, NVG, FLIR, Anime, Noir. Configurable preset selection—including same-style reselection—opens the shared Parameters surface directly below Detection and scrolls it into view; share-link restoration does not force that disclosure. Presets remain in the map Display. The shared Parameters surface moves into Cockpit Display for the session and returns on exit, with slider values contained by the panel at its supported widths; the bottom Visual Presets tray owns the MAP SOURCE label, centered status, and five-tile source row. Its compact wing is a keyboard disclosure: Enter/Space opens and focuses Map Source, Escape closes and returns focus, and unavailable sources remain tabbable with their reason exposed. Expanded left-panel headers use the same container-owned background treatment without changing their collapsed launchers; a soft 28% cyan divider identifies expanded titles on both side rails. Cockpit portals HUD, Detection, the single shared Parameters surface, and 3D controls, but not the visual-preset grid. Parameters follow the active Cockpit vision treatment and remain directly below Detection. Changing the top vision style does not close an open Display or Radio utility; explicitly opening Display still collapses Live Signals. Its left-side Data Layers and Contact interactions do not collapse an expanded Display or Radio utility. Presentation-only adaptive collapse is reconsidered after HUD and viewport changes, while explicit collapse remains the only persisted user intent. Display orders 3D immediately above Celestial and Clean UI immediately below it. The top-center action group places Clear Layers to the left of Share, then Tilt Map and North Up, and Reset Globe to the right. Tilt Map swings the camera between a straight-down map and a 35-degree oblique around the point under the centre of the view, keeping that point and the distance to it; North Up rotates around the same point until north is at the top, keeping the pitch. Its needle shows the current bearing. Both decline without moving the camera when nothing is under the centre of the view (a camera facing the sky), and both are explicit camera actions, so either one releases a followed aircraft the way the other camera controls do. They follow Reset Globe out of Clean UI, recording, Scene playback and Cockpit. Clear Layers turns off the currently selected manager-owned data layers, including an active Context choice, while retaining visual, HUD, map, and panel settings. A disabled layer may still release camera work that it owns through its normal teardown. Direct Data Layers entry into Space Missions excludes the new mission ON intent from its pre-entry snapshot. Its OFF control therefore leaves Space Missions off and restores Satellites to their exact pre-entry visibility and parameter state. The title and loading logos use a blue 10 px outer-eye stroke with a translucent slate fill, while the globe-and-cage gaze travels up to 34 SVG units toward the pointer for clearer feedback at the compact title size.- Context, Cockpit, and Radio interaction contract: explicit Contacts, Space Missions, and successful Cockpit actions reveal the Context panel, while restoration and replay preserve its prior collapsed state. Contacts uses the dedicated right-side chooser; the underlying Global Context coordinator is registered for lifecycle and restoration but is not duplicated in Data Layers. Inside Cockpit, the focused summary card is titled Contact in both visible copy and its accessible control labels. The top-center Cockpit vision cycle is the fixed, duplicate-free sequence Normal, CRT, NVG, FLIR, Anime, and Noir. Entry selects the map preset already in use, so a FLIR map opens on the existing FLIR item while Normal remains available exactly once. NONE is not offered. Cockpit choices temporarily override the map preset, while Exit Cockpit and Reset both restore the captured map style and its exact shader intensities. Selecting a Cockpit vision treatment with configurable parameters opens Cockpit Display and reveals those parameters through the existing right-side accordion; an inherited parameterless Normal preset does not force it open. Expanded Cockpit Display uses a container-integrated header and soft 28% cyan divider matching the expanded left-side panel treatment; its collapsed launcher keeps the standalone glass surface and muted divider. Cockpit Display and Radio use right-rail chevrons: left to expand and right to collapse. Cockpit side surfaces use one expanded body per side: Display or Radio collapses Live Signals and vice versa. When both utilities close, Live Signals reopens unless the user explicitly collapsed it. Expansion notifications fire only on a real collapsed-to-expanded transition, preventing repeated close synchronization from re-entering the disclosure coordinator. Data Layers collapses Contact, while expanding Contact returns that panel to its visible launchers. Live viewport-height changes remeasure both utility lanes and keep their collapsed launchers inside the obstacle-free corridor above Contact and Live Signals. An expanded Data Layers panel is solved against the viewport rather than the Cockpit cards: the CONTACT card and the peripheral Intel HUD corners stop shortening its corridor while it is open, so it unfurls downward from its collapsed launcher position and renders over them (
#left-panel-stackis z-index 147, above the Intel HUD readouts at 146 and the Cockpit HUD at 145), scrolling internally when the layer list is longer than the corridor. Cesium's credit line is never passable and still bounds the corridor. Layer toggles stay live from there, and collapsing returns the plain launcher. The map-only Clear, Share, Tilt Map, North Up, and Reset Globe actions are hidden for the duration of Cockpit, both as a group and as individual controls. It uses theradarsymbol; both tabs are reachable with Tab and Left/Right arrows switch between them. Its action row places the single Cockpit entry before Search Nearby Sites. Cockpit removes the duplicate floating map entry and topline exit; the bottom-centerEXIT COCKPITcontrol (offset downward by a-95pxbottom margin) plusEscape/Cown exit, with entry/exit focus transfer and failure-safe shortcut routing. The exit control sits at the bottom-center compass position. A Cockpit-only control strip sits directly above Live Signals, anchored 12px under the REC readout it shares the right margin with and clamped to keep 8px above the briefing card, never rising pastmax(96px, 12vh); Cockpit owns that anchor and republishes it every layout tick (the left accordion no longer donates its corridor). Its minimal Display popover exposes Intel HUD, Detection, Parameters, and 3D aircraft. During Cockpit, those existing standard Display controls move into the Cockpit popover and retain their normal nested interaction: HUD plus Tactical/Minimal/Cyber layout, Detection plus Density/Allocation/Fade/Outside tuning, and 3D plus Proximity/All mode. The same nodes and state return to the map Display on exit, so Cockpit does not maintain a second control state. Detection is owned by the CONTACTS session, not by Cockpit (validated 2026-08-18): activating Contacts forces detection on at the shared military preset (MILITARY_DETECTION_PRESET— Dense @ 75%, the same frozen object the CRT/NVG/FLIR styles apply), and it then stays on for the whole session — cockpit enter, cockpit exit and third-person tracking are moves WITHIN Contacts and do not touch detection at all. A manual DETECT change during the session holds for the rest of it. Deactivating Contacts restores the pre-Contacts state, except that a map style chosen during the session keeps its own auto-enable preset (that rule is younger than the entry snapshot). The trigger lives on_syncContactsDetection(), called from_syncContextModeButtons()and gated on!_contextModeChangingso it fires at transaction settle, never at click — a failed activation cannot strand detection on. Policy insrc/contactsDetectionPolicy.js. Detection continues to show surrounding aircraft in Cockpit while omitting only the active first-person subject's bracket; handoffs move that suppression to the new subject and exit restores the selected aircraft's map-view bracket. Remaining Cockpit bracket strokes render at 45% of their normal presentation opacity to reduce visor clutter; callouts, density, allocation, fade, and Outside tuning are unchanged and normal bracket opacity returns on exit. AIR presentation follows the same retained 3D mode: Proximity uses 150/185 km and All uses 400/450 km. With 3D off, in-range contacts remain rotating 2D silhouettes; with 3D on, ready admitted models take over under the Cockpit cap of 60. Loading/capped contacts remain 2D, out-of-range contacts use rotation-free dots, and exit restores map treatment. Its Radio popover exposes compact power, transport, station, and volume controls. Cockpit Previous/Next preserves selection, autoplay, and broadcaster fallback audio without starting the map-view station flights that compete with the first-person camera. These are a Cockpit-only accordion: each static header uses the left accordion's label and divider with a dedicated directional-chevron disclosure button and no full-row hover treatment. Only one utility body expands at a time. On desktop, its collapsed sibling remains visible whenever both controls fit above Live Signals; a constrained corridor gives the expanded utility the full height and temporarily hides that sibling, restoring it as soon as room returns. Collapsed Display matches the collapsed Data Layers launcher width. Expanded Display uses the standard map Display panel's 272 px width, glass shell, header surface, and internal spacing. Radio retains its independent compact and expanded widths. Display no longer follows the Data Layers corridor: that corridor is solved against left-lane obstacles and has nothing to say about the right margin, which dropped the strip into Live Signals below roughly 830 px of browser height. Cockpit owns the strip's anchor and republishes it on every layout tick — including the settling pass after HUD transitions and asynchronous map-provider swaps — hanging it 12 px under the REC readout, clamping it up to keep 8 px above Live Signals, and never letting it rise pastmax(96 px, 12vh). The utility height is measured from that resolved top and floors on a launcher height rather than a fixed minimum, so expansion is bounded to the real corridor below the Cockpit topline and above Live Signals, including when a tall panel takes over the corridor. A hidden sibling is also removed from the accessibility tree, and focus transfers to the expanded utility if a layout transition hides the focused launcher. The shared map Display, Radio detail, ordinary Global Context, and Authoring surfaces stay hidden in Cockpit. Data Layers remains available. Its Cockpit-only stack paints above the curved speed and altitude rulers, matching the existing Context, Signals, Display, and Radio surface ordering without changing map-view stacking. Narrow mobile Cockpit viewports suppress the legacy layer stack, peripheral HUD, and secondary Context/briefing panes so the flight instruments and primary controls remain unobstructed. Outside Cockpit, Radio starts collapsed while off and lives only in its section inside Context; the detailed panel's normal accordion control owns its disclosure and its Enable/Disable action owns power. Radio volume in the full and Cockpit surfaces, plus Space Mission replay speed, use Display / Sharpen's muted 3px rail, circular cyan thumb, glow, and mono value treatment. Their larger transparent hit areas, keyboard focus indication, ranges, disabled states, and mission speed scale remain control-specific. Outside Cockpit, panel collapse state is independent. Multiple expanded panels in either desktop lane share the measured viewport-safe corridor. If a later panel would receive less than half its intrinsic height, the layout presents it as a collapsed, accessible launcher without overwriting the user's saved preference; the panel most recently opened by the user keeps the lane, so an older expanded sibling yields when necessary. In a constrained Tactical lane, later competing panels collapse to their launchers even when they would narrowly exceed that threshold. While the left lane remains constrained, every collapsed sibling launcher is hidden and the primary panel uses the complete safe corridor; the launchers return when that panel closes or the stack fits again. An expanded Tactical Display claims the right lane and hides every collapsed CCTV and Context sibling, including a layout-collapsed Context; those launchers return when Display releases the lane. Display itself remains the single scroll surface when a visual preset adds Parameters: those rows do not form a nested scroll surface. The bottom Visual Presets MAP SOURCE row keeps the3Dstatus and four source tiles inside its padded tray; the row wraps from one row to two at 620 px, and live viewport changes keep the complete tray inside the screen. Adaptive remeasurement preserves the user's scroll position across Tactical, Minimal, and HUD Off layouts. During an active Scene run, Clear Selected Layers, Tilt Map, North Up and Reset Globe remain hidden until playback stops or completes because the Scene transport owns layer and camera sequencing for that interval. The Cockpit Contact summary exposes Previous and Next contact navigation plus its collapse control; it does not offer a Focus action because the first-person Cockpit camera remains owned by the tracked aircraft. A voice Cockpit request that retargets to a filtered civilian or military contact carries voice selection authority through navigation, so the aircraft entered is also the durable target used by Copy Link and reload. Missing context values render as—with an accessible “Unavailable” name. Because the panel hosts its own Previous/Next controls, the ONLY condition that hides it is the absence of a context snapshot. Contact identity never gates visibility — hiding on a non-aircraft subject stranded the operator the moment Next landed on a vessel or an installation.- Contact readout: foreign subjects and CONTACT LOST. When the selected contact is not the tracked aircraft (a vessel, an installation, another aircraft), the panel stays up and keeps its label, cohort counts, nearest contact, distance and evaluated time live. Only the nose-relative direction arrow and BRG readout are dashed: those two are measured in the tracked aircraft's own frame while the rest of that row is measured from the selected contact, and rendering both live presents one mixed-frame reading as a single measurement. A contact that leaves its feed holds the panel in a
CONTACT LOSTstate (data-state="lost", the same panel-level cue mechanism asuncertain, in the amber the app already spends on unknown/stale inputs): the last rendered values stay on screen rather than being recomputed against a position that stopped updating, and Previous/Next stay operable so the operator can step off. It fires on two paths, and both retain the snapshot: an eviction-origin selection clear (reason: 'evicted'— the aged-out branches inflights.js/militaryFlights.js, AIS pin exhaustion, and a viewport refresh that drops a selected record), and a refresh whose presence check comes back absent. A DELIBERATE clear (click-away, Escape, voice stop, layer disable) still clears the subject and takes the panel down; an untagged clear defaults to deliberate.- Presence contract (
hasContact).flights,militaryFlightsandaisLiveVesselseach exposehasContact(id):true/falsein O(1) from the layer's own keyed map, ornullwhen the layer holds no data and therefore cannot answer (disabled, or not yet loaded). Presence consumers MUST use it and must never infer absence fromgetAllPositions, which stops at its cap — the live flights layer routinely carries ~11k contacts against a 1,000-row cap, so "not in the returned rows" is not "gone".nullleaves the previous verdict untouched, so a silent layer can never fabricate a CONTACT LOST cue.- Compact data-attribution panel: the complete Cesium credit inventory remains available without taking over the viewport. Its expanded desktop panel is capped at 70dvh/36rem, the wrapped 12px credit list scrolls inside it, and narrow screens retain Cesium's surface with an internal scroller. The title, close control, links, and persistent Google/Cesium line remain unchanged and visible.
- Distant-aircraft recession: both civilian and military billboard fleets retain their locked class/ground scale and
NearFarScalar, then multiply a limb-relative taper in the existing ~12 Hz tick. The taper is 1 below 0.5× geometric limb distance and smoothsteps to 0.45 scale plus a 0.35 alpha-haze factor at the limb. That treatment eases continuously back to identity from 3,500–4,500 km camera height, avoiding a globe-view threshold pop. These values, the start ratio, blend band, composed floor, and write epsilon remain runtime tunings. No aircraft is count-culled or made fully transparent. Focus emphasis and limb haze multiply at one deadband-gated write site, and their product is clamped to 0.20 before freshness alpha is applied. Ambient fleet models receive that same composed alpha; class/ground/cockpit repaints preserve the current limb scale instead of dropping it for a tick.- Focus-aware contact de-emphasis: civilian/military aircraft and satellites publish the selected target's padded screen bounds and camera distance from the same per-frame position cache already consumed by their tracked entity. Ambient flight sprites, AIS chevrons, CCTV icons, and satellite points then ease their own alpha where they compete with that target; no draw-order assumption participates. The always-visible rule is narrowly amended rather than removed: emphasis never falls below 0.25, contacts never blink or disappear, entry/exit use 6 px hysteresis, and writes use a 0.005 alpha deadband. Defaults are 18 px padding, 300 ms attack, 600 ms release, an 8%-of-target-distance range hysteresis band, and
nearerBehavior: 'allow';dimandpartialremain runtime/evidence tunings. Ambient overlap includes each sprite's own rendered extent. The gated AIS, CCTV, and satellite passes retain an active-emphasis count so a settled dim contact always completes its release after tracking ends.- Terrain-height bounds: because every distinct 5-decimal point is its own cache key, its own line in
.gev-cache/terrain-heights.json, and its own upstream call, four limits bound what a caller can grow. Coordinates outside WGS-84 range are rejected with400at parse (finite is not the same as on Earth); the point cache holds at most 50,000 entries, evicting oldest-first — 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 into memory (the ceilingrocketLaunchesProxyalready applies); and the route carries an always-on limiter of 90 requests/minute/client, 270 global. That cap is an order of magnitude above measured demand — a fullnpm run test:trackrun drove 10 requests, peaking at 8/min.- Terrain-height resilience:
/api/terrain/heightscaches canonical 5-decimal points individually, reconstructs reordered/overlapping batches in exact request order, and refreshes only missing or stale points. Upstream calls and browser requests are both chunked at 64 points, sized to fit their 30s deadlines across Re:Earth's observed 87-186 ms/point range; a chunk that still fails contributes nulls for its own positions instead of discarding the chunks that resolved. Network, 429, and 5xx failures receive bounded jittered retries withRetry-After; stale real heights remain usable per point, while an uncached absent height still returns 502 rather than becoming a fabricated ground value. A position the upstream answers with a null ellipsoid is reported as an absent height rather than a refresh failure, and is left uncached so a later poll re-asks it. ARetry-Afterlonger than the 10 s retry budget ends the retries at once. After a 429 that outlasts its retries, the proxy pauses upstream calls for theRetry-After(60 seconds when none, at most 10 minutes). A request whose points are all cached, fresh or stale, is still served; a request with any uncached point gets 502 and the page uses its geoid fallback for that chunk. Each request that was waiting on the upstream when the 429 came back logs one warning; requests refused during the pause do not log. Client geoid fallbacks wait 60 seconds before retrying and self-heal to Re:Earth on the first later successful fetch. Smaller chunks reduce timeout risk; complete camera batches still wait for their sequential requests, and unresolved placement continues to use the existing prior until real heights arrive.- Overpass cache admission:
/api/overpassrefuses cross-site and forwarded POSTs through the shared local-request gate, then limits requests to 300/min per socket peer and 900/min globally before reading or validating bodies. Tag-filter brackets are scanned once; nested or unmatched brackets return 400. It then checks fresh memory, identical in-flight work, and fresh disk entries before invoking its separate local 90/min upstream limiter. Cache and single-flight responses therefore do not spend upstream quota; upstream-bound misses retain the existing limiter, configured-upstream, stale, and sanitization behavior. An unconfigured transport serves dated stale data or a non-retryable capability error.- CCTV world-click focus: clicking an in-world CCTV icon or ambient card activates it and routes the camera flight through the panel FOCUS policy. Aircraft/satellite tracking releases outside cockpit; cockpit retains the view, keeps the CCTV activation, and surfaces the existing refusal toast. Auto-hop and programmatic camera activation remain activation-only. CCTV world clicks must stay within 6 px and 400 ms; drag-like or long gestures are inert, and re-clicking the already active camera emits no focus request. A clean empty-space click clears the active CCTV camera in place without moving the view or disabling the layer. Sibling-layer picks and ADJUST-mode interactions never trigger that clear. The resulting null selection remains stable across rendering and updates; configured auto-hop is held until a later explicit activation or AUTO HOP toggle-on.
- Tracked-flight close-range feel: the existing 150 m camera floor is unchanged, while the selected-only civilian and military model cap is now 200 px so minimum range reads as close. Pointer travel over 6 px suppresses selection and empty-space untracking; duration over 400 ms suppresses only untracking, so a stationary slow press on a plane still selects it. Escape still releases tracking in place. The 200 px feel needs close-range field verification; fleet model sizing remains unchanged.
- Deterministic sprite stacking: contact collections reassert the stable bottom-to-top order CCTV, FIRMS, bikeshare, AIS, military, then civilian flights after every relevant layer init/enable and immediately after FIRMS lazily registers its detection sprites. Always-visible contact depth settings are unchanged. Cesium OIT weighted blending may soften strict alpha layering on some GPUs, so the ordering remains a real-browser check.
- Deterministic card stacking: CCTV, FIRMS, vessel, and tracked-target cards use the shared world-overlay canvas. Detection paints through the same host/frame contract onto one host-owned blend surface beneath that canvas — parented into
#cesiumContainerso itsscreenblend still reaches the WebGL scene. The exact detection, ambient-label, ambient-track, ambient-card, thumbnail, selected, and tracked lane sequence is binding; the detection callback runs first and z-index preserves its shipped z5 position below the z6 cards.- Cross-layer vessel ownership: clicking a sibling-layer contact leaves the active vessel card, HUD, context, and trail unchanged while the sibling handles the pick, preventing two camera commands from one click. Starting entity tracking still clears vessel inspection; AIS itself never sets
viewer.trackedEntity. Own unkeyed or evicted vessel-record picks andgev-trail:*remain no-ops. CCTV choices made from the panel dropdown do not currently emit a cross-layer event and therefore do not clear vessel inspection.- World-overlay host and Phase 2–6 source migrations:
src/overlays/worldOverlay.jsowns one shared DPR-aware text/card canvas, one detection blend surface, and one world-overlay post-render scheduler. Both surfaces share the same sizing, clear, projection, and teardown paths. The host includes bounded per-source/per-collision-domain arbitration, horizon/viewport culling, shared keyhole fading, cached UI exclusions, cockpit source gating, pooled hit rectangles, and development diagnostics. UI exclusion is a per-rectangle PLACEMENT PREFERENCE over currently visible chrome — it clips no canvas and vetoes no entry. Rectangles are never coalesced into bounding unions (that requirement existed only for even-odd canvas holes, and the host punches none). Exclusion strength is decided per element by its EFFECTIVE stacking level (the outermost positioned ancestor carrying a z-index, i.e. the stacking context that competes with#world-overlay-root): chrome ABOVE the host — map panels z90-1000, the dock, the cockpit windows inside#cockpit-hudz145 — is a soft preference, so an entry with no collision-free placement keeps its full placement set and simply renders beneath that chrome. Chrome at or BELOW the host keeps an absolute veto:#intel-hudis z2, under both host surfaces (detection z5, cards z6), so a kept placement there would paint over HUD text. Placement runs two passes — prefer variants clear of all chrome, else variants clear of the below-host chrome, else drop the entry. Cockpit line art (rims, arcs, rails, toplines, readouts) is not in the inventory at all: the AR-HUD model puts world-space content beneath the cockpit's screen-space HUD by z-order. Its empty-host path performs no post-setup layout reads or canvas work under resize/mutation noise; removed entry records are pruned, and text measurement uses a host-lifetime, 1,024-entry LRU. Steady-state rendering reuses arbiter output, placement, paint-item, and paint-rectangle storage; track display text is rebuilt only when its title or detail changes. DOM mutations only flag exclusions dirty, with selector/layout scans deferred until there is overlay paint work, and host teardown severs pooled record/entry references before releasing every pool. A custom paint-lane contract gives source-owned batched painters the same DPR-sized context, per-frame view-projection matrix, ellipsoid occluder, keyhole geometry, and cached UI rectangles without transferring their selection policy into the host. Datacenters and Dams now publish card entries into that host on their existing 450 ms screen-grid cadence. Each source retains two deterministic contenders per grid cell and publishes at most 160 entries. The legacy 700/900 winner ceilings are no longer the effective shipped caps; activeambient-cardsource budgets sum to a bounded 1,150-card shared lane: two 96-card infrastructure budgets, FIRMS' shipped 18-card cohort, AIS's existing configured 900-row absolute ceiling, and CCTV's shipped 40-card ambient maximum. Runtime AIS demand still derives from its shipped 118 px grid: 112 candidates at the 1600×900 allocation viewport and 170 at full HD; its existing 150 px greedy separation usually admits fewer. The host owns final cross-source declutter without letting source count grow beyond the pre-migration source bounds. Cards use the infrastructure name plus available operator/capacity for datacenters or river for dams. Their native Cesium points, stems, polygons, selection/picking, terrain sampling, and enable/disable lifecycle remain inlocalGeojson.js; it creates no native label graphics. The bundled public-release snapshots omit contact-oriented fields and note values containing email or phone identifiers. Runtime cards and entity context do not depend on those fields; geometry, identity, name, operator/capacity/river metadata, and ODbL attribution remain intact. Stem position and polyline properties are constant between initialization, cameramoveEnd, and successful near-surface ground samples rather than per-frame callbacks or every 450 ms visibility pass. Unchanged or sub-0.5 m tips do not callsetValue, and each record alternates between two preallocated two-position stem arrays so real tip changes notify Cesium without steady-state allocation. Shared placement records retain the raw anchor plus one signed integer leader offset; painters apply that offset without materializing four pairs of computed doubles per entry. The arbiter's spread distances and compact placement-availability masks live in pooled numeric buffers. Because solve occupancy only grows, few-placement identities whose complete set is blocked are dismissed once instead of being returned and re-spread; the authoritative collision lookup remains the final semantic check. Protected entries that cannot separate completely choose the placement with the least total protected-rectangle overlap rather than stacking on the first option. Cards use the host's shared keyhole fade and global detection fade/opacity controls with no source-local edge constants; cached keyhole geometry is invalidated by live tuning as well as canvas size, so Fade changes reach the next frame. Outside opacity defaults to the shipped floor (5 % when this landed; 1 % since the 2026-08-24 final lock, 3 % on 08-23). TheKEYHOLE_OUTSIDE_OPACITY_DEFAULTchange from 0 to 0.05 also affects detection: callouts and brackets previously hard-culled outside the keyhole now paint at the OUTSIDE default (1% since the 2026-08-24 final lock; aircraft brackets hold the 0.35 readable floor) and consume ambient budget viewport-wide under the same fade-don't-cull principle; this remains pending visual review. Cards also reproduce the formerscaleByDistancecurve (1.0× at 250 km to 0.62× at 9,000 km). Mapped Military Installations create no empty native labels; selected names remain in the tracked readout. Submarine-cable labels moved into the shared host on 2026-08-18 (Option 2, superseding the Phase-5 Option-1 native exception): the same nearest-160 bounded cohort now publishesambient-labelentries on a dirty sweep with exactly two dirty conditions — cameramoveEnd/ layer enable / load completion, plus a motion fallback that samples the camera at most once per 2 s and only re-arms past 250 m of travel since the last swept position, so tracked and orbit cameras (which never emitmoveEnd) cannot starve the sweep while a parked camera still costs zero. The former 500 ms timer path was removed in the same-day perf round — timer-driven stem re-sizes rebuilt the 2,629-instance batched stem primitive mid-motion. The 2,629 reference stems are staticized constants (no per-frameCallbackProperty), and an unchanged cohort is never republished so a parked camera stays governor-idle at the source level. The 2026-08-18 host fix extends that to the HOST level: chrome mutation observation is scoped (body childList filtered to inventory-chrome add/remove; per-occluder attribute observation element-only, no subtree) and the right-rail allocator writes--right-panel-allocated-heightonly on real change, so parked idle with ANY live overlay source measures 0 postRender fires / 5 s — empty-scene control parity (pre-fix ~56-61; dams cross-checked at 0). Genuine chrome changes (occluder add/remove, own-attribute flips, resizes) still invalidate;qa-cables-overlay.mjsgates idle at ≤6 / 5 s.- Phase 6 detection consolidation:
src/data/detection.jsretains its ownLabelArbiterinstance and existing 125 ms solve cadence, density/altitude budgets, layer quotas, Elastic/Weighted allocation, manual scalar matrix projection, tier palette, batched bracket paths, callout painter, acquire/keyhole fades, sparse focus ring, banner, scanlines, suspension, and diagnostic object. It no longer creates or sizes a canvas, clears pixels, builds a camera matrix or UI inventory, observes layout, or attaches a render listener. The host creates and lifecycle-manages#world-overlay-detection-surface, DPR-sizes it through the same frame path as#world-overlay-canvas, and invokes detection against its plain source-over context. Detection writes the exact shipped thememixBlendModeandfilter + drop-shadow(...)strings to that provided element, preserving scene-level CRT/NVG/FLIR glow and once-per-layer filtering. The surface is host-owned but parented into#cesiumContaineratz-index:5, not into#world-overlay-root: the root is a stacking context (z-index:6), i.e. an isolated blending group, and a surface inside it has itsmix-blend-mode: screensilently discarded by the browser instead of compositing against the WebGL scene. z-index (5 under the root's 6) keeps it beneath ambient-through-tracked host paint. The >22 ms odd-frame relief valve is restored: the host does not clear the detection surface on a held frame, while unrelated shared lanes repaint, andthrottleSkipCountremains a live diagnostic. Detectiondata-*fields now live on#world-overlay-canvas, while the StyleManager diagnostic API remains unchanged. Disable and suspension deactivate only the detection lane, leaving unrelated host entries intact; teardown unregisters the lane before the host is destroyed. The production-shaped allocation gate covers 5,000 visible Dense observations at 2,500 km under the unchanged 154 B/observation/frame ceiling. The host owns one world-overlaypostRenderlistener; the repository has fivepostRenderlisteners total (host, celestial ring, annotations SVG, missions frame tick, and traffic).- World-label accounting: All intended label/card migrations use the shared host. The former native
LabelGraphicsexception for cable reference labels was retired on 2026-08-18; cable text may render over tile geometry, so zero native world-label creation sites remain. The mission replay DOM vehicle, awareness compass, annotation SVG callouts, and host-owned detection isolation target are explicit exceptions; the awareness ring and cockpit contact pips are non-text out-of-scope surfaces. Annotation callout text, geometry, leaders, and behavior retain their tested presentation; a future annotation style system is outside this phase.src/overlays/worldOverlayTokens.jsis the single home for shared world-overlay, CCTV-thumbnail, and detection-theme presentation constants. Former source renderers own no canvas, DPR, or world-overlay listener path; the duplicate detection rounded-rectangle helper and dead tactical compatibility helpers are gone. No temporary dual-renderer flag existed to remove.activeCameraCardEnabledremains a product presentation control, and_detectionUserOverriddenremains the documented style-switch persistence state. Browser/GPU performance comparison remains operator-side; the final accounting records only comparable population reductions and deterministic Node allocation/solve measurements. Those allocation gates are calibrated on Node.js 24.14.x;package.jsonpermits supported product runtimes on Node 24 or 26, while the allocation runner separately enforces Node 24 for these two calibrated probes. Each probe compiles synchronously and discards explicit-GC transition chunks before applying the unchanged byte ceilings. The unit runner executes ordinary test files with Node's default parallelism, then runs only the two explicit-GC allocation microbenchmark files sequentially and one at a time with--expose-gc, so unrelated tests cannot perturb their calibrated budgets and each isolated test process has the same GC contract as its measurement worker. A worker spawn, exit, output, or availability failure is a failing gate, not a passing skip.- Phase 5 earthquake labels: Earthquake disc ellipses and pickable entities remain Cesium-native, but their magnitude text is now an
ambient-labelsource in the shared world-overlay host. The source formatsM#.#text and depth-band accent colors, publishes only the 96 largest current events with stable id tie-breaking, and declares a 48-winner ambient-label budget. Host keyhole fading, horizon culling, UI exclusion, and final collision apply. Disable/destroy clear and hide the source; real earthquake entities carry no native label graphic.- Phase 5 bikeshare selection: The selected station keeps its native cyan point highlight, while its station name, availability counts, capacity, and operational warnings now publish as one protected selected-lane host card. The card reads the point's authoritative Cartesian directly, declares zero ambient quota, and therefore cannot be evicted by ambient budgets. Shared keyhole fading, horizon culling, UI exclusion, and selected-card paint order apply. Clear, disable, and destroy remove the host entry; the selected Cesium entity carries no label graphic.
- Phase 5 ISS and tracked-satellite labels: The ISS path and large red point remain native, while persistent
ISStext is a one-entry moving ambient-label host source. Its getter reads the already-propagated point cache, preserving the 1 Hz fleet epoch and eliminating the former second ISS propagation. Satellite tracking continues to publish exactly one protected tracked-lane card throughgevLabelModeland_trackedDisplayCached; the tracked entity is point-only. Tracking ISS suppresses the ambient entry and untracking restores it, preventing duplicate ISS text. Disable, orbit-text preference changes, catalog rebuild, and destroy clear/hide the source.- Phase 5 active CCTV projection label: The active camera's monitor plane remains native Cesium geometry, while its camera-name label is one protected selected-lane host entry. The entry closes over the same cached
positions.labelCartesian updated whenever the plane geometry moves, so label and plane retain a single placement authority. Only the active, projection-visible camera publishes; hide, disable, runtime destruction, and state clear remove the source. Monitor-plane entities carry no native label graphic.- Phase 5 tracked civil aircraft label: A tracked flight's Cesium entity is a billboard-only camera target with no native label. The flight source retains callsign/registration fallback, flight-level/altitude, speed, stale state, airline/type, and plausible-route formatting in
gevLabelModel; the one protected tracked-lane host card renders that complete model. Its position getter reads only_trackedDisplayCached, the same authoritative Cartesian already consumed by the tracked visual and camera, and never advances dead reckoning from the host frame.- Scheduled route plausibility: Aircraft route labels and selected-route context reject the distant backwards, forwards and antipodal extensions of an airport pair's great circle. The existing 130 km endpoint admission and 200 km cross-track tolerance remain; a projected fix outside the finite origin-to-destination minor arc also has a generous 200 km extension at either end. Missing coordinates and coincident or antipodal endpoint pairs retain their loose, unjudgeable behavior; vertical-trend checks are unchanged.
- Aircraft label convention (both flight layers, 2026-08-18): every civil and military label surface resolves callsign → registration → icao24 (
_contactLabel()inflights.js; the same chain inline inmilitaryFlights.js) — tracked readout, detection card,getNearby,getDetectableObjects,getAllPositions().label,getTrackedSubject, the analyst record, the Context subject/nearest list, the Cockpit signal list, and thetrack_entityvoice narration. Registration is aircraft IDENTITY, not route, so unlike origin/destination it is not routePlausible-gated. Identity staysicao24on every keyed surface (getNearby().icao24, the detectionsourceIddeclutter hashes, and theidthattrackByIdand the Context cohorts resolve) — only the displayed string follows the chain. Because adsbdb enrichment can answer after selection, the Context subject re-resolves its label each refresh (resolveSubjectLabel()) instead of freezing the selection-time snapshot.- Phase 5 tracked military aircraft label: The military tracking entity is likewise billboard-only and label-free. Its source-owned model retains callsign/registration fallback, stale cue, aircraft type, registration, operator, altitude, and speed, rendered as the sole protected tracked-lane card with the amber military accent. Its getter reads only the military
_trackedDisplayCachedpublisher, keeping the host, visual, and camera on one dead-reckoned frame sample.- Phase 5b Space Mission labels: Launch markers publish through a bounded 48-candidate / 24-winner ambient-label source. Selecting a mission clears that overview and publishes its launch-site, stage re-entry, payload position, and orbit annotations as protected selected-lane entries; every source-formatted line and accent remains intact. Static entries reuse the mission geometry Cartesians, the payload getter reads its per-frame live cache, and catalog-backed orbit text reads the cache updated with the ring matrix. Refresh, deselect, disable, and destroy replace or clear the real sources. Mission Cesium entities carry no label graphics, and the shared host replaces the former quadratic label overlap pass.
- Phase 5 cable depth-testing decision (2026-08-02, REVISED 2026-08-18 → Option 2): the 2026-08-02 ruling kept submarine-cable reference labels native (
disableDepthTestDistance: 0) as the sole approved world-label exception so photorealistic tiles could occlude label text. On 2026-08-18 that exception was retired after performance measurement: the native path evaluated 5,258CallbackPropertychannels per frame across 2,629 reference entities and re-batched a 160-labelLabelCollectionper sweep, costing the layer ~9.5 ms/frame during camera motion (≈42 → ≈59 fps measured headless at a mid-Atlantic orbit). Cable text is now a bounded nearest-160ambient-labelhost cohort (telegeographySubmarineCables.jspublishes on the dirty sweep —moveEnd/enable plus the 2 s/250 m motion fallback for tracked and orbit cameras — skipping identical cohorts); stems/points stay Cesium-native, depth-tested, and pickable, so only TEXT lost tile occlusion — the same trade every other host label already shipped: labels may render on top of tiles. The dedicatedsubmarine-cablesallocation row gates the new source at 17,015 B/frame median (106.3 B/candidate, Node 24) under a 19,000 budget, and the all-live aggregate row was recalibrated with the cable cohort folded in (164,711 B/frame median, 190.6 B/candidate, 182,000 budget); the intermediate phase rows keep their historical pre-cable composition.load()carries a load-generation ownership token (the militaryAwareness activationId pattern): a stale aborted load bails after every await and never clears a successor's lifecycle, so rapid toggle/destroy sequences cannot double-add data sources. The 639-candidate Phase-5b row (353 painted, 133,769 B/frame, 209.3 B/candidate) remains as an intermediate historical gate; as of the 2026-08-18 recalibration the all-live aggregate — every shared-host source including Radio and the migrated cable cohort — is 864 candidates / 398 painted at 164,711 B/frame (190.6 B/candidate) under a 182,000 budget. The image-inclusive ceiling remains attributable to CCTV, while the isolated mission row measures 108.2 B/candidate/frame under the shared 154 ceiling. FIRMS severity/source formatting, its pre-existing 150 px greedy selector, selected-fire semantics, and LOD distance limits stay infirmsHeatmap.js;firmsLabels.jsis formatting-only. FIRMS entries use the host's tactical card painter, vertical above/below placement, severity top rule, shared UI exclusion/clip, horizon culling, distance-alpha channel, and always-onedgeFade: 'keyhole'policy. Selected fires are protected in the selected lane and bypass both the 18-card ambient cohort and distance fade while excluding ambient cards from their footprint. Disable and destroy clear the source rather than leaving a stale host entry. This is not a pixel-for-pixel port: global collision/UI avoidance can choose fewer cards, host horizon culling is explicit, and ordinary map mode now retains the shared Outside floor instead of bypassing edge fade through the former cockpit/celestial gate.- Vessel card ownership:
aisLiveVessels.jsretains the 800 ms visibility pass, 118 px one-winner grid, priority ranking, 150 px greedy separation, type/detail formatting, and the AIS positions produced by its unchanged sea datum path. It publishes ambient tactical cards and one protected selected card into the host.vesselLabels.jsis formatting/policy-only: no vessel canvas, projection, paint loop, or post-render listener remains. Ambient vessels shareambient-card; the selected entry paints in the selected lane, bypasses the ambient cohort/distance fade, and its protected rectangle excludes sibling ambient cards. Tracked readout still excludes AIS, avoiding a duplicate card for the same selection. Disable and destroy clear the host source. This is not a pixel-for-pixel port: shared cross-source collision, UI exclusion, and horizon culling can admit fewer cards than the isolated canvas, and the shared always-on keyhole policy now applies the shipped Outside floor in ordinary map mode instead of using the interim active-mask gate.- Tracked-readout ownership:
trackedReadout.jsis now a presentation-model bridge only; it owns no canvas, projection, post-render listener, layout, keyhole fade, or paint path. Civilian flights, military flights, satellites, and mapped installations write explicitgevLabelModelobjects and exposegevDisplayPositiongetters backed by their layer-owned frame/display cache. Selected AIS vessels continue using the vessel source's protected selected card, so they do not create a duplicate tracked readout. The host registers the active readout in the protected tracked lane with a zero ambient quota; protected semantics bypass that quota and reserve the painted footprint against ambient cards. Moving sources never fall back to a freshentity.position.getValue()in post-render. Annotation fade queries the host's actualgetOverlayPaintRect('tracked', trackedId)after layout and unions it with the tracked billboard extent. Untrack, context clear, and UI destroy clear/hide the source. This is not a pixel-for-pixel port: placement is host-rounded, cross-source/UI exclusion and horizon culling now apply, accents are source-stable rather than detection-theme-derived, and the old screen-coordinate deadband was removed in favor of the authoritative layer frame cache. Civilian and military poll reconciliation refreshes this model after fresh kinematics and again when a missed poll enters theSTALEgrace period. Satellite pre-render propagation refreshes its altitude line from the same per-frame SGP4 sample used by the tracked dot and camera.- CCTV thumbnail ownership:
cctv.jsretains the 20/28/40 zoom selection, 40-card shipped maximum, 112 px source declutter, eviction grace, frame fetching/cadence/retry, stable slot cache, last-success persistence, hover pinning, and activation.cctvCards.jsis now source policy plus pure lifecycle helpers only; it owns no canvas, post-render subscription, Cesium projection, layout solve, paint pass, or hit store. The shared host paints its exact 96×54 thumbnail inside the 104×77 shipped chrome, applies the same 1.0→0.45→0.35 altitude scale and 7,500→9,500 m fade, shared keyhole fade, full UI exclusions, and tracked/protected footprint exclusion. Ambient entries paint nothing before their first successful frame; a user-pinned entry retains the documented immediate empty-chrome exception, and later failures never clear the last successful frame. The active camera is excluded from the 40-card ambient quota and has no host card by default: its monitor plane is the active representation. The product optioncctvLayer.setCardPresentationOptions({ activeCameraCardEnabled: true })may publish it through the retained protected path. CCTV leaders use the source cyan, remain vertical at the camera anchor except for the off-card edge clamp, and counter-scale to one CSS pixel through the altitude transform. Card hits come fromhitTestWorldOverlayafter a scene pick miss with no canonical scene-object ID. CCTV billboard ownership is proven by the owning collection/object rather than a bare upstream ID, so an independent sibling with a colliding ID still wins. As a completed, narrowly extended ownership hardening step, property-bearing Entity picks must also be the exact stored CCTV coverage/projection object; copying thecctvCameraIdproperty cannot impersonate a camera. Card hits pass through the 6 px / 400 ms gesture guard before activation and the existing click-to-fly event. CCTV cards are not sprite-focus-dimmed. Disable/destroy clear and hide the host source, stop pacing, detach in-flight image handlers, and empty source caches.- Cockpit left-panel chrome: opening any left-stack panel fades the overlapping left pitch-rail glyphs out. The separately right-anchored pitch rail remains visible, and panel/context-card stacking is unchanged.
- Cockpit context stacking: the context card renders above visor glass, pitch/heading instruments, altitude/speed tapes, and the signal window. Topline vision and exit controls retain the highest in-cockpit layer.
- Space Missions dependency shutdown: disabling a mission dependency or leaving the mode closes mission context and restores the exact pre-entry Satellite enabled state and presentation parameters. Restored parameters never re-show primitives while the Satellite layer is disabled.
- Flights 3D/cockpit hardening: the destination-direction cue is an inline SVG rather than a remotely loaded Material Icons ligature, so a blocked or late font can no longer expand the literal word
navigationacross the cockpit. The groundedairplane.glbbelly offset is calibrated to the current asset's measured Y-up bounds (0.063native units). Contact Previous/Next and Live Signals aircraft handoffs now re-seed the first-person camera anchor at the selected flight immediately; bounded feed-correction smoothing remains scoped to the same aircraft. When NEXT has no flight inside the normal 250 km Context window, it searches both civilian and military feeds at successively doubled radii through 16,000 km and transfers the cockpit to the closest available aircraft. Tracked cameras now share one ENU frame across Cesium and the close-range guard: switches relocate to the new aircraft, zoom stops 150 m short of crossing the target, and the icon/model, label, trail head, and camera are prepared from the same frame. The guard frames once and then leaves Cesium's EntityView as the sole continuous camera writer. The tracked entity remains a pure position/billboard target with no unused aircraft orientation input; 3D→2D handoffs seed the current screen-projected course, course projection uses the camera's right/up basis so it remains valid through the >180° rear half of a tracked orbit, and the host readout consumes that same settled frame cache without a second dead-reckon. Selected 3D models are seeded at the tracked position before scene insertion and update before scene preparation from one frame-cached sample, retain real-world scale at ordinary ranges, cap at 200 px when very close for a continuous 2D→3D handoff, and use a 40 px selected model floor near the long-range 3D→2D cutoff so the glTF silhouette remains comparable to the selected billboard; ambient model sizing is unchanged.- Minimal HUD right rail: constrained focus layout keeps the collapsed CCTV and Context launchers visible and accessible while the expanded Display panel scrolls inside an explicit remaining-height budget. The rail reserves both launcher heights and inter-panel gaps without a self-reversing layout measurement. Tactical HUD instead gives the expanded Display, CCTV, or Context panel exclusive use of the rail and hides its collapsed siblings until the active panel is collapsed.
- Context-flow hardening: the cockpit left accordion stays below the HUD inside its measured safe top/bottom lane. Global Context focus guards exist only for the synchronous selection window, history survives same-layer reselection, and NEXT availability uses the same UNKNOWN-cohort gate as the navigation action. Contacts entry does not wait for unrelated serialized layer teardown; Space Missions waits for every incompatible live/current layer to settle off before replay data starts. A rejected teardown keeps that layer authoritatively enabled, restores any siblings already stopped, and aborts replay entry; the isolation guard remains active until Rocket Launches finishes enabling. Manually enabling another Data Layer is additive in both an active mode and the neutral shell: it does not exit Context, and the added layer joins the pre-entry snapshot when the session is eventually restored. Manually disabling a required mode dependency still exits and restores that union. While Space Missions is active, direct incompatible enables are refused before initialization or polling and explain that the mode must be exited. User entry alone owns the pre-entry snapshot; programmatic dependencies do not replace it, and rapid re-entry snapshots the pending settled restore target rather than partial manager state. Contacts-mode entry chooses the nearest aircraft to the current camera from one uncapped civilian-plus-military pool (military wins an exact distance tie), then falls back to the nearest AIS vessel. An initially empty set gets one retry on the next Awareness refresh tick rather than permanently losing automatic acquisition. Clearing a manually selected subject cancels that pending retry so an intentional deselection cannot acquire another contact. NEXT keeps a cycle-scoped visited set separate from PREVIOUS history. Once every current target is visited, it starts a deterministic new walk with the current subject retained as visited instead of re-admitting all candidates into a nearest-contact ping-pong. Expanded flight searches use the same reset rule. Vessel focus uses a 3 km bounding sphere at entry and during cycling. The user-facing mode is CONTACTS: it cycles the nearest contact of whichever supported type is selected (civilian or military plane, AIS vessel, or mapped installation). Satellites are explicitly outside the Awareness navigation cohorts and retain their independent tracking UX. The cockpit briefing opt-in is labeled
CYCLE OFF/CYCLE ON, with state-specific help that distinguishes page cycling from continuously refreshed live data. The neutral standby summarizes both chooser modes, while theCONTACTSand cycle controls keep their short visible state as the accessible name and expose longer help only as a title.- CCTV focus and teardown: explicit camera choices still activate during cockpit mode, but they retain aircraft tracking, suppress the view flight, and ask the user to exit cockpit. Layer enable is activation-only when a tracked entity or cockpit owns the view, with no flight or cockpit-exit toast. Voice select/next/previous/nearest actions report when tracking or cockpit refuses their requested flight without hiding the successful selection. Camera deactivation and layer disable both clear temporary probe clamps; disable re-arms the active record, re-enable restores nominal geometry without probing, and the next real activation re-runs the obstruction probe. Disable uses a direct hide sweep; obstruction hits retain the field-derived 12 m floor. Geometry-drain progress notifications are coalesced to roughly 300 ms or ten batches, whichever arrives first. Natural completion publishes its final state; disable publishes the terminal state explicitly when it cancels a drain. Coverage polylines are lazy: catalog init creates none, while enable in the default COVERAGE ON mode materializes the active camera and visible neighbor cohort (70 entities at the 14-camera cap). Activation always materializes the selected frustum, including COVERAGE OFF when projection remains on. Empty-space deselection removes the active projection and active-relative coverage emphasis but preserves the layer, coverage mode, ambient cards, catalog, panel settings, and viewer pose. With no active camera the panel exposes no stale dropdown/FOCUS/calibration target; NEXT starts at the first catalog entry and PREVIOUS starts at the last. While aircraft tracking or cockpit mode owns the view, the geometry drain rechecks ownership per batch and drops to two records every 250 ms. Enable focus decisions conservatively combine pre- and post-await ownership.
- Share-link camera restoration re-applies its settled pose and requests a render after the flight completes, ensuring Google Photorealistic 3D Tiles stream at a deep-link destination without requiring manual camera input.
- Reversible Context handoff: enabling Global Context snapshots the exact enabled-layer set and every runtime parameter it changes, then clears unrelated active Data Layers. Mode switches, required-dependency disables, direct disable, and teardown restore that pre-entry state before control leaves Context. Directly switching between Flights and Space Missions remains supported without losing the original snapshot. Contacts waits for the dependency releases it owns before restoring that snapshot, so the first Space Missions selection completes without a stale teardown superseding it.
- Space Mission horizon occlusion: mission dots and hover reticles apply their surface-anchor horizon state before Cesium draws or picks the frame, while shared-host labels use the same hidden-globe horizon contract. Rear-side markers therefore cannot remain from the prior camera frame, including in request-on-demand photoreal views; the conservative limb margin remains. Graphics that begin with Cesium's implicit visible default are enrolled in that pass on their first frame rather than bypassing the cull. Selecting a mission isolates its launch anchor until Show All / Deselect or the panel close control clears the selection.
- Cockpit flight signals: Live Signals shows the current and nearest flight names as larger clickable controls. Selecting a name transfers the active cockpit tracker to that flight; type and distance remain secondary text without repetitive event-category headings.
- Ground-safe cockpit: the first-person anchor and final camera position are clamped to the shared mesh-first rendered-surface floor with 12 m of clearance. In the photoreal stack, grounded entry keeps the existing safe map camera until that rendered mesh cell resolves instead of trusting a delayed/raw aircraft height as a temporary surface. Successful one-shot model ground snaps populate the same shared mesh cache, and repeated grounded source timestamps lift their stored history when the floor warms. Landing and taxi tracks therefore cannot place the camera below terrain or inside photoreal 3D Tiles.
- Display panel width: across desktop HUD layouts, the expanded right-rail Display panel uses a 272 px glass-backed surface; its collapsed tab and narrow-screen layout retain their existing responsive widths.
- Cockpit rolling telemetry: the central speed, heading, and altitude values use per-digit vertical rolls when their displayed values change. Increasing and decreasing values move in opposite directions, and heading accounts for the 359°/000° wrap.
- Cockpit route cue: when reliable destination coordinates are available, the estimated-destination arrow occupies a fixed centered slot above the lower telemetry, follows the apparent ground-plane perspective, and rotates to show relative bearing within a legible ±120° steering range. Flights without destination enrichment omit the cue.
- The skylight feature set and six field-test hardening rounds shipped 2026-07-03; CCTV v2 shipped 2026-07-04. Voice tools are 28. Global Context can be entered or exited directly, and Cockpit voice control supports status, entry from a selected or tracked aircraft (establishing Contacts first), exit, and filtered Previous/Next navigation through the full nearby-contact cohort. Contacts exposes source-honest counts inside its 250 km subject window.
- Height-datum system (2026-07-08): Caltrans + TfL camera packs (~900 cameras) and the height-datum work fix entity heights on the ellipsoidal globe end-to-end (geoid module, keyless Re:Earth terrain for the OSM stack, ground-floor system with rendered-mesh sampling, always-visible sprites/trails, OpenSky credit governor). The 2026-07-08 CHANGELOG entry records the subsystem's architecture, invariants, residuals, and verification.
- Height-datum test surface:
npm test184 unit ·npm run test:track43 tracking invariants · headless QA harnesses underscripts/qa-*.mjsincl.qa-height-datum.mjs(numeric heights) andqa-floor-verify.mjs(any-airport ground-truth oracle).- 2026-08-19 — display-time ground floor (flights layer). A grounded contact's render height is picked once per poll from the floor of its FIX cell, but what renders is the dead-reckoned position, which drifts across cells for the whole segment and for hundreds of metres while a contact coasts on a stale feed. On a graded apron that buried sprites under the mesh (measured −15.5 m at KAUS;
qa-floor-verifyreported FAIL). The fleet pass and the tracked display path now re-floor the DISPLAYED coordinate againstcachedGroundFloor— read-only, grounded contacts only, and never while a 3D model owns the visual (T7 ground-snap one-shot). The poll's floor warm/sample batch additionally collects each grounded contact's display CORRIDOR (the ground it is about to cross — toward its newest fix while interpolating, along its course while coasting), need-ranked and deduped so parked contacts never crowd out moving ones. Budget is charged only for cells with no floor yet — warm cells are still emitted, because the mesh sampler must see a cell again once its DEM prior lands — which gives a service bound that falls out of the policy: every contact is served withinceil(contacts / budget)polls. Sample spacing along a projected arc derives from its length (never a fixed count, whose spacing widens with speed), and the cell walk steps at an eighth of a cell, so any cell the path occupies for ~14 m of ground or more is collected. The corridor integrates the SAME constant-rate turn the dead-reckon does, so a sustained-turn taxi warms its arc rather than a straight tangent. Visual ownership — not model existence — decides when the clamp stands aside: a model owns the visual only while it is actually RENDERING (ready && show, the pair the billboard handoff itself consults), and the tracked path also requires_trackedModelRegimeActive(). So neither a retained-but-hidden model (3D off, zoomed out, cockpit) nor one still loading suppresses flooring while the billboard is what the user sees. The visual/data split is deliberate: visual consumers are floored at the per-frame cache, while_describeFlight— and sofindByQuery,getTrackedInfo,getTrackedSubject— keeps reporting sensor truth (barometricaltitudeM, fix-timerenderAltitudeM), because a query or an altimeter should answer what the aircraft reported, not where its icon was nudged to clear the tiles. Residual, unchanged by this work: the floor is a ~111 m cell, so intra-cell relief (terminals, jet bridges) and contacts moving faster than the 30 s warm batch can still read metres low.qa-floor-verify.mjsnow exits non-zero on FAIL (1 = FAIL, 2 = INCONCLUSIVE); it previously exited 0 on every verdict, which is how the burial stayed invisible. It also honoursQA_BASE_URLand puppeteer's pinned Chrome-for-Testing, like the other harnesses. The viewer-distance gate, the corridor walk and cell hysteresis take the short way across 180° (src/data/approxDistance.jsis the one distance both the flights records and the mesh-floor sampler use), so contacts just across the antimeridian are floored like any other.- 2026-07-16: the FIRMS Active Fires layer is LIVE — the bundled 2026-05-25 snapshot (58 MB) is deleted; a new
/api/firmsproxy (vite.config.js) merges VIIRS NOAA-20/NOAA-21/Suomi-NPP NRT world CSVs (days=2 → trailing-24h clamp, 30 min memory+disk cache, single-flight, serve-stale-on-failure) behind server-sideFIRMS_MAP_KEY(keyless → 503 + in-app KEY REQUIRED chip). Client polls 10 min (src/data/firmsHeatmap.js; adaptersrc/data/firmsAdapt.js, CSV parsersrc/data/firmsCsv.js)./api/firms/statusreports cache age + MAP_KEY transaction usage.- 2026-07-16: Traffic supports optional live TomTom flow through the server-side, budget-governed
/api/tomtomproxy; keyless installs retain the byte-identical white-dot simulation.- 2026-07-22 (CCTV v3 Parts A+B): replay, color-coded viewsheds, save-gated direct-manipulation calibration,
viewshed/adjustvoice actions, shared-floor E/N drag grounding, and bounded snapshot requests are integrated. Citywide static-plane LOD/pacing work remains outside runtime.
2026-07-02 milestone: the skylight aircraft/satellite/enrichment work and pre-ship hardening fixes landed. Runtime changes reflected below: voice tools 17→20 at the 2026-07-02 milestone (
next_iss_pass+ the 19 already on main), type-aware 8-class aircraft sprites + path-derived rate-limited display heading, adsbdb flight enrichment (cached proxy + route-plausibility gate), disk-cached CelesTrak TLE proxy, ISS pass prediction, and per-layer data attribution. Gate at close: unit 98/98, build clean, track 19/19, + five QA harnesses (heading 16/16, sprites 9/9, cctv 5/5, failstate 5/5, attribution 18/18). New modules:src/data/{motionModel,aircraftMeta,aircraftClass,aircraftIcons,issPass,routePlausible,dataCredits}.js. The live runtime now declares 30 voice tools; the 17→20 count above is retained only as milestone history.
Interface localization (October 2026)
Shipped locales: en (source and fallback), de, es, fr, it, ja, pt (Brazilian Portuguese), ru, uk, zh-CN and zh-Hant. The code lives in
src/i18n/. How to mark UI text: docs/I18N.md; translator workflow:
docs/TRANSLATORS.md.
- Keys are the English text.
t('Save keys'),tn(count, '{count} camera', '{count} cameras'), andmsg('Live Flights')for text stored in data and translated later. Markup opts in withdata-i18n(element text) anddata-i18n-title/-aria-label/-placeholder(that attribute's English value). English has no catalog; every other locale issrc/i18n/locales/<code>.json, and a missing entry falls back to English. - Locale choice.
?lang=→ stored choice (gev:locale:v1) →navigator.languages→ English. Anypttag (pt-BR,pt-PT) selectspt. Chinese tags default tozh-CN; Traditional Chinese tags selectzh-Hant. ExplicitHansselectszh-CNregardless of region.?lang=is not stored, and share links drop it, so a recipient keeps their own language. The globe picker in the bottom-right corner (src/ui/localePicker.js) stores the choice, drops?lang=(keeps it only when storage is blocked) and reloads. It asks first when the reload would lose work registered withregisterUnsavedWork: unsaved keys or CCTV calibration, Draw marks, imported map files, the open cockpit, a voice session. - Gate.
npm run i18n(andsrc/i18n/catalog.test.mjsundernpm test) extracts every message and fails on missing, empty, unused, placeholder-mismatched, still-English (plural forms included) or wrongly-shaped plural entries. - Not translated by design: layer ids, share-link and hash parameters, API fields, voice tool schemas, provider names, feed data (callsigns, vessel and station names) and attribution.
- Unmarked text. The same check lists UI text that is not marked at all
(markup without
data-i18n*, literals reaching the page withoutt/tn/msg) and fails on it. Text kept on purpose says why:translate="no",data-i18n-runtime, or// i18n-ignore: <reason>.src/tools/is skipped: it writes for AI clients, not the page.
Canonical Docs Order
Use docs in this order when details conflict:
docs/CURRENT-STATE.md(this file)docs/opensky-auth.md(OpenSky authentication)CHANGELOG.md(release history)
Historical planning documents may not match runtime behavior.
Current Baseline
- Repository metadata and public URLs use the
bilawalsidhu/gods-eye-viewproject identity. Runtime behavior is defined by this document and the current source tree rather than historical branch notes.
Runtime Stack
- Vite + CesiumJS app with Google Photorealistic 3D Tiles
- Scene/HUD/style systems in
src/ui/andsrc/hud.js - Layer management in
src/data/manager.js - Map stack switching in
src/mapStackController.js - Voice control in
src/voice/(OpenAI Realtime over WebRTC) - Voice map whiteboard annotations in
src/annotations/ - 3D aircraft/model tracking surfaces in
src/data/flights.jsandsrc/data/militaryFlights.js - Detection overlay and tracked-target readout in
src/data/detection.js,src/data/detectionDraw.js, andsrc/data/trackedReadout.js - Proxy middleware and API wiring in
vite.config.js
Active Data Layers in Runtime
Qualified Radio playback requests—category, station, country, coordinates, or nearby place—always use station selection. Unqualified “turn on/start the radio” requests use Play; a qualified Play-shaped tool call is normalized to Select so its criteria cannot be silently ignored.
| Layer | Source | File | Proxy | Update Interval |
|---|---|---|---|---|
| Live Flights ✈️ | OpenSky Network; bounded adsb.lol regional fallback | src/data/flights.js |
/api/flights (OAuth + fallback) |
30s |
| Military Flights 🎖️ | adsb.lol /v2/mil | src/data/militaryFlights.js |
/api/military |
15s |
| Live AIS Vessels 🚢 | AISStream websocket | src/data/aisLiveVessels.js |
/api/vessels |
60s (+800ms visibility pass) |
| Mapped Installations ⌖ | OpenFreeMap military areas; optional configured Overpass names and Google Places search | src/data/militaryInstallations.js |
OpenFreeMap tiles, /api/military-installations, /api/google/text-search |
viewport-driven; transient errors back off, missing capability switches to tiles |
| Earthquakes | USGS | src/data/earthquakes.js |
— | 60s |
| Air quality | ECCC MSC GeoMet OGC API (AQHI, Canada only) | src/layers/airQuality/ |
— | 20 min readings, 12 h station catalog |
| Satellites | CelesTrak | src/data/satellites.js |
/api/celestrak |
120s |
| Space Missions (30d) | Launch Library 2 + CelesTrak | src/data/rocketLaunches.js |
/api/launches + /api/celestrak/active |
5 min |
| Traffic | Selectable TomTom / OpenStreetMap / Hybrid roads; optional TomTom flow (BYOK) | src/data/traffic.js |
browser-direct OpenFreeMap tiles; /api/tomtom for flow |
viewport-driven; capped tile cache |
| CCTV | Austin + Caltrans (CA) + TfL London + Ontario 511 + Fintraffic (FI) + DriveBC (BC) + TxDOT (TX) + Estonia (Tallinn, Tarktee) + Live Traffic NSW + Open Calgary Open Data + Statens vegvesen (NO, stills + live HLS, Kartverket ground heights) + King County (WA) Open Data + WSDOT (WA) Open Data + NYC DOT TMC (opt-in) + City of Toronto RESCU + Catalonia (Servei Català de Trànsit) + Hong Kong Transport Department + FL511 (FL, opt-in) + Freeway Bureau, MOTC (Taiwan) + Autostrada del Brennero (IT, A22 stills) + Street View fallback | src/data/cctv.js |
/api/cctv |
10s (active) |
| Radio | Radio Browser (public-domain station directory) | src/data/radio.js |
/api/radio/stations, /api/radio/click/:uuid |
45 min directory refresh |
| Transit 🚌 | Operator GTFS-Realtime VehiclePositions (7 keyless regions, src/data/transitFeeds.js) |
src/layers/transit/ via src/app/layers/transit.js |
/api/transit |
15s (poll + delayed playback) |
| Bikeshare 🚲 | GBFS (Lyft + BCycle + Social Bicycles + PBSC incl. Bike Share Toronto, Bike Itaú (Brazil), Bicing (Barcelona) and seven other Spanish cities + Urban Sharing, Entur, JCDecaux (Lillestrøm, Valencia, Seville) + nextbike (AMBici)) | src/data/bikeshare.js |
/api/gbfs |
60s |
| Directions 🧭 | OSRM on FOSSGIS servers (OpenStreetMap) | src/data/directions.js |
/api/route (steps=1) |
on placement / mode change |
| Datacenters ▣ | OSM extract (bundled) | src/data/infrastructure.js |
— | static |
| Dams ▰ | OpenInfraMap/OSM extract (bundled) | src/data/infrastructure.js |
— | static |
| Submarine Cables ◠ | TeleGeography public map (bundled) | src/data/telegeographySubmarineCables.js |
— | static |
| FIRMS Active Fires ▲ | NASA FIRMS live (VIIRS ×3 NRT + MODIS NRT, trailing 24h) | src/data/firmsHeatmap.js |
/api/firms (FIRMS_MAP_KEY) |
10 min (proxy TTL 30 min) |
| Street Level 📷 | Mapillary street-level imagery (vector tiles, Graph API, MapillaryJS) | src/layers/streetLevel/ via src/app/layers/streetLevel.js |
/api/mapillary/status, /api/mapillary/tiles/coverage/{z}/{x}/{y} (MAPILLARY_CLIENT_TOKEN) |
camera-driven (320 ms debounce, ≤9 tiles, server memory cache 24 h) |
| Wind 🌬 | NOAA GFS 10 m wind (keyless, 0.25°→1° grid; animated particles) | src/data/wind.js |
/api/wind |
1 h (forecast cycle) |
GNSS Integrity 📡 (gnss-integrity) |
adsb.lol point snapshot, low navigation accuracy binned per 0.5° cell (GEV NIC/NACp threshold + gpsjam.org formula and bands, keyless, ODbL) | src/layers/gnss/ via src/app/layers/gnss.js |
/api/gnss-integrity |
camera-driven (600 ms debounce, minimum 2.5 s between reads) + 60s poll (server cache 60 s; 30 min rolling window) |
S2 GhostMaps 👻 (s2-ghostmaps) |
S2 Underground's public GhostMaps repository on GitHub (KMZ intel products, keyless, CC BY-NC-SA 4.0) | src/layers/ghostMaps/ via src/app/layers/ghostMaps.js |
/api/ghostmaps/catalog, /api/ghostmaps/events (server-sent events), /api/ghostmaps/file/{product} |
branch head checked every 15 s; new commits pushed to the page; 5 min catalog backstop |
| Fire Perimeters 🔥 | NIFC WFIGS current interagency perimeters (keyless, paged past the 2000-record cap); InciWeb catalog + incident page origin and update time checks for verified incident-page links | src/layers/perimeters/ via src/app/layers/perimeters.js |
/api/fire-perimeters + /api/fire-perimeters/inciweb/* |
5 min (server caches: catalog 1 h; publication 30 min) |
| Prohibited & Restricted Airspace 🚫 | FAA Special Use Airspace prohibited (P) and restricted (R) zones (keyless FeatureServer, simplified geometry) | src/layers/droneAirspace/ via src/app/layers/droneAirspace.js |
/api/drone-airspace |
1 h (server cache 1 h; stale-on-error) |
GNSS Integrity samples a 250 nm circle around the Earth point under the screen
centre. Camera reads debounce for 600 ms with a monotonic 2.5 s minimum gap;
nearby views share whole-degree cache keys, and a minute poll refreshes data.
Counts and bands cover visited views over 30 minutes, including regions now off
screen. Non-pickable ground primitives preserve Street Level and perimeter
selection. The legend describes a low-accuracy score, not the raw aircraft share;
getStats().levels exposes nominal, medium and high counts separately.
Transient source failures keep activation enabled and show unavailable/degraded health with the next automatic retry. Upstream 429 deadlines are capped at one hour, with the longest concurrent deadline retained; 5xx, timeout and malformed responses impose a 15 s server cooldown. Cache hits and shared reads do not spend upstream admission. Cache age and cooldown use a monotonic clock; observation age comes from the upstream snapshot and is preserved in the row and voice stats. Snapshots older than 30 minutes or without a source timestamp are refused. Disable/destroy cancels pending requests, timers, subscriptions and primitives.
The GEV classifier remains unvalidated; anomalies do not confirm interference.
See method, provenance and limits, including
MLAT-positioned GPS loss, derived-field exclusions and the NIC 0 exception.
scripts/qa-gnss.mjs covers live freshness, bands, built primitives, camera/globe
anchors, recovery, keyboard and mobile states; emergency repainting is covered
by scripts/qa-emergency-squawk.mjs. Both accept --timeout-ms for slow boot.
Fire Perimeters uses capped, timed server reads with stale-on-error caching and a per-client limit. A stale snapshot is served for at most 24 hours; the layer row then reads STALE, dated by that snapshot's fetch, and get_fire_perimeters returns stale and retrieved_at with "(data may be stale)" in its summary. Unchanged snapshots retain geometry; link checks cancel on disable or selection change, and the row legend shows reported containment.
Prohibited & Restricted Airspace is off by default (share-link token 6) and sits in
Infrastructure. The proxy pages TYPE_CODE IN ('P','R') from the FAA SUA
FeatureServer, caps the body, and reuses a one-hour snapshot, serving the last
good payload as stale when a refresh fails. Client-supplied URLs are rejected.
Polygons keep holes; pick ownership is deliberately unregistered so sibling
cards still work over a zone. The row legend and selected card state that the
layer is informational only and not a substitute for B4UFLY / LAANC.
Street Level is a docked, collapsible right-rail panel (#street-level-panel,
layer token 0, option owner street-level, panel ui token t) that starts
collapsed. It opens itself only when the user, voice or a tool switches the
layer on or a photo opens, never on a restore, and those opens are not stored.
Like the iD editor's photo overlay, it has one chip per registered provider,
shared 360°/flat and captured-since filters (relative days, so a link keeps its
meaning), one viewer host and one on-globe credit per active provider. Each
provider draws in one colour (PROVIDER_COLORS in
src/layers/streetLevel/policy.js); 360° cones are rings and the selected
sequence is GEV cyan. Share options: m (Mapillary), r (the other providers' switches by id,
e.g. google-1 for Street View), p (a/p/f panoramas), s (since, days). Mapillary, Google
Street View, KartaView and Panoramax are registered. Without
MAPILLARY_CLIENT_TOKEN the panel reads KEY REQUIRED and the controls are
disabled. With it, coverage draws as z11–14 sequence lines below 60 km; the proxy strips the unused image layer from z14
tiles (12 MB → ~80 KB) and shares one upstream fetch between concurrent
requests for a tile.
Line coverage accepts a provider’s numeric zoomFor result or its legacy
{zoom, kind} shape. Overview levels and invalid zooms show the zoom-in hint
instead of silently requesting no tiles.
Coverage is z11–14 sequence lines below 60 km; above that nothing is drawn and
the panel says to zoom in. Tiles come from the visible ground within range of
the camera, nearest first, at most 9 (the halves of a view across
the date line count a shared tile once). Lines are draped
GroundPolylinePrimitives (ClassificationType.BOTH), so they follow terrain
and Google 3D tiles alike; cones and the photo marker are ground-clamped
billboards with a horizon cull. Without MAPILLARY_CLIENT_TOKEN the panel
reads KEY REQUIRED, says which key to add and disables the filters; a rejected
key reads KEY REJECTED and stops tile requests until the layer goes off; a 429
pauses them for its Retry-After.
The server proxy (server/providers/mapillary.js) adds the token to
coverage-tile requests (the browser holds it too, for MapillaryJS and the Graph
API), serves only z11–14 coverage, refuses upstream redirects, strips the
unused image layer, shares one fetch per tile, runs at most six at once
(cancelling those every client has left) and keeps tiles in memory for 24 h
(a body too big to be a trimmed tile is served but not kept). While Mapillary
rate-limits, the proxy answers 429 itself until the Retry-After (seconds or an
HTTP date, at most 10 minutes) is over, so the rest of a view does not go on
asking.
The header pill is the layer switch. Lighting a chip swaps the source;
clicking the lit chip turns the layer off. A chip without a usable key
explains the key instead of switching, and the other chips keep switching.
Lit off its map stack, a provider leaves the panel saying which map it
needs. When the switched-on providers lack
their keys but a switched-off one would draw with a key already set (no
Mapillary token, Street View on Google 3D with a Google key), the layer
reports it (keyAlternatives), and the panel's KEY REQUIRED, its coverage
line and the layer row's tooltip say to light it. The
viewer sits under the header with EXPAND, FIT/FILL and close above the
image. SINCE is a stepped slider whose readout names the cut-off date.
The viewer caption's bearing wraps after rounding to whole degrees, so a view
facing 359.5° or more reads 0° rather than 360°.
MapillaryJS loads on the first photo. Opening one frames it with a single
camera flight through the application's deferred navigation (begin /
reassert): it releases tracking, is refused in the cockpit, and is skipped
if newer navigation took the camera while the photo loaded; closing the photo
cancels it. EXPAND is the browser's Fullscreen API on the viewer element; on
iPhone, which has no element fullscreen, the viewer opens as a top-layer
popover instead (hidden where neither API exists). Clean View, recording and
the cockpit take an expanded photo out of full screen. At phone width the
viewer height comes from the rail band so the whole photo fits.
Providers implement the contract in src/layers/streetLevel/registry.js. The
core routes clicks by pick prefix, swaps viewer adapters in the one host,
manages each provider's credit and fans the filter out to every provider. To
add a provider: implement the definition, register it in
src/app/layers/streetLevel.js and add its modules to
scripts/package-boundaries.json; its share switch travels in r by id.
A provider with line coverage builds it on lineCoverage.js (camera-driven
XYZ tiles near the ground, draped, the selected sequence drawn again over
them) and its cones
on sequenceCones.js, passing its tile fetch, zoom ladder, colours and pick
prefixes; Mapillary, KartaView and Panoramax all do. With providers on different keys the
layer's requiresKeyId is null and each chip reports its own key.
Google Street View (providers/google/) runs on the Maps JavaScript API with
the browser GOOGLE_MAPS_API_KEY, which needs that API enabled; it is loaded
on first use (mapsLoader.js, CSP script-src https://maps.googleapis.com).
Its chip starts off (defaultOn: false; a link without its switch keeps it off) because every
panorama load is billed to the key. Google's terms forbid Street View beside
non-Google maps, so the provider declares requiresMapStack: 'photoreal':
off Google 3D it does not appear at all (no chip, globe
credit, layer source or button, and Google's script is not loaded), lookups
and opens skip it, and leaving Google 3D closes its panorama; the share
option keeps the user's choice for when Google 3D returns. Google offers no coverage layer
for other maps, so it draws none: with it on, a click on the map itself
(scenePick.isSurfacePick: nothing picked or Google 3D tile content, so a
Mapillary line under the pointer wins; no tool holding the pointer; camera
below 3 km) opens its nearest Google-collected outdoor panorama within 50 m
and leaves the camera where it is (groundClick.js). Higher up the hint
says to zoom in. OPEN STREET VIEW beside that hint (#sl-nearest-btn)
does the same at the view centre, so Street View needs no pointer; it is
disabled until the camera is at street zoom (and aimed at the sky, it opens
nothing and says to aim at a street). One provider is lit at a time:
setProviderEnabled(id, true) darkens the others, a chip click sends every
switch in one update so the share link records the swap, and params that
light several keep the one already on, else the first in chip order
(litByParams; reapplying them never flips). Mapillary, free, is lit by
default. The chip reads GOOGLE STREET VIEW, and an open panorama's globe
marker is a Google-blue view cone (marker: {kind: 'view-cone'}) in place
of GEV's amber pin. Its
panoramas are all 360° and it draws no coverage, so it declares filters: []
(a provider's filters default to both pano and since): lookups ignore
the IMAGERY and SINCE filters, and the panel hides each filter row no lit provider
honours (filterControls). The viewer is Google's StreetViewPanorama in its own
element in the viewer host, without Google's fullscreen, close and address
controls (the address goes in the caption); a pose goes out at once and
again when the panorama's month, photographer and address arrive (one
successful lookup per panorama per session, seeded by the nearest lookup;
a details lookup that fails or takes over ten seconds waits five seconds
before a view event can retry, and an unmount drops lookups in flight). Opening
another panorama settles the previous open before registering its wait; closing
then releases the current wait immediately. A details lookup released while its
service loads makes no panorama request after that service becomes available. A key
Google refuses (gm_authFailure: the API not enabled, a referrer it does
not allow, billing off) reads KEY REJECTED and fails a pending open at
once; a lookup or open Google never answers fails after 10 or 20 s. In CI
(no Google key, no Google 3D) the gate checks that nothing of Street View
shows off Google 3D, even switched on by a share link; the click and button
step skips as no Google key.
KartaView (providers/kartaview/) is keyless and browser-side: CORS is open
on api.kartaview.org (the documented api.openstreetcam.org answers but
serves empty tiles), cdn.kartaview.org and storage*.openstreetcam.org.
Its chip starts off. Coverage comes from z14 JSON tiles below 2.5 km above
ground and z13 up to 6 km (lower zooms take seconds per tile; z10 does not
answer), so higher up the hint says to zoom in, and an empty view says there
are no KartaView photos in it. A tile's lines are OSM way segments with
photos and carry no sequence, date or projection, so decode.js chains
segments that meet end to start (about four times fewer lines; linear time,
at most 20,000 segments a tile and 5,000 points a line), they are
drawn as kv:line:<z/x/y/n>, up to 1,600 per tile, and never highlighted, and the
provider declares filters: ['pano', 'since'] and coverageLabel ("N
streets in view · click a line for its nearest photo"). KartaView filters
coverage only in its PNG tiles (/sequence/tiles/{x}/{y}/{z}.png?fieldOfView=360|plane&startDate=YYYY-MM-DD;
its JSON, GeoJSON and jsoncanvas tiles come back empty once a filter is
asked for), so a filtered tile is drawn from the cached JSON segments that
lie on the filtered PNG's drawn pixels (maskedSegments: at least half the
points sampled every 2 px have a drawn pixel within one; on Jakarta's 360°
filter, 332 of 1,041 segments lie on it, 676 miss it, 33 fall between).
A filter change reloads the lines (lineCoverage.reload), keeps the JSON segments, and caches masks per
filter and tile. A dense filtered PNG can take KartaView over a minute the
first time (it answers 504 at 60 s and keeps drawing), so a timeout or 5xx is
asked again 4 s later, four tries in all; then the panel says it did not
filter the area in time. KartaView's startDate is the upload date (its
server filters by nothing else), so SINCE goes by upload date throughout,
the cut-off taken as its UTC day (passesKartaViewFilter): the nearest
lookup asks the photo API for the projection (SPHERE/PLANE; it ignores
fieldOfView) and upload date and checks each answer the same way (when
photos are near but none passes, the panel says so), and the cones pass on
each photo's projection and upload date. The provider declares
sinceBasis: 'upload', so while it is lit the panel shows "(by
upload date)" beside the SINCE value, as ANY DATE (by upload date) (sinceNote in the UI state). Tiles come uncompressed and
uncacheable, so the last 24 decoded tiles are kept for 10 minutes while
KartaView is lit (createTileCache, dropped when it is switched off); one
that does not answer in 20 s is asked once more, and one that is not a line
list is not cached. A click on a line reaches
handlePick(id, {groundPoint}), and the provider asks the core
(actions.openNearest) for its nearest showable photo within 50 m, without
framing: many photos are archived (their file answers 409, while a HEAD says
200) or have no file, so the six nearest are probed at once with a GET cut
off at the headers, and where all are gone the panel says KartaView's photos
there are archived. Only a definite answer (403/404/409/410, or 200) is
remembered; a storage timeout or 5xx is asked again. The open photo's
sequence draws as cones from the 250-photo page holding it
(/2.0/sequence/{id}/photos); the panel counts the drive's own photos
(countActivePhotos, asked once per sequence). The camera direction (the
marker, cones and a panorama's centre) is the direction of travel
to the next photo a few metres on (assignCourses): KartaView is dashcam
imagery, while its heading is the device compass, which reads 0 for whole
drives without one and can sit a steady 110° off inside a car. Until the
drive's page arrives the compass stands in (0 counts as unknown), and the
pose is sent again once the course is known. The open photo's marker is a
purple view cone (marker: {kind: 'view-cone'}), as Street View's is blue. Pages follow photo ids, which only roughly
follow the index, so the page is found by search (both ways, halving past the
end when many photos were deleted), a neighbour page joins within 12 photos
of an edge, and the cones re-centre whenever a photo of the selected sequence
opens on another page. ‹ › (and ← →) open through the core without framing,
step on from the photo already on its way, skip archived photos, and disable
up front at the ends. api.kartaview.org speaks HTTP/1.1, so of the
browser's six connections to it tiles take three, lookups two and pages one
(newest first), and a lookup never waits behind tiles; the photo API answers
429 to more than about three requests at once, so a 429 is retried with
backoff (at most 10 s), and a lookup gets 8 s, then 15 s once more (KartaView
sometimes sits on a lookup for 11 s and answers the same one in 3 s); a
request that could not be made at all (no network, CORS) says so and is not
repeated. Answers
are read with a running byte count and cut off past 16 MB (KartaView sends
them chunked, without a length). Photo URLs are built from
storage and filepath*: the API's fileurl* always name storage1, where
photos stored elsewhere 404, and a fileurl* fallback must itself be an
https storage host. The viewer (viewer.js) shows a flat photo at 1,280 px,
then 2,560 px, from the image proxy (pr:sharp/rs:fit:W:H/<base64url>),
with KartaView's own 1,280 px file as the fallback, and a 360° photo's
equirectangular wrapped_proc image (13,000 px wide upstream) resized to
1,024 px, then 2,048 (docked) or 4,096 px (expanded, or once the viewer
grows), decoded off the main thread into an ImageBitmap and uploaded as RGBA
(about 90 ms → 15 ms for 4,096 px on a phone-class CPU), on panorama.js, a
single-triangle WebGL renderer (image centre = the camera heading; drag,
pinch, wheel, Shift+← →, ↑ ↓, + −; the canvas re-measures every frame; a
closed viewer keeps a 1×1 texture). Without WebGL, or if the proxy fails
within 15 s, a 360° photo shows its front view. A lost context draws the
photo again on a new canvas (a lost canvas only gives back its lost
context), through the core and facing where the user was looking; a newer
photo that was opening is drawn on the new canvas instead. FIT / FILL hide
for panoramas (supportsRenderModes). The
caption has no place name (KartaView's is where the drive began) and links to
the full-size photo through the image proxy, since kartaview.org's photo
pages fail for signed-out visitors. The viewer itself is photoViewer.js,
shared with Panoramax: each provider hands it a PhotoSources table (first
and sharper flat and panorama URLs, and the caption fields).
Panoramax (providers/panoramax/) is keyless and browser-side: the
meta-catalogue api.panoramax.xyz (CORS open) federates every instance, and
each picture is served by the instance holding it (OpenStreetMap France,
IGN, MapComplete…; IGN's redirect to its OVH object storage). Its chip
(orange, #ff6f00, Panoramax's own) starts off. Coverage comes from its
vector tiles (/api/map/{z}/{x}/{y}.mvt, an hour's HTTP cache): sequence
lines from z14 below 1.8 km above ground, z13 to 5 km, z12 to 12 km, z11
to 30 km and z10 to 60 km, as Mapillary's (a dense z10 tile over Paris is
~2.1 MB and 8,600 sequences, 750,000 points). decode.js simplifies each
line (Douglas–Peucker to the segment, 4 units of a 4,096 tile: about a
pixel where it is drawn, 6–10% of a GPS track's points), and the tiles in
view draw their longest lines within 90,000 points (maxPoints,
keepOrder), split evenly between the tiles in view (10,000 points
each). Every point becomes ~8 ground-line vertices:
unsimplified, the lines hung the tab. Decoded tiles are kept while the
provider is on (16, ten minutes, tileCache.js, shared with KartaView).
Above 60 km nothing is drawn and the panel says to zoom in.
A tile decodes in slices of about 8 ms, the page getting a turn between
them (a 2 MB z10 tile is ~200 ms of work). A busy API (429/503) holds the
tiles back for its Retry-After, which the browser cannot read here (the API
exposes no headers), so the wait doubles from 5 s to 5 minutes until it
answers; an answer may take as long as it keeps arriving (silent for 15 s
is given up), so a slow link still gets its megabyte tiles. Both
filters apply: lines carry their day and camera type. A click on a
line asks the core (actions.openNearest) for that sequence's picture
nearest the click, within the camera height / 100 (50–400 m: from high up
a click lands well off a line), without framing, then any sequence's within
50 m. Each lookup is two searches side by side, ranked by distance in the
browser: place_position (nearest first, but even at
place_fov_tolerance=180 it leaves out pictures looking away from the
point) and a bounding box (those too, roughly nearest first, so 20 are
asked for: ~60 kB, against ~600 kB for 100); the filters go to both (filter=field_of_view=360, datetime=<since>/..). A line is dated
by its sequence's first day, so a sequence still growing can hide under
SINCE; a line with no camera type or day stays drawn. Lines keep the
decoded longest-first order under the per-tile cap (keepOrder). Once the picture is on screen
its sequence is highlighted and drawn as cones from the z15 tiles around it
(their pictures layer, ~110 bytes a picture, against ~7.5 kB for a STAC
item), plus the neighbours within 150 m of an edge, on their own connection
(coverage tiles take three, searches two); each tile's list for the sequence
is kept. The open picture's own tile draws at once and the neighbours join
as they arrive; one that fails is not counted, so a picture opening in it
re-centres the cones there. The cones re-centre when a picture opens outside the tiles
they were drawn from, judged from the moment their load starts, and centre
only on a picture actually shown (never on a lookup that came back after
the user moved on); a lookup nobody waits for any more is stopped. ‹ › follow the picture's own prev/next
links, so stepping costs one small search per picture. The viewer shows the
2,048 px SD derivative (equirectangular for a 360° picture, decoded at
2,048 × 1,024) and swaps in the HD original only when the viewer is wider
than SD (flat) or, for a panorama, expanded; once, shrunk while it decodes.
When SD fails a flat picture falls back to the thumbnail, and to the
original only in the expanded viewer. Every download of a photo, first or
sharper, stops when another opens or the viewer closes. Originals wider than
8,192 px are never fetched; a file's header (JPEG, PNG, WebP, GIF) is read
before it decodes, and one declaring more than 64 MP or 16,384 px a side
is refused undecoded, as is an <img> that decodes larger and any file
over 48 MB. The caption names every
producer (an uploading app and its photographer; never an account named by
an email address), or else the instance holding the picture ("via
panoramax.ign.fr"), and the picture's licence (CC BY-SA 4.0, Licence
Ouverte 2.0…; StreetPose license), and links to the picture in the
Panoramax viewer. Esc anywhere in the docked viewer clears the selected
sequence before the panel can take it (KartaView too). After a GPU reset
the viewer draws the same picture again itself (onReopen), leaving the
core's lookups alone. The MCP globe panel allows the meta-catalogue and the
three biggest instances; a picture on another instance shows only in the
app.
Street Level has two browser gates. npm run qa:street-level -- --url <server>
runs against real Mapillary and needs MAPILLARY_CLIENT_TOKEN. npm run qa:street-level:fixtures -- --url <server> answers every Mapillary request
from fixtures (providers/mapillary/coverageFixture.mjs,
scripts/fixtures/street-level/), and KartaView's and Panoramax's too, so it
needs no network. It opens and closes a photo from Mapillary, KartaView and
Panoramax; stepping, EXPAND, FIT/FILL, the filters and Street View stay with
the unit tests. CI runs the fixture gate against a production build served by vite preview with a dummy token.
Directions is a keyless front end to the routing the voice agent already
uses. Its row chips are the whole interface: DRIVE / WALK / BIKE pick the
profile; SET A and SET B arm the next globe click (Escape or a second press
cancels), placing clamped A/B markers; with both placed the layer requests
/api/route?…&steps=1, drapes the geometry as a ClassificationType.BOTH
ground polyline with the annotation renderer's flowing-dash material (so it
reads on the keyless terrain globe and on 3D tiles alike), and drops one white
point primitive per intermediate maneuver. Those dots are anchored on the
shared ground floor (cachedGroundFloor — the rendered mesh cell on the
photoreal stack, the Re:Earth DEM cell on the keyless terrain stacks) and stay
hidden until their cell answers, so a route through Denver is not marked a
kilometre and a half below the city. Clicking a dot opens a protected
shared-host card with the instruction, the leg after it, and the next
instruction; Escape or a click on empty globe clears it. Below the chips the
row lists the turns in order — distance and instruction per step, each one a
button a keyboard can reach; clicking one opens that maneuver's card once its
ground cell has resolved, and the step the camera is on is highlighted and
scrolled into view during FLY. SWAP
reverses the endpoints and reroutes; changing the mode reroutes; CLEAR removes
everything. The row meta reads OSM routing · 21 km · 25 min · Drive; while
routing it shows Routing…, and a router miss reads
No route found between A and B — the layer never substitutes a straight line.
The layer holds continuous render only while a route is drawn (the dashes
animate). Disable clears all state.
At the metre/kilometre boundary, route distance labels use the rounded
ten-metre value to select the unit, switching to kilometres at 995 metres;
a 996-metre leg displays as 1.0 km rather than 1000 m.
Keys needed: none. Directions works on a keyless boot, and the layer is off until the operator turns it on.
Context behavior. Directions is not a Context layer: neither Contacts nor
Space Missions reads it, depends on it, or lists it as a companion. It is
therefore one of the layers an exclusive Context mode switches off on entry
(_clearLayersOutsideContextMode) and restores on exit, like every other
non-participating layer — the route and both endpoints are cleared with it, and
placing them again is the way back. Cockpit does not touch the layer either,
but it does own the camera: pressing FLY inside Cockpit is refused with the
shell's "Exit cockpit to fly to a route" toast, because FLY now goes through
the same navigation authority as every other camera destination.
Clicks and the camera. Arming SET A or SET B claims the shared pointer
(src/data/inputOwnership.js) as owner directions and keeps the lease it is
given; re-arming for the other endpoint reuses that lease rather than asking
for a second claim, and every release names the lease, so a superseded instance
can never free the claim its replacement holds. The claim is held until
the click that uses it has finished being dispatched — every layer binds its
own handler to the same canvas and they all run inside one browser event, so a
claim dropped the instant the endpoint is placed would hand the rest of that
same click to the ambient handlers bound after this one. It is returned on the
next tick after placement, and immediately on cancel, Escape, CLEAR, disable
and destroy; if another tool already holds it nothing is armed and the row says
so. Maneuver-dot selection is an ordinary ambient handler and yields while any
tool holds the pointer. FLY runs through the UI shell's immediate-navigation
facade — the same camera authority validated voice destinations use — and is
handed the shared ground-floor read and corridor warm, so the dolly does not
fly a mountain corridor at sea level. A reroute (SWAP, or a profile change),
CLEAR, disable and destroy stop the flight this layer started and only that
one: flyRoute returns a motion id and interruptCameraMotionIfActive refuses
to stop a motion someone else began.
/api/route (server/providers/places/routes.js) asks OSRM for maneuvers only
when the request does (steps=1), so a caller that does not read them — the
voice route annotation, fly_route — gets the same small response it always
did; a cached response that carries steps still serves those callers their own
shape. Identical requests that are in flight at the same time share one
upstream call. Outbound calls pass a shared gate that spaces them at least one
second apart across every client and profile, which is the rate the routing
service's usage policy states; past a bounded queue the answer is a 429 with a
Retry-After rather than a queue that grows until everything times out. The
upstream host is pinned and a redirect is refused rather than followed, and a
rate limit from the routing service is reported as one instead of as "no route
found". src/data/routeSteps.js turns OSRM maneuver type / modifier / exit /
road name into one plain-English sentence per decision, folding
exit roundabout steps into the roundabout they leave, and caps one route at
200 maneuvers — a route with more is reported as cut off, in the row summary
and as the last line of the list, because the arrival step is among the ones
dropped. A route of exactly 200 is complete, and a roundabout exit after the
200th keeps folding into it.
Directions ramp and exit text uses “on the left/right” only for a bare side; slight and sharp modifiers read as “slightly/sharply left/right”. Lane instructions name the left or right lane. The localized page and proxy English stay in sync.
Route duration labels select their unit after rounding seconds, switching to
minutes at 59.5 seconds; a 59.6-second leg displays as 1 min, rather than 60 s.
A step's card opens from its dot on the globe or from its line in the list, and
the list covers every step including departure and arrival, which have no dot.
What a card needs is a resolved ground anchor: a step whose ground cell has not
answered yet opens nothing rather than a card hanging at sea level. The cells
are re-read for up to 90 seconds, which outlasts a cold terrain round trip;
reroute, CLEAR and disable cancel that.
Transit is off by default, and share links use token j. The movement panel order
places Transit directly after Street Traffic and before Bike Share. Cameras and
utilities retain their own groups. The layer polls
registered feeds near the camera every 15 seconds, below a 3,000 km altitude
gate. Sixteen keyless regions are available: Boston, Austin, Minneapolis–St Paul,
Helsinki, the Netherlands, Norway, South East Queensland, and nine in
Ontario — Toronto (TTC surface vehicles; the subway publishes no
positions), MiWay, Hamilton HSR, Durham Region, YRT/Viva, Guelph,
Burlington, Barrie and Kingston. Mode silhouettes
identify buses, trams, subways, trains, ferries and unclassified vehicles.
Retained transit history uses the same outlier quarantine as live observations and preserves the active live bracket. Rejected reports do not publish route or mode changes to sprites, cards or detection text.
Timestamp playback. Vehicles play between reports at 1× or hold while waiting for the next report. The display delay adapts within 25–120 seconds; late updates do not make a vehicle accelerate. A short reported crawl still plays between fixes. Stationary segments schedule a wake at the next motion boundary. Motion wording and orientation come from the displayed segment; underflow says “Waiting for update,” and stale reports override confident motion wording. Reported speed and stop status remain explicitly labelled as reports. Duplicate timestamps and implausible observations cannot create a zero-duration journey or immediately relocate a visible vehicle.
Selected history. Selecting one vehicle draws its available recent path, clipped to the displayed time. The browser retains up to 15 minutes and 128 fixes per vehicle, under a global 32 MiB history budget. Capacity pressure may shorten that history. Only the selected vehicle has a trail: completed segments are batched primitives, and a small moving head ends at the marker. Trail geometry is bounded to 640 prepared vertices, prioritizing the active corridor and shortening the oldest geometry first. Active and future marker corridors retain their own 640-vertex budget; long report intervals use coarser subdivisions than 25 m instead of dropping an admitted vehicle. Supported scenes use ground polylines classified against both terrain and 3D tiles, including after map-stack changes, with per-segment age colours. History changes rebuild the body. The head is one short draped corridor, clipped to displayed time by a material uniform; only subdivision crossings rebuild it. Ordinary frames upload no head geometry. Without ground-polyline support, ordinary primitives retain a mode-coloured 0.55-alpha depth-fail path, including the head, without dimming it to near-black. Instance attribute updates write back the same typed array returned by Cesium; its getters return copies, so reading a second time discards a visibility or colour edit. Normal instance colours follow the age ramp once per display second or style change. Subdivision crossings update only changed visibility attributes. Deselecting releases selection geometry; marker paths retain only active and future corridors.
MBTA history is also retained in the running proxy: up to 15 minutes and 128
observations per vehicle, with a 16 MiB feed limit and 64 MiB process limit.
GET /api/transit/trail/mbta/<encodedVehicle> reads only observations already
received by this process; it never contacts the operator. Selection reads it
once and cancels the request on deselection or disable. Reload can recover
available history while the same server stays running. Restart clears it, and
there is no background collection or promise of complete coverage. Other feeds
remain live-enabled with session-local trails; proxy history is opt-in per feed.
The catalog and source tests pin that distinction and the MBTA / MassDOT credit.
Epoch metadata grows as contexts arrive. Snapshot members are protected during
ingestion, eviction uses receipt-ordered queues, and capacity loss is disclosed
when history is recreated.
Surface and route limitations. Heights are aligned to work with Google 3D tiles. Draped history is prepared on selection even while marker floors are unresolved; Cesium resolves its surface. Without ground-polyline support, unknown historical surfaces leave gaps. A selected body follows layer enablement and vehicle existence, so an off-screen head does not hide an on-screen tail. Sparse reports can cut across corners; these paths are not surveyed route geometry. Subway markers are projected to street level because the feed does not provide depth, and the selected card explains that choice. Actual street-level views still require visual inspection.
Appearance. Normal fleet frames are 20 px, growing to 30 px on selection;
CRT, NVG, thermal and noir frames are 26/39 px. Source rasters are 48/96 px before padding.
External halos are 1/1.5 px in normal, 1.25/1.25 px in CRT and 2/2 px in sensors, calculated from the
final unpadded display size. Six modes, two sizes and three profiles cap the
raster cache at 36 variants. Normal mode colours are bus #5EF08A, tram
#FFC24A, subway #FF4538, rail #D9A6FF, ferry #5FD6FF, and unknown
#D8DDE5. Selection keeps the mode colour. Sensors reuse each normal silhouette's exact
outer path, filled opaque white without interior panel lines, with an opaque
#05080C halo. Only the external padding grows; the unpadded silhouette remains
at CRT size. There is no envelope body. Black-hot reverses the contrast and
Ironbow maps the white input to its hottest palette entry through the thermal
effect; sprite code never applies a red tint. Creation, mode changes, selection, deselection, map presets and
Cockpit vision use the same sprite styling function.
Sensor signatures remain in-scene. The thresholds in qaMetrics.js require
white-hot, Ironbow and noir centres ≥0.85 luma and halo minima ≤0.25;
black-hot uses centre ≤0.15 and halo maximum ≥0.75. Surveillance uses ≥90%
of its phosphor peak white (0.65992 from RGB 0.16/1/0.22), halo minimum ≤0.25
with the centre minus sampled background reported without gating. Each preset needs at least
six verified, unobscured sprites; insufficient coverage is UNEXERCISED.
The sampler excludes overlapping footprints, including otherwise ineligible
sprites, and chooses nine source-raster points in the widest solid white band, avoiding
window/panel gaps. Equal-width tram bands prefer the upper solid body over the
geometric centre; the nine-point spacing and all thresholds remain unchanged.
The sampler then verifies pick ownership at all nine points.
Noir samples also exclude its deliberately vignetted outer field, using shader
settings and screen position rather than observed brightness. Selected-trail
diagnostics report every condition even when selection fails.
Transit brackets use the normal-composite overlay surface: a 1.25 px mode-colour stroke over 3.25 px dark backing. Other contact themes retain their existing behavior. The selected trail uses a 3 px core over 5 px backing, mode-coloured in normal and white input in sensors. Alpha falls linearly from 0.80 at the head to 0.45 at two minutes, 0.20 at ten minutes and 0.08 at fifteen minutes.
Cards and DETECT. The card presents route, mode/operator, displayed motion, display delay and newest-report age, reported speed/stop, vehicle ID, and available trail duration. Report age is rounded to whole seconds and stays in seconds while that rounded count is below 90; otherwise it shows completed minutes calculated from the rounded second count. Its anchor follows the rendered marker; text is checked at 4 Hz and published when it changes. DETECT labels retain route, mode and motion, with the selected label suppressed beside its fuller card. Candidate membership changes on revisions, coalesced at 4 Hz; paints read cached contacts and strings. Marker migration explicitly updates cached position references. Freshness and delay use the shared sample’s anchored clock; newest bearings are explicitly labelled as reports. Rejected fixes cannot alter lag reservoirs or the target delay. Returning from a hidden tab explicitly synchronizes visible playback before its next frame.
Playback reserves numeric scratch before the first sample, including unknown heights, and reuses caller-owned sample and segment storage.
Scene work. Stationary and animated billboards use separate, adjacent collections above CCTV and Bikeshare. Traffic, Bikeshare and Transit share camera sensitivity claims; the original sensitivity returns after the last release. Visibility checks combine the camera frustum and ellipsoid occlusion at no more than 4 Hz; missing map bounds do not admit the globe's far side. Camera arrivals rebuild an unprepared or cold marker corridor during that visibility sweep, adopt a loaded surface, and update both heightPending and surfaceReady diagnostics. A resolved display sample clears its pending-height gate without waiting for a poll. Frame work visits moving visible vehicles, up to 64 pending height changes, and the selected head. Rotation visits visible membership at 5 Hz, with selected rotation per frame. The transit render hold exists only for visible positional or orientation animation. Polls, surface completions and trail fades request individual renders. Disable clears vehicles, detection references, requests, timers and holds; destruction also releases collections and registrations.
The scripts/qa-layer-visibility.mjs gate uses the development-only registration seam and real browser tab visibility to prove periodic work pauses while hidden, explicit refresh still works, resume performs one overdue refresh, and disabled layers stay stopped.
Acceptance harness. Run against an already running development server:
QA_BASE_URL=http://localhost:4305 QA_TAG=keyless node scripts/qa-transit.mjs
QA_BASE_URL=http://localhost:4305 node scripts/qa-transit-recovery.mjs is the
focused keyless camera-arrival regression. With destination tiles loaded, it
injects nine disclosed moving-bus reports and delays their floor acquisition,
selects the hidden vehicle, then releases the floor and calls camera.setView.
Within two seconds it requires a visible marker, a prepared visible body and
at least six of eight paired off/on trail pixel samples. It uses production
selection, visibility and geometry owners and writes screenshots plus JSON;
it does not establish Google 3D acceptance.
The full run samples completed frames at 1920×1080 CSS pixels, records DPR, and
measures moving fleets of 800 and 3,000 vehicles with 128 fixes each and DETECT
on. Street/metro targets are respectively: transit frame CPU p95 ≤1/4 ms, frame
interval p95 ≤20/25 ms, billboard uploads ≤256 KiB/1 MiB per frame, and retained
heap growth ≤24/48 MiB. The 3,000-vehicle CPU budget is 4 ms (recorded browser
measurement 3.4 ms); the 800-vehicle budget stays at 1 ms (measured 0.9 ms).
Transit-authored frame allocation is ≤8 KiB/frame at both fleets, attributed
by source stack to src/layers/transit/**, src/data/contactPlayback.js,
src/data/contactTrailRenderer.js and src/data/transit*.js. Whole-frame
allocation and its three largest allocators are reported without gating:
Cesium billboard buffer uploads and the shared DETECT painter are outside
this layer's allocation budget. Follow-up: pool shared DETECT placement and
paint scratch in _buildLabelPlacements, _materializeCandidate,
_drawOverlay and their cohort/arbiter helpers. Heap growth is measured
conservatively as GC-bracketed page growth; allocation sampling includes objects
collected during the interval. These are acceptance budgets, not measured claims
for every machine.
Artifacts in qa-shots/transit/ include per-budget metrics and allocation
profiles, the Boston Red/741/742/Green-E matrix on measured bright and dark
backgrounds across normal, NVG, white-hot, black-hot and Ironbow, selected and
restored screenshots, reload recovery, and disable checks. Matrix routes are
scripted fixtures; the separate live-feed/reload checks use actual observations.
The separate Boston live pass uses actual MBTA observations at 120–600 m and
20–45 degree oblique views, with live selection and sensor readback.
Live visual cities are otherwise chosen for local daytime operation. Motion checks use
injected playback independently of live fleet activity. Thermal checks verify
style activation before reading generated controls; DETECT checks set density
and exercise the real OFF/restore button. Cesium render errors fail selection
acceptance.
The trail-visible section requires a visible Google 3D tileset. It selects a
scripted straight bus with CapMetro metadata under the isolated qa-trail-capmetro
feed ID, with nine retained fixes, runs for 20 seconds at each
300/600/900 m view (55/45/45 degrees), then freezes playback and waits for sustained tile and primitive readiness.
It captures off/on/off/on framebuffer patches at eight distinct projected surface
points outside the padded sprite and selection bracket. Google 3D selected trail visible at 6 of 8 projected samples
prints the selected key, fix count, elapsed display time, path length, hit count,
per-point coordinates/hits and renderer strategy. At least six points need a
repeatably changed green core or dark backing at the same pixels. Repeated off
and on channels and their contrast deltas must agree within 8 RGB levels;
background refinement, overlapping samples and unchanged dark roads fail. The dark
backing must reduce summed RGB by at least 40%, and either match needs RGB
distance of at least 40, so a faint depth-fail wash cannot satisfy visibility. Artifacts are
<tag>-<altitude>m-trail-visible.jpg and matching JSON. Run the same harness
against the base and patched keyed builds; partial runs remain diagnostic.
Node checks run Cesium's final geometry pipeline, including batch IDs and
position encoding: 1,576 bytes per head subdivision and 2,014,128 bytes for the
1,278-instance body. The body plus a separate 81,792-byte instance reserve totals
2,095,920 bytes, below the 2 MiB body limit; the head remains below its separate
2 KiB/frame geometry limit. Ordinary frames upload no head geometry. These are
pipeline buffer measurements, not measured GPU time or instance-texture uploads. The same 20-second browser interval scopes
WebGL uploads to the trail body/head, forces one body rebuild, reports head CPU
p95/max, and gates body uploads at 2 MiB and head uploads at 2 KiB/frame.
Required empty or unexercised checks fail acceptance. The passing final line is
HARNESS: PASS (0 unexercised). A QA_SECTIONS subset is diagnostic only and
cannot claim full acceptance.
Lifecycle and proxy. Contacts temporarily disables Transit and restores its prior state on exit; Space Missions refuses enablement while isolating replay data. Cockpit enters through Contacts and does not independently enable Transit. The proxy fetches registered URLs only, validates redirect origins, bounds responses and decoding, caches successful snapshots, and backs off after failed refreshes. A 304 confirms contact without appending history. Missing vehicles are removed after two missed polls; reports older than ten minutes and feeds silent for five minutes are removed. Credits appear when the feed is used.
src/data/militaryAwareness.js remains registered internally as the Contacts
coordinator, but it is not a user-visible Data Layers entry. Its visible entry
point is the right-side CONTEXT chooser's CONTACTS mode.
Contacts distance labels select metres or kilometres after rounding to the
nearest metre, so a 999.5-metre contact displays as 1.0 km, not 1000 m.
At global scale, ambient Radio cluster badges are hard-opacity shared-host entries: count/category updates and identity replacement do not run keyhole or enter/exit ramps. Shared collision, viewport rejection, allocation, and horizon culling can still remove an invalid placement, and Cesium still owns the cluster point geometry and picks. Their 50,000 km line-of-sight range covers the supported full-globe camera above 24,000 km, including farther horizon clusters. Unclustered visible stations publish nearest-first ambient labels through that same host, capped at 16 labels globally, 32 at intermediate zoom, and 48 nearby; cluster and singleton candidates still share the Radio source's 64-entry ambient cohort. Cesium continues to own each 13 px station point, horizon visibility, and direct/nearby picking. Native points and shared-host text use one 50,000 km interaction limit, so a painted singleton or cluster label always retains a pickable point at its anchor throughout the supported high-global view. No Radio entity uses native Cesium label text.
Delivery constraint: PR #10 is stack-only and unsafe standalone. PR #11 owns
the required Radio lifecycle/authority repair, so PR #10 must not merge or ship
unless PR #11 is included in the delivered stack.
Selected and singleton globe labels use the same compact 30-character
presentation: a credible explicit or leading-decimal frequency is rendered
first (93.9 FM — Station), otherwise the station name is ellipsized. Full
upstream names remain unchanged in the directory, player, and search state.
Fresh Radio sessions open on All stations while enable and restoration remain
silent. Directory refreshes retain cluster overlay identity for unchanged
represented membership and discard only identities containing removed stations.
When the user explicitly clicks Enable inside the expanded Radio section, the
Context panel performs one internal post-render scroll to reveal the station
filter and primary transport together without moving keyboard focus, the page,
or the globe. Compact controls, voice/tools, restoration, Data Layers, and
programmatic activation do not trigger this reveal.
Radio directory admission is atomic on both sides of the proxy boundary. A
refresh is healthy only when it reaches the minimum accepted-query and station
coverage; schema-valid responses with zero normalized stations count as failed
queries rather than inflating refresh health. Each specialist query also needs
an accepted station whose normalized tags match that requested category; rows
tagged only for another category remain usable catalog data but do not earn
specialist health credit. A partial cold result remains
usable but is explicitly DEGRADED and has no accepted catalog generation, while a
partial or malformed refresh cannot replace a warm catalog. The client likewise
rejects stale/future freshness metadata, incomplete rows, and empty catalogs as
a whole, preserving its last usable stations with STALE/DEGRADED state.
Every healthy admission publishes a monotonically increasing generation scoped
to a restart-stable catalogInstance token (a new server process starts a
fresh sequence — never read as a repeat or a regression) with a deeply
immutable station snapshot; stale/degraded warm responses retain the same
generation so tuner and cluster consumers can preserve exact station identity.
Generation semantics assume the app's actual single-process dev-server
deployment; concurrent replicas behind one origin are out of scope.
The client snapshot contains only the normalized station-field allowlist,
preserves object identity for an idempotent repeat of the same generation, and
degrades without replacement if a fresh response presents an older generation.
Snapshot records mark community metadata as untrusted, and Radio tool results
omit station names so directory text never becomes model instruction context.
One bounded country parser maps recognized ISO codes and English/common names —
including widely used exonyms the Intl display label omits (Turkey, Holland,
Burma, and similar) — through proxy metadata and final station selection, while
malformed, non-ISO, control-containing, ambiguous, and oversized inputs fail
closed. Literal or resolved
non-global IPv4/IPv6 targets are refused. Destroy fully releases the Radio audio
session, voice ducking/restoration, request state, filter, selection, volume,
accepted snapshot, and feed telemetry before re-initialization; monotonic
ownership tokens plus the session boundary keep late callbacks from a retired
session inert.
A tuner drag resolves previews against the single accepted snapshot captured at
pointer-down, even while a newer catalog is admitted. Release starts playback
only when the current same-ID record still exactly matches the frozen
presentation and stream metadata; removal or replacement reports the channel as
unavailable and never silently retargets the drag. A cold degraded fallback may
still populate the directory and globe, but its null accepted generation cannot
populate or begin the tuner.
Context entry and exit consider both rejected lifecycle promises and resolved
false manager results to be transaction failures. Entry awaits isolation and
restores the exact prior layer snapshot when isolation or activation fails;
when a direct Context shell fails after lifecycle work starts, reconciliation
waits outside the manager notification until that shell's queue settles, then
restores the complete snapshot rather than treating the in-flight layer as an
exclusion. An uncertain shell is retried and incomplete cleanup retains the
snapshot for a later restore. Direct-shell isolation and its compensating
rollback share one operation-scoped notification token, so an inner lifecycle
failure cannot announce separately from the outer blocked action. Context exit
waits for every sibling transition, retains the exact pending snapshot
after failed compensation, and can retry it later instead of silently reporting
a partial restore. User-facing Context chooser, direct Data Layer shell,
mapped-installation Search, rollback/exit, and Radio chip routes settle those
failures through the existing toast surface, release busy controls, and avoid
unhandled promises. A wrapped operation owns exactly one failure or blocked
notification; its synchronous manager event is suppressed only for that
operation, while unwrapped lifecycle failures retain the manager-level fallback.
The toast is a polite atomic status announcement.
When no tracked entity owns the follow camera, Radio Previous/Next and tuner
previews rotate to the requested broadcaster without changing zoom. A local
view whose optical center remains safely over the Earth, or a full globe already
contained in the viewport keyhole, uses one direct station flight and preserves
its initiating view angle. If a fit-capable Earth disc is clipped/off-center, or
a closer oblique view leaves the viewport center outside the Earth disc, Radio
first animates a centered north-up nadir composition, then focuses the latest
station from that canonical frame. Closer views keep their initiating altitude;
an extreme zoom-out is capped at 13,000 km so recovery returns to a useful
whole-globe scale instead of preserving an empty-space view. The two-stage navigation has one generation:
a playback fallback retargets it to the broadcaster that will play, while a newer
Radio action—including direct globe or non-moving voice selection—supersedes
older callbacks, and layer disable/destroy invalidates
the pending stages. While Flights, Military Flights, or any other
viewer.trackedEntity is active, Previous/Next and tuner previews continue to
change station and playback without cancelling or flying the camera. Tracking
acquired between the recenter and focus stages suppresses the later stage, and
a delayed fallback rechecks the same live ownership. Voice Radio navigation
remains non-moving; explicit station focus remains a separate user-requested
route and also yields to a live tracked entity.
The Radio tuner exposes the complete current filtered directory, up to 750
stations, in stable catalog and filter order. Needle progress is absolute across
that directory: the left, center, and right of the control resolve to the first,
middle, and last available station. Every position snaps to a real station, so
there are no selectable static gaps. During a drag, the bounded virtual tape
moves left as the needle moves right and travels faster than the needle without
creating DOM nodes for the complete directory. Camera movement never re-ranks or
rebuilds this order. Tuner-owned preview flights remain monotonic on the captured
strip, and release commits the frozen station before selection and playback
settle. That exact release clears older fallback ownership, so a broadcaster
failure cannot silently play a station left over from an earlier non-playing
cycle; the failed target retains its static/error handoff until Stop or another
explicit choice. Pointer cancellation instead cancels the active preview flight and
restores the exact pre-drag station ordering, absolute position, and frozen
presentation-only marker without starting a replacement camera flight,
committing, or autoplaying. That marker is restored even when an
accepted concurrent catalog removes the station or replaces its metadata, but
it never becomes current playback authority. The next accepted healthy catalog
refresh clears that presentation-only marker, including when the accepted
generation repeats and the immutable directory snapshot is intentionally kept
by identity. Every accepted filter action also clears and rebuilds the marker,
including a request for the already-active filter; a lifecycle-rejected filter
event leaves the restored marker and tuner band untouched. An accepted filter
action synchronously rebuilds the complete navigation pool after the layer state
notification, so Previous/Next and the tuner cannot observe an empty interim
directory. This restoration
also holds at either directory endpoint. Previous/Next traverses the same stable
pool and updates the absolute tuner position.
Cluster refresh identity follows the prior cluster contributing the greatest
absolute number of stations to the new cluster. This preserves majority identity
during merges instead of allowing a fully retained minority to win; deterministic
similarity and stable-ID tie breakers cover equal contributors and splits.
Every positive overlap participates in greatest-contributor discovery, so ratio
thresholds cannot discard a diffuse or one-station maximum. Inheritance is
bilateral mutual-best: a current cluster accepts only a greatest contributor and
a prior identity transfers only to a strongest split child. If that identity is
already claimed by an equal or stronger child, the later cluster receives fresh
identity instead of falling through to a historical minority. Disjoint clusters
also receive fresh identity; sequential fresh IDs are allocated in canonical
membership/station order so input permutations do not rename them.
Voice interprets “turn on/start the radio” as Radio Play, including when it is combined with a camera action. Explicit “show/enable the Radio layer/markers” remains a silent layer-only action and does not close the voice session.
Successful explicit user playback from Play/Resume, Previous/Next, the tuner,
or a globe station closes an active voice session only after Radio reaches
playing; a failed stream leaves voice active.
An interrupted or superseded voice turn aborts pending Radio location resolution
before it can enable the layer or select a station. Radio enable and disable are
abort-aware manager transactions: cancellation restores the authoritative
pre-transaction state only when the compensating lifecycle call succeeds and
emits no settled explicit-intent event for Context persistence. Failed
activation and teardown expose explicit enabling and disabling lifecycle
states while the manager retains the last authoritative visibility. The public
lifecycle vocabulary is enabling, enabled, disabling, and disabled, plus
a separate uncertainty bit. Radio controls present that phase and remain
non-interactive until the lifecycle is certain enabled; only a successful
transaction publishes the new enabled or disabled settlement. Radio's data
layer state, player message, compact status, launch controls, and generic Data
Layers row explicitly show UNCERTAIN when cleanup cannot establish authority;
their accessible labels name the uncertainty while Enable/Disable remains
available to reconcile it.
The Radio source, shared overlays, selected marker, and pick handler use the same
manager-owned presentation gate:
they remain hidden and inert throughout enabling, disabling, cancellation,
failure, and uncertain reconciliation, and activate only for certain enabled.
The same gate rejects direct station selection, Previous/Next cycling and its
camera/fallback preparation, tuner-static and category-filter mutation, volume
mutation, and every non-Pause playback toggle before fallback, selection, or
audio state changes. Voice volume and station-starting actions require a fresh
manager lifecycle read.
Every returned control_radio result, including status, failure,
cancellation, and a missing Radio module, exposes that same authoritative
lifecycleState plus lifecycleUncertain; the manager lifecycle record takes
precedence over the stable enabled fallback. Status only reads this state and
does not enqueue lifecycle or player work.
Generic set_layer_visibility results expose the same atomic enabled,
lifecycleState, and lifecycleUncertain summary for Radio on success,
fulfilled-false failure, rejection, cancellation, reversal, and missing-module
outcomes. Realtime suppression and settlement refresh all three fields together,
so dedicated and generic Radio routes cannot publish mixed lifecycle snapshots.
disabled is inert, while a transitional or uncertain shell is reconciled
through the manager and may proceed only after a fresh read confirms certain
enabled; false, rejection, or cancellation preserves the prior player state.
The existing immediate Stop/Pause authority is unchanged.
Cancelled-disable compensation reports failure and records whether lifecycle
state remains uncertain. Any same-target request made while state is uncertain
performs the real lifecycle work instead of taking the stable-state no-op, then
clears that reconciliation debt only after a confirmed enable or disable. A module-local
AbortError is a cancellation even while the caller signal remains live, while
that settled transaction releases its abort listener so the old caller cannot
disable a later successful retry. A resolved false from init, enable, first
update, or disable is a lifecycle
failure. Unrelated sibling tools remain independent, while each Radio control
captures the current playback-handoff epoch. A later Radio
Pause or Stop provisionally freezes prepared or already-started playback
handoff work without aborting active Select/Play auto-enable or an independent
explicit dedicated or generic Radio visibility ON. After semantic success it
cancels the active Select/Play lane and clears the frozen handoff. Semantic
failure leaves active work live and releases and resumes the frozen handoff.
Disable and generic OFF own visibility as well as playback and may cancel both.
These controls commit their authority only after semantic success, so a failed
stronger control cannot suppress a valid completed sibling. Pause uses the
production player's synchronous boolean contract; Stop and Disable additionally
handle their awaited failure paths. The control's function output is sent before a failed reservation releases,
so resumed playback cannot close the voice channel before the failure is
reported. Resumed handoffs own their attempt-scoped cleanup, so a stale predecessor
cannot clear the successor's in-flight result or block a later failed
reservation from resuming it again.
Generic set_layer_visibility Radio disable participates in the same ownership
domain as dedicated Radio controls, so an older Select cannot reverse it. Direct OFF
from either Radio control or the Data Layers row publishes intent before joining
the lifecycle queue and aborts in-flight voice work before an intermediate ON
event can settle. Every absolute manager visibility request also advances a
per-layer intent epoch and aborts the older absolute lifecycle transaction,
including a same-target request whose newer origin must own persistence. An
obsolete queued request never starts; obsolete in-flight cleanup keeps
presentation transitional and hidden, and only the latest request may adopt or
reconcile that state and publish settled visibility. The epoch is rechecked
after synchronous lifecycle-presentation callbacks, so a re-entrant newer
request prevents the older transaction from arming a timer or publishing a
settled visibility event. Superseding an uncertain same-target retry also keeps
the last authoritative enabled boolean until cleanup confirms the real module
state. Direct OFF also freezes
prepared or already-started playback handoff
until the manager queue settles; confirmed OFF discards that handoff, while a
failed OFF releases the reservation and resumes the valid prepared result. A
successful Stop cancels stale Radio work across older response ids as well as
its own response. If a Radio action has already completed its station mutation
before the later control succeeds, only its playback handoff is suppressed; its
result reads enabled state from the manager and audio state from the Radio
module. Voice Pause is a playback-only no-op while the layer is disabled; it
never enables Radio and reports a fulfilled-false pause as failure. A
newer-response or user-origin Stop still cancels stale handoff work.
Only a newer user turn or session teardown aborts the complete active-tool set.
A cancelled turn cannot publish a late Radio playback request. Dedicated Radio
controls and generic layer-visibility commands both forward cancellation.
Each explicit Radio play attempt owns one active audio element; replacement
retires the prior element, making its queued callbacks inert. Pause retires the
current stream and fallback attempt, so delayed callbacks from a replaced or
paused stream cannot change current state or start another station.
Pause and Stop settle attempt ownership and authoritative audio state before
synchronous playback-control observers run, so reentrant voice cleanup cannot
overwrite a released stream with a stale paused state. Layer
destruction likewise retains an enabled manager entry when module disable or
destroy fails semantically, preventing an active orphan and allowing retry.
Bundled datasets live in src/data/local_data/ with per-folder provenance READMEs; they are lazily loaded via src/data/localGeojsonCore.js (Vite ?url assets) when toggled on.
A bundled dataset that fails to load is a broken install, not a blip, so it is
never swallowed: localGeojson.js guards response.ok, reports error +
lastUpdate through getStats() (UNAVAILABLE chip, not a green ON over an
empty globe), and commits its Cesium data source only after setup completes so
a partial failure retries on the next enable. The two non-layer packs
(naturalEarthRegions.js, neighborhoodPolygons.js) have no stats contract to
report into, so they instead refuse to memoize a failure —
src/data/retryableLoad.js caches success permanently and retries a failed
load after a doubling cooldown (5 s → 5 min), which keeps one bad load from
silently demoting every later lookup for the session.
Context / Contacts coordinator (July 2026)
- The internal Context coordinator is available in every visual style. Its dedicated right-side
CONTEXTchooser exposes the neutral shell; the coordinator is not duplicated in Data Layers and does not enable a live-data dependency until a mode is selected. - The expanded
CONTEXTview offers mutually exclusiveCONTACTSandSPACE MISSIONSmodes. SelectingCONTACTSenables the context-owned Flights, Military Flights, AIS Vessels, and Mapped Installations dependencies only when they are not already user-enabled; selectingSPACE MISSIONSenables the recent-launch layer and its Satellite dependency.CONTACTScycles the nearest supported contact of whatever type is selected. Satellites are deliberately excluded from those Awareness cohorts and keep their own tracking UX. Selecting the active mode again returns to the neutral chooser and releases only mode-owned dependencies. - If a civilian or military aircraft is already tracked when
CONTACTSbecomes operational, that source-owned track is adopted as the Context subject before nearest-contact autofocus. Context rechecks the tracker after its dependencies settle, so a newer selection wins, while an explicit clear during activation prevents fallback from silently selecting a replacement. Cockpit entry remains unavailable until that Context transaction has settled, so its camera takeover cannot clear Cesium tracking before adoption. Adoption does not recreate tracking or transfer camera ownership; it initializes the normal 250 km ring, history, proximity results, and Cockpit Previous/Next state for the original aircraft. SEARCH NEARBY SITESretains the bounded OSM results and makes one user-initiated, view-biased Google Maps Places text search for “military installation.” Google results are source-stamped, deduplicated against OSM by rounded location/name, and remain mapped context rather than operational claims. If Places is unavailable or the API is not enabled for the supplied key, OSM context remains available. A view across the antimeridian is searched around its own centre, with a radius from its real width.- The Context header holds only its title and collapse control. It no longer carries a Radio shortcut or a compact Radio player: Radio is the
RADIOsection inside the panel, with its own disclosure and the singleENABLEpower control. Cockpit keeps its own compact Radio utility. - Context is set for reading in every theme: mode, standby and action labels are 10 px, and the panel draws no glow halo. Outside Cyber, the two modes form one segmented control, the standby guidance is plain left-aligned text rather than a dashed placeholder, and the panel's section titles share the side panels' dock-label colour. Cyber keeps its separately framed mode buttons, its red panel titles, and in layout-focus a framed, centred standby card over its radar art.
- The expanded desktop header omits the redundant
ONlabel; the active mode button carries state. Expanded Contact results also omit the duplicateGLOBAL CONTEXT/CONTEXT ONLYstatus row and begin with the selected subject and its 250 km scope. Global Context does not fabricate a selected-entity model preview: the provisional hand-authored aircraft wireframe was removed because it was not geometry extracted from the selected entity's actual asset. - Dependency ownership is reversible: disabling Global Context releases only dependencies it enabled, while user-enabled layers remain on. This also removes the Military-layer suppression handoff when Global Context owned Military, allowing an already-enabled civilian Flights layer to resume its normal mixed rendering. If OpenSky is unavailable and has no last-good cache, Flights requests a capped 250 nm adsb.lol point snapshot around the current view anchor and labels that provenance explicitly; it never relabels military-feed rows as civilian data. If both inputs fail, Flights remains
UNKNOWN. - Space Missions is replay-isolated: Rocket Launches and Satellites are the only Data Layers permitted while the mode is active. Direct UI and voice entry capture the same pre-entry snapshot; internal dependency and restoration enables do not create a user-owned Context session. Entry waits for incompatible layers to shut down, direct incompatible enables are blocked before lifecycle work, and the entry gate remains active through the complete Rocket Launches enable. A newer same-target ON request takes ownership of the pending entry without releasing its isolation snapshot, including when it arrives while the prior request is awaiting the adoption guard. A caller abort, resource cancellation, newer OFF, or layer teardown waits for exact manager settlement and restores that snapshot without resurrecting Rocket Launches. If an abort lands after only part of a restore settles, Context completes the same exact target without the stale caller signal and then replays newer explicit layer intent. Dedicated Voice Context cancellation reports a stable cancelled result plus the current Context state; generic layer visibility additionally exposes manager phase, reason, successor, and lifecycle details. Once an exact voice visibility intent commits, a newer voice turn cannot relabel it as cancelled while Context settlement completes; pre-commit aborts remain cancellable and final lifecycle mismatches remain failures. Clear Selected Layers reserves its complete captured OFF set before sequential teardown; a newer absolute request of any origin supersedes only its layer reservation and remains authoritative. A rejected layer teardown retains the truthful enabled state, rolls already-stopped siblings back to the captured pre-entry set, and aborts replay; rapid exit/re-entry serializes the full Satellite enabled-state and parameter restore before a new snapshot is taken. Contacts remains additive and restores user-enabled layers normally.
- Enabled Data Layer controls report normalized feed health on the button (
LOADING,DEGRADED,STALE,FALLBACK, orUNAVAILABLE) while the metadata line retains the source and reason. A partial CelesTrak group failure keeps the usable catalog and reportsDEGRADED; a total outage keeps last-good catalog data visible but reportsUNAVAILABLE. - On activation it focuses the nearest currently observed aircraft across the civilian and military feeds, with military winning an exact distance tie; if none are available, it focuses an observed AIS vessel. The aircraft search is deliberately uncapped and one refresh-tick retry handles initially empty feeds. This is an attention-priority navigation shortcut, not a high-risk, affiliation, or threat classification.
- A selected aircraft, AIS vessel, or mapped installation gets a 250 km context window with nearby cohort counts, nearest examples, source labels, and stale/unavailable reasons. It emits
NEARBYorUNKNOWN; no detection, engagement, affiliation, or sensor-activity conclusion is calculated. - For a selected live aircraft or vessel, the context window refreshes from the existing tracker/feed position every 750 ms, so nearest distances and cohort counts follow the subject without introducing a duplicate poll loop.
- Aircraft cohort membership uses every locally loaded, selectable contact inside the 250 km window, including a plane hidden only by horizon culling or because its 3D model owns the visual. Counts and navigation therefore do not change with the current camera angle or billboard/model handoff.
- Nearby examples in the context panel are focus controls: they use their owning layer's existing selection/tracking path, then frame that contact. Static-installation distances use ellipsoidal surface distance so the count matches the ground-projected context disk.
- Context selection transfers camera ownership by subject type: selecting a civilian or military flight keeps that layer's moving follow camera, while selecting an AIS vessel or mapped installation first releases any prior aircraft tracker and performs only the source layer's one-time framing. The camera therefore remains user-controlled after non-aircraft selection instead of continuing to move with the previously selected plane.
- Selected AIS vessels use their layer-owned full-detail presentation model in the shared world-overlay host; mapped installations promote their own entry to a selected card. Both remain crisp above post-processing without duplicate selected labels; non-selected AIS cards retain their source-owned grid/visibility selection and are host-batched with other world cards.
- Space Mission ascent replay uses the compact rocket/thrust overlay only through orbit insertion. Once the replay enters its orbit phase, that vehicle glyph is replaced by a fixed-size cyan dot following the same orbit path and callout.
- While a subject is selected, an inner keyhole compass rotates against camera heading and up to three cyan shafted bearing arrows lock to its single faint tick-marked rim, with their labels inset just inside the circle. Labels explicitly separate the geographic bearing (
BRG) from the contact's reported course (CRS) so the pointer direction is not confused with aircraft heading. They point toward the nearest observed/mapped examples; each cohort displays up to ten examples while retaining the complete locally loaded in-range cohort for navigation, with three visible at a time and a ten-second page rotation shared by the panel and arrows. This distinct neutral-context color avoids implying that all context indicators are military-flight symbols.PREVIOUS,FOCUS, andNEXTcontrols navigate selection history or the next nearby cohort example through the existing tracker. NEXT uses a cycle-scoped visited set and starts a deterministic new walk after exhausting the current candidates instead of re-admitting the nearest visited contact. - Awareness
BRGandCRSlabels wrap after rounding to whole degrees, so a direction of 359.5° displays as000°rather than360°. - Installations are viewport-bounded OSM map features (
military=airfield|naval_base|range|barracks|baseandlanduse=military), capped to a 10° non-dateline request and 700 upstream features. The proxy caches five minutes and serves a one-hour stale fallback. Empty, stale, unavailable, and zoom-too-wide states remain visibly distinct. - With a selected subject, Contacts hands the installation layer the subject position every refresh. The layer loads a window around the subject (see Installations and map-source guidance), because a follow or Cockpit camera never fires
moveEndand often looks at the horizon: a first load from a globe view used to leave the row at "?" for the whole track. The row and the Cockpit scope readWITHIN 100 KMinstead ofCURRENT VIEWPORT ONLY, and an empty window reads "none mapped within 100 km; not a complete 250 km survey".scripts/qa-context-arrivals.mjsflies synthetic aircraft over Camp Mabry and Fort Cavazos from a wide start, presses CONTACTS, Cockpit and SEARCH NEARBY SITES, and records a clip. - The selected-only visual is one static, unfilled blue circle. It marks the 250 km proximity context window only; it is not coverage or a radar/weapon envelope. Missing broadcasts and unmapped sites are explicitly not evidence of absence.
- The right rail's collapsed Display, CCTV, and Context controls use the same compact sizing language as the left rail's collapsed Data Layers and Authoring controls. These compact-state widths do not constrain expanded panel or child-content widths.
Motion & Symbology Correctness (June 10, 2026)
- Flights (commercial + military) render one poll interval behind real time (30s/15s) and interpolate between two known feed-stamped fixes (OpenSky
time_position; adsb.lolreceipt − seen_pos). The display latency is an intentional product decision — do not "fix" it away. The whole fleet dead-reckons at ~12Hz (1m² write gating); aircraft get a 3-poll grace period (faded icon) before removal. When a position epoch pauses, both layers coast for at least 60 seconds of contact grace with an absolute five-minute ceiling. Source backoff marks each contact and the cockpitSTALE; the cockpit then holds the exact layer position instead of continuing inertial flight. Repeated-position kinematic changes create a forward-only synthetic fix rather than mutating history, and grounded history is lifted only when no owned 3D model already controls its datum. A nominally successful worldwide OpenSky response whose own snapshot epoch is more than two minutes old prefers the existing 250-nm viewport-scoped adsb.lol fallback and labels the source/coverage accordingly when that upstream is available. If the fallback is also unavailable, the layer's freshness/error fields use the source epoch—never the cache receipt time—so the UI reports an old snapshot rather than “just now.” - World-space headings at every angle (
src/data/iconOrientation.js): aircraft/vessel icon rotation uses the camera right/up basis per tick (alignedAxis always ZERO), which is exact at screen center and for orthographic/nadir views and remains stable through >180° tracked orbits. Perspective rays vary across the viewport, so off-center contacts at oblique pitch can diverge from an exact finite-difference window projection; a regression test pins that known regime, and field evidence decides whether to adopt exact projection with the basis method as fallback. Fleet rotations refresh on camera-pose change; tracked entities per frame. Billboards are horizon-culled via a shared EllipsoidalOccluder. - Military/OpenSky reconciliation (
src/data/militaryRegistry.js): known-military ICAOs render amber in the flights layer (60s self-poll of the cached mil endpoint when the military layer is off) and are suppressed there while the military layer renders them. - Satellites: 838-sat core catalog (stations/visual/GPS/GLONASS/Galileo/GEO), tracking lands ~726km out via
viewFrom(tracked entity owns a point graphic so the tracking camera engages), rings realign via primitive modelMatrix (no per-second rebuild flicker), optionalsetParams({catalog:'dense'})Starlink mode. - Satellite classes (
src/data/satelliteClass.js): every satellite is classified from the CelesTrak group it was ingested with — no extra fetch and no heuristics — into STATION (warm white), NAV (cyan; GPS/GLONASS/Galileo deliberately share one color so the GNSS family reads as one thing), GEO (violet), VISUAL (muted blue-gray catch-all), and COMMS (dim slate, dense Starlink shell only). That module is the single source of truth for class, label, and color, so the dot, the card, and the legend swatch cannot disagree. Two deliberate palette rules: no class may sit in the 40–48° amber band, which is the app-wide known-military convention; and COMMS stays far below VISUAL in Rec.601 luminance so the dense shell stays separable when NVG/FLIR collapse the scene to one channel. The ISS keeps its long-standing red hero dot rather than the STATION color — it carries a permanent name label and its card still readsSTATION · ISS.satelliteClassOfis the single owner of that ISS rule, so the card label and the legend tally can never disagree: during a stations-feed outage the ISS is ingested asvisual, and both surfaces still file it under STATION. - Class is also a text field, not just a color:
NAV · GPS/GEO/COMMS · STARLINK/STATION · ISSleads the tracked card's detail block and replaces the raw CelesTrak tag on the detection-overlay label. Because that canvas composites above the post-FX chain, the class stays readable in NVG/FLIR after the in-scene dot colors are flattened. - The satellites row in DATA LAYERS carries per-layer sub-controls (
DataLayerManager._syncRowControls, the first consumer of the optionalgetRowControls()layer hook): a DENSE chip exposing the existingcatalogparam, and a swatch legend with live per-class counts. Default is the sparse core catalog. The chip is stateless — it declares the params to apply and the manager owns the write — so the Space Missions capture/restore path over the same param stays authoritative. Controls stay hidden while the layer is off. An explicit CORE or DENSE choice participates in versioned local and share-link state; temporary Space Missions overrides do not. - The DENSE chip reports the dense LOAD, not the catalog param. The param flips synchronously while the Starlink shell takes seconds to arrive over a chunked load, and CelesTrak 502s that feed regularly. So the chip reads
DENSE ···(busy, disabled) while loading, ACTIVE only once dense points are actually on screen, andDENSE ✕with the reason on hover when the load fails — a failure also revertscatalogtocore, drops any partial chunk, and leaves the chip clickable to retry. A load is judged by points added, not by HTTP status: a 200 carrying an empty body, a passed-through HTML error page, or only TLEs the core catalog already owns fails with the same revert semantics as a 502. Any explicit request forcoreclears a latched error even when the mode does not change, so a Space Missions restore of an already-core snapshot never leaves the user with a failure they did not cause. Because the load settles asynchronously, the layer pushes a re-render through the optionalsetRowControlsListener()hook; nothing else would repaint that row before the 5-minute catalog refresh, so the count and legend would otherwise sit stale. - A dependency owner takes the row with it. Space Missions borrows this layer for TLE lookup with
showPoints:false; while points are hidden the layer returns empty row controls, so the legend never describes an empty sky and the chip cannot accept a write that the owner's restore would silently revert. - The detection-overlay record cache (
_detectionObjects) is cleared with the catalog on every rebuild: it stamps id/class at creation only, and a rebuild can re-tag a satellite when a partial CelesTrak outage changes which group wins dedupe. - Catalog numbers above 99999 can be written in a TLE as Alpha-5: a letter A–Z, skipping I and O, for the first two digits, so
A0000is 100000 andZ9999is 339999. CelesTrak's TLE feed carries no satellite above 99999 (that needs the OMM feed, #1191), so this is compatibility with Alpha-5 TLEs.src/sources/tle.jsdecodes the field through one shared helper:noradIdFromSatnum()for the satellites layer, which keys and deduplicates its catalog by that number, andtleCatalogNumber()for the satellite tools. Plain digits read as before (noradIdFromSatnum()accepts any length, as an OMM satrec carries the number unpadded); a malformed field — a sign, decimal point, hex, exponent, embedded space, stray or lowercase letter, or blank — is rejected rather than partly read, asNaNin the layer andnullin the tools. - CCTV estimated bearings: a camera whose pack marks
headingConfidence: 'low'(the id-hashfallbackHeadingFromIdbearing) showsHDG n° (ESTIMATED)in the HUD and draws its coverage wireframe dashed; colours, widths and active/idle emphasis are unchanged. A manual calibration (calSource: 'manual') or curated pose (poseSource: 'curated') is never presented as estimated, matching the CAL badge. Public camera state carriesheadingConfidenceandheadingEstimated(src/layers/cctv/headingConfidence.js, #639). - CCTV identified hardware: a camera whose source or seed names a hardware
modelthat resolves insrc/layers/cctv/modelSpecs.js(vendored CC0 records from the CCTV Camera Database) gets the model's specs appended to the HUD meta line. Only fixed single-lens hardware fills the FOV, and only when the source or seed states none and the pose is not curated, shown asFOV n° (DATASHEET)until a manual calibration moves it. PTZ, varifocal and multi-sensor datasheets publish a capability range, not the lens's current zoom, so those cameras keep their estimate and showSPEC FOV min–max° (CAPABILITY)beside it. Public camera state carriesfovSource,specFovMinDeg,specFovMaxDegandspec(#452). - FIRMS: no ground clamping (zero 3D-tiles height sampling), ≤18 screen-decluttered ambient labels, click-to-inspect detail card, 2.5k/3k sprite budgets viewport-clipped by FRP. Fire-power labels share display rounding before localization: one decimal below the rounded 10 MW boundary, then whole megawatts (for example, 9.96 MW reads 10 MW).
- CCTV v2 foundation: a pitched
frustum wireframe (4 corner rays + far-cap rectangle) with a monitor plane at the frustum's
far cap, retargeting the existing video/canvas texture pipeline. Stills are drawn at their source aspect, centered on the placeholder dark (
#01070c), instead of being stretched to the 16:9 canvas. Manual calibration only — auto-calibration and the drape mesh pipeline are deleted. A one-shot activation obstruction probe (pickFromRayon camera activation, clamping the plane short of the first hit) remains; ground placement is superseded by the shared-floor v3 behavior below. Calibration persists togodsEyeView.cctv.calibration.v2(wiped clean, no v1 import); a panel-only CAL badge showsCALIBRATED/CURATED/RAW PRIOR(no in-world tint). Panel is titled "CCTV" (not "CCTV MESH"). Staggered geometry/frame loading is active-first and uses 4 records per 120 ms normally, or 2 per 250 ms while tracking/cockpit owns the view (re-evaluated each batch), with coalesced progress notifications (roughly 300 ms or ten batches; natural completion and disable each publish their terminal state through their own completion paths) and a LOADING FRAMES chip and the preview-first auto-expanding panel are unchanged. Coverage polylines are created lazily instead of inserting five entities for every catalog camera during initialization: default COVERAGE ON enable creates the active/visible 14-camera cohort, and activation always creates the selected frustum even with COVERAGE OFF. Field validation passed 2026-07-04 (core look + downtown no-clip confirmed); that round fixed three findings: the ground clamp now lifts the cap center only so the wireframe stays a true pyramid welded to the plane (was a flattened fan / the ~47.5 m divergence — RESOLVED), re-selecting the active camera is a no-op (killed a click-flash), and texture swaps gate on canvas content (killed a periodic white flash). Camera icons are cyan for stills, violet for cameras with a video feed, and amber for the active camera. Coverage is now metro-wide: 250 cameras (CCTV_AUSTIN_MAX_SOURCESdefault 36 → 250, hard bound 300), filtered tocamera_status === TURNED_ON(~815 live of 1,003 rows). City packs (2026-07-04): Caltrans (districts 4/7/11/3 — SF, LA, San Diego, Sacramento; cap 300) and TfL London JamCams (cap 250) join Austin (cap 250) as keyless default sources. Ontario 511 (2026-09-12) adds highway cameras including Kitchener-area routes (cap 1,000, all enabled rows from the current ~944-camera catalog). The 511on.ca API now requires a developer key, so the pack needsONTARIO_511_API_KEYand is skipped without one (#1228). All sources are RAW PRIOR poses, and the layer is stills-first. sources. Fintraffic Finland road weather cameras (2026-09-13; cap 300,CCTV_FINTRAFFIC_MAX_SOURCES, kill switchCCTV_FINTRAFFIC_ENABLED=0) are the fourth pack: one keyless GeoJSON station list covering the whole country, where one preset (a station's fixed view) is one camera — 806 GATHERING stations carry 2,256 in-collection presets, prioritized to 300 against seven anchors on the main road spine.CCTV_MAX_SOURCESis a 4,000 catalog-wide ceiling shared round-robin across packs, so a lower global cap thins every region instead of starving the last pack. ~1,100 cameras total, all RAW PRIOR poses, stills-first. The catalog lives for 15 minutes; once it expires,/api/cctvkeeps answering from it while one background refresh reloads every pack, so only the very first load waits for the slowest pack (a live first load took about 22 s, and is bounded by Tarktee's 30 s). A background refresh that fails or comes back empty keeps the old catalog and is retried after another 15 minutes, not on the next request. Estonia's Tarktee DATEX feeds, which answer in 2 to 16.6 s, get 30 s (TARKTEE_FETCH_TIMEOUT_MS) where other packs get 15 s. Both Tarktee XML catalogs refuse redirects and have a 4 MiB per-document limit, enforced against declared length and streamed bytes. A rejected fetch, unsuccessful response, or body-read failure releases both available response bodies; read failures also cancel an active sibling reader. Cameras sharing an image number are deduplicated before the Tarktee pack cap is applied. - CCTV v3 UX — viewshed + calibration gizmo (built 2026-07-05 and field
validated 2026-07-21): the COVERAGE toggle is a
tri-state cycle
OFF → ON → VIEWSHED; viewshed mode renders each visible camera's frustum as a translucent color-coded volume (golden-angle hue per camera,cctvViewshed.js) welded to the same 5 points as the wireframe — zero new scene queries or update cadences. The 7 calibration sliders are deleted: ADJUST mode puts a direct-manipulation gizmo on the active camera (cctvGizmo.js— heading/pitch rings, E/N/U arrows, range handle at the cap center, FOV handles on the cap edges; all 7 offset DOF), plus a click-to-edit effective-pose readout (HDG/PITCH/FOV/RANGE/HGT/ΔN/ΔE, absolute values). Persistence is now save-gated: edits are live but unsaved (CAL · EDITEDchip) until SAVE CAL writes the v2 store. Do not add Translation arrows use a depth-test-free pickable tip so E/N/U ownership remains unambiguous even where shafts overlap other handles. Avoid hover effects that mutate gizmo polyline geometry (width) — the primitive rebuild blanks the pick buffer and eats the following click (root-caused 2026-07-05). Gizmo input checks the topmost, depth-test-free handle withscene.pickfirst and usesdrillPickonly as an overlap fallback; this keeps hover and press responsive on software GL without changing the real-GPU interaction. Frame serving is bounded independently from the 10-second active refresh: upstream and Street View attempts abort after 8 seconds, and the panel/monitor plane keep at most one same-camera image request in flight. This prevents a slow provider from being cancelled and restarted forever while staleSNAPSHOT · OKhealth remains beside a pending preview. Grounding is shared with every other height consumer: CCTV warms/resolvesgroundFloor.jscells, readscachedGroundFloor(), and delegates optional Google 3D refinement to the unchangedmeshFloorSampler.js. During E/N gizmo movement the prior floor is frozen (constant elevation and zero transient samples); release or reset makes one resolution request at the committed anchor. U edits remain pure geometry and enforce the 2 m minimum mount height above whichever shared floor wins, including a rooftop. Public camera state exposesgroundPriorMas the immutable Re:Earth ellipsoidal datum reference; it is kept separate from live frustum geometry because Google-3D can refine the rendered ground to the photogrammetric mesh. - CCTV citywide ambient cards (built 2026-07-29; shared-host migration
2026-08-02): the LOD-selected nearby static cameras (20/28/40 by zoom,
cctvLod.js) get screen-space thumbnail cards through the shared world-overlay host showing paced static frames — reselection oncamera.moveEndonly, at most one frame fetch per second layer-wide, per-source cadences (Austin 5 min, TfL/Caltrans 3 min, Fintraffic 10 min — its stations publish on a 600 s collection interval). Zero-flicker: a card renders nothing until its first frame, a drawn frame persists through failed fetches, and eviction grace (2-pass/5 s) stops budget-edge churn. Camera icons stay visible at every zoom. Eligible candidates are filtered to in-view stills with valid IDs, finite distances, and one deterministic representative per camera before ranking; videos, hidden/malformed rows, and duplicate outliers cannot alter the density scale or displace a valid winner. They are ranked with a deterministic 50/50 blend of eye distance and normalized screen-center offset before the existing 5×4 distribution pass, so the center wins contested density without removing peripheral coverage or changing the bounded count. The active camera keeps the v3 monitor plane and is excluded from the 40-card ambient ring with no thumbnail by default. An explicitactiveCameraCardEnabledpresentation option can publish the retained protected-card path. Disable tears the tier down completely. Coverage/viewshed semantics unchanged. - Satellites: orbit rings rotate about Earth's Z by ΔGMST every ~1s (exact inertial→ECEF compensation; no SGP4 re-runs); the tracked satellite propagates per frame with one shared epoch for dot/label/camera. Verified: ISS holds <1km perpendicular to its ring while tracked. Pass prediction (
src/data/satellitePass.js;getNextSatellitePass,getNextIssPass) scans at 20s, bisects rise/set to ~0.2s, fits the peak with a parabola, and marks a pass visible when the satellite is outside a cylindrical Earth shadow while the observer's Sun is at or below −6°.next_iss_passpreserves its geometric-pass ordering and appends visibility metadata. Generalnext_satellite_passaccepts a loaded NORAD ID or unambiguous name, searches 24 hours and optionally requires estimated visibility. These estimates do not account for weather or satellite brightness. - Space Missions (30d): recent launches render as bounded shared-host, horizon-occluded mission markers using Launch Library 2 v2.3 detailed records. Enabling the layer selects the unified right-side Context panel's Space Missions mode and enables the required satellite layer. Before applying its temporary dense/hidden Satellite mode, Space Missions snapshots the complete standalone Satellite parameter set and exact enabled-layer set. Disabling Space Missions from either Context or the left Data Layers rail restores those parameters and the exact prior enabled state, so a Satellite layer that was already on stays on while a mode-owned dependency returns off; enabling Satellites by itself remains independent. Selecting a mission isolates its launch-to-orbit transfer and dashed satellite orbit, fills that same panel with navigation/details, and animates a small phase-colored marker along the exact displayed Cartesian samples. Marker hit testing drill-picks through photorealistic tiles so the depth-test-free tactical dot remains reliably selectable; its text is non-interactive shared-host presentation. The selected pad is the camera's zoom pivot: the overview remains centered on its launch site, wheel zoom approaches that site instead of drifting elsewhere, and camera pitch progressively changes from global nadir to an oblique close 3D view. Its protected shared-host callout remains visible and expands to include both mission and launch-site names.
FOCUSflies directly to a 12 km oblique frame around the selected launch site and retains the same anchored zoom/orbit behavior.REPLAY ASCENTresets the selected marker at the pad, frames it from an oblique third-person angle, and follows it through the mission-specific compressed ascent directly into one orbital lap at the default1×rate. A live0.25×–4×slider changes ascent and orbit playback speed; adjusting it mid-replay preserves the current path position and historical mission timestamp. Re-entry/recovery cannot be inserted into replay. At orbit insertion the camera smoothly pulls back over the first fifth of the orbital replay and pitches to a globe-scale nadir view while continuing to target the moving replay point. Replay Cancel, mission navigation, deselection, layer disable, and data refresh all release camera ownership. Reconstructed paths are one continuous 128-sample geodetic curve: horizontal departure begins near zero while altitude rises quickly, then the climb progressively bends toward insertion without the former hard 120 km corner. Unmatched orbit fallbacks are smooth planar inclined rings rather than longitude/latitude ground-track curves. The panel lists disclosed payload names, types, operators/manufacturers, mass, multiplicity, and destination when supplied; an empty LL2 payload collection is shown asCLASSIFIED / MULTI-PAYLOAD. Launcher, spacecraft, and recoverable payload stages appear in a compact stage table with serial/flight/reuse details, recovery outcome/type, destination, and final coordinates when those records exist; an empty recovery collection omits the section. Stage recoveries with confirmed coordinates use those coordinates; return-to-launch-site records use the pad; downrange-only records receive an explicitly labeled estimated endpoint along the ascent azimuth. Available endpoints render as static 2 px dashed descent/recovery paths with a fixed final-position dot and an estimated atmospheric-interface segment when applicable. The ascent is geodetically densified above the ellipsoid toward the orbit's nearest insertion point, then rendered withArcType.NONE, so it neither cuts through Earth nor separates from the marker. Because LL2 does not normally supply continuous ascent telemetry, pad-to-insertion paths without upstream trajectory samples are labeledRECONSTRUCTED ESTIMATE/ASCENT ESTIMATE; only supplied trajectory samples receive the replay wording. Selected-orbit framing fits the complete ring, rear-side linework uses normal scene depth occlusion, and current distance, speed, and callout data come only from a launch-year-validated satellite match. Speed is the magnitude of the SGP4 inertial velocity vector at the same propagation epoch as position, displayed in km/s with km/h available as hover detail. Unavailable operator, site, launch-time, orbit, current-altitude, and speed values omit their detail rows instead of reserving panel space with placeholders. Newly launched payloads absent from the core operational groups use CelesTrak's cached active TLE feed as a lookup-only fallback; weak constellation-name matches are rejected. The replay callout maps compressed animation progress onto Launch Library's mission-relative timeline, showing the historical UTC date/time at the marker's current path position; unavailable timelines remain explicit. Matched live satellite positions propagate at one-second cadence and use a distinct green dot/callout with the current UTC date/time. When no mission is selected, the Context panel presents a scrollable newest-first roster of every launch in the rolling window, including the smaller 5 px operator-colored marker, provider, and launch date. Hovering or keyboard-focusing a row shows four compact cyan corner brackets on both that roster row and its corresponding globe dot, rotates the globe at the current zoom to center it, and gives its label declutter priority without selecting it; the globe label remains unbracketed. Selecting a roster row invokes the same mission isolation and full-orbit framing as clicking its globe marker. The replay vehicle is one screen-space SVG/CSS HUD overlay rather than separate Cesium billboard, label, and reticle graphics. It is hidden during ordinary Focus and manual close views, where the standard selected launch-site label remains visible, and exists only while ascent replay is active. Its fixed pixel scale is shared by ascent and insertion, so camera range never resizes the rocket on screen before the phase boundary. Generic Launch Library pad names are reduced to their identifying suffix, and replay timestamps use a cyan state title over unprefixed white UTC date/time values.REPLAY ASCENTholds the unframed cyan/white rocket at the pad for a real-timeT−10countdown, transitions throughLIFTOFF, and attaches six tapered cyan/white ellipse waves directly below it from liftoff through insertion to convey thrust without adding scene geometry. While replay is active, the single start button is replaced by compact Play, Pause, and Cancel icon controls. Pause freezes countdown or mission time, vehicle/stage positions, camera target, labels, and thrust-wave animation; Play resumes from that exact frame, and Cancel releases replay camera ownership. The rocket and thrust group rotates from the path's live screen-space tangent, so its nose follows the visible ascent curve while the adjacent text remains upright. The camera begins as a close oblique launch chase, then smoothly widens between roughly 120 and 420 km vehicle altitude into a higher oblique context view that keeps the moving rocket targeted while exposing the ascent bend and orbit connection. At insertion the rocket/thrust glyph is replaced by the fixed-size cyan orbit dot, which the camera follows through the existing globe-scale orbit pullback. The chase camera limits per-frame yaw changes across heading wraps so it cannot abruptly cross in front of the vehicle and make ascent read in reverse; the replay-speed slider affects mission playback but not countdown duration. Mission world text has no nativeLabelGraphics: overview launch markers publish at most 48 ambient candidates for a 24-winner budget, while selecting a mission clears that overview source and publishes its launch-site callout, stage re-entry annotations, live/estimated payload-position readout, and orbit annotation as protected selected-lane entries. The source retains the exact former strings and colors. Static anchors reuse the Cartesian values used to build their mission geometry; the moving payload entry reads the layer's per-frame live-position cache; catalog-backed orbit annotation positions are cached in the same one-second matrix update that realigns the orbit primitive. Keyhole edge fade, horizon culling, final collision placement, and UI exclusion are owned by the shared host. Deselect restores the bounded overview, and refresh, disable, and destroy replace or clear both mission sources. A selected mission renders its orbit as four repeating tactical sectors, each containing one prominent cyan dot followed by one hundred thin translucent dashes. The bright dots act as orbit anchors while the subdued dash field remains depth-tested against the globe and is shown only for the selected mission. Close selected-pad views add one static 500 m-radius cyan launch-zone ring with a low-opacity translucent fill over the sampled photoreal launch-site surface. The single scene primitive is created only for the visible selected site and is otherwise dormant. It appears during Focus, sufficiently close manual zoom, and the replay countdown, but is suppressed above 120 km camera altitude, beyond 180 km direct camera-to-pad range, for unselected missions, and whenever Space Missions is inactive. Focus establishes a launch-site-centered camera transform once; subsequent manual heading and pitch changes remain centered on that site without an automated per-frame correction. Surface mission markers and labels use an additional conservative globe-limb margin before the exact ellipsoid occluder boundary, preventing near-horizon visibility from alternating between frames. - AIS vessels: chevron symbology (naval cyan base, type tints), world-space headings, MMSI-keyed reconciliation (selection survives refreshes; pinned 3 refreshes with STALE marker when absent), detection-overlay integration (
type: 'SEA'), contextStore registration for voice Q&A. Empty-space clicks, id-less photorealistic-tile picks, and Escape dismiss the vessel card/HUD/context and clear its trail; picks owned by another layer (includinggev-trail:*) and raw vessel-record picks without a live MMSI key are no-ops for vessel selection. Click and key handlers detach while the layer is disabled and reinstall on enable. Selecting another vessel replaces the selection and trail, and reconciliation clears a trail if its owning vessel is evicted. - Track trails: server accumulates per-MMSI ring buffers (
/api/vessels/track?mmsi=, Float32+Uint32, 64 samples, 30s/25m thinning); aircraft backfill proxies/api/flights/track(OAuth, own credit bucket). Military aircraft keep only their locally accumulated trails:/api/military/trackvalidates the hex, then answers503 history_unavailablewithout an upstream request. - Shared
src/data/pickRegistry.jsstops the two flight layers' click handlers from fighting over the camera.
Optional Overpass configuration
Public Overpass instances are not used by default. OVERPASS_UPSTREAMS is a
server-only comma-separated list of HTTP(S) instances the operator runs or pays
for, resolved lazily after env loading. It replaces the empty default list;
there is no built-in fallback. Local/private instances are allowed. The stable
gods-eye-view/… User-Agent is unchanged. URLs never appear in response headers
or upstream errors, including old cached endpoint metadata.
Disk cache entries for both routes carry a SHA-256 tag of the configured
instance set (order does not matter) and are served only while it matches, so
repointing OVERPASS_UPSTREAMS refetches instead of serving another instance's
answers; untagged entries stop serving once an instance is configured.
The shared transport guards /api/overpass and /api/military-installations.
With no configured upstream, each route serves existing cache data (including
expired entries, marked stale with the original cache/retrieval date) without
refresh. An annotation miss returns HTTP 503, OVERPASS_NOT_CONFIGURED,
retryable:false; installations return that capability as HTTP 200 and select
the working vector-tile source without a failed-network console entry.
GET /api/overpass/status answers {configured}; the browser asks once per
page and, when nothing is configured, returns the unavailable result for
annotation and location-feature queries without sending them (so no 503 is
logged). An older server without the probe is queried as before. The probe
times out after 3 s; a failed or timed-out probe is retried after 30 s, and
queries in between go to the server. Each query stops waiting on the probe
when its own signal aborts.
Configured endpoints have per-instance cooldowns for 406/429, honoring
Retry-After; absent delays back off from 30 seconds to five minutes. Empty
elements is a valid answer. Existing byte, query, concurrency and cache caps
remain. Malformed capability-probe responses remain unknown and are retried after the existing 30-second backoff; valid configured decisions remain memoized.
Consumer capability misses never schedule automatic retries.
Keyless browser validation also probes the optional Google tiles-token route. The token client cancels rejected response bodies before returning unavailable, so they do not remain pending in browser network observation. The Overpass gate accepts only that same-origin route's expected 404 console diagnostic; other HTTP and JavaScript errors still fail acceptance.
Operator-configured Overpass area and focus-footprint queries use body-level
out geom (with center for focus), preserving relation member outlines.
Member geometry is printed only inside a box around the point (6 km for
neighbourhoods, 3 km for street areas, 1.5 km for focus footprints); a
relation cut by the box does not close and falls back to its pin.
Street Traffic and ALPR never use Overpass. Installations switch once to vector
tiles after a capability miss. Annotation and location-feature sources retain
a distinct unavailable result and stop querying for that source lifetime.
scripts/qa-overpass-offload.mjs <dev-url> checks Austin road dots, ALPR,
Camp Mabry markers/outlines, at most 12 Fort Cavazos installation markers,
an area annotation with zero console errors (and zero /api/overpass queries
when unconfigured), and zero browser requests to external Overpass/Nominatim hosts. It waits for
visible photoreal tilesets, dismisses first launch and measures at least 150
street-view dots within -3/+25 m of sampled mesh height, for both keyed and
keyless OpenFreeMap roads (an isolated page overrides only TomTom key availability).
Keyed Austin defaults to Hybrid (roadSource: TomTom + OpenStreetMap) with positive flow coverage.
It records milliseconds from enabling Street Traffic to first rendered dots at
2 km on a fresh page for each mode, plus warmed street-view timings and orbit screenshots;
src/overpassOffload.test.mjs separately proves server-handler zero egress.
All QA modes record per-view request counts, decoded response-body bytes,
transferred bytes and browser cache hits for OpenFreeMap TileJSON/tiles, ALPR
extract metadata/tiles and local /api/tomtom/* responses. --tour measures
Austin, London, Dubai, San Diego, Tokyo and São Paulo with all three layers on,
at 450 m and -35° pitch (São Paulo: 1250 m), then revisits Austin to measure
cache reuse. The tour disables camera collision adjustment and asserts the actual
latitude, longitude, altitude and pitch so remote terrain cannot shift a revisit.
--compare captures TomTom, OpenStreetMap and Hybrid at identical close tilted
photoreal views: Twin Peaks, Lombard Street, Loop 360, Dubai and Shibuya. The
mode order rotates per view so each mode meets cold caches somewhere. It records
first-dot latency (null where TomTom has no roads), projected dots,
surface-aligned dots (-3/+25 m), floaters, sinkers and missing samples, saving
qa-shots/compare-<view>-<mode>.png and overpass-offload-compare-result.json.
The comparison also clicks a real row chip and verifies its stored preference.
--pan runs 20 settled Austin moves: ten 150–300 m steps, five 1–2 km steps,
and five exact returns. It records OFM requests/bytes, flow tile requests, all
local API calls and dots per move, rejects repeat XYZ downloads and any new
OFM request on a return, and checks small-pan tiles against admitted bounds.
Its TomTom session ceiling is 32 browser flow requests (0.53% of the default
6,000/day upstream budget); cached server responses also count toward this
conservative session bound. --profile captures cold/warm Austin at 450 m,
-35 degrees with settled mesh. Development roads:* phase measures include
status, TileJSON, per-tile fetch/decode, parsing and surface sample counts/CPU
time, slice maxima and cache hits; no obsolete zero-valued parse-time sampling
aggregate is emitted. The six-city tour records first-dot and on-mesh counts
and asserts the 4 s cold / 2.5 s cached-return deadlines.
Reports go to qa-shots/overpass-offload-result.json and separate
acceptance/tour/pan/profile result files, with a console summary table. These are browser
session measurements; TomTom upstream usage still depends on the server cache
and budget. MB uses decimal bytes; transfer totals include response headers.
Configured Overpass responses still use overpassPayloadIsData() for cache
admission and stale fallback: refusals and body-level errors are never data.
Identical requests share a configured endpoint sequence and last-good fallback;
all configured endpoints retain the stable application User-Agent. HTTP refusals
rotate only among those configured endpoints, honoring their cooldowns.
Street Traffic reports OpenFreeMap road failures separately from TomTom flow:
OpenFreeMap tiles rate-limited, OpenFreeMap tiles timed out, or
OpenFreeMap tiles unavailable (HTTP 406). RoadRequestError retains a numeric
status (null when absent); malformed snapshots and network failures read
OpenFreeMap tiles unavailable, never HTTP undefined. Detailed-pass failures
retain the major roads and display the same upstream reason with reduced coverage.
The TomTom /api proxy still reports key unavailability (503), daily budget (429),
and unreachable upstream (502/504) separately from road geometry.
Share-link v2 layer state (August 2026)
- Generated share links use a deterministic v2 hash. Existing camera, visual, HUD, detection, post-processing, celestial, scope, and map-stack fields remain, with compact fields for enabled layers, allowlisted layer options, panel state, and the active preset's allowlisted shader controls. An absent layer field uses deterministic defaults; an explicit empty field means no enabled layers.
- The registry seals only after every production layer registers, and every
layer has an explicit serialization disposition. Unknown, empty, or repeated
enabled-layer tokens reject the whole layer payload (
l=alone is the valid explicit-empty form); unknown option tokens are ignored. Restoration settles independently per layer so one failed or unavailable source cannot block its siblings. - Layer tokens are permanent public compatibility identifiers. The reservation
JSON ledger at
src/data/layerStateTokenReservations.jsonpins all existing one-character assignments (all letters plus0,1and2on currentmain) even if a layer is later removed. New layers allocate the remaining unreserved single-character digits first (3through9on currentmain), then the next unreserved two-character base-36 token (00throughzz) after rebasing onto the merge-timemain; the dot-delimited v2 codec accepts both widths without changing existing links or the schema version. The contributor check reads the complete published JSON ledger fromorigin/main, including retired reservations, so later assignments cannot silently replace published ones. The pre-ledger base is accepted only at its known source blob; unknown historical shapes fail closed. Pull-request CI runs this check againstorigin/main. scripts/qa-layer-token-twochar.mjssimulates exhaustion of the earlier digit slots and injects a no-network00layer only into isolated dev-browser responses. It proves encoding, a cold-recipient reload, restoration, explicit OFF, and legacy-token compatibility. This is synthetic end-to-end evidence, not production allocation:00remains unreserved until the first eligible real layer receives it after the free digits through the normal allocation process and passes its own application share/reload check.- Stable visible options are limited to aircraft 3D mode, selected civilian and military flight IDs, Satellite catalog and selection, CCTV coverage/projection/ auto-hop, and Radio filter/volume. Playback and tuning, live-data health, calibration, caches, lifecycle state, temporary Context ownership, and derived effects are deliberately excluded. Radio restore never selects or plays a station.
- Normal loads restore the last successful explicit UI, voice, or tool choice from versioned local storage. Any valid camera share wins for the current load without overwriting recipient preferences. Restore ownership is split by visibility, option/selection, camera, visual, map, and individual panel lane: a newer explicit action supersedes only the field it owns. In particular, navigation cannot turn unrelated layers off, and an option change cannot cancel the same layer's visibility transition. Every explicit HUD, detection, post-processing, scope, or celestial action from the UI, keyboard, voice, or public tool facade claims the visual lane before mutation. Invalid requests do not claim that lane or partially change controls. Direct globe pointer and wheel gestures supersede the delayed shared camera and selected-subject Follow without aborting unrelated layer visibility or display-option restoration.
- The initial restore has one terminal promise spanning the camera flight,
visual/map/panel callback work, every production layer result, and the
destination-scoped selected-subject Follow result. Hash writes remain
suppressed and the startup screen continues to read
Restoring shared view...until that aggregate settles. Destroy settles it as destroyed rather than permitting late mutation. A superseded shared visibility intent follows the authoritative successor chain to a terminal lifecycle result, including a same-target re-enable or opposite-target disable, before releasing the layer barrier. Flights, Military, and Satellite first-update fetches consume the manager AbortSignal; disable and destroy also abort their module-owned feed or dense-catalog requests. - Only one Flights, Military, or Satellite tracking ID can be durable at once. Explicit selection clears the other families, Stop Tracking clears active and pending IDs, and ambiguous incoming multi-family selections fail closed rather than letting feed arrival order choose the camera owner.
- An explicit aircraft selection made inside Contacts promotes the owning Flights or Military layer from a mode-owned dependency into durable state, so leaving Context, reloading, or opening the link can restore it. Passive Contacts autofocus does not revoke a pending selected aircraft; the exact shared/local target wins when its feed row arrives.
- A shared Flights, Military, or Satellite subject that has not arrived yet
publishes a persistent top-center
ACQUIRINGprogress state while the existing deferred-restore latch and source-specific deadline remain active. Success, expiry/failure, cancellation, superseding intent, owner-layer disable, explicit navigation, and teardown all settle and clear that state; caller abort remains authoritative after the pending handoff. A latch that rejects its deferred selection emits only the terminal failure and never a false acquisition state. An unrelated manager failure preemptsACQUIRINGfor its full visible dwell; if acquisition is still owned afterward, the progress state resumes. A share-specific terminal failure that arrives while another failure is visible is queued, and its own fixed dwell starts only when that message reaches the screen. Only terminal failures use the existing fixed-dwell error presentation. - Radio category persistence shares the live directory's bounded normalizer,
including generated genre identifiers with spaces or
&such asHip HopandR&B. - Shared panel state starts from deterministic defaults and excludes responsive auto-collapse. Partial or malformed panel fields cannot import or overwrite recipient-local layout preferences.
- A fresh Cockpit entry temporarily collapses the standard left/right map panels and opens Cockpit's own Contact and Live Signals rails. This runs only on entry: Previous/Next preserves any panel the operator opens while already inside. Exit restores the exact standard-panel open/collapsed snapshot from before entry; Cockpit-only disclosure changes do not replace that map layout. Opening Data Layers while inside Cockpit temporarily collapses the Contact panel to prevent overlap. Closing Data Layers restores Contact only when that accordion action collapsed it; an operator's own Contact collapse remains authoritative. Voice selection of the nearest aircraft near a named place follows the requested-layer enable → location arrival → destination refresh → nearest airborne lookup → aircraft selection path, excludes on-ground records, and never enters Contacts or Cockpit unless either mode is named explicitly. The destination refresh also runs when the requested layer was already enabled. The lookup inspects the full loaded fleet and tracks by stable ICAO identity. That complete route is one atomic voice action, so Realtime sibling calls cannot race the nearest-aircraft query ahead of layer enablement. A healthy fallback feed is queried normally and its source is returned with the selection; fallback with no airborne records remains an honest no-data result, not an enable failure.
- Voice treats the parent Context panel and Contacts as separate intents. An
explicit request to open Context expands only
global-context-panel; it does not choose a mode. An explicit request to open Contacts expands that parent first, activates the Contacts sub-view, and returns the settled 250 km window. Itsaircraftcount is the exact civilian-plus-military total when both feeds can answer, orunknownwhen either component is unavailable. - Cockpit's top vision switch cycles all six map looks: Normal, CRT, NVG,
FLIR, Anime, and Noir. Entry selects the matching map style without adding
a duplicate carousel item. There is no empty
NONEentry.
Live AIS Vessels (June 2026)
- Server-side
wswebsocket towss://stream.aisstream.io/v0/streammaintained by Vite middleware;AISSTREAM_API_KEYnever reaches the browser (AISStream has no browser CORS). Thewspackage is used rather than Node's built-in WebSocket specifically because only it can hard-abort a wedged socket (see the watchdog note in the delta block at the top). - Browser polls same-origin
/api/vesselscache every 60s. - The first enable in a session starts one 30-second client grace timer. Until
an accepted vessel position arrives,
live/open/connectingtransport reportsLOADING; the timer is not restarted by the 60-second poll. Expiry or a definitive transport/credential failure reportsUNAVAILABLE. Accepted warm vessels survive later zero-position refreshes as stale/degraded data, while disable/re-enable owns a new timer and superseded responses remain inert. - Client render cap
VITE_AIS_LIVE_MAX_ROWS(default 12,000); type-colored ship icons (tanker/cargo/passenger/fishing/tug); screen-space label clustering caps active labels atVITE_AIS_LIVE_LABEL_MAX_ROWS(default 900). - Click-to-inspect wired into the voice context store.
Voice Control (June 2026)
GEV MIC button (bottom UI) starts an OpenAI Realtime session over WebRTC:
- Token flow: browser fetches a short-lived client secret from
/api/realtime/token; the Vite middleware usesOPENAI_API_KEYby default, or the signed-in local ChatGPT/Codex OAuth access token when OAuth was explicitly selected in Provider Settings, and posts the full session config (instructions, tool schemas, VAD, truncation) toapi.openai.com/v1/realtime/client_secrets. Both long-lived credentials stay server-side; OAuth minting is loopback-only. SDP exchange goes directly toapi.openai.com/v1/realtime/callswith the ephemeral token. - Session defaults (env-tunable): model
gpt-realtime-2(orgpt-realtime-2.1-miniwhen the MINI tier is selected — see the model-tier entry below), voicemarin, reasoning effortlow, semantic VAD with low eagerness, no response interruption, context window truncated to ~3,000 post-instruction tokens with 0.5 retention ratio — the conversational window stays short because map state is fetched live per turn. - Thirty tools (argument schemas in
src/voice/actionSchemas.js, served with their descriptions byserver/providers/openai/tools.js, executed client-side insrc/voice/gevActions.js):fly_to_location,select_nearest_aircraft,adjust_camera_zoom,zoom_to_globe,set_layer_visibility,show_data_layers_menu,set_panel_open,set_visual_style,get_entity_context,get_current_view_state,set_hud,set_cyber_sonar,set_detection,set_map_stack,set_post_processing,control_scene,control_cctv,set_context_mode,control_cockpit,control_radio,track_entity,stop_tracking,frame_overhead,annotate_map,clear_annotations,move_camera,fly_route,analyst_query,next_iss_pass, andnext_satellite_pass.
Reading
npm testtotals: the count depends on the Node major. The two GC-bracketed allocation microbenchmarks (src/data/focusAllocations.test.mjs= 1 test,src/overlays/worldOverlayAllocation.test.mjs= 13) only RUN on the calibrated Node 24 runtime; on any other major the runner skips both files and their 14 tests are absent from the total. A branch total quoted without its Node version is therefore not reproducible. As of the fly_route cinematic branch: 2,281 on Node 25.6.1 (allocation suites skipped) = 2,255 onmain+ 26 route pins; the same tree on Node 24 reports 2,295.
- Camera verbs (
src/cameraVerbs.js) — one motion slot, driven per clock tick.move_cameraorbits/pans/tilts/rotates;fly_routeis a cinematic dolly along an existing route annotation.rotate_globerotates the world-space camera rigidly around Earth's center using its screen-up axis, preserving geocentric range and framing. Left/right describe visible globe motion, whilerotateturns the camera in place. Omitted globe direction/mode default to right/continuous; explicit values prevail. Other camera motions keep their once default. Admission requires 3D, no Cockpit/tracking owner and the whole globe inside the existing keyhole circle, using the shared keyhole/projection geometry and vertical camera FOV with a rotation-local bounding-sphere cone for off-axis containment. Canvas-edge clipping alone does not block a rigid rotation or its unchanged Globe View preset. There is no altitude cutoff. An already smaller, fully framed globe rotates directly; repeated nudges, direction and speed changes recheck current framing without demanding a reset merely because ellipsoid altitude varied. A close-up or overlap refuses without moving the camera. When the unchanged Globe View preset fits, voice says “Globe rotation isn’t available from this view. You can say ‘Reset to Globe View and rotate the globe.’” without asking a permission question or automatically resetting. Explicit Reset/Show Globe View commands remain direct; an explicit reset-and-rotate request waits for reset success before requesting rotation. If OpenAI dispatches both tools together, rotation waits for earlier same-response resets and is skipped if any fails or is cancelled. OpenAI rejects later tool calls in the same response after a globe refusal, including calls arriving while globe admission is pending, and preserves response-scopedtool_choice: nonefor the refusal follow-up; Gemini rejects further tool execution, including queued sibling calls, until fresh typed input or admitted speech. Model interruption, advisory captions and microphone toggles do not release that guard. There is no pending Yes/No offer. Outside the refused response, explicit-command interpretation and avoiding repeated resets remain model-guided; a distinct model-issued reset call is not an app-enforced proof of user intent. A failed standalone reset can still be retried by the model, and a reset result does not guarantee the camera has fully settled. Before suggesting a reset, a cloned camera checks the unchanged Globe View destination against the same keyhole/depth rules without moving the live camera. If that preset cannot fit,globe-reset-unavailableasks for further zoom and the refusal instructs further zoom instead of a reset; this also covers ultrawide views. Unsupported view geometry or non-WGS84 projection assumptions fail closed without the closer-zoom instruction. Supported camera inputs/navigation intents cancel rotation before their action. Finite radius greater than the ellipsoid's maximum radius proves whole-arc clearance at constant centre-distance; no low-altitude swept solver is needed. The unchanged Globe View reset preset remains 18,000,000 m. Terrain and buildings remain outside the guarantee.- The route dolly is shaped, not linear (2026-08-20). A trapezoid speed profile (smoothstep up, cruise, smoothstep down, distance taken as the closed-form integral) eases both ends without changing the pace — duration is still
totalM / ROUTE_M_S[speed], with one exception: a 0.5 s minimum keeps a degenerate route from being an instant teleport, so routes under 10 m (slow) / 20 m (normal) / 45 m (fast) fly SLOWER than the speed word, never faster. Turns bank up to 10° (a 90° street corner settles near 7.5°), measured as a triangular pulse over a 4 s window centred on the camera so the roll leads in and unwinds after. Altitude breathes ±20 m around the 260 m mean and lifts up to 26 m into turns. Pitch is LOCKED at −32°; heading comes from a gaze that leads the path by 6.5 s of travel.prefers-reduced-motionzeroes the roll and the altitude shaping and keeps the easing. - Invariants — do not "fix" these. (1) Every release levels the roll: completion unwinds it through the ease-out envelope, and
interruptCameraMotionzeroes it synchronously (heading/pitch/position preserved). Cesium keeps the last up vector it was handed, so skipping this leaves the user holding a tilted horizon. (2) Heading is interpolated as an ANGLE about the local up, never as a Cartesian lerp — a lerp cannot cross an antipodal pair, so an out-and-back route looked backwards for the entire return leg;signedTurnRadbranches the exact-180° case deterministically because the cross product's sign there is a floating-point coin toss. (3) A COLD floor cell is missing data, not flat ground: route vertices carry height 0, so trusting them flew a mountain corridor at 260 m above the ELLIPSOID. The corridor warm is fire-and-forget, so the dolly ARMS — camera untouched, no teleport onto the route — for up to 1.2 s waiting for real floor data, falls back to a rendered-mesh probe (scene.sampleHeight) when the DEM stays cold — latched to ONE firing per flight, for exactly the cells the cache could not answer, so a route costs at most 8sampleHeightcalls however long it arms — and otherwise holds the launch altitude for the whole route. A corridor is only RESOLVED when every cell is accounted for: one warm cell says nothing about the ground under the other seven, and treating it as an answer let the dolly descend to 460 m over a 1,600 m rendered surface. The probe reads the rendered surface at the current LOD (rooftops and primitives included) — a better estimate than nothing, not a guaranteed upper bound. Never descend blind. The floor is SMOOTHED in both directions for the ride while the hard clearance clamp reads the RAW sample, so a cell boundary cannot pop the eye but a cliff is still cleared on the frame it is seen. A pre-departure floor is adopted whole; one arriving mid-flight is eased onto, and the eye's descent rate is capped. - Pins:
src/routeCinematics.test.mjs(26 tests), withscripts/qa-flyroute-mutations.mjsreverting each fix individually to prove they are load-bearing (19 named defects); rendered proofscripts/qa-flyroute-cinema.mjs(drives the real voice runner, measures the real Cesium camera every frame, writes a labelled contact sheet).
- The route dolly is shaped, not linear (2026-08-20). A trapezoid speed profile (smoothstep up, cruise, smoothstep down, distance taken as the closed-form integral) eases both ends without changing the pace — duration is still
- Public control facade on StyleManager (
setHudVisible/setHudLayout/setDetection/setMapStack/setBloom/setSharpen/setOrbit/setCleanView/getControlStateplusrunImmediateNavigation): every setter syncs DOM sliders + share links + scene snapshots and returns{ok, ...state}— voice confirms only what actually happened. Validatedmove_camera,fly_route,frame_overhead, strongest-fire focus, and entity tracking use the shared navigation transaction, which refuses Cockpit before mutation, advances authority, releases follow owners, cancels stale work, and only then starts the requested action. All four entity layers exposefindByQuery/getNearby/getAllPositions/trackById|selectById/stopTracking/getTrackedInfo. - Scene context (
get_entity_context): selected entity from the context store, or visible entities ranked by distance to the view target (≤100km altitude); plus basemap context — view-target picking, 7-point viewport sampling, view-scale classification (global/continental/regional/metro/city/local), reverse geocoding (center ≤750km, viewport samples ≤3,000km), Google Places Nearby via/api/google/nearby-places(≤25km), known-landmark matching againstCITY_POIS, coarse country inference fallback. Context assembly is capped at 1.5s with cache fallbacks; caches are deduped in-flight.- Every selectable layer writes that one slot (2026-08-21). The tracking layers — flights, military, satellites — publish selection on their own awareness lane (
gev:awareness-subject-selected, consumed by the readout card and the Contacts panel) and for the life of the voice tools never wrote the shared slot, soscope:'selected'silently answeredin_viewwith a contact plainly selected on screen. They now callselectTrackedSubjectContext/refreshTrackedSubjectContext/clearTrackedSubjectContext(src/data/contextStore.js) on select / poll / deselect. Invariants: (a) that write path must NOT dispatchgev:entity-selected— tracking layers already own an event lane and a second one makes two surfaces fight over one subject; (b) exactly one record per tracking layer, because a frozen snapshot of a moved contact must never reach the visible-entity scan; (c) precedence is recency, not layer — one slot, last selection wins, so clicking an overlay entity supersedes a tracked plane as the voice subject while the plane stays tracked. Satellites refresh on the 1 s propagation beat, not per frame. - Context-mode vocabulary is symmetric (2026-08-21).
set_context_modeacceptscontacts; the internal id isflights. Every model-readable field (mode,entering,priorMode, nestedcontext/contextRollback, and the transition diagnostic text) is reported in the accepted vocabulary, with the internal id preserved as<field>Internal; an absent secondary mode staysnullrather than claiming to beoff. Mapping lives insrc/contextModePolicy.jsso UI text and voice payloads cannot drift. Reporting the internal id made the model readmode:'flights'as "Contacts is off" and refuse to answer from thecontactsWindowcounts in the same payload. - Analyst → track handoff carries a key, not just a label (2026-08-21).
analyst_queryitems includeicao24/mmsialongside the displayid, and contact lookup uses the shared tiered ranking insrc/data/contactMatch.js: hex exact → callsign exact → registration exact → callsign prefix → registration prefix → callsign substring → registration substring. The tiers keep an exact callsign ahead of a colliding registration regardless of feed order, and registrations compare separator-insensitively (G-ABCD/GABCD,05-8152/058152,N123AB/N-123AB). - A typed command supersedes the turn it interrupts (2026-08-21; hardened 2026-10-17).
sendTextCommanddefers itsresponse.createbehind an active response instead of colliding with it, marks that response superseded so a late function call from it is refused rather than dispatched, and drops the old turn's queued follow-up. A refused call is still ANSWERED — a terminal{ok:false, superseded:true}function_call_output— because an unansweredfunction_callstrands a pending call and deadlocks the model; the refusal creates no response of its own. A burst of typed commands coalesces into one response while keeping both conversation items. If the local channel rejects the user item, that command requests no response and adds no deferred answer; earlier accepted deferred input remains queued. Interruption and pointer preparation still happen before the send attempt. - Subject reconciliation across satellite catalog rebuilds (2026-08-21). A dense↔core toggle or TLE refresh clears and repopulates the catalog, so the published subject is re-resolved against the new satrec; a subject that did NOT survive releases the slot. The per-frame refresh cannot do this itself —
_getTrackedFramePositionreturns early once the satellite has no catalog entry — so the reconcile runs at rebuild completion. An empty catalog is a rebuild in flight, not a disappearance. - One aircraft-proximity snapshot (2026-08-22; hardened 2026-09-30).
collectAircraftProximityWindow(src/data/militaryAwareness.js) computes the Contacts panel cohort once. The voice analyst reads the exact retained snapshot and evaluation timestamp instead of re-scanning live layers, so the panel readout and spoken count for one centre cannot cross snapshots. Invariant: do not re-derive a proximity count anywhere else. Each layer retains 20,000 rows and samples one row beyond the cap; a saturated cohort reportscomplete:false,truncated:true, and is spoken as a lower bound. They diverged before because the panel read live billboard positions while the analyst used last-fix coordinates over a 2,000-record slice — 111 on screen, 15 spoken. Explicit regions, arbitrary points, and a radius different from the panel radius deliberately keep the general record engine. Entity-centred results carrywindow: {engine:'contacts-window', centeredOn, radiusKm, flights, military, aircraft, complete, evaluatedAt}. - Centre precedence for nearby asks: explicit place in the question > Contacts subject (a selected non-contact entity never silently becomes the centre) > an entity the user names > the current view, said aloud. Contacts active with no subject uses the view rather than reading an empty panel.
- Contact-match ties break on hex ascending.
track_entityis a mutation fulfilling "follow that one", and the model's observed answer to a non-ok track result is to retry with guesses rather than ask, so the lookup always commits rather than returning an ambiguity. What it owes the caller is stability: hex is unique and always present, so the same query resolves to the same contact for as long as both are loaded.
- Every selectable layer writes that one slot (2026-08-21). The tracking layers — flights, military, satellites — publish selection on their own awareness lane (
- Visual grounding: at
localview scale with no structured identity, the client captures the Cesium canvas (≤1200px JPEG, black-frame detection, double-render for freshness) and sends it asinput_imagewith a strict "do not invent labels" instruction. - Context window: only the latest viewport screenshot stays in context — the client deletes the prior image item (
conversation.item.delete) before adding a new one (images are the most expensive item, re-billed every turn). Text history is bounded by the server-sidetruncation: { type: 'retention_ratio', retention_ratio, token_limits.post_instructions }set in/api/realtime/token(cache-friendly batched truncation). There is intentionally no client-side per-turn conversation-item cap — deleting from the front of history each turn busts the Realtime prompt cache. A spatially-aware summarize-and-prune policy is specced for a future iteration. - Model tier + spend guard (
src/voice/voiceCost.js, August 2026): hosts without the voice panel keep theSTD/MINItoggle and running session-cost readout (~$0.42) in the heading. Panel hosts place these controls in Voice Settings and mirror warning, capped or unknown cost states in the heading.- Tier selection.
standard=gpt-realtime-2(default),mini=gpt-realtime-2.1-mini(~3× cheaper per audio token). The client sends?tier=to/api/realtime/token; the endpoint resolves it through the shared registry, so an unknown, empty, or hostile value falls back tostandardrather than reaching OpenAI as a model id. Responses echoX-GEV-Voice-Tier/X-GEV-Voice-Model(plusX-GEV-Voice-Tier-Fallback: 1when a bogus tier was downgraded). Persisted atgodsEyeView.voiceCost.tier. - Applies NEXT session. The model is fixed when the ephemeral token is minted, so a live session always keeps the model it connected with; toggling mid-session only records the preference (the button title says so). The cost tracker's lifetime is the session's lifetime and its model binding is immutable from
start()tostop()— rebuilding it on toggle would erase accrued spend and let repeated toggles bypass the cap. The tracker may only be replaced once the session is FULLY SETTLED (isVoiceSessionSettled(): not active and no data channel and no peer connection) —!isActive()alone is not enough, because theerrorstatus reports inactive while the transport can still deliver a lateresponse.done. The toggle itself reads and writes only the persisted preference, never the live tracker. - Env overrides.
OPENAI_REALTIME_MODEL/OPENAI_REALTIME_MODEL_MINIremain authoritative per tier, so a drifted upstream model id is a.envfix rather than a code change. Because an override can point a tier at any model, the client prices against the model id the server actually echoed, not the tier it requested. An unrecognised id is billed at the most expensive known rates plus one console warning — under-metering is what lets a cap be overrun. - Metering. Token usage from each
response.doneis folded into a per-session estimate. Cached tokens are subtracted from their modality totals; any aggregate-minus-details residual (and any payload with no detail at all) is attributed to audio rates, so uncertainty always resolves upward. - Thresholds (one object, persisted at
godsEyeView.voiceCost.limits): soft warning at $2 — amber readout plus exactly one console line; hard cap at $5 — the session ends through the ordinary stop path (data channel and peer connection closed, mic tracks stopped) and the readout readsSession ended — cost cap.0/negative disables a threshold and round-trips through storage as an'off'sentinel (rawInfinitywould JSON-serialize tonulland silently restore the default); a corrupt entry falls back to the defaults rather than disarming the cap. - Cap semantics — in-flight tools COMPLETE and are NOT rolled back. A session-ending latch (
isSessionEnding()) is checked at the tool-dispatch site, so no new tool is dispatched once the cap trips.extractFunctionCallsyields at most one call per event, so that single check covers the whole batch. A tool already executing may still finish its map mutation (a camera flight, a layer toggle, an annotation). This is deliberate: unwinding a partially applied map change has no safe general implementation, and a half-reverted camera/layer/annotation state is worse than a completed one. - In-flight response at teardown → the accounting is INCOMPLETE. Usage only arrives with
response.done, which never comes for a response cut off by teardown (stop()closes the peer connection, and the server cancels rather than completes it). Rather than invent a token count for it, the tracker is markedincomplete: the chip shows a trailing*(~$1.00*) as a see-note mark and the tooltip carries the reason. Deliberately not presented as a lower bound — the estimate can also run high (residuals and unrecognised models bill at worst-case rates, and sub-cent totals round up), so it is partial rather than directional. (A bounded teardown drain was tried and removed:pc.close()closes the data channels a drain would listen on, so it was structurally dead.) - ⚠️ Model ids and rates are external facts read from OpenAI's model + pricing pages on 2026-08-18 and marked VERIFY-AT-RELEASE in
voiceCost.js.
- Tier selection.
- Reliability: tool-call dedupe (2.5s window across call/item/args keys); response-create queueing that respects active responses and defers follow-ups when the user starts speaking; per-tool follow-up instructions so the agent confirms only what actually happened (zoom confirms only on
ok=true). - Aircraft identity honesty: “What is this aircraft?” reads callsign, operator, registration, type, and route only from the selected contact context. Missing operator, route, or type enrichment is named explicitly rather than silently omitted or inferred from the callsign.
- Diagnostics: every client/server event is posted to
/api/realtime/debug-logand appended to.gev-logs/realtime-conversations.jsonl(gitignored) with secret/image redaction; last 30 errors persist inlocalStorage(gev-realtime-errors);window.__gevVoiceCommands.getDiagnostics()in the console. - Counting semantics ("near") — a CONTRACT; new count-bearing tool work inherits it. Three honest numbers exist for one question: the Contacts cohort (250 km around the subject, what the panel shows),
analyst_query's count of currently-loaded records for the requested scope, and the layer-wide loaded total incoverage.layersQueried[].total. They diverge legitimately — the flights layer loads by viewport, so after a camera dive the loaded set can hold a fraction of the cohort (field case: panel 42, analyst 8). The contract:- Contacts ACTIVE → "near / nearby / how many aircraft" means the Contacts window — the panel's numbers, spoken verbatim. Mechanism:
contactsWindow({centeredOn, radiusKm, flights, military, vessels, complete}), carried by bothanalyst_queryandget_current_view_state, derived bycontactsWindowFromSnapshot()from the same snapshot the panel renders so the two cannot drift. Voice filtering and follow-up memory use that immutable snapshot too. A cohort whose feed cannot answer reports'unknown', never a confident zero; a cohort at the 20,000-row retention bound reports a lower bound. - Contacts OFF → "nearby" means in view; "near <place>" means a radius around that place. A radius query with Contacts active and no explicit centre is centred on the active contact, not the camera.
- Every count names its scope in words — "42 in your window", "8 in view", "about 30 within 250 km of Austin" — never a bare number.
analyst_queryreturnsscopeLabelso this is mechanical. Two different numbers with named scopes are not a contradiction. - The loaded-data caveat is stated once when relevant: counts cover loaded data, and the flights layer loads where you look (appended to
coverage.notefor radius/view scopes over viewport-loaded layers).
- Contacts ACTIVE → "near / nearby / how many aircraft" means the Contacts window — the panel's numbers, spoken verbatim. Mechanism:
- Degradation: API-key mode without
OPENAI_API_KEYreturns 503 as before. Selecting OAuth without a usable local ChatGPT/Codex sign-in starts the localcodex loginbrowser flow, polls for the resulting local token, and selects OAuth when sign-in completes. If the Codex login flow cannot start or does not complete, the rest of the app is unaffected. - Login lifecycle and storage: one pending Codex login is shared by the login and status routes. Process failure, success without readable credentials, and a two-minute timeout each report a clear failure; timeout and server shutdown cancel the owned process. The launcher resolves
CODEX_HOMEbefore changing directories and refuses a mismatchedCODEX_AUTH_JSON. GEV reads file credentials only, never writes or refreshes them, and does not require the vendor refresh token. Expired access tokens reportCODEX_OAUTH_REAUTH_REQUIRED. OAuth status and login routes also use the same-site request gate. - Auth button focus: changing voice auth updates the existing controls in place. Enter, Space, and mouse selection keep focus on the auth button; Tab then follows the normal control order. Pending sign-in blocks repeated activation without removing the button from the focus order. Completion does not take focus from another control or discard text in the key fields.
scripts/qa-voice-auth-focus.mjschecks these paths in Chromium with fixture sign-in responses. - Support boundary: the local OAuth option is experimental. OpenAI's documented Sign in with ChatGPT plan-sharing inference endpoint is
/v1/responses; support for third-party reuse of Codex credentials at/v1/realtime/client_secretsremains unconfirmed. Technical success does not establish a supported auth or billing contract.
AI HUD Summary (June 2026)
- HUD
SUMMARYreadout requests a five-word intelligence-style summary from/api/openai/hud-summary(modelOPENAI_HUD_SUMMARY_MODEL, defaultgpt-5-nano, minimal reasoning). - Input is the live basemap label context (place/street/nearby-place labels + enabled layers) — the model is instructed not to infer from coordinates.
- Output is sanitized to exactly five words; falls back to the deterministic telemetry summary on error/timeout (5s abort); typewriter animation on update.
- A summary asked for before the camera starts moving never paints over the next view:
moveStartmarks the summary dirty and bumps its revision, asmoveEnddoes, aborts the request in flight and stops a reply still typing; neither a reply nor a fallback line (on an error, the 5 s timeout, the abort, or a failed context build) paints once the revision has moved. A dropped request forgets its signature and a timeout counts as a failure, so the next 15 s tick asks again even over the same place. An interrupted typewriter reply also forgets its signature; fully displayed answers remain reusable.
Place-search providers
Coordinate input requires exactly two nonempty components; extra comma or semicolon separators decline to geocoding rather than silently choosing a location.
Location search/fly-to, annotations and Radio location lookup receive one
placeSearch.geocode(query, { bias, signal }) service. src/standalone composes
two offline providers first — a strict decimal-degree/geo-URI coordinate reader and an
exact-match reader over the bundled city and landmark names — then Google when
configured and Photon/OpenStreetMap as fallback, including Google transport
failures or declined requests. The offline pair answer with no request and no
key, and decline everything they are not certain of: a coordinate must be a
decimal-degree pair (either order when N/S/E/W fix the axes), or a two-dimensional
WGS84 geo:latitude,longitude URI with an optional ;crs=wgs84. URI scheme and
CRS names are case-insensitive; internal whitespace, altitude, uncertainty, other
parameters, query strings and fragments decline rather than silently dropping
information. For example, geo:39.7392,-104.9903 resolves offline to the same exact
point, decimal label and framing box as 39.7392, -104.9903. A bundled name
must match exactly, so 12junk, 34oops and austin, tx both reach the
geocoders instead of being answered from the bundle. Degrees/minutes/seconds and
grid references are not parsed. An answer either provider gives is exact, so
nearby-landmark recovery stands aside for it — otherwise a typed coordinate
could be replaced by whatever Places finds near the current view. The portable ./search export
Google first when configured, Photon/OpenStreetMap as fallback (including Google
transport failures or declined requests), and the local /api/geocode route
over Nominatim as a last resort when neither answers. That route keeps to the
public instance's usage policy: an identifying User-Agent and Referer, at
most one request per second shared with the cockpit's reverse lookups, answers
cached for five minutes, one upstream call shared between identical searches
already in flight, a queue bounded at four waiting searches (a burst past it is
refused with 429 and Retry-After, never held), and a queued search dropped
once its caller has given up rather than spending a slot on an answer nobody
will read. A hit whose coordinates or bounding box are missing or out of range
is discarded rather than coerced. The portable ./search export
provides the service and adapters; it reads no environment or application state.
Existing browser/server key setup is unchanged.
Providers normalize coordinates, canonical name, label, place types and optional bounds. Camera framing, nearby landmark recovery and footprint selection remain in their consumers, including the Capitol identity/containment safeguards. Radio keeps localized country names in labels rather than station filters. Nearby/text-search endpoints retain their existing behavior.
Google geocoding is a server route, not a browser call (#363). Forward and
reverse lookups go to /api/google/geocode and /api/google/reverse-geocode,
which carry GOOGLE_MAPS_SERVER_API_KEY (falling back to GOOGLE_MAPS_API_KEY)
and share the Places routes' opt-in GEV_RATELIMIT_GOOGLE_PER_MIN throttle.
Google's Geocoding web service refuses referrer-restricted keys, so the
previous browser call forced the bundled browser key — visible in devtools by
design — to be left unrestricted; it now needs only the Map Tiles API and the
Maps JavaScript API (Street View). Both routes answer in Google's own
status/results shape rather than the Places routes' places: [] contract,
because the browser adapters normalize that shape directly. The browser key still gates the provider client-side, so a keyless
session spends no request to learn it has no Google: the route's own keyless
answer reads as an unanswered provider, which would stop the chain caching the
negative Photon or Nominatim then produces. Its query is capped at 200
characters, its reverse coordinates are validated against WGS84 bounds before
the request is paid for, and a malformed viewport bias is dropped rather than
refused, since a bias only ranks results.
Only valid answers and definitive misses enter bounded caches; malformed replies, HTTP refusals and outages remain retryable. Searches share a 12-second total deadline, with Photon requests capped at six seconds each. Caller/application cancellation stops retries and late cache writes. Replacing a location search cancels the previous lookup; disposing its controls cancels the active lookup. Photon uses a soft proximity bias, up to five candidates, and an unbiased retry for name mismatches. A "place, region" query prefers a candidate named exactly for the place whose city, county, state, country or country code (or USA/UK) contains every word of the region, and a settlement or administrative area over a POI when both fit. Nearby results that fail that check are retried unbiased. After the region checks of both passes, a nearby place whose whole name is the query (one with a comma in it) comes next, then the unbiased name match. Invalid or wrapped bounds returned by providers are omitted rather than framing the wrong part of the globe.
A location search that finds nothing, or fails, leaves the field open with the
query still in it, focused and selected, so it can be corrected and resubmitted
without reopening search. The miss is shown under the field as a persistent
role="alert" message ("Location not found." or "Search failed." with what to
do next) referenced by the input's aria-describedby, with aria-invalid set;
it is not a toast. Typing, a new search, or closing the emptied field clears it.
A search that arrives still clears and closes the field.
For request viewport bias, Photon unwraps antimeridian-crossing longitude bounds before computing their midpoint, then wraps that midpoint back into ±180 degrees. Malformed viewport bounds add no proximity bias. Nominatim omits a crossing request viewbox and continues searching without that geographic preference.
Map Stack Switcher (June 2026)
src/mapStackController.jsswitches between Google Photorealistic 3D (photoreal, the default when a Google or ion key is present), keyless Esri World Imagery (the zero-key default landing, with keyless terrain), Bing Aerial / Aerial-with-Labels via Cesium ion world imagery (requireCESIUM_ION_TOKEN), and OSM tile fallback. Bing Road is retired: it is gone fromMAP_STACKS, from theset_map_stackenum, and from the voice aliases (road phrasings now resolve to OSM, the one shipped road basemap). An oldmap=bing-roadlink is simply an unknown id and takessetStack()'s existing photoreal fallback with the Google 3D tile lit — pinned live inscripts/qa-map-source-tray.mjs.- The bottom Visual Presets tray presents a five-tile MAP SOURCE row (
#map-stack-chips,src/mapStackChips.js): Google 3D, Esri Satellite, Bing Aerial, Bing Labels, and OSM. The duplicate left#stack-panelis retired. The five tiles share one row on desktop and two rows on narrow screens, carryaria-pressedon the active source, and remain keyboard-reachable with a visible focus outline. - The lit tile follows controller state, not the click: a rejected switch (no ion token) or a superseded one (rapid A→B) leaves the genuinely active source lit, and the tray heading keeps its short-label status readout (
...while switching, amber onlastError). - Ion stacks remain visible and keyboard-focusable when no ion token is configured, but expose
aria-disabled="true"and do not switch. Their accessible label and tooltip quotegetStacks().unavailableReason— the same stringsetStack()puts in the toast. OSM works keyless. TheIONbadge is gated on the stack's ownrequiresIon, so aphotorealchip unavailable because the Google tileset failed says so instead of falsely demanding an ion token. - Stack choice participates in share links (
src/sharelink.js) and falls back to the best available stack when the requested one is unavailable (keyless boots land on Esri; OSM takes over automatically if Esri is unreachable; while that construction fallback is on screen Esri is retried in the background, 30 s doubling to a 10-minute cap with each attempt abandoned after 60 s, and replaces OSM once it loads. The switch to OSM after repeated Esri tile failures is not retried; selecting Esri again tries it). Share-link restore, theset_map_stackvoice tool, and the chip row all land on the same_setMapStack()path.
Voice Map Whiteboard / Annotations (June 2026)
- Runtime entry:
src/main.jscallsinitAnnotations({ viewer, tileset }), exposeswindow.__gevAnnotations, and passes the engine into the voice action runner. - Engine contract:
src/annotations/annotationEngine.jsowns annotation state, TTL/fade lifecycle, concurrent anchor resolution, duplicate detection keyed on geometry, cancellation on clear/newer generations, and a hard cap of 120 live marks. Deferred outline upgrades drain FIFO at concurrency 2; queued work retains the owning abort controller and is discarded on a generation change before it can fetch. - Resolver contract:
src/annotations/annotationResolver.jsconverts names/coords/screen pixels into world anchors and optional geometry. The resolver is type-aware: Google Geocode/Places gives a centroid + scope, operator-configured OSM/Overpass supplies admin/place/footprint/street/enclosing-area geometry, route requests use/api/route, and ambiguous/far results are rejected or recovered near the current view instead of drawing misleading blobs. Only explicitly country/state/county-scoped asks bypass near-view recovery and proximity gating; state scope requires a leadingstate of …/the state of …phrase, while bare names, proper names ending in “State,” and administrative geocode result types alone remain guarded. Overpass throttles remain distinct from normal transients:Retry-Afteris honored for one retry, and a repeated throttle ends only that mark's outline upgrade. - With no detailed-feature capability, annotations retain their pins and say "Detailed outline unavailable"; location focus reports the same result without retrying. Missing capability never synthesizes an outline from a radius. Manual geometry, bundled neighborhoods, Natural Earth regions and bundled state/province/county outlines remain.
- The shared default place search resolves unqualified
Alps,the Alpsand theAlps mountainsforms from the bundled European Natural Earth range before network geocoders. Navigation uses its contained anchor and region viewport; annotations still require the existing proximity and outline-containment checks. Coordinates and supplied presets keep priority; geographic qualifiers, local POI names, other ranges and custom geocoder arrays retain their existing resolution. A failed Alps pack load reports a search failure rather than caching an unrelated peak as the range, and the bundled loader retains its normal retry cooldown. - County queries rank eligible US counties and international admin-1 units together, retaining exact aliases and country/state qualifiers.
- Bundled administrative outlines (
src/data/adminBoundaries.js): states and provinces worldwide (Natural Earth admin-1,src/data/local_data/natural_earth/states_provinces.json) and US counties and county equivalents (US Census Bureau,src/data/local_data/us_census_counties/counties.json), both public domain and rebuilt byscripts/build-admin-packs.mjs. An area ask whose words name a unit ("Texas", "TX", "State of Texas", "Bavaria", "Travis County, Texas", "Orleans Parish") is drawn from the pack before any geocode: no network request, no proximity gate, no outline pending. Same-named units go to the state/country qualifier, then the one under or nearest the camera ("Washington County"); every qualifier must place the unit ("Travis County Courthouse, Texas" is not the county). A bare name answers alone only for a prominent unit (a US state, a Canadian province, Bavaria) or a city-state (Berlin, Vienna), and never when it is also a country or a city ("Georgia", "New York", "Santa Fe", "Victoria") or a generic word ("North"); those, and smaller units ("Kent", "Long Island"), are left to the geocoder. When the geocoder types the place as a state or county, the unit carrying the name and containing the geocoded point is drawn from the pack before any Overpass query (a US county first, so "New York County" is Manhattan). Outlines keep every part and hole (anno.polygons: Hawaii's islands, Berlin inside Brandenburg). Each pack loads on the first ask that needs it, never at start-up; the analyst region lookup uses the same packs. Gate:scripts/qa-admin-outlines.mjs. - Renderer contract:
src/annotations/hybridAnnotationRenderer.jsroutes drapedarea/routegeometry to world-space Cesium rendering and reticles/pins/arrows/callouts to the screen-space SVG renderer. Area labels are screen-space callouts so all captions share one visual language; progressive outline upgrades convert the existing screen group in place when the anchor snaps to the resolved centroid. - Tooling: voice has
annotate_mapandclear_annotationstools. Annotations accumulate and persist by default; clearing is explicit only. Partial failures, approximate synthesized zones, and route fallbacks are returned as structured tool results so the voice layer can be honest. - Console/dev API:
window.__gevAnnotations.tour(),.demo(),.annotate(),.clear(),.count(), and.list()are the deterministic no-mic test surface. - Current known resolver gap: mall/lifestyle districts such as "The Domain, Austin" can prefer a named building over the broader retail envelope. Product decision is that districts should become envelope + key buildings, but the scoring change still needs a careful multi-case validation pass.
Manual Whiteboard Drawing (September 2026)
- DISPLAY ▸ Draw turns the globe into a whiteboard drawn by hand. Pick a shape (Area, Line, Pin), click the vertices on the real world, type an optional label and pick a colour, and finish. Keyboard contract, live only while Draw is on and never while you are typing in another field: Enter finishes the shape (as does a double-click, and the two cannot both submit it — the session is replaced before the annotate call is awaited); Backspace removes the last vertex; Esc cancels the shape in progress, and a second Esc — Esc on an empty session — leaves draw mode. Typing in the label field keeps Backspace; Enter there also finishes. Clear wipes the board (the shape in progress and every placed mark), so removing marks does not require the console or the microphone.
- Runtime entry:
createApplicationTools(src/app/tools.js, behind thesrc/standalone/tools.jsshim) callsinitDrawTool({ viewer, annotations })afterinitAnnotationsand defers itsdestroy(), so disposing the shell returns every DOM listener, the Cesium handler, the preview data source, the borrowed click actions and thewindow.__gevDrawToolhandle. Its markup is a DISPLAY-rail component template (src/ui/templates/display-controls.html), assembled intoindex.htmlat build time. That handle (setActive,setShape,addVertex(lon, lat),finish,cancel,clearAll,diagnostics) is the no-mouse test seam. - Contract: a finished shape is submitted through the SAME
annotationEngine.annotate()the voice agent uses, with geometry supplied andmanual: true(type: area+ring,type: route+path,type: pin+ coordinates).resolveSpecshort-circuits manual specs (resolveManualSpec) so no geocoder, Places or Overpass call is made. A drawn area is a real (not synthesized) outline and drapes solid; a drawn line is aroutewithmode: 'manual', labelled with its length and never a travel time; a pin is a point. Drawn marks therefore render with the whiteboard look, persist, de-dup, appear in.list(), and clear withclear_annotations/.clear(). - A vertex is a lon/lat, and the mark is DRAPED. Clicks are picked through
pickWorldFromScreen— one of two scene helpers the annotation resolver factory (src/annotations/resolver.js) exposes alongsidesampleGroundHeight, so the app has ONE depth-aware pick cascade rather than a second written beside it — which returns the height the pick landed on as well as the coordinate, so the live preview hangs on the roof or hillside under the pointer instead of sinking to sea level. The world renderer draws areas and routes withclampToGround, so the height is dropped at finish in one place (finishSpec) rather than being carried downstream and discarded silently: what you get is the outline you drew, lying on the surface beneath it. - Draped marks classify onto
ClassificationType.BOTH— terrain AND 3D tiles — not onto 3D tiles alone. With onlyCESIUM_3D_TILE, every area fill, area outline, route and arrow rendered to nothing on a keyless boot, where Cesium's own globe carries Esri/OSM imagery and there are no tiles to classify against; the labels appeared and the geometry did not. That hit spoken annotations exactly as hard as hand-drawn ones.scripts/qa-draw-tool.mjscounts the mark's own colour in the rendered frame before and after a shape is finished, on a keyed AND a keyless server, so "the board says there is a mark" can no longer pass for "the mark is on screen". - A finished area is a CLOSED ring. The fill polygon closes itself but the outline beside it is a polyline, so an open ring drew every edge except the one back to the first vertex — a triangle with two sides.
finishSpecrepeats the first position at the end. A line is never closed: joining its ends would invent a segment nobody drew. - Drawn-line distance labels select metres or kilometres after rounding to the nearest metre, so a 999.5-metre line displays as
1.0 kmrather than1000 m. - Measurements and centres are computed on unwrapped longitudes (
unwrapLongitudes/wrapLongitudeindrawMode.js, used byringAreaM2,ringCentroidand the engine's manual-area anchor). A ~50,000 m² rectangle straddling the antimeridian reads its 0.002° width as 359.998° under raw arithmetic: it measured thousands of square kilometres and anchored on the Greenwich meridian, half a world from the shape. Path length was already safe — haversine is periodic in the longitude delta — and a test pins that so a "fix" cannot break it. - Bounds. One shape holds at most
MAX_VERTICES= 512 points; two clicks within 0.5 m are one vertex (that is what makes a double-click finish without leaving a stray point); a vertex that is not a finite on-globe coordinate is refused. A shape that encloses nothing is refused rather than placed: three collinear points are not an area and a line under a metre long is not a line, and the hint says which. - Pointer ownership: while a session is open the tool holds the single pointer claim (
claimPointer('draw'),src/data/inputOwnership.js) and every ambient scene selection handler — the shared tracking click gesture behind flights, military and CCTV, plus vessels, satellites, launches, fires, installations, bikeshare, radio, submarine cables, the bundled GeoJSON layers and the CCTV calibration gizmo — returns before picking. A vertex over an aircraft is a vertex, not a selection. - The claim is a LEASE.
claimPointer(owner)returns an opaque token ornull;releasePointer(lease)takes that token. A second claim fails even under the same owner name, because two live instances of one tool — an old one mid-teardown and its replacement — are two owners; releasing by name let the old instance free the claim its successor was holding, and every layer started selecting through the live session's vertices. A superseded lease now releases nothing. - Cesium's own click actions are borrowed too. The Viewer binds a selecting LEFT_CLICK and a tracking LEFT_DOUBLE_CLICK on its own handler, outside every layer and therefore outside the shared claim — guarding the layers alone still left a drawn vertex selecting the entity under it. Both are removed for the session and restored, as the same function objects, when it ends or the tool is destroyed.
- Enter belongs to whatever has the keyboard. The finish key is taken only from the canvas, the page body and the tool's own label field. With a control focused — Clear, a shape button — Enter activates that control, as a keyboard user expects; it does not quietly finish the shape.
- Pure half:
src/annotations/drawMode.js(session, minimum vertices, double-click de-dup at 0.5 m, vertex ceiling, degenerate-geometry refusal, ring closing, dateline-safe length/area/centroid, hint text). Regression surface:src/annotations/drawMode.test.mjsandsrc/data/inputOwnership.test.mjs(pure),src/annotations/drawToolBehaviour.test.mjs(the tool driven against DOM and viewer doubles — leases, borrowed input actions, teardown, the Enter key, preview heights),src/annotations/drawTool.test.mjs(structural: where files live, which ids the markup carries, what the docs may claim), and the rendered harnessscripts/qa-draw-tool.mjs. - The annotation tree is a declared ownership group:
./annotations→src/annotations/index.jsinpackage.json, with its module graph listed inscripts/package-boundaries.json, so an annotation module reaching outside that graph is a gate failure rather than a surprise in a layer's bundle.drawMode.js,drawTool.jsand their tests are inscripts/format-scope.json.
S2 GhostMaps (October 2026)
- What it shows. Events ▸ S2 GhostMaps draws the current edition of S2 Underground's KMZ intel products from
github.com/s2underground/GhostMaps. The products are the Common Intelligence Picture and the 30-day kinetic pull (both on by default), the Border Crisis Map, the Iran Common Intelligence Picture, Iran Kinetic Events, Yemen FLOT, and Latest Drop: the newest dated incident file outside those series,0_Archive,BasedataandReference Material. Each product has a chip in the row. The chip choice is kept inlocalStorage(gev.ghostmaps.products) and falls back to the defaults when storage is blocked. - Editions.
src/layers/ghostMaps/catalog.js(pure) matches whole repository paths per product. The newest edition wins by the date written in the file name, then byExport_N/vN. Undated names, such as the fixed 30-day file, are identified by blob sha. Files over 50 MiB are left out (the 62 MB census KMZ, for example). A renamed folder drops the product rather than matching elsewhere. - Updates. The repository is not ours, so there is no webhook.
server/providers/ghostMaps.jsstarts with the first/api/ghostmapsrequest, so an install that never enables the layer never contacts GitHub.- It reads the branch head from git's smart-HTTP ref advertisement (
/info/refs?service=git-upload-pack, about 0.5 KB, outside the REST rate limit) everyGHOSTMAPS_POLL_SECONDS(15 s by default, 5 s minimum). Failures back off from 15 s to 5 min. - Only a new head costs REST calls: one recursive tree and one commit. An optional
GITHUB_TOKENraises the REST limit. - After an upstream outage, a successful branch-head check clears the error and pushes a recovered catalog even when the commit is unchanged. Cached products stay in place; subsequent healthy unchanged checks stay silent.
- The new catalog goes to
.gev-cache/ghostmaps/catalog.jsonand to every open page over server-sent events (/api/ghostmaps/events, 25 s heartbeat, at most 64 subscribers). Default products are downloaded at once. - The layer reloads only products whose blob changed. The new edition's data source is added before the old one is removed. A product that leaves the catalog leaves the globe.
- The row reads
LIVEwhile the stream is open. A five-minute catalog read is the backstop.
- It reads the branch head from git's smart-HTTP ref advertisement (
- Files.
/api/ghostmaps/file/{product}serves only catalog products. Files are downloaded by commit-pinned raw URLs, so a CDN cannot return an older edition. They are cached on disk by blob sha, and blobs no current product uses are pruned. A request whose?v=names the current blob isimmutable. Repository paths never reach the page. - Source ownership. The application catalog receives
sources['s2-ghostmaps']through the shared source contracts; an omitted or null source stays unavailable without opening a stream or fetching. The standalone source factory supplies the same-origin implementation and accepts replacements. Packed-image URLs are released on cancelled or failed product construction, and remain owned by a successfully loaded product until it is removed. - Voice and locales. S2 can be toggled through its registered id and aliases. The model already sees the S2 GhostMaps brand in the id, and compact alias-hint separators keep the complete voice prompt within its existing 52,000-character limit; accepted aliases are unchanged. The row and reports are translated in Spanish, Russian and Simplified Chinese.
- Rendering. The page parses the KMZ with the import module's zip, KMZ and XML helpers into one
CustomDataSourceper product.- Points become ground-clamped billboards with the depth test off, so they never sink into terrain or 3D tiles. Because the Google 3D map hides the globe, nothing else would stop far-side pins drawing through the planet. Pins are therefore horizon-culled with Street Level's
createHorizonCullbefore their first frame and again whenever the camera moves. - Lines and polygon outlines are ground-clamped polylines.
- Styles, StyleMaps and inline styles are honoured. Icons come only from images packed in the same KMZ and at most 512 KB. Anything else uses a per-product round marker.
- Network links, overlays, models and remote hrefs are never followed. Hidden placemarks are skipped. Bounds are 20,000 placemarks and 250,000 positions per product.
- Points become ground-clamped billboards with the depth test off, so they never sink into terrain or 3D tiles. Because the Google 3D map hides the globe, nothing else would stop far-side pins drawing through the planet. Pins are therefore horizon-culled with Street Level's
- Reports. Clicking a marker opens a card with the product, folder, edition date and the placemark's report fields.
src/layers/ghostMaps/records.jsreads ArcGIS popup tables as text only:<Null>and internal fields are dropped and values are capped. Description markup is never inserted into the page. The firsthttp(s)research link opens from the card. The card isstateless, so it comes and goes with the click, as CCTV cards do. It is alsohorizonCulled, so it hides when its marker turns behind the planet. Picks are registered unders2-ghostmaps. - Validation. Unit tests:
src/layers/ghostMaps/*.test.mjscover editions, the catalog, ref parsing, description tables and wrapping;server/providers/ghostMaps.test.mjscovers commit detection, events, pinned downloads, cache pruning, restart and routing.node scripts/qa-ghostmaps.mjs --url http://localhost:4173covers the live load, the open stream, picks and cards (asserted on the overlay host's diagnostics, since thedesynchronizedoverlay canvas can read back a stale buffer headless), far-side culling, a synthetic push, chip persistence, disable and phone width.
Import Your Own Files (October 2026)
- DISPLAY ▸ Import reads GeoJSON (
.geojson/.json), KML, KMZ and GPX from the picker or a file drop anywhere in the app. Each file gets show/hide, Fly-to and Remove controls. GeoJSON and GPX use the row's color; KML/KMZ retain their styles and show a hatched swatch. The file bytes remain on the device for the page session. Rendered overlays can appear in map screenshots sent through voice or other existing screenshot tools. - Ground and height. GeoJSON XYZ coordinates become ground footprints. KML ground lines/rings are tessellated; static recorded tracks preserve their original vertices, segment gaps and explicit altitude modes. KML with absolute/relative altitude keeps that meaning. Ground pins resolve in batches of 256 against loaded terrain and, in the nearby camera footprint on photoreal maps,
clampToHeightMostDetailed, then draw in one owned native BillboardCollection per file, with static positions and no per-frame point Entity visualizer or clamping callbacks. Point metadata remains indataSource.importedEntitiesfor framing/style inspection; non-point geometry stays in the normal source collection. Pin collections mount only after source admission, follow file show/hide, and are destroyed on removal. Settled camera/map-source changes and completed terrain loads refresh visible files through the same serial queue; distant points retain their latest available height. No detailed worldwide terrain sweep is started. Nearby mesh sampling can add requests/time; configured photoreal providers require separate validation. The depth test is disabled for ground pins, matching Draw's marker visibility policy. - Navigation. Row Fly-to uses
runImmediateNavigation('file', …); Cockpit can refuse it and existing follow/track ownership is released first. Automatic Fly-to is stamped when a batch is submitted and reasserted before flying. Pointer/wheel gestures and newer navigation supersede it, including time spent in the queue. Framing uses every finite visible point/path/polygon-hole/wall vertex plus rotated overlay bounds, with viewport-aware range and a nadir view. The target uses available terrain/cached ground height and the ground guard on arrival. Empty footprints never fall back to deferredviewer.flyTo. - Clock. Imports expose no source clock and no entity availability. KML times/view metadata are removed before loading; recorded tracks become static lines. GPX segments remain separate and GPX times are ignored. The shared clock stays unchanged.
- Input handling. Coordinates and GeoJSON geometry types are checked before Cesium allocation. Only short plain names reach GeoJSON property bags; file IDs, custom CRS and styling properties are removed. KML feature/geometry IDs are removed while Style/StyleMap references remain. Local style references are trimmed, incomplete style pairs removed, hidden-folder flags normalized to the import's show control, and unsupported prototype-named elements removed. UTF-16 BOMs and common KML namespace export defects are handled. Empty drawable content is refused. A loader failure or timeout does not hold the next file indefinitely; teardown aborts pending waits.
- References and images. File-authored remote links/icons/styles, network links, models and screen/photo overlays are removed. Packed KMZ and embedded PNG/JPEG/GIF/WebP/BMP images remain local; decoded dimensions are checked before image decoding against a total 16-megapixel and 8,192-pixel-side budget per file. Blocked IconStyle images fall back to local pins; authored empty icons remain empty. Colored overlays without an Icon survive; overlays with a removed external image are dropped. Metadata, native labels and descriptions are removed, and generated pins share one lossless image URL per canvas.
- Bounds. File size ≤50 MiB; inflated KMZ ≤120 MiB; ≤20,000 entities (including multipart children and Folder containers); ≤250,000 positions; ≤2,000 normalized style definitions and distinct image links; ≤12 files per session. Exact repeated styles share definitions without changing inheritance or inline overrides. Files of 20–30 MB fit the byte budget, subject to geometry/image limits. Refusals name a reason and batches continue; with multiple failures the hint shows the first reason.
- Lifecycle and keyboard.
createApplicationToolswiresinitGeoFileImportwith viewer, navigation, ground services and map-source subscription. Destroy aborts pending work, unsubscribes, removes data sources, revokes blob URLs, removes document/canvas listeners and releaseswindow.__gevGeoImport. List rebuilds retain keyboard focus; removing a row hands focus to the next row, else the previous row, else Import. - Validation.
node scripts/qa-geo-import.mjscovers all four formats and real painted show/hide. Runnode scripts/qa-geo-import-regressions.mjsagainst both dev and preview for XYZ/track heights, failure recovery, namespace/BOM support, portrait framing and panel drops.node scripts/qa-geo-import-large.mjs [--file /absolute/path/sample.kml] [--expect-local-pins]checks a 27.6 MB repeated-style file, native style/image equivalence, large-file painted output and frame times. It records the WebGL renderer; software-rendered runs do not establish GPU performance. Unit tests live beside the data/controller/framing modules.
Recent Imagery (September 2026)
CMR pagination counts raw rows when checking page completion and the record budget. A catalog is truncated when upstream hits exceed the rows examined or the record cap drops valid granules from the last page; discarded malformed rows do not imply unseen imagery.
Programmatic boxes passed to the exported setBox and searchHls operations
reject missing, blank or boolean edges instead of interpreting them as zero
coordinates. Valid finite numeric edges remain accepted; the app’s drag tool,
pins and share links already supply complete boxes.
- The layer sits in the Data Layers panel's Cameras group beside Cameras (CCTV); Mapped ALPR Cameras moved to the Infrastructure group to make room.
- DATA ▸ Recent Imagery (
recent-imagery,src/layers/recentImagery/) lists the last 30 UTC days of imagery for a selected box and drapes the days you pick: HLS S30 (Sentinel-2) and L30 (Landsat 8/9) at 30 m, and the VIIRS NOAA-21 daily overview at 250 m. Keyless and browser-direct: NASA CMR for which days have HLS granules in the box (catalog.js, both collections concurrently, paged withCMR-Search-Afterup to 2,000 records each,truncatedagainst CMRhits), GIBS WMTS for tiles, Worldview Snapshots for thumbnails and exports. Worldview'sData-Presentheader is the only availability signal: GIBS answers an empty tile with HTTP 200, and the JPEG overview would drape black, so a day confirmed empty never drapes. - Box. SELECT BOX (
src/ui/imageryBoxTool.js) claims the pointer like Draw, borrows the stock click actions and freezes the camera only mid-drag; USE VIEW takes the camera rectangle;boxFromPinAtgrows a 10 km box. Boxes must be ≤ 1,000 km a side, on one side of the dateline and inside the Web-Mercator limit (validateBox). A refusal (boxError, including the tool's own viareportBoxRefusal) stays at the top of the panel until a box succeeds; a view over the cap is refused with its size, never shrunk. An oversized box or view (Box is … km wide · limit 1,000 km,View is … wide · …) sets the hint toZoom in or draw a smaller boxand enables ZOOM IN:zoomToFit()flies top-down (1.2 s) to the refused box's centre, or the ground under the canvas centre for a view, at the height whose view is about 400 km wide for the canvas aspect and the camera's vertical FOV (fitViewHeightM, clamped 5–400 km), and returns that height. - Modes, pins and the preview. The mode is explicit:
image(one pinned day, slota),basemap(that day on the left of a swipe, the basemap on the right: slotasplits LEFT and there is no second layer) andab(dayaleft, daybright).setAssignment(slot, key | null)/toggleAssignment(slot, key)pin and unpin; a day is in one slot at most (pinning it in the other slot moves it) and an empty day cannot be pinned.setMode()keeps both pins;bonly drapes inab. While the mode's first empty slot exists the focused day previews in it: a click at once, arrow keys after 250 ms of quiet (FOCUS_DEBOUNCE_MS, one timer, last day wins); an unprobed day is followed and previews when its probe says present, an empty or unknown day never drapes and the previous preview stays. A pin change drops the preview until focus moves again, and unpinning takes that layer off at once.rankLatestpicks START HERE coverage first (a present HLS day whose footprints cover the box, or have none, beats a newer sliver), then newest with all granules ≤ 20% known cloud, then newest; slivers only aspartial; else the newest non-empty VIIRS day; it is focused and previewed after each search when nothing is pinned. Escape clears the preview (clearPreview()), then cancels the box tool, then blurs — with SELECT BOX armed too, through the tool'sonEscapeinterceptor wired increateApplicationTools. Pins, mode, box and split changes made in the panel are published withadoptLayerParams, never while a share link is being applied. - Rendering.
rendering.jsowns at most two GIBS layers on the hostsrc/maps/imageryHost.jsresolves:viewer.imageryLayerswhile the globe shows, the photoreal tileset'simageryLayerswhile it is hidden, or none (the panel says so). Layers are rebuilt on a new host, never re-parented.MapSourceController.subscribe()fires after every settled stack switch (silent switches and fallbacks included) and the layer rebinds on it. Tile requests pass a six-slot semaphore of our own (Cesium's global limits are untouched). UnderrequestRenderModea deferred tile needs a later frame, but a deferral must never request one itself or a stalled tile keeps the scene rendering every frame: a request that finds the semaphore full asks for nothing (the settling request asks for one frame), and a tile Cesium itself deferred goes through one coalesced 250 ms retry. Thumbnails: three fetches at once, sixteen decoded images, least-recently-used where agetread is never a use; eviction keeps the probe result. The strip's visible window is probed around the focused card; the pinned and previewed days are probed wherever they sit, at the focused card's priority, so a pin restored or assigned off-screen still drapes. - Esri surface and the swipe. While any image is on the map (a preview included) the layer holds the map lease (
acquireImageryComparison, ownerrecent-imagery,esri-if-photoreal): on Google 3D the map switches to Esri, because 30 m pixels over 3D buildings do not read, and Cesium's split only works on the globe. The lease is handed back, and Google 3D restored unless the operator changed maps meanwhile, when the last image leaves the map or the layer is disabled; the notice line says so while it holds. While a day is shown the imagery keeps Esri: a manual switch that lands on Google 3D is answered at once with a new lease under the same policy (keepEsri), the notice readsImagery stays on Esri · CLEAR to use Google 3D, and the divider never suspends because of a map switch. It re-leases once per switch generation and only for a manual one (MapSourceController#getSwitchOrigin()ismanualfor an outsidesetStack,automaticfor the controller's own fallback or recovery), never for the lease's own switch and never while releasing, so CLEAR and disable still return to Google 3D; a lease another owner holds stays refused. When Esri fails and the controller falls back to Google 3D on its own, the layer lets the lease go without restoring anything, both days drape on the tileset without a swipe and the guidance readsEsri map unavailable · no swipe; only the next manual switch to Google 3D takes Esri again, so a failing Esri map is never retried in a loop. Switching between globe stacks (Esri, OSM, Bing) only rebinds. On a tileset host both images drape without a swipe. The split control (createImagerySplit, labels A / B, or IMAGE / BASEMAP) exists only while a swipe is live, recentres on every new comparison (not while scrubbing the second day), except once for a split restored from a link. There is no keyboard shortcut (Space is the app's push-to-talk): SWAP (#ri-swap, in the controls row, its slot reserved and the button disabled while no swipe is live) trades the two sides, A to the right and B to the left, or the image to the right of the basemap, and a second click restores; the hint readsDrag the divider · SWAP trades sides. The swap is not in the share link, and every new comparison starts unswapped. One opacity applies to both drapes. Another owner holding the lease is reported and the images still drape. - Panel.
#recent-imagery-panelis static rail markup insrc/ui/templates/context.htmlbeside#weather-panel, registered inPanelLayoutController,PanelChromeand the share panel state (uitokeni), hidden while the layer is off and opened on first appearance unless a stored or shared collapse choice exists (dataset.collapsedPreference).LayerPanel.attachRecentImagery(factory)mounts the readout (createRecentImageryPanelinsrc/ui/recentImagery.js) into itsdata-rail-scrollerbody, as the weather readout is mounted. The body is a fixed stack in which no block changes height and no control moves in any state:#ri-actions(SELECT BOX · USE VIEW · CLEAR, rail-card actions),#ri-notice(one line:#ri-notice-textwith the refusal, error, CLEAR notice or the Esri note, and#ri-zoom-inin a fixed slot at its right end, reserved unless an oversized box or view can be fitted),#ri-strip(day cards newest first: thumbnail, date, sensor, cloud, START HERE, PREVIEW, and a SHOW chip or A and B chips; ← → Home End move, S / A / B pin, Enter previews),#ri-hint(one line: the next step),#ri-selection(#ri-modeIMAGE · VS BASEMAP · A / B, then#ri-slot-aand#ri-slot-bwith#ri-unpin-a/#ri-unpin-bkept in place when empty),#ri-controls(#ri-opacity, EXPORT / EXPORT A and EXPORT B) and#ri-details, a collapsed rail card holding the focused day's acquisition, coverage and granules, catalog notes, the empty-days toggle and the imagery credit. Absent controls are reserved (invisible), never removed. Every render preserves the body'sscrollTop; opening DETAILS then scrolls its card into view once. - Share links. Layer options (token
1): box edges as integers at degrees × 100000, pinsaandbas compact days (S20260918),mmode (0 image, 1 basemap, 2 A / B),psplit percent (share-link only: never stored locally) andvfor the daily overview. Row chips:MORE DETAIL · 30 m(on) andDAILY OVERVIEW · 250 m(off). - Regression surface: the colocated
*.test.mjsbeside each module (shared doubles insrc/layers/recentImagery/testDoubles.mjs;src/ui/recentImagery.test.mjsincludes a DOM-structure guard: every control keeps its place in the tree, nothing is hidden, and every fixed-height block keeps the class the stylesheet fixes its height by; it has no layout, so it cannot see movement),src/ui/layerPanel.test.mjs(mount),src/maps/controller.test.mjs(subscribe),src/data/layerState.test.mjs(codec),src/sharelink.celestial.test.mjs(panel token), and the live gatescripts/qa-recent-imagery.mjs, which also measures every control's rect, relative to the panel, across the transitions within 1 px; that measurement is the real layout-shift check.QA_RECENT_IMAGERY_SLOW_RENDER=1gives loaded software renderers a 4 s zoom deadline and an 8-sample stability tail; the default stays 2 s and 12 samples.
3D Aircraft + Tracking (June 2026)
- The TRACKED contact's 2D↔3D handoff is DEFAULT behaviour (2026-08-19), driven by camera distance alone. It does NOT consult the DISPLAY-rail
3Dtoggle, which continues to own the FLEET (the un-instanced draw-call budget stays the operator's decision). Policy lives insrc/data/trackedModelRegime.jsand is shared by both layers: enter belowTRACKED_MODEL_ENTER_ALT_M= 150,000 m and hand back to the billboard only aboveTRACKED_MODEL_EXIT_ALT_M= 172,500 m. The swap distance was set by playtesting on 2026-08-20: a first pass at 1,000,000 m switched too early; 2D reads correctly at ~600 km and the handoff belongs at ~150 km. Consequence, recorded on purpose: the tracked contact now enters 3D NEARER than the FLEET does (MODEL_ALT_CEIL_M= 800,000 m, unchanged), so with the DISPLAY-rail3Dtoggle on, 150–800 km draws surrounding contacts as models while the selected one is still a glyph. Nothing double-draws (the fleet pass skips the tracked icao) and aligning the two is a fleet-side decision, deliberately out of scope. The two thresholds are asymmetric on purpose: a single threshold makes a tracked orbit sitting ON the boundary strobe billboard↔model as the camera's altitude wobbles across it. Do not collapse them. The latch is scoped per selection, so a new target re-evaluates against the ENTER ceiling rather than inheriting the previous target's exit band. Exactly ONE model is involved; it loads on demand when the regime opens, is HIDDEN (not released) on regime exit so re-entry has no load gap, and is released by the existing teardown on deselect/re-track/destroy. Cockpit and TR-3B suppression are unchanged. Two invariants around it: (a) the hysteresis latch AND the load-failure latch are per-selection state cleared by_resetTrackedSelectionState()in the tracking lifecycle (deselect / re-track / cross-layer / init / destroy) — the predicate's icao-change guard is defence only, since it needs a drawn frame while nothing is selected and the render governor's idle mode does not promise one; (b) on-demand loading is bounded at 3 attempts per selection with a 1.5 s backoff and one console warning naming the asset — the driver runs everyscene.preUpdate, so an unbounded catch means a missing GLB spins load→reject at frame rate. The billboard stays the visual throughout a failed load. - Grounded 3D handoff is terrain-validity gated (2026-08-23). A ready civilian or military glTF does not own the visual until
groundSnap(src/data/groundSnap.js) can answer with a MEASURED photoreal-surface height. On success the layer writesheight + the model's measured belly offsetbefore revealing the model. Cache movement is measured on the WGS84 surface, not across altitude, so a stationary contact keeps its snap through poll-time vertical-datum changes. Model existence or GPU readiness alone never suppresses the billboard floor, and ordinary zoom/style/deselect transitions preserve a valid snap cache. Two states, and the difference is the whole design. COLD — nothing has ever resolved for this icao (tiles still streaming on first sight, sample failure, backoff after a first miss): there is no evidence of where the ground is,heightForreturns null, the model stays hidden and the depth-test-free 2D billboard remains opaque and floored. WARM — a snap resolved and then a >MOVE_INVALIDATE_M(50 m) taxi move stopped it answering directly: the measurement is DEMOTED to a bounded last-known rather than deleted, and it keeps answering while the resample is outstanding, so a taxiing aircraft does not pop 3D→2D→3D across a 2–30 s retry backoff. The bound isHELD_SNAP_MAX_DRIFT_M= 250 m from the spot the value was measured at, past which the hold is dropped rather than stretched and the contact is COLD again. It is spatial with no timer beside it (ground under a contact that has not moved does not change; what invalidates the value is the contact MOVING) and deliberately a quarter of the billboard chain'sHELD_FLOOR_MAX_DRIFT_KM— that hold only ever RAISES a sprite, while a held snap IS the model's placement, so its error shows in both directions. A fresh sample releases the hold, and so does a ground flip (which already callsforget). A loading model is HIDDEN, never zero-scaled: admission setsshow = false(Cesium's default istrue, and an unplaced primitive would claim the visual at the identity matrix), and ownership isready && show— Cesium 1.138'sModel.updatehas noshowguard, so hiding a primitive costs its load nothing. - Ground-snap clock rollback recovery. If the wall clock moves before the sampling-window start, ground snap opens a new window while retaining the normal four-samples-per-250-ms budget. Retry timestamps more than the maximum 30-second backoff ahead of the current clock are ignored. A smaller rollback can retain a bounded retry delay of up to 30 seconds.
- Commercial and military aircraft use the same high-level FLEET model regime: 2D billboards when zoomed out, optional glTF models when closer, controlled by the DISPLAY rail
3Dtoggle andProximity/Allmodes. Since 2026-08-16 (Hangar fleet) models are PER-CLASS: real CC-BY GLBs for light/bizjet/turboprop/widebody/helicopter/uav (CLASS_MODEL_REALinsrc/data/aircraftClass.js— meters-baked, scale 1, per-model belly/radius; provenance inpublic/models/README.md), the sharedairplane.glbfor the remaining civilian classes, and the military layer maps weight classes (real GLBs / 747 heavies /jet.glbfastjets, per-model heading offsets, flat amber or emergency red). Textured civilians carry a HEAVY tint, not a light one:MODEL_COLOR_BLEND_AMOUNTis0.94in BOTH layers under Cesium'sColorBlendMode.MIX, so the class colour supplies 94% of the surface and the asset's own texture ~6%. The visual direction is clean light silhouettes with only a weak diffuse contribution from the approved textures, so liveries deliberately do NOT read. IR boost raises the blend to a full1.0. Under NVG/FLIR (map preset or Cockpit vision) models render unlit flat-white at full alpha and scene fog is disabled (fog otherwise blacks out distant models with the globe hidden); state restores on exit. - Aircraft models skip Cesium's dynamic environment map (2026-10-24). Flights, Military Flights and Local ADS-B pass
environmentMapOptions: AIRCRAFT_ENVIRONMENT_MAP_OPTIONS({ enabled: false },src/data/aircraftModelLighting.js) to every drawn model. Otherwise eachModelrenders the sky around it and reads it back from the GPU (readPixels) when it is first placed, and again whenever it moves more than 1 km, the scene's atmosphere settings change, or (under sunlight dynamic lighting) an hour of scene time passes (DynamicEnvironmentMapManager, Cesium 1.138). With the class colour supplying 94% of the surface, the models are lit from Cesium's default spherical harmonics instead, with no specular environment map, so the sky's model-lighting cube map no longer reaches aircraft.src/data/aircraftModelLighting.test.mjspins the option at every drawn model call; the two preload calls only warm the glTF cache and are never drawn. - The DISPLAY-rail
3Dtoggle is the user-facing activation path for both aircraft layers. Their small approved GLBs build their render resources without Cesium's frame-spread job queue, preventing continuous Photorealistic 3D Tiles streaming from starving model readiness; model caps, tracking, camera, and fallback billboards are unchanged. - The
3Dtoggle DEFAULTS ON inproximityon a first run. Proximity is itself the budget — models appear only belowMODEL_ALT_CEIL_Mand only for the nearestMODEL_MAXin view — so the default costs nothing at globe scale, andallremains a deliberate opt-in. A fresh boot runs NO layer-state restoration (LayerStateCoordinator.start()returns early with neither a share payload nor stored state), so four independent initializers decide what a first-run operator sees and must agree:booleanOption('models3d', 'e', true)insrc/data/layerState.js,_models3dEnabled = truein BOTH flight layers,this._models3dEnabled = trueinsrc/ui/aircraftDisplay.js, and theactive/visibleclasses on#models3d-toggle/#models3d-mode-rowinindex.html. All four are pinned together insrc/data/layerState.test.mjs. Explicit state still wins: because the codec omits default-valued options,models3d: falseis now what travels in a link (lo=…f.e.0) and restores OFF at both aircraft layers. Consequence for returning users: a storedgev:layer-state:v2blob is a FULL options snapshot, so a session that wrote one before this change carriesmodels3d:falseand keeps 3D off until the operator flips it (or clears the key) — the durable snapshot is treated as the recipient's own state, by design. - Consequence of the flip on the recorded tracked/fleet inversion: the 150–800 km band where surrounding contacts draw as models while the SELECTED one is still a glyph is now what an operator sees WITHOUT arming anything. The inversion itself is unchanged and still deliberate (see
src/data/trackedModelRegime.js); only its reachability changed. - Civilian and military 2D aircraft use the established distance scale:
3×near the camera and a0.5×floor from 8,000 km outward. A standard 20 px ambient icon therefore remains about 10 px at globe altitude while retaining the established close-range silhouette. Any compact alternative requires before/after visual evidence. - Fleet model eligibility is distance-based with on-screen priority and hard caps (
MODEL_MAX,MODEL_MAX_ALL) to avoid draw-call explosions. Each model owns its ownmodelMatrix; shared scratch matrices are forbidden because they caused stacking/flicker. - Tracked aircraft use standalone model primitives driven from the already-settled dead-reckoned display position, while the tracked Cesium entity remains billboard-backed so
viewer.trackedEntityalways has a ready bounding sphere. - Flights and military layers mirror the same tracking invariants: no warm-up freeze/jump, altitude-scaled framing, trail head glued to the displayed plane, no pull-out when switching targets, and no cross-layer orphan when switching between commercial and military tracks.
- Regression surface:
npm run test:trackdrives the real app headless with synthetic aircraft feeds and asserts the tracking invariants without depending on live OpenSky/adsb data. Arrival-rotation checks observe the first reveal within a bounded wall-clock window before the one-second idle fallback, rather than assuming the 80 ms fleet cadence fits inside four rendered frames; an incorrect first orientation fails immediately.src/data/trackedModelRegime.test.mjspins the tracked contact's threshold math, the enter/exit asymmetry, and the default-on / cockpit / TR-3B / deselect wiring in both layers.
TR-3B conversion Easter egg (August 2026)
- With a contact tracked, CONTEXT ▸ CONTACTS shows a small 🛸 chip beside COCKPIT (
#tr3b-toggle, gated byCockpitView.syncTr3bToggle()on a tracked contact — not on the cockpit entry policy). Pressing it converts that contact into a TR-3B and pressing it again restores the real aircraft. State lives insrc/data/tr3bRegistry.js: a session-scoped module-levelSetkeyed by ICAO 24-bit address, shared across both flight layers the same waymilitaryRegistry.jsis, so a conversion holds through a civil↔military handoff. It is deliberately NOT persisted (no localStorage, no share-link param, no schema change) and layer teardown deliberately leaves it intact — only a page reload clears conversions. - The sprite is two hidden kinds (
tr3b,tr3bHot) insrc/data/aircraftIcons.js, authored in the same 96×96 nose-up pipeline as the eight class silhouettes, so the triangle points along the display course through the existing screen-projected rotation path withalignedAxisstillZERO. They are unreachable fromclassifyAircraft(). Variant selection rides the existingirBoostlayer param, so under NVG/FLIR/surveillance the hull stays cold and the four emitters render hot; a style switch re-images only converted contacts, never the rest of the fleet. - Both layers resolve every
aircraftIcon()call through a local_iconKind()shim (identity for unconverted contacts), so no refresh path — poll reconciler, two-tier raster swap, presentation pass, tracked entity — can revert a conversion. - The class label follows the conversion across every surface that reports one: tracked card, cockpit/
getTrackedInfo, Contacts, and the analyst record'saircraftClass(tr3b, the style-independent id, so a query answers the same in FLIR as in Normal). Callsign, flight level, speed, and route stay live-feed truth. - A converted contact is billboard-only. It is excluded from model eligibility at SELECTION time, so it never consumes a
MODEL_MAXcap slot, with the handoff guard and the tracked-model regime guard kept as defence. The billboard stays shown, so the contact keeps satisfying thegetNearby/getDetectableObjectsvisibility guards and still works in Contacts and Cockpit. - Regression surface:
src/data/tr3bRegistry.test.mjs.
Split-flap status chips (August 2026)
- The three status chips flip their LABELS over character by character when the text changes, like a departure board:
#global-loading-label("LOADING LIVE DATA" → "LOAD COMPLETE"),#traffic-sync-label, and#cctv-sync-label("loading frames" → "camera grid ready"). All three route throughsetSplitFlapText()insrc/splitFlap.js; there is no other writer of those three elements. The progress counters (#*-sync-progress,#global-loading-detail) are deliberately left as plaintextContent— they tick several times a second, and flapping them reads as a slot machine. #global-loading-labeldoubles as the universal top-center status banner, so anything routed through_showGlobalStatusNotice()flaps as well — in particular the share-link restore notices, of which "Shared military flight could not be restored — feed unavailable" is the longest at 63 characters. That needs no special case:planSplitFlap()compresses the stagger to hold the 620 ms budget (26 ms → 6.9 ms per column at that length), every column is reserved for the whole cascade, andelement.textContentis the complete notice at every instant, so thearia-liveregion announces the whole sentence rather than a fragment. A notice deferred minutes past boot is equally safe:ensureHost()re-validates the shell on every call, and the long-livedTextnode is never replaced.- DOM text is the truth, and its node NEVER moves — do not "fix" this. The first call upgrades a chip label into a permanent shell (
ensureHost): a.gev-flap-textspan holding one long-livedTextnode, plus anaria-hidden.gev-flap-cellssibling. After that the ONLY text operation for the life of the chip isnode.data = next. Nothing is reparented, so the label is never transiently empty and thearia-liveregion never sees a removal/reinsertion pair it could announce twice.element.textContentis the settled string at every instant, because the cells carry no text at all: both glyphs are CSS generated content (::beforefromdata-flap-prev= outgoing,::afterfromdata-flap-next= incoming), which never reachestextContent. This keeps QA pins honest and lets_updateTrafficSyncChip's owntextContent !==guard keep working. The shell is built on a tick where the text is NOT changing, so no real label change ever carries a structural mutation. - No animation loop, and exactly ONE
setTimeoutper change. CSSanimation/transitiononly, triggered once per text change and staggered through a per-cell--gev-flap-delay. The single timer is the settle that strips the cells; the width ease ends on atransitionend/transitioncancellistener, never a second timer. Idle cost is zero, there is no periodic work, and nothing requests a Cesium render or takes a render-governor hold.setSplitFlapTextis a no-op on unchanged text, which is required — the chips are repainted by a 60 ms and a 500 ms ticker. - Only what was visible flaps away. An interrupted cascade (A→B cut short by C) derives each column's outgoing glyph from
visibleGlyphs()— what that column is actually SHOWING at that instant, which for a column whose stagger has not elapsed is still A, not the pending B.FLAP_TURN_RATIOmust track thegev-flap-out/gev-flap-inkeyframe crossover in style.css. - Columns never renumber mid-cascade — do not "optimise" this away. For the whole cascade the board keeps one column per index of the LONGER string, each holding its own width; a column the new string does not reach flaps to a BLANK in place (
data-flap-next=" ") rather than collapsing. Collapsing stacks the absolutely-positioned outgoing glyphs on one x AND lets a later glyph slide into an earlier column, which makesvisibleGlyphs()lie and the interrupt rule flap the wrong glyph away. Pinned by "a cleared column holds its place instead of letting later glyphs slide left". - Length changes are eased, never snapped, and the ease is placed so it never fights the flaps (
.gev-flap-sizing): a GROWING label reserves its columns as the cells go in and eases at the START; a SHRINKING one holds full width for the whole cascade and eases at SETTLEMENT. - Accessibility: the cells sit in an
aria-hiddenwrapper and the settled string is real text in the a11y tree, so thearia-livechips announce the label once per change rather than character fragments. Noaria-labelis used — ARIA prohibits naming a generic<span>. Because the text node is permanent and only its data changes, a label update is a singlecharacterDatamutation and settlement is none — node churn in a live region can double-announce. - A chip hidden by clean-UI, recording mode, or an un-
.visible(opacity: 0) traffic/CCTV chip swaps instantly instead of animating where nobody can see it;prefers-reduced-motion: reducedoes the same. - Kill switch:
SPLIT_FLAP_ENABLEDinsrc/splitFlap.js. Set itfalseand every chip returns to a plain instant swap with no other change. - Regression surface:
src/splitFlap.test.mjs.
Panoptic Detection + Tracked Readout (June 2026)
src/data/detection.jssamples enabled layers through each layer'sgetDetectableObjects()contract and renders bounding boxes/labels from the shared host's sole Cesium post-render callback so boxes align with the final camera frame.- Detection diagnostics count fading labels from the arbiter rows that are actually rendered. The label QA harness uses time-weighted label exposure for churn and requires conclusive solve/frame samples at both its 12,000-object pathological field and 5,200-object normal field without relaxing budgets.
src/data/detectionDraw.jsperforms the batched, DPI-crisp canvas drawing for tier-colored labels, corner brackets, callouts, and distance-scaled tracked boxes. Unit tests cover label measurement and draw geometry.src/data/trackedReadout.jspublishes a protected shared-host callout above tracked aircraft and satellites or selected mapped installations. It reads only each layer's cached display position—never a fresh entity position evaluation—preventing readout jitter against the rendered target. AIS selection remains in the vessel source's protected card path.
Not Currently in Runtime
- Weather radar (removed before OSS v1 after QA; no reliable visible payoff)
- General replay/timeline systems outside the Space Missions experience
- LiDAR explorer and paired-point CCTV calibration experiments
Auth + Launch
Vite's development filesystem deny list excludes local .gev-logs/ and
.gev-cache/ contents and the package checkout's root dist/, while allowing
dependency dist/ assets. Checkout glob characters are escaped. This does not
apply to public/ assets or built-preview serving.
- Recommended launcher:
./scripts/dev-fresh.sh(also:dev-secure.shfor stricter bindings,dev-cctv.shfor CCTV source-pack tuning) - A successful Pinokio install writes the owner-only
pinokio/.installedmarker. The nested launcher menu resolves that marker from its own directory: an absent marker exposes Install, a present marker exposes Start, and a running server with a captured ready URL exposes Open God's Eye View. That default item uses Pinokio'spopoutoption to open the system browser; the Server item retains the launcher console. The app's framing headers stay unchanged. This does not assume all Pinokio desktop versions enforce those headers in their embedded view. - Build gate:
npm run build - Network access: local-only by default (
HOST=localhostin dev-fresh.sh); LAN is an explicit opt-in viaHOST=0.0.0.0(launcher prints a key-exposure warning + LAN URL; see SECURITY.md) - OpenSky default mode: OAuth (
OPENSKY_AUTH_MODE=oauth;anonworks without credentials) - Setup doctor resolves
OPENSKY_AUTH_MODEfrom the environment and dotenv files. Explicitanonand OAuth mode without a client pair report keyless anonymous access (rate-limited); a complete OAuth pair retains the existing presence-only capability wording. Basic and auto modes report the selected mode without guessing which credentials runtime will accept. The proxy's auth behavior is unchanged. - Google key expected in Keychain service
google-maps-api(orGOOGLE_MAPS_API_KEY, or.env) - OpenSky credentials expected in Keychain service
opensky-network(or env, or.env);OPENSKY_AUTH_MODEandOPENSKY_CREDENTIALS_FILEread from.envtoo - Optional-key precedence in
dev-fresh.shis uniform — explicit shell env, then.env, then Keychain:OPENAI_API_KEY(Keychainopenai-api/api-key— voice + HUD summary),AISSTREAM_API_KEY(aisstream-api/api-key— live vessels),CESIUM_ION_TOKEN(cesium-ion/token— Bing stacks),TOMTOM_API_KEY(tomtom-api/api-key— live traffic flow),FIRMS_MAP_KEY(firms-map/map-key— live fires),LL2_API_TOKEN(.envonly) - An empty string is not "unset" on either side of the launcher, and both sides are handled.
scripts/read-dotenv-value.mjshides the requested key fromprocess.envfor the duration of the read (Vite'sloadEnvotherwise lets an inherited empty export win over the parsed files) and restores it after. A key the launcher resolves to nothing is then removed from the dev server's environment outright (env -u), not merely omitted — the child inherits this shell's environment, and Vite backfills.envonly over undefined variables, so an empty export in either place would shadow a configured key.CCTV_CALTRANS_DISTRICTSis the deliberate exception: empty is its documented Caltrans kill switch and is passed through as-is .envsupported via.env.exampletemplate
Proxy/Security Baseline
- CCTV proxy rejects client-specified upstream URLs (server-side source allowlist only).
- CCTV upstream still-image fetches use an explicit abort controller with an eight-second timeout; the timer is cleared on every success or failure path.
- OpenSky response cache stores successful upstream responses only; OAuth token refresh calls are coalesced.
- A cold OpenSky failure uses the current camera subpoint only to request a cached adsb.lol point fallback capped at 250 nm. A fresh OpenSky response or last-good cache wins; a nominally successful worldwide snapshot more than two minutes old prefers viewport-scoped adsb.lol when available, otherwise the stale source is reported honestly. The fallback is visibly source-labeled and is never presented as a worldwide snapshot.
- GBFS proxy refuses upstream redirects (
redirect: 'manual'; any 3xx becomes a 502 and the redirect target is logged server-side only) and enforces its 5 MB response cap while the body streams, cancelling the upstream read past the cap; CCTV health map is bounded. - Proxy error payloads are sanitized (no internal error details returned to clients).
- That holds for the OpenAI and CCTV media paths too:
/api/openai/hud-summarynever relays OpenAI's ownerror.message,/api/realtime/tokenpasses successful ephemeral-token responses through but answers with a fixed error when minting fails or upstream rejects the request, and a failed CCTV media fetch stores a fixed camera healthmessage—GET /api/cctv/healthserializes that field and the CCTV panel renders it as a status label, so it is a client surface as much as the response body is. OPENAI_API_KEYremains server-side only and remains the default voice credential. The optional OAuth mode reads the local ChatGPT/Codex access token server-side on loopback only; the browser receives only ephemeral Realtime client secrets from/api/realtime/token.AISSTREAM_API_KEYis server-side only; the browser reads the same-origin/api/vesselscache./api/google/nearby-placeskeeps the Google key out of Places requests issued for voice scene context./api/google/text-searchkeeps the Google key server-side for view-biased Places recovery used by annotation resolution./api/overpassis bounded by body/response caps, per-client/global rate limits, concurrency limits, operator-configured upstreams, in-flight dedupe, cache bounds, and static validation that every selector is spatially bounded./api/military-installationsuses an independent limiter with the same 90-per-client/300-global one-minute bounds, so viewport installation refreshes never consume/api/overpassannotation capacity./api/routeproxies bounded OSRM route requests for annotation routes, with profile allowlisting, distance caps, response caps, caching, and sanitized "no route found" errors.- Track endpoints:
/api/vessels/track?mmsi=(server-accumulated ring buffers; sub-route handled before the rows snapshot),/api/flights/track?icao24=(OAuth, 60s cache, sanitized errors, independent OpenSky credit bucket),/api/military/track?hex=(400 for an invalid hex, otherwise503 history_unavailablewithRetry-After: 1800andCache-Control: no-store, no upstream request; military tracking uses its locally accumulated trail). - Realtime debug logs redact API keys, bearer tokens, client secrets, and image data URLs before writing to disk; request bodies are size-capped.
/api/realtime/debug-logenforces an always-on 120-per-client/400-global one-minute limiter (not the opt-inGEV_RATELIMIT_OPENAI_PER_MINbucket the cost-bearing OpenAI routes share), appends asynchronously through a serialized queue, and rotatesrealtime-conversations.jsonlat 32 MB keeping one prior generation, so the sink is bounded at twice that regardless of session length. A malformed record answers 400 and a failed write 500, both with a fixed message.
UI/UX Runtime Defaults
- Trackpad pinch zoom is registered as Cesium's
Ctrl+wheelcamera input. Pixel-mode pinch deltas receive bounded amplification before reaching the canvas; ordinary wheel, line/page-mode and native touch-pinch paths are left unchanged. The application scene owns and removes the relay listener. - Z ladder: panels around 100, voice pill 150, toast 200, clean-view exit 300.
- Panel POSITION keys are versioned
v8(godsEyeView.v8.panelPos.<id>); collapsed-state keys remainv6. The one-time position reset clears stale DISPLAY placements that could overlap the Context rail. - Map Source lives in the bottom Visual Presets tray. The left accordion contains no MAP STACK panel, and the
kpanel token that addressed it is gone from the share registry, so legacyui=k...state takes the ordinary unknown-token skip. - A dock popover (Visual Presets, Location) auto-dismisses on mouse-away unless pinned. Focus inside the tray defers that dismissal only when the browser reports
:focus-visible— keyboard focus and typed-into fields hold the tray open; a mouse-clicked tile does not, because Chromium focuses a<button>on press. - GEV MIC control is a glass capsule (var(--glass-bg), blur(24px) saturate(1.4), 999px radius; panel radius in error state).
- The desktop right rail (
#right-context-rail) ownsDISPLAY,CCTV, its active parameter controls, andGLOBAL CONTEXTas one fixed responsive stack in that order. Its compact buttons use the same 176 px width as the left accordion and one consistent 50 px height, share the left stack's 52 px edge inset and measured top baseline across HUD variants, then constrain themselves against visible HUD/chrome rectangles and the remaining vertical corridor.DISPLAYis no longer draggable and legacy saved coordinates are ignored. - The right rail is labeled DISPLAY (formerly "MOVE") and groups, in order, HUD, DETECT, Bloom, Sharpen, 3D, Clean-UI (HUD + DETECT promoted to the top). Its expanded controls retain the same compact 176 px width as the right-side tabs instead of growing to the wider Context detail-card width. It starts expanded on first run and respects the user's later
v6collapse choice. Collapses/expands with directional chevrons (◀collapsed,▶expanded). - Display and Context use matching 330 px expanded widths and matching compact tab dimensions. The parameter panel is part of Display's expanded content. DISPLAY may remain open beside one contextual panel; CCTV and Context are mutually exclusive. In Tactical HUD, expanding CCTV or Context hides the other contextual launcher while DISPLAY remains independently available. The most recently opened right-rail panel owns the constrained lane even when it appears later in DOM order; passive restoration and automatic disclosure do not replace that explicit owner. Minimal and other HUD layouts retain the collapsed launchers; when their active panel exceeds the measured corridor, the rail reserves sibling heights and gaps and scrolls the active panel internally.
- At 720px and below both panel stacks (
#left-panel-stack,#right-context-rail) go edge-to-edge and scroll vertically. Every hosted panel's decorative.panel-glowis pinned to its panel box there (inset: 0insrc/ui/styles/responsive.css): on desktop the glow overhangs the panel by 18–20 px as harmless paint, but inside a scroll container that overhang is scrollable overflow on both axes and drew a horizontal scrollbar band plus a vertical scrollbar that scrolled nothing but glow whenever CCTV, Context, Data Layers or Authoring was expanded. The glow's blur is ink overflow and never scrolls, so the look is unchanged. Do not fix this by clipping the stacks:overflow-x: clipwould not remove the vertical scrollbar. Context draws no glow at all (display: none), so it is not part of that pin. Pinned bysrc/ui/panelRails.test.mjsagainst every(max-width: 720px)block in the flattened stylesheet. STYLE PRESETSandLOCATIONSstart collapsed, expand on intentional hover/click, and auto-collapse after hover leave delay.- Collapsed mini-status indicators show active style and active location/landmark.
- Detection mode is user-controlled and should persist when switching styles. Since 2026-08-22 it also STARTS on — Dense @ 75% for every style on a first run, Normal included — as a
GLOBAL_POST_DEFAULTSbaseline that does NOT set_detectionUserOverridden. Exception (unchanged): selecting a military style (CRT/NVG/FLIR) auto-enables the same Dense preset, but only until the user manually changes detection this session (_detectionUserOverriddengate), after which style switches never touch it. - Detection runs in the bottom lane of the shared host's single world-overlay
postRenderlistener (notpreRender) to eliminate bounding-box drift at close zoom. - Detection takes NO continuous-render hold (2026-08-22,
src/data/detectionRenderDemand.js). It repaints on CHANGE and asks the governor for exactly one more frame while work that spans frames is still outstanding. This is load-bearing for the detection-on-by-default flip: the old unconditionalholdContinuousRender('detection')would have pinned every idle first-run tab at 60 fps, defeating the render governor. Measured on a parked scene with zero layers: 0 renders per 5 s with detection ON, identical to OFF; reinstating the hold gives 301. Gated byscripts/qa-perf.mjs§1b, which also counts the PAINTER's own frames so a painter that had been disabled outright could not score a perfect idle. The invariants — each of which a live adversarial review found broken in the first cut:- Every kind of outstanding work must terminate. A predicate that can stay true forever is the hold under another name. Three qualify: the enable fade-in, label fades, and a solve the frame could not run.
- Label fades count in BOTH directions. A newly selected label is
selected; counting only the fade-out tail left it invisible on a parked scene until an unrelated frame arrived. - Paint and demand share ONE monotonic timestamp — the host frame's
frame.timestamp(performance.now(), sampled once per frame). Re-sampling dropped the terminal frame of a fade (paint at 219 ms drew alpha 0.99545; a policy re-reading at 220 ms said "done"), and a wall clock that jumps backwards keeps demand alive until it catches up. Nothing in the draw pass may useDate.now(). - A skipped paint DEFERS, never cancels. The relief valve's skip and its follow-up request
come from one decision (
detectionPaintSkipDecision), so it cannot drop the only frame that was requested. - A changed detectable set dirties the solve. Detection PULLS candidates per paint but
re-solves on a private 125 ms throttle, so a layer tick that swapped contact A for B could be
spent on a paint that declined to re-solve.
markDetectionSourcesChanged()is called from the manager's layer tick and visibility change, next to the render request each already makes — discrete events seconds apart, never per frame — and it deliberately does not request a frame itself, because the caller already did. - AIR brackets stay prompt because the AIRCRAFT LAYERS hold the loop, not detection
(verified live 2026-08-23). Brackets — including the alpha-floored ones — are painted inside
_drawOverlayfrom live positions and take no part in the sources-changed notification, so the obvious worry is a floored bracket sitting stale on a parked scene. It cannot: an AIR bracket exists only while an aircraft layer is enabled, andflights.enable()/militaryFlights.enable()each take a continuous-render hold for their own per-frame fleet animation. For exactly as long as there is anything to bracket, the scene renders every frame. Measured on a parked camera:holds: ["flights"],requestRenderMode: false, and an outside-aircraft population change moved the painted bracket count with no camera input. This is a COUPLING, sodetectionRenderDemand.test.mjspins it — a later perf pass that strips those holds the way it stripped detection's would take bracket promptness with it, silently. - Known, pre-existing, and deliberately out of scope here: the overlay's backing store does not
re-derive on a DPR change mid-session (
worldOverlaysizing — untouched by this work).
- The detection MODE BANNER (
DENSE VIS:… SRC:… DENS:…% ELASTIC …ms) is developer telemetry and is HIDDEN by default. It paints only under?detectDebug=1(thetrafficDebugconvention), resolved once perinitDetection. The same numbers are always available fromgetDetectionDiagnostics(). On a zero-object frame the "armed, nothing in view" signal is carried by the scanlines and sparse focus ring, not the banner. - The HUD summary's
NEAR <landmark>callout is capped at 150 km (metro scale). Beyond that it falls through to theSECTOR <lat> <lon>readout. The POI catalogue covers eight cities, so a looser bound made the HUD announce landmarks on other continents. - Panoptic mode shows labels on ALL items (no stride skipping). Tracked/selected items keep bounding box but suppress label (skipLabel flag). Tracked items get enlarged bounding boxes (56×44 vs default 22×14).
- The 3D aircraft toggle reveals
ProximityandAllmodes and drives both commercial and military aircraft layers. It ships ON inProximity, so the button paints lit and the mode row paints open from markup; the panel is therefore ~36 px taller than before, which Cockpit's Display/Radio strip absorbs through its existing primary-only corridor solver. - The host-painted tracked-target readout sits above the post-FX layer and follows tracked aircraft/satellites or selected mapped installations using each layer's display-position contract.
- Cockpit entry is gated to the operational Contacts context bundle. The
Context chooser must be in Contacts mode, Live Flights and Military Flights must both
be enabled, and a civilian or military aircraft must be tracked. The visible
Cockpit actions and the
Cshortcut use the same gate, so ordinary standalone flight-layer selection cannot enter a context-dependent cockpit. - Aircraft cockpit view: selecting a commercial or military aircraft reveals a
COCKPITaction (C). Cockpit mode temporarily releases Cesium's orbit-follow transform and drives a first-person camera from the tracked aircraft's existing smoothed display position and course. Its concave helmet-visor HUD shows callsign, UTC time, coordinates, curved roll/pitch guides, and a seven-division heading tape. Ambient commercial and military AIR contacts use a Cockpit near/far band selected by the shared Display 3D mode: Proximity admits at 150 km and retains to 185 km; All admits at 400 km and retains to 450 km. The shared 3D toggle now applies in Cockpit: Off keeps in-range contacts as rotating 2D aircraft silhouettes, while On lets a ready admitted glTF take over without a drawing gap. The Cockpit model cap remains 60 and can only lower the map budget; in-range contacts that are capped or still loading remain 2D silhouettes instead of degrading to out-of-range dots. Contacts outside the selected band use small rotation-free cyan-white pips for civilian aircraft and amber pips for military. The pilot's own airframe is not drawn in first person, and exiting clears the Cockpit band and restores normal map silhouettes/models. The normal left accordion remains available for Layers and Authoring and keeps the same 26vh HUD-aligned anchor used in map mode, independent of whether the bottom-left Contact card is expanded or collapsed; the right-rail CCTV and Context chooser are hidden to avoid duplicating or overlapping the cockpit presentation. When the intelligence HUD is enabled, its classification, scene summary, collection/orbit metadata, coordinates, and imaging-status text remain visible as reduced peripheral cockpit telemetry; the optical center and flight instruments stay clear, and turning the HUD off still hides it. Ground speed, exact heading, and rendered altitude form one compact lower-center instrument cluster, keeping the horizon and peripheral view open. A second live altitude tape hugs the inside-right visor rim: its rail and nine moving ticks are derived from the same responsive keyhole radius, remain 20 px inside the circular edge even on wide displays, fade deeply at both vertical ends, and move continuously behind a fixed current-altitude pointer; its interval tightens automatically near the surface and it is suppressed on narrow screens. Cockpit mode also has a weather-backed transparent volumetric-cloud pass derived from the supplied FBM/domain-warped R&D shader. It renders at no more than 520×320, uses 24 ray steps/three FBM octaves at 12 FPS, is clipped to the visor, fails clear when Open-Meteo is unavailable, and stops its animation completely on cockpit exit. It does not restore the prior CPU weather canvases, precipitation, scene fog, or any map-mode effect. When Contacts is active for the tracked aircraft, the compactCONTACTrail adds the 250 km subject window, four cohort counts, nearest observed/mapped example with relative bearing and distance, freshness, explicit uncertainty, and Previous and Next controls plus its own collapse control. The mirrored right rail is a three-page cockpit briefing carousel: source-backed live signals, location-matched regional headlines, and physical-region/current-weather context from bundled Natural Earth polygons and Open-Meteo. It is manual-first: Previous, Next, and direct page controls are always available, and the visibleCYCLE OFF/CYCLE ONcontrol starts or stops the nine-second page cycle. The cycle pauses on hover or keyboard focus and stops while collapsed, hidden, or outside cockpit mode; live signal data continues refreshing either way. Empty news matches and unavailable news use compact text states rather than reserving an empty media frame; partial local data remains explicit. Article links open their original publisher, and no headline is treated as verified risk intelligence. On desktop both cockpit rails use the same width and share a bottom-aligned safe baseline in opposite corners above the peripheral MGRS/GSD/time telemetry, leaving both that text and the lower-center instrument cluster readable. They collapse independently to slim tabs without stopping live data updates. Narrow screens use separated top/bottom fallbacks. Empty-space globe clicks are inert while cockpit owns the camera;C,Escape, orEXIT COCKPITexplicitly exits and restores the same tracked entity and standard follow camera. Unknown feeds remain unknown, selection loss still exits safely without inventing a replacement track, and the cockpit never presents the summary as threat scoring or an all-clear. This is a desktop first-person presentation, not a WebXR session. - Cockpit left-panel clearance: the Cockpit Contact card and peripheral HUD participate in the adaptive left accordion's live obstacle measurements, including live viewport-height changes. Expanding Layers or Authoring keeps the active panel in the available upper-left corridor with internal scrolling; it does not cover the Contact card, lower Cockpit controls, or Cesium credit line. Outside Cockpit the hidden card does not alter the normal corridor.
- Cockpit Context scope: the 250 km radius applies to the air/sea proximity cohorts. Installation counts come only from the currently loaded viewport and are labeled
CURRENT VIEWPORT ONLYin the cockpit as well as the normal Context panel; neither surface presents them as a complete 250 km installation survey. - Cockpit camera anchor: first-person mode does not write feed-boundary corrections directly into the camera. A cockpit-only inertial anchor advances from the selected aircraft's displayed course and speed, then converges toward the authoritative delayed track with correction capped below forward motion. The displayed kinematics are derived from the same consecutive fix segment as the rendered position, with raw feed speed/course used only as fallback; a transient zero/missing feed speed therefore cannot freeze a visibly moving aircraft after layer enable or a map/cockpit handoff. Rendered altitude continues to come from that interpolated track position. Late ADS-B fixes and short render stalls can remove drift without accelerating or reversing the view. Camera placement runs before scene update/culling at a bounded 20 Hz so a moving cockpit does not force Photoreal 3D Tiles to retraverse on every display frame; textual instruments update at 10 Hz and context/layout work at 4 Hz. Every far Cockpit contact pip shares one stable Cesium texture-atlas entry and skips unused screen-projected course calculations, while only in-range 2D aircraft silhouettes pay the screen-projected rotation cost; ambient glTF collections are hidden/retained rather than synchronously destroyed at cockpit entry, and context rails lay out only on explicit content/state changes and viewport resize. The deliberate 15/30-second layer interpolation delays and per-Cesium-frame position caches remain unchanged.
- Cockpit route, vision, and view controls: visible on-screen
COCKPIT,RESET, andEXIT COCKPITcontrols replace reliance on theCshortcut. RESET uses the same canonical globe route as the map and voice actions, exits Cockpit, and releases its camera ownership rather than exposing the hidden map-style top action. When the tracked commercial flight has a plausible ADSBDB route, the top of the right briefing rail shows a compactFROM → TOairport strip and the visor shows a centered estimated-destination chevron with its relative bearing; its destination label rounds to whole degrees in 000–359, wrapping rounded north to 000° while the chevron continues to use the relative bearing independently of label rounding; absent or implausible route data hides the strip and cue rather than guessing. The cockpit-local vision control is an interactivePREV / CURRENT / NEXTcarousel overNORMAL,CRT,NVG,FLIR,ANIME, andNOIR; its previous/next actions wrap, and activating the current value advances to the next style. Cockpit enters on the active map preset, so a Cyber FLIR map opens Cockpit on FLIR while Normal remains a distinct option instead of producing a second FLIR entry. Cockpit choices temporarily activate the existing Cesium post-process stages, while exiting Cockpit through either EXIT or RESET restores the pre-entry map style. The regional-news page uses a free Google News RSS locality query first, with the existing GDELT query retained only as a fail-soft fallback; linked headlines remain reporting, not verified incidents or risk intelligence. - Cockpit wind-direction labels wrap after rounding to whole degrees, so a direction of 359.5° displays as
N · 0°rather thanN · 360°. - Cockpit missing observations: missing local weather observations remain unknown rather than being converted to zero or clear skies, in the briefing and in the cloud effects; a missing temperature drops the whole local weather record, including its wind and condition.
- Cockpit weather status: the earlier multi-canvas atmospheric compositor remains fail-closed and is not attached to the live viewer. Cockpit clouds are a separate transparent WebGL pass with a capped 520×320 framebuffer, 24 ray steps, three FBM octaves, and a 12 FPS ceiling. It defaults off and starts only when local storage explicitly contains the persisted
WX ONopt-in ('1'). When opted in, observations refresh after five minutes or 25 km of aircraft movement, fail transparent when unavailable or clear, and stop on exit or disable.WX OFFgoverns atmospheric rendering only: the briefing still fetches source-backed Natural Earth region, headline, and Open-Meteo local-information data, aborting and replacing any in-flight request when the selected aircraft changes. No weather effect runs in map mode and no synthetic fallback is shown. - Cockpit trail visibility: entering cockpit hides the selected aircraft's trail body and head so they cannot cross the first-person view; exit restores them. This cockpit-only presentation change does not alter the normal map-mode invariant that aircraft trails render through terrain using their depth-fail material.
- Aircraft course slew: civilian and military 3D models retain the 60°/s course limiter, but each rendered frame can consume at most 250 ms of accumulated slew time. A long tile/render stall therefore catches up over multiple visible frames instead of turning one delayed frame into a heading snap.
- Manual-first cockpit briefing: the right-side Live Signals / Regional News / Local Info carousel does not advance automatically on page load. Previous, Next, and direct page controls remain available; the visible
CYCLE OFF/CYCLE ONtoggle explicitly starts or stops the nine-second page cycle, which still pauses on hover/focus and while collapsed, hidden, or outside cockpit mode. Live signal data continues refreshing in either state. - Photoreal horizon blend: Cesium's sky atmosphere remains enabled behind the hidden base globe, but its light intensity, saturation, and brightness are reduced from the library defaults so the distant Google Photorealistic 3D Tiles boundary blends into the sky instead of producing a bright cyan horizon seam.
- Cockpit direction and speed tapes: plausible destination metadata now drives one translucent, isometric visor chevron labeled directly below with the estimated geographic bearing; the prior full geodesic dashed path is not rendered. A mirrored live ground-speed tape follows the inside-left keyhole rim using the same responsive curve, end fades, fixed pointer, and fractional tick motion as the altitude tape on the right. Speed values scroll upward as they increase while altitude values scroll downward. Its tick endpoints and current-speed pointer share the rail's inset-circle origin, so the markings stay attached to the visible curve rather than drifting inward with the text-label gutter.
- Voice control UI (
#gev-voice-control) shows status states OFF / CONNECTING / LISTENING / EXECUTING / ERROR.
Current Global Post Defaults
Reasonable-defaults batch (2026-08-22), extended and partly revised
2026-08-23. First-run defaults move together as one coherent console
presentation. Every one is a FIRST-RUN baseline only: a
share link or the operator's own hand still wins over it, and none of them sets
the _detectionUserOverridden / explicit-intent flags that would suppress a
separate landed behaviour. Pinned in src/reasonableDefaults.test.mjs (feather,
detection, OUTSIDE opacity) and src/data/layerState.test.mjs (3D).
Current defaults (scope feather revised to 100% on 2026-10-25;
detection and outside opacity retain the 2026-08-24 defaults):
the first-run look is Detection DENSE
75%, ELASTIC allocation, Fade 7%, OUTSIDE opacity 1%, Scope ON with feather
100%, and 3D fleet mode PROXIMITY, with AIRCRAFT_BRACKET_FLOOR_ANCHOR at 0.01 so
brackets keep their approved brightness exactly at the new OUTSIDE default. Two
terms that must never be conflated: scope FEATHER softens the black scope-mask
edge; detection FADE is the label/card fading band around the keyhole. The
OUTSIDE slider's step stays 1 so every low stop is reachable.
Allocation, defined precisely (matches
src/data/labelArbiter.js allocateLayerQuotas): ELASTIC begins with
roughly equal capacity across active layers and redistributes unused
entitlement (labelArbiter.js:170); WEIGHTED allocates using visible
demand with square-root demand scaling and semantic layer weights
(labelArbiter.js:181). First-run default: ELASTIC.
A default has THREE surfaces, and a PARSE fallback that is not one of them.
The value literals (engine constant, markup value, markup readout, src/ui/visualPresets.js
GLOBAL_POST_DEFAULTS, the share generator's starting state) must all move
together, because a fresh boot runs no restore and those literals ARE the startup
state. The scf / ko PARSE fallbacks deliberately do NOT move: they answer
what an OLD LINK that omits the field meant, and such a link was authored under
that era's default (scf → 35, ko → 5). Every link since carries both fields
explicitly, because the generator always writes them, so no era whose default
later changed depends on a fallback either way.
They do NOT all persist the same way, and only one of them persists at all. Worth stating plainly, because "a default you can override" and "a default that remembers" are different promises:
- 3D models have durable storage —
gev:layer-state:v2in local storage, written byLayerStateCoordinatoron explicit intent. A session that stored a snapshot keeps whatever it stored, across tabs and restarts. - Detection mode/density and scope feather have NO storage key at all. Their
only durable carrier is the URL hash (
dm/dd,scf), written on a 500 ms debounce. A same-URL reload therefore keeps them only if that debounce already fired; a bare URL or a new tab returns to these defaults. That is unchanged by this batch — it is simply what these controls have always done — but it is the reason "stored state wins" is true of the 3D toggle and not of the other two.
Known edge (detection, pre-existing, deliberately not redesigned here): a
hash-restored dm=OFF restores the MODE but not _detectionUserOverridden,
which is session-scoped. A recipient of an OFF link who then selects a military
style therefore gets the style's auto-enable, where the original author — who had
turned detection off by hand — would not have. The default flip makes this edge
easier to meet (detection is now on more often), but does not create it.
- Bloom:
OFF, intensity slider at100% - Sharpen:
ON, intensity slider at49% - HUD:
ON, layouttactical - Detection:
DENSE@75%— ON for EVERY style on a first run, Normal included (wasOFF@50%). It is literally the same frozenMILITARY_DETECTION_PRESET { mode:'dense', densityPct:75 }object the military styles and the Contacts context mode already apply (Contacts OWNS detection while active and restores the prior state on exit —contactsDetectionPolicy.js; Cockpit deliberately does not touch detection at all), read byGLOBAL_POST_DEFAULTS, so there is one tactical look rather than several that can drift. Fade opens at7%since the 2026-08-24 final lock (16%before it). Style-switch semantics are unchanged: CRT/NVG/FLIR still carrydetection: MILITARY_DETECTION_PRESETand still yield to_detectionUserOverridden; Normal still has noSTYLE_PRESET_DEFAULTSentry, so switching TO Normal touches nothing. A share link carryingdm=OFFstill restores OFF. - Detection OUTSIDE opacity:
1%(moved5% → 3% → 1%on 2026-08-24).KEYHOLE_OUTSIDE_OPACITY_DEFAULTinsrc/celestialRing.js, mirrored by#detection-opacity-slider's markup value AND readout,GLOBAL_POST_DEFAULTS.detectionOutsideOpacityPctinsrc/ui/visualPresets.js, and_detectionOutsideOpacityPctinsharelink.js. The slider'sstepis now1, so 1–4 % are reachable at all (at the previous step of 5 the entire sub-default range was one stop wide).AIRCRAFT_BRACKET_FLOOR_ANCHORinsrc/data/detectionPolicy.jsMOVES WITH IT — the AIR bracket floor is calibrated soAIRCRAFT_BRACKET_ALPHA_FLOOR(0.35) lands exactly at the default, and the mapping follows bracket brightness rather than slider position. ThekoPARSE fallback stays at5. - Scope:
ON, feather:100%.SCOPE_FEATHER_RATIO_DEFAULTinsrc/scopeMask.jsmatches the slider, readout and share generator. The range is 0–200%; explicit 0% is a hard edge. Missing-scflinks retain the historical 35% parse fallback. - 3D aircraft models:
ON, modeproximity— see the 3D Aircraft section above and the DISPLAY-rail entry below (wasOFF). - Style shader starting params: CRT/NVG/FLIR pixelation
1.2(just above the native1.0floor); thermal/FLIR ships an optional Ironbow "Predator" palette (paletteuniform, default0= accurate grayscale).
Operational Notes
- Earthquake discs are STATIC geometry. Every quake is a
CLAMP_TO_GROUNDellipse; aCallbackPropertyaxis re-tessellates its ground primitive every frame, which cost 32.4 ms/frame and 30 fps on the shipped 58-event feed. The axes are plain numbers, redefined only when a poll brings new data, and the former ±15% radius pulse is gone. Because nothing in the layer animates per frame, it holds NO continuous-render hold — the governor stays idle with earthquakes on, and the manager'slayer-tick/layer-visibilityrequests carry new data to the screen. Pinned insrc/data/earthquakes.test.mjs. - Geocode framing has an off-centre sanity gate. A viewport that is both
bigger than any city (>300 km diagonal) and not centred on its own geocoded
location (anchor >15% of the diagonal from the centroid) is replaced by a
40 km metro box on that location. This is what stops "Tokyo" — which geocodes
as the PREFECTURE, islands and all — from framing open Pacific.
countryresults are EXEMPT by decision (several have the same pathology from overseas territories; reframing a country is a product decision, not a bug fix), and an explicitviewMode: 'overview'ask bypasses the gate entirely so "show me an overview of Hawaii" still frames the whole administrative area. - Viewport framing is antimeridian-safe.
flyToViewportBoundspads from the short-way-round longitude span and wraps the padded edges, so a dateline- crossing box stays its true width. Raw subtraction inflated a 0.41° metro box to 86.7° and a 60° territory to 132°. - Point distances take the short way round 180°. The annotation
resolver's
approximateDistanceM(voice "point out" proximity checks) andplaces_nearbydistances wrap the longitude difference. Raw subtraction put Savusavu 38,148 km from a camera over the Lau Islands, so the proximity check refused it, and sorted a place 2 km across 180° last. - CCTV calibration persists at
godsEyeView.cctv.calibration.v2(v2 rebuild; the store was wiped clean, no import from the oldv1key). - CCTV v3 floor QA pins are: zero samples for heading-only edits; zero transient samples and constant elevation during E/N drag; one shared-floor resolution on release; late one-shot shared-cell work is permitted during viewshed idle. The A+B harness intentionally excludes citywide LOD assertions.
- Panel positions have a versioned storage name,
godsEyeView.v8.panelPos.<panel-id>, but the current rails lay panels out adaptively and write none. Collapsed state does persist for every panel atgodsEyeView.v6.panelCollapsed.<panel-id>('0'open,'1'closed, absent means the panel's own default). - Legacy draggable-panel position keys may remain in local storage for backward compatibility, but the map-mode right rail ignores them; collapsed states still persist at
godsEyeView.v6.panelCollapsed.<panel-id>. - Flight/military tracked entities cache dead-reckoned positions per frame to avoid callback desync flicker.
- Aircraft 3D-model and tracking invariants are covered by
npm run test:track; run this before touchingflights.js,militaryFlights.js,detection.js, ortrackedReadout.js. - Annotation resolver behavior is pinned by
src/annotations/annotationResolver.test.mjs; re-run that suite before changing place-resolution scoring. - Layer input handlers (click + keydown) are detached on disable for flights/military/satellites/AIS vessels.
- Traffic tile cache is capped and traffic layer supports explicit destroy cleanup.
- Traffic feed state is honest about simulation.
getStats().modeis the CONFIGURED source ('live' = a TomTom key is present, 'sim' = keyless), NOT this instant's health — health rides onerror. Keyless reads FALLBACK withSIMULATED — add TomTom key for livein both the sync chip and the panel meta line; an unreachable/api/tomtom/statusreadsSIMULATED — traffic service unreachable; a total flow-fetch failure in live mode setserror(DEGRADED ·SIMULATED — <reason>) and zeroes the stale coverage number.stats.loadingcovers outstanding flow work as well as the road fetch, so a flow failure landing after the first paint still ends the shared loading batch as LOAD FAILED. Harnesses must gate on!stats.error, never onmode === 'live'alone. When the drawn flow came from the proxy's cached STALE-BUDGET / STALE-ERROR tiles,getStats()reportsstale: trueandoldestTileAt, the row reads STALE with the oldest tile's age, and the sync chip readsSTALE · Roads: <source> · Flow: cached TomTom tilesinstead of LIVE. - Traffic runs in
simmode (white dots, hardcoded speeds) unlessTOMTOM_API_KEYis configured (env or Keychaintomtom-api/api-key), which enableslivemode: TomTom flow vector tiles via the budget-governed/api/tomtomproxy (.gev-cache/tomtom/, 120 s TTL,TOMTOM_DAILY_TILE_BUDGETdefault 6k/day, sized so a 31-day month stays inside TomTom's 200K/month free allowance), decoded client-side for green/amber/red dot color and speed/density scaling (trafficFlowStyle.js). - Road source (
src/layers/traffic/roadModes.js): the Street Traffic row has TomTom / OSM / Hybrid chips (roadModeparam, stored in the existing layer statelofield, no new share token);?trafficRoads=tomtom|osm|hybridoverrides restored state until the user picks a chip. With no stored choice the default is Hybrid with a TomTom key and OSM without one; share links without a road-source choice (including older ones) likewise use the installation's default (Hybrid when keyed, OpenStreetMap when keyless). Without a key every choice draws OSM roads and the status says the choice needs a key. TomTom draws only flow-tile lines with their own flow; roads without flow are not drawn and the status says so. Afull-coverage flow line carries both travel directions, aone_sideline the direction it is drawn in. OSM draws OpenFreeMap roads with flow matched onto them (below). Hybrid draws eligible TomTom motor-road lines and adds OpenFreeMap roads only where no eligible TomTom line runs within 35 m in the same travel direction (30 degrees), tested every 10 m; only uncovered stretches of at least 40 m are kept, and they are simulated. When exactly one side is a motorway/trunk mainline (TomTomMotorway/International road; OSM ramps count as ordinary roads) the radius is 15 m, so public frontage roads beside a freeway stay. Flow values are ignored for this test, so an uncertain overlap drops the OpenFreeMap copy, never the TomTom line; opposite carriageways stay. OpenFreeMap tiles stream first and are drawn plain until flow arrives, then one replacing snapshot applies the TomTom lines and dedupe. Status examples:LIVE · Roads: TomTom · Roads without flow hidden,LIVE · Roads: TomTom + OpenStreetMap · Flow 81%; Hybrid with no TomTom line in view names OpenStreetMap alone. TomTom mode skips the z14 detail pass and never requests OpenFreeMap unless it falls back without a key. Matching reusesflowMatch.js: seven road samples, a 35 m nearest-segment radius, 30 degree travel-bearing tolerance, and a bounded 3x3 probe in a 100 m spatial grid. At least half the samples must match; the median flow level supplies speed/color and any matched closure stops dots in that travel direction. Two-way roads have independent forward/reverse records; one-way direction is enforced. Equal-distance candidates with contradictory flow/closure are left simulated. Matching runs only on load/refresh, never per frame. Flow stays at z12 with the same 16-tile cap, 120-second cache and server daily budget. Lines are clipped to tile cores before caching and to the look-at viewport fetch box before road conversion or matching. Unmatched roads retain white simulated dots at free-flow speeds (unless the explicit uncovered-roads hide option is enabled). Cached OSM road snapshots are rematched on every load; TomTom and Hybrid views are recomposed from the bounded tile caches each load because their roads carry flow. A snapshot is cached only when the load drew plain OpenStreetMap roads, and the cache is not read before the TomTom status probe settles (except for an explicit OSM choice), so Hybrid fill is never replayed as OSM roads. Failed flow refreshes clear old matches and report simulation.flowCoveragePctis the percentage of shown dots on roads with flow, including all unmatched dots in the denominator and excluding dots hidden by closures. The OSM keyed status readsLIVE · Roads: OpenStreetMap · Flow: TomTom · N% cov. Road fetch coverage follows a 5×5 ellipsoid sample of the central screen square. Horizon rays are capped at 10 km from nadir (reduced if necessary to fit 16 z12 tiles); polygon-intersecting tiles load nearest first. Up to 16 z14 tiles provide near detail, with z12 roads beyond them. Cached tiles serve revisits without requests, and settled coverage replaces the box-overlap gate. - OSM geometry (OSM mode and Hybrid fill) comes from OpenFreeMap's immutable versioned tiles:
successful TileJSON metadata prefetched at enable and cached for the source lifetime, z12 wide pass and
z14 detail below 4.5 km. The nearest 16 intersecting detail tiles cover the
near field; coarse roads remain outside successfully loaded detail cores.
Partial coverage retains coarse roads under missing tiles and retries at most
three times with bounded backoff, reusing successful tiles. A failed detail pass retains major roads and separately
reports "Detailed roads unavailable". OpenMapTiles admits motorway, trunk,
primary, secondary, tertiary and minor roads, excluding access restrictions,
path subclasses, indoor ways and tunnels. Bridges and ramps inherit the motor
road's eligibility; the ramp flag alone never admits a path. Service ways,
including untyped ones, parking aisles, driveways, drive-throughs and alleys,
are excluded: their tiles do not establish public through traffic. TomTom
admits only its eight named motor-road types, rejecting Non public road,
Parking road, Walkway road and unknown categories for geometry, flow matching
and Hybrid deduplication. Eligibility follows provider classification; omitted
or incorrect upstream restrictions remain a source limitation.
Only eligible road geometry receives dots;
reverse one-way lines are reversed. The shared tile source caps each view at
16 tiles per pass (32 OpenFreeMap tiles total), four concurrent requests and 4 MiB per response. OpenFreeMap retains
up to 192 decoded tiles/64 MiB (four times body bytes as a storage estimate);
other tile sources retain the 64-tile/24 MiB defaults. Identical source-version/XYZ
requests share one download/decode, with independent caller cancellation. Parsed road views have a separate 24 MiB/64-entry cap; incomplete
views are not retained as complete snapshots. Buffer geometry is clipped and sub-12 m line slivers dropped; tile
road fragments are not stitched. Roads preserve bends and insert
waypoints at most 150 m apart, splitting paths at 80 vertices. A cancellable
preparation pass paints decoded tiles independently. Explicit enable starts
acquisition immediately; camera gestures retain the 320 ms settle debounce,
but moveEnd starts the final view immediately. Detail acquisition runs alongside
the major preview, using the same bounded tile cache. Once dots exist they stay
alive until the detail snapshot replaces them, avoiding repeated preview picks.
Status/flow and roads start
concurrently; neither TileJSON repeat reads nor flow impose a paint barrier.
Only dot-budget-admitted roads crossing the canvas (100 px margin), within
the bounded reticle range, receive surface work. Near and upper-screen roads
share the height-station budget, with fewer dots on distant roads.
Roads split into deterministic chunks no longer than 600 m, preserving every
bend while keeping dot budgets on nearby street sections. Height stations
are at most 150 m apart along the road, with heights
interpolated over the preserved intervening bends. On photoreal maps, surface
preparation first reuses rendered depth. Up to four height/projection iterations
must converge within 2 m of the road coordinate; foreground hits, invalid heights,
off-canvas points and translucent-depth picking fall back to the existing mesh
sampler. Depth and settled-height certification require a completed render of
the current camera and surface. This avoids redrawing a pick frustum for every visible station. Preparation yields after a 12 ms slice (an individual synchronous Cesium pick
cannot be preempted). Validated coordinate heights use a per-scene 40,000-point
LRU across loads. The LRU belongs to the surface it sampled (the terrain
provider, or the set of visible 3D tilesets), so a map-provider switch
drops it. Each height records its detail band (log2 of the camera-to-point
distance in 100 m steps); a point seen from a closer band is re-sampled
against the finer mesh, and a failed finer sample keeps the coarser one.
Revisits at the same or a coarser band sample nothing. Provisional shared-floor estimates remain hidden until a
local rendered-mesh/terrain sample resolves; a globally loading tileset does
not block streets whose mesh is already present. Non-finite/out-of-band
(+/-9000 m) samples defer their road and show a local-surface status, never
an OpenFreeMap failure. Samples floor on cached ground/visible terrain.
Samples acquired during refinement are revalidated when the visible mesh settles.
Shared floor cells are read only; raw mesh samples never enter their cache.
Roads retain their dot populations by source, geometry and travel direction.
Partial tiles add roads without clearing the point collection; full source
snapshots replace the accumulated graph in every road mode; completed
snapshots retire only roads that disappeared. Density changes apply at most
768 additions/removals per animation frame, with 180 ms growth/fades. The
total cap stays 6,000 at street level and scales by (1,000 / altitude)^2
above 1 km (about 667 at 3 km, minimum 400). Surface admission shares that
budget, plus a deterministic 1,000-unique-height-station ceiling, nearest roads
first (
surfaceBudgetOmittedreports roads outside that ceiling). Below 4.5 km the wide pass prepares a 32-road preview before the complete detail pass; previews never wait/retry missing mesh or retire existing roads. Full coarse coverage remains available above that band. Flow matching is memoized for admitted roads within each camera load. Surviving dots keep their identity, road, direction and progress. Flow updates change color, speed and closures in place. Dots use normal depth testing, including jammed traffic, so buildings occlude roads behind them. Surface preparation is reused within a camera load. A local height estimate selects the provisional viewport in elevated cities; measured preview heights and rendered center depth refresh it before detail selection. It never admits unmeasured dots. Spacing uses height above that local surface, retaining street density at high elevations; later heights ease into separate retained waypoint arrays, bounded to 12 projected pixels per waypoint per frame near the canvas with reused scratch objects. Visible waypoints keep that limit even when a trial step leaves the canvas; unsafe near-plane steps shrink before publication. Off-screen corrections use bounded world-space easing and do not hold loading open. Road acquisition and body reads have cancellable deadlines. Queued tile paints and terrain callbacks share pass cancellation; expired passes cannot publish, while completed coarse roads remain available. The traffic status chip polls busy work every 100 ms and completes without a split-flap delay; idle and global status reads keep their 500 ms cadence. Refinement is one bounded pass, never a publish loop. That pass replaces early cached mesh heights even in the same distance band: tilesets can briefly report loaded before finer destination tiles begin streaming. Ordinary revisits still reuse validated cached heights. At a road endpoint the departing dot fades before a new simulated vehicle identity enters at the other end, preserving one-way travel. Detection uses those stable identities rather than collection slots.visitMotionDotsexposes identity, road identity, primitive and source classification for the motion gate.scripts/qa-traffic-motion.mjsrecords timestamped CDP frames and checks cold/warm still-camera holds in all three road modes;--baselinealso measures rebuild churn in older runtimes using primitive object identities.--journeyadds the South Congress/downtown/Camp Mabry route, layer toggles, Capitol, greenbelt, commercial parking and corridor holds, with a 1.5-second steady-state limit including loading and population changes. It checks every shown dot against the allowed source classes and records phase timings. Positions and segment distances are precomputed, with no new animation-loop allocation. Visible-globe roads use cached terrain without offscreen mesh picks. The road-source label names the geometry being drawn, with partial/unavailable states shown plainly. - Vector tile sources accept tiles only from an allowed origin, which defaults
to the configured
tileJsonUrlorigin; an explicitallowedOriginoverrides it. Repointing onlytileJsonUrlloads tiles from that host or fails with an origin error rather than silently falling back to OpenFreeMap. - TileJSON caches successful metadata. Transient failures retry after a five-second cooldown; invalid metadata/origins stay unavailable until the source is cleared. Clear resets metadata and cancels pending requests. Every failed read cancels its body and aborts its owned request, including declared and streaming byte-cap failures.
- Development captures opened with
?trafficDebug=1mint an interaction anchor from the exactcamera.changedevent that arms each debounced load, then emit scheduling-correlated User Timing entries for productionresponse.json, road parse, dot construction, heat-line rebuild, and next-post-render boundaries. Every trace is paired with the exact camera-change that scheduled its load; mismatches are counted drops. Cesium'smoveEndremains a diagnostic mark only: it arrives about 500 ms after stillness, typically after fetch has begun, and fetch never waits for it. Production builds remove the flag, hooks, counters, and timing labels. - Voice debug log:
tail -f .gev-logs/realtime-conversations.jsonl(gitignored).
Replay chase-camera updates run in Cesium preUpdate before scene traversal, preventing 3D-tile refinement stutter when the mission replay speed is reduced. Replay Ascent first gives the selected launch site a five-second tile-preparation hold before displaying the T-minus countdown. Replay ascent duration is mission-specific: disclosed insertion, SECO, or separation timing is compressed into the replay, while sparse records use reconstructed path length with bounded fallback timing instead of a universal fixed duration. Replay transitions directly from ascent to orbit; stage re-entry/recovery remains static contextual linework and is never a camera-tracked playback phase. The screen-space rocket/thrust symbol renders at 50% of its 92 × 138 px design box, while the separate callout text remains unchanged. Successful/upcoming selected missions show a current orbit marker: green for a reliable TLE match or amber and explicitly estimated when no live match exists. Failed launches show their source status and suppress live/estimated orbit markers and fallback rings. A retained Launch Library orbit is labeled as the planned target, an absent ascent is reported as unavailable, and replay-only controls remain hidden unless the selected mission has a rendered track; authoritative supplied trajectory points remain visible when present. Mission orbit primitives realign by model matrix each tick from the same current-GMST frame as their marker, preventing ring/marker drift; their host annotation reads the position cache updated in that same tick. During depth-dominant ascent segments, the replay rocket retains its last valid path-facing rotation rather than snapping toward the camera. Mission selection and replay overlays never call photoreal sampleHeight() from the render loop; the launch-zone ground primitive and precomputed surface-safe replay path avoid remote tile-refinement probes that previously caused a one-second globe texture pulse. The shared host replaces the former mission-label visibility churn and quadratic overlap loop; the layer's remaining frame sweep only culls native point/billboard geometry and refreshes selected UTC copy when its displayed second changes. Replay samples uneven path vertices by cumulative distance and normalizes camera-yaw easing to frame time.
Orbit replay framing uses one combined bounding sphere for Earth and every sample of the selected orbit. The camera derives its final range from that full envelope, while its look-at target retains a radial bias toward the moving vehicle rather than collapsing onto the singular Earth-center frame. Compact-orbit launchers therefore remain tracked during camera rotation, and highly eccentric transfer orbits still keep both the globe and their distant apogee arc visible. The orbital camera stays on one side of the mission's 3D orbit plane and uses the vehicle radial as visual up, so forward motion remains screen-left through polar/local-heading wraps instead of alternating left and right. The fixed-size cyan orbit dot retains one pixel scale throughout the pullback. While replay owns the camera, the selected launch-site host label is suppressed so it cannot duplicate or overlap the replay vehicle's DOM callout; cancel/completion restores it.
Mission ascent paths use a long cubic insertion transition that matches the incoming climb direction and the sampled orbit tangent. Because a Cartesian cubic can otherwise chord through the ellipsoid for some inclined insertion geometries, every blended sample preserves the original climb's smooth minimum-altitude envelope. This removes the artificial right-angle insertion corner and corresponding rocket heading snap without allowing the ascent path to enter the globe.
Insertion is source-aware: catalog-backed missions propagate the matched satellite to the historical insertion epoch. Projected missions have no authoritative historical phase, so their orbital plane starts over the launch site and follows a plausible launch azimuth—south-southwest for western North American sites and polar missions, eastward otherwise. The projected insertion advances only by the disclosed ascent duration or a ten-minute fallback, producing one continuous downrange climb into the forward orbit tangent instead of using UTC as an arbitrary phase and correcting through a 180-degree hook.
The reconstruction does not add a full revolution around Earth: ordinary launch vehicles use a gravity turn and downrange acceleration before orbital insertion, rather than spiraling around the planet during powered ascent.
At close range, the selected launch site's 500 m highlight is a single material-backed GroundPrimitive classified against both terrain and photoreal 3D Tiles. It has no fixed world-space height offset, so the translucent disc and rim remain draped across the rendered launch-site surface during tile refinement. A small render-state polygon depth bias keeps coplanar ring fragments above the photoreal mesh at low oblique angles without making the geometry float or drift.
The Space Missions roster prioritizes data-rich records using available mission, orbit, payload, trajectory, timeline, and recovery fields; launch time remains the tie-breaker.
When no live catalog track is available, the mission view marks the approximate orbital ring as PROJECTED ORBIT in purple and renders the ascent-to-insertion transfer in green; catalog-backed satellite orbits remain cyan.
The replay vehicle is a smaller solid cyan silhouette without the former orange flame; its initial pad anchor uses photoreal terrain, globe height, or launch elevation fallback so it remains above the surface during tile loading. Replay begins at a close launch-complex range so pad detail remains visible before the camera widens into the ascent context view. Its initial camera heading is perpendicular to the ascent/orbit direction for a profile view, then eases into tangent tracking. Small screen-space reprojection changes are damped for the animated marker on both ascent and orbit, while large camera or phase changes snap to the authoritative path position. Space Missions keeps the Satellite layer available for catalog/TLE matching but suppresses its standalone fleet points and orbit rings. While those visuals are hidden, their per-frame dense propagation, one-second core point-buffer rewrite, and one-second orbit-matrix rotation are suspended; selected mission telemetry continues to propagate independently. The selected mission's live or estimated satellite marker uses fractional wall-clock time, so it moves continuously rather than creating a once-per-second position discontinuity and one-frame photoreal globe LOD pulse.
During ascent replay, the camera, Cesium callbacks, and HTML vehicle overlay share one replay sample per rendered frame. The tracked overlay is projected directly from that shared position instead of applying a second screen-space lag filter, preventing the vehicle and globe from repeatedly advancing and snapping back.
After the initial broadside launch profile, the ascent chase camera stays in a rear-quarter view about 30 degrees off the vehicle's forward path bearing, widening smoothly toward 45 degrees as orbit context appears. Cesium's HeadingPitchRange already places the camera opposite the supplied heading vector, so replay does not add a second 180-degree inversion; the trajectory therefore travels away toward the horizon while remaining visibly offset from the screen centerline.
During orbit replay, the camera continues following the selected vehicle but eases its look-at target down toward the vehicle's sub-satellite globe anchor. The range expands when necessary for high-altitude missions, keeping both Earth and the tracked label visible through the full revolution. The active replay clock clamps at the final orbital sample rather than wrapping to ascent progress zero; replay completion therefore leaves the final globe/orbit framing in place and does not return to the launch site.
Reconstructed mission orbits use a small downrange launch-to-insertion arc, so their estimated ground track is not artificially drawn directly over the launch pad in top-down views. The ascent remains connected to the ring at its selected insertion point.
Collapsed right-rail controls use the same 176 px width as collapsed left-rail controls, while expanded right-side detail panels retain their independent widths. DISPLAY starts expanded only on first run and then respects persistence; DISPLAY may remain open beside CCTV or Context, while CCTV and Context remain mutually exclusive without persisting forced collapses. Selecting a dedicated Context mode opens its right-side surface and clears unrelated layers after first snapshotting their exact state. Final exit restores the original enabled set and changed parameters. Cockpit View hides the right-side CCTV control because CCTV is not part of the cockpit rail. Airborne cockpit altitude uses the tracked aircraft's reported aviation MSL altitude, never the potentially negative Cesium terrain/ellipsoid render height; confirmed grounded contacts display 0 ft without rewriting that source field. A cold photoreal floor shows ACQUIRING SURFACE for at most five seconds, then uses the source target-height fallback instead of freezing the camera indefinitely.
Replay transport uses one Play/Pause toggle plus Cancel. During ascent only the active thrust ring is visible; stage-recovery handoff uses a pulsing dot.
Tooling Snapshot
tools/cesium-render.mjs: headless Cesium render capture via Puppeteer.tools/streetview-panorama.mjs: Street View tile panorama stitcher.tools/streetview-headings.mjs: heading sweep capture; supports neighbor traversal.tools/pano-pinhole.mjs: equirectangular-to-pinhole reprojection.tools/sat-ortho.mjs: Map Tiles ortho stitch and centered crop with georef corners.scripts/track-regression.mjs: headless real-app regression harness for aircraft tracking/model/detection invariants (npm run test:track).scripts/qa-map-source-tray.mjs: browser proof for the four-source Map Source tray — presentation, keyboard disclosure, responsive bounds, unpinned auto-dismiss, ACQUIRING status, and retired/unknown stack-id restore (QA_BASE_URL=http://localhost:4173 npm run qa:map-source-tray). Add-- --keylessto force the no-ion-token expectations on a keyed server; both invocations are gates.scripts/qa-l9-matrix.mjs: the L9 release-candidate QA matrix in one command (node scripts/qa-l9-matrix.mjs --url http://localhost:4173). Orchestrates theqa-*.mjsfleet plustrack-regressionas subprocesses and adds repo/feed/in-browser probes; a check whose key the target lacks is SKIPPED with an OWNER-RUN tag rather than failed. Run with--listto print the manual checks it cannot automate.
Maintenance Rule
When runtime behavior or architecture changes, update this file in the same change set as code updates.
Dependency security baseline
The lockfile uses DOMPurify 3.4.17, protobufjs 8.8.0, PostCSS 8.5.28,
source-map-js 1.2.2 and nanoid 3.3.19. Cesium remains on 1.138.0. Browser QA
uses Puppeteer 25.10.0; image-processing tools use Sharp 0.35.5. QA scripts await Puppeteer's asynchronous
executable-path lookup before testing or passing the path to Chrome. Supported Node versions remain
24.14.x and 26.x. Use npm ci to reproduce the checked-in dependency tree.
Traffic city navigation
Traffic checks the final camera view on arrival. Retryable road failures get at most three parked retries with 1.5–30 second backoff; permanent capability misses stop both the retry timer and enable-time kick. New views can try again. Leaving the traffic altitude range or disabling cancels outstanding work. Superseded road/flow responses cannot publish or refill cleared caches.
Optional frame-rate readout
Backtick (`) toggles an FPS readout beneath the title logo. It counts actual Cesium post-render events over one-second windows and does not request extra frames. Typing fields, modified keys and key repeats do not toggle it. The readout starts hidden each session and releases its timer and frame listener when hidden or when the application is disposed.
Radio components
The radio entry composes one layer from directory ingestion, station queries, selection, globe rendering and playback modules. Its source supplies directory metadata and click reporting; scene, ground and overlay services are provided explicitly. Each constructed layer owns its catalog, audio and lifecycle state. Existing catalog validation, category filters, tuning and voice playback behavior remain unchanged. Audio connects directly to the broadcaster after an explicit play action; the source does not relay or record streams.
Local RTL-SDR and Local ADS-B
The Radio panel holds a Local RTL-SDR card below internet radio. One browser
WebUSB session (src/sdr/controller.js) drives a USB RTL-SDR in desktop Chrome
or Edge on localhost or HTTPS; nothing opens until CONNECT. Demodulation and
Mode S decoding run in a worker; FM audio plays through an audio worklet.
Local FM and internet radio are one listening surface: starting local FM stops
internet-radio playback, and starting internet radio stops local FM. ADS-B
reception does not interrupt internet radio.
- FM seek. Seeking wraps across 87.5–108 MHz, covers both band edges and skips the starting station. Spectra carry a decoder epoch, so a retune or mode change rejects earlier output. A failed scan retune stops seeking. This integrates Aditya's (@Rohilalala) contribution in #1174; it does not change the peak-selection algorithm or guarantee that a final station retune succeeds.
- Streaming and playback. Four ordered USB reads keep input available while
the page handles other work; control changes drain those reads before touching
the receiver. The worker must finish loading before reads start. Decoder work
is limited to eight unacknowledged blocks / 512 KiB, with a one-second oldest
block watchdog. Overload stops the receiver with an explicit error. Stop
terminates the worker immediately and disables the audio session; stale decoder
output and audio from retired sessions are refused. FM audio starts after an
80 ms prebuffer and retains at most 256 ms, dropping the oldest frames on
overflow. FM signal display updates are coalesced to at most four per second;
spectra are computed only while seeking. Optional
decoderDiagnosticsadds bounded timing samples and overload snapshots without recording audio or IQ. - Modes. FM (87.5–108 MHz, tune, seek, volume) or ADS-B at 1090 MHz. With a receiver open, selecting ADS-B enables the Local ADS-B layer; selecting FM disables it. Enabling the layer switches the receiver to ADS-B; disabling it leaves the receiver mode unchanged.
- Gain. AUTO (tuner AGC) or a manual R820T step from 0.0 to 49.6 dB,
stored per mode in
gev:sdr:gain:v1. Defaults: ADS-B 28.0 dB, FM AUTO. Changes apply to the open receiver without reconnecting. - Receiver stats (ADS-B). CRC-valid messages per second, aircraft heard, aircraft with a fresh position and the IQ level.
- Error text. A receiver error sets two fields (
errorState()insrc/sdr/controller.js):messageis the UI language for the Radio card,errorTextis the English text the Local ADS-B layer reports as its statserror(localAdsbStatus(), falling back tomessagewhen a receiver sets none); the Layers panel translates it where it shows it. - Devices. An already-authorized receiver opens without the WebUSB
picker. ADS-B prefers a device whose product name matches ADS-B or 1090 (the
1090 MHz channel of a dual-channel board); FM avoids ADS-B/UAT channels.
A device chosen in the picker is remembered per mode by vendor, product,
product name and serial in
gev:sdr:device:v1. CHANGE DEVICE reopens the picker. LOCATE uses browser location for receiver-relative CPR decoding.
The Local ADS-B layer (local-adsb, src/layers/localAdsb/) is off by default
and registered as local-only: it never enters share links or stored layer
state. Its aircraft get the public Flights treatment in magenta, alongside
the public Flights layer. Public Flights renders about one poll interval
(~30 s) behind for smooth interpolation while local aircraft are shown as
heard, so a local marker usually leads its public twin; both markers are drawn
on purpose:
- Class. Each aircraft is classified with
aircraftClass.jsfrom its ADS-B emitter category (dump1090 strings:A1light,A3large,A7rotorcraft; set A/B/C/D = type code 4/3/2/1) and, once known, its adsbdb ICAO type. Silhouette and per-class scale come fromaircraftIcons.js(×0.8 on the ground), tinted magenta. - 3D. The DISPLAY rail's 3D toggle and its proximity/all mode apply: the
layer reads the Flights layer's
models3dparams and uses the same ceiling, caps, add/keep radii, visible-first eligibility (src/data/modelEligibility.js) and per-class GLB (src/layers/flights/modelSpec.js), tinted magenta through the same MIX colour blend Flights uses for military amber. Heights are barometric + geoid N, as Flights renders a contact without a geometric altitude; a grounded model rides the one-shot ground snap and belly offset. - Motion. No display delay. The marker is extrapolated forward from the
newest fix with
motionModel.js(arc extrapolation, path-derived course blended over the reported track, rate-limited slew); a new fix is absorbed by a correction that decays over 900 ms. Coasting continues while messages arrive and stops 10 s after the last one (60 s at most). - Sanity filter. For airborne positions the browser decoder prefers a
fresh even/odd pair, then a frame decoded relative to the aircraft's own
position (under 10 minutes old), then the receiver location. Surface
positions are stricter: a single surface frame decodes only against the
aircraft's own recent fix, and only when that fix is provably within 45 NM
(it could not have gone farther since at its speed limit); the receiver
location never decodes a lone surface frame and only picks the quadrant of
a surface pair. Without an existing suitable fix for that aircraft
(airborne or surface, provably within 45 NM), surface acquisition
requires an even/odd pair. Every fix
must be reachable from the last accepted one at 1.5 × reported ground
speed + 50 kt (1,000 kt without a speed, 350 kt for A1/A7/B1/B4) over the
elapsed time + 1 s, plus 500 m.
Refused fixes are counted (
positionsRejectedin the SDR state; the layer applies the same check to merged feed records and reports the total asrejectedPositionsin its stats). Three refusals in a row re-anchor. - Trail. A selected aircraft draws a magenta trail of the positions the receiver heard (up to 10 minutes / 600 fixes, dropped with the aircraft), with the tracked-flight trail look and a live head segment. No network backfill.
- Enrichment. Only the selected aircraft (type + route) and aircraft about to render as models (type) are looked up, through the Flights source's cached adsbdb proxy with the same once-per-session, four-in-flight, 200 ms drip rules.
A marker drops once its newest position is 60 s old; an aircraft is forgotten
after 60 s without a message. Clicking a marker opens a readout card (ICAO,
callsign, class and category, adsbdb operator/type and a plausible route when
known, altitude, ground speed, track, vertical rate, position age, message
count, "Heard by your receiver"); markers are not camera-followed. Positioned
aircraft join detection boxes, labelled only with a decoded callsign.
The card's TRK label wraps after rounding to whole degrees, so a track of
359.5° displays as TRK 0° rather than TRK 360°.
Receivers publish one record shape from src/sources/adsbRecords.js
(icao, callsign, category, onGround, lat, lon, altitudeFt,
groundSpeedKt, trackDeg, verticalRateFpm, lastPositionAt,
lastMessageAt, messageCount, rssiDbfs, band, source). band is 1090 or 978; source is
webusb (browser SDR) or feed (decoder feed).
normalizeDump1090Aircraft(json, nowMs, { band }) maps a dump1090/readsb/
skyaware978 aircraft.json document into the same records. Voice
set_layer_visibility accepts local-adsb ("local ADS-B", "my receiver",
"my antenna").
Decoder feeds. The layer's second input is the server route
GET /api/local-receivers/aircraft
(server/providers/local-receivers.js), configured only by the
LOCAL_RECEIVER_FEEDS environment value (band=url, comma-separated, e.g.
1090=http://localhost:8080/data/aircraft.json). Every host must pass
parseTapAddress (loopback, RFC1918, localhost, *.local), the scheme must
be http(s) and the path must end in aircraft.json; an invalid entry is logged
at startup, reported invalid and never fetched. The route reads all feeds in
parallel (2 s timeout, redirects refused, 2 MB cap, single-flight with a 1 s
cache) and returns { configured, generatedAt, feeds: [{ band, label, status, aircraft, ageMs }], records }, where a feed is live, stale (its own now
is over 10 s old), unreachable or invalid. Labels name the band only;
addresses and upstream error text stay on the server. Unconfigured, it returns
{ configured: false, feeds: [], records: [] } without fetching.
The browser session (src/layers/localAdsb/feeds.js) polls the route every
second only while the layer is enabled and only while the route reports
configured; the Local RTL-SDR card makes one probe request at startup to show
its read-only "Decoder feeds: 1090 live · 978 live" line. Feed records are
rebased to the browser clock and kept up to 60 s after their last message.
mergeLocalAdsbRecords merges both inputs by ICAO, keeping the newest position
(tie: newest message) and the bands/sources heard in the last 60 s; the card's
receiver line names them ("Heard by your receiver · 1090 MHz + 978 MHz UAT ·
browser SDR + decoder feed"). Aircraft heard only on 978 MHz UAT draw with a
thin light-magenta ring. With feeds configured the row status reads "2 feeds
live · 14 heard" when every feed is live. While any input is producing (the
browser receiver streaming, or a feed live) the row stays nominal and names the
other feeds as a trailing note ("3 heard · USB 5.8 msg/s · feed 1090 stale"),
since a decoder stopped so the browser can take the dongle is not a fault. With
nothing producing, every readable feed stale reads STALE and anything else is
an error. Without feeds it keeps the WebUSB statuses. See
docs/LOCAL-RECEIVERS.md.
Bundled geography and submarine cable components
Submarine cables use separate source, geometry, rendering, interaction and
lifecycle modules. The layer factory accepts cable and landing-point GeoJSON
collections from a source with fetch(signal) and a display label. The default
source loads the same bundled TeleGeography files. Disabling still removes all
three Cesium data sources; enabling rebuilds from the accepted parsed cache,
and destroying clears it. Load ownership prevents cancelled work from adding
entities after teardown.
Natural Earth regions and neighborhood polygon lookup are package exports.
Their existing lazy loaders, retry behavior, bundled datasets and attribution
are unchanged. The cable dataset remains CC BY-NC-SA 3.0 and is not covered by
the project's MIT license; see DATA_SOURCES.md.
Map source factories and coordination
Map selection is composed from separate imagery, terrain and 3D factories. The default registry retains Google 3D, Bing Aerial/Labels, Esri Satellite and OSM, including their setup guidance and existing preset/share IDs. Esri still falls back to OSM on construction failure or two active tile failures, and credits follow the source actually displayed. Terrain remains lazy while the photoreal globe is hidden.
Map-stack subscribers hear each settled activation once. A subscriber removed during a delivery, or every subscriber once the controller is destroyed, gets no further callback from it, and a destroyed controller accepts no new ones.
Map credentials are passed to each source constructor rather than changing SDK-wide Google or ion defaults.
Bing Aerial and Bing Labels resolve their ion imagery endpoint with the supplied
token, then construct the Bing provider with tileProtocol: 'https'. Both styles
request HTTPS tiles even on HTTP localhost, with the existing Content Security
Policy unchanged. Ion attribution and Bing's per-tile credits are preserved;
other ion imagery assets continue through the SDK's standard ion factory.
The controller accepts other source registries without adding provider branches. Each instance caches source construction, ignores superseded scene changes and releases imagery layers/listeners on replacement. Destroy invalidates pending work and releases owned resources, including late factory results. Supplied 3D tilesets remain owned by the caller; tilesets created through the controller's factory are added to its viewer and removed on destruction.
Live CCTV integration candidate (#489)
Daniel Slay's shared-decoder panel/projection feature is adapted to bounded
HTTP(S) HLS. DelDOT uses official HTTPS HLS URLs; RTMP/ffmpeg remux is excluded
because subprocess transport does not inherit the Node redirect policy. Server
sessions reserve synchronously, cap memory and response bytes, reject redirects,
and abort on release/shutdown. No disk store or background sweep exists while
inactive. The panel paints the same video at at most 640px/15 fps; collapse stops
its paint loop. Camera deactivation destroys the decoder, and fatal playback
errors revert to the labeled frame fallback. Camera changes clear the old panel
frame. hls.js is imported only when an HLS camera starts. Two live sessions may
coexist with at most eight independent consumer leases each; no-access lease
expiry is 15 seconds. Closing one consumer leaves other consumers running. Lease
ids use crypto.randomUUID where the page is a secure context and an equivalent
v4 id from crypto.getRandomValues on plain-HTTP LAN pages. Encrypted/fMP4/byte-range playlists fail
closed. Performance and live agency reliability require recorded validation.
Wind forecast models
Wind supports selectable GFS/IFS models with persisted model choice. Each fetch chooses the available cycle and forecast step nearest the current time; rows show both issue and valid UTC timestamps. Grid identities include model, cycle, forecast step and any requested scalar field. The six model/field cache slots each retain the last two issued grids for manifest/grid rollover, coalesce concurrent requests, retain last-good data on failures and retry no faster than once per minute. Model changes clear the old field immediately, cancel old acquisition and ignore superseded results. Last-good data is marked stale; the wind row shows it, and get_wind returns stale and adds "(forecast may be stale)" to its summary.
Local Weather review candidate
Weather combines the GFS/ECMWF surface forecast with three observation layers: NOAA Rain radar (CONUS), Satellite clouds (regional GOES or slower global infrared), and 15-minute lightning density (Americas and Pacific). One transient observed clock drives all enabled observation rows. Earlier/Later use the sorted union of non-suspended products' advertised times; each product selects its nearest frame at or before the requested UTC time, never a future frame or an interpolation. The maximum gap is 30 minutes for radar, regional infrared and lightning, and 3 hours for hourly global infrared. A missing eligible frame hides that product and reports the gap; rows show actual displayed UTC time with synced/nearest labels. Manifest rollover preserves the requested history time, even when frames expire.
Latest follows each product's own newest frame, so its timestamps can differ.
Playback advances two seconds after all products settle, wraps, and stops with
fewer than two union times or when all products are suspended. Hidden documents,
reduced motion and unavailable imagery hosts suspend products. Re-enabling or
restoring a host joins the retained history target. The wind row explicitly says
“Forecast · does not follow history” while the observed clock is in history mode.
The catalog exposes weatherClock and destroys it with its lifetime signal.
Source, opacity, coverage navigation and transport controls live in the WEATHER panel's cards and timeline, not in the rows. The viewer clock is untouched. Observation layers are off by default; share/persist includes appearance and product, while history is transient and shared links open latest. Rendering retains the previous frame when an incoming frame fails; no eligible historical frame instead hides imagery.
Desktop wind now allows 7,200 baked native GPU paths (narrow viewports remain at 1,200). This doubles the maximum desktop geometry budget, not forecast resolution. Temperature uses stronger fixed −40..50°C colors; the underlying 1° forecast and numeric inspection values are unchanged. No volumetric cloud height or local rain arrival prediction is claimed. NOAA source limits are documented in DATA_SOURCES.
Traffic source refinements: ALPR admits z9-z12 detail only when the view fits 16 tiles, returning explicit zoom guidance before I/O and allowing the next smaller view to load normally. Outside the US/Canada extracts the row reads “No ALPR data for this area — US and Canada only” and suppresses the nearby count. Military viewport membership checks every merged footprint. Different source identities require topological contact; identity aliases are zoom-scoped so coarse associations cannot join separated detailed parcels.
Country outlines use the lazy Natural Earth admin-0 pack. UK constituent countries use their own map-unit geometry; parent-country qualifiers stay separate from target aliases. The pack includes substantial islands. Names and aliases resolve offline; ambiguous Georgia follows the camera or an explicit country qualifier. Geocoded country boundaries require both a matching name and containment of the geocoded point.
Installation camera-load keys belong to their scheduled view; an older aborted request cannot clear a newer view’s pending key.
Name acquisition has a ten-second deadline and retry cooldown. Close views publish polygons immediately and enrich only the current view when names arrive. Military names load lazily from the separately licensed Overture/OSM pack when installation context needs them. Typed OSM ids join polygon fragments before merging; canonical group identity survives partial views independently of the largest named member supplying the title. Enrichment reconciles canonical ids to the named parent across zooms, migrating selection, context and marker ownership through explicit aliases. Geometry-union evidence stays zoom-scoped. Parent names and other member names remain available across same-zoom pans. Wide views use a viewport-culled, area-ranked grid of at most 512 point primitives and 24 decluttered titles. Wide-point and polygon titles share one world-overlay source using the same ambient-card entries, paint lane and persistent label arbitration as datacenters and dams, with a 24-title source ceiling. Camera changes reproject existing markers and titles without source loads or per-frame allocation. Selection promotes the record’s own entry to a selected card; clicking empty ground restores its ambient title without a second readout. Selected footprints receive a 22% outline-colored classification fill on Google 3D tiles or the active terrain, removed on deselect/disable and rebound on map-source changes. Labels follow the host’s Clean-UI behavior; disable/destroy clears the source and removes marker listeners. Named point ids carry into polygon views; wide named points remain until their close polygons publish. Unnamed polygons remain “Military area”.
Tile sources accept a separate tileFetchImpl for public vector tiles; API
requests retain fetchImpl. The exported Overpass ALPR source uses /api/overpass;
the standalone layer defaults to the hourly tile source.
OSM-derived layers share one OpenStreetMap data attribution entry. Displayed OSM data retains a short inline OpenStreetMap credit, plus OpenMapTiles while OpenFreeMap tiles are used; its display follows the existing map-credit rules. Routing, Warendorf cameras, datacenters, dams and the Nepal locator retain that shared credit while displayed.
Military fragments stitch by source identity. Different mapped parcels merge only when their footprints touch or contain one another, never across a seam tolerance gap.
Shared native dropdowns and numeric fields
- Scene and CCTV selects share the default
gev-fieldsurface. HUD layout and draw colour retain compact density. Radio filter, tuner gain and FM frequency use the same base while retaining their feature-specific skins and layout. - The CCTV inline number editor uses
createNumberInputwith its existing precision and inline density. Enter/blur commit, Escape cancellation, frozen camera base, camera-switch guards and disposal remain with CCTV controls. - Options, values, native keyboard/typeahead, disabled attributes and browser validity remain native. Frequency bounds, tuner commit timing and calibration parsing are unchanged. Specialized tuner graphics and calibration chips retain their own controls. Provider settings and scene-authoring dialogs are outside this change.
scripts/qa-form-fields.mjsexercises native selections, number validity, calibration commit/cancel and stale-camera/disposal guards against an installed controller with fixture ports, plus default/Cyber and narrow translated layouts.
Docker health reporting
The Docker image probes its local HTTP root with Node every 30 seconds, using
the launcher's parsed PORT (default 4173). A successful HTTP response marks
the container healthy. Checks time out after 10 seconds, with a 30-second
startup grace period and three consecutive failures before reporting unhealthy.
This reports app HTTP availability, not individual provider health, and does not
configure automatic restarts.