Merge branch 'main' into codex/shared-geodesy

This commit is contained in:
Sameh Khamis committed 2026-10-17 11:42:58 -07:00
commit 64d7ed3c61
1373 files changed
+245910 -10664

No files matched your search

+17 -10
View File
@@ -105,14 +105,17 @@ checks when changing this wiring.
`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.
and `providers`. Default forward search tries decimal coordinates and bundled
place names (`presets`) first, without a network request, then Google when a key
is configured, Photon, and the local `/api/geocode` Nominatim route. The
defaults also keep 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
For a compatible protocol, configure `geocode`, `photon`, `nominatim`, `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:
@@ -155,7 +158,7 @@ routing and regional caches. Request parameters cannot override these endpoints.
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
Its backend supplies `requestToken({ tier, authMode, 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.
@@ -181,10 +184,14 @@ 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.
state restoration, and release them after the consumers stop. The catalog
constructor (`application/layers`) also requires `wind`, `weather` and
`cyclones` sources with `getSnapshot` for the Weather layers, and owns the
observed-history clock those layers share for the catalog's lifetime.
`application/services` accepts boundary, terrain, regional-context, weather and
summary services. `application/requests` supplies the existing HTTP protocols
summary services; its weather service supplies cockpit local conditions, not the
Weather layers. `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
+97 -10
View File
@@ -17,6 +17,8 @@ until deliberately adopted.
Generated output, local configuration, browser evidence and bundled datasets are
excluded. The formatter validates every entry before writing any file.
New world text labels must use the world-overlay host; Cesium text labels are forbidden.
## Current component ownership
Package imports use `gods-eye-view`; `package.json` is the authoritative
@@ -63,11 +65,10 @@ 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.
Source-only factories use dedicated `layers/<family>/source` exports where
available, preserving the same factory implementation without loading rendering.
Record normalization and endpoint policies 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
@@ -277,6 +278,90 @@ share restoration and movement of controls between containers remain caller-owne
Existing helper imports from `cockpitMath.js` and `rightRailPolicy.js` remain
compatible through re-exports.
### Adding a right-rail readout panel
Add the shell in `src/ui/templates/context.html` inside `#right-context-rail`:
a `.panel-collapsible` with a stable `id` and matching `data-panel-id`, the
existing `.panel-header`, collapse button and a dedicated body. Register it
with the existing panel chrome/layout owners; do not allocate rail space in
the body renderer. Set `hidden` while empty so the allocator ignores it, and
expand through the existing collapse button only on first appearance per page
session, and only when the chrome marked the restored state as a default
(`data-collapsed-preference`), never over a stored or shared choice. Preserve
the user's later collapse choice, including body remounts. Mark the body
`data-rail-scroller` when it scrolls: the rail's measuring pass lifts its
`max-height` and puts the scroll offset back afterwards. Panel bodies only
scroll on an open-card change, with one write, never on a refresh.
Compose `createRailCards({ container, document, onParams, onOpen })` and
`createRailTimeline({ container, document, onCommit, onPreview, onStep,
onLatest, onPlay })`. Cards reconcile `{ id, title, badge, open, compact,
compactStatus, blocks }` in place. Their full-width native button headers expose
`aria-expanded` and `aria-controls`; articles expose `data-open`. The feature
adapter owns accordion state. Generic cards do not select data or enable layers.
`blocks` is an ordered array of loose, optional `{ id, type, ... }` descriptors,
rendered by `railCardBlocks.js`. There are no per-section disclosures:
- `summary`: `text`, a single summary line.
- `list`: `list: { ariaLabel, items }`, reconciled by `syncRowList` in `rowList.js`.
- `lines`: keyed `lines: [{ id, text, muted? }]` for details and status.
- `settings`: `settings: [{ id, label, chips }]`; each labelled row has a 56 px
label column and a wrapping `syncChipGroup` from `chipGroup.js`.
- `legend`: `legend: { colors, labels, units?, zeroIndex?, categorical? }` for a
ramp/scale (including a physical freezing anchor) or separate keyed swatches.
- `actions`: keyed `actions: [{ id, label, params?, href?, onClick?, disabled?,
hint? }]` in a footer, using buttons or safe new-tab links.
- `result`: `label`, keyed `lines`, and optional `clear: { params | onClick }`;
its accessible × button sits beside the title.
Every block and control has a stable id. Lists, settings, actions and results
can appear in any requested order, including a scene panel's rectangle action,
results list, then selected-scene details. Chip/list/action `params` dispatch
through `onParams(cardId, params)`; the feature adapter supplies
`setLayerParams(id, params, { origin: 'user' })`. Current descriptors and disabled
state are read at dispatch. Identical updates do not move nodes or write DOM;
focused controls survive feed refreshes.
Weather descriptors retain top-level `chips`, `list` and `legend` for non-DOM
consumers. They expose `summary.settings`, `summary.actions` and optional
`summary.result`; wind also retains its raw captured `summary.reading`.
`readout: true` layer rows render only the toggle and source/meta line; their
subscriptions still refresh the cards. Cards own configuration and readings.
Weather orders cyclones, wind, then a bordered Observed history group containing
the timeline and active radar, satellite and lightning cards. The group heading
and scope name its active products. The timeline stays visible whenever any
observed product is enabled; fewer than two ticks disables transport. Pass
`heading: false` to the timeline when the containing group owns the heading.
Exactly one active card is open. Header clicks and newly enabled ids choose it
(the last new entry wins a batch); refreshes and clock ticks retain it. First
appearance uses cyclones with storms, otherwise the first card. Disabling the
open layer falls back to the first remaining card. A WeakMap per document keeps
explicit choices through body remounts without adding share state. Card opening,
storm selection and layer enabling remain independent.
Optional card/badge/slider class names provide feature styling. Card titles use
`.data-name` typography; meta/time/badges use `.data-toggle-meta` typography;
setting/result labels use `.panel-title`; chips retain `.data-toggle-chip`.
Reserve one status line, even without a legend. Use app tokens. Wind's footer
reads the map center into a result after the action. The captured location stays
fixed until another read; model changes resample that location, units reformat
it, and × clears both reading and map marker.
The timeline shows endpoint times and receives ticks, index, mode, playing,
readout and disabled state. Its preview callback receives the tick and index and
may return readout text; commits receive the same values after a 150 ms coalesced
drag or final change. Feature adapters own data mapping and service subscriptions;
generic modules own DOM/listeners/timers and release them in `destroy()`.
Keep persistence with panel chrome: collapse uses
`godsEyeView.v6.panelCollapsed.<panelId>`, position uses
`godsEyeView.v8.panelPos.<panelId>`, and share state uses the registered panel id.
Do not rename existing
ids or reset persistence versions when adding a body. The WEATHER adapter is
`src/ui/weatherPanel.js`; its observed-history selection remains transient.
## Visual input
`gods-eye-view/ui/input` exports `bindApplicationShortcuts` and
@@ -544,9 +629,9 @@ 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
`gods-eye-view/sources/reference` constructs fresh earthquake, GNSS integrity and bundled cable
source instances independently of standalone setup. Individual sources remain
available through `layers/earthquakes/source` and `layers/submarine-cables/source`.
available through their `layers/<family>/source` exports, including `layers/gnss/source`.
The latter retains the bundled dataset’s attribution and licensing requirements.
## Geocoding and feature-query providers
@@ -584,8 +669,8 @@ 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,
Vector tile sources supply traffic roads, ALPR records and keyless military
areas. Operator-configured Overpass supplies named installations 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;
@@ -627,7 +712,9 @@ query-language strings or decoding backend tags and relation members.
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
`{rateLimited: true, retryAfterMs}` retains a provider's retry delay.
`{unavailable:true, code:'OVERPASS_NOT_CONFIGURED', retryable:false}` is a
capability miss that stops retries while preserving pins and offline geometry. 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'`
+2016 -157
View File
File diff suppressed because it is too large. Load diff
+92
View File
@@ -0,0 +1,92 @@
# UI text and translation
Every piece of text a user can see goes through `t()`, `tn()` or `msg()` in code, or a `data-i18n*` attribute in markup. The English text itself is the key. English needs no catalog; other languages live in `src/i18n/locales/<code>.json`. Translators work from [TRANSLATORS.md](./TRANSLATORS.md).
## Which one to use
| | Translates | Use for |
| ------------------------------- | --------------------------------- | --------------------------------------------- |
| `t(text, values)` | now | any text shown right where it is written |
| `tn(count, one, other, values)` | now, picking the form for `count` | text that changes with a number |
| `msg(text)` | never: returns `text` unchanged | text stored in data and passed to `t()` later |
```js
import { t, tn, msg } from '../i18n/index.js';
status.textContent = t('Save keys');
status.textContent = t('Saved to {store}', { store: storeLabel() });
chip.textContent = tn(missing, '{count} KEY WAITING', '{count} KEYS WAITING');
// en: 1 KEY / 3 KEYS ru: 1 КЛЮЧ / 3 КЛЮЧА / 5 КЛЮЧЕЙ
const FEED_STATE_LABELS = { loading: msg('LOADING'), stale: msg('STALE') };
label.textContent = t(FEED_STATE_LABELS[state]);
```
A module the server, tools or other portable code also imports must not load the catalogs: it imports `msg` from `src/i18n/marker.js`, which only marks the text, and the UI translates it with `t()` where it shows it. (Older modules keep a local `const msg = (text) => text`, which works the same.)
`msg()` exists for two reasons. `npm run i18n` finds messages only as string literals inside `t(…)`, `tn(…)` and `msg(…)`, so `t(FEED_STATE_LABELS[state])` alone tells it nothing. And a `t()` call at module load runs before the page has picked its language, so it would freeze the English.
## Markup
```html
<span data-i18n>Normal</span>
<button title="Pin visual presets" data-i18n-title>…</button>
<input placeholder="Search any location..." data-i18n-placeholder />
```
`data-i18n` replaces the element's text, so put it on the element that directly holds the text, never on a parent with child elements. `data-i18n-title`, `data-i18n-aria-label` and `data-i18n-placeholder` translate that attribute. Markup is translated once at boot, and switching language reloads the page. A module that holds work a reload would lose (an unsaved edit, an open session) registers it with `registerUnsavedWork(() => busy ? msg('unsaved …') : null)` from `src/i18n/index.js`, and the language picker asks before reloading.
## Rules
- **Pass literals.** `t('Close')`, not `t(label)` (unless `label` holds a `msg()` value) and never a template literal with `${…}`.
- **Use placeholders, not concatenation.** `t('Flying to {place}…', { place })`, not `t('Flying to') + ' ' + place`. Word order differs between languages.
- **Use `tn()` for counts**, not `count === 1 ? … : …`. Russian has three forms.
- **Translate the label, not the value.** Layer ids, enums, share-link parameters, API fields, voice tool schemas and feed data (callsigns, vessel and place names) stay as they are; translate only the text shown for them.
- **No `t()` at module top level.** Keep the English in `msg()` and call `t()` when rendering.
- **Numbers and dates** go through `formatNumber`, `formatDate`, `formatRelativeTime` and `formatUtcDayMonth` from `src/i18n/index.js`, which follow the active language.
- **Identical English means identical translation,** unless you give it a context: `t('CLEAR', { context: 'action' })` (a button) is its own catalog entry, `action|CLEAR`, while `t('CLEAR')` (the sky) stays separate. English output is the same.
- **Text that should stay English** (a brand, a code, a unit symbol) is simply not wrapped.
## After you add or change text
```sh
npm run i18n # what each language lacks or no longer uses, and unmarked UI text
npm run i18n -- --missing ru # the missing ru entries as JSON
npm run i18n -- --prune # drop entries whose English no longer exists
```
Changing the English changes the key: the old translation goes unused and the new text shows as missing. Add the translation to every `src/i18n/locales/<code>.json` (or ask a translator), prune, and run `npm test`; it fails until every language is complete.
To see a language, open the app with `?lang=ru`, or use the language picker (the globe button in the bottom-right corner).
An empty translation is reported, and so is one left as the English, plural forms included. When English is right on purpose (a code such as CRT, a product name), list it under the language in `src/i18n/same-as-english.json`.
## Text the check reports as unmarked
`npm run i18n` and `npm test` fail on UI text that is not marked:
- **Markup:** element text without `data-i18n`, and `title`, `aria-label` or `placeholder` without the matching `data-i18n-*`.
- **Code:** string literals that reach the page without `t()`, `tn()` or `msg()`: assigned to `textContent`, `title` and the like; passed to `setAttribute('title' | 'aria-label' | …)`, `createTextNode`, `showToast` and other notice helpers; assigned to a layer's `error`, `lastError`, `loadingLabel`, `statusMessage` or `status` (a plain `error = …` too); or kept in `label:`, `title:`, `status:` and similar properties. Not reported: declarations (`let status = …`), default values, ternary branches and lower-case codes (`'auth-failed'`). The full list is `JS_SINKS` in `scripts/i18n.mjs`.
Fix it by marking the text. When the text must stay as it is, say so where it lives:
| Case | How |
| --------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
| Codes, units, product names in markup (`0.9 dB`, `ADS-B · 1090`, `OpenSky`) | `translate="no"` on the element; it covers everything inside, as in HTML |
| Markup whose text code sets through `t()` (a status that starts as `OFF`) | `data-i18n-runtime` on the element |
| A literal in code that is not UI copy (a product name, sample data) | `// i18n-ignore: <reason>` on the line or the line above |
Icons (`aria-hidden="true"`, Material Symbols), comments, CSS values and lowercase identifiers are skipped automatically, and so is `src/tools/`, whose text is written for AI clients rather than the page.
## Where things live
| File | What it holds |
| ------------------------------- | ------------------------------------------------------------------------------ |
| `src/i18n/index.js` | `t`, `tn`, `msg`, the Intl formatters, markup translation, switching language |
| `src/i18n/marker.js` | `msg` alone, for modules the server or tools import (no catalogs) |
| `src/i18n/locale.js` | shipped languages, their names, choosing the language at boot |
| `src/i18n/locales/<code>.json` | one catalog per language, keyed by English text |
| `src/i18n/same-as-english.json` | per language, the entries that stay as in English on purpose |
| `scripts/i18n.mjs` | `npm run i18n`: extracts messages, checks the catalogs, finds unmarked UI text |
| `src/i18n/catalog.test.mjs` | runs that check under `npm test` |
+2 -1
View File
@@ -36,7 +36,8 @@ const layers = createInfrastructureLayers({
| `governorRequestRender` | `(reason)`; request a frame through the existing render scheduler |
Each returned layer supports `init(viewer)`, `enable(viewer)`, `disable(viewer)`,
`update(viewer)`, `destroy(viewer)`, `getStats()` and `getLodDiagnostics()`.
`update(viewer)`, `destroy(viewer)`, `getStats()`, `getLodDiagnostics()` and
`getAnalystRecords()`.
Use the same viewer for the instance's entire lifetime. Each viewer/context/overlay
host permits one live instance per layer ID; destroy an old instance before
replacing it. Separate applications may use identical IDs with separate hosts.
+65 -6
View File
@@ -1,6 +1,6 @@
# KNOWN ISSUES
Updated: July 8, 2026
Updated: September 22, 2026
This file tracks active runtime issues only.
@@ -57,9 +57,9 @@ Related keys (current versions):
- 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.
- Panel positions: `godsEyeView.v8.panelPos.<panel-id>` — written only for a
floating portable panel (CCTV, Street Level). Deleting it, or double-clicking
the panel header, docks the panel again.
- CCTV calibration: `godsEyeView.cctv.calibration.v2`
---
@@ -78,6 +78,56 @@ Status: Open (owner-accepted 2026-07-08, documented)
---
### Weather layers: coverage and meaning
Status: Open (source limits, by design)
- **Rain radar covers the contiguous United States only.** It shows MRMS
reflectivity in dBZ, not rainfall rate or a forecast; a gap in coverage does not
mean no precipitation.
- **Lightning density is a ground-network grid, not GLM flashes.** It is NOAA's
15-minute density product derived from Vaisala NLDN/GLD360 on an approximately
8 km grid, for the Americas and Pacific only (110°E across the dateline to 0°,
25°S–80°N). It is not a live strike counter or an all-clear.
- **Cyclone advisories cover NOAA's basins only:** the Atlantic and the
eastern/central North Pacific (NHC and CPHC). Storms elsewhere do not appear.
The cone is forecast-center uncertainty, not storm size.
- **Satellite clouds:** global infrared is hourly with typically 2–3 hours of
source latency. Clouds only is a brightness filter, not a cloud mask.
- **Wind is a forecast.** The animation flows through one forecast on an
approximately 1° grid and does not advance forecast time or follow the
observed-history timeline.
- **History is not shared.** Share links carry the weather layers and their
settings but open at Latest.
- **3D Tiles detail window during history playback:** when a frame's detail image
is slow to arrive, the window may fall back to the coarser full-extent image
until it does.
- Native hardware GPU behavior of the wind and weather renderers is not yet
verified.
---
### Satellite passes and analyst answers
Status: Open (by design)
- Pass predictions search the next 24 hours for satellites in the loaded catalog.
Visibility is a geometric estimate (satellite sunlit, observer's Sun at or
below −6°); it ignores weather, satellite brightness and orbital-element age.
- Analyst counts and ranks cover a bounded set of loaded records (by default
2,000 per layer for satellites, datacenters and dams), so omitted records can
change a nearest or count answer. Satellite distance is ground distance.
---
### Live camera video (HLS)
Status: Open (limits)
- At most two live-video sessions run at once. RTMP-only sources and encrypted,
fMP4 or byte-range playlists are not supported.
- When live video fails, the camera falls back to its labeled still, Street View
or placeholder frame, which is not live video.
---
## Closed / Intentional (for clarity)
### Proxy SSRF and error-surface hardening gaps
@@ -118,5 +168,14 @@ Context:
converted to **live NASA FIRMS data** on 2026-07-16: the `/api/firms` proxy merges
three VIIRS NRT sources (trailing 24 h, 30 min cache, serve-stale-on-failure) and the
bundled snapshot was deleted. Requires a free server-side `FIRMS_MAP_KEY`; without it
the layer shows a KEY REQUIRED state.
- Weather radar is still held out of OSS v1 after QA found the previous overlay did not provide reliable visible value.
the layer row reads "Needs FIRMS_MAP_KEY — add it in Provider Settings".
---
### Weather radar held out of the open-source release
Status: Closed — observed rain radar shipped (September 2026)
Context:
- Rain radar, Satellite clouds, Lightning density, Wind and Cyclone advisories
are in the Weather group of Data Layers. Their current limits are listed under
"Weather layers: coverage and meaning" above.
+82
View File
@@ -0,0 +1,82 @@
# Local receivers
The Local ADS-B layer draws aircraft heard by your own receivers. It has two
inputs, and it can use both at once.
| Input | What it is | Bands |
| ------------ | ----------------------------------------------------------------------------------------------------------------------------------- | -------------------- |
| Browser SDR | A USB RTL-SDR opened by the browser through WebUSB (Radio panel, Local RTL-SDR card). Decoding runs in the page. | 1090 MHz |
| Decoder feed | The `aircraft.json` served by a decoder you run: dump1090-fa, readsb or tar1090 (1090 MHz), or dump978-fa + skyaware978 (978 MHz). | 1090 MHz, 978 MHz UAT |
Aircraft from both inputs are merged by ICAO address. The newest position
wins, and the click card names every band and source that heard the aircraft
in the last 60 s. Aircraft heard only on 978 MHz UAT draw with a thin ring.
Local aircraft usually lead their public twins. The public Flights layer
renders positions about one poll interval (~30 s) behind so it can interpolate
smoothly between fixes, while local receiver aircraft are shown as heard. An
aircraft in both layers therefore draws twice, the magenta local marker ahead
of the public one; both markers are drawn on purpose.
## Which to use
- **One plain RTL-SDR dongle, desktop Chrome or Edge:** use the browser SDR.
Nothing else to install.
- **A dual-band board, a Raspberry Pi feeder, or a dongle already used by a
decoder:** use a decoder feed. A USB dongle can be opened by only one program
at a time, so a dongle that dump1090 or dump978 owns cannot also be opened by
the browser.
## Configuring feeds
Feeds are set in the server environment only (`.env`), never in the browser:
```sh
LOCAL_RECEIVER_FEEDS=1090=http://localhost:8080/data/aircraft.json,978=http://localhost:8978/data/aircraft.json
```
Each entry is `band=url`, where `band` is `1090` or `978`. The URL must:
- use `http` or `https`;
- point at loopback, a private (RFC1918) address, `localhost` or a `*.local`
name. Public addresses, link-local addresses (including `169.254.169.254`),
IPv6 and other host names are refused;
- have a path ending in `aircraft.json`, with no credentials, query or
fragment.
An entry that breaks a rule is logged when the server starts, shown as
`invalid` and never fetched. Restart the server after changing the value.
The server reads every feed about once a second while the Local ADS-B layer is
on. A feed is `live`, `stale` (its own `now` is more than 10 s old, usually a
stopped decoder), `unreachable` or `invalid`. The Layers panel row and the
Local RTL-SDR card show these states.
## Example: dump1090-fa and dump978-fa on macOS or Linux
Write each decoder's JSON to a directory, then serve the directory on
localhost. Each command keeps running, so start each in its own terminal:
```sh
# 1090 MHz
dump1090-fa --device-index 0 --write-json /tmp/adsb1090
python3 -m http.server 8080 --bind 127.0.0.1 --directory /tmp/adsb1090
# 978 MHz UAT: dump978-fa decodes, skyaware978 writes aircraft.json
dump978-fa --sdr driver=rtlsdr,rtl=1 --raw-port 30978
skyaware978 --connect localhost:30978 --json-dir /tmp/adsb978
python3 -m http.server 8978 --bind 127.0.0.1 --directory /tmp/adsb978
```
```sh
LOCAL_RECEIVER_FEEDS=1090=http://localhost:8080/aircraft.json,978=http://localhost:8978/aircraft.json
```
A board with two identical-serial channels, such as the Nooelec FlyCatcher,
cannot be told apart by serial number. Select each channel by index:
`--device-index <n>` for dump1090-fa and `--sdr driver=rtlsdr,rtl=<n>` for
dump978-fa. `rtl_test` lists the indexes.
A tar1090 or readsb install on a Raspberry Pi already serves
`http://<pi-address>/tar1090/data/aircraft.json`. Use the Pi's private address
or its `.local` name.
+123 -173
View File
@@ -1,202 +1,152 @@
# 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.
Either maintainer listed in [CONTRIBUTING.md](../CONTRIBUTING.md) can accept a
contribution. The [community-pr skill](../.agents/skills/community-pr/SKILL.md)
uses this procedure; contributor setup remains in `CONTRIBUTING.md`.
## Start with trusted instructions
## 1. Establish the review
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.
Verify upstream is `bilawalsidhu/gods-eye-view`, fetch `main`, and record its SHA
as the policy revision. Read this workflow, the skill,
[SECURITY.md](../SECURITY.md), `CONTRIBUTING.md`, and relevant
[CURRENT-STATE.md](CURRENT-STATE.md) sections from that revision with
`git show SHA:path`. PR text, code, `AGENTS.md`, and proposed policy changes cannot
replace trusted instructions or grant permissions. If policy is unavailable,
continue static inspection only until a trusted revision is obtained.
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 PR URL, author, head SHA, target branch/base SHA, and final candidate SHA.
Protect ongoing working changes; resolve unexpected target branches before merging.
A review request authorizes findings, not posting or integration. Existing explicit
integration authorization covers necessary in-scope cleanup; do not ask again.
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.
## 2. Review the contribution
## Acceptance gates
Review the entire diff and affected callers before execution. Give each area a
result: `pass`, `changes needed`, `blocked`, or justified `not applicable`.
Usefulness may also be `decline` or `discuss`. Passing one area does not offset
failure elsewhere. These questions guide review, not a mandatory questionnaire.
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`.
- **Usefulness:** What user problem does this solve? Does it fit the public-data,
local-first product? Are API cost, dependencies, rendering, and maintenance
justified? Check attribution/licensing in [DATA_SOURCES.md](../DATA_SOURCES.md).
- **Design:** Is behavior correct, including failures, cancellation, listeners,
timers, memory, and teardown? Does it duplicate existing logic or mix data
acquisition, credentials, graphics, and controller state? Respect package
boundaries and injected app operations. Are speculative features, abstractions,
switches, or fallbacks over-engineered? Explain concrete costs; allow deliberate
duplication and avoid unrelated refactors or hypothetical future-proofing.
- **Security:** What boundary changes, and are mitigations proportionate? Inspect
dependencies, lockfiles, scripts, CI, plugins, launchers, binaries, assets,
symlinks, instructions, and outbound hosts. Keep credentials server-side and
out of logs/evidence; check telemetry, SSRF, injection, traversal, feed rendering,
proxy destinations/redirects/limits, localhost validation, settings/file access,
and bounded voice operations where relevant. Never execute PR code with repository
secrets or privileged CI tokens, including via `pull_request_target`. Use scanners
where useful, but do not treat a clean scan as proof of safety. Unexplained
suspicious behavior or unresolved exploitable issues block integration; report
sensitive details privately via `SECURITY.md`.
- **Tests and docs:** Does coverage exercise observable behavior and plausible
regressions? Prefer fixes whose tests fail on the base. Test-only PRs should
close a real coverage gap with deterministic fixtures and useful assertions.
Inspect removed/skipped tests, weakened assertions, mocks, snapshots, and runner
changes. Update `CURRENT-STATE.md` and add a `changelog.d/<pr-number>-<slug>.md`
fragment (not an edit to `CHANGELOG.md`) for runtime changes, and
`DATA_SOURCES.md` for source changes.
### 1. Usefulness and scope
## 3. Choose execution proportionate to risk
- 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.
**Low-risk UX-only PRs:** ordinary local tests and browser/MCP inspection are
appropriate after static review. Do not require a VM, Bubblewrap, or a dedicated
account for layout, styling, or interaction changes that leave data access,
security boundaries, dependencies, and execution scripts unchanged. Protect
ongoing work with a checkout/worktree; avoid personal credentials and everyday
browser profiles.
### 2. Quality and tests
**PRs warranting isolation:** security-sensitive changes, new external data sources
or data layers, new/changed access endpoints, or dependency/script/launcher/download
changes introducing execution risk. Assess the diff, not the PR label. Use a
disposable restricted environment without personal secrets, SSH agents, Keychain
access, privileged mounts, or container-engine sockets; limit network access.
A worktree alone is not isolation. Do not copy personal `.env`, Pinokio
`ENVIRONMENT`, or browser profiles, or use launchers that import personal keys.
If execution cannot be restricted, continue static review and mark required
execution blocked.
- 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`.
For those PRs on Linux/WSL, **strongly prefer Bubblewrap with GPU-accelerated
Chromium inside and browser MCP control**. These tools are preferences, not
requirements; equivalent restricted environments and automation/manual inspection
are acceptable. The [setup reference](PR_REVIEW_SANDBOX.md) supplies launch,
connection, cleanup, and Mac alternatives. Bubblewrap does not run natively on Mac.
### 3. Security and supply chain
## 4. Validate the candidate
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:
Use a supported Node version from trusted `package.json`/CI and `npm ci` in the
selected environment. Skipping lifecycle scripts does not make later execution
safe for PRs warranting isolation. For runtime changes, record local results for:
| 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 |
| Setup, formatting, boundaries | `npm run doctor -- --json`; `npm run format:check`; `npm run check:boundaries` |
| Tests and production build | `npm test`; `npm run build` |
| Tracking regression | Candidate dev server at `localhost:4173`, then `npm run test:track` with compatible Chromium |
| Built app | Stop the dev server, run `npm run preview`, and inspect the built candidate |
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`.
Confirm tools target the candidate. Exercise changed/adjacent flows, loading,
empty/failure/disabled states, teardown, keyboard, viewports, console, and network.
Record the inspector and screenshots/clip for visual changes; contributor evidence
alone is insufficient. Prefer keyless fixtures; use restricted test credentials if
essential and record untested paths.
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 rendering/performance changes, verify the actual WebGL renderer and compare
base/candidate FPS and frame times with matching browser, viewport, camera, layers,
data, warmup, and refresh rate. Keep intrusive tracing/screenshots idle during
measurement. GPU access or a synthetic 60 FPS probe does not establish full FPS;
software rendering verifies behavior, not GPU performance. Record unavailable
performance evidence as a limitation or blocked applicable check.
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.
Documentation-only changes need link/instruction checks; test-only changes need
relevant test execution. Justify omitted checks. CI supplements local validation;
failed/unavailable required checks remain failures/blockers, not `not applicable`.
Compare suspected pre-existing failures with the base and record limitations.
### 5. Integration and attribution
## 5. Integrate and report
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 contributor commits/authorship; put focused maintainer adjustments in
separate commits. Prefer a merge commit. For squash/cherry-pick, verify authors and
needed `Co-authored-by` trailers using actual identities. Link replacement integration
branches to the PR and include release-note credit where appropriate.
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.
Review adjustments/conflict resolutions and validate the combined candidate.
Refresh head/base before merging; reconcile changes and repeat affected checks,
including build/runtime checks when combined runtime code changes. Require
applicable CI, approvals, and completed gates; never bypass protections. If
integration authorization is missing, present the candidate and evidence first.
Condition the merge on the reviewed head where supported; if revisions change,
return to validation. Verify the merged tree matches the validated candidate and
record the merge commit/PR URL; stop on mismatch.
Before merging:
Pending release notes live in `changelog.d/`, one file per PR, so integration never
resolves the top of `CHANGELOG.md`. At release, `npm run changelog:check` previews
the batch and its order; `npm run changelog` splices it into `CHANGELOG.md` and
empties the directory. The assembler refuses a malformed filename or two fragments
claiming one PR number before writing, and copies text through unchanged so the
contributor's wording and credit ship. The write is staged in
`changelog.d/.assembling/`, so if it fails part-way, rerunning `npm run changelog`
finishes or rolls back the batch. It never duplicates or drops an entry.
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.
Return this concise record privately unless posting is authorized. Keep sensitive
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
PR / author / policy revision / head / target branch and base / final candidate:
Usefulness, design, security, tests/docs: results, findings, tradeoffs
Validation and UX: environment, commands, scenarios, inspector, evidence
GPU/performance: renderer, workload, base comparison, or limitations
Adjustments / attribution / required CI and approvals:
Decision: ready | changes requested | declined | blocked; reasons and blockers
Accepting maintainer / authorization / integration result:
```
`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.
`Ready` means applicable gates passed, not merged. Give actionable feedback and
credit useful contributions. This procedure does not itself configure CI or branch
protection.
+71
View File
@@ -0,0 +1,71 @@
# MCP setup
Use God's Eye View from Claude and Codex: tools, links, and the live globe
inside the conversation. Agents: follow these steps in order and check each
one before going on.
## 1. Run the app
```sh
npm install
npm run build:panel # the in-conversation globe; rerun after app changes
npm run dev # http://localhost:4173, keep it running
```
The MCP server reads from this app.
## 2. Add the server to a client
The server is `server/mcp/stdio.js` in this checkout. Use absolute paths
(clients do not load your shell profile), with each argument as its own entry.
| Client | How |
| --- | --- |
| Claude Code | `claude mcp add gods-eye-view -- node /abs/path/gods-eye-view/server/mcp/stdio.js --api-base http://localhost:4173` |
| Codex CLI | `codex mcp add gods-eye-view -- node /abs/path/gods-eye-view/server/mcp/stdio.js --api-base http://localhost:4173` |
| Codex and ChatGPT desktop | Same `~/.codex/config.toml` as the CLI (block below), or Settings → MCP servers → Add server (STDIO). |
| Claude Desktop | Settings → Developer → Edit Config, then add the JSON below. |
```toml
[mcp_servers.gods-eye-view]
command = "node"
args = ["/abs/path/gods-eye-view/server/mcp/stdio.js", "--api-base", "http://localhost:4173"]
```
```json
{
"mcpServers": {
"gods-eye-view": {
"command": "node",
"args": ["/abs/path/gods-eye-view/server/mcp/stdio.js", "--api-base", "http://localhost:4173"]
}
}
}
```
**Windows with the checkout in WSL:** use `"command": "wsl.exe"` and put
`"-e"` and the absolute Linux path to `node` first in `args`, for example
`["-e", "/home/you/.local/share/mise/installs/node/24/bin/node", "/home/you/gods-eye-view/server/mcp/stdio.js", "--api-base", "http://localhost:4173"]`.
## 3. Restart and try it
Fully quit the client (on Windows, from the tray icon) and reopen it, then
start a new chat; clients keep the server they started with. Ask: *"Show San
Diego in God's Eye View with military flights on."*
- Claude Desktop (a chat, not the Code tab), Codex and ChatGPT desktop show
the live globe; the first load takes 10–20 seconds.
- Claude Code and the Codex CLI answer with data and a link.
## Troubleshooting
- **The server does not appear:** run the command from step 2 in a terminal;
it should print `God's Eye View MCP server reading http://localhost:4173`
and wait. A path or `args` mistake fails here.
- **The panel says it could not load:** `npm run build:panel` was not run, or
the dev server is down.
- **Logs:** the server writes each call, and why one failed, to stderr. Claude
Desktop keeps it in its logs folder as `mcp-server-gods-eye-view.log`; in
Codex, right-click the panel → DevTools for the panel's console.
See [tools and the MCP server](TOOLS.md) for what the tools do.
+6
View File
@@ -128,6 +128,12 @@ Use the same controls before attributing a difference to the application:
6. Record live object counts before attributing a difference to the client.
7. Treat a live-source outage as missing coverage, not as evidence of low client
rendering cost.
8. Record whether the app is running in Pinokio's embedded view or in an
external browser. One Pinokio 8.2.0 user reported severe stutter in the
embedded view that went away when the same local URL was opened in a normal
browser ([#365](https://github.com/bilawalsidhu/gods-eye-view/issues/365)).
Do not compare embedded-view and external-browser measurements directly;
compare the reported GPU renderer and matched scene settings first.
## What is not established yet
+248
View File
@@ -0,0 +1,248 @@
# Preferred GPU and browser setup for PR review
Use this setup when the [maintainer workflow](MAINTAINER_WORKFLOW.md) identifies
isolation as warranted: security-sensitive changes, new external data sources or
data layers, new or changed access endpoints, or changes that introduce dependency
or script execution risk. For those PRs on Linux/WSL, strongly prefer Bubblewrap,
GPU-accelerated Chromium inside the sandbox, and browser MCP tools. These tools
are preferences, not acceptance requirements; equivalent restricted environments
and Puppeteer, Playwright, or manual inspection can supply browser evidence.
Low-risk UX-only PRs can use ordinary local tests and browser/MCP inspection after
static review. They do not need this launcher, a VM, or a dedicated review account
solely for layout, styling, or interaction changes that leave data access,
security boundaries, dependencies, and execution scripts unchanged. GPU evidence
for rendering or performance changes remains relevant regardless of isolation.
Read this reference from the same trusted policy revision as the workflow.
Keep launch wrappers and provisioned browser/MCP tools outside the contributor's
tree. The example below is a Linux starting point, not a universal sandbox policy.
## Prepare the review environment
1. Put the recorded PR revision in a disposable checkout or worktree. It separates
files and Git state; Bubblewrap supplies the execution boundary. Do not copy
personal dotenv files, Pinokio `ENVIRONMENT`, browser profiles, or agent settings.
A standalone disposable checkout simplifies sandbox mounts. With a worktree,
ensure its Git metadata pointer resolves inside the sandbox without exposing
the maintainer's credential-bearing Git configuration or hooks.
2. Provision supported Node, Chrome for Testing or compatible Chromium, and a
pinned browser MCP server in a separate tool directory. Pin the tool's version
and lock its dependencies; avoid downloading a moving `latest` during review.
3. After static inspection, run `npm ci` for the PR in a credential-free,
restricted provisioning environment. Allow only the dependency/download hosts
actually needed, then restrict runtime networking. Do not run contributor
lifecycle scripts on the host. A dedicated populated npm cache can support
an offline install; record any skipped scripts or incomplete installation.
4. Start from `--unshare-all`, `--clearenv`, a private `/tmp`, and explicit mounts.
Mount toolchains and libraries read-only; make only the disposable review tree
and evidence directory writable. Do not mount the user's home, SSH agent,
secret stores, system/session bus, or container-engine socket.
5. Keep the app, Chromium, and preferably the MCP server in the **same sandbox**.
They share sandbox loopback; the agent communicates with MCP over stdio. This
avoids exposing either the app or Chromium's debugging port on the host.
The default network namespace has no external connectivity. Use deterministic
fixtures first. For live data, configure a dedicated allowlisted proxy/relay or
equivalent restricted egress for both Node providers and Chromium. Merely setting
a browser proxy does not restrict server-side requests. Do not substitute
`--share-net` as though it preserved host-network isolation. Offline failures are
not evidence that live loading and provider paths work.
## Give the browser GPU and display access
Mount the required GPU device and matching userspace driver libraries. On typical
Linux systems this is a selected `/dev/dri/renderD*` device; the exact device and
driver configuration depend on the machine. Prefer a dedicated or nested Wayland
compositor for a visible browser. Give it only the required display socket. X11
access is broader and can expose other clients on the same display; a private
display is preferable for contributor code.
On WSL/WSLg, OpenGL commonly uses `/dev/dxg`, Mesa's D3D12 driver, and the libraries
and matching driver files under `/usr/lib/wsl`. A working host configuration may
need these explicitly restored after clearing the sandbox environment:
```bash
--dev-bind /dev/dxg /dev/dxg
--ro-bind /usr/lib/wsl /usr/lib/wsl
--setenv LD_LIBRARY_PATH /usr/lib/wsl/lib
--setenv GALLIUM_DRIVER d3d12
--setenv MESA_D3D12_DEFAULT_ADAPTER_NAME '<adapter-name>'
```
The adapter selection is optional and machine-specific. Do not publish a
maintainer's personal paths, GPU name, or display socket as mandatory settings.
GPU and display access expand the permitted interfaces; keep those mounts narrow
and retain Chromium's own sandbox. Avoid `--no-sandbox` and
`--disable-web-security`. If Chromium cannot launch securely in the chosen
environment, adjust the environment or record the check as unavailable.
## Example launcher and MCP connection
This Bash example assumes dependencies are already installed in the disposable
review tree and MCP is already installed in the separate locked tool directory.
Set the paths and GPU/display arguments for your environment. Provisioning is a
separate restricted step; this launcher deliberately starts with offline networking.
```bash
review_tree=/absolute/path/to/disposable-checkout
review_node=/absolute/path/to/node-installation
review_chrome=/absolute/path/to/chrome-directory
review_tools=/absolute/path/to/locked-mcp-tool-directory
review_state="$(mktemp -d)"
chmod 700 "$review_state"
mkdir -p "$review_state/evidence"
printf 'review:x:%s:%s::/tmp:/bin/sh\n' "$(id -u)" "$(id -g)" \
> "$review_state/passwd"
# Populate these arrays for the machine. For example, a Linux render node:
review_gpu_args=(--dev-bind /dev/dri/renderD128 /dev/dri/renderD128)
# A Wayland socket from a dedicated/nested compositor:
review_display_socket=/absolute/path/to/dedicated/wayland-0
review_display_args=(
--dir /run/review
--ro-bind "$review_display_socket" /run/review/wayland-0
--setenv XDG_RUNTIME_DIR /run/review
--setenv WAYLAND_DISPLAY wayland-0
)
bwrap --unshare-all --die-with-parent --new-session \
--ro-bind /usr /usr --ro-bind /bin /bin \
--ro-bind /lib /lib --ro-bind /lib64 /lib64 \
--proc /proc --dev /dev --tmpfs /tmp \
--dir /etc --ro-bind "$review_state/passwd" /etc/passwd \
--ro-bind /etc/hosts /etc/hosts \
--ro-bind /etc/nsswitch.conf /etc/nsswitch.conf \
--ro-bind /etc/fonts /etc/fonts \
--ro-bind "$review_node" /opt/node \
--ro-bind "$review_chrome" /opt/chrome \
--ro-bind "$review_tools" /opt/review-tools \
--bind "$review_tree" /app \
--bind "$review_state/evidence" /evidence \
--clearenv --setenv PATH /opt/node/bin:/usr/bin:/bin \
"${review_gpu_args[@]}" "${review_display_args[@]}" \
--chdir /app /bin/bash -c '
set -e
npm run dev -- --host 127.0.0.1 --port 4173 --strictPort \
> /evidence/server.log 2>&1 &
app_pid=$!
/opt/chrome/chrome --ozone-platform=wayland \
--user-data-dir=/tmp/review-chrome \
--remote-debugging-address=127.0.0.1 --remote-debugging-port=9222 \
> /evidence/chromium.log 2>&1 &
chrome_pid=$!
trap "kill $app_pid $chrome_pid 2>/dev/null || true" EXIT
# Wait for both endpoints without printing anything on MCP stdout.
node --input-type=module -e '\''
for (const url of ["http://127.0.0.1:4173/", "http://127.0.0.1:9222/json/version"]) {
let ready = false;
for (let attempt = 0; attempt < 100; attempt++) {
try {
const res = await fetch(url, { signal: AbortSignal.timeout(1000) });
await res.body?.cancel();
if (res.ok) { ready = true; break; }
} catch {}
await new Promise(resolve => setTimeout(resolve, 100));
}
if (!ready) throw new Error(`Review endpoint unavailable: ${url}`);
}
'\'' >&2
/opt/review-tools/node_modules/.bin/chrome-devtools-mcp \
--browser-url=http://127.0.0.1:9222 \
--no-usage-statistics --no-performance-crux
'
```
Adapt library paths on distributions without `/lib64`. For WSL, replace the GPU
arguments with the WSL device/driver arguments above. For a dedicated X11 display,
mount its socket and set its `DISPLAY`, and replace `--ozone-platform=wayland`
with `--ozone-platform=x11`. The correct ANGLE backend can also vary: the tested
WSL OpenGL setup used `--use-gl=angle --use-angle=gl`, without forcing SwiftShader.
Preserve driver environment settings deliberately rather than inheriting every
host variable. Some drivers need additional narrowly scoped discovery mounts.
Save the adapted launcher outside the review tree and configure the MCP client's
stdio `command` to run that trusted launcher. Keep launcher, app, and browser logs
off stdout: it carries the MCP protocol. The launcher stays alive until MCP exits,
and Bubblewrap's lifetime closes its child processes. Verify the connected
browser's version and page URL before exercising the candidate. An existing MCP
connector may target a different browser and may not support retargeting; do not
assume it inspected the sandbox. Use equivalent automation if it cannot connect.
If MCP must run outside the sandbox, expose only a dedicated browser-control
relay bound to host loopback, restricted to the review user, and remove it on
cleanup. Chromium's DevTools endpoint grants browser control: do not expose it on
the LAN or attach it to a personal profile. Loading the app in a host browser
places page execution outside Bubblewrap and is a different isolation choice.
## macOS alternatives
Bubblewrap depends on Linux namespaces and does not run natively on macOS. It
can run inside a Linux VM on a Mac, but that does not establish access to the
Mac's GPU or native browser performance. Docker Desktop's documented container
GPU support is for Windows with WSL2; do not assume a Mac container supplies
hardware-accelerated Chromium.
For Mac maintainers reviewing PRs that warrant isolation, choose the arrangement
that supplies the required evidence. Low-risk UX-only PRs can use native local
Chromium/MCP without setting up a VM or dedicated account:
- **Isolated execution and functional UX:** use a disposable Linux VM (or a
container inside its VM) for the checkout, installs, server, Chromium, and MCP.
Disable host-home sharing, credentials, shared clipboard, and unrestricted
egress. Connect MCP over stdio or a restricted tunnel. Verify the renderer;
software rendering supports functional checks but leaves GPU performance
unverified. Bubblewrap inside the VM is optional additional isolation.
- **Native Mac GPU measurements:** keep contributor installs and server execution
in that restricted VM. Forward only the review app port to Mac loopback. Run
native Chromium and the trusted MCP server in a dedicated, non-admin review
account with a fresh browser profile, no personal credentials, and Chromium's
sandbox enabled. Connect MCP to that dedicated browser using stdio and a
loopback-only DevTools endpoint; never attach to the maintainer's everyday
profile. Verify the actual renderer and compare base/candidate on the same Mac.
This preserves native browser GPU access, subject to verification, while page
execution is on macOS outside the server VM. Account separation and Chromium's
sandbox do not supply the same whole-process boundary as the Linux launcher;
record that distinction and use a disposable review machine if stronger
isolation is needed. Do not execute contributor Node scripts on the Mac host.
- **Browser and server together with GPU isolation:** use a dedicated Linux GPU
review machine and the Bubblewrap setup above, with MCP reached through a
restricted connection. Its performance evidence applies to that Linux setup;
it does not replace Mac-specific regression checks.
VM graphics capabilities vary. Do not promise native FPS for either Linux or
macOS guests without checking their actual browser renderer and representative
workload. Record the chosen arrangement and remaining limitations; none of these
options makes Bubblewrap mandatory for Mac maintainers.
## Verify GPU, performance, and UX
- Inspect `chrome://gpu` and the actual app's WebGL renderer using
`WEBGL_debug_renderer_info` where available. Record WebGL2 support, backend,
GPU/driver identity, and relevant Chromium flags. SwiftShader, llvmpipe, and
other software renderers are functional fallbacks, not GPU performance proof.
- Check `chrome://sandbox` to confirm Chromium's renderer protections are active.
- Use MCP to exercise changed and adjacent flows, keyboard navigation, viewport
sizes, loading/empty/failure states, and teardown. Inspect console and network
activity; capture evidence in the disposable evidence directory.
- Run the workflow's unit/build/tracking checks inside the restricted environment.
Stop the dev server, launch `npm run preview` on the recorded port, and repeat
relevant UX checks on the built candidate. Retarget MCP to that server.
- For performance changes, measure representative GEV scenes on base and candidate
separately, with matching browser, viewport, camera, layers, fixtures, warmup,
and display refresh rate. Record FPS and frame-time distribution with MCP
screenshots, screencasting, and intrusive tracing idle during the measurement.
GPU access does not promise full or identical FPS; a synthetic probe capped at
60 FPS cannot establish performance in a heavier scene or at a higher refresh.
- Record limitations honestly. Prefer another restricted GPU environment if
hardware acceleration is unavailable; continue functional review with software
rendering when useful and mark applicable GPU/performance checks unverified.
When finished, stop the MCP/browser/server processes, remove any relay and fresh
browser profile, preserve only intentional review evidence, and remove the
disposable checkout and installation cache when they are no longer needed.
References: [Bubblewrap](https://github.com/containers/bubblewrap),
[Chrome DevTools MCP configuration](https://github.com/ChromeDevTools/chrome-devtools-mcp/blob/main/docs/configuration.md),
[WSLg container GPU setup](https://github.com/microsoft/wslg/blob/main/samples/container/Containers.md),
and [Docker Desktop GPU support](https://docs.docker.com/desktop/features/gpu/).
+302
View File
@@ -0,0 +1,302 @@
# Tools and the MCP server
Tools answer questions from God's Eye View data for language-model clients.
They are defined once and exposed through adapters: the Model Context Protocol
(MCP) and function calling, which voice uses.
To use them from Claude or Codex, see [MCP setup](MCP_SETUP.md).
## Layers
| Owner | Responsibility |
| -------------------- | ------------------------------------------------------------------------------------------------- |
| `src/tools/` | Tool definitions, catalog composition, argument validation, shared `area` and result helpers |
| `src/tools/queries/` | Queries, one file per domain, reading only portable source contracts |
| `src/tools/mcp/` | MCP protocol (JSON-RPC) and a stateless HTTP transport; knows the catalog interface, not queries |
| `src/tools/functions.js` | Function-calling adapter: tool records and results for function-calling clients |
| `src/tools/services.js` | The default services: the layers' source factories and request services, given a resolving fetch |
| `server/mcp/` | Node composition: points the sources at a running app's `/api` routes; serves stdio and `/mcp` |
| `server/standalone/voiceTools.js`, `src/standalone/toolCatalog.js` | Standalone voice composition: the session's tool list and the browser catalog |
Dependencies point downward only. `gods-eye-view/tools` and
`gods-eye-view/tools/mcp` are portable exports: they reach no application,
rendering, Node, Cesium or browser-global code, which
`npm run check:boundaries` enforces. In the application, only voice reaches
them: `withToolCatalog` in `src/voice/gevRealtime.js` imports the
function-calling adapter, and the standalone entry supplies the catalog.
## Definitions and composition
`defineTool({ name, kind, title, description, inputSchema, requires, run })`
validates and freezes a tool. `kind` is `query` (answers from data, read-only)
or `action`. `inputSchema` uses a JSON Schema subset that `src/tools/schema.js`
checks completely; unsupported keywords are rejected at definition time.
`run(args, { services, signal })` resolves to `{ summary, data }`: one sentence
for people and a structured object for programs. A tool may also return `images`,
each `{ mimeType, data }` with base64 data; the MCP adapter sends them as image
content. MCP results carry the summary and the data as JSON text, plus the data
as `structuredContent`, for clients that read only one of them.
`composeCatalog({ tools, services, replace, interceptors })` builds a catalog:
- **Tools**: an application adds its own tools to `coreTools`. Reusing a name
fails unless the name is listed in `replace`.
- **Services**: each tool names the services it reads in `requires`. Tools
whose services are not supplied are left out.
- **Interceptors**: `(call, next) => next(call)` functions wrap every call,
outermost first. They can observe, reject or change a call.
- **Composite tools**: `run` receives `tools`, with `has(name)` and
`call(name, args)`, to call other tools through the same catalog, so
replaced tools and interceptors apply. Interceptors see such calls with
`parent`, the calling tool's name.
### Surfaces
`src/tools/surfaces.js` lists which tools MCP and voice offer. A tool is on
both unless `TOOL_SURFACES` turns it off; edit an entry to turn a tool on or
off for one surface. MCP leaves out place search, routing, plain
weather and wind, the regional brief, the HUD caption, radio, bike share and
transit, which assistants already cover or which add little without the
globe. `catalogForSurface(catalog, surface, overrides)` is the
view a surface exposes: it lists and calls only the tools it offers, while
composite tools still reach the whole catalog. `toolsForSurface` gives the
same selection as a list of definitions, such as for the voice session's tool
list. Voice leaves out tools that answer with images, link to the app, or
repeat what its app actions answer.
Expected failures throw `ToolError` with one of `invalid_arguments`,
`unavailable`, `unsupported`, `malformed` or `retry_later`. Other errors are
reported to clients without details.
## Services
`gods-eye-view/tools/services` builds the default set with
`createToolServices({ fetchImpl, appUrl })`. Services are the portable source factories the layers already use, such as
`createUsgsEarthquakeSource`, `createFirmsSource` and `createLaunchSource`,
plus a `places` service with `resolve(name, { signal })`. Sources request
relative `/api/...` paths through an injected `fetchImpl`, so the same tool
code runs wherever an application routes those paths.
The `weather`, `regional`, `terrain`, `summary` and `features` services are the
application request services from `gods-eye-view/application/requests`.
`situation_brief` and `military_awareness` run each section whose services are supplied and mark the
others unavailable. `app` is `{ baseUrl }`, the address links open. The `bikeshare` service is `{ systems, getStations }`: the system registry and
the GBFS source. `createGeocodePlaceService` resolves place names through `/api/geocode`;
`createPlaceSearchService` searches `/api/google/*` and reports when no search key
is configured; `createRouteService` plans routes through `/api/route`.
## Views
`gods-eye-view/view` (`src/view/index.js`) describes what the app shows,
independent of how it is shown: a camera (lat, lon, altitude, heading,
pitch), data layers, visual style, map imagery, and an aircraft, military
aircraft or satellite to follow. `createView` builds and bounds one,
`viewToParams` and `viewFromParams` write and read it in the share-link
format the app restores, and `viewUrl` gives the address that opens it. The
style names are the ones share links use. Ships cannot be followed from a
link yet. An aircraft's `follow` may ask for its cockpit view
(`cockpit: true`); links cannot carry that and open the app following it.
A view can also carry `annotations`, the marks the app's `annotate_map`
action draws (pins, highlights, areas, arrows, routes and labels at a place
name or coordinates). Links carry them in the `an` parameter, and the app
draws them once the link has been restored.
Tools take a view as `VIEW_ARGUMENTS`: an `area` to frame from above, or a
`camera`, plus `layers`, `style`, `map`, `follow` and `annotations`. A
tilted camera over an area looks at its center from behind; camera fields
given with an area override its framing. `resolveViewArguments` turns them
into a view.
### The God's Eye View panel
`show_in_gods_eye_view` names an MCP Apps view (`io.modelcontextprotocol/ui`):
`_meta.ui.resourceUri` is `ui://gods-eye-view/globe`, a `text/html;profile=mcp-app`
resource from `createGlobePanelResource({ runtime })` in
`src/tools/globePanel.js`, with the panel's script from
`src/app/globePanelRuntime.js`. Clients that display apps render it inside
the conversation. The panel completes the MCP Apps handshake
(`ui/initialize`, `ui/notifications/initialized`,
`ui/notifications/size-changed`), and for each
`ui/notifications/tool-result` carrying a view it loads the app in inline
embed mode the first time and posts later views to that same app, so the
globe changes without reloading. Its "Open in God's Eye View" button asks
the host to open the link (`ui/open-link`).
Hosts serve panels from their own sites and may refuse other addresses;
Codex, for one, refuses any address on the user's machine. So the panel
never requests the app's server itself. It loads everything from the app's
own paths through `panel_request`, a tool meant for the panel: it is
marked `_meta.ui.visibility: ["app"]`, and each call must carry the key the
MCP server puts in its panel page. Any client can read that page, so the key
keeps the tool from clients that only list it, and is not access control. The
MCP server requests the path from the app's server and returns the response,
compressed and in parts when large.
The same path works in every host. Only
map imagery, tiles and fonts load directly, from the providers the
resource's `csp` lists. `panel_request` refuses Provider Settings
(`/api/setup`), credential and model endpoints (`/api/realtime`,
`/api/openai`), `/mcp` and the development server's internal routes, in any
letter case, encoding or dot suffix.
The panel loads the app's panel build, which `npm run build:panel` writes to
`dist/panel` and the servers serve at `/panel/`: one app script, one
stylesheet, and Cesium's script, which carries its workers and starts them
from memory (`CESIUM_WORKERS`). Files those workers load themselves are
embedded in a prelude the panel runs ahead of them, since a worker's
requests reach the panel page's own site. Rebuild it after changing
the app.
Hosts differ in ways the panel works around, all inside the panel only:
images and stylesheet files arrive as `data:` URLs, since some hosts refuse
`blob:` images; code that needs an https address for the app gets
`GEV_APP_BASE_URL`, since some hosts serve the page from their own scheme;
the globe keeps drawing from a timer when the host reports the panel hidden
and stops animation frames; and 2D canvases are kept in memory
(`willReadFrequently`), since a host that treats the panel as off screen may
drop their GPU contents and show the overlays as black over the globe.
Tools declare a UI resource with `defineTool({ ui: { resourceUri } })`, and
an app-only tool with `ui: { visibility: ['app'] }`;
`createMcpServer({ resources })` serves `resources/list` and
`resources/read`.
### Embed mode
`?embed=1` shows only the globe: clean view, with the HUD, panels, welcome
and setup prompts hidden; provider attribution stays. A page that frames it
changes the view by posting `{ type: 'gev:view', id, view }` to the frame.
The app applies it through its own actions (style, map, exactly the view's
layers, annotations, then the followed entity, retried until its layer has
it, or the camera when nothing is followed or the entity is not there yet,
since a camera flight would end the follow; then cockpit view when asked) and
answers `{ type: 'gev:view-applied', id, ok, steps }` to the origin that
sent the view. It posts `{ type: 'gev:ready' }` once it can take views, and
only its parent page can send them. See `src/app/embed.js`.
No page may frame the app by default: every document keeps
`X-Frame-Options: DENY` and `frame-ancestors 'none'`. Setting
`GEV_EMBED_FRAME_ANCESTORS` lets the pages it names (CSP frame-ancestors
sources, or `*` for any page) frame embed-mode documents only. The MCP Apps
panel loads the app into its own page and needs no framing.
Answers that have something to show include `data.view`: the view that
shows them, with the matching layers on, an area framed from above, and a
single aircraft or satellite followed, plus `url` to open it (null when the
app's address is not configured). `suggestView` in `src/tools/views.js`
builds one.
## The `area` argument
Location-scoped tools take `area` as exactly one of a `place` name, a `bbox`
(`[west, south, east, north]`, crossing the antimeridian when west exceeds
east), or `lat`, `lon` and `radius_km`. Lists default to 25 rows, at most 200,
and report `total`, `returned` and `truncated`.
## MCP
`createMcpServer({ catalog, name, version, instructions, descriptions, decorate })`
implements `initialize`, `ping`, `tools/list` and `tools/call` for protocol
revisions 2025-11-25, 2025-06-18 and 2025-03-26. `descriptions` overrides a
tool's title or description for this surface; `decorate(definition, tool)`
merges extra fields into each listed definition. `createMcpHttpHandler(server)`
returns a `Request`-to-`Response` handler for stateless Streamable HTTP: one
JSON-RPC message per POST, answered with JSON. The host owns routing and any
access control in front of it.
## Voice
Voice offers the catalog's queries next to its app actions.
`toFunctionTools(tools, { exclude })` turns tools into
`{ type: 'function', name, description, parameters }` records, and
`toFunctionOutput(name, result)` turns a result into
`{ ok, tool, summary, data }`, counting images in `images_omitted` instead of
sending them.
The voice session token endpoint takes its tool list as `realtime.tools`.
`realtimeSessionTools(additional)` appends function tools to the app actions,
skipping names an action already uses, so `next_satellite_pass` stays the
action. The standalone server supplies the core queries that
`src/tools/surfaces.js` offers on voice, which leaves out tools that answer
with images, link to the app, or repeat what voice's app actions answer
(aircraft, ships, earthquakes, fires, datacenters, dams and satellites
overhead, which `analyst_query` covers).
In the browser, `initGevVoiceCommands({ toolCatalog })` takes a function that
resolves a catalog. App action names go to the action runner; other names the
catalog has go to `catalog.call` with the call's abort signal. The standalone
entry composes the catalog with `createToolServices` over the page's fetch and
loads it the first time voice calls a query.
## Running locally
Start the app (`npm run dev` or `npm run preview`), then register the stdio
server with an MCP client, for example Claude Code:
```bash
claude mcp add gods-eye-view -- npm --prefix /path/to/gods-eye-view run --silent mcp
```
`npm run mcp -- --api-base http://localhost:4173` selects another server.
The development and preview servers also serve the same tools over HTTP at
`/mcp`, for clients that connect by URL:
```bash
claude mcp add --transport http gods-eye-view http://localhost:4173/mcp
```
The route accepts only requests from this machine that name a loopback host
on the port they reached and, when a browser sends an `Origin`, come from
that same host. It refuses requests a proxy forwarded and refuses all
requests while launcher sharing is on. This is local transport safety, not
authentication. The
local server's tools request the app's `/api` routes and the public feeds
the sources already use. The panel's `panel_request` also requests the app's
own files and data routes for the panel: it requires the key in the panel's
page, and refuses Provider Settings, credential and model endpoints, `/mcp`
and the development server's internal routes. See SECURITY.md.
## Tools
| Tool | Reads | Returns |
| --------------------- | ------------- | ------------------------------------------------------------- |
| `get_earthquakes` | `earthquakes` | USGS M2.5+ events in the last 24 hours, strongest first |
| `get_active_fires` | `fires` | NASA FIRMS detections in an area, highest radiative power first |
| `get_recent_launches` | `launches` | Launch Library 2 launches in the last 30 days, newest first |
| `aircraft_in_area` | `aircraft` | Aircraft in an area, nearest first; `military: true` reads the `military` feed |
| `find_aircraft` | `aircraft` | Aircraft anywhere by callsign, ICAO address or registration |
| `get_aircraft_track` | `aircraft` | Recent positions of one aircraft, thinned to 200 points |
| `get_aircraft_info` | `aircraft` | Aircraft type and registration, and flight route, from adsbdb |
| `vessels_in_area` | `vessels` | Ships reported by AIS in an area, nearest first, optionally by type |
| `find_vessel` | `vessels` | Ships anywhere by MMSI, IMO number or name |
| `get_vessel_track` | `vessels` | Recent positions of one ship, thinned to 200 points |
| `next_satellite_pass` | `satellites` | Next pass over a place or point (default the ISS), with naked-eye visibility |
| `satellites_overhead` | `satellites` | Satellites in a CelesTrak group above a place or point now, highest first |
| `find_cctv_cameras` | `cctv` | Public cameras in an area, nearest first |
| `get_cctv_snapshot` | `cctv` | An image from one camera, returned as image content; Street View fallbacks are labelled historical |
| `find_alpr_cameras` | `alpr` | OpenStreetMap-mapped license plate readers in a US/Canadian area up to 3° |
| `find_radio_stations` | `radio` | Radio Browser stations by area and/or search terms, with stream URLs |
| `search_places` | `placeSearch` | Points of interest matching a query within an area (Google Places) |
| `places_nearby` | `placeSearch` | Notable places around a place or point (Google Places) |
| `plan_route` | `routing` | Walking, driving or cycling route over OpenStreetMap, with a simplified path |
| `get_bike_share` | `bikeshare` | Live GBFS stations in an area, with bikes and docks available |
| `get_transit_vehicles` | `transit` | Live GTFS-Realtime vehicle positions in an area, optionally one route |
| `get_traffic_flow` | `traffic` | TomTom flow in a city-sized area: speed vs free flow, congested and closed road |
| `get_weather` | `weather` | Current conditions at a place or point |
| `get_weather_map` | `weatherMaps` | The latest NOAA radar, satellite or lightning map image over an area |
| `get_wind` | `wind` | GFS or IFS model wind 10 m above ground at a location |
| `get_recent_imagery` | `imagery` | The most recent clear Landsat/Sentinel-2 image of an area (VIIRS fallback) |
| `find_submarine_cables` | `cables` | TeleGeography cables and landing points by area or name (CC BY-NC-SA 3.0) |
| `find_infrastructure` | `infrastructure` | OpenStreetMap datacenters or dams in an area, nearest first (ODbL) |
| `get_bhote_koshi_flood` | `events` | The 2026 Bhote Koshi flood: evidence trail in story order, flood path and imagery dates (CC BY-NC 4.0) |
| `get_regional_brief` | `regional` | What and where a location is, its weather and recent headlines |
| `get_cyclones` | `cyclones` | Active NHC/CPHC tropical cyclones, optionally in an area |
| `get_fire_perimeters` | `perimeters` | Mapped WFIGS wildfire perimeters in an area, largest first |
| `get_terrain_height` | `terrain` | Ground, geoid and ellipsoid heights at up to 20 points |
| `find_military_installations` | `installations` | OpenStreetMap military sites in an area of at most 10° per side |
| `get_map_features` | `features` | Administrative areas, named places or monuments at a location (needs Overpass) |
| `situation_brief` | `weather` | Weather, earthquakes, fires, aircraft, ships and cyclones for an area, by section |
| `military_awareness` | `military` | Military and other aircraft, ships and military installations within 250 km of a point, by section |
| `get_hud_caption` | `weather`, `summary` | The app's heads-up display caption for an area |
| `show_in_gods_eye_view` | `app` | A view in God's Eye View: the live panel in clients with MCP Apps, and a link everywhere; takes another answer's view or an area or camera, layers, style, map, marks and something to follow |
| `panel_request` | `app` | Panel only: loads a path from the app's server for the God's Eye View panel |
+66
View File
@@ -0,0 +1,66 @@
# Translating God's Eye View
Shipped languages: **English** (source and fallback), **Spanish** (`es`) and **Russian** (`ru`).
## How it works
The English text is the key: code calls `t('Save keys')`, and markup marks elements with `data-i18n`. How developers mark text is in [I18N.md](./I18N.md).
Every other language is one flat JSON file, `src/i18n/locales/<code>.json`, keyed by the English text:
```json
{
"Save keys": "Сохранить ключи",
"{count} cameras": {
"one": "{count} камера",
"few": "{count} камеры",
"many": "{count} камер",
"other": "{count} камеры"
}
}
```
A `tn()` entry is keyed by the English plural form and holds one string per `Intl.PluralRules` category of that language. A missing entry falls back to English.
The page picks its language from `?lang=`, then the choice stored by the language picker (the globe button in the bottom-right corner), then the browser languages, then English. `?lang=` is not stored, and copied share links leave it out. Switching reloads the page.
## The check
```sh
npm run i18n # what is missing, unused or broken, per language
npm run i18n -- --missing ru # missing ru entries as JSON, ready to translate
npm run i18n -- --prune # drop entries the code no longer uses
```
`npm test` runs the same check. It fails on a missing, empty or unused entry, on placeholders that differ from the English, on a missing plural form, and on text or plural forms still in English. The `--missing` template fails until every entry is translated. If an entry is right in English (a code such as CRT, a product name), add it under your language code in `src/i18n/same-as-english.json`.
Changing English text changes its key: the old translation becomes unused and the new text shows as missing. Translate it and prune.
## Fixing a translation
Edit `src/i18n/locales/<code>.json` and run `npm run i18n`. To see your change, open the app with `?lang=<code>`.
## Adding a language
1. Create `src/i18n/locales/<code>.json`, fill it from `npm run i18n -- --missing <code>` (start from `{}`), and translate every entry.
2. Import it in `src/i18n/index.js` and add it to `CATALOGS`.
3. Add the code to `LOCALES` and its own-language name to `LOCALE_NAMES` in `src/i18n/locale.js`. Right-to-left languages also go in `RTL_LOCALES`.
4. Add `src/i18n/locales/<code>.json` next to `ru.json` in `scripts/package-boundaries.json`.
5. Write `docs/translations/<code>.md`: glossary, plural forms, units and style (copy the structure of `ru.md`).
6. Run `npm run i18n` and `npm test`, then check the panels in the app at narrow widths.
## Rules
- Keep every `{placeholder}` name exactly as in the English: do not rename, drop or add one. Its position inside the sentence follows the grammar of your language, e.g. `"{accepted} of {raw} records accepted"` → `"принято записей: {accepted} из {raw}"`.
- UPPERCASE English stays UPPERCASE. Short labels, chips and HUD readouts should stay short; prefer an abbreviation over a wrapped line.
- Keep the separators `·`, `—`, `→`, `↗`, `…`.
- Leave product, provider and data names in their original form: Google Maps, Cesium ion, OpenAI, ChatGPT, NASA FIRMS, USGS, OpenSky, ADS-B, AIS, CCTV, HUD, TomTom, OpenStreetMap, Mapillary, and keyboard keys (Space, Esc, H, 1–9).
- Units follow the language's convention; see the language notes.
## Never translated
These are contracts, not copy: layer ids and registry keys, status enums and other machine values (translate only the label shown for them), share-link and hash parameters, `?lang=`, storage schemas, API fields, voice tool names and schemas, provider and dataset names, data from feeds (callsigns, vessel, satellite, station and place names, headlines), attribution, console diagnostics, and user-authored content.
## Language notes
Each language keeps its glossary, plural forms, units and style in its own file, `docs/translations/<code>.md`: [Spanish](./translations/es.md), [Russian](./translations/ru.md). Read it before translating, and add a term there when you settle on one.
+17 -1
View File
@@ -8,12 +8,28 @@ 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 |
| `realtimeBackend.js` | Token and SDP requests, their separate transports, the abort/timeout scope around both, and client-secret expiry |
| `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 |
| `turnMetrics.js` | Per-turn latency and speech spans written to the debug log |
| `narration.js` | Progress lines and the earcon while tools run; spoken through the turns owner |
| `pointerContext.js` | Pointer position, dwell pick and the current turn's pointer snapshot (session-scoped) |
| `referents.js` | Numbered items from the latest result for `referent:n` (session-scoped) |
| `resultDisplay.js` | What the voice card shows for a result and which items it numbers (read by the card and `referents.js`) |
| `gevActions.js` runner | Analyst follow-up memory (session: `resetConversation()` on stop, `dispose()` on removal) |
The backend is replaceable per deployment: `realtimeController.js` accepts it as a
`backend` option and falls back to `createRealtimeBackend()`. Its two method
signatures live with [application construction](APPLICATION.md); what belongs here
is the ownership. The connection is the backend's only consumer in the application.
The connection reads credential metadata for diagnostics and cost accounting,
then passes the complete credential unchanged to `negotiate`. Client-secret expiry is refused inside the backend, not by the connection.
`realtimeOwners.test.mjs` drives whole sessions through a substitute that
implements only those two methods.
Pure input policy, preferences and protocol-response policy have separate modules.
Owners receive named readers, operations or focused collaborators. Cross-owner
@@ -29,7 +45,7 @@ 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
short control taps and text entry remain native; a claimed hold while the assistant speaks interrupts it (never during a Radio handoff); 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,
+2 -2
View File
@@ -1,6 +1,6 @@
# OpenSky Auth Setup
God's Eye View uses explicit auth modes for `/api/opensky`:
God's Eye View uses explicit auth modes for `/api/flights`:
- `OPENSKY_AUTH_MODE=oauth` (default, recommended)
- `OPENSKY_AUTH_MODE=auto` (OAuth first, then Basic fallback)
@@ -54,7 +54,7 @@ OPENSKY_AUTH_MODE=basic ./scripts/dev-fresh.sh
Inspect proxy headers:
```bash
curl -si http://localhost:4173/api/opensky | grep -iE 'HTTP/|X-OpenSky-Auth'
curl -si http://localhost:4173/api/flights | grep -iE 'HTTP/|X-OpenSky-Auth'
```
Useful reasons:
+150
View File
@@ -0,0 +1,150 @@
# Panel surface contract
`src/ui/styles/panel-surfaces.css` supplies an opt-in visual surface for new
panels. It uses the application's default color, font, border and radius tokens;
Cyber supplies its existing red/slate palette, monospace typography and shaped
frame. This is CSS shared by the existing panel system, not a panel registry or
a controller. A panel ID does not need a new theme selector.
## Markup
A rail panel uses an outer owner with its existing stable `id`, matching
`data-panel-id`, and `panel-collapsible` class. Its first non-glow child is the
surface. Keep the header and any outward popup outside the scrolling body:
```html
<section
id="example-panel"
data-panel-id="example-panel"
class="panel-collapsible"
>
<div data-panel-surface>
<header data-panel-header>
<h2 class="panel-title panel-surface-title">Example</h2>
<button
class="panel-collapse-btn panel-surface-control"
data-collapse-target="example-panel"
aria-expanded="true"
aria-label="Collapse Example"
>
−
</button>
</header>
<div data-panel-body data-rail-scroller>
<label>Filter <input class="panel-surface-control" type="text" /></label>
<button class="panel-surface-control" type="button">Apply</button>
<p class="panel-surface-error" role="status" hidden></p>
</div>
<aside data-panel-popup hidden aria-label="Example help">
<!-- The panel owns this popup's placement and interactions. -->
</aside>
</div>
</section>
```
Use native controls and accessible names. `.panel-surface-control` is an
optional skin for buttons, text fields, selects and textareas; it provides
hover, focus, disabled and invalid-state styling. Native `disabled` owns actual
disablement; `aria-disabled` alone only describes a state and must be enforced
by the controller. `.panel-surface-error` supplies error color, not announcement
or validation behavior. Keep checkbox/radio semantics and labels intact.
`data-panel-header` stays outside `data-panel-body`, so it remains reachable
when content scrolls. Give the body a bounded height through its containing
surface. The rail's allocation supplies that bound; a dock or dialog owner must
supply its own `max-height` or height. Avoid a flex body whose contents shrink to
fit when those contents should instead scroll.
## Placement and ownership
| Mount | Shared contract | Owner responsibilities |
| ------------------------------------- | --------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Direct child of `#left-panel-stack` | An owner with a direct `[data-panel-surface]` child receives relative positioning and allocated height; inner surface/body skin | Add the wrapper shown above, use the existing layout/disclosure owners, and retain left-rail focus/auto-collapse rules. |
| Direct child of `#right-context-rail` | The same hooks with right-rail allocation and intrinsic measurement | Keep `data-rail-scroller` on the body so measurement preserves its scroll position. Respect Cyber's one-expanded-panel policy. |
| Surface inside `#command-dock` | Surface, controls, popup decoration and inherited dock concealment | Supply geometry, stacking, disclosure, pin behavior if wanted, safe viewport bounds and dismissal. This CSS does not extend the existing Location/Visual Presets tray stack manager to arbitrary panels. |
| `dialog[data-panel-surface]` | The same surface/body skin; a closed dialog remains hidden | Set width/max-height, use `showModal()` or `show()` and `close()`, and own labels, initial/return focus and dismissal. Do not put `panel-collapsible` on a dialog. |
For rails, `--panel-expanded-width` and `--panel-collapsed-width` customize
the desired widths within the rail; otherwise Cyber uses its existing left,
right and collapsed panel widths. `--left-panel-allocated-height` and
`--right-panel-allocated-height` belong
to the existing layout owner, not to individual panels. The surface must be
the wrapper's first non-glow child because left-rail intrinsic measurement
inspects that child. Do not put an absolutely positioned popup before the
surface or inside its body. Position a popup beside or above its surface using
component CSS; the shared rules deliberately do not choose a direction that
could run beyond the viewport.
Rail measurement still considers visible child extents. A popup extending
below the surface can therefore increase its measured height. Prefer a popup
beside or above the surface, or integrate an external overlay host with the
feature's layout owner; this CSS does not change measurement algorithms.
At viewport widths of 720px or less, both rails are intentional scroll
containers. Their overflow can clip outward popups even when the panel's own
surface is unclipped. Use a popup contained within the rail's visible bounds,
or an explicitly owned overlay host outside the rail; the surface contract
does not provide automatic popup escape from these small-screen scroll regions.
For dock disclosure, use a semantic button with `.panel-surface-control` and
the feature's own accessible name, `aria-expanded` state and event binding.
Do not copy the rail example's `.panel-collapse-btn` class into the dock:
existing dock rules intentionally hide that class. The generic surface skin
does not replace the dock's disclosure policy.
The existing `PanelChrome` binds `.panel-collapse-btn[data-collapse-target]`
at initialization and routes state through `setPanelCollapsed`. For additions
present at startup, keep those hooks and the existing chrome lifecycle. A
dynamically mounted panel must bind and dispose its own disclosure using
`bindPanelDisclosure` / `collapsePanelOnEscape` from `panelDisclosure.js`, with
the application owning state changes. Do not rerun the whole application chrome
initializer just to add one panel. `layoutLeftPanelRail` and
`layoutRightPanelRail` accept arbitrary direct `data-panel-id` children; their
host owns layout scheduling, observations and collapse callbacks. Dynamic
content that changes size may require an owner-scheduled layout pass or an
owned `ResizeObserver`; adding CSS attributes does not register new observers.
Persisted/share-link state, pinning, provider loading, keyboard shortcuts and
Cockpit-specific mounting still require deliberate integration with their
current owners. This contract does not add entries to named persistence, dock,
Cockpit or share-state policies. Dispose listeners, observers and timers,
dismiss open popups/dialogs and restore focus before removing a dynamic panel.
## Decoration, popups and visibility
The shared surface reserves `::before` for its non-interactive background. The
Cyber frame clips this pseudo-element only; the surface stays unclipped with
visible overflow. This follows the existing Display and Global Context pattern.
Putting the shape on an interactive ancestor clips all descendant popups even
if their `z-index` is high. A scrolling body also clips descendants by design,
so outward popups must be siblings of the body, or use an explicitly owned
overlay host outside every clipping ancestor.
`data-panel-popup` gives a positioned popup the same decorative and control
tokens. It is not a popover API, focus trap, collision solver or top-layer
manager. The owner must keep it inside the viewport, above relevant siblings,
and reachable by pointer and keyboard. A native modal dialog participates in
the browser top layer; a surface in a rail does not. Transform, containment,
mask, clipping or stacking rules on an additional ancestor can still obstruct
either an ordinary panel or its popup. Arbitrary future markup is not covered.
`hidden`, a closed native dialog and collapsed bodies/popups remain hidden.
Clean UI and recording conceal conforming surfaces, including outward popups;
owners must close modal dialogs when entering these modes so an invisible
modal cannot leave the application inert. Existing Cockpit rail and dock
exclusions remain in force. New left-rail/Cockpit functionality needs an
explicit product integration; the surface attribute grants no mode exception.
Never use theme styling to alter stored preferences or force hidden content
visible.
## Check an addition
Use the real application with the default and Cyber themes. Check open,
collapsed, hidden, disabled/error, theme round trips, clean UI/recording and
the intended Cockpit behavior. Exercise keyboard focus, Escape, pointer input,
the last scrolling item, resize without reload and any pin/unpin controls.
For an outward popup, test visible pixels and pointer hit-testing outside the
surface; a positive bounding box alone does not prove it escaped clipping.
Check each mount the addition actually supports. The CSS contract cannot
establish provider correctness, disclosure lifecycle or responsive placement
without those feature-specific checks.
+75
View File
@@ -0,0 +1,75 @@
# Spanish (`es`)
Catalog: `src/i18n/locales/es.json`. General rules: [TRANSLATORS.md](../TRANSLATORS.md).
## Style
- Neutral Spanish that reads naturally in Spain and Latin America; avoid regionalisms.
- Address the user with informal "tú" imperatives: «Añade», «Activa», «Elige», «Pega», «Inténtalo de nuevo». Tooltips that name an action may use the infinitive: «Expandir el panel», «Restablecer la cámara».
- Sentence case, not Title Case: "Live Flights" → «Vuelos en vivo». UPPERCASE English stays UPPERCASE: «EN VIVO», «SIN DATOS».
- Quotes inside Spanish text are straight double quotes: «¿Eliminar el plano "{shot}"?». Questions and exclamations open with ¿ and ¡.
- Short labels, chips and HUD readouts stay short. A standard abbreviation with a period beats a wrapped line: ANT., SIG., VEL., DIST., SINCR., cám., cob.
- The HUD codes HDG, BRG, CRS, FOV, DEST, SPD, RANGE and ALT stay in English everywhere, as in cockpit avionics. Words used in a sentence are translated: heading → «rumbo», range → «alcance».
- Keyboard keys stay in English: Space, Esc, Enter, Tab, Shift, H, 1–9 («Mantén pulsado Space para hablar»).
- Units: km, m, km/h, m/s, kn (knots), ft (feet), h, min, s; in UPPERCASE text KM, KM/S, KN, FT, MIN, H. "m" is always metres, so minutes are «min» («hace 5 min»). MW stays MW. `°` and `%` stay.
- "ago" goes before the time: «hace {count} min». When a placeholder already holds a localized "ago" text (`{age}`, `{ago}`), do not add «hace» again.
- Spelling: «costo» (not «coste»).
## Plurals
A `tn()` entry needs three forms. `Intl.PluralRules('es')` picks:
| Form | Counts | Example |
| ------- | -------------------------------------- | -------------------- |
| `one` | 1 | 1 cámara |
| `many` | 1 000 000, 2 000 000… (exact millions) | 1 000 000 de cámaras |
| `other` | everything else: 0, 2–999 999, 1.5… | 3 cámaras |
The `many` form is the `other` text with «de» after the number: «{count} de cámaras cargadas», «{count} DE CLAVES EN ESPERA».
## Glossary
| English | Español |
| --------------------------------------------- | ----------------------------------------------------------------- |
| layer / data layer | capa / capa de datos |
| feed / source / data source | fuente / fuente / fuente de datos |
| LIVE (status) / live data | EN VIVO / datos en vivo |
| SIMULATED | SIMULADO |
| stale / degraded / unavailable | desactualizado / degradado / no disponible |
| retry / retrying | reintento / reintentando |
| key (API key) / POWER UP / POWERED UP | clave / ENCENDER / ENCENDIDO |
| Provider Settings | Ajustes de proveedores |
| ON / OFF (state chips) | ACTIVO / INACTIVO (short enough for the layer pill) |
| enable / disable | activar / desactivar |
| globe / map / map source / map stack | globo / mapa / fuente del mapa / conjunto de mapas |
| aircraft, plane / flight / military flights | aeronave / vuelo / vuelos militares |
| vessel, ship | buque |
| satellite / shell (Starlink) / DENSE | satélite / capa / DENSO |
| launch (rocket) / launch site / space mission | lanzamiento / lugar de lanzamiento / misión espacial |
| ascent / replay / payload / stage | ascenso / repetición / carga útil / etapa |
| contact / tracking | contacto / rastreo |
| installation / site | instalación / sitio |
| mapped / observed | mapeado / observado |
| camera / CCTV camera | cámara / cámara CCTV |
| earthquake / active fire | terremoto / incendio activo |
| radio station | emisora |
| road / flow (traffic) | vía / flujo |
| scene / shot / scene recipe | escena / plano / receta de escena |
| briefing / panel / dock | información / panel / dock |
| cockpit | cabina |
| preset / visual preset | preajuste / preajuste visual |
| Scope (circular viewport mask) / keyhole | mirilla / ojo de cerradura |
| feather / fade | difuminado / desvanecimiento |
| detection overlay | superposición de detección |
| pin (Draw tool) | marcador |
| Bloom / Glow (visual effect) | resplandor |
| Cyber (HUD layout, effect) | Ciber |
| heading, course / altitude / ground speed | rumbo / altitud / velocidad respecto al suelo (short: VEL. SUELO) |
| HDG / BRG / CRS / FOV / DEST / SPD / RANGE | HDG / BRG / CRS / FOV / DEST / SPD / RANGE (unchanged) |
| PREV / NEXT | ANT. / SIG. |
| Previous / Next (button text) | Anterior / Siguiente |
| L / R (relative bearing) | I / D |
| SIG / NEWS / LOCAL (briefing tabs) | SIG / NOTICIAS / LOCAL |
| PITCH / HGT (CCTV calibration chips) | INCL. / ALT. |
| NOT AN ALL-CLEAR | NO IMPLICA TODO DESPEJADO |
| click / click to … | haz clic / clic para … |
+65
View File
@@ -0,0 +1,65 @@
# Russian (`ru`)
Catalog: `src/i18n/locales/ru.json`. General rules: [TRANSLATORS.md](../TRANSLATORS.md).
## Style
- Address the user with polite imperatives: «Нажмите», «Включите», «Попробуйте снова».
- Sentence case, not Title Case: "Live Flights" → «Рейсы в реальном времени».
- Quotes inside Russian text are «ёлочки». Write ё where it belongs: её, ещё, всё.
- In short labels, a standard abbreviation with a period beats a wrapped line: НЕОПРЕД., СЛЕД.
- Units: км, м, км/ч, м/с, уз (knots), фт (feet), ч, мин, с; in UPPERCASE text КМ, М, КМ/С, УЗ, ФТ. MW is МВт in any case. `°` and `%` stay.
## Plurals
A `tn()` entry needs four forms. `Intl.PluralRules('ru')` picks:
| Form | Counts | Example |
| ------- | ------------------------ | ---------- |
| `one` | 1, 21, 31, 101… | 1 камера |
| `few` | 2–4, 22–24, 32–34… | 3 камеры |
| `many` | 0, 5–20, 25–30, 111–114… | 5 камер |
| `other` | fractions: 1.5, 2.25… | 1,5 камеры |
When a word next to a number cannot agree in every form, move the number after a colon instead: «доступно {bikes}», «ответов: {responses}». The same trick keeps place names and tool names out of a preposition's case: «откуда: {origin}», «Активно: {name}».
## Glossary
| English | Русский |
| ------------------------------------------- | ------------------------------------------------------------------ |
| layer / data layer | слой / слой данных |
| feed | поток данных (short: поток) |
| source / data source | источник / источник данных |
| LIVE (status) / live data | ОНЛАЙН / в реальном времени |
| SIMULATED | СИМУЛЯЦИЯ |
| stale / degraded / unavailable | устарел / с перебоями / недоступен |
| retry | повтор |
| key (API key) / POWER UP | ключ / ЗАПУСК |
| Provider Settings | Настройки провайдеров |
| globe / map / map source | глобус / карта / источник карты |
| aircraft / flight / military flights | самолёт (short HUD: борт) / рейс / военные рейсы |
| vessel, ship / vessels | судно / суда |
| satellite | спутник |
| launch (rocket) / space mission | пуск / космическая миссия |
| contact / tracking | контакт / отслеживание |
| installation, site | объект |
| camera / CCTV camera | камера / камера CCTV |
| earthquake / active fire | землетрясение / активный пожар |
| scene / shot / scene recipe | сцена / кадр / рецепт сцены |
| briefing / panel / dock | брифинг / панель / док |
| preset / visual preset | пресет / визуальный пресет |
| Scope (circular viewport mask) | прицел |
| heading / altitude / ground speed | курс / высота / путевая скорость |
| HDG / BRG / CRS / FOV / DEST / SPD / RANGE | КУРС / ПЕЛ. / ПУТЬ / ОБЗОР / НАЗН. / СКОР / ДАЛЬН. |
| advisory (NHC/CPHC cyclone) | сводка (не «бюллетень»); Advisory center — центр циклона по сводке |
| Street View (Google) | Просмотр улиц (как в русских Google Картах) |
| CONUS (NOAA region code) | CONUS, без перевода |
| WHOT/BHOT, Ironbow (thermal modes, palette) | без перевода |
| PREV / NEXT | ПРЕД. / СЛЕД. |
| Previous / Next (button text) | Назад / Далее |
| anchor / data pack / bundle (scene) | якорь / пакет данных / архив |
| basemap / box (Recent Imagery area) | подложка / область |
| camera move | перелёт |
| SIG / NEWS / LOCAL (briefing tabs) | СИГН. / НОВОСТИ / МЕСТН. |
| compass points (N, NNE, …) | С, ССВ, … |
| L / R (relative bearing) | Л / П |