# 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. User zoom is capped at 40,000 km (or a stricter host limit). Explicit camera flight, share-link and story destinations can go farther; from there, user zoom can't climb any higher and zooms in gradually from the current height, with no jump to the cap. Cesium can still use the limit when it computes an automatic bounding-sphere framing distance. Above 300 km, outward movement from the controller's configured zoom bindings (wheel, pinch distance and unmodified right-drag by default) pitches the camera down towards 8° below the Earth's limb. Large steps reach the limb immediately; the remaining margin eases into place, including after zooming stops or reaches the ceiling. Continuous outward inertia remains guarded without a time cutoff. Two-finger tilts, modifier-specific tilts and other camera gestures cancel the correction. A two-finger gesture counts as a pinch zoom only while the finger spacing changes more than the fingers move up or down, so spacing drift during a tilt is ignored. Explicit navigation handoffs, including toolbar tilt, cancel it before animation begins. Zooming in, disabled zoom/input, non-3D modes, tracked transforms and flights also cancel it; external pose changes cancel stationary settling. Oblique views below 300 km are left alone. `QA_BASE_URL=http://localhost:4173 node scripts/qa-zoom-out-guard.mjs` exercises the real browser controller at desktop and phone widths with keyless fixtures. Cesium's free look (Shift+drag, which turns the camera in place) works only below 1,000 km. Above that height a few drags could turn the whole planet out of frame and leave only stars, so Shift+drag does nothing there. Cesium also turns the camera in place for a drag that starts on the stars below its trackball height (7,500 km by default); that height is lowered to 1,000 km, so such a drag orbits the globe above it, as it does higher up, and a tilt (drag or touch) while the screen centre is on the stars there tilts about the planet's edge or does nothing, wherever it starts. Rotation drags that start on the planet, and zoom, are unchanged. How much of the frame shows the planet's surface is measured by casting a 32×18 grid of view rays at the ellipsoid; while the scope mask is on (the default) only the rays inside its keyhole count, since the mask hides or dims the rest. Voice tilt and in-place turns of a free 3D camera above 1,000 km stop when about a tenth of the frame still shows the surface (or about as much as it showed when it started), never less than about 1% of the visible frame, and a view that already shows less only keeps or gains what it has. A turn that cannot go half a degree further without crossing that limit is refused with a reason instead of reported as done, and a turn it accepts turns at least that half degree. A voice tilt to an angle (or a bare tilt, to -45°) that the limit stops short of is aimed at the angle where it stops, and its result names that angle (`pitchDeg`) with a `note`. A turn that keeps or gains surface is not refused, even after the window has been resized, except that near the limit, where the planet's edge crosses the sample grid, sampling wobble can occasionally refuse one (most often above about 15,000 km and on phone or ultrawide frames); nothing then turns away from the planet. A turn that starts where the last voice turn left the camera keeps that turn's limit, or a higher one if the planet now fills more, so repeating the request does not take any more; once anything else has moved or turned the camera, a turn is measured from what is on screen. When a camera move ends above 1,000 km with no ray on the surface, the camera turns straight down at the same position and heading. The end of a move is taken from Cesium's move-end event and from a check of its own that samples the pose every 250 ms after a move starts and acts once it has held for half a second, because at some of these poses the camera drifts by a rounding error every frame and Cesium never sees it stop. That check keeps watching until move-end arrives, recovering once after each move that comes to rest, and gives up five minutes after the camera last moved. Limb glow, and a surface sliver up to about one grid cell thick (some 50 px on a 900 px tall frame), can count as out of view. The zoom-out guard above keeps wheel and pinch zoom-outs in view; this catches the other ways a move can end looking away, such as a saved or shared view that did. Views with more surface than that, such as the oblique orbital Director shots, are left as they are, and so are tracked or locked cameras, a camera flight in progress, a Director scene while it plays (holds included), a pointer held down on the globe (a drag held still; with several fingers down, until the last is released) and the 2D and Columbus modes; a check a flight, scene or held pointer defers runs once it ends. A release outside the window that the page never sees counts as given once the window loses focus, the page is hidden, the canvas loses the pointer's capture, or the pointer moves with no button down. A Director shot loaded or replayed outside a run is not a playing scene. A view that loses its surface without a camera move (a resize, or turning the scope on) is left as it is until the next move. A voice turn requested while the camera is tracked is judged by the pose voice's release of the follow camera will leave, before that release, so a refused turn keeps the tracking. 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 shown animated dots, a pulsing jam heat-line, or road lines whose geometry is still building (replaced lines stay drawn until their successors are ready, so a rebuild never blinks); hidden dots (`dots` off) and settled flow lines hold nothing. 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. ## Wildlife Sightings — iNaturalist and Acartia **Wildlife Sightings** (`wildlife-sightings`, share-link token `9`) is off by default in the Wildlife group between Events and Weather. CONFIRMED and six taxa presets round-trip in share links. Voice is off for this layer for now; it is outside analyst queries and DETECT. Community observations are not a census; private records are excluded. `/api/inaturalist/observations` requests verifiable wildlife in view within 48 h (whales 7 d). Recency uses observation time: the `d1` date filter and a strict rolling cutoff. Date-only rows qualify only when their day starts within the window. Close views use a cached random sample of at most 200 rows; global samples use newest observations first. Observation caches last five minutes (global ten minutes), with stale fallback for at most one hour. Observations and grid tiles share a paced priority queue: a four-call burst, then one request per second, at most four in flight, ≤60/minute and a persisted 8,000/day budget. Superseded consumers leave before dispatch. The recovered queue tail isolates failures; ordinary grid failures do not fail observations. Provider 429 cooldown is shared; 5xx cooldown is resource-specific. Failed tiles retain good cells for at most one hour. Stale replies carry `X-Data-Stale` and known `Retry-After`. Failures keep filters and drawn rows, reporting DEGRADED with retry timing; logs exclude request URLs and upstream exception text. A partial failure still draws what arrived: a failed iNaturalist request, or one failed half of an antimeridian view, keeps a good Acartia reply and the other half, retains that area's last iNaturalist rows and reports DEGRADED. The update fails outright only when every requested source failed. Each failed source keeps its HTTP status and `Retry-After` (the HTTP status counts when no `X-Upstream-Status` is sent), and the next request waits out the longest 429 or 5xx cooldown among them. One update requests both sources, so the cooldown holds the whole layer; a fully successful refresh clears it. `/api/acartia/sightings` adds Salish Sea marine mammals within 45.5–50.5° N, 126–121° W, cached ten minutes and stale for at most 24 h. A valid empty snapshot, including one whose rows all fall outside coverage or the species checks, is a fresh success that clears earlier rows; only a malformed or failed upstream reply serves stale and starts a retry cooldown. WHALES accepts only recognized cetaceans; other identified marine mammals appear under ALL or MAMMALS. Sensitive iNaturalist locations remain 0.2° privacy cells; Acartia positions are rounded to 0.01° on the server. Acartia comments are never retained. Observer identity is retained only for photo attribution. `/api/inaturalist/grid` supplies a dedicated, nonpickable billboard density glow, with a view-relative density ramp from cool teal to warm amber. Overlapping Gaussian splats span four logical cell widths; mono and CRT styles follow map and cockpit vision changes. Empty snapshots paint nothing. Full footprints stay inside the globe limb with a 5% radius margin; opacity fades from full at 3,000 km to zero at 8,000 km camera altitude. Culling and cached coordinates add no per-frame work and leave sighting counts intact. Only real sampled observation rows produce taxon badges at their public positions. The badges use 28 px screen buckets, a 22 px overlap check and a 5,000-observation cap. Tinted circles have a white Material Symbols glyph and 1.5 px dark outline, remaining 12–22 px at all distances, including selection. Glyphs: birds `raven`, mammals `pets`, reptiles `egg_alt`, amphibians `water_drop`, fishes `set_meal`, insects `bug_report`, arachnids `pest_control`, plants `eco`, fungi `umbrella`, other animals `category`, unknown `question_mark`. A plain circle displays while the icon font loads. Row counts read “{n} sightings in view”, using visible grid totals when available. Views request at most six tiles, coarsening zoom for complete coverage. Server and client keep bounded 400-tile caches for 15 minutes. Cached, stale and adjacent-zoom tiles publish before network waits; each arriving tile replaces its covered area, including healthy empty tiles. Grid tiles publish even while the independent observation request is pending or failed. Enable waits for one camera settle; camera acquisition uses moveEnd with a 600 ms debounce. The documented UTFGrid endpoint's unspecified cell records have a schema guard; see `KNOWN-ISSUES.md`. Ambient cards use a fixed 96 px image width and a short creator name credit, with a bounded frame cache. New cards fill free screen regions; visible cards survive a random sample refresh until they leave the view or observation window. The 110/125 km show/hide thresholds provide hysteresis; cards scale and fade gradually through 120 km. Only painted photos suppress their own badge and label. Labels yield to painted cards, then compete by ambient score; repeated translated names within 80 screen pixels yield to higher-priority content. At most 150 labels enter the host. Titles prefer a word boundary for ellipses; accessible names remain complete. Selected thumbnails or names use the shared selected chrome and paint lane, with no canvas source link. Clicking a badge, label or photo flies to its supplied position and opens a compact anchored photo card. Selection stays pinned across camera, filter and sample changes until explicit deselection or layer disable. Its medium image uses `object-fit: contain`, with names, compact observed age and creator/licence. The card is the single source link to iNaturalist. Obscured sightings also stay text-only; Acartia stays text-only with provider credit and whale guidance, and its source link is always `https://acartia.io/`, never a contributed URL. Placement avoids actual HUD, toolbar and rail rectangles, panning only when necessary while preserving zoom. Escape, close or an outside click closes the card and clears selection. Camera/layout changes reposition the card without an idle frame listener; shell teardown also removes its host and subscription. Attribution appears in the layer row and Data attribution popover: “iNaturalist contributors · licences per record”, plus Acartia when drawn. Disable/destroy releases the glow collection, texture atlas, timers, queues and style listeners. Synthetic fixtures cover privacy, acquisition, rendering, selection and card placement. Deterministic operation checks run in `npm test`; wall-clock budgets run in `npm run test:perf`. All 11 locales are supported. ## Prohibited & Restricted Airspace — FAA special-use airspace The Infrastructure group includes **Prohibited & Restricted Airspace** (`drone-airspace`, share-link token `8`), off by default. `/api/drone-airspace` pages the public FAA Special Use Airspace FeatureServer for `TYPE_CODE` P (prohibited) and R (restricted), simplifies geometry, caches for an hour, and serves the last good snapshot as stale when a refresh fails. The globe draws ground-clamped outlines with a translucent fill: red for prohibited, orange for restricted. Selecting a zone shows name, an altitude range built from the `LOWER_*`/`UPPER_*` value, unit and reference fields (for example `SFC to 5,000 ft MSL` or `20,000 ft MSL to Unlimited`), times of use and controlling agency, plus a notice that the layer is informational only and not a substitute for official UAS flight-planning tools. Pick ownership is unregistered so sibling layer cards still work over a zone. Voice is off for this layer for now (`VOICE_OFF_LAYERS`: the Realtime prompt has no room), so neither voice nor the analyst can toggle or query it. The layer covers FAA prohibited and restricted special-use airspace only, not the UAS Facility Map / LAANC grids, National Security UAS Flight Restrictions or TFRs; the label and legend say so. See the live-layers table below. Vessel sources can report healthy empty coverage using a current, complete snapshot with zero raw rows, a healthy transport and a positive last-message time. This clears obsolete contacts and settles startup without manufacturing position timestamps. Stale, incomplete and malformed empty results retain the existing recovery behavior. Every `/api` response on the dev and preview servers carries `Content-Security-Policy: default-src 'none'; sandbox` and `X-Content-Type-Options: nosniff`. `apiResponseHeadersPlugin` sets them ahead of the Host check and matches `/api` the way the router mounts it (any letter case, a `/` or `.` boundary, absolute-form targets), so errors, partial responses, Host refusals and unknown routes are covered too; a route's own policy is sent as an additional policy, never in place of it. The app document's policy is unchanged. The GBFS proxy relays only bodies that parse as JSON, re-serialized as `application/json; charset=utf-8`; anything else answers 502. Catalog source contracts and their consuming layers are declared in `src/app/sourceComposition.js`. Omitted or null acquisition sources preserve layer registration and serialization, report unavailable, and are refused by the existing visibility lifecycle before initialization. Availability is held in an explicit catalog lookup supplied to the lifecycle, without modifying layer instances; unavailable reasons use the layer display name. Invalid supplied methods still fail construction. `createStandaloneApplication` and `createStandaloneCatalog` accept `sources` overrides; unspecified entries retain standalone defaults, and null entries explicitly remove a source. Street Level continues to accept an explicit provider list. An empty Street Level provider list uses the same unavailable lifecycle and disabled layer toggle. Directions (`getRoute`) and Recent Imagery (`searchHls`, `getThumbnail`, `getTileTemplate`) use the same source configuration boundary. Standalone selects their default adapters; layers retain route normalization, imagery selection and rendering. Recent Imagery adapters consume the existing HLS/VIIRS product schema and provide their own tile attribution. The source factories are exported at `layers/directions/source` and `layers/recent-imagery/source`. Configuration is applied at construction time; this does not introduce live source replacement. Application catalog composition accepts an optional `streetLevelProviders` list. An explicit list replaces the default Mapillary provider; an empty list removes all imagery providers. Without that option, Mapillary is registered only when `sources.mapillary` is supplied. An absent source leaves Street Level registered with a truthful no-provider state, preserving layer controls and share tokens. Malformed selected sources and provider definitions still fail validation. An explicit provider list bypasses the unused Mapillary source, including its validation. Layer share identities and the legacy Mapillary `m` option remain stable. Custom provider switches use the `r` option (`example-1*panoramax-0`), with URL-safe separators and a shared layer-neutral boolean-switch codec, sorted by provider ID rather than registration order, and round-trip through links and stored state. A receiver applies only switches for its registered providers; absent custom switches leave current defaults unchanged. Explicit provider toggles persist only their requested switch IDs through the state coordinator; unrelated live values remain transient. The custom switch field is bounded to 256 characters; provider registration rejects compositions that cannot fit rather than truncating identities. A malformed or duplicate entry invalidates the complete custom-switch field. Existing whole-link size limits still apply. ## Utilities panel — October 29, 2026 UTILITIES is its own left-side panel, between Data Layers and Scenes, with its own rail item (`handyman`). It is a `RailPanel` (`#utilities-panel`) in `src/ui/views/LayerPanelsView.vue` and holds the Directions layer row, which `LayerPanel` renders into `#utilities-toggles` instead of the Data Layers list, then Draw (`DrawControls.vue`) and Import (`GeoImport.vue`), which left DISPLAY with their ids unchanged. Without `#utilities-toggles` (a host that omits the panel) Directions stays in Data Layers under its Utilities heading. Radio, formerly the other row under that heading, stays in Data Layers under "Audio". Like the other side panels it follows the one-open-panel rule, remembers its state, and is not part of share links. It hides in clean view and recording. In Cockpit it stays with Directions alone, as Data Layers does; Draw and Import hide there, as DISPLAY always did, and opening Utilities folds the Cockpit context rail away as Data Layers does (and Contact expanding closes it). Voice `show_data_layers_menu` for Directions opens Utilities, the panel that holds its row, and `set_panel_open` reaches it as `utilities-panel` or by "utilities", "draw", "drawing" or "import". On a phone it sits collapsed in the Layers sheet under Data Layers. ## 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. `find_submarine_cables` lists and counts each cable once: TeleGeography splits a few cables (2Africa, Echo) into several features under one id, and such a cable matches when any of its parts crosses the area. Catalog calls check their cancellation signal immediately before executing a tool. Already-cancelled queries do not start; cancellation after execution starts continues to follow the existing tool and caller behavior. Fire-perimeter queries test polygon interiors and holes on continuous unwrapped longitudes, matching the short-edge convention used for boundary intersections. An outline crossing ±180° contains nearby boxes and radius areas without also containing their counterparts near Greenwich. Returned perimeter coordinates use a circular mean for longitude, including multipart outlines. Incident cards also use a circular longitude mean over the largest outer ring (excluding its repeated closing vertex). Their relative-area comparison follows short longitude edges so a small dateline-crossing part cannot outweigh a larger ordinary part. Regression coverage lives in `src/tools/area.test.mjs`, `src/tools/queries/environment.test.mjs` and `src/layers/perimeters/cards.test.mjs`. ## Scope display controls Display's native Scope checkbox defaults ON with feather at 100%, adjustable from 0–200%. OFF disables the slider while retaining its value. HUD layout changes preserve both settings; share links and scenes restore them. The historical missing-`scf` fallback remains 35%; explicit 0% keeps the hard edge. With Scope OFF, Tactical and Minimal add text-sized black backplates at 70% opacity behind telemetry, vertical metadata and scene chrome. Original HUD text color, glow and opacity are preserved. Scope OFF brightens the tagline and Active Style label with the shared secondary text color. The title plate covers the eye and title without moving the title or tagline. Scope ON retains the original HUD presentation; Cyber retains its existing skin. `node scripts/qa-scope-hud.mjs` checks native input, range, layout persistence, restoration and both Scope presentations. `QA_SCREENSHOT_DIR` saves screenshots. ## HUD layouts — October 29, 2026 Display > HUD > Layout offers Tactical, Minimal and Cyber; the `set_hud` voice action accepts the same three. The Operator layout is retired. Share links keep its enum code so older links still parse, and a link, scene or saved state that names Operator restores the default Tactical layout. Operator was the only layout that showed the HUD's top banner bar, so its markup and styles are removed too. ## Cyber HUD — September 30, 2026 Display > HUD > Layout includes Cyber, also available through the HUD voice action and shared visual state. An explicit first transition into Cyber selects FLIR with Ironbow 0.42. An explicit switch back to another HUD layout restores the visual preset used before Cyber; restored links and scenes remain authoritative. The skin uses shared red/slate panel treatments in map and cockpit, with compact 200px collapsed controls and wider expanded panels. Other HUD layouts retain their existing presentation. Chamfered panel expand/collapse controls use an inset keyboard-focus outline and red hover border in both map and cockpit, so clipping does not hide the highlight. Cyber's map-side right rail opens one panel at a time while keeping collapsed launchers available. Display, CCTV and Context scroll their contents inside fixed headers and decorative frames. The narrow-screen rail remains scrollable to reach each panel. Radio retains the shared nested Context player without relocating playback controls on theme changes. Voice help/error popups and Location/Visual Presets pins extend beyond their Cyber frames without being clipped: the shapes decorate non-interactive pseudo-elements. The lower-left telemetry card leaves room for attribution's full logo row. New panels can use the [shared surface contract](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 Audio. 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. A weather source can describe its own products (credit, units, coverage, legend) through an optional `display` object in each product manifest: `source` (≤60 characters, the row's source name), `credit` (≤200, the imagery credit in Data attribution, shown as plain text and updated when a refreshed manifest changes it), `units` (≤40), `coverage` (≤48, the coverage badge and info line), `info` (≤600, the info tooltip), `legend` (2–16 `{ label, color }` stops; label a plain decimal number or text, at most 16 characters either way, a number reading back exactly as the legend shows it: within ±2^53−1, with no leading zeros and no digits lost to rounding; color `#rrggbb`) and `extent` (`{ west, south, east, north }` degrees, non-empty; west greater than east crosses the antimeridian; used for coverage navigation and the outside-coverage status). `units` and `legend` describe one scale and apply only together: if either is absent or invalid, both keep the built-in values, and the radar info heading names the units in use. Text must be single-line, without control characters (U+0000–U+001F, U+007F–U+009F), line or paragraph separators, or invisible and format characters: soft hyphen (U+00AD), combining grapheme joiner (U+034F), Arabic letter mark (U+061C), Mongolian vowel separator (U+180E), zero-width and direction marks (U+200B–U+200F), bidi embedding and override (U+202A–U+202E), word joiner, invisible operators and bidi isolates (U+2060–U+2069), variation selectors (U+FE00–U+FE0F), byte-order mark (U+FEFF) and tag characters (U+E0000–U+E007F). Each other field is validated on its own; an absent or invalid field keeps the built-in value, so the bundled nowCOAST manifests, which carry no `display`, look exactly as before. Accepted source and units text is source data and is shown untranslated. The static Data attribution entries remain the application's `credits` list. 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. Explicitly selecting a storm publishes one shared `weather-cyclones` record: advisory provenance, source position, the English classification name and, while the proxy serves its cache, the cache fetch time. The automatic row selection does not claim shared context; refreshes do not re-announce it. Clear or disable reports `deliberate`; a storm leaving the feed reports `evicted`, with no fallback. 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. Vessel cards and the HUD wrap the direction after rounding to whole degrees, so a course of 359.5° or more reads `0°` (`0DEG` in the HUD) rather than `360°`. 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 `