mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 10:53:59 +00:00
refactor(voice): isolate gemini live provider core
This commit is contained in:
commit
10916227cb
655 files changed
+45946
-26483
No files matched your search
+219
-142
@@ -1,97 +1,19 @@
|
||||
# God's Eye View Current State
|
||||
|
||||
Authoritative typed input and OpenAI speech longer than 500 characters after
|
||||
policy normalization are unverified: tool dispatch asks for a shorter command.
|
||||
Later authoritative input can cancel cooperative preparation and the same input's
|
||||
deferred Radio playback; completed layer changes or station selection remain
|
||||
reported truthfully. This limit refusal does not infer a provider request or
|
||||
change providers.
|
||||
|
||||
OpenAI admits each nonempty provider speech-input ID once per owned connection
|
||||
lifetime, before input, tool, narration or response-queue effects. Stop/reset
|
||||
clears that exact admission history; storage grows with admitted inputs and ID
|
||||
bytes. A newer local input or Stop from an observer prevents the older onset
|
||||
from overwriting it. An admitted onset superseded before becoming current is
|
||||
remembered only as retired for its later real commit: it grants no authority
|
||||
and reserves no response early. Existing input-record and response-queue limits
|
||||
are unchanged.
|
||||
|
||||
OpenAI explicitly requested typed and tool-follow-up responses carry a client
|
||||
request ID in response metadata. Their returned IDs select only that original
|
||||
request; untagged automatic responses use speech-commit slots and cannot borrow
|
||||
typed authority. Unknown or malformed client IDs fail stale. Deferred requests
|
||||
retain their input identity, and Stop or newer input invalidates old captures.
|
||||
A tool follow-up retains the requesting response's record rather than borrowing
|
||||
another current input. Gemini keeps its separate existing response binding.
|
||||
OpenAI response-creation IDs are also admitted once per owned lifetime, before
|
||||
binding, narration, metrics or response bookkeeping. Other distinct events for
|
||||
the response continue normally. This exact history grows with response count
|
||||
and ID bytes and is cleared on Stop/reset. Rejection cleanup completes before
|
||||
callbacks that may submit newer input, preserving that input without retrying
|
||||
the rejected request.
|
||||
|
||||
Voice retains authentic user-turn identity when OpenAI input transcription is
|
||||
explicitly disabled or fails. Gemini native tool calls use current socket,
|
||||
session, local-turn and call ownership; unordered input captions are advisory
|
||||
and cannot grant or revoke tool authority. First-party typed input remains
|
||||
authoritative through deferred delivery and same-turn continuation until new
|
||||
input or Stop supersedes it. An accepted fresh `set_voice_provider` call refuses
|
||||
tools that have not started in that local turn without undoing completed work.
|
||||
Provider selection remains manual. Built-in embed instructions default to
|
||||
host-neutral guidance; standalone opts into its chooser. Hosts with fewer than
|
||||
two distinct usable provider options hide the chooser and retain neutral guidance.
|
||||
Failed tool output retains bounded, redacted reason data with static follow-up
|
||||
instructions. Gemini boundary waits use its injected clock.
|
||||
Native Gemini call IDs must be nonempty protocol strings of at most 1,024
|
||||
characters, without control characters. Their exact identities stay retired for
|
||||
the socket lifetime. At the 256-identity budget, another new identity ends the
|
||||
session with a visible restart instruction before any batch action is queued;
|
||||
old identities are never evicted to admit replays.
|
||||
|
||||
Gemini sentence-level voice activity does not by itself prove a reply is being
|
||||
generated. Local activity bookkeeping keeps an expiring session open for a
|
||||
possible reply; it does not identify server generations or authorize tools.
|
||||
An older retired completion preserves newer submitted input. A native terminal
|
||||
completion also settles a silent user turn, while tools and queued audio still
|
||||
delay presentation completion. Noise-only activity can wait until the native
|
||||
expiry deadline with the microphone muted; no shorter recovery timer is added.
|
||||
See [voice ownership](VOICE-OWNERSHIP.md) for deferred-input and protocol limits.
|
||||
|
||||
Gemini push-to-talk suppresses empty and near-silent captures before submitting
|
||||
context, activity markers or audio. Sustained input energy admits bounded
|
||||
pre-roll and the rest of that hold; release still flushes its tail. Physical
|
||||
Space holds silence local playback and revoke pending Radio handoff. Only
|
||||
admitted speech retires the current provider/tool turn; a silent hold does not
|
||||
create a provider reply or a native-boundary wait. This is an energy floor, not speech recognition or a
|
||||
guarantee against loud background noise. Open-mic behavior is unchanged.
|
||||
On narrow or short screens, transient toast notices are positioned above the
|
||||
bottom voice controls rather than covering the provider selector.
|
||||
|
||||
Gemini retires old replies before forwarding deferred input. Output refreshes
|
||||
the five-second retirement inactivity bound; deferred-input deadlines remain
|
||||
absolute. New input, Stop and provider replacement revoke older work. Existing
|
||||
map-stack work lacks caller cancellation. Pending annotation lookups and deferred
|
||||
outlines now share the voice call's cancellation signal; committed marks and
|
||||
unrelated annotation work survive. Stronger Gemini Radio controls start without
|
||||
waiting for an older pending selection; successful controls cancel covered work
|
||||
without treating siblings as a new turn or erasing committed selection. “Turn
|
||||
Radio on” enables its layer without starting audio; playback requires an explicit
|
||||
play, listen or resume request. Radio playback suspends active voice; pausing or
|
||||
disabling Radio resumes that same session preference only when Radio interrupted
|
||||
it. Manual playback follows the same rule, without a playback-duration limit.
|
||||
Provider replacement and explicit voice Stop cancel automatic resume; manual
|
||||
voice startup pauses Radio. A toast explains when Radio playback stops voice.
|
||||
A previously inactive voice session stays off when Radio stops. Local regression checks
|
||||
do not establish provider startup or browser/audio acceptance.
|
||||
|
||||
OpenAI ends an owned session after more than 16 unmatched response reservations
|
||||
with a visible restart-required error. It preserves ordered correlation and
|
||||
requires explicit restart rather than dropping old provenance or reconnecting.
|
||||
OpenAI provider text arriving after a synchronous action committed cannot
|
||||
retroactively prevent that change; later tools remain blocked.
|
||||
Provider classification recognizes sentence punctuation at finalized segment
|
||||
boundaries while retaining quoted, negated, compound and non-voice-provider
|
||||
exclusions.
|
||||
Shared distance checks use `src/geoDistance.js` with `{ lat, lon }` points and
|
||||
the IUGG mean spherical Earth radius, 6371.0088 km. `greatCircleKm` is
|
||||
canonical; `greatCircleMeters` is its exact metre conversion. Missing,
|
||||
non-finite, and out-of-range points (latitude outside ±90, longitude outside
|
||||
±180) return `Infinity`, so threshold and nearest-item consumers exclude invalid
|
||||
positions. Older exported `haversineKm` entry points remain as compatibility
|
||||
wrappers only. `src/data/approxDistance.js`, the equirectangular
|
||||
viewer-proximity approximation, is deliberately separate, as are intentional
|
||||
local angular, projection, ellipsoidal prefilter, and small-viewport
|
||||
approximations. Four great-circle copies remain for a follow-up:
|
||||
`src/data/adminBoundaries.js` and `src/tools/area.js` pass unwrapped longitudes
|
||||
across the antimeridian, so they cannot use the strict ±180 check as-is, and
|
||||
`src/sources/adsbRecords.js` and `server/providers/common/geo.js` keep their own
|
||||
implementations.
|
||||
|
||||
Vessel sources can report healthy empty coverage using a current, complete
|
||||
snapshot with zero raw rows, a healthy transport and a positive last-message
|
||||
@@ -190,14 +112,36 @@ perimeters, terrain height, military installations and map features, plus a
|
||||
combined situation brief, military awareness around a point, the app's heads-up display caption, and a link that
|
||||
opens the app over an area. Tools reuse the layers' portable source factories, take a
|
||||
shared `area` argument (place name, bounding box, or point and radius) and cap
|
||||
lists at 25 rows by default. Voice offers the same queries next to its app
|
||||
lists at 25 rows by default. `aircraft_in_area` and `vessels_in_area` can also
|
||||
count every match by a field (`group_by`: airline, read from the ICAO
|
||||
designator in the callsign, or operator, country or type for aircraft; type or
|
||||
destination for ships; the ten largest groups, then other and unknown counts;
|
||||
the summary says when a feed reports none of the field) and summarize airborne
|
||||
altitude and speed (`stats`: count, min, median, mean, max).
|
||||
Both cover all matches, not just the rows returned; voice leaves these two
|
||||
tools out, so they reach MCP clients only. Voice offers the same queries next to its app
|
||||
actions: the session lists them, and the browser runs them through the same
|
||||
catalog, loaded on first use. See [tools and the MCP server](TOOLS.md).
|
||||
Aircraft altitude-band queries require a reported barometric or geometric altitude
|
||||
for each match, so a grounded contact that reports none is left out of a band too.
|
||||
Queries without an altitude band retain unknown-altitude contacts.
|
||||
|
||||
Catalog calls check their cancellation signal immediately before executing a
|
||||
tool. Already-cancelled queries do not start; cancellation after execution starts
|
||||
continues to follow the existing tool and caller behavior.
|
||||
|
||||
Fire-perimeter queries test polygon interiors and holes on continuous unwrapped
|
||||
longitudes, matching the short-edge convention used for boundary intersections.
|
||||
An outline crossing ±180° contains nearby boxes and radius areas without also
|
||||
containing their counterparts near Greenwich. Returned perimeter coordinates
|
||||
use a circular mean for longitude, including multipart outlines. Incident cards
|
||||
also use a circular longitude mean over the largest outer ring (excluding its
|
||||
repeated closing vertex). Their relative-area comparison follows short longitude
|
||||
edges so a small dateline-crossing part cannot outweigh a larger ordinary part.
|
||||
Regression coverage lives in `src/tools/area.test.mjs`,
|
||||
`src/tools/queries/environment.test.mjs` and
|
||||
`src/layers/perimeters/cards.test.mjs`.
|
||||
|
||||
## Cyber HUD — September 30, 2026
|
||||
|
||||
Display > HUD > Layout includes Cyber, also available through the HUD voice
|
||||
@@ -462,7 +406,12 @@ doubling to a 15 s cap, with every retry waiting behind one shared cooldown
|
||||
plus up to 1.5 s of jitter, and `Retry-After` extending the cooldown when the
|
||||
upstream exposes it. One console warning is logged per cooldown window. Other
|
||||
failures keep Cesium's handling and the flat `EllipsoidTerrainProvider`
|
||||
fallback. `scripts/qa-terrain-429.mjs` reproduces the burst against a keyless
|
||||
fallback; while that flat terrain is on a shown globe the map controller
|
||||
retries Re:Earth in the background (30 s, doubling to a 10-minute cap, each
|
||||
attempt abandoned after 60 s) and installs the real terrain without waiting
|
||||
for a map switch or an imagery retry. Behind a 3D tileset it neither retries
|
||||
nor installs; the next globe activation does.
|
||||
`scripts/qa-terrain-429.mjs` reproduces the burst against a keyless
|
||||
dev server, counts real throttles with their headers as evidence, synthesises
|
||||
throttles with `--inject N` when the upstream is not throttling, and fails if
|
||||
a throttled tile never loads.
|
||||
@@ -520,6 +469,8 @@ reference. Legacy projects retain their ordinary flights, content and edits.
|
||||
Seeking between ordinary camera poses uses the shortest longitude arc across the
|
||||
dateline while preserving both authored endpoint longitudes, and turns heading
|
||||
and roll the short way for any authored angles.
|
||||
A seek to a shot's start, as a shot action makes, selects that shot rather than
|
||||
the end of the previous one.
|
||||
See [camera directions](DIRECTOR-CAMERA.md).
|
||||
|
||||
Realtime voice composes separate connection, response/tool, Radio handoff, input
|
||||
@@ -545,6 +496,11 @@ to layer toggles, fly_to_location, frame_overhead, select_nearest_aircraft,
|
||||
get_entity_context, annotate_map and analyst_query. Analyst speech and the card
|
||||
share one deterministic count/scope headline; lower bounds use "At least" and
|
||||
partial answers name the unanswered layers. `get_current_view_state` lists enabled layers only.
|
||||
The model-free `gods-eye-view/agent/world-packet` export exposes
|
||||
`getWorldPacket({ viewer, styleManager, dataManager, sceneDirector })` to
|
||||
non-voice consumers. It reads the same enabled-only state and feed
|
||||
provenance before voice adds speech/card fields, with no new network requests
|
||||
or mutations.
|
||||
The voice card nested in the mic control (`voiceCard.js`) shows captions, plan
|
||||
steps and the result. `resultDisplay.js` is the one adapter from a result to
|
||||
the card: builder results show as built, and analyst results show a title
|
||||
@@ -618,7 +574,7 @@ are protected from fallback writes. Version 6 is the export format, with
|
||||
zero pitch, low camera heights, scope and detection edits preserved. See [the document contract](SCENE-DOCUMENT.md);
|
||||
see [Director](DIRECTOR.md) for camera, pack, action and sharing support.
|
||||
|
||||
Search framing and annotations request semantic map features from an explicit source. The Overpass adapter owns bounded queries, member/tag decoding and request deadlines; callers retain candidate ranking, outline caching, deferred retries and scene placement. Empty, unavailable, transient and throttled outcomes remain distinct. Traffic sources return road records and installation sources return mapped records with freshness/saturation metadata, so their layers no longer decode upstream elements. ALPR already normalizes its records in the source. Traffic uses OpenFreeMap roads with optional matched TomTom congestion; ALPR uses an hourly OpenStreetMap extract. Keyless installations use military-area tiles. Detailed feature queries require operator-configured Overpass.
|
||||
Search framing and annotations request semantic map features from an explicit source. The Overpass adapter owns bounded queries, member/tag decoding and request deadlines; callers retain candidate ranking, outline caching, deferred retries and scene placement. Empty, unavailable, transient and throttled outcomes remain distinct; a data reply is refused only for its `remark`, never for text in element tags. Traffic sources return road records and installation sources return mapped records with freshness/saturation metadata, so their layers no longer decode upstream elements. ALPR already normalizes its records in the source. Traffic uses OpenFreeMap roads with optional matched TomTom congestion; ALPR uses an hourly OpenStreetMap extract. Keyless installations use military-area tiles. Detailed feature queries require operator-configured Overpass.
|
||||
|
||||
Nepal media preloads survive repeated camera-flight updates, but Stop, event disable and replacement remove abandoned frames and revoke pending Facebook sessions. Fallback evidence-card clicks respect the shared drawing-tool pointer lease.
|
||||
|
||||
@@ -662,7 +618,9 @@ Reference feed construction is exported through `sources/reference`; the cable s
|
||||
|
||||
Source factories have dedicated `layers/<family>/source` exports. ALPR and earthquake record normalization and CCTV source policy no longer pull rendering into source consumers. Catalog construction and voice feed reads use their focused owners; compatibility entries remain available. Settings filesystem hardening lives under `server/standalone/`. Import-direction checks complement export ownership checks; source behavior, settings policy and rendering are unchanged.
|
||||
|
||||
Voice controls bind to a protocol-independent session factory. The default WebRTC adapter preserves the existing Realtime connection, push-to-talk, cost controls and radio handoff. Session subscriptions expose state, transcript, action call/progress/result, interruption, completion and turn-metrics events; stopping or replacing a session cancels pending actions. Alternate adapters can use the same controls and action runner.
|
||||
Voice controls bind to a protocol-independent session factory. The default WebRTC adapter preserves the existing Realtime connection, push-to-talk, cost controls and radio handoff. Session subscriptions expose state, transcript, action call/progress/result, interruption, completion and turn-metrics events; stopping or replacing a session cancels pending actions. An active session's normal Stop publishes one idle transition, including adapters that emit their own terminal state; a restart triggered by that transition keeps ownership of its new startup state. Alternate adapters can use the same controls and action runner.
|
||||
|
||||
The standalone app offers OpenAI and Gemini Live through an explicit provider chooser. Gemini keeps its permanent API key on the local server and mints a short-lived, single-use credential with server-owned instructions and the standalone action catalog. Its native WebSocket adapter supports open microphone and push-to-talk input, streaming audio, captions and the existing action runner. Switching providers ends the old conversation and releases its microphone and connection; only the input mode carries over. Gemini uses bounded shutdown draining and explicit manual restart, without automatic reconnection or a promise of uninterrupted session duration. Hosts that do not opt into the provider facade retain their existing default or custom adapter.
|
||||
|
||||
Voice action argument schemas have one portable owner under `src/voice/actionSchemas.js`. The Realtime provider builds 30 tools using separate description-only metadata. Description customization cannot replace argument types, enum values or required fields.
|
||||
|
||||
@@ -677,7 +635,9 @@ pinning, first-position grace, source time and sea-surface placement are unchang
|
||||
|
||||
Military flights also separate portable records and source acquisition from
|
||||
Cesium snapshot application. The metadata owner retains aviation feet, source
|
||||
fix times, sticky fields and partial-feed retention. Rendering retains ground
|
||||
fix times, sticky fields and partial-feed retention. A row without track or
|
||||
speed holds the last known value, as civil records do; a grounded row without
|
||||
speed reads 0. Rendering retains ground
|
||||
model ownership, motion history, military registration and tracking cleanup.
|
||||
|
||||
Civil flights separate source acquisition, portable record reconciliation and
|
||||
@@ -860,8 +820,17 @@ and `./layers/launches`. Each owns its catalog, render state, tracking, interact
|
||||
and teardown. Source adapters retain the existing CelesTrak and Launch Library
|
||||
endpoints; standalone entries provide scene services and the satellite dependency.
|
||||
The mission factory separates paths, replay, camera, panel, overlays and placement.
|
||||
Launch Library pad coordinates retain unknown values: missing fields use the
|
||||
reported location coordinates when available, and launches without a known pad
|
||||
position stay off the map. Genuine zero coordinates remain valid. Undisclosed
|
||||
stage coordinates, downrange distance and flight numbers remain unknown; the
|
||||
mission panel omits the flight-number label when no number was reported.
|
||||
Pending mission requests abort on disable/destruction and malformed launch
|
||||
snapshots preserve the last accepted display. Catalog groups, propagation cadence,
|
||||
snapshots preserve the last accepted display. A Launch Library stale-cache fallback
|
||||
retains its missions but reports STALE in Data Layers; a fresh snapshot clears that
|
||||
provenance. Hiding retained missions does not relabel them as fresh, and teardown
|
||||
clears the instance state. Injected getLaunches-only sources remain supported.
|
||||
Catalog groups, propagation cadence,
|
||||
tracking intent, replay timing, controls and source attribution remain unchanged.
|
||||
|
||||
The fire layer now exposes `./layers/firms`: an instance factory with separate
|
||||
@@ -935,6 +904,13 @@ use the supplied asset resolver, including the preload path. Enrichment receives
|
||||
an abort signal and cannot update a later lifecycle after destruction. Existing
|
||||
camera, terrain floor, trail, selection and measured model-size policies remain.
|
||||
|
||||
The tracked-aircraft descriptor both flight layers hand to the cockpit carries
|
||||
`verticalRateMps` beside `velocityMps` and `track`. Civil records store the rate as
|
||||
`verticalRate` and military records as `verticalRateMps`; the query seam maps both to
|
||||
one name, so `resolveTrackedAircraftInfo` returns a single shape for either layer. The
|
||||
rate is reported, never derived: a blank poll keeps the last transmitted value and an
|
||||
absent rate stays null rather than reading as level flight.
|
||||
|
||||
## Transponder emergency highlighting
|
||||
|
||||
`src/data/aircraftEmergency.js` derives one status per poll from the Mode A
|
||||
@@ -1234,6 +1210,10 @@ Automatic collapse remains presentation only, prioritizes the latest explicit
|
||||
panel, and honors keyboard focus on the right. Mobile layout still releases
|
||||
desktop allocation styles. Stable Display allocation avoids unnecessary style
|
||||
writes. This extraction does not change panel positions or layout defaults.
|
||||
Context expansion through the panel owner or adaptive right-rail restoration
|
||||
closes an open compact Radio disclosure before Context/Space Missions becomes
|
||||
visible. The mini remains available while Context is collapsed. This fixes the
|
||||
Radio-overlap path reported in #804 without changing z-index or layout policy.
|
||||
The right rail skips panels hidden by other CSS (computed `display: none`), but
|
||||
keeps the collapsed panels its own exclusive presentation hides, so automatic
|
||||
collapse can still be undone.
|
||||
@@ -1312,7 +1292,13 @@ String arrays regardless of the reported feed version. Nonempty English text
|
||||
(including regional `en-*` tags, case-insensitive) takes precedence, then the first
|
||||
usable translation. An unusable `name` falls back to `short_name` with the same
|
||||
rules; if neither supplies text, the station remains available with an empty name
|
||||
(the globe uses its station-ID label and the tool returns `null`). Malformed
|
||||
(the globe uses its station-ID label and the tool returns `null`). A run of underscores,
|
||||
with the spaces around it, reads as one word break and the ends are trimmed:
|
||||
JCDecaux's Spanish contracts publish `_PLAZA_MUSICO_LOPEZ_CHAVARRI`. Other spacing
|
||||
stays as published. Capacity counts docks: PBSC adds the bikes a station accepts
|
||||
in its geofenced area to `capacity` and reports that part as `geofenced_capacity`
|
||||
(Bilbao Bizi: 839 = 39 docks + 800), so the layer, its colour ratio and
|
||||
`get_bike_share` read `capacity − geofenced_capacity` when the extension is present. Malformed
|
||||
translation entries do not interrupt loading of the other stations. This covers
|
||||
station names, not complete GBFS v3 support.
|
||||
`scripts/qa-gbfs-names.mjs` checks synthetic localized names through the real layer,
|
||||
@@ -1437,6 +1423,27 @@ outside its promise, so an unusual mirror status cannot crash the server.
|
||||
Node-only package entries expose both provider factories. Routes, payloads,
|
||||
fallback behavior and existing dev/preview hook registration are preserved;
|
||||
browser rendering is unchanged.
|
||||
The portable frame source retains the proxy's known `X-CCTV-Source` kind. Camera
|
||||
snapshot tools label Street View fallback imagery as historical and credit Google,
|
||||
while upstream frames retain their current-view label and catalog attribution.
|
||||
Synthetic placeholders are unavailable as snapshots. Injected frame sources may
|
||||
supply `sourceKind`; missing or unknown provenance keeps `current_view` null and
|
||||
does not claim the image is current. The `current_view` field distinguishes camera-feed
|
||||
provenance from historical fallback imagery; it does not measure capture time or
|
||||
image freshness.
|
||||
|
||||
## CCTV missing numeric fields
|
||||
|
||||
CCTV numbers are read as missing when they are absent, null, blank or boolean,
|
||||
on the server (`toFiniteNumber`, every pack and the file/env source packs) and
|
||||
in the browser (`safeNumber`). `/api/cctv/sources` sends an unknown number as
|
||||
JSON null; the browser then uses the bundled seed camera's value when one
|
||||
shares the id, else the field's default — id-hash heading, pitch −17°, FOV 74°
|
||||
(which a matched datasheet can still replace), range 700 m, mount 24 m, the
|
||||
city's ground elevation — rather than reading zero and clamping it to the
|
||||
field's floor. A camera with a missing latitude or longitude and no seed
|
||||
position is skipped by the browser catalog instead of being placed at 0,0.
|
||||
Real zeros, including `"0"`, are kept.
|
||||
|
||||
## Terrain, traffic, fire and bike-share provider modules
|
||||
|
||||
@@ -1446,13 +1453,28 @@ plugin order, server-key selection, validation, disk caches, budgets, retries
|
||||
and stale/error responses remain unchanged. Each has a Node-only package entry
|
||||
under `gods-eye-view/server/providers/`. Portable terrain mechanics, traffic tile
|
||||
math and GBFS source rules are available under `gods-eye-view/sources/`.
|
||||
The browser layers and their rendering remain in their existing modules.
|
||||
The TomTom `/api/tomtom/*` routes run the shared same-site admission gate before
|
||||
status, cache or upstream work, refusing cross-site browser requests while
|
||||
preserving same-origin app requests and non-browser local callers. The browser
|
||||
layers and their rendering remain in their existing modules.
|
||||
GBFS null or empty availability counts retain an unknown value through station
|
||||
parsing and rendering; they use the neutral station color rather than reporting
|
||||
an empty dock. Explicit zero and caller-supplied numeric fallbacks remain valid.
|
||||
|
||||
## Terrain height cache bound
|
||||
|
||||
The `/api/terrain/heights` route refuses cross-site browser requests before
|
||||
rate limiting, cache access or upstream work: a `Sec-Fetch-Site` other than
|
||||
`same-origin`/`none` (an `<img>` on another page), or a foreign or opaque
|
||||
`Origin`, gets `403`. The per-client limiter keys on the socket address, which
|
||||
for such a request is the visitor's own browser, so without the gate any page
|
||||
could spend the visitor's 90/min quota and leave the app's own lookups on
|
||||
`429`. Unlike the cost-bearing routes, the terrain gate does not refuse
|
||||
reverse-proxy forwarding headers: terrain is keyless and needed for rendering,
|
||||
and a `403` there would send every chunk to the geoid fallback for an instance
|
||||
served behind the documented reverse proxy. Same-origin app requests,
|
||||
non-browser local callers and proxied same-origin requests keep working.
|
||||
|
||||
The terrain proxy parser rejects finite longitudes outside [-180, 180] and
|
||||
latitudes outside [-90, 90] for every client before upstream resolution. One
|
||||
out-of-range point rejects the whole batch; inclusive WGS84 bounds remain valid.
|
||||
@@ -1490,9 +1512,20 @@ The Node-only `gods-eye-view/server/providers/space` export supplies factories;
|
||||
`sources/space` supplies fixed upstream URL builders with no I/O or environment
|
||||
access. Callers retain validation, transport and response policy.
|
||||
|
||||
LL2 recent-launch requests use UTC minute-aligned bounds over the same 30-day
|
||||
window, so installations refreshing in the same minute share an upstream cache
|
||||
key. The upper bound trails the current time by less than one minute. Requests
|
||||
identify the app and repository through User-Agent; the existing 15-minute local
|
||||
cache, stale fallback and optional server-only Authorization remain unchanged.
|
||||
Provider Settings identifies LL2 tokens as optional paid higher-rate upgrades:
|
||||
standard public access needs no key, and its field can remain blank.
|
||||
|
||||
The shared TLE text parser skips incomplete named sets and resumes at the next
|
||||
complete set. A missing orbital line does not discard the later satellites in
|
||||
the catalog; satellite layers and next-pass tools use the same parser.
|
||||
the catalog. A set whose two lines carry different catalog numbers, or a
|
||||
malformed catalog number, is skipped the same way, so a spliced catalog cannot
|
||||
pair one satellite's elements with another's. Satellite layers and next-pass
|
||||
tools use the same parser.
|
||||
|
||||
## Build configuration and local provider boundaries
|
||||
|
||||
@@ -2673,6 +2706,17 @@ down | auth-failed` (plus the unchanged `missing-key`/`unsupported`) with
|
||||
> tunings. Ambient overlap includes each sprite's own rendered extent. The
|
||||
> gated AIS, CCTV, and satellite passes retain an active-emphasis count so a
|
||||
> settled dim contact always completes its release after tracking ends.
|
||||
> - **Terrain-height bounds:** because every distinct 5-decimal point is its own
|
||||
> cache key, its own line in `.gev-cache/terrain-heights.json`, and its own
|
||||
> upstream call, four limits bound what a caller can grow. Coordinates outside
|
||||
> WGS-84 range are rejected with `400` at parse (finite is not the same as on
|
||||
> Earth); the point cache holds at most 50,000 entries, evicting oldest-first
|
||||
> — terrain does not move, so an evicted point costs one refetch and never a
|
||||
> wrong answer; a cache file above 24 MB is not read back into memory (the
|
||||
> ceiling `rocketLaunchesProxy` already applies); and the route carries an
|
||||
> always-on limiter of 90 requests/minute/client, 270 global. That cap is an
|
||||
> order of magnitude above measured demand — a full `npm run test:track` run
|
||||
> drove 10 requests, peaking at 8/min.
|
||||
> - **Terrain-height resilience:** `/api/terrain/heights` caches canonical
|
||||
> 5-decimal points individually, reconstructs reordered/overlapping batches
|
||||
> in exact request order, and refreshes only missing or stale points. Upstream
|
||||
@@ -3294,7 +3338,11 @@ test:track` 43 tracking invariants · headless QA harnesses under
|
||||
> metres low. `qa-floor-verify.mjs` now **exits non-zero on FAIL** (1 = FAIL,
|
||||
> 2 = INCONCLUSIVE); it previously exited 0 on every verdict, which is how the
|
||||
> burial stayed invisible. It also honours `QA_BASE_URL` and puppeteer's
|
||||
> pinned Chrome-for-Testing, like the other harnesses.
|
||||
> pinned Chrome-for-Testing, like the other harnesses. The viewer-distance
|
||||
> gate, the corridor walk and cell hysteresis take the short way across 180°
|
||||
> (`src/data/approxDistance.js` is the one distance both the flights records
|
||||
> and the mesh-floor sampler use), so contacts just across the antimeridian
|
||||
> are floored like any other.
|
||||
> - **2026-07-16:** the FIRMS Active Fires layer is **LIVE** —
|
||||
> the bundled 2026-05-25 snapshot (58 MB) is deleted; a new `/api/firms` proxy
|
||||
> (vite.config.js) merges VIIRS NOAA-20/NOAA-21/Suomi-NPP NRT world CSVs
|
||||
@@ -3321,6 +3369,39 @@ test:track` 43 tracking invariants · headless QA harnesses under
|
||||
> attribution 18/18). New modules: `src/data/{motionModel,aircraftMeta,aircraftClass,aircraftIcons,issPass,routePlausible,dataCredits}.js`.
|
||||
> The live runtime now declares 30 voice tools; the 17→20 count above is retained only as milestone history.
|
||||
|
||||
### Interface localization (October 2026)
|
||||
|
||||
Shipped locales: **en** (source and fallback), **es** and **ru**. The code lives in
|
||||
`src/i18n/`. How to mark UI text: [`docs/I18N.md`](./I18N.md); translator workflow:
|
||||
[`docs/TRANSLATORS.md`](./TRANSLATORS.md).
|
||||
|
||||
- **Keys are the English text.** `t('Save keys')`, `tn(count, '{count} camera',
|
||||
'{count} cameras')`, and `msg('Live Flights')` for text stored in data and
|
||||
translated later. Markup opts in with `data-i18n` (element text) and
|
||||
`data-i18n-title` / `-aria-label` / `-placeholder` (that attribute's English
|
||||
value). English has no catalog; every other locale is
|
||||
`src/i18n/locales/<code>.json`, and a missing entry falls back to English.
|
||||
- **Locale choice.** `?lang=` → stored choice (`gev:locale:v1`) →
|
||||
`navigator.languages` → English. `?lang=` is not stored, and share links drop
|
||||
it, so a recipient keeps their own language. The globe picker in the
|
||||
bottom-right corner (`src/ui/localePicker.js`) stores the choice, drops
|
||||
`?lang=` (keeps it only when storage is blocked) and reloads. It asks first
|
||||
when the reload would lose work registered with `registerUnsavedWork`:
|
||||
unsaved keys or CCTV calibration, Draw marks, imported map files, the open
|
||||
cockpit, a voice session.
|
||||
- **Gate.** `npm run i18n` (and `src/i18n/catalog.test.mjs` under `npm test`)
|
||||
extracts every message and fails on missing, empty, unused,
|
||||
placeholder-mismatched, still-English (plural forms included) or
|
||||
wrongly-shaped plural entries.
|
||||
- **Not translated by design:** layer ids, share-link and hash parameters, API
|
||||
fields, voice tool schemas, provider names, feed data (callsigns, vessel and
|
||||
station names) and attribution.
|
||||
- **Unmarked text.** The same check lists UI text that is not marked at all
|
||||
(markup without `data-i18n*`, literals reaching the page without `t`/`tn`/`msg`)
|
||||
and fails on it. Text kept on purpose says why: `translate="no"`,
|
||||
`data-i18n-runtime`, or `// i18n-ignore: <reason>`. `src/tools/` is skipped:
|
||||
it writes for AI clients, not the page.
|
||||
|
||||
## Canonical Docs Order
|
||||
|
||||
Use docs in this order when details conflict:
|
||||
@@ -3367,10 +3448,10 @@ its criteria cannot be silently ignored.
|
||||
| Satellites | CelesTrak | `src/data/satellites.js` | `/api/celestrak` | 120s |
|
||||
| Space Missions (30d) | Launch Library 2 + CelesTrak | `src/data/rocketLaunches.js` | `/api/launches` + `/api/celestrak/active` | 5 min |
|
||||
| Traffic | Selectable TomTom / OpenStreetMap / Hybrid roads; optional TomTom flow (BYOK) | `src/data/traffic.js` | browser-direct OpenFreeMap tiles; `/api/tomtom` for flow | viewport-driven; capped tile cache |
|
||||
| CCTV | Austin + Caltrans (CA) + TfL London + Ontario 511 + Fintraffic (FI) + DriveBC (BC) + TxDOT (TX) + Estonia (Tallinn, Tarktee) + Live Traffic NSW + Open Calgary Open Data + Statens vegvesen (NO, stills + live HLS) + King County (WA) Open Data + WSDOT (WA) Open Data + NYC DOT TMC (opt-in) + City of Toronto RESCU + Catalonia (Servei Català de Trànsit) + Street View fallback | `src/data/cctv.js` | `/api/cctv` | 10s (active) |
|
||||
| CCTV | Austin + Caltrans (CA) + TfL London + Ontario 511 + Fintraffic (FI) + DriveBC (BC) + TxDOT (TX) + Estonia (Tallinn, Tarktee) + Live Traffic NSW + Open Calgary Open Data + Statens vegvesen (NO, stills + live HLS) + King County (WA) Open Data + WSDOT (WA) Open Data + NYC DOT TMC (opt-in) + City of Toronto RESCU + Catalonia (Servei Català de Trànsit) + FL511 (FL, opt-in) + Street View fallback | `src/data/cctv.js` | `/api/cctv` | 10s (active) |
|
||||
| Radio | Radio Browser (public-domain station directory) | `src/data/radio.js` | `/api/radio/stations`, `/api/radio/click/:uuid` | 45 min directory refresh |
|
||||
| Transit 🚌 | Operator GTFS-Realtime VehiclePositions (7 keyless regions, `src/data/transitFeeds.js`) | `src/layers/transit/` via `src/app/layers/transit.js` | `/api/transit` | 15s (poll + delayed playback) |
|
||||
| Bikeshare 🚲 | GBFS (Lyft + BCycle + Social Bicycles + PBSC incl. Bike Share Toronto, Bike Itaú (Brazil) and Bicing (Barcelona) + Urban Sharing, Entur, JCDecaux NO + nextbike (AMBici)) | `src/data/bikeshare.js` | `/api/gbfs` | 60s |
|
||||
| Bikeshare 🚲 | GBFS (Lyft + BCycle + Social Bicycles + PBSC incl. Bike Share Toronto, Bike Itaú (Brazil), Bicing (Barcelona) and seven other Spanish cities + Urban Sharing, Entur, JCDecaux (Lillestrøm, Valencia, Seville) + nextbike (AMBici)) | `src/data/bikeshare.js` | `/api/gbfs` | 60s |
|
||||
| Directions 🧭 | OSRM on FOSSGIS servers (OpenStreetMap) | `src/data/directions.js` | `/api/route` (`steps=1`) | on placement / mode change |
|
||||
| Datacenters ▣ | OSM extract (bundled) | `src/data/localLayers.js` | — | static |
|
||||
| Dams ▰ | OpenInfraMap/OSM extract (bundled) | `src/data/localLayers.js` | — | static |
|
||||
@@ -3518,7 +3599,10 @@ controls (the address goes in the caption); a pose goes out at once and
|
||||
again when the panorama's month, photographer and address arrive (one
|
||||
successful lookup per panorama per session, seeded by the nearest lookup;
|
||||
a details lookup that fails or takes over ten seconds waits five seconds
|
||||
before a view event can retry, and an unmount drops lookups in flight). A key
|
||||
before a view event can retry, and an unmount drops lookups in flight). Opening
|
||||
another panorama settles the previous open before registering its wait; closing
|
||||
then releases the current wait immediately. A details lookup released while its
|
||||
service loads makes no panorama request after that service becomes available. A key
|
||||
Google refuses (`gm_authFailure`: the API not enabled, a referrer it does
|
||||
not allow, billing off) reads KEY REJECTED and fails a pending open at
|
||||
once; a lookup or open Google never answers fails after 10 or 20 s. In CI
|
||||
@@ -4286,7 +4370,7 @@ silently demoting every later lookup for the session.
|
||||
- The internal Context coordinator is available in every visual style. Its dedicated right-side `CONTEXT` chooser exposes the neutral shell; the coordinator is not duplicated in Data Layers and does not enable a live-data dependency until a mode is selected.
|
||||
- The expanded `CONTEXT` view offers mutually exclusive `CONTACTS` and `SPACE MISSIONS` modes. Selecting `CONTACTS` enables the context-owned Flights, Military Flights, AIS Vessels, and Mapped Installations dependencies only when they are not already user-enabled; selecting `SPACE MISSIONS` enables the recent-launch layer and its Satellite dependency. `CONTACTS` cycles the nearest supported contact of whatever type is selected. Satellites are deliberately excluded from those Awareness cohorts and keep their own tracking UX. Selecting the active mode again returns to the neutral chooser and releases only mode-owned dependencies.
|
||||
- If a civilian or military aircraft is already tracked when `CONTACTS` becomes operational, that source-owned track is adopted as the Context subject before nearest-contact autofocus. Context rechecks the tracker after its dependencies settle, so a newer selection wins, while an explicit clear during activation prevents fallback from silently selecting a replacement. Cockpit entry remains unavailable until that Context transaction has settled, so its camera takeover cannot clear Cesium tracking before adoption. Adoption does not recreate tracking or transfer camera ownership; it initializes the normal 250 km ring, history, proximity results, and Cockpit Previous/Next state for the original aircraft.
|
||||
- `SEARCH NEARBY SITES` retains the bounded OSM results and makes one user-initiated, view-biased Google Maps Places text search for “military installation.” Google results are source-stamped, deduplicated against OSM by rounded location/name, and remain mapped context rather than operational claims. If Places is unavailable or the API is not enabled for the supplied key, OSM context remains available.
|
||||
- `SEARCH NEARBY SITES` retains the bounded OSM results and makes one user-initiated, view-biased Google Maps Places text search for “military installation.” Google results are source-stamped, deduplicated against OSM by rounded location/name, and remain mapped context rather than operational claims. If Places is unavailable or the API is not enabled for the supplied key, OSM context remains available. A view across the antimeridian is searched around its own centre, with a radius from its real width.
|
||||
- The expanded desktop header omits the redundant `ON` label; the active mode button carries state. Expanded Contact results also omit the duplicate `GLOBAL CONTEXT` / `CONTEXT ONLY` status row and begin with the selected subject and its 250 km scope. Global Context does not fabricate a selected-entity model preview: the provisional hand-authored aircraft wireframe was removed because it was not geometry extracted from the selected entity's actual asset.
|
||||
- Dependency ownership is reversible: disabling Global Context releases only dependencies it enabled, while user-enabled layers remain on. This also removes the Military-layer suppression handoff when Global Context owned Military, allowing an already-enabled civilian Flights layer to resume its normal mixed rendering. If OpenSky is unavailable and has no last-good cache, Flights requests a capped 250 nm adsb.lol point snapshot around the current view anchor and labels that provenance explicitly; it never relabels military-feed rows as civilian data. If both inputs fail, Flights remains `UNKNOWN`.
|
||||
- Space Missions is replay-isolated: Rocket Launches and Satellites are the only Data Layers permitted while the mode is active. Direct UI and voice entry capture the same pre-entry snapshot; internal dependency and restoration enables do not create a user-owned Context session. Entry waits for incompatible layers to shut down, direct incompatible enables are blocked before lifecycle work, and the entry gate remains active through the complete Rocket Launches enable. A newer same-target ON request takes ownership of the pending entry without releasing its isolation snapshot, including when it arrives while the prior request is awaiting the adoption guard. A caller abort, resource cancellation, newer OFF, or layer teardown waits for exact manager settlement and restores that snapshot without resurrecting Rocket Launches. If an abort lands after only part of a restore settles, Context completes the same exact target without the stale caller signal and then replays newer explicit layer intent. Dedicated Voice Context cancellation reports a stable cancelled result plus the current Context state; generic layer visibility additionally exposes manager phase, reason, successor, and lifecycle details. Once an exact voice visibility intent commits, a newer voice turn cannot relabel it as cancelled while Context settlement completes; pre-commit aborts remain cancellable and final lifecycle mismatches remain failures. Clear Selected Layers reserves its complete captured OFF set before sequential teardown; a newer absolute request of any origin supersedes only its layer reservation and remains authoritative. A rejected layer teardown retains the truthful enabled state, rolls already-stopped siblings back to the captured pre-entry set, and aborts replay; rapid exit/re-entry serializes the full Satellite enabled-state and parameter restore before a new snapshot is taken. Contacts remains additive and restores user-enabled layers normally.
|
||||
@@ -4317,6 +4401,7 @@ silently demoting every later lookup for the session.
|
||||
- **The DENSE chip reports the dense LOAD, not the catalog param.** The param flips synchronously while the Starlink shell takes seconds to arrive over a chunked load, and CelesTrak 502s that feed regularly. So the chip reads `DENSE ···` (busy, disabled) while loading, ACTIVE only once dense points are actually on screen, and `DENSE ✕` with the reason on hover when the load fails — a failure also reverts `catalog` to `core`, drops any partial chunk, and leaves the chip clickable to retry. A load is judged by points added, not by HTTP status: a 200 carrying an empty body, a passed-through HTML error page, or only TLEs the core catalog already owns fails with the same revert semantics as a 502. Any explicit request for `core` clears a latched error even when the mode does not change, so a Space Missions restore of an already-core snapshot never leaves the user with a failure they did not cause. Because the load settles asynchronously, the layer pushes a re-render through the optional `setRowControlsListener()` hook; nothing else would repaint that row before the 5-minute catalog refresh, so the count and legend would otherwise sit stale.
|
||||
- **A dependency owner takes the row with it.** Space Missions borrows this layer for TLE lookup with `showPoints:false`; while points are hidden the layer returns empty row controls, so the legend never describes an empty sky and the chip cannot accept a write that the owner's restore would silently revert.
|
||||
- The detection-overlay record cache (`_detectionObjects`) is cleared with the catalog on every rebuild: it stamps id/class at creation only, and a rebuild can re-tag a satellite when a partial CelesTrak outage changes which group wins dedupe.
|
||||
- **Catalog numbers above 99999** can be written in a TLE as Alpha-5: a letter A–Z, skipping I and O, for the first two digits, so `A0000` is 100000 and `Z9999` is 339999. CelesTrak's TLE feed carries no satellite above 99999 (that needs the OMM feed, #1191), so this is compatibility with Alpha-5 TLEs. `src/sources/tle.js` decodes the field through one shared helper: `noradIdFromSatnum()` for the satellites layer, which keys and deduplicates its catalog by that number, and `tleCatalogNumber()` for the satellite tools. Plain digits read as before (`noradIdFromSatnum()` accepts any length, as an OMM satrec carries the number unpadded); a malformed field — a sign, decimal point, hex, exponent, embedded space, stray or lowercase letter, or blank — is rejected rather than partly read, as `NaN` in the layer and `null` in the tools.
|
||||
- **CCTV estimated bearings:** a camera whose pack marks `headingConfidence: 'low'` (the id-hash
|
||||
`fallbackHeadingFromId` bearing) shows `HDG n° (ESTIMATED)` in the HUD and draws its coverage
|
||||
wireframe dashed; colours, widths and active/idle emphasis are unchanged. A manual calibration
|
||||
@@ -4723,12 +4808,17 @@ Coordinate input requires exactly two nonempty components; extra comma or semico
|
||||
|
||||
Location search/fly-to, annotations and Radio location lookup receive one
|
||||
`placeSearch.geocode(query, { bias, signal })` service. `src/standalone` composes
|
||||
two offline providers first — a strict decimal-degree coordinate reader and an
|
||||
two offline providers first — a strict decimal-degree/geo-URI coordinate reader and an
|
||||
exact-match reader over the bundled city and landmark names — then Google when
|
||||
configured and Photon/OpenStreetMap as fallback, including Google transport
|
||||
failures or declined requests. The offline pair answer with no request and no
|
||||
key, and decline everything they are not certain of: a coordinate must be a
|
||||
decimal-degree pair (either order when N/S/E/W fix the axes), and a bundled name
|
||||
decimal-degree pair (either order when N/S/E/W fix the axes), or a two-dimensional
|
||||
WGS84 `geo:latitude,longitude` URI with an optional `;crs=wgs84`. URI scheme and
|
||||
CRS names are case-insensitive; internal whitespace, altitude, uncertainty, other
|
||||
parameters, query strings and fragments decline rather than silently dropping
|
||||
information. For example, `geo:39.7392,-104.9903` resolves offline to the same exact
|
||||
point, decimal label and framing box as `39.7392, -104.9903`. A bundled name
|
||||
must match exactly, so `12junk, 34oops` and `austin, tx` both reach the
|
||||
geocoders instead of being answered from the bundle. Degrees/minutes/seconds and
|
||||
grid references are not parsed. An answer either provider gives is exact, so
|
||||
@@ -4785,7 +4875,7 @@ are omitted rather than framing the wrong part of the globe.
|
||||
- The bottom Visual Presets tray presents a **five-tile MAP SOURCE row** (`#map-stack-chips`, `src/mapStackChips.js`): Google 3D, Esri Satellite, Bing Aerial, Bing Labels, and OSM. The duplicate left `#stack-panel` is retired. The five tiles share one row on desktop and two rows on narrow screens, carry `aria-pressed` on the active source, and remain keyboard-reachable with a visible focus outline.
|
||||
- The lit tile follows controller state, not the click: a rejected switch (no ion token) or a superseded one (rapid A→B) leaves the genuinely active source lit, and the tray heading keeps its short-label status readout (`...` while switching, amber on `lastError`).
|
||||
- Ion stacks remain visible and keyboard-focusable when no ion token is configured, but expose `aria-disabled="true"` and do not switch. Their accessible label and tooltip quote `getStacks().unavailableReason` — the same string `setStack()` puts in the toast. OSM works keyless. The `ION` badge is gated on the stack's own `requiresIon`, so a `photoreal` chip unavailable because the Google tileset failed says so instead of falsely demanding an ion token.
|
||||
- Stack choice participates in share links (`src/sharelink.js`) and falls back to the best available stack when the requested one is unavailable (keyless boots land on Esri; OSM takes over automatically if Esri is unreachable). Share-link restore, the `set_map_stack` voice tool, and the chip row all land on the same `_setMapStack()` path.
|
||||
- Stack choice participates in share links (`src/sharelink.js`) and falls back to the best available stack when the requested one is unavailable (keyless boots land on Esri; OSM takes over automatically if Esri is unreachable; while that construction fallback is on screen Esri is retried in the background, 30 s doubling to a 10-minute cap with each attempt abandoned after 60 s, and replaces OSM once it loads. The switch to OSM after repeated Esri tile failures is not retried; selecting Esri again tries it). Share-link restore, the `set_map_stack` voice tool, and the chip row all land on the same `_setMapStack()` path.
|
||||
|
||||
### Voice Map Whiteboard / Annotations (June 2026)
|
||||
|
||||
@@ -4879,7 +4969,7 @@ pins and share links already supply complete boxes.
|
||||
- **Modes, pins and the preview.** The mode is explicit: `image` (one pinned day, slot `a`), `basemap` (that day on the left of a swipe, the basemap on the right: slot `a` splits LEFT and there is no second layer) and `ab` (day `a` left, day `b` right). `setAssignment(slot, key | null)` / `toggleAssignment(slot, key)` pin and unpin; a day is in one slot at most (pinning it in the other slot moves it) and an empty day cannot be pinned. `setMode()` keeps both pins; `b` only drapes in `ab`. While the mode's first empty slot exists the focused day previews in it: a click at once, arrow keys after 250 ms of quiet (`FOCUS_DEBOUNCE_MS`, one timer, last day wins); an unprobed day is followed and previews when its probe says present, an empty or unknown day never drapes and the previous preview stays. A pin change drops the preview until focus moves again, and unpinning takes that layer off at once. `rankLatest` picks START HERE coverage first (a present HLS day whose footprints cover the box, or have none, beats a newer sliver), then newest with all granules ≤ 20% known cloud, then newest; slivers only as `partial`; else the newest non-empty VIIRS day; it is focused and previewed after each search when nothing is pinned. Escape clears the preview (`clearPreview()`), then cancels the box tool, then blurs — with SELECT BOX armed too, through the tool's `onEscape` interceptor wired in `createApplicationTools`. Pins, mode, box and split changes made in the panel are published with `adoptLayerParams`, never while a share link is being applied.
|
||||
- **Rendering.** `rendering.js` owns at most two GIBS layers on the host `src/maps/imageryHost.js` resolves: `viewer.imageryLayers` while the globe shows, the photoreal tileset's `imageryLayers` while it is hidden, or none (the panel says so). Layers are rebuilt on a new host, never re-parented. `MapSourceController.subscribe()` fires after every settled stack switch (silent switches and fallbacks included) and the layer rebinds on it. Tile requests pass a six-slot semaphore of our own (Cesium's global limits are untouched). Under `requestRenderMode` a deferred tile needs a later frame, but a deferral must never request one itself or a stalled tile keeps the scene rendering every frame: a request that finds the semaphore full asks for nothing (the settling request asks for one frame), and a tile Cesium itself deferred goes through one coalesced 250 ms retry. Thumbnails: three fetches at once, sixteen decoded images, least-recently-used where a `get` read is never a use; eviction keeps the probe result. The strip's visible window is probed around the focused card; the pinned and previewed days are probed wherever they sit, at the focused card's priority, so a pin restored or assigned off-screen still drapes.
|
||||
- **Esri surface and the swipe.** While any image is on the map (a preview included) the layer holds the map lease (`acquireImageryComparison`, owner `recent-imagery`, `esri-if-photoreal`): on Google 3D the map switches to Esri, because 30 m pixels over 3D buildings do not read, and Cesium's split only works on the globe. The lease is handed back, and Google 3D restored unless the operator changed maps meanwhile, when the last image leaves the map or the layer is disabled; the notice line says so while it holds. While a day is shown the imagery keeps Esri: a manual switch that lands on Google 3D is answered at once with a new lease under the same policy (`keepEsri`), the notice reads `Imagery stays on Esri · CLEAR to use Google 3D`, and the divider never suspends because of a map switch. It re-leases once per switch generation and only for a manual one (`MapSourceController#getSwitchOrigin()` is `manual` for an outside `setStack`, `automatic` for the controller's own fallback or recovery), never for the lease's own switch and never while releasing, so CLEAR and disable still return to Google 3D; a lease another owner holds stays refused. When Esri fails and the controller falls back to Google 3D on its own, the layer lets the lease go without restoring anything, both days drape on the tileset without a swipe and the guidance reads `Esri map unavailable · no swipe`; only the next manual switch to Google 3D takes Esri again, so a failing Esri map is never retried in a loop. Switching between globe stacks (Esri, OSM, Bing) only rebinds. On a tileset host both images drape without a swipe. The split control (`createImagerySplit`, labels A / B, or IMAGE / BASEMAP) exists only while a swipe is live, recentres on every new comparison (not while scrubbing the second day), except once for a split restored from a link. There is no keyboard shortcut (Space is the app's push-to-talk): SWAP (`#ri-swap`, in the controls row, its slot reserved and the button disabled while no swipe is live) trades the two sides, A to the right and B to the left, or the image to the right of the basemap, and a second click restores; the hint reads `Drag the divider · SWAP trades sides`. The swap is not in the share link, and every new comparison starts unswapped. One opacity applies to both drapes. Another owner holding the lease is reported and the images still drape.
|
||||
- **Panel.** `#recent-imagery-panel` is static rail markup in `src/ui/templates/context.html` beside `#weather-panel`, registered in `PanelLayoutController`, `PanelChrome` and the share panel state (`ui` token `i`), hidden while the layer is off and opened on first appearance unless a stored or shared collapse choice exists (`dataset.collapsedPreference`). `LayerPanel.attachRecentImagery(factory)` mounts the readout (`createRecentImageryPanel` in `src/ui/recentImagery.js`) into its `data-rail-scroller` body, as the weather readout is mounted. The body is a fixed stack in which no block changes height and no control moves in any state: `#ri-actions` (SELECT BOX · USE VIEW · CLEAR, rail-card actions), `#ri-notice` (one line: `#ri-notice-text` with the refusal, error, CLEAR notice or the Esri note, and `#ri-zoom-in` in a fixed slot at its right end, reserved unless an oversized box or view can be fitted), `#ri-strip` (day cards newest first: thumbnail, date, sensor, cloud, START HERE, PREVIEW, and a SHOW chip or A and B chips; ← → Home End move, S / A / B pin, Enter previews), `#ri-hint` (one line: the next step), `#ri-selection` (`#ri-mode` IMAGE · VS BASEMAP · A / B, then `#ri-slot-a` and `#ri-slot-b` with `#ri-unpin-a` / `#ri-unpin-b` kept in place when empty), `#ri-controls` (`#ri-opacity`, EXPORT / EXPORT A and EXPORT B) and `#ri-details`, a collapsed rail card holding the focused day's acquisition, coverage and granules, catalog notes, the empty-days toggle and the imagery credit. Absent controls are reserved (invisible), never removed. Every render preserves the body's `scrollTop`.
|
||||
- **Panel.** `#recent-imagery-panel` is static rail markup in `src/ui/templates/context.html` beside `#weather-panel`, registered in `PanelLayoutController`, `PanelChrome` and the share panel state (`ui` token `i`), hidden while the layer is off and opened on first appearance unless a stored or shared collapse choice exists (`dataset.collapsedPreference`). `LayerPanel.attachRecentImagery(factory)` mounts the readout (`createRecentImageryPanel` in `src/ui/recentImagery.js`) into its `data-rail-scroller` body, as the weather readout is mounted. The body is a fixed stack in which no block changes height and no control moves in any state: `#ri-actions` (SELECT BOX · USE VIEW · CLEAR, rail-card actions), `#ri-notice` (one line: `#ri-notice-text` with the refusal, error, CLEAR notice or the Esri note, and `#ri-zoom-in` in a fixed slot at its right end, reserved unless an oversized box or view can be fitted), `#ri-strip` (day cards newest first: thumbnail, date, sensor, cloud, START HERE, PREVIEW, and a SHOW chip or A and B chips; ← → Home End move, S / A / B pin, Enter previews), `#ri-hint` (one line: the next step), `#ri-selection` (`#ri-mode` IMAGE · VS BASEMAP · A / B, then `#ri-slot-a` and `#ri-slot-b` with `#ri-unpin-a` / `#ri-unpin-b` kept in place when empty), `#ri-controls` (`#ri-opacity`, EXPORT / EXPORT A and EXPORT B) and `#ri-details`, a collapsed rail card holding the focused day's acquisition, coverage and granules, catalog notes, the empty-days toggle and the imagery credit. Absent controls are reserved (invisible), never removed. Every render preserves the body's `scrollTop`; opening DETAILS then scrolls its card into view once.
|
||||
- **Share links.** Layer options (token `1`): box edges as integers at degrees × 100000, pins `a` and `b` as compact days (`S20260918`), `m` mode (0 image, 1 basemap, 2 A / B), `p` split percent (share-link only: never stored locally) and `v` for the daily overview. Row chips: `MORE DETAIL · 30 m` (on) and `DAILY OVERVIEW · 250 m` (off).
|
||||
- Regression surface: the colocated `*.test.mjs` beside each module (shared doubles in `src/layers/recentImagery/testDoubles.mjs`; `src/ui/recentImagery.test.mjs` includes a DOM-structure guard: every control keeps its place in the tree, nothing is hidden, and every fixed-height block keeps the class the stylesheet fixes its height by; it has no layout, so it cannot see movement), `src/ui/layerPanel.test.mjs` (mount), `src/maps/controller.test.mjs` (`subscribe`), `src/data/layerState.test.mjs` (codec), `src/sharelink.celestial.test.mjs` (panel token), and the live gate `scripts/qa-recent-imagery.mjs`, which also measures every control's rect, relative to the panel, across the transitions within 1 px; that measurement is the real layout-shift check. `QA_RECENT_IMAGERY_SLOW_RENDER=1` gives loaded software renderers a 4 s zoom deadline and an 8-sample stability tail; the default stays 2 s and 12 samples.
|
||||
|
||||
@@ -5447,9 +5537,9 @@ When runtime behavior or architecture changes, update this file in the same chan
|
||||
|
||||
## Dependency security baseline
|
||||
|
||||
The lockfile uses DOMPurify 3.4.15, protobufjs 8.8.0, PostCSS 8.5.28, and
|
||||
nanoid 3.3.19. Cesium remains on 1.138.0. Browser QA uses Puppeteer 25.10.0;
|
||||
image-processing tools use Sharp 0.35.4. QA scripts await Puppeteer's asynchronous
|
||||
The lockfile uses DOMPurify 3.4.17, protobufjs 8.8.0, PostCSS 8.5.28,
|
||||
source-map-js 1.2.2 and nanoid 3.3.19. Cesium remains on 1.138.0. Browser QA
|
||||
uses Puppeteer 25.10.0; image-processing tools use Sharp 0.35.5. QA scripts await Puppeteer's asynchronous
|
||||
executable-path lookup before testing or passing the path to Chrome. Supported Node versions remain
|
||||
24.14.x and 26.x. Use `npm ci` to reproduce the checked-in dependency tree.
|
||||
|
||||
@@ -5489,6 +5579,22 @@ Local FM and internet radio are one listening surface: starting local FM stops
|
||||
internet-radio playback, and starting internet radio stops local FM. ADS-B
|
||||
reception does not interrupt internet radio.
|
||||
|
||||
- **FM seek.** Seeking wraps across 87.5–108 MHz, covers both band edges and
|
||||
skips the starting station. Spectra carry a decoder epoch, so a retune or mode
|
||||
change rejects earlier output. A failed scan retune stops seeking. This
|
||||
integrates Aditya's (@Rohilalala) contribution in #1174; it does not change
|
||||
the peak-selection algorithm or guarantee that a final station retune succeeds.
|
||||
- **Streaming and playback.** Four ordered USB reads keep input available while
|
||||
the page handles other work; control changes drain those reads before touching
|
||||
the receiver. The worker must finish loading before reads start. Decoder work
|
||||
is limited to eight unacknowledged blocks / 512 KiB, with a one-second oldest
|
||||
block watchdog. Overload stops the receiver with an explicit error. Stop
|
||||
terminates the worker immediately and disables the audio session; stale decoder
|
||||
output and audio from retired sessions are refused. FM audio starts after an
|
||||
80 ms prebuffer and retains at most 256 ms, dropping the oldest frames on
|
||||
overflow. FM signal display updates are coalesced to at most four per second;
|
||||
spectra are computed only while seeking. Optional `decoderDiagnostics` adds
|
||||
bounded timing samples and overload snapshots without recording audio or IQ.
|
||||
- **Modes.** FM (87.5–108 MHz, tune, seek, volume) or ADS-B at 1090 MHz.
|
||||
With a receiver open, selecting ADS-B enables the Local ADS-B layer;
|
||||
selecting FM disables it.
|
||||
@@ -5754,32 +5860,3 @@ datacenters, dams and the Nepal locator retain that shared credit while displaye
|
||||
|
||||
Military fragments stitch by source identity. Different mapped parcels merge only
|
||||
when their footprints touch or contain one another, never across a seam tolerance gap.
|
||||
|
||||
### Gemini Live provider integration
|
||||
|
||||
OpenAI admits each nonempty provider speech item ID once per connection lifetime,
|
||||
so duplicate or previously admitted starts cannot retire current input, abort its
|
||||
tools, restart narration or drop a queued typed request. The exact ID history is
|
||||
retained until Stop/reset rather than evicting older IDs; memory grows with the
|
||||
number and total size of admitted IDs in that lifetime. New unseen input still
|
||||
supersedes earlier work. Observer-triggered Stop, replacement or newer typed
|
||||
input cannot be overwritten when the earlier onset callback resumes.
|
||||
|
||||
Typed input keeps its own request identity across an interruption acknowledgment
|
||||
for a locally known, unobserved Gemini automatic activity. This single-use
|
||||
predecessor record does not treat captions as authority or make VAD alone wait
|
||||
for a generated reply. It expires on successor output, completion or newer input;
|
||||
later unattributed interruption markers retain the existing interruption policy.
|
||||
A genuine new automatic speech onset also revokes typed input when it arrives
|
||||
separately during that predecessor wait, without adding another retirement wait.
|
||||
On OpenAI, a newer speech onset discards a typed response still waiting to be sent;
|
||||
a deferred typed response with no newer input still answers once. The discarded
|
||||
request may go unanswered if the superseding speech is noise or gets no response.
|
||||
|
||||
Standalone voice supports OpenAI and Gemini; embedded hosts default to OpenAI and opt into Gemini through `voice.supportedProviders`. The provider dropdown uses the existing microphone control and result card. Manual selection reconnects an active session in its prior input mode; push-to-talk stays muted until a fresh Space hold. Voice that was off stays off.
|
||||
|
||||
Gemini uses a separate server-side `GEMINI_API_KEY` and one-session ephemeral credentials from `/api/gemini/token`. Device-rate microphone capture is resampled to 16 kHz; playback uses a separate 24 kHz context. Microphone permission precedes token minting. Bounded control queues, retired replies and pending tool actions are invalidated on interruption, Stop, replacement and teardown; input capture identities follow the physical microphone gate and end on Stop, replacement and teardown rather than on every interruption. `goAway` stops new turns and drains the current reply until completion or its supplied deadline, then requires an explicit restart.
|
||||
|
||||
Both providers use the shared action schemas, tool-result policy, pointer context and Radio ownership. Successful Radio playback, voice-requested or manual, suspends active voice and shows a toast; pausing, stopping or disabling that Radio resumes the prior voice mode once, with no time limit. Radio-only station, tuner and volume changes keep the lease; a tuner drag that ends paused without a new station also resumes voice. Explicit voice Stop, starting voice, provider replacement and teardown revoke it.
|
||||
|
||||
Clear standalone provider-switch requests receive the fixed spoken dropdown guidance; neither provider can change the selected provider through a tool. Ambiguous, quoted, negated, conditional and compound text remains ordinary conversation. The separate API-key setup entry remains available; the core provider does not add a Voice Settings panel or voice actions for opening key setup.
|
||||
@@ -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` |
|
||||
@@ -49,7 +49,8 @@ failure elsewhere. These questions guide review, not a mandatory questionnaire.
|
||||
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 `CHANGELOG.md` for runtime changes, and
|
||||
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.
|
||||
|
||||
## 3. Choose execution proportionate to risk
|
||||
@@ -124,6 +125,15 @@ 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.
|
||||
|
||||
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.
|
||||
|
||||
Return this concise record privately unless posting is authorized. Keep sensitive
|
||||
security evidence in the private report:
|
||||
|
||||
|
||||
+3
-3
@@ -263,17 +263,17 @@ and the development server's internal routes. See SECURITY.md.
|
||||
| `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 |
|
||||
| `aircraft_in_area` | `aircraft` | Aircraft in an area, nearest first; `military: true` reads the `military` feed; `group_by` and `stats` count and summarize every match |
|
||||
| `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 |
|
||||
| `vessels_in_area` | `vessels` | Ships reported by AIS in an area, nearest first, optionally by type; `group_by` and `stats` count and summarize every match |
|
||||
| `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` | The current image from one camera, returned as image content |
|
||||
| `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) |
|
||||
|
||||
@@ -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.
|
||||
+20
-131
@@ -5,22 +5,22 @@ overall lifetime. It coordinates startup and shutdown. `realtimeFacade.js`
|
||||
preserves existing methods and properties by delegation, without copying state.
|
||||
The session adapter, action runner and backend interfaces remain unchanged.
|
||||
|
||||
| Owner | State and responsibility |
|
||||
| ------------------------ | ---------------------------------------------------------------------------------------------------------------- |
|
||||
| `realtimeConnection.js` | Start generation, request cancellation, peer/data channel, acquired microphone stream and playback element |
|
||||
| `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) |
|
||||
| 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
|
||||
@@ -45,122 +45,11 @@ 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 claimed hold silences local playback when the Radio speaker guard permits it;
|
||||
admitted speech retires the provider turn and supersedes its tool work. A denied local claim
|
||||
preserves queued confirmation audio; a later native Gemini interruption retires
|
||||
the old reply and cancels its handoff. A click-started session stays open-mic.
|
||||
Gemini PTT waits for two consecutive 10 ms PCM windows above a low energy floor
|
||||
before submitting pointer context, activity markers or audio. Up to 400 ms of
|
||||
bounded pre-roll preserves speech onset; after admission, tails and pauses pass
|
||||
through until release. An empty or near-silent hold submits no new provider turn
|
||||
and clears its local pending-input state. It still silences existing local
|
||||
playback as an explicit Space hold and revokes a pending Radio handoff. A silent
|
||||
hold alone preserves the current provider/tool turn until its natural completion. This energy check is not a speech classifier:
|
||||
very quiet input below the floor is suppressed and sustained loud noise can pass.
|
||||
Open-mic capture and native automatic activity detection are unchanged.
|
||||
Current, uncancelled tool calls receive one terminal output within their owning
|
||||
provider conversation. Gemini suppresses calls retired by a session or local-turn
|
||||
boundary, or cancelled by the server. OpenAI answers superseded calls with a
|
||||
terminal refusal while the same conversation remains owned; callbacks from a
|
||||
replaced conversation cannot publish into the new session.
|
||||
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. Eligible stronger Radio controls begin without waiting
|
||||
for an older station lookup; a successful control cancels covered pending work.
|
||||
Already committed selection and unrelated sibling calls retain their authority.
|
||||
“Turn Radio on” enables the layer without selecting or playing a station;
|
||||
playback requires an explicit play, listen or resume request. Radio playback
|
||||
suspends an active voice session and remembers it until Radio is paused or
|
||||
disabled. This also applies to manual playback; merely enabling the layer does
|
||||
not suspend voice. Pausing or disabling Radio leaves a previously inactive
|
||||
voice session off. Provider replacement or explicit voice Stop cancels the
|
||||
saved resume; manually starting voice pauses Radio. A live cost meter stays
|
||||
bound to its negotiated model,
|
||||
even when the next-session preference changes. Clear standalone first-party
|
||||
provider requests remain ordinary provider turns, so the active OpenAI or Gemini
|
||||
model says in its normal voice, “I can’t switch voice providers directly. Hover
|
||||
over the Voice panel and use the provider dropdown.” Hosts without at least two
|
||||
distinct usable chooser options hide that control and
|
||||
instead explain that provider selection is managed by the app. The built-in
|
||||
endpoint defaults to this neutral guidance; the standalone host opts into chooser
|
||||
guidance. No tool can change the selected provider. Authoritative typed requests and
|
||||
recognized OpenAI speech block app actions; Gemini native-call refusal is
|
||||
described below. These refusals never change or save provider state, reconnect
|
||||
voice or remount a session. Ambiguous, quoted, negated, conditional and compound text
|
||||
remains ordinary conversation. Manual selection is the only provider switching
|
||||
path.
|
||||
|
||||
Gemini native tool calls are bound to the current socket, session epoch, local
|
||||
turn and unique call ID. Native input captions are unordered advisory segments:
|
||||
they are shown without authoritative request attribution and cannot grant or
|
||||
revoke a tool's lifetime. First-party typed input retains its authenticated
|
||||
request token through transport pressure, deferred delivery and same-turn
|
||||
continuation, until real new input or Stop revokes it. An accepted fresh
|
||||
`set_voice_provider` call refuses every not-yet-started tool for the rest of its
|
||||
local turn; malformed or replayed calls cannot quarantine a new turn. No tool
|
||||
changes or saves the selected provider, reconnects voice or remounts a session.
|
||||
|
||||
Gemini's sentence-level `ACTIVITY_START`/`ACTIVITY_END` signals track detected
|
||||
speech, not proof that a reply generation exists. Automatic activity alone does
|
||||
not retire an unanswered generation; observed server output or an explicit
|
||||
submitted push-to-talk/typed request retains the reply fence. Local activity
|
||||
owners preserve expiry liveness across an older retired completion, without
|
||||
identifying server generations or granting tools authority. The old boundary
|
||||
cannot clear a newer submitted request's pending reply. If expiry discards a
|
||||
newest deferred request, the ordered retired boundary closes the session rather
|
||||
than allowing its superseded forwarded predecessor to run under that request.
|
||||
Shorter repeated expiry notices retain that supersession. A native terminal
|
||||
completion settles even a silent user turn; pending tools, queued controls and
|
||||
audio still delay presentation completion, and no speech or captions are invented.
|
||||
|
||||
Noise-only automatic activity may receive no generation. During expiry it can
|
||||
wait with the microphone muted until the supplied deadline, capped at ten
|
||||
minutes; no shorter no-evidence timer assigns reply identity. Native Live events
|
||||
do not provide a VAD-sentence generation ID or guarantee their ordering relative
|
||||
to tools. Local activity ownership cannot prove that arbitrary undelimited late
|
||||
output belongs to an old generation; tools retain the socket/session/local-turn
|
||||
and call-ID policy above.
|
||||
|
||||
There is no first-party typed-command UI. Embedders and programmatic `sendTextCommand`
|
||||
callers can issue rapid typed requests while open-mic input is active. While a
|
||||
typed request is deferred behind an older reply boundary, concurrent microphone
|
||||
PCM may be discarded for up to that boundary's deadline. It is never replayed
|
||||
under the typed owner's identity. `deferredAudioDrops.nonPttOwner` and
|
||||
`deferredAudioDrops.closedActivity` report reason-specific lifetime-cumulative
|
||||
counts alongside `droppedAudioChunks`; they do not change admission or recovery.
|
||||
|
||||
`sendTextCommand` return values are provider-dependent. Gemini returns `true`
|
||||
when it admits the request locally (sent or queued) and `false` for empty input,
|
||||
an expiring session or failed local admission. Its empty-input and expiry checks
|
||||
precede connection validation; ordinary nonempty input requires an open socket.
|
||||
Oversized text and some context or queue-limit violations throw. A queued request
|
||||
can still be superseded or fail its bounded wait, which status events report.
|
||||
OpenAI returns `undefined` for successful submission or connected empty input,
|
||||
`false` when sending the user conversation item fails, and throws when it is not
|
||||
connected. Local admission does not guarantee provider delivery or execution.
|
||||
|
||||
For authoritative typed input and OpenAI speech, provider classification
|
||||
normalizes sentence punctuation at word boundaries, so "Switch to. Gemini."
|
||||
still identifies the direct request. Later authoritative segments can cancel
|
||||
cooperative pending resources, including generic and CCTV layer initialization.
|
||||
Pending annotations and their deferred outlines use the calling voice operation's
|
||||
cancellation signal. Stop, interruption and replacement cancel only that work;
|
||||
already committed marks and unrelated annotation owners remain. Completed actions
|
||||
and map-stack changes without caller cancellation cannot be undone. Gemini
|
||||
captions do not supply cancellation authority.
|
||||
Gemini retires old replies before forwarding deferred input; continued old output
|
||||
refreshes its five-second inactivity bound while deferred-input deadlines stay
|
||||
absolute. Stop and replacement invalidate both waits and their continuations.
|
||||
OpenAI input with explicitly disabled or failed optional transcription retains
|
||||
its authentic speech identity for ordinary tools. Missing, empty or untracked
|
||||
input cannot supply tool authority, and provider switching remains manual.
|
||||
|
||||
OpenAI response reservations remain ordered: a delayed response must consume its
|
||||
own slot. More than 16 unmatched reservations ends the owned session and shows
|
||||
"Voice lost turn tracking. Start voice again." A deliberate restart restores a
|
||||
fresh transport and turn tracker; it never automatically reconnects or assigns
|
||||
old responses to new input. A missing response before overflow remains ambiguous
|
||||
with a delayed one, so this does not establish native leak frequency or repair
|
||||
every possible FIFO mismatch.
|
||||
verified muted playback. A live cost meter stays bound to its negotiated model,
|
||||
even when the next-session preference changes.
|
||||
|
||||
Run the unit suite and both live recorded-audio modes against a configured local
|
||||
server:
|
||||
|
||||
@@ -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 … |
|
||||
@@ -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) | Л / П |
|
||||
Reference in new issue
Block a user