Merge origin/main into fix/issue-362-opensky-doctor

This commit is contained in:
Sameh Khamis committed 2026-09-20 16:43:01 -07:00
commit a716b059b8
698 files changed
+189254 -23441

No files matched your search

+96
View File
@@ -100,3 +100,99 @@ Later component extractions can replace a constructor's internals without
changing the startup contract or moving standalone imports into the package.
Run the unit suite, package boundary gate, build, tracking and first-run browser
checks when changing this wiring.
## Geospatial services
`src/search` owns forward/reverse geocoding, place search and route provider
interfaces. `createStandaloneApplication({ geospatial })` accepts `endpoints`
and `providers`; the defaults preserve Google/Photon search, Google place context
and the local OSRM route proxy. Location, annotations, HUD labels and voice use
the composed service. Provider-specific credentials belong in the selected
transport; server secrets never belong in browser configuration.
For a compatible protocol, configure `geocode`, `photon`, `reverse`, `textSearch`,
`nearby` or `route` endpoints. These are developer-selected configuration, not
URLs accepted from page queries or model arguments. A different protocol supplies
an adapter function instead. The exported `createDefaultPlaceSearch` constructor
supports the same options for direct composition:
```js
const services = createDefaultPlaceSearch({
resolveApiKey,
signal,
endpoints: { photon: 'https://search.example/api/', route: '/api/routes' },
providers: {
route: routeProvider,
routeProfiles: ['foot', 'car'],
},
});
```
Forward `providers.geocode` is the existing ordered geocoder array. Other
operations are independent functions: `reverseGeocode(latitude, longitude,
{ signal })`, `textSearch(query, point, { signal })`, `nearby(point, { signal })`
and `route(coordinates, profile, { signal })`. Points use `latitude`, `longitude`
and `radiusM`; route coordinates use `[longitude, latitude]` in WGS84. Routes
return `{ geometry, distanceM, durationS }` with metres and seconds. Unavailable
routes return null, retaining the explicitly labelled direct-line fallback.
Place search returns normalized place arrays; reverse lookup returns address,
locality, region, country and label fields used by scene context. Unsupported
operations are reported in `capabilities`; adapters can provide attribution and
supported route profiles. A provider's lifetime and caller cancellation both
invalidate late response bodies.
Node middleware is configured separately. `googlePlacesContextProxy` accepts
`endpoints: { nearby, textSearch }`, `resolveApiKey` and `fetchImpl`.
`overpassProxy({ routing: { endpoints: { foot, car, bike }, fetchImpl } })`
selects compatible OSRM base URLs while preserving coordinate/span limits.
`regionalBriefProxy({ placeProvider })` accepts the regional lookup function;
`createRegionalPlaceProvider({ endpoint, requestJson })` supplies the existing
serialized Nominatim implementation. Configuration is per instance, including
routing and regional caches. Request parameters cannot override these endpoints.
### Voice connections and controls
Voice controls accept an action runner and a controller factory through
`createVoiceCommands` (`./voice/commands`). The default Realtime controller owns
microphone tracks, playback, push-to-talk, tool cancellation and radio handoff.
Its backend supplies `requestToken({ tier, signal })` and
`negotiate({ offerSdp, credential, signal })`. The Realtime-compatible adapter
(`./voice/realtime-backend`) accepts separate token and connection transports
and endpoints. Only the short-lived client secret reaches the SDP endpoint.
Stopping or ending the application lifetime aborts connection requests and
rejects delayed responses; a new start requests a fresh secret. Secret expiry
limits connection creation and does not describe the connected session lifetime.
`createStandaloneApplication({ voice })` passes these construction options to
the controls. An incompatible protocol needs a separate controller adapter;
changing an endpoint alone does not translate protocol messages. No alternate
model is bundled by this extraction. The Node Realtime provider accepts
`realtime: { endpoint, models: { standard, mini }, resolveApiKey, fetchImpl }`;
existing environment variables remain the default configuration. Model choices
come from server configuration. Keep secret keys in the server adapter.
Tool schemas, model defaults and cost estimates are unchanged. Unknown model
IDs retain the existing conservative estimate until their rates are registered.
### Composing the existing application components
`application/scene`, `application/controls`, `application/data` and
`application/tools` expose the existing globe, controls, catalog and tools as
separate constructors. Pass these to `createApplication`; use its `signal` and
`defer` for ownership. The compatibility catalog is still page-scoped: construct
one application per page. Controls, actions and renderers use the same layer
instances. Configure sources with `application/sources` before registration or
state restoration, and release them after the consumers stop.
`application/services` accepts boundary, terrain, regional-context, weather and
summary services. `application/requests` supplies the existing HTTP protocols
with configurable endpoints and a scoped transport. Changing an endpoint works
only for a compatible protocol; another protocol supplies a service adapter.
Cancellation discards late response bodies, and replacing a source invalidates
its pending results. Source disposal does not silently reinstate a default.
`ui/composition` supplies the default engines to the shell. Applications can
supply a HUD implementation or request policy through control services. The
shared chrome owns the welcome/loading transition; standalone composition adds
Provider Settings. `build/html` expands an allowlist of component markers from
`src/ui/templates`; unknown names cannot read arbitrary filesystem paths. The
standalone document expands to the same markup as before this extraction.
+296 -39
View File
@@ -1,58 +1,98 @@
# Formatting and component boundaries
Run `npm run format` to format the files in `scripts/format-scope.json` and
Run `npm run format` to format the files in `scripts/format-scope.json` plus
automatically discovered runtime JavaScript in `scripts/format-runtime.json`, and
`npm run format:check` to check that same list without writing. CI checks the
entire adopted list on Linux and Windows. Prettier is pinned in the development
combined scope on Linux and Windows. Prettier is pinned in the development
dependencies; use the installed version so local and CI output agree. The shared
configuration specifies two spaces, single quotes, semicolons and LF endings.
Add new reusable modules and their tests to the list as they are extracted.
Owned runtime modules are discovered automatically; add their tests and other
non-runtime files to the explicit list as they are adopted.
Keep mechanical formatting in its own commit after behavior is stable. Existing
source-text regression assertions still apply; investigate failures and preserve
their behavioral coverage when a move or line wrap changes a tested shape.
Files outside the list retain their surrounding style until deliberately adopted.
Files outside the runtime roots and explicit list retain their surrounding style
until deliberately adopted.
Generated output, local configuration, browser evidence and bundled datasets are
excluded. The formatter validates every entry before writing any file.
## Current component ownership
| Surface | Owns | Receives from its caller |
| -------------------------------------- | ---------------------------------------------------------------------- | --------------------------------------------------- |
| `gods-eye-view/infrastructure` | Datacenter/dam definitions and fresh layer construction | Context, overlay and render operations |
| `gods-eye-view/infrastructure/geojson` | Data loading, Cesium entities, selection handling and resource cleanup | A viewer and those same operations |
| `gods-eye-view/infrastructure/lod` | Pure visibility budgets and selection policy | Position/visibility records and camera measurements |
| `src/data/localGeojson.js` | Standalone compatibility wiring | The application's existing shared services |
| `src/main.js` and `src/standalone/` | Standalone browser startup | Local configuration |
Package imports use `gods-eye-view`; `package.json` is the authoritative
export inventory. Use declared exports rather than reaching into internal files.
The application and infrastructure exports are browser source modules. Use their documented
exports instead of importing standalone startup or reaching into internal files.
The application owns the viewer, context store, overlay host and render scheduler;
layers use the supplied callbacks. See [the infrastructure contract](INFRASTRUCTURE-LAYERS.md).
| Owner | Responsibility and lifetime |
| ---------------------- | ------------------------------------------------------------------------------------------------------- |
| `src/app/` | Construct supplied components, share scene/request services, cancel startup and dispose the application |
| `src/standalone/` | Select the default catalog, local sources and setup controls |
| `src/ui/` | Navigation generations, restoration, visual state, panel snapshots and their subscriptions |
| `src/data/` | Lifecycle, context and feed state; legacy default-layer facades |
| `src/layers/<family>/` | Source acquisition, records and Cesium resources with explicit controller/renderer owners |
| `src/sources/` | Portable protocols and source contracts; request state belongs to each factory instance |
| `src/services/` | Supplied application operations; scene construction owns caches and cancellation |
| `src/voice/` | Portable action schemas/session, common controls, action execution and separate protocol adapters |
| `server/providers/` | Node route factories, process-scoped provider caches and shutdown cleanup |
| `server/standalone/` | Environment, local settings writes and server composition |
`npm run check:boundaries` builds every declared package export, with app Vite
configuration disabled. `scripts/package-boundaries.json` lists each export's
component, owned modules and external runtime dependencies. A new export must be
classified. Imports outside the declared modules fail, including unused and
literal dynamic imports. Cesium stays external so the consuming application
supplies the same compatible instance as its viewer. Existing consumer tests
also check import-time inactivity and asset URLs under a non-root base.
See [application construction](APPLICATION.md) and the
[infrastructure contract](INFRASTRUCTURE-LAYERS.md) for construction interfaces.
These checks cover the declared exports, not every import in the application.
They check build-time imports, not arbitrary runtime-generated module URLs.
Keep runtime module discovery out of these exports. When extracting another
component, add its ownership and consumer tests together. Node services must use
separate entry points and their own checks when they become reusable; importing
them into a browser component is not supported.
## Import direction gates
`gods-eye-view/application` owns construction order, startup state, cancellation
and disposal of caller-supplied components. Its only owned module is
`src/app/application.js`. `gods-eye-view/application/viewer` separately owns the
standard Cesium viewer configuration in `src/app/viewer.js`; Cesium stays external.
Neither export imports standalone UI, layers, tools or configuration. See
[application construction](APPLICATION.md) for the contracts and current limits.
`npm run check:boundaries` runs two complementary checks:
UI panels and individual source adapters remain future extractions. They should
become smaller modules with explicit lifecycle owners as their callers migrate.
1. `scripts/check-import-directions.mjs` parses every runtime JS/MJS/CJS file in
`src/` and `server/`, including files unused by the current bundle. Static,
literal dynamic and re-export edges are checked; computed module imports and
CommonJS `require` are rejected. Browser graphs cannot reach Node, server or
test modules through helpers. Reusable modules cannot select standalone setup,
and provider modules cannot import application/rendering modules.
2. `scripts/check-package-boundaries.mjs` builds every declared export without
app Vite configuration or environment files. `scripts/package-boundaries.json`
assigns each export exactly once and lists its owned modules and external
dependencies. Unused imports still count. Node exports have only a `node`
condition; browser groups cannot use build-only dependency exceptions.
Portable source graphs cannot reach application/rendering, Node, Cesium or
browser globals. This includes `sources/*`, dedicated `layers/*/source` exports,
flight/military/vessel record and ingestion exports, action schemas, the session
interface, lifecycle and feed state. The browser-global rule reserves platform
names such as `document` and `window` in these modules; it is an architectural
check, not a JavaScript sandbox. Common voice controls cannot depend on a
Realtime protocol implementation. Negative fixtures cover indirect helpers,
self-package imports, symlinks and unreachable files.
Source factories have dedicated exports for ALPR, bikeshare, CCTV, earthquakes,
FIRMS, installations, launches, radio, satellites and traffic. They preserve the
same factory implementations without loading layer rendering. ALPR/earthquake
record normalization and CCTV source endpoint policy have plain owners separate
from geometry/cards. Source exports do not start acquisition at import time.
Other layer `ingestion.js` files may still coordinate Cesium resources; the
portable contract applies to the explicitly reviewed graphs above. Their layer
controller remains the owner of rendering/cleanup until a focused extraction
moves it. Do not label all ingestion modules platform-independent by filename.
## Compatibility entries and owners
| Retained entry | Owner and current reason |
| --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| `src/ui.js` | UI facade; direct callers retain standalone StyleManager defaults. Normal assembly uses `src/ui/composition.js` |
| `src/data/manager.js` | Lifecycle/panel compatibility and existing tests; normal assembly constructs lifecycle and presentation separately |
| `src/data/<layer>.js` | Legacy default instances; new assembly calls layer factories with supplied sources |
| `src/data/localGeojson.js` | Infrastructure compatibility factories; catalog imports services from `src/app/localGeojsonServices.js` directly |
| `src/app/sourceSlot.js` | Re-export for direct callers/tests; portable slot ownership is `src/sources/sourceSlot.js` |
| `src/app/sources.js`, `src/services/application.js` | Existing default/override API; current application construction supplies its own instances |
| `server/providers/local.js` | Existing Node composition barrel and provider helper re-exports |
The two exact standalone-import exceptions are `src/ui.js` to
`src/standalone/catalog.js` and `server/providers/local.js` to
`server/standalone/key-setup.js`. They do not authorize new compatibility
back-edges. Voice actions use plain `src/data/feedState.js`, not the manager
facade. Settings filesystem hardening belongs in
`server/standalone/key-setup-hardening.mjs`; its settings policy is unchanged.
Test modules and the Node-only allocation benchmark are outside browser runtime.
## Build and standalone server configuration
@@ -144,9 +184,14 @@ reconstruction mechanics with injectable acquisition dependencies.
`gods-eye-view/sources/traffic` exports tile math and budget calculations.
`gods-eye-view/sources/gbfs` exports host/path acceptance and cache-header rules.
These entries import no Node middleware, application configuration or rendering.
Callers retain their request admission and transport policy. FIRMS CSV parsing
remains an owned dependency of the FIRMS Node provider. The boundary gate
checks each entry independently.
Callers retain their request admission and transport policy.
`gods-eye-view/sources/firms-csv` exports the existing CSV parser, header
recognition, acquisition-time conversion and trailing-day filter independently
of the Node middleware. It imports no Node, DOM, rendering or network code.
The Node provider continues using the same implementation; contract fixtures
cover malformed rows, acquisition times, empty feeds and the inclusive time
window. The boundary gate checks each entry independently.
Browser terrain sampling, traffic matching/drawing, fire overlays and bike-share
layer lifecycle remain in their current modules. This extraction changes no
@@ -385,3 +430,215 @@ also checks scene ownership after asynchronous work, so late results cannot
replace a newer selection. Supplied tilesets are caller-owned; factory-created
tilesets and provider caches are controller-owned. Construct a fresh controller
for a new viewer or configuration lifetime.
The application-components group assembles the existing page-scoped catalog and
its engines. Its explicit module graph includes the smaller layer/UI groups;
those independent groups retain their narrower gates. Source adapters and
request services enter through construction, without replacing global fetch.
The Node build group also owns the allowlisted static HTML template assembler.
## Application catalog
`gods-eye-view/application/catalog` captures caller-supplied layer instances and
matching registration metadata. `application/data` registers that catalog, attaches
coordinators after registration and seals it before controls start restoration.
`application/controls` binds its layer services from the same catalog. The existing
control surface and v2 sharing codec retain their established layer IDs; changing
that schema requires a corresponding codec change.
`standalone/catalog` selects the existing page-scoped default instances and metadata.
Reusable data setup imports no standalone layer defaults. The current compatibility
source setters remain available while callers migrate to instance construction.
### Layer construction
`application/layers` constructs the current catalog from explicit source objects
and an application AbortSignal. Small `src/app/layers` modules wire existing scene
services into each family factory. Standalone provider selection lives in
`src/standalone/layerSources.js`. The construction export has its own checked
dependency graph, excluding standalone setup and compatibility layer instances.
Both aircraft layers share the catalog's classification registry; launches use
its satellites and Contacts uses its aircraft, vessels and installations. Data
registration, controls and voice actions read those same instances. Destruction
remains the manager's responsibility; classification also observes application
abort when startup has not reached registration. Scene engines remain page-owned,
so this change does not introduce multiple simultaneous viewers.
Direct `src/data` compatibility entries retain their old defaults and testing
exports. Browser regression probes use the registered instance's testing surface
to avoid accidentally inspecting an unused compatibility instance. Existing source
setters apply only to compatibility instances; the normal application supplies
its sources at construction.
### Application operations
`application/operations` accepts request-service instances and an application
lifetime. It constructs terrain resolution, coarse floor/mesh caches and an
annotation resolver without selecting upstream providers. Its checked graph is
separate from standalone setup. Scene construction returns these operations; the
catalog and controls use the same surface owner. Layers still own their individual
ground-snap caches and model resources.
Terrain cancellation rejects late replies before caching, clears floor queues and
removes the map-stack listener. Annotation lookup caches are instance-owned and
cleared on cancellation. Geometry selection and floor policies are unchanged.
HUD and weather controllers accept their respective service; regional lookup and
location framing use the supplied operations. Voice shares the same boundary and
floor services, with analyst memory scoped to the runner. Direct compatibility
entrypoints retain default services; normal assembly does not configure their
source slots.
## Layer lifecycle and presentation
`data/lifecycle` owns registrations, visibility intent, refresh transactions,
parameters and teardown. Its package group contains one module and no external
imports. Adding a panel, renderer or application dependency fails the boundary
build, including unused imports.
`app/layerPresentation` mounts the toggle panel and turns lifecycle activity into
render requests and detection invalidation. It owns hidden-panel refresh and
listener cleanup. Application data assembly constructs both owners explicitly;
`data/manager` is the compatibility facade for direct callers. The ordinary
state subscriptions retain their existing event contract.
## UI state owners
- `ui/navigation` owns navigation generations, pending search presentation and
tracking handoff. It reads Cockpit admission and uses supplied tracking operations.
- `ui/share-restoration` owns the initial restore transaction, layer coordinator,
status notices and gesture/timer cleanup.
- `ui/visual-settings` owns style preferences, detection overrides, display inputs
and IR cleanup. Its engine services and panel operations are explicit.
- `ui/panel-chrome` owns disclosure, docking, collapse preferences and temporary
Cockpit panel snapshots, composing the existing positioning/layout controllers.
These package groups contain their own dependencies and exclude application
assembly. `applicationShell` retains composition and compatibility methods;
those methods delegate to the state owner. Source-based regression checks inspect
that implementation owner, and browser acceptance exercises the assembled UI.
## Civil-flight records and acquisition
`layers/flights/records` owns metadata, sticky observations, geoid values and
missing-poll admission. `layers/flights/ingestion` owns the source and request
lifetime, backoff and freshness state. Both are portable package groups with no
Cesium, viewer, billboard, model or application imports.
The layer composes these with `snapshotRenderer`, which applies record changes
to Cesium history and primitives and coordinates existing follow operations.
Occlusion points live with rendering state. Eviction still releases tracking
before deleting records; incomplete snapshots retain recent contacts for the
existing bounded interval. The renderer retains its existing per-frame scratch
objects; reconciliation occurs on source refresh, not on each frame.
`layers/military/records` and `layers/military/ingestion` have the same portable
ownership boundary. Military observations retain their aviation-foot readout,
source-time fallback and model-owned ground policy. The renderer supplies that
ownership fact and handles stale-ground lifting, Cesium history and primitives;
record reconciliation does not import the engine or application.
`layers/vessels/records` owns plain AIS metadata, stable MMSI identity and bounded
retention. `layers/vessels/ingestion` owns source requests and feed state through
explicit operations. Both exports exclude Cesium, DOM and application assembly.
The snapshot renderer applies record changes; rendering owns a weak map of
geometry and billboard resources used by cards, picking and trails.
`gods-eye-view/sources/reference` constructs fresh earthquake and bundled cable
source instances independently of standalone setup. Individual sources remain
available through `layers/earthquakes/source` and `layers/submarine-cables/source`.
The latter retains the bundled dataset’s attribution and licensing requirements.
## Geocoding and feature-query providers
`createDefaultPlaceSearch` accepts an explicit Nominatim selection:
```js
const places = createDefaultPlaceSearch({
geocoding: {
provider: 'nominatim',
searchEndpoint: '/places/search',
reverseEndpoint: '/places/reverse',
},
fetchImpl,
signal,
});
```
The endpoints must return Nominatim JSONv2. They are supplied by application
composition; no instance is selected automatically and this does not install
those routes. For a remote instance, configure its full URLs and transport
requirements. The caller owns request scheduling, identifying headers and any
instance-specific usage policy. The existing public fallback retains its shared
queue, pacing and cache. `endpoints.nominatim` still names the legacy
Google-shaped `/api/geocode` fallback; it is not a raw Nominatim endpoint.
Coordinates and supplied presets remain offline. Explicit Nominatim selection
replaces the network forward-geocoder chain; it does not silently fall back to
another instance. Reverse geocoding is available only when a reverse endpoint
is configured. Other operations (Places text/nearby search and routing) retain
their independent providers. For custom compositions, `createNominatimProvider`
from `./search/nominatim` can be passed to `createPlaceSearch` and
`createGeospatialServices`; it exposes no route or nearby-search capability.
An empty search array is a definitive miss. HTTP failures, malformed results
and oversized responses remain retryable. Attribution identifies OpenStreetMap /
Nominatim.
Overpass currently supplies traffic road geometry, ALPR camera records, military
installation footprints, and annotation geometry (administrative boundaries,
neighborhoods, streets, building/grounds outlines and monument candidates).
Nominatim forward/reverse lookup does not replace those queries. Layer source
interfaces select traffic, camera and installation ingestion separately;
annotation and camera-framing consumers use the feature operations documented
below. The Overpass adapter translates those operations and normalizes records;
an alternate backend implements that source interface. Rendering and geometry
selection remain consumers of those results.
`./sources/nominatim` exports the lower-level JSONv2 client and normalizers for
server adapters without importing search composition.
`./sources/http-body` exports bounded text/JSON readers using web primitives;
`./sources/overpass` exports the quoted-string/comment lexer. The lexer alone is
not a query validator: spatial bounds, timeouts and other policy remain in the
server sanitizer. Existing server imports keep their compatibility exports.
Runtime formatting discovers tracked and new non-ignored `.js`, `.mjs` and `.cjs`
files under the configured owned roots. Tests retain explicit adoption. Git and
Prettier exclusions keep local/generated data out; resolved paths are validated
before any writes. New runtime modules do not need another scope-list entry.
## Map-feature sources
Application request services expose `features`. Callers may supply another source
with the same operations; the default is `createOverpassFeatureSource` from
`sources/map-features`. Legacy boundary-query configuration remains supported.
Search and annotation code request candidates and rank them without constructing
query-language strings or decoding backend tags and relation members.
| Operation | Result used by the caller |
| --------------------------------------------------------------------- | ------------------------------------------------------- |
| `getAdministrativeAreas(point, options)` | Named administrative candidates and geometry references |
| `getAreaGeometry(id, options)` | Geometry for one selected administrative reference |
| `getNeighborhoodAreas(point, options)` | Neighborhood polygon candidates |
| `getStreetAreas(point, options)` / `getStreetLines(point, options)` | Named areas and road lines for street annotations |
| `getFootprints(point, options)` / `getEnclosingAreas(point, options)` | Building and surrounding-ground candidates |
| `getMonuments(point, options)` | Named point candidates |
| `getFocusFootprints(point, options)` | Building/landmark candidates for camera framing |
Points use `{lat, lon}`. Options carry `signal`. A successful array, including an
empty array, is definitive; `null` means a transient failure;
`{rateLimited: true, retryAfterMs}` retains a provider's retry delay. Candidates
carry `id`, `names` (`primary`, `english`, `official`, `alternate`, `short`),
`coordinates` (objects with `lat` and `lon`), `building` and optional `heightM`,
`center` and `point`. Administrative candidates use `category: 'administrative'`
and a numeric `level`. Provenance may retain compatibility metadata, but ranking
does not use backend tags. The Overpass implementation retains its existing
operation-specific query radii and deadlines.
Traffic's source response decodes to `{roads}`. Each road supplies longitude/latitude
`coordinates`, a road-class `type` and `oneway` (`-1`, `0` or `1`). Scene construction
still owns thinning, terrain sampling and Cartesian waypoints. Installation
sources supply `{records, droppedCount, status, saturated}`; viewport filtering,
exact-bound retry and rendering stay in the layer. Legacy cache saturation is
decoded in the source. Source and geometry modules remain independent of Cesium,
DOM and platform middleware.
+632 -16
View File
@@ -1,5 +1,242 @@
# God's Eye View Current State
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.
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.
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.
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 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, transient and throttled outcomes remain distinct. 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. Default providers, footprints, road directions, exact-viewport retries and source attribution are unchanged.
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/result, interruption and completion events; stopping or replacing a session cancels pending actions. Alternate adapters can use the same controls and action runner.
Voice action argument schemas have one portable owner under `src/voice/actionSchemas.js`. The Realtime provider builds the same 28 tools using separate description-only metadata. Description customization cannot replace argument types, enum values or required fields.
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. 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, docking 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.
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.
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.
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.
The optional **ALPR Cameras** layer shows community-mapped OpenStreetMap locations,
not camera footage or plate records. City-scale queries use the existing Overpass
provider, with capped results, retry, cached-data and incomplete-coverage notices.
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 Overpass 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 retain native Cesium badges and ground-clamped
wedges. Unknown bearings never acquire a wedge. 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 and ground clamping 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;
@@ -217,6 +454,22 @@ 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.
## 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
@@ -342,7 +595,9 @@ The existing declared media size ceiling remains 64 MiB.
## GBFS upstream bounds
GBFS refuses upstream redirects and enforces its 5 MiB response cap while
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.
@@ -593,7 +848,7 @@ Non-object or array-valued properties reject the response instead of being treat
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: August 24, 2026
Updated: September 15, 2026
## Aircraft and vessel server modules
@@ -661,7 +916,13 @@ fetching, trailing-24-hour filtering and partial-success caching are unchanged.
> is present — everyone gets the same tile.
>
> Keyless, the honest surface is the **layer row**, which reads
> `UNAVAILABLE · NASA FIRMS · LIVE · KEY REQUIRED`, and the earthquakes half
> `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
@@ -1324,7 +1585,15 @@ This is the current runtime/source-of-truth snapshot for the project.
> 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 and Reset Globe to the right. Clear Layers turns off the currently
> 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.
@@ -1370,8 +1639,9 @@ This is the current runtime/source-of-truth snapshot for the project.
> 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, and Reset Globe actions are hidden
> for the duration of Cockpit, both as a group and as individual controls.
> 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
@@ -1481,9 +1751,9 @@ This is the current runtime/source-of-truth snapshot for the project.
> 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 and Reset Globe remain
> hidden until playback stops or completes because the Scene transport owns
> layer and camera sequencing for that interval.
> 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.
@@ -2250,14 +2520,311 @@ its criteria cannot be silently ignored.
| 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 | OSM Overpass (+ optional TomTom live flow) | `src/data/traffic.js` | `/api/overpass` + `/api/tomtom` | viewport-driven |
| CCTV | Austin + Caltrans (CA) + TfL London Open Data + Street View fallback | `src/data/cctv.js` | `/api/cctv` | 10s (active) |
| CCTV | Austin + Caltrans (CA) + TfL London + Ontario 511 + Fintraffic (FI) + DriveBC (BC) + TxDOT (TX) + Estonia (Tallinn, Tarktee) + Live Traffic NSW + Open Calgary Open Data + 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) | `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/localLayers.js` | — | static |
| Dams ▰ | OpenInfraMap/OSM extract (bundled) | `src/data/localLayers.js` | — | static |
| Submarine Cables ◠ | TeleGeography public map (bundled) | `src/data/telegeographySubmarineCables.js` | — | static |
| FIRMS Active Fires ▲ | NASA FIRMS live (VIIRS ×3 NRT, trailing 24h) | `src/data/firmsHeatmap.js` | `/api/firms` (`FIRMS_MAP_KEY`) | 10 min (proxy TTL 30 min) |
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.
**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 past that cap 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 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. Seven keyless regions are available: Boston, Austin, Minneapolis–St Paul,
Helsinki, the Netherlands, Norway and South East Queensland. 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. 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.
**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.
@@ -2601,7 +3168,16 @@ silently demoting every later lookup for the session.
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 — ~800 cameras total, all RAW PRIOR poses, stills-first.
sources. Ontario 511 (2026-09-12) adds keyless highway cameras including Kitchener-area routes
(cap 1,000, all enabled rows from the current ~944-camera catalog). 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.
- **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
@@ -2634,7 +3210,8 @@ silently demoting every later lookup for the session.
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). Zero-flicker:
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,
@@ -2664,6 +3241,7 @@ silently demoting every later lookup for the session.
- `/api/overpass` fans out across four public mirrors. `overpassPayloadIsData()` governs cache reads, writes, and stale fallback: only a 2xx that is neither rate-limited nor a body-level runtime error qualifies. Previously stored refusals are ignored on both fresh and stale reads, so upgrading does not require manually clearing the disk cache.
- HTTP refusals such as 406 now rotate alongside the existing network, rate-limit, and runtime-error cases. A refusal from one mirror no longer prevents reaching healthy alternatives or persists under the seven-day road/month-long boundary cache TTLs. Concurrent identical queries share one mirror sequence; if it fails, both the initiating and joined callers can use the same last-good data.
- A refusal every mirror agrees on is still reported with the first mirror's status and body, so a genuinely malformed query says what upstream said — but only after every mirror has had the chance to answer it. `fetchOverpassPayload` takes injectable endpoints and fetch so the rotation is tested without a live mirror (`src/overpassProxy.test.mjs`).
- Every mirror is asked with a User-Agent that names the application, its version and the project address (`OVERPASS_USER_AGENT` in `server/providers/overpass/constants.js`), which is what the OSM API usage policy asks for. Mirrors may refuse a client they cannot identify; such a refusal is never treated as data and costs the fan-out that mirror, so the header is what keeps the list at full strength.
### Share-link v2 layer state (August 2026)
@@ -2838,8 +3416,28 @@ silently demoting every later lookup for the session.
Location search/fly-to, annotations and Radio location lookup receive one
`placeSearch.geocode(query, { bias, signal })` service. `src/standalone` composes
Google first when configured and Photon/OpenStreetMap as fallback, including
Google transport failures or declined requests. The portable `./search` export
two offline providers first — a strict decimal-degree 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), and 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.
@@ -2875,6 +3473,23 @@ are omitted rather than framing the wrong part of the globe.
- 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.
- **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`.
### 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.
@@ -3179,7 +3794,7 @@ easier to meet (detection is now on more often), but does not create it.
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.
- Draggable panel positions persist at `godsEyeView.v7.panelPos.<panel-id>` (collapsed states at `godsEyeView.v6.panelCollapsed.<panel-id>`).
- 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`.
@@ -3200,7 +3815,8 @@ easier to meet (detection is now on more often), but does not create it.
- 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 40k/day),
(`.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 (`flowTiles.js`), matched onto Overpass roads
(`flowMatch.js`), and rendered as green/amber/red dot color + speed/density
scaling (`trafficFlowStyle.js`); closures spawn no dots; unmatched roads stay
+110
View File
@@ -0,0 +1,110 @@
# Director camera directions
Version 4 scene documents can name geographic anchors and describe an explicit
camera move followed by a hold. Existing camera-pose shots still use the ordinary
camera flight. Import, playback, Load, Replay and seek accept the new directions;
[EDIT DETAILS](DIRECTOR-SHARING.md) captures anchors and move starts; a graphical
path editor remains a later extension.
```json
{
"version": 4,
"scenes": [{
"id": "austin",
"title": "Across Austin",
"anchors": [
{ "id": "west", "title": "West viewpoint", "lat": 30.2672,
"lon": -97.76, "alt": 6000, "altitudeReference": "ellipsoid" },
{ "id": "east", "title": "East viewpoint", "lat": 30.2672,
"lon": -97.73, "alt": 7000, "altitudeReference": "ellipsoid" }
],
"shots": [{
"id": "crossing",
"title": "Cross the city",
"durationSec": 4,
"holdSec": 2,
"camera": { "anchorId": "east", "heading": 10, "pitch": -45, "roll": 0 },
"move": {
"from": { "anchorId": "west", "heading": 350, "pitch": -45, "roll": 0 },
"easing": "cubic-in-out"
},
"visual": { "style": "normal" },
"layers": {}
}]
}]
}
```
## One owner for each authored value
An anchor is a scene-local geographic position, with a stable `id`, optional
`title`, latitude, longitude, height and explicit altitude reference. Each scene
accepts at most 1,024 anchors. IDs must be unique within that scene. References to
missing anchors fail validation before replacing a project.
The shot's `camera` is its destination. It can be an inline pose or an
`anchorId` plus orientation. The optional `move.from` uses the same pose/reference
shape. References cannot also carry inline coordinates. They do not form chains.
Heading, pitch and roll belong to each camera pose, so two shots can look in
different directions from the same geographic anchor. Omitted orientation uses
heading 0, pitch −35 and roll 0 degrees.
An explicit move requires `move.from`, `move.easing`, `durationSec` and `holdSec`.
Its duration is 0.2–86,400 seconds; the hold is 0–86,400 seconds. The destination
is not copied into another field. `linear` and `cubic-in-out` are the supported
easings. A shot without `move` retains the existing ordinary flight, even in a
version-4 file. Its destination may also reference an anchor.
Capture continues to create ordinary camera-pose shots. Update Shot stores the
current viewport as an inline destination, preserving an explicit move's start
and easing; it does not change a shared anchor and thereby move other shots.
Editing imported directions and anchors currently means editing the scene JSON.
## Coordinates and altitude
Latitude/longitude use WGS84 degrees. Orientation uses the application's Cesium
heading/pitch/roll convention in degrees: heading rotates from local north,
negative pitch looks down, and zero pitch is horizontal. Camera positions use
meters above the WGS84 ellipsoid, matching `Cartesian3.fromDegrees` and captured
camera poses. These heights are not mean-sea-level elevations or heights above
the visible surface.
Every anchor and every inline endpoint of an explicit move must say
`"altitudeReference": "ellipsoid"`. Ordinary legacy poses without that field
already use the same reference. Terrain-relative and mean-sea-level references
are rejected; no terrain request, default ground estimate or implicit conversion
is made. Authors must provide ellipsoidal heights with enough clearance for their
scene. The format does not promise collision avoidance or terrain following.
During an explicit move, latitude and altitude interpolate between endpoints.
Longitude takes the shortest signed arc across the dateline; heading and roll
also take their shortest signed arcs. An exact 180-degree tie takes the negative
arc. This is an authored coordinate interpolation, not a geodesic or a camera
flight with an automatically raised arc. Endpoints are exact, including zero
pitch. Curved paths and look-at targets are later extensions.
## Time, seeking and cancellation
Load and Replay of an explicit move start at its authored `from` pose, regardless
of the current viewport. Play uses that same start. Both live playback and seek
use `sampleCameraMove`; a seek into the hold uses the exact destination. Backward
seeks do not depend on a prior camera position. Existing scene-pack media/reveal
rules can extend holds as before.
Each active explicit move owns one animation-frame callback. Stop, replacement,
import, teardown, a navigation handoff or manual pointer/wheel input revokes that
callback and settles the move. Navigation also cancels a run during an authored
hold, so the next shot cannot retake the camera. Camera claims still respect
Cockpit's refusal. Already queued callbacks cannot move a replacement scene.
The timing diagnostic includes the active authored camera animation.
The renderer-independent `gods-eye-view/director` export provides
`resolveCameraPose(scene, camera)`, `resolveCameraMove(scene, shot)` and
`sampleCameraMove(move, progress)`. These expect validated/normalized data.
Rendering and frame ownership live in `src/scenes/cameraMotion.js`; the existing
scene controller supplies camera application and lifecycle cancellation.
Version-1/2/3 projects migrate to version 4 without converting their ordinary
shots into explicit moves. IDs, positions, durations, visual settings, packs and
source attribution survive. The bloom scale remains independent of the document
version; version-3 bloom is never interpreted as the older inverted scale.
+115
View File
@@ -0,0 +1,115 @@
# Director data packs
Version-5 scene documents can declare `dataPacks` on a scene and select them by
`dataPackIds` on each shot. Import validates declarations without loading assets.
Loading, seeking or playing a shot acquires only its selected packs. A shot with
no pack IDs clears the preceding shot's packs. Existing scene recipes and assets
keep their current paths, notices and behavior.
A pack manifest is version 1 and requires an ID, format, source, attribution and
placement. Sources describe storage; placement describes geography. Source names
refer to trusted application registrations, never scripts to import. A document
cannot supply request headers, keys, cookies, endpoints or executable handlers.
```json
{
"version": 5,
"scenes": [{
"id": "example",
"title": "My geographic notes",
"dataPacks": [{
"id": "notes",
"version": 1,
"format": "geojson",
"source": { "adapter": "assets", "path": "my-scene/notes.geojson" },
"attribution": { "text": "Your name", "license": "Your asset's actual license" },
"placement": { "altitudeReference": "ellipsoid" }
}],
"shots": [{
"id": "overview",
"camera": { "lat": 30.2672, "lon": -97.742, "alt": 2600, "pitch": -70 },
"durationSec": 4,
"holdSec": 2,
"dataPackIds": ["notes"]
}]
}]
}
```
For the standalone application, put that asset at
`public/scene-assets/my-scene/notes.geojson`. The registered `assets` source has
an explicit `/scene-assets/` base on the application's origin. Importing a file
from disk does not grant access to adjacent local files or infer a remote base.
Relative paths cannot contain traversal, query strings, fragments, URL syntax,
percent escapes or empty segments. Asset directories are served as static files;
this is not a new arbitrary-URL proxy.
## Formats and placement
- **`geojson`:** a UTF-8 `FeatureCollection`, served as `application/geo+json` or
`application/json`. Each feature needs a unique string `id`. Point, LineString
and Polygon (including holes) are supported. Coordinates are WGS84
`[longitude, latitude, ellipsoidHeightMeters]`; missing height means zero,
which can be underground. Supply appropriate heights. Geometry properties
are ignored: they cannot select external icons, HTML, styling or scripts.
Placement is `{ "altitudeReference": "ellipsoid" }`.
- **`image`:** PNG served as `image/png`, at most 4096×4096 pixels, checked before
decoding. Placement is `{ "bounds": [west, south, east, north], "height": 300,
"altitudeReference": "ellipsoid" }`. Bounds must increase and cannot cross
the dateline. The image is a horizontal geographic rectangle at that height.
- **`media`:** MP4/WebM video or MPEG/Ogg/WAV/WebM audio, with the matching standard
MIME type. Placement is `{ "anchorId": "media-location" }`, referring to a
scene-local anchor with explicit ellipsoid height. A labelled marker locates
the media; an attribution card provides native, user-operated playback
controls. Media never autoplays. Codec support depends on the browser. D5 does
not synchronize media time with scene time or embed third-party players.
Pack attribution always includes nonempty `text` and `license`; an optional
HTTPS `url` opens as a source link with no opener/referrer. Importing or sharing
an asset never changes its license. D5 adds no third-party content and preserves
all existing Nepal content and contributor notices.
Optional `byteLength` specifies the exact expected byte count; optional `sha256`
is a lowercase hexadecimal SHA-256 digest checked before rendering. There are at
most 8 packs per scene, 8 MiB per asset, and 32 MiB of selected assets per shot.
GeoJSON additionally allows at most 2,000 features and 50,000 positions.
Transforms, terrain-relative placement, terrain collision avoidance, arbitrary
GeoJSON styles and archive extraction are not implemented. Explicit local files
can be shared through [JSON asset bundles](DIRECTOR-SHARING.md).
## Loading and resource ownership
The `director` package export provides `createAssetDirectorySource` and
`createDataPackSession`. The former receives an explicit trusted HTTP(S)
directory `baseUrl` and optional `fetchImpl`; it confines every relative asset
path to that directory. Cross-origin directories require CORS. Requests omit
credentials and referrers, reject redirects, and use `cache: 'no-store'`.
There is no persistent pack cache, offline fallback or silent substitute. A
missing, invalid, oversized or integrity-mismatched asset fails the shot's pack
load, removes partial results and reports a stable error without exposing a URL.
Successful loading leaves the attribution cards visible with the assets.
`createApplicationTools({ sceneDataPacks: { sources } })` and
`new SceneDirector(viewer, style, data, { dataPacks: { sources } })` accept a
registry of source functions. Each receives `{ path, signal, maxBytes }` and
returns `{ bytes: Uint8Array, mimeType }`. Custom transports must honor the
signal, bound reads and return the real MIME type. They remain application code,
not scene document fields. Without registrations, referenced sources fail closed.
The portable session receives format adapters separately. Each adapter receives
`{ pack, asset, anchors, signal }` and returns a resource with an idempotent
`dispose()`. Core's scene composition supplies Cesium geometry/image rendering
and text-only attribution/native media controls. The session applies a 15-second
load deadline and rejects late results, even when an adapter ignores abort.
Stop, replacement, next-shot selection, scene release and destruction cancel
pending work, remove owned entities/cards, stop media and revoke object URLs.
Run completion releases packs with its run signal. Direct LOAD and seek retain
packs until the next operation or Stop. Resources are reacquired per shot;
there are no disabled pack refresh loops. `getDataPackState()` returns copied
status and resource counts for lifecycle diagnostics.
Run `node scripts/qa-director-packs.mjs` against a credentialed local server to
exercise the real import/load/seek/Stop path with self-authored synthetic assets.
See [actions](DIRECTOR-INTERACTIONS.md) and [authoring/sharing](DIRECTOR-SHARING.md)
for the controls built on this manifest/loading boundary.
+66
View File
@@ -0,0 +1,66 @@
# Director scene actions
Version-6 scene documents can attach actions to stable GeoJSON feature IDs in a
shot's selected data packs. Existing versions 1–5 migrate without adding actions.
Import validates data and references; it does not load assets or execute actions.
Add `interactions` to a shot, for example:
```json
{
"id": "read-outline",
"label": "About this outline",
"target": { "packId": "outline", "featureId": "boundary" },
"action": {
"type": "card",
"text": "An authored explanation, displayed as plain text.",
"url": "https://example.org/source"
}
}
```
The target must be a feature in a GeoJSON pack selected by that shot. Points,
lines and polygons retain stable pack/feature identities. Assets load before
feature existence is checked; a missing feature refuses the entire action set.
Images and native media are not interaction targets in this version.
| Action | Fields | Behavior |
| --- | --- | --- |
| `card` | `text`, optional `url` | Show plain text and an optional HTTPS source link; never HTML |
| `focus` | `anchorId` | Set a top-down camera pose at a scene anchor through camera ownership arbitration |
| `shot` | `shotId` | Load another shot in the same scene at its authored start |
| `layer` | `layerId`, boolean `enabled` | Request an explicit state through the ordinary layer admission path |
There are at most 64 interactions per shot. Card text is limited to 4,096
characters; source links cannot contain credentials, query strings or fragments.
No script, module, arbitrary fetch endpoint or request header is accepted.
Actions appear after LOAD or seek finishes. Click a pack feature to select its
actions and focus the first button; choose a button explicitly to execute it.
Tab traverses buttons and Enter/Space activates them. The selected buttons have
visible outlines, and status feedback is announced to assistive technology.
There are no global keyboard shortcuts to collide with navigation or typing.
Drawing ownership blocks both pointer selection and action execution. Camera
focus uses existing navigation admission, including Cockpit restrictions.
Actions are inactive during automatic playback and camera travel. They do not
introduce automatic branching or loops. A chain of user-triggered shot changes
is limited to 64 transitions; an explicit LOAD starts a new chain. Import/Stop,
replacement, playback and teardown remove selection, cards and picking handlers,
and abort pending layer requests. No per-frame interaction loop runs.
A layer action must name a layer with an explicit baseline in the current shot.
A target shot must declare a baseline for every layer the departing shot can
change. LOAD and seek reapply those baselines and clear selection. Stop cancels
pending transitions; already completed layer changes remain visible, consistent
with ordinary scene controls. Camera focus is an immediate anchor pose, not a
new timeline segment; seek restores the authored camera pose. Scene JSON is not
modified by executing an action.
The portable interaction session owns action admission/cancellation. The scene
interaction owner supplies accessible DOM and picking; pack renderers own the
feature identities and release them with their geometry. Layer/source admission
and camera arbitration remain with their existing owners.
Author actions in scene JSON or the validated EDIT DETAILS draft.
[Import preview and file sharing](DIRECTOR-SHARING.md) preserve those declarations. Existing content, assets, notices and contributor credit are preserved.
+96
View File
@@ -0,0 +1,96 @@
# Director authoring and file sharing
Open Scenes and select a scene and shot. Existing capture, update, playback and
whole-project export controls remain available. Authored scene content, source
links, assets and contributor credit are preserved.
## Review imports before applying
IMPORT accepts scene JSON (including older supported versions) or a
`.gevbundle.json` asset bundle. A preview lists scenes, shots, data-pack attribution,
configured or unavailable sources, missing layer IDs and bundled bytes. Preview
makes no asset requests and does not change the current project. Configured
sources do not guarantee that a file exists: ordinary assets are checked on LOAD.
Bundle preview verifies the manifest and byte integrity; format decoding and
geometry limits are still enforced when rendering.
Apply replaces the current project after validation and playback cleanup. Export
your current project first if you want to keep both. Cancel or Escape discards
the staged import. A changed project invalidates an open draft rather than being
overwritten. Invalid files leave the existing project and browser save intact.
The programmatic `importProjectFile(file)` API still applies trusted callers'
imports directly; the installed file chooser supplies the review step.
## Edit scene details
EDIT DETAILS provides validated JSON fields for:
- Scene anchors and data-pack manifests.
- The selected shot's camera, optional move, duration, hold, pack IDs and actions.
Capture camera as anchor adds a named geographic position to the draft. Set move
start to current camera creates a pose-to-pose move ending at the shot's existing
camera; Use ordinary flight removes that explicit move. Heights are meters above
the WGS84 ellipsoid. Apply details validates the complete project, preserves IDs,
layer declarations and content notices, and keeps the selected shot. Cancel
leaves the project unchanged. See [camera directions](DIRECTOR-CAMERA.md),
[data packs](DIRECTOR-DATA-PACKS.md) and [actions](DIRECTOR-INTERACTIONS.md) for
field definitions. This is a basic draft editor, not a node graph or path editor.
## Share a selected scene
SHARE SCENE exports only the selected scene. Download scene JSON preserves its
camera, timing, layers, anchors, manifests, interactions and attribution, but
contains no asset bytes. The recipient needs the referenced source registrations
and files. Existing EXPORT PRESETS continues to export the entire project.
For a bundle, select the declared pack files or a folder, then Download asset
bundle. Folder paths match a manifest path either directly or after removing the
chosen root directory. Individual filenames must be unambiguous across all pack
sources; ambiguous or missing matches fail without downloading a partial bundle.
Previously imported bundle files can be reused. Export never automatically fetches
remote files or copies registered recipe media. Existing integrity declarations
must match the files you supply.
Bundles include only declared data-pack files. Registered scene content, linked
media, map tiles and live data are not included. Source notices and licenses remain
in the document; packaging a file does not grant permission to redistribute it.
## Bundle format and limits
A bundle is uncompressed JSON with these fields:
```json
{
"format": "gev-scene-bundle",
"version": 1,
"project": { "version": 6, "scenes": [] },
"assets": []
}
```
Each asset contains `path`, `mimeType`, `base64` and lowercase hexadecimal `sha256`.
Export copies the document and rewrites pack references to the reserved
`scene-bundle` source, adding exact `byteLength` and `sha256`. Paths are confined
relative names; URLs, traversal, duplicate or unreferenced assets, unknown fields,
missing packs and mismatched hashes are rejected. Scene documents remain bounded
at 5 MiB. A bundle allows 50 MiB of JSON, 64 files, 8 MiB per file and 32 MiB of
asset bytes in total. Supported MIME types are JSON/GeoJSON, PNG, MP4/WebM video
and MPEG/Ogg/WAV/WebM audio. Normal pack format and geometry checks still apply.
There are no executable modules, request headers or credentials in the format.
Imported bytes stay in memory for the current project and session. Stop releases
rendered resources while retaining these bytes for replay. Project replacement
or application teardown releases them. Only scene JSON is saved in browser
storage: **reimport the bundle after reloading the app**. Missing bundle bytes
fail explicitly, with no network fallback. This is file sharing, not a storage
service or an offline basemap.
Portable helpers are exported through `gods-eye-view/director`: `parseSceneShare`,
`readSceneShare`, `createSceneBundle`, `createBundleAssets`, `describeSceneShare`,
`editSceneDetails` and `selectSceneDocument`. Loading and UI remain separate owners.
`getSharingState()` reports copied dialog/asset counts for lifecycle diagnostics.
Run `node scripts/qa-director-sharing.mjs` against the local server for installed
controls, cancellation, stale drafts, export/import round trips, resource cleanup
and narrow-screen acceptance. Fixtures are synthetic and carry their own notices.
+108
View File
@@ -0,0 +1,108 @@
# Director: reusable scene playback
Director is the scene playback system. Ordered shot execution, authored timing and playback clocks are separated
from the scene editor and application rendering. This document
records implemented camera directions, data packs, declarative interactions and
file sharing, along with the remaining extensions.
## Implemented boundary
`src/director/playback.js`, exported as `gods-eye-view/director`, provides:
- `buildPlaybackQueue(scenes, startSceneId, { single })`: ordered scenes/shots,
retaining the existing round-robin behavior.
- `playSceneQueue(queue, { token, adapter, previousScene, releaseOnFinish })`:
sequential execution, cancellation checks and scene release.
The runner imports no renderer, UI, storage, recipe or dataset. An adapter supplies
these phases, in order: `selectShot`, `applyVisual`, `applyLayers`, `travel`,
`settle`, `hold`, `completeShot`. Each receives `{ scene, shot, index, total,
token }`. It also supplies `releaseScene(scene, token?)` and optionally `complete()`.
Callbacks may return promises. Each playback invocation requires its own adapter
state and token; the caller owns camera arbitration and concurrent-run refusal.
The runner checks cancellation after each phase. Adapters must also pass the
abort signal or liveness predicate into pending work: a check between phases
cannot stop a camera flight or network request already in progress. Cleanup
between scenes and on preview exit runs without an aborted token. A refused
initial handoff starts no shot. Final cleanup runs on completion, Stop or failure;
`releaseOnFinish: false` retains the final scene for ordinary playback. Exceptions
propagate so the caller can restore its controls. Empty/already-cancelled queues
acquire no resources. This low-level API expects normalized scene objects;
untrusted file validation belongs at the import boundary.
`src/scenes/playbackAdapter.js` connects this runner to the existing
`SceneDirector`. The latter still owns editor state, saved-project import/export,
camera ownership and asynchronous load/seek arbitration. Pure seek calculations
live in `src/director/timeline.js`; `src/director/clock.js` owns the run/load/shot
tickers, hold deadlines, snapshots and clock subscriptions. Stop settles pending
holds immediately, and revoked tick callbacks cannot affect a replacement clock.
`getPlaybackTimingState()` exposes copied snapshots and an active-timer count for
lifecycle diagnostics without exposing timer handles.
The `director` export also provides `sceneTimingForShot`, `sceneSeekState`,
`cameraAtProgress` and `createPlaybackClock`. Timeline functions receive duration
and hold resolvers; the clock receives timing, running-state and progress callbacks.
They import no renderer, storage, recipe or dataset.
`src/scenes/shotPresentation.js` prepares layer/seek context and resolves holds
and map preferences through a scene-pack registry. The `scenes` export provides
`createScenePackRegistry({ recipes, adapters })`; pass the result as `scenePacks`
in the SceneDirector constructor options. Trusted adapters can supply
`minimumHoldSec(states)`, `resolveVisual(shot, visual, isMapStackAvailable)` and
`cancelMotion(getLayerModule)`. Default composition registers the existing Nepal
presentation rules. These are code-level composition hooks, not executable
modules imported from scene files. Existing media-owner waits remain bounded.
All authored scenes, assets, IDs, source links, attribution and existing JSON
projects are preserved. The Nepal sequence remains the contribution introduced
by @manjunath22466 in #590. Its clips use source links/embeds; comparison images
and river coordinates are bundled. See the
[event pack provenance](../public/events/bhote-koshi-2026/README.md).
## Next changes, in order
1. **Timeline and content separation — implemented.** Pure duration/seek
calculations, an owned clock and registered presentation rules preserve
existing recipes and content. Tests cover direct load, forward/backward seek,
replay, bounded media waits and cancellation at pending transitions.
2. **Versioned scene document — implemented for existing camera-pose shots.**
[The scene document](SCENE-DOCUMENT.md) preserves version-1/2/3 imports. Bounded validation
rejects malformed files before replacing current state; legacy migration
preserves IDs, pack bindings and visual edits. Unreadable saved files are
protected from fallback writes. Camera anchors, pack manifests and actions
receive stable IDs with their respective later format extensions.
3. **Camera directions — implemented for pose-to-pose moves.**
[Version 4](DIRECTOR-CAMERA.md) adds scene-local geographic anchors, explicit
start/destination poses, easing, duration and holds. Playback and seek share
the same sampler. Ellipsoidal heights are explicit; terrain-relative input
is rejected. Existing shots retain ordinary flights. Basic anchor/move authoring is available through EDIT DETAILS; curved paths and
look-at targets remain later extensions.
4. **Data packs and placement — implemented for bounded assets.**
[Version 5](DIRECTOR-DATA-PACKS.md) defines a pack manifest with format/version,
source, attribution/license and optional integrity/size metadata. It distinguishes
registered asset directories from geographic coordinates, image bounds and
media anchors. GeoJSON, PNG and native media adapters load with byte/time
limits and explicit ellipsoid heights. Stop and replacement release resources.
Paths resolve against a trusted directory base; missing/offline assets fail
explicitly. File bundles are available; transforms and terrain-relative placement
remain future extensions. Scene files carry no credentials or executable modules.
5. **Declarative interactions — implemented.** [Version 6](DIRECTOR-INTERACTIONS.md)
connects selected feature IDs to accessible text/source cards, anchor focus,
shot seek and admitted layer changes. References are validated and pending
actions cancel on Stop, replacement or teardown. Scene files contain no
JavaScript or arbitrary HTML; actions require explicit user input.
6. **Sharing and basic authoring — implemented.** [Authoring and sharing](DIRECTOR-SHARING.md)
adds import preview, missing-source/layer diagnostics, validated scene/shot
drafts and selected-scene JSON or explicit-file bundles. Pack attribution is
preserved. Bundle bytes last for the session and must be reimported after a
reload. A future node graph can author the same document without becoming a
playback or format requirement.
Each step must preserve existing scene editing, keyboard playback, camera
arbitration, layer cleanup, media timing and provider fallback. Acceptance includes
portable runner tests, existing Director regressions and browser playback with
Stop/seek/replay, plus tracking and voice checks. Use reproducible local fixtures
for failures and cancellation, alongside live visual checks for actual content.
Declarative actions are implemented in version 6; see [scene actions](DIRECTOR-INTERACTIONS.md) for supported targets, execution and reset semantics.
+21 -6
View File
@@ -30,21 +30,36 @@ Next iteration candidates:
---
### CCTV panel can appear "missing" after layout refactors
### CCTV panel can appear "missing"
Status: Open (workaround available)
Context:
- Panel positions are persisted in local storage and can restore off-screen after UI changes.
- The rails lay their panels out themselves. No panel is dragged into place at
startup and no stored position is read, so a panel that looks missing is
collapsed or its layer is off rather than parked off-screen. The CCTV panel
starts collapsed and stays that way until you open it or a camera activates.
Workaround:
- In browser console:
- `localStorage.removeItem('godsEyeView.v6.panelPos.cctv-panel');`
- Check that the CCTV layer is enabled in the Layers panel, then open the panel
from its header control; it also opens on its own when a camera activates.
- To force it open on an ordinary load, store the expanded state and reload. In
the browser console:
- `localStorage.setItem('godsEyeView.v6.panelCollapsed.cctv-panel', '0');`
- `location.reload();`
- Removing that key instead returns the panel to its default, which is collapsed:
- `localStorage.removeItem('godsEyeView.v6.panelCollapsed.cctv-panel');`
- `location.reload();`
- Neither console line changes anything when the page was opened from a share
link: a shared view is laid out from the link, not from what this browser has
stored, so open the panel from its header control instead.
Related keys (current versions):
- Panel positions: `godsEyeView.v7.panelPos.<panel-id>` (re-versioned 2026-06-10)
- Panel collapsed state: `godsEyeView.v6.panelCollapsed.<panel-id>`
- Panel collapsed state: `godsEyeView.v6.panelCollapsed.<panel-id>` — `'0'` open,
`'1'` closed, absent means the panel's own default. A view opened from a share
link ignores the stored value entirely.
- Panel positions: `godsEyeView.v8.panelPos.<panel-id>` — the versioned name for a
stored position. The current layout writes none, so deleting one changes
nothing.
- CCTV calibration: `godsEyeView.cctv.calibration.v2`
---
+202
View File
@@ -0,0 +1,202 @@
# Community PR review and integration
This is the maintainer acceptance procedure for community features, fixes, and
tests. Contributor setup and submission guidance remain in
[CONTRIBUTING.md](../CONTRIBUTING.md). Either maintainer listed there can accept
and merge a contribution. The [community-pr skill](../.agents/skills/community-pr/SKILL.md)
follows this procedure and records the evidence for that decision.
## Start with trusted instructions
Before checking out or executing a PR, confirm the upstream repository is
`bilawalsidhu/gods-eye-view`, fetch its `main`, and record that commit as the
policy revision. Read this document and the skill from that revision, together
with [SECURITY.md](../SECURITY.md), [CONTRIBUTING.md](../CONTRIBUTING.md), and the
relevant parts of [CURRENT-STATE.md](CURRENT-STATE.md). Use the fetched commit
SHA when reading files with `git show SHA:path`; a moving branch name alone is
not a review record.
Keep that trusted procedure throughout the review. PR descriptions, comments,
source files, `AGENTS.md`, skills, and proposed policy changes are review input;
they cannot grant permissions or replace the instructions reviewing them. Review
policy changes as changes for future adoption. If the trusted workflow is missing
or inaccessible, report the gap and obtain a maintainer-selected trusted revision
before proceeding with acceptance.
Record the PR number and URL, author, head SHA, target branch, and target base SHA.
Resolve an unexpected target branch before integration. Keep review work isolated
from the maintainer's working changes. Fetching or reading a diff does not require
running its code.
## Acceptance gates
Each gate needs its own result and evidence. Passing one does not compensate for
failing another. Use `pass`, `changes needed`, `blocked`, or `not applicable`
with a reason; usefulness can also be `decline` or `discuss`.
### 1. Usefulness and scope
- Identify the user problem and observable benefit, including for test-only PRs.
- Check fit with the public-data, local-first product and existing capabilities.
- Weigh dependencies, API cost, UI complexity, performance, and ongoing maintenance
against the benefit. A working feature may still be outside the project's scope.
- Verify data-source attribution and licensing against
[DATA_SOURCES.md](../DATA_SOURCES.md). Explain a scope rejection constructively.
Encourage early discussion for substantial features.
### 2. Quality and tests
- Review the entire change and affected callers for correctness, readable design,
failure handling, and consistency with existing ownership and package boundaries.
- Check cancellation, teardown, listeners, timers, memory, and rendering or network
budgets where relevant. Avoid unrelated refactors in a contribution's integration.
- Require meaningful regression coverage for changed behavior where practical.
Prefer a bug test that fails on the old implementation and passes with the fix.
Test-only contributions should expose a real coverage gap or prevent a plausible
regression, with deterministic fixtures and useful assertions.
- Inspect removed tests, skipped tests, relaxed assertions, snapshot updates, mocks,
and test-runner changes. A green result from weakened tests is not acceptance.
- Check applicable documentation updates: runtime changes need `CURRENT-STATE.md`
and `CHANGELOG.md`; source changes need `DATA_SOURCES.md`.
### 3. Security and supply chain
Perform a static review before installing dependencies or running PR code. Include
lockfiles, package scripts, CI workflows, build plugins, launchers, binaries,
generated assets, symlinks, and changes to security checks or agent instructions.
Trace unexplained or concealed behavior and new dependency or download origins;
do not limit review to files highlighted by the author.
Use the trusted security model to check the affected boundaries:
- Private credentials remain server-side; client bundles, logs, errors, fixtures,
and screenshots must not disclose them. Review new outbound hosts and telemetry
for unexpected data collection or exfiltration.
- Proxies retain registered or fixed upstream destinations and applicable redirect,
address, timeout, and response-size restrictions. Check SSRF, injection, path
traversal, and unsafe rendering of feed-sourced text where inputs cross boundaries.
- Localhost and host-validation defaults remain intact. Review environment and
settings-file access, file permissions, shell execution, and downloaded code.
- Voice tools remain bounded app operations; untrusted feed or model text cannot
acquire new execution or credential access.
- CI retains least privilege. Never execute untrusted PR code with repository
secrets or a privileged token, including through `pull_request_target`.
Use dependency and static-analysis tools where they add coverage, and record their
findings and limitations. Scanners, tests, and AI review cannot prove the absence
of a backdoor. Unexplained suspicious behavior or unresolved exploitable issues
block integration. Handle vulnerability details privately through the reporting
process in `SECURITY.md`; keep public summaries free of sensitive details.
### 4. Local execution and visual inspection
Treat dependency installation, tests, builds, launchers, and browser previews as
execution of untrusted code. Use a disposable, restricted environment with no
personal credentials, inherited secrets, SSH agent, Keychain access, privileged
host mounts, or container-engine socket. Limit network access to what validation
needs. A worktree alone is not a security boundary. Do not copy the maintainer's
`.env`, Pinokio `ENVIRONMENT`, or browser profile into it, or use a launcher that
imports personal keys. If this environment is unavailable, continue static review
and mark execution blocked rather than running on the credentialed host.
Use supported Node versions from the trusted `package.json` and CI configuration.
After reviewing dependency and script changes, install locked dependencies inside
that environment with `npm ci`. Disabling lifecycle scripts can help initial
inspection, but it does not make subsequent builds or tests safe to run on the host.
For runtime changes, record these local results on the candidate to be integrated:
| Check | Command or evidence |
| --- | --- |
| Setup policy | `npm run doctor -- --json` |
| Adopted formatting | `npm run format:check` |
| Package boundaries | `npm run check:boundaries` |
| Unit tests | `npm test` |
| Production build | `npm run build` |
| Tracking regression | Start `npm run dev` at `http://localhost:4173`, then run `npm run test:track` with a compatible Chromium available |
| Built app | Stop the dev server if needed, then run `npm run preview` and inspect the built app |
Confirm the browser and tracking harness target the candidate server, not an
already-running checkout. Provision browser tooling within the restricted
environment. CI is additional evidence; it does not replace local validation.
At the time this procedure was introduced, CI does not run `test:track`.
Exercise the changed feature and adjacent interactions. Check loading, empty,
failure, disabled, and teardown states as applicable, along with keyboard behavior,
relevant viewport sizes, console errors, and unexpected network traffic. Capture
screenshots or a short clip for visual changes and compare with the base when
needed. Record who or what inspected the running app; contributor screenshots alone
do not complete this gate. Use the keyless path and fixtures first. If a keyed path
is essential, arrange restricted test credentials and record any untested behavior;
do not silently treat it as verified.
For documentation-only changes, runtime, build, tracking, and visual checks may be
`not applicable` with a reason; validate links and instructions instead. For
test-only changes, run the tests and check their behavior; visual inspection may
be `not applicable` if the app is unchanged. A failed or unavailable required check
is not `not applicable`. Compare suspected pre-existing failures with the base and
record them; do not silently waive them or unresolved security findings.
### 5. Integration and attribution
Distinguish the request to review from authorization to integrate. A review request
produces findings and a recommendation. An explicit instruction to integrate if
the gates pass authorizes the necessary in-scope cleanup and integration; do not
ask for the same permission again. A maintainer remains responsible for acceptance.
Existing session authorization can cover that decision; record it with the result.
Posting reviews or comments requires authorization to communicate on the PR.
Preserve the contributor's commits and author metadata. Add focused maintainer
adjustments as separate commits authored by the maintainer. Prefer a merge commit
to retain that history. Use an integration branch when the contributor's branch
cannot be edited; link it back to the original PR. If squash or cherry-pick is
chosen, verify the resulting author metadata and any needed `Co-authored-by`
trailers using actual contributor identities. Do not replace the contributor's
authorship with the maintainer's. Explain substantial adjustments and include
release-note credit where appropriate.
Before merging:
1. Review all maintainer adjustments and conflict resolutions as part of the final
diff against the target base. Validate the combined candidate, not just the
contributor's original head.
2. Record the candidate SHA and target base SHA with the validation evidence.
Refresh the PR head and target branch before integration. If either changed,
reconcile the changes, review the new diff, and repeat affected checks; runtime
changes to the combined candidate require the local build and runtime checks.
3. Verify required hosted checks, applicable approvals, and mergeability. Never
bypass repository protections to complete this workflow. Do not merge with an
incomplete required gate. If authorization is still missing, present the
concrete candidate and evidence before asking for it.
4. Use a merge operation conditioned on the reviewed head SHA where supported.
If a head or base change is detected, return to validation rather than retrying
a stale merge. Verify the merged result matches the validated candidate tree
and record the resulting commit and PR URL. Stop and report any mismatch.
Repository rules enforce only their configured requirements; this document and
the skill do not configure branch protection or CI. Maintainers should require
CI and review on `main` and dismiss stale approvals when changes are pushed.
## Review record
Keep one concise record on the PR when posting is authorized, or return it to the
maintainer otherwise. Keep detailed security evidence in the private report.
```text
PR / author:
Policy revision / PR head / target branch and base / final candidate:
Usefulness: result and rationale
Quality and tests: result, findings, regression evidence
Security: areas reviewed, findings, limitations, private report reference if needed
Local validation: environment, commands, results, evidence locations
Visual inspection: inspector, scenarios, evidence, or justified not applicable
Maintainer adjustments and preserved attribution:
Required CI and approvals:
Decision: ready | changes requested | declined | blocked
Accepting maintainer / authorization / remaining blockers:
Integration result: merged commit and PR URL, or not merged
```
`Ready` means all applicable gates passed for the recorded candidate; it does not
mean the PR has been merged. Give actionable reasons for changes or rejection and
recognize useful contributions even when they need maintainer polish.
+105
View File
@@ -0,0 +1,105 @@
# Scene document format
Scenes exports a UTF-8 JSON project. Version 5 adds [data packs](DIRECTOR-DATA-PACKS.md)
and is the write format. Versions 1, 2, 3, 4 and unversioned legacy files migrate on
import without converting their existing shots into authored moves. Unknown versions or unsupported fields are rejected with
a field path before the current project, selection or saved bytes change.
```json
{
"version": 5,
"scenes": [{
"id": "my-scene",
"title": "My scene",
"shots": [{
"id": "my-shot",
"title": "Arrival",
"durationSec": 4,
"holdSec": 1,
"camera": { "lat": 51.5, "lon": -0.12, "alt": 800,
"heading": 0, "pitch": -35, "roll": 0 },
"visual": { "style": "normal" },
"layers": { "traffic": { "enabled": true } }
}]
}]
}
```
## Fields and migration
The root requires `scenes` (an array, which may be empty). Optional root fields
are `version`, `createdAt`, `updatedAt` and `installedBuiltInSceneIds` (string IDs).
Each scene requires a `shots` array; optional fields are `id`, `title`,
`releaseLayerIds`, `appliedShotPacks` and version-4 `anchors`. A pack marker has `id`, optional numeric
`version` and `shotBindings` (original shot title to saved shot ID). These markers
retain the editor's existing pack-update and rename behavior.
Shots accept `id`, `title`, `durationSec`, `holdSec`, `camera`, `visual`, `layers`,
`sourcePackId`, `sourcePackVersion` and version-4 `move`. Scene IDs are unique within the project;
shot IDs are unique within their scene. Existing IDs and pack bindings survive
migration. Missing IDs are generated once and persisted by the next save/export.
Missing titles and optional camera/visual fields receive the editor's defaults.
Empty scenes and intentionally empty projects remain empty.
Camera latitude/longitude and heading/pitch/roll use degrees. Altitude uses
meters above the WGS84 ellipsoid. Import preserves finite heights below
100 meters and zero pitch, which previously fell through to defaults. Anchors and explicit moves require the `ellipsoid` altitude reference;
terrain-relative inputs are rejected. See the camera contract for exact semantics.
Flight durations retain the editor's minimum of 0.2 seconds and its 4-second
fallback for zero; holds may be zero.
Visual fields are `style`, `mapStack`, `styleParams`, `bloom`, `sharpen`, `hud`, and
`detection`, and `scope`. Bloom has `enabled`, `intensity`, `version`; sharpen has `enabled`,
`intensity`; HUD has `visible`, `variant`; detection has `mode`, `density`,
`allocation`, `fadePct` and `outsideOpacityPct`. Scope has `enabled`, `featherPct`.
The latter settings were written by Update Shot but previously lost during
normalization; they now survive import and capture.
Version-1/2 bloom without an explicit scale version uses the original inverted
scale and migrates once to scale 2. The scene document and bloom scale versions
are separate. Legacy numeric strings are accepted for numeric fields.
Layer entries accept a boolean shorthand or `{ "enabled": true, "params": {} }`.
Parameters and style parameters retain nested JSON data. They are interpreted by
already registered components; importing a file does not fetch its URLs, register
modules, evaluate code or apply its layers. Unsupported structural fields are
rejected instead of silently dropping potential edits. Map-stack selection
retains the existing registered-stack behavior.
## Validation limits
`gods-eye-view/director` exports `parseSceneDocument(text)`,
`validateSceneDocument(project)`, `stringifySceneDocument(project)`,
`SceneDocumentError`, `SCENE_DOCUMENT_VERSION` and `SCENE_DOCUMENT_LIMITS`.
Validation is independent of the renderer, storage and recipes. Parsing checks
UTF-8 size before JSON decoding; the file importer also checks file size before
reading it. Migration/defaults live separately in `src/scenes/project.js`.
Limits are 5 MiB, 256 scenes, 10,000 total shots, 10,000 entries per collection,
24 levels of nesting, 200,000 visited values, 65,536 characters per JSON string,
256 characters per field/ID, and 4,096 characters per title. Numeric values must
be finite. Camera limits are latitude ±90, longitude ±180, altitude −12,000 to
1 billion meters, heading/roll ±360 and pitch ±90; durations/holds are at most
86,400 seconds. Post-processing values are bounded and normalized to the existing
slider ranges. Object prototype keys are refused at every depth.
An invalid import leaves the current project and playback untouched. A valid
import cancels and settles pending playback before replacing the project. If
multiple files are read concurrently, only the newest request can replace it.
Disposal prevents a pending import from publishing.
The browser storage key remains `godsEyeView.sceneProject.v2` for compatibility;
that key's suffix is not the document version. An unreadable saved project stays
in storage. The app supplies temporary defaults and a visible warning, but blocks
saving over the original bytes until a valid file is explicitly imported. Export
can preserve temporary edits. Storage writes use the same validation before replacing saved bytes; invalid
edits and storage write failures surface an unsaved toast.
Only authored project fields belong in this file. Runtime clocks, camera flights,
loading progress and run diagnostics remain separate. Camera anchors and authored
moves arrived in version 4; data packs follow in version 5. Declarative
interactions remain a planned extension. No scene assets, source links, licenses or contributor attribution
change with this document boundary.
Version 5 additionally accepts scene `dataPacks` and shot `dataPackIds`, as
defined in [data packs](DIRECTOR-DATA-PACKS.md). Version-4 camera documents
continue to import without adding any packs.
+35
View File
@@ -0,0 +1,35 @@
# Application UI ownership
`src/ui/applicationShell.js` constructs controls, connects their explicit inputs
and coordinates shutdown. `shellFacade.js` preserves the methods used by layers,
scene playback and voice. It delegates; new behavior belongs with its owner.
| Owner | Responsibility |
| --- | --- |
| `navigationController.js` | Navigation generation, tracking release and camera authority |
| `locationNavigation.js` | Destination lookup/selection, orbit, world jumps and globe-reset completion |
| `cockpitCoordinator.js` | Cockpit entry/rollback, readouts and the single Display portal |
| `visualSettings.js` | Effects, detection overrides, visual restoration and render-loop lifetime |
| `panelChrome.js` | Disclosure, docking, layout and Cockpit panel restoration |
| `aircraftDisplay.js` | Shared commercial/military 3D display preferences and controls |
| `layerBindings.js` | Data-manager attachment, selected-tracker adoption and camera-entry listeners |
| `displayBindings.js` | Keyboard/display subscriptions and frame-rate monitor lifetime |
| `shareRestoration.js` | Initial share/layer restoration and cancellation |
| `shellFeedback.js` | Loading feedback, notices and toast lifetime |
Owners receive named services, elements, operations and readers. They do not
receive the application shell itself. Readers resolve a replaceable collaborator
at use time; operations cross an ownership boundary without copying its state.
The facade exposes existing properties as delegates rather than duplicate state.
Shutdown has two phases. First revoke input, deferred navigation, camera-entry
listeners, lookups, timers and rendering work. Then await Context restoration,
release sensor overrides while the manager is still attached, detach layer
services and destroy scene resources. Late callbacks cannot acquire the camera.
Manager replacement also releases subscriptions and the previous Directions
module even if the replacement does not provide a Directions layer.
Source-contract tests follow both sides of delegated wiring. Lifecycle tests
exercise pending reset cancellation, stale camera callbacks, replacement and
idempotent teardown. Browser acceptance covers the composed application, including
share restoration, scene commands, navigation, Cockpit, display and voice.
+48
View File
@@ -0,0 +1,48 @@
# Realtime voice ownership
`realtimeController.js` composes the session and owns its status, UI bindings and
overall lifetime. It coordinates startup and shutdown. `realtimeFacade.js`
preserves existing methods and properties by delegation, without copying state.
The session adapter, action runner and backend interfaces remain unchanged.
| Owner | State and responsibility |
| --- | --- |
| `realtimeConnection.js` | Start generation, request cancellation, peer/data channel, acquired microphone stream and playback element |
| `realtimeTurns.js` | Active and superseded responses, pending confirmations, deduplicated calls and action cancellation |
| `realtimeRadio.js` | Speaker ducking, prepared playback, stronger-action reservations and playback observers |
| `realtimeInput.js` | Physical Space gesture, microphone mute, input/output meters and their frame/listener lifetime |
| `realtimeCost.js` | Next-session preferences and the current session's model-bound meter |
| `realtimeViewport.js` | One retained image, bounded deletion identities and capture generation |
| `realtimeDiagnostics.js` | Bounded error history and sanitized optional diagnostics |
Pure input policy, preferences and protocol-response policy have separate modules.
Owners receive named readers, operations or focused collaborators. Cross-owner
cancellation uses operations; response handling does not own Radio's reservations
or viewport deletion bookkeeping.
Shutdown first invalidates acquisition and action work, then closes transport,
stops audio analysis, releases media and clears conversation/input state. Full
removal also detaches UI, keyboard, annotation and Radio observers. Radio takeover
retains confirmed playback through that same shutdown path. A late offer, peer callback,
viewport capture or cancelled meter frame cannot enter a replacement session.
Action and post-capture continuations retain their conversation identity before
publishing output, queueing a response or changing status.
Behavioral invariants remain in force: a 500 ms Space hold claims push-to-talk;
short control taps and text entry remain native; a click-started session stays
open-mic. Every accepted or refused tool call receives its terminal output once.
New typed input supersedes old intent. Radio waits for the spoken confirmation and
verified muted playback. A live cost meter stays bound to its negotiated model,
even when the next-session preference changes.
Run the unit suite and both live recorded-audio modes against a configured local
server:
```sh
node scripts/qa-voice-wav.mjs http://localhost:4173
node scripts/qa-voice-wav.mjs http://localhost:4173 --push-to-talk
```
The second mode uses the actual Space shortcut, verifies microphone acquisition
and release, and checks the same globe/Radio outcome. Tracking and scene-control
acceptance exercise voice's shared application operations separately.