mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
Conflicts in es, ru, zh-CN and zh-Hant catalogs: both sides appended keys at the end (cyclone errors from this branch, routing refusals from main); kept both. Co-Authored-By: Claude Opus 5.5 <[email protected]>
6401 lines
573 KiB
Markdown
6401 lines
573 KiB
Markdown
# 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.
|
||
|
||
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](TOOLS.md).
|
||
|
||
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](TOOLS.md).
|
||
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, Operator 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.
|
||
|
||
## 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 and compact
|
||
header disclosure, 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](panel-surfaces.md)
|
||
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, 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, Scenes, 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-SHARING.md).
|
||
|
||
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](DIRECTOR-INTERACTIONS.md). 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](DIRECTOR-DATA-PACKS.md). 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](DIRECTOR-CAMERA.md).
|
||
|
||
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](VOICE-OWNERSHIP.md).
|
||
|
||
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 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](UI-OWNERSHIP.md).
|
||
|
||
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](SCENE-DOCUMENT.md);
|
||
see [Director](DIRECTOR.md) 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 Scenes. 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 Scenes, 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.
|
||
|
||
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.
|
||
|
||
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's sideways sway stays within a fixed range at any session age; Wind scales
|
||
its amplitude.
|
||
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.
|
||
Context expansion through the panel owner or adaptive right-rail restoration
|
||
closes an open compact Radio disclosure before Context/Space Missions becomes
|
||
visible. The mini remains available while Context is collapsed. This fixes the
|
||
Radio-overlap path reported in #804 without changing z-index or layout policy.
|
||
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 Scenes 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 Scenes 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](APPLICATION.md).
|
||
|
||
## 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](CODE-BOUNDARIES.md).
|
||
|
||
## 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](INFRASTRUCTURE-LAYERS.md) 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, compact, 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.
|
||
|
||
## Installations and map-source guidance
|
||
|
||
- Keyless mapped installations use OpenFreeMap `landuse` military 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 carries `landuse=military` only 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 the `landuse` layer. 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 camera `moveEnd`; 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 of `style.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, read
|
||
> `Needs FIRMS_MAP_KEY — add it in Provider Settings`. A layer declares which
|
||
> key it needs as a key-registry id and reports `stats.keyRequired` while 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 showing
|
||
> `LOAD FAILED`. A genuine lifecycle or fetch failure still retains failure
|
||
> priority.
|
||
>
|
||
> **Acceptance changed with that ruling (2026-08-23).** `qa-firstrun` no 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-cables` puts ~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=0` suppresses → `?welcome=1` replays (past both
|
||
> suppressions, for demos/support) → the durable
|
||
> `localStorage['gev:first-run-mission:v1'] === 'suppressed'`, written **only**
|
||
> by the "Don't show this again" checkbox → the per-session
|
||
> `sessionStorage['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-dismiss` button that calls
|
||
> the same `dismiss()` 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 `::before` outside 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 by
|
||
> `LayerStateCoordinator._commitExplicit` only for origin `user`/`voice`/`tool`).
|
||
> A mission enables **its own** layers at `origin: '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 by
|
||
> `src/firstRunExperience.test.mjs`.
|
||
>
|
||
> **Voice is instruction-only.** Both globe missions are expressible with
|
||
> shipped tools (`set_layer_visibility`'s enum already carries
|
||
> `local-datacenters`, `local-dams`, `telegeography-submarine-cables`,
|
||
> `local-firms`, `earthquakes`; `zoom_to_globe` supplies the camera), so
|
||
> `GEV_REALTIME_TOOLS` is **byte-identical to `main`** and pinned by sha256 in
|
||
> the unit suite. One instruction paragraph in `vite.config.js` teaches 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 `body` for 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 at `z-index: 200` against the card's `175`, 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** with `elementFromPoint`; 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 calls `preventDefault()` **and**
|
||
> `stopImmediatePropagation()`, and the launcher skips `defaultPrevented` events.
|
||
> `stopPropagation()` alone does **not** stop later listeners on the same
|
||
> `document` — that is exactly how the compact Radio disclosure made one key
|
||
> close the disclosure _and_ dismiss the launcher.
|
||
>
|
||
> **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 `setItem` reverts 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 `--teeth` negative control, which removes
|
||
> the launcher and requires EVERY launcher-dependent section to go red — it
|
||
> always exits non-zero, `1` meaning the control is healthy and `2` meaning 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 in `src/app/tools.js`,
|
||
> requests one frame through `governorRequestRender` after every layer
|
||
> disable and then trims those arrays. It reads private Cesium fields;
|
||
> `src/sceneCommandRelease.test.mjs` pins 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 call `governorRequestRender`). Any NEW
|
||
> per-frame visual animation MUST register a hold; any new discrete scene
|
||
> mutation MUST request a frame — `scripts/qa-perf.mjs` is the gate. The
|
||
> circular scope is an explicit canvas (`src/scopeMask.js`, DISPLAY-rail
|
||
> SCOPE toggle + FEATHER slider, hash keys `sc`/`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.
|
||
> 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 key
|
||
> `sce` pins 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):**
|
||
> `sourceState` in `src/data/militaryAwareness.js` treats 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/vessels` poll answers, so the manager's
|
||
> lifecycle settles to `enabled` — but until the server-side socket delivers
|
||
> a position, `firstConnectPhase` stays `'loading'` and `getStats()` reports
|
||
> `loading: true`, `lastUpdate: null`, count 0, and an UNDEFINED status.
|
||
> Without the predicate that window prints an all-clear `0`.
|
||
> - **Mapped installations never reaches that window.**
|
||
> `militaryInstallations.enable()` is synchronous and the manager awaits
|
||
> `update()`, which owns the first mapped-data fetch, so the lifecycle stays
|
||
> `enabling` for the whole fetch and the pre-existing `enabling` branch
|
||
> covers it. Confirmed live on :4272 across a held 17 s first fetch (34
|
||
> samples, `enabling` throughout, panel non-numeric) and across a failing
|
||
> one. Its `status: 'idle'` is not what saves it — the lifecycle is; the
|
||
> module has no `loading` status at all.
|
||
> Any new dependency that can be slow must report `loading` and `lastUpdate`
|
||
> honestly 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 consults `cachedMeshFloor` at the
|
||
> contact's CURRENT position: a measured floor more than
|
||
> `HELD_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, `displayFloorHeightM` only 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 fallback
|
||
> `sampleHeight` misses 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 what `MESH_FLOOR_BELOW/ABOVE_PRIOR_M` budget 15 m / 80 m for) and
|
||
> the contact's OWN cell only (a neighbour ~111 m away may be a terminal roof —
|
||
> `neighborFloorM` leans 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 on `airplane.glb` and 6.14 m on
|
||
> `jet.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.mjs` reads 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_NATIVE` stores RAW
|
||
> glTF coordinates and `modelAnchorWorld()` assembles
|
||
> `modelMatrix × components.transform × axisCorrection` — the same chain
|
||
> `ModelSceneGraph` renders with, built from Cesium's own exported
|
||
> `Axis.Y_UP_TO_Z_UP` / `Axis.Z_UP_TO_X_UP` and 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 by
|
||
> `modelMatrix`; that is half the correction (the defaults also apply
|
||
> `Z_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. `modelMatrix` carries 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 through `modelAnchorWorld` and 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 of `airplane.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.
|
||
> `radiusM` carries `computedScale`, 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_ANCHOR`
|
||
> mirrors `KEYHOLE_OUTSIDE_OPACITY_DEFAULT` (kept Cesium-free on purpose), the
|
||
> two are pinned together, and **the anchor MOVES WITH THE DEFAULT** — both are
|
||
> `0.01` since the 2026-08-24 final lock (`0.03` on 08-23, `0.05` before). That pin is the
|
||
> tripwire for a default move: the mapping is pinned to bracket BRIGHTNESS,
|
||
> not slider position. The `detection-opacity-slider` `step` is **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 in `detectionPolicy.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 a `renderAltitudeM`, 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 within `HELD_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 via `neighborFloorM()`, which takes the LOWEST of
|
||
> at least `NEIGHBOR_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
|
||
> (`displayFloorHeightM` only 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
|
||
> synchronous `Map` reads with no I/O — every DEM request is driven by
|
||
> `warmGroundFloor` from 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 at `FLOOR_EASE_MAX_STEP` so 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 an `on_ground` flap 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_KM`
|
||
> refuses 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, and
|
||
> `scripts/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; `tilesLoaded` goes 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) and
|
||
> `scripts/qa-floor-hold.mjs` (live, real GPU — the proxy is failed mid-run and
|
||
> the contact is measured against `scene.sampleHeight`). This floor-hold path
|
||
> currently applies to `flights.js`; `militaryFlights.js` does not use it.
|
||
> - **Screen picks are validated before conversion (2026-08-21):** anything that
|
||
> comes back from `scene.pickPosition()` must clear
|
||
> `isPickedWorldPosition()` (`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 throws `DeveloperError: 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 in `getViewTargetCartesian()` and
|
||
> `pickWorldFromScreen()` falls through to `pickEllipsoid` and then the globe
|
||
> ray, and callers receive the same `null` they 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 inside
|
||
> `requestIdleCallback` (now catches and reports once per viewer at
|
||
> `console.debug`), and `IntelHUD._updateSummary()` awaits its context INSIDE
|
||
> a guard, because every caller invokes it as `void 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 in `src/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 through `runImmediateNavigation('scene', …)`
|
||
> instead of a bare `camera.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's `getParams()` 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 on
|
||
> `SCENE_KEPT_SELECTION_PARAM_KEYS`; a name like `trackedVesselMmsi` therefore
|
||
> cannot slip through by not matching the older spelling. CCTV's
|
||
> `selectedCameraId` is the one recorded keep: it raises a monitor plane and
|
||
> never writes `viewer.trackedEntity` or 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 id `SCENE_EXCLUSIVITY_PROBE_LAYER_ID` is
|
||
> reserved by a test against the real layer registry. The exit itself is the
|
||
> ordinary `setContextMode('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 refused
|
||
> `setEnabled` is surfaced in the status line and a `shot_layers_refused`
|
||
> telemetry 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 an `await` only stops what has not started, so both awaited
|
||
> operations are now themselves cancellable. Layers: the run/LOAD owns an
|
||
> `AbortController` whose signal is passed to every `setEnabled`, and STOP,
|
||
> supersession, and run teardown abort it — the data manager rolls an aborted
|
||
> enable back through the module's own `disable()`, 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, and
|
||
> `mapStackController` invalidates a switch only when another `setStack()`
|
||
> arrives — a winning state that omits `mapStack` (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
|
||
> while `getSwitchGeneration()` 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-`await` flag checks remain as a backstop. Known remainder: run cleanup
|
||
> still waits on a suspended
|
||
> visual/map operation, because `setRecordingMode(false)` is not idempotent
|
||
> (a second call restores HUD `auto` rather 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. `screen`
|
||
> can 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 same `detection` slot, so callouts keep their z-position beneath every
|
||
> ordinary overlay card and beneath the tracked readout. Plate fills come from
|
||
> per-theme `calloutPlate` / `calloutPlateSpace` tokens (~63% / ~73% of the
|
||
> tracked card's `CARD_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 into
|
||
> `labelBg`** — 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 to `SKY_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 of
|
||
> `HORIZON_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, `1` means 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's `EllipsoidalOccluder` already
|
||
> 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-dock` is anchored at `2vh` down to 721px and
|
||
> re-anchors to a flat `8px` at 720px while `#cesium-credits` keeps its `2vh`
|
||
> base, so anything reasoning "the 2vh terms cancel" is only true in the
|
||
> 721–900px band. `src/creditAttribution.test.mjs` is a **fail-closed** cascade
|
||
> model: it flattens `style.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`/`margin`
|
||
> shorthands, 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 loses `var(--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.js` owns the copy for both), and every other camera
|
||
> destination invalidates the searched label — `_stampNavigation` covers
|
||
> voice/reset/takeover/selection, and scene playback calls the public
|
||
> `clearSearchedLocation()` per shot. `#command-dock` still hides
|
||
> `.location-mini-status` with `display: none !important`, so none of this is
|
||
> on screen; a `display:none` subtree 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. `reduceTrafficSyncFeedback` returns an empty
|
||
> `progressText` once the sync lands and `#traffic-sync-progress:empty`
|
||
> collapses the slot; the renderer writes the empty value rather than
|
||
> guarding on truthiness, or the busy `...` strands beside the settled label.
|
||
> - **`#active-style-name` has exactly one writer (2026-08-20):** the style-name
|
||
> mapping in `setStyle`. 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 `at` epoch-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 is
|
||
> `expired` only 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/vessels` reports `live | stale | reconnecting |
|
||
down | auth-failed` (plus the unchanged `missing-key`/`unsupported`) with
|
||
> `silentForMs`, `reconnectAttempt` and `nextAttemptAt`. Silence is REPORTED
|
||
> at 120s and ACTED ON at 300s; recovery walks a 5s/15s/60s/300s ladder and
|
||
> then stops at a terminal `down` with a slow 15-min retry running behind it
|
||
> (a retry never flips the chip back to "connecting" — only real data clears
|
||
> `down`). 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 honours
|
||
> `Retry-After` and 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 is `ws.terminate()`,
|
||
> never `close()` (the built-in WebSocket has no hard-abort and its `close()`
|
||
> 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
|
||
> is `src/data/aisStreamAdapter.js`, tested directly with mock sockets; the
|
||
> transport assumption is pinned in `src/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 reads
|
||
> `UNAVAILABLE`. Disable/re-enable starts a new isolated session. A socket
|
||
> with no received message or no usable positions does not advance
|
||
> `lastUpdate` or 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`, then `cancelled`, then `complete`) 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 as
|
||
> `LOAD FAILED`, not `LOAD 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-globe `zoom-in` guidance is
|
||
> not presented as loading. Terminal completion, cancellation, and failure
|
||
> labels are centered in that surface without an empty detail slot. The circular `RESET GLOBE` action sits beside the top-center share
|
||
> control in map view; Cockpit hides the complete action group and provides a
|
||
> cockpit-styled `RESET` beside `EXIT 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 Context `FOCUS` action 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, Snow.
|
||
> 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, Noir, and Snow. 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-stack` is 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 the `radar` symbol; 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-center
|
||
> `EXIT COCKPIT` control (offset downward by a `-95px` bottom margin) plus `Escape`/`C` own 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 past `max(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/Operator/Minimal 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 `!_contextModeChanging` so it fires at transaction settle, never at click
|
||
> — a failed activation cannot strand detection on. Policy in
|
||
> `src/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 past
|
||
> `max(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 Scenes 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. When Context
|
||
> is collapsed, its Radio header icon opens compact controls whose explicit
|
||
> Enable/Disable action owns power; stable close and full-panel buttons own
|
||
> compact dismissal and one-way detailed expansion independently. When Context
|
||
> is already expanded, that same icon skips the floating compact card, expands
|
||
> the embedded Radio section, scrolls it into view, and moves keyboard focus to
|
||
> its disclosure control. The detailed panel's normal accordion control owns
|
||
> its collapse. Compact/detailed state and playback continuity remain shared.
|
||
> Radio volume in the full, compact, 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 the
|
||
> `3D` status 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 LOST` state
|
||
> (`data-state="lost"`, the same panel-level cue mechanism as `uncertain`,
|
||
> 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 in `flights.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`, `militaryFlights` and
|
||
> `aisLiveVessels` each expose `hasContact(id)`: `true`/`false` in O(1) from
|
||
> the layer's own keyed map, or `null` when the layer holds no data and
|
||
> therefore cannot answer (disabled, or not yet loaded). Presence consumers
|
||
> MUST use it and must never infer absence from `getAllPositions`, 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".
|
||
> `null` leaves 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'`; `dim` and `partial` remain 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 with `400` at 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
|
||
> ceiling `rocketLaunchesProxy` already 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 full `npm run test:track` run
|
||
> drove 10 requests, peaking at 8/min.
|
||
> - **Terrain-height resilience:** `/api/terrain/heights` caches 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 with `Retry-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. A `Retry-After`
|
||
> longer than the 10 s retry budget ends the retries at once. After a 429 that
|
||
> outlasts its retries, the proxy pauses upstream calls for the `Retry-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/overpass` refuses 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 `#cesiumContainer` so its `screen` blend 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 and `gev-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.js` owns 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-hud` z145 — 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-hud` is 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; active
|
||
> `ambient-card` source 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 in
|
||
> `localGeojson.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, camera `moveEnd`,
|
||
> 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 call
|
||
> `setValue`, 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). The
|
||
> `KEYHOLE_OUTSIDE_OPACITY_DEFAULT` change 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 former
|
||
> `scaleByDistance` curve (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 publishes
|
||
> `ambient-label` entries on a dirty sweep with exactly two dirty
|
||
> conditions — camera `moveEnd` / 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 emit `moveEnd`) 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-frame `CallbackProperty`), 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-height` only 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.mjs` gates idle at ≤6 / 5 s.
|
||
> - **Phase 6 detection consolidation:** `src/data/detection.js` retains its own
|
||
> `LabelArbiter` instance 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 theme
|
||
> `mixBlendMode` and `filter + 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 `#cesiumContainer`
|
||
> at `z-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 its `mix-blend-mode: screen` silently 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,
|
||
> and `throttleSkipCount` remains a live diagnostic. Detection `data-*` 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-overlay `postRender`
|
||
> listener; the repository has five `postRender` listeners 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 `LabelGraphics` exception 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.js` is 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. `activeCameraCardEnabled` remains a product
|
||
> presentation control, and `_detectionUserOverridden` remains 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.json` permits
|
||
> 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-label` source in the shared world-overlay host. The source formats
|
||
> `M#.#` 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 `ISS` text 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 through `gevLabelModel` and `_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.label` Cartesian 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()` in `flights.js`; the same chain inline in
|
||
> `militaryFlights.js`) — tracked readout, detection card, `getNearby`,
|
||
> `getDetectableObjects`, `getAllPositions().label`, `getTrackedSubject`, the
|
||
> analyst record, the Context subject/nearest list, the Cockpit signal list,
|
||
> and the `track_entity` voice narration. Registration is aircraft IDENTITY,
|
||
> not route, so unlike origin/destination it is **not** routePlausible-gated.
|
||
> Identity stays `icao24` on every keyed surface (`getNearby().icao24`, the
|
||
> detection `sourceId` declutter hashes, and the `id` that `trackById` and 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
|
||
> `_trackedDisplayCached` publisher, 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,258 `CallbackProperty` channels per frame across 2,629
|
||
> reference entities and re-batched a 160-label `LabelCollection` per 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-160 `ambient-label` host cohort (`telegeographySubmarineCables.js`
|
||
> publishes 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 dedicated
|
||
> `submarine-cables` allocation 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 in `firmsHeatmap.js`;
|
||
> `firmsLabels.js` is 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-on
|
||
> `edgeFade: '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.js` retains 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.js` is formatting/policy-only: no vessel
|
||
> canvas, projection, paint loop, or post-render listener remains. Ambient
|
||
> vessels share `ambient-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.js` is 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 explicit `gevLabelModel` objects and expose
|
||
> `gevDisplayPosition` getters 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 fresh
|
||
> `entity.position.getValue()` in post-render. Annotation fade queries the
|
||
> host's actual `getOverlayPaintRect('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 the `STALE` grace 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.js` retains 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.js` is 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 option `cctvLayer.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 from
|
||
> `hitTestWorldOverlay` after 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 the
|
||
> `cctvCameraId` property 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 `navigation`
|
||
> across the cockpit. The grounded `airplane.glb` belly offset is calibrated
|
||
> to the current asset's measured Y-up bounds (`0.063` native 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 the `CONTACTS` and 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 test` 184 unit · `npm run
|
||
test:track` 43 tracking invariants · headless QA harnesses under
|
||
> `scripts/qa-*.mjs` incl. `qa-height-datum.mjs` (numeric heights) and
|
||
> `qa-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-verify` reported FAIL). The fleet
|
||
> pass and the tracked display path now re-floor the DISPLAYED coordinate
|
||
> against `cachedGroundFloor` — 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 within
|
||
> `ceil(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
|
||
> so `findByQuery`, `getTrackedInfo`, `getTrackedSubject` — keeps reporting
|
||
> sensor truth (barometric `altitudeM`, fix-time `renderAltitudeM`), 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.mjs` now **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 honours `QA_BASE_URL` and 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.js` is 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/firms` proxy
|
||
> (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-side `FIRMS_MAP_KEY` (keyless → 503 +
|
||
> in-app KEY REQUIRED chip). Client polls 10 min (`src/data/firmsHeatmap.js`;
|
||
> adapter `src/data/firmsAdapt.js`, CSV parser `src/data/firmsCsv.js`).
|
||
> `/api/firms/status` reports cache age + MAP_KEY transaction usage.
|
||
> - **2026-07-16:** Traffic supports optional live
|
||
> TomTom flow through the server-side, budget-governed `/api/tomtom` proxy;
|
||
> 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`/`adjust` voice
|
||
> 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**, **ru**, **zh-CN** and **zh-Hant**. The code lives in
|
||
`src/i18n/`. How to mark UI text: [`docs/I18N.md`](./I18N.md); translator workflow:
|
||
[`docs/TRANSLATORS.md`](./TRANSLATORS.md).
|
||
|
||
- **Keys are the English text.** `t('Save keys')`, `tn(count, '{count} camera',
|
||
'{count} cameras')`, and `msg('Live Flights')` for text stored in data and
|
||
translated later. Markup opts in with `data-i18n` (element text) and
|
||
`data-i18n-title` / `-aria-label` / `-placeholder` (that attribute's English
|
||
value). English has no catalog; every other locale is
|
||
`src/i18n/locales/<code>.json`, and a missing entry falls back to English.
|
||
- **Locale choice.** `?lang=` → stored choice (`gev:locale:v1`) →
|
||
`navigator.languages` → English. Chinese tags default to `zh-CN`; Traditional Chinese tags
|
||
select `zh-Hant`. Explicit `Hans` selects `zh-CN` regardless 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 with `registerUnsavedWork`:
|
||
unsaved keys or CCTV calibration, Draw marks, imported map files, the open
|
||
cockpit, a voice session.
|
||
- **Gate.** `npm run i18n` (and `src/i18n/catalog.test.mjs` under `npm 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 without `t`/`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:
|
||
|
||
1. `docs/CURRENT-STATE.md` (this file)
|
||
2. `docs/opensky-auth.md` (OpenSky authentication)
|
||
3. `CHANGELOG.md` (release history)
|
||
|
||
Historical planning documents may not match runtime behavior.
|
||
|
||
## Current Baseline
|
||
|
||
- Repository metadata and public URLs use the `bilawalsidhu/gods-eye-view`
|
||
project 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/` and `src/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.js` and `src/data/militaryFlights.js`
|
||
- Detection overlay and tracked-target readout in `src/data/detection.js`, `src/data/detectionDraw.js`, and `src/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) + 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) |
|
||
|
||
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](../DATA_SOURCES.md#gnss-integrity), 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.
|
||
|
||
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
|
||
`GroundPolylinePrimitive`s (`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:
|
||
|
||
```sh
|
||
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.
|
||
When Context is collapsed, explicitly activating its Radio header icon reveals
|
||
the compact transport; hover and focus alone do not open it. When Context is
|
||
expanded, the icon instead expands and scrolls to the embedded Radio section.
|
||
Its accessible label, controlled region, and expanded state keep describing the
|
||
current route throughout enabling, enabled, disabling, and uncertain lifecycle
|
||
renders. Both routes preserve Radio power, playback, station, filter, and volume
|
||
state.
|
||
|
||
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 `CONTEXT` chooser 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 `CONTEXT` view offers mutually exclusive `CONTACTS` and `SPACE MISSIONS` modes. Selecting `CONTACTS` enables the context-owned Flights, Military Flights, AIS Vessels, and Mapped Installations dependencies only when they are not already user-enabled; selecting `SPACE MISSIONS` enables the recent-launch layer and its Satellite dependency. `CONTACTS` cycles 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 `CONTACTS` becomes 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 SITES` retains 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 expanded desktop header omits the redundant `ON` label; the active mode button carries state. Expanded Contact results also omit the duplicate `GLOBAL CONTEXT` / `CONTEXT ONLY` status 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`, or `UNAVAILABLE`) while the metadata line retains the source and reason. A partial CelesTrak group failure keeps the usable catalog and reports `DEGRADED`; a total outage keeps last-good catalog data visible but reports `UNAVAILABLE`.
|
||
- 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 `NEARBY` or `UNKNOWN`; 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`, and `NEXT` controls 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 `BRG` and `CRS` labels wrap after rounding to whole degrees, so a direction of 359.5° displays as `000°` rather than `360°`.
|
||
- Installations are viewport-bounded OSM map features (`military=airfield|naval_base|range|barracks|base` and `landuse=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 `moveEnd` and 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 read `WITHIN 100 KM` instead of `CURRENT VIEWPORT ONLY`, and an empty window reads "none mapped within 100 km; not a complete 250 km survey". `scripts/qa-context-arrivals.mjs` flies 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 Scenes 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.lol `receipt − 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 cockpit `STALE`; 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), optional `setParams({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 reads `STATION · ISS`. `satelliteClassOf` is 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 as `visual`, and both surfaces still file it under STATION.
|
||
- Class is also a **text field**, not just a color: `NAV · GPS` / `GEO` / `COMMS · STARLINK` / `STATION · ISS` leads 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 optional `getRowControls()` layer hook): a **DENSE** chip exposing the existing `catalog` param, 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, and `DENSE ✕` with the reason on hover when the load fails — a failure also reverts `catalog` to `core`, 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 for `core` clears 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 optional `setRowControlsListener()` 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 `A0000` is 100000 and `Z9999` is 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.js` decodes the field through one shared helper: `noradIdFromSatnum()` for the satellites layer, which keys and deduplicates its catalog by that number, and `tleCatalogNumber()` 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, as `NaN` in the layer and `null` in the tools.
|
||
- **CCTV estimated bearings:** a camera whose pack marks `headingConfidence: 'low'` (the id-hash
|
||
`fallbackHeadingFromId` bearing) shows `HDG 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 carries `headingConfidence` and
|
||
`headingEstimated` (`src/layers/cctv/headingConfidence.js`, #639).
|
||
- **CCTV identified hardware:** a camera whose source or seed names a hardware `model` that
|
||
resolves in `src/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 as `FOV 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 show `SPEC FOV min–max° (CAPABILITY)` beside it. Public
|
||
camera state carries `fovSource`, `specFovMinDeg`, `specFovMaxDeg` and `spec` (#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 (`pickFromRay` on camera activation, clamping the plane short of the first hit) remains;
|
||
ground placement is superseded by the shared-floor v3 behavior below. Calibration persists to
|
||
`godsEyeView.cctv.calibration.v2` (wiped clean, no v1 import); a panel-only CAL badge shows
|
||
`CALIBRATED`/`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_SOURCES`
|
||
default 36 → 250, hard bound 300), filtered to `camera_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 needs `ONTARIO_511_API_KEY` and 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 switch `CCTV_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_SOURCES` is 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/cctv` keeps 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 · EDITED` chip) 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 with `scene.pick` first and uses `drillPick` only 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 stale `SNAPSHOT · OK` health
|
||
remains beside a pending preview. Grounding is shared with every other height consumer:
|
||
CCTV warms/resolves `groundFloor.js` cells, reads `cachedGroundFloor()`, and delegates optional
|
||
Google 3D refinement to the unchanged `meshFloorSampler.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 exposes `groundPriorM` as 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 on `camera.moveEnd` only, 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 explicit
|
||
`activeCameraCardEnabled` presentation 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_pass` preserves its geometric-pass ordering and appends visibility metadata. General `next_satellite_pass` accepts 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. `FOCUS` flies directly to a 12 km oblique frame around the selected launch site and retains the same anchored zoom/orbit behavior. `REPLAY ASCENT` resets 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 default `1×` rate. A live `0.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 as `CLASSIFIED / 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 with `ArcType.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 labeled `RECONSTRUCTED 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 ASCENT` holds the unframed cyan/white rocket at the pad for a real-time `T−10` countdown, transitions through `LIFTOFF`, 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 native `LabelGraphics`: 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 (including `gev-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/track` validates the hex, then answers `503 history_unavailable` without an upstream request.
|
||
- Shared `src/data/pickRegistry.js` stops 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.json` pins all existing
|
||
one-character assignments (all letters plus `0`, `1` and
|
||
`2` on current `main`) even if a layer is later removed. New layers allocate
|
||
the remaining unreserved single-character digits first (`3` through `9` on
|
||
current `main`), then the next unreserved two-character
|
||
base-36 token (`00` through `zz`) after rebasing onto the merge-time `main`;
|
||
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 from `origin/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 against `origin/main`.
|
||
- `scripts/qa-layer-token-twochar.mjs` simulates exhaustion of the earlier
|
||
digit slots and injects a no-network `00` layer 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: `00` remains 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 `ACQUIRING` progress 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 preempts `ACQUIRING`
|
||
for 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 as `Hip Hop`
|
||
and `R&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.
|
||
Its `aircraft` count is the exact civilian-plus-military total when both feeds
|
||
can answer, or `unknown` when either component is unavailable.
|
||
- Cockpit's top vision switch cycles all seven map looks: Normal, CRT, NVG,
|
||
FLIR, Anime, Noir, and Snow. Entry selects the matching map style without adding
|
||
a duplicate carousel item. There is no empty `NONE` entry.
|
||
|
||
### Live AIS Vessels (June 2026)
|
||
|
||
- Server-side `ws` websocket to `wss://stream.aisstream.io/v0/stream` maintained by Vite middleware; `AISSTREAM_API_KEY` never reaches the browser (AISStream has no browser CORS). The `ws` package 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/vessels` cache every 60s.
|
||
- The first enable in a session starts one 30-second client grace timer. Until
|
||
an accepted vessel position arrives, `live`/`open`/`connecting` transport reports
|
||
`LOADING`; the timer is not restarted by the 60-second poll. Expiry or a
|
||
definitive transport/credential failure reports `UNAVAILABLE`. 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 at `VITE_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 uses `OPENAI_API_KEY` by 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) to `api.openai.com/v1/realtime/client_secrets`. Both long-lived credentials stay server-side; OAuth minting is loopback-only. SDP exchange goes directly to `api.openai.com/v1/realtime/calls` with the ephemeral token.
|
||
- **Session defaults** (env-tunable): model `gpt-realtime-2` (or `gpt-realtime-2.1-mini` when the MINI tier is selected — see the model-tier entry below), voice `marin`, reasoning effort `low`, 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 by `server/providers/openai/tools.js`, executed client-side in `src/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`, and `next_satellite_pass`.
|
||
|
||
> **Reading `npm test` totals:** 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 on
|
||
> `main` + 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_camera` orbits/pans/tilts/rotates; `fly_route` is a cinematic dolly along an existing route annotation. `rotate_globe` rotates 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, while `rotate` turns 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-scoped `tool_choice: none` for 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-unavailable` asks 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-motion` zeroes 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 `interruptCameraMotion` zeroes 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; `signedTurnRad` branches 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 8 `sampleHeight` calls 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), with `scripts/qa-flyroute-mutations.mjs` reverting each fix individually to prove they are load-bearing (19 named defects); rendered proof `scripts/qa-flyroute-cinema.mjs` (drives the real voice runner, measures the real Cesium camera every frame, writes a labelled contact sheet).
|
||
- **Public control facade** on StyleManager (`setHudVisible/setHudLayout/setDetection/setMapStack/setBloom/setSharpen/setOrbit/setCleanView/getControlState` plus `runImmediateNavigation`): every setter syncs DOM sliders + share links + scene snapshots and returns `{ok, ...state}` — voice confirms only what actually happened. Validated `move_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 expose `findByQuery/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 against `CITY_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, so `scope:'selected'` silently answered `in_view` with a contact plainly selected on screen. They now call `selectTrackedSubjectContext` / `refreshTrackedSubjectContext` / `clearTrackedSubjectContext` (`src/data/contextStore.js`) on select / poll / deselect. **Invariants: (a)** that write path must NOT dispatch `gev: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_mode` accepts `contacts`; the internal id is `flights`. Every model-readable field (`mode`, `entering`, `priorMode`, nested `context`/`contextRollback`, and the transition diagnostic text) is reported in the accepted vocabulary, with the internal id preserved as `<field>Internal`; an absent secondary mode stays `null` rather than claiming to be `off`. Mapping lives in `src/contextModePolicy.js` so UI text and voice payloads cannot drift. Reporting the internal id made the model read `mode:'flights'` as "Contacts is off" and refuse to answer from the `contactsWindow` counts in the same payload.
|
||
- **Analyst → track handoff carries a key, not just a label** (2026-08-21). `analyst_query` items include `icao24`/`mmsi` alongside the display `id`, and contact lookup uses the shared tiered ranking in `src/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). `sendTextCommand` defers its `response.create` behind 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 unanswered `function_call` strands 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 — `_getTrackedFramePosition` returns 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 reports `complete: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 carry `window: {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_entity` is 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.
|
||
- **Visual grounding**: at `local` view scale with no structured identity, the client captures the Cesium canvas (≤1200px JPEG, black-frame detection, double-render for freshness) and sends it as `input_image` with 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-side** `truncation: { 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 the `STD`/`MINI` toggle 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 to `standard` rather than reaching OpenAI as a model id. Responses echo `X-GEV-Voice-Tier` / `X-GEV-Voice-Model` (plus `X-GEV-Voice-Tier-Fallback: 1` when a bogus tier was downgraded). Persisted at `godsEyeView.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()` to `stop()` — 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 the `error` status reports inactive while the transport can still deliver a late `response.done`. The toggle itself reads and writes only the persisted preference, never the live tracker.
|
||
- **Env overrides.** `OPENAI_REALTIME_MODEL` / `OPENAI_REALTIME_MODEL_MINI` remain authoritative per tier, so a drifted upstream model id is a `.env` fix 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.done` is 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 reads `Session ended — cost cap`. `0`/negative disables a threshold and round-trips through storage as an `'off'` sentinel (raw `Infinity` would JSON-serialize to `null` and 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. `extractFunctionCalls` yields 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 marked `incomplete`: 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`.
|
||
- **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-log` and appended to `.gev-logs/realtime-conversations.jsonl` (gitignored) with secret/image redaction; last 30 errors persist in `localStorage` (`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 in `coverage.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:
|
||
1. **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 both `analyst_query` and `get_current_view_state`, derived by `contactsWindowFromSnapshot()` 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.
|
||
2. **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.
|
||
3. **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_query` returns `scopeLabel` so this is mechanical. Two different numbers with named scopes are not a contradiction.
|
||
4. **The loaded-data caveat is stated once when relevant**: counts cover loaded data, and the flights layer loads where you look (appended to `coverage.note` for radius/view scopes over viewport-loaded layers).
|
||
- **Degradation**: API-key mode without `OPENAI_API_KEY` returns 503 as before. Selecting OAuth without a usable local ChatGPT/Codex sign-in starts the local `codex login` browser 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_HOME` before changing directories and refuses a mismatched `CODEX_AUTH_JSON`. GEV reads file credentials only, never writes or refreshes them, and does not require the vendor refresh token. Expired access tokens report `CODEX_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.mjs` checks 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_secrets` remains unconfirmed. Technical success does not establish a supported auth or billing contract.
|
||
|
||
### AI HUD Summary (June 2026)
|
||
|
||
- HUD `SUMMARY` readout requests a five-word intelligence-style summary from `/api/openai/hud-summary` (model `OPENAI_HUD_SUMMARY_MODEL`, default `gpt-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: `moveStart` marks the summary dirty and bumps its revision, as `moveEnd` does, 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.js` switches 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 (require `CESIUM_ION_TOKEN`), and OSM tile fallback. Bing Road is **retired**: it is gone from `MAP_STACKS`, from the `set_map_stack` enum, and from the voice aliases (road phrasings now resolve to OSM, the one shipped road basemap). An old `map=bing-road` link is simply an unknown id and takes `setStack()`'s existing photoreal fallback with the Google 3D tile lit — pinned live in `scripts/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-panel` is retired. The five tiles share one row on desktop and two rows on narrow screens, carry `aria-pressed` on 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 on `lastError`).
|
||
- 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 quote `getStacks().unavailableReason` — the same string `setStack()` puts in the toast. OSM works keyless. The `ION` badge is gated on the stack's own `requiresIon`, so a `photoreal` chip 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, the `set_map_stack` voice tool, and the chip row all land on the same `_setMapStack()` path.
|
||
|
||
### Voice Map Whiteboard / Annotations (June 2026)
|
||
|
||
- Runtime entry: `src/main.js` calls `initAnnotations({ viewer, tileset })`, exposes `window.__gevAnnotations`, and passes the engine into the voice action runner.
|
||
- Engine contract: `src/annotations/annotationEngine.js` owns 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.js` converts 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 leading `state 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-After` is 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 Alps`
|
||
and the `Alps mountains` forms 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 by `scripts/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.js` routes draped `area`/`route` geometry 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_map` and `clear_annotations` tools. 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 the `src/standalone/tools.js` shim) calls `initDrawTool({ viewer, annotations })` after `initAnnotations` and defers its `destroy()`, so disposing the shell returns every DOM listener, the Cesium handler, the preview data source, the borrowed click actions and the `window.__gevDrawTool` handle. Its markup is a DISPLAY-rail component template (`src/ui/templates/display-controls.html`), assembled into `index.html` at 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 and `manual: true` (`type: area` + `ring`, `type: route` + `path`, `type: pin` + coordinates). `resolveSpec` short-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 a `route` with `mode: '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 with `clear_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 alongside `sampleGroundHeight`, 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 with `clampToGround`, 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 only `CESIUM_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.mjs` counts 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. `finishSpec` repeats 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 km` rather than `1000 m`.
|
||
- **Measurements and centres are computed on unwrapped longitudes** (`unwrapLongitudes` / `wrapLongitude` in `drawMode.js`, used by `ringAreaM2`, `ringCentroid` and 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 or `null`; `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.mjs` and `src/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 harness `scripts/qa-draw-tool.mjs`.
|
||
- The annotation tree is a declared ownership group: `./annotations` → `src/annotations/index.js` in `package.json`, with its module graph listed in `scripts/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.js` and their tests are in `scripts/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`, `Basedata` and `Reference Material`. Each product has a chip in the row. The chip choice is kept in `localStorage` (`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 by `Export_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.js` starts with the first `/api/ghostmaps` request, 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) every `GHOSTMAPS_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_TOKEN` raises 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.json` and 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 `LIVE` while the stream is open. A five-minute catalog read is the backstop.
|
||
- **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 is `immutable`. 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 `CustomDataSource` 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 `createHorizonCull` before 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.
|
||
- **Reports.** Clicking a marker opens a card with the product, folder, edition date and the placemark's report fields. `src/layers/ghostMaps/records.js` reads 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 first `http(s)` research link opens from the card. The card is `stateless`, so it comes and goes with the click, as CCTV cards do. It is also `horizonCull`ed, so it hides when its marker turns behind the planet. Picks are registered under `s2-ghostmaps`.
|
||
- **Validation.** Unit tests: `src/layers/ghostMaps/*.test.mjs` cover editions, the catalog, ref parsing, description tables and wrapping; `server/providers/ghostMaps.test.mjs` covers commit detection, events, pinned downloads, cache pruning, restart and routing. `node scripts/qa-ghostmaps.mjs --url http://localhost:4173` covers the live load, the open stream, picks and cards (asserted on the overlay host's diagnostics, since the `desynchronized` overlay 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 in `dataSource.importedEntities` for 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 deferred `viewer.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.** `createApplicationTools` wires `initGeoFileImport` with viewer, navigation, ground services and map-source subscription. Destroy aborts pending work, unsubscribes, removes data sources, revokes blob URLs, removes document/canvas listeners and releases `window.__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.mjs` covers all four formats and real painted show/hide. Run `node scripts/qa-geo-import-regressions.mjs` against 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 with `CMR-Search-After` up to 2,000 records each, `truncated` against CMR `hits`), GIBS WMTS for tiles, Worldview Snapshots for thumbnails and exports. Worldview's `Data-Present` header 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; `boxFromPinAt` grows 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 via `reportBoxRefusal`) 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 to `Zoom in or draw a smaller box` and 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, slot `a`), `basemap` (that day on the left of a swipe, the basemap on the right: slot `a` splits LEFT and there is no second layer) and `ab` (day `a` left, day `b` right). `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; `b` only drapes in `ab`. 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. `rankLatest` picks 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 as `partial`; 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's `onEscape` interceptor wired in `createApplicationTools`. Pins, mode, box and split changes made in the panel are published with `adoptLayerParams`, never while a share link is being applied.
|
||
- **Rendering.** `rendering.js` owns at most two GIBS layers on the host `src/maps/imageryHost.js` resolves: `viewer.imageryLayers` while the globe shows, the photoreal tileset's `imageryLayers` while 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). Under `requestRenderMode` a 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 a `get` read 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`, owner `recent-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 reads `Imagery 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()` is `manual` for an outside `setStack`, `automatic` for 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 reads `Esri 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 reads `Drag 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-panel` is static rail markup in `src/ui/templates/context.html` beside `#weather-panel`, registered in `PanelLayoutController`, `PanelChrome` and the share panel state (`ui` token `i`), 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 (`createRecentImageryPanel` in `src/ui/recentImagery.js`) into its `data-rail-scroller` body, 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-text` with the refusal, error, CLEAR notice or the Esri note, and `#ri-zoom-in` in 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-mode` IMAGE · VS BASEMAP · A / B, then `#ri-slot-a` and `#ri-slot-b` with `#ri-unpin-a` / `#ri-unpin-b` kept 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's `scrollTop`; opening DETAILS then scrolls its card into view once.
|
||
- **Share links.** Layer options (token `1`): box edges as integers at degrees × 100000, pins `a` and `b` as compact days (`S20260918`), `m` mode (0 image, 1 basemap, 2 A / B), `p` split percent (share-link only: never stored locally) and `v` for the daily overview. Row chips: `MORE DETAIL · 30 m` (on) and `DAILY OVERVIEW · 250 m` (off).
|
||
- Regression surface: the colocated `*.test.mjs` beside each module (shared doubles in `src/layers/recentImagery/testDoubles.mjs`; `src/ui/recentImagery.test.mjs` includes 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 gate `scripts/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=1` gives 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 `3D` toggle, which continues to own the FLEET (the un-instanced draw-call budget stays the operator's decision). Policy lives in `src/data/trackedModelRegime.js` and is shared by both layers: enter below `TRACKED_MODEL_ENTER_ALT_M` = 150,000 m and hand back to the billboard only above `TRACKED_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-rail `3D` toggle 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 every `scene.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 writes `height + the model's measured belly offset` before 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, `heightFor` returns 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 is `HELD_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's `HELD_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 calls `forget`). **A loading model is HIDDEN, never zero-scaled:** admission sets `show = false` (Cesium's default is `true`, and an unplaced primitive would claim the visual at the identity matrix), and ownership is `ready && show` — Cesium 1.138's `Model.update` has no `show` guard, 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 `3D` toggle and `Proximity` / `All` modes. Since 2026-08-16 (Hangar fleet) models are PER-CLASS: real CC-BY GLBs for light/bizjet/turboprop/widebody/helicopter/uav (`CLASS_MODEL_REAL` in `src/data/aircraftClass.js` — meters-baked, scale 1, per-model belly/radius; provenance in `public/models/README.md`), the shared `airplane.glb` for the remaining civilian classes, and the military layer maps weight classes (real GLBs / 747 heavies / `jet.glb` fastjets, per-model heading offsets, flat amber or emergency red). Textured civilians carry a HEAVY tint, not a light one: `MODEL_COLOR_BLEND_AMOUNT` is `0.94` in BOTH layers under Cesium's `ColorBlendMode.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 full `1.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 each `Model` renders 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.mjs` pins the option at every drawn model call; the two preload calls only warm the glTF cache and are never drawn.
|
||
- The DISPLAY-rail `3D` toggle 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 `3D` toggle DEFAULTS ON in `proximity` on a first run.** Proximity is itself the budget — models appear only below `MODEL_ALT_CEIL_M` and only for the nearest `MODEL_MAX` in view — so the default costs nothing at globe scale, and `all` remains 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)` in `src/data/layerState.js`, `_models3dEnabled = true` in BOTH flight layers, `this._models3dEnabled = true` in `src/ui/aircraftDisplay.js`, and the `active` / `visible` classes on `#models3d-toggle` / `#models3d-mode-row` in `index.html`. All four are pinned together in `src/data/layerState.test.mjs`. Explicit state still wins: because the codec omits default-valued options, `models3d: false` is now what travels in a link (`lo=…f.e.0`) and restores OFF at both aircraft layers. **Consequence for returning users:** a stored `gev:layer-state:v2` blob is a FULL options snapshot, so a session that wrote one before this change carries `models3d:false` and 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 a `0.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 own `modelMatrix`; 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.trackedEntity` always 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:track` drives 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.mjs` pins 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 by `CockpitView.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 in `src/data/tr3bRegistry.js`: a session-scoped module-level `Set` keyed by ICAO 24-bit address, shared across both flight layers the same way `militaryRegistry.js` is, 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`) in `src/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 with `alignedAxis` still `ZERO`. They are unreachable from `classifyAircraft()`. Variant selection rides the existing `irBoost` layer 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's `aircraftClass` (`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_MAX` cap slot, with the handoff guard and the tracked-model regime guard kept as defence. The billboard stays shown, so the contact keeps satisfying the `getNearby` / `getDetectableObjects` visibility 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 through `setSplitFlapText()` in `src/splitFlap.js`; there is no other writer of those three elements. The progress counters (`#*-sync-progress`, `#global-loading-detail`) are deliberately left as plain `textContent` — they tick several times a second, and flapping them reads as a slot machine.
|
||
- `#global-loading-label` doubles 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, and `element.textContent` is the complete notice at every instant, so the `aria-live` region 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-lived `Text` node 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-text` span holding one long-lived `Text` node, plus an `aria-hidden` `.gev-flap-cells` sibling. After that the ONLY text operation for the life of the chip is `node.data = next`. Nothing is reparented, so the label is never transiently empty and the `aria-live` region never sees a removal/reinsertion pair it could announce twice. `element.textContent` is the settled string at every instant, because the cells carry no text at all: both glyphs are CSS generated content (`::before` from `data-flap-prev` = outgoing, `::after` from `data-flap-next` = incoming), which never reaches `textContent`. This keeps QA pins honest and lets `_updateTrafficSyncChip`'s own `textContent !==` 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 `setTimeout` per change.** CSS `animation`/`transition` only, 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 a `transitionend`/`transitioncancel` listener, 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. `setSplitFlapText` is 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_RATIO` must track the `gev-flap-out`/`gev-flap-in` keyframe 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 makes `visibleGlyphs()` 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-hidden` wrapper and the settled string is real text in the a11y tree, so the `aria-live` chips announce the label once per change rather than character fragments. No `aria-label` is used — ARIA prohibits naming a generic `<span>`. Because the text node is permanent and only its data changes, a label update is a single `characterData` mutation 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: reduce` does the same.
|
||
- Kill switch: `SPLIT_FLAP_ENABLED` in `src/splitFlap.js`. Set it `false` and 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.js` samples enabled layers through each layer's `getDetectableObjects()` 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.js` performs 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.js` publishes 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.sh` for stricter bindings, `dev-cctv.sh` for CCTV source-pack tuning)
|
||
- A successful Pinokio install writes the owner-only `pinokio/.installed`
|
||
marker. 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's `popout` option 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=localhost` in dev-fresh.sh); LAN is an explicit opt-in via `HOST=0.0.0.0` (launcher prints a key-exposure warning + LAN URL; see SECURITY.md)
|
||
- OpenSky default mode: OAuth (`OPENSKY_AUTH_MODE=oauth`; `anon` works without credentials)
|
||
- Setup doctor resolves `OPENSKY_AUTH_MODE` from the environment and dotenv files. Explicit `anon` and 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` (or `GOOGLE_MAPS_API_KEY`, or `.env`)
|
||
- OpenSky credentials expected in Keychain service `opensky-network` (or env, or `.env`); `OPENSKY_AUTH_MODE` and `OPENSKY_CREDENTIALS_FILE` read from `.env` too
|
||
- Optional-key precedence in `dev-fresh.sh` is uniform — explicit shell env, then `.env`, then Keychain: `OPENAI_API_KEY` (Keychain `openai-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` (`.env` only)
|
||
- An empty string is not "unset" on either side of the launcher, and both sides are handled. `scripts/read-dotenv-value.mjs` hides the requested key from `process.env` for the duration of the read (Vite's `loadEnv` otherwise 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 `.env` only over undefined variables, so an empty export in either place would shadow a configured key. `CCTV_CALTRANS_DISTRICTS` is the deliberate exception: empty is its documented Caltrans kill switch and is passed through as-is
|
||
- `.env` supported via `.env.example` template
|
||
|
||
### 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-summary` never relays OpenAI's own `error.message`, `/api/realtime/token` passes 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 health `message` — `GET /api/cctv/health` serializes 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_KEY` remains 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_KEY` is server-side only; the browser reads the same-origin `/api/vessels` cache.
|
||
- `/api/google/nearby-places` keeps the Google key out of Places requests issued for voice scene context.
|
||
- `/api/google/text-search` keeps the Google key server-side for view-biased Places recovery used by annotation resolution.
|
||
- `/api/overpass` is 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-installations` uses an independent limiter with the same 90-per-client/300-global one-minute bounds, so viewport installation refreshes never consume `/api/overpass` annotation capacity.
|
||
- `/api/route` proxies 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, otherwise `503 history_unavailable` with `Retry-After: 1800` and `Cache-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-log` enforces an always-on 120-per-client/400-global one-minute limiter (not the opt-in `GEV_RATELIMIT_OPENAI_PER_MIN` bucket the cost-bearing OpenAI routes share), appends asynchronously through a serialized queue, and rotates `realtime-conversations.jsonl` at 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+wheel` camera 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 remain `v6`. 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 `k` panel token that addressed it is gone from the share registry, so legacy `ui=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`) owns `DISPLAY`, `CCTV`, its active parameter controls, and `GLOBAL CONTEXT` as 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. `DISPLAY` is 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 `v6` collapse 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-glow` is pinned to its panel box there (`inset: 0` in `src/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 Scenes 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: clip` would also clip the Context radio popover and would not remove the vertical scrollbar. Pinned by `src/ui/panelRails.test.mjs` against every `(max-width: 720px)` block in the flattened stylesheet.
|
||
- `STYLE PRESETS` and `LOCATIONS` start 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_DEFAULTS` baseline 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 (`_detectionUserOverridden` gate), after which style switches never touch it.
|
||
- Detection runs in the bottom lane of the shared host's single world-overlay `postRender`
|
||
listener (not `preRender`) 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
|
||
unconditional `holdContinuousRender('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 by
|
||
`scripts/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
|
||
use `Date.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
|
||
`_drawOverlay` from 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, and `flights.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, so `detectionRenderDemand.test.mjs` pins 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 (`worldOverlay` sizing — 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` (the `trafficDebug` convention), resolved once per
|
||
`initDetection`. The same numbers are always available from
|
||
`getDetectionDiagnostics()`. 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 the `SECTOR <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 `Proximity` and `All` modes and drives both commercial and military aircraft layers. It ships ON in `Proximity`, 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 `C` shortcut 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 `COCKPIT` action (`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 Scenes 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 compact `CONTACT` rail 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 visible `CYCLE OFF` / `CYCLE ON` control 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`, or `EXIT COCKPIT` explicitly 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 Scenes 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 ONLY` in 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`, and `EXIT COCKPIT` controls replace reliance on the `C` shortcut. 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 compact `FROM → TO` airport 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 interactive `PREV / CURRENT / NEXT` carousel over `NORMAL`, `CRT`, `NVG`, `FLIR`, `ANIME`, `NOIR`, and `SNOW`; 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 than `N · 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 ON` opt-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 OFF` governs 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 ON` toggle 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:v2` in local storage,
|
||
written by `LayerStateCoordinator` on 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 at `100%`
|
||
- Sharpen: `ON`, intensity slider at `49%`
|
||
- HUD: `ON`, layout `tactical`
|
||
- **Detection: `DENSE` @ `75%`** — ON for EVERY style on a first run, Normal
|
||
included (was `OFF` @ `50%`). It is literally the same frozen
|
||
`MILITARY_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 by
|
||
`GLOBAL_POST_DEFAULTS`, so there is one tactical look rather than several that
|
||
can drift. Fade opens at `7%` since the 2026-08-24 final lock (`16%` before it). Style-switch semantics are
|
||
unchanged: CRT/NVG/FLIR still carry `detection: MILITARY_DETECTION_PRESET` and
|
||
still yield to `_detectionUserOverridden`; Normal still has no
|
||
`STYLE_PRESET_DEFAULTS` entry, so switching TO Normal touches nothing. A share
|
||
link carrying `dm=OFF` still restores OFF.
|
||
- **Detection OUTSIDE opacity: `1%`** (moved `5% → 3% → 1%` on 2026-08-24).
|
||
`KEYHOLE_OUTSIDE_OPACITY_DEFAULT` in `src/celestialRing.js`, mirrored by
|
||
`#detection-opacity-slider`'s markup value AND readout,
|
||
`GLOBAL_POST_DEFAULTS.detectionOutsideOpacityPct` in `src/ui/visualPresets.js`, and
|
||
`_detectionOutsideOpacityPct` in `sharelink.js`. The slider's `step` is now
|
||
`1`, 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_ANCHOR` in
|
||
`src/data/detectionPolicy.js` MOVES WITH IT — the AIR bracket floor is
|
||
calibrated so `AIRCRAFT_BRACKET_ALPHA_FLOOR` (0.35) lands exactly at the
|
||
default, and the mapping follows bracket brightness rather than slider
|
||
position. The `ko` PARSE fallback stays at `5`.
|
||
- **Scope: `ON`, feather: `100%`**. `SCOPE_FEATHER_RATIO_DEFAULT` in
|
||
`src/scopeMask.js` matches the slider, readout and share generator. The range
|
||
is 0–200%; explicit 0% is a hard edge. Missing-`scf` links retain the historical
|
||
35% parse fallback.
|
||
- **3D aircraft models: `ON`, mode `proximity`** — see the 3D Aircraft section
|
||
above and the DISPLAY-rail entry below (was `OFF`).
|
||
- Style shader starting params: CRT/NVG/FLIR pixelation `1.2` (just above the native `1.0` floor); thermal/FLIR ships an optional Ironbow "Predator" palette (`palette` uniform, default `0` = accurate grayscale).
|
||
|
||
## Operational Notes
|
||
|
||
- **Earthquake discs are STATIC geometry.** Every quake is a `CLAMP_TO_GROUND`
|
||
ellipse; a `CallbackProperty` axis 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's `layer-tick` / `layer-visibility` requests
|
||
carry new data to the screen. Pinned in `src/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. `country`
|
||
results 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
|
||
explicit `viewMode: 'overview'` ask bypasses the gate entirely so "show me an
|
||
overview of Hawaii" still frames the whole administrative area.
|
||
- **Viewport framing is antimeridian-safe.** `flyToViewportBounds` pads 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) and
|
||
`places_nearby` distances 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 old `v1` key).
|
||
- 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 at `godsEyeView.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 touching `flights.js`, `militaryFlights.js`, `detection.js`, or `trackedReadout.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().mode` is the
|
||
CONFIGURED source ('live' = a TomTom key is present, 'sim' = keyless), NOT
|
||
this instant's health — health rides on `error`. Keyless reads FALLBACK with
|
||
`SIMULATED — add TomTom key for live` in both the sync chip and the panel
|
||
meta line; an unreachable `/api/tomtom/status` reads
|
||
`SIMULATED — traffic service unreachable`; a total flow-fetch failure in live
|
||
mode sets `error` (DEGRADED · `SIMULATED — <reason>`) and zeroes the stale
|
||
coverage number. `stats.loading` covers 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 on `mode === 'live'` alone. When the drawn flow came from the proxy's
|
||
cached STALE-BUDGET / STALE-ERROR tiles, `getStats()` reports `stale: true`
|
||
and `oldestTileAt`, the row reads STALE with the oldest tile's age, and the
|
||
sync chip reads
|
||
`STALE · Roads: <source> · Flow: cached TomTom tiles` instead of LIVE.
|
||
- Traffic runs in `sim` mode (white dots, hardcoded speeds) unless `TOMTOM_API_KEY`
|
||
is configured (env or Keychain `tomtom-api`/`api-key`), which enables `live` mode:
|
||
TomTom flow vector tiles via the budget-governed `/api/tomtom` proxy
|
||
(`.gev-cache/tomtom/`, 120 s TTL, `TOMTOM_DAILY_TILE_BUDGET` default 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 (`roadMode` param, stored in the existing layer
|
||
state `lo` field, no new share token); `?trafficRoads=tomtom|osm|hybrid`
|
||
overrides 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. A `full`-coverage flow line carries
|
||
both travel directions, a `one_side` line 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 (TomTom
|
||
`Motorway`/`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 reuses `flowMatch.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. `flowCoveragePct` is 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
|
||
reads `LIVE · 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 (`surfaceBudgetOmitted` reports 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. `visitMotionDots`
|
||
exposes identity, road identity, primitive and source classification for the motion gate.
|
||
`scripts/qa-traffic-motion.mjs` records timestamped CDP frames and checks
|
||
cold/warm still-camera holds in all three road modes; `--baseline` also
|
||
measures rebuild churn in older runtimes using primitive object identities.
|
||
`--journey` adds 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 `tileJsonUrl` origin; an explicit `allowedOrigin` overrides
|
||
it. Repointing only `tileJsonUrl` loads 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=1` mint an interaction anchor
|
||
from the exact `camera.changed` event that arms each debounced load, then emit
|
||
scheduling-correlated User Timing entries for production `response.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's `moveEnd` remains 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
|
||
`-- --keyless` to 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
|
||
the `qa-*.mjs` fleet plus `track-regression` as 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 `--list` to 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 `decoderDiagnostics` adds
|
||
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()` in
|
||
`src/sdr/controller.js`): `message` is the UI language for the Radio card,
|
||
`errorText` is the English text the Local ADS-B layer reports as its stats
|
||
`error` (`localAdsbStatus()`, falling back to `message` when 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.js` from its
|
||
ADS-B emitter category (dump1090 strings: `A1` light, `A3` large, `A7`
|
||
rotorcraft; set A/B/C/D = type code 4/3/2/1) and, once known, its adsbdb
|
||
ICAO type. Silhouette and per-class scale come from `aircraftIcons.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 `models3d` params 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 (`positionsRejected` in the SDR state; the layer
|
||
applies the same check to merged feed records and reports the total as
|
||
`rejectedPositions` in 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-field` surface. 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 `createNumberInput` with 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.mjs` exercises 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.
|