diff --git a/.env.example b/.env.example
index d1e3361f0..58aa29ac1 100644
--- a/.env.example
+++ b/.env.example
@@ -55,8 +55,14 @@ GOOGLE_MAPS_SERVER_API_KEY=
# CLIENT-EXPOSED — use a public assets:read token with URL restrictions.
CESIUM_ION_TOKEN=
-# Optional: OpenAI Realtime voice control. Do not prefix with VITE_.
+# Optional: OpenAI Realtime voice control. API key is the default cloud auth.
+# Provider Settings can instead opt voice into the signed-in local
+# ChatGPT/Codex OAuth session; the browser still receives only a short-lived
+# Realtime client secret. Do not prefix with VITE_.
OPENAI_API_KEY=
+# Optional: forbid API-key voice sessions. Select ChatGPT OAuth in Provider
+# Settings; missing or expired Codex sign-in then fails closed.
+# GEV_PREFER_CODEX_OAUTH=true
OPENAI_REALTIME_MODEL=gpt-realtime-2
# Cheaper voice tier, used when the GEV MIC panel's MINI toggle is on.
# Model ids and prices are external facts that drift — if OpenAI moves this id,
@@ -67,6 +73,14 @@ OPENAI_REALTIME_REASONING_EFFORT=low
# Keep only a short conversational window. Current map state is fetched live.
OPENAI_REALTIME_CONTEXT_TOKENS=3000
OPENAI_REALTIME_CONTEXT_RETENTION=0.5
+# Voice-card captions of what you said. Billed separately by OpenAI and
+# counted in the session cost meter and cap; off disables them.
+OPENAI_REALTIME_TRANSCRIBE_MODEL=gpt-4o-mini-transcribe
+# The voice debug log (.gev-logs/realtime-conversations.jsonl, rotated at
+# 32 MB, never expired) omits transcripts, typed or spoken text, and tool
+# arguments/results (including serialized outputs) by default. Set to 1 to
+# keep them while debugging; delete the log when you are done.
+GEV_VOICE_LOG_CONTENT=
OPENAI_HUD_SUMMARY_MODEL=gpt-5-nano
# Per-IP rate limit for the OpenAI cost endpoints
# (/api/realtime/token + /api/openai/hud-summary), in requests per minute.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1adbd599e..69d093c40 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,7 +1,120 @@
# Changelog
+- Treat explicitly current, complete empty vessel coverage as a successful update,
+ clearing obsolete contacts while retaining stale or incomplete snapshots.
+
+- Custom Street Level provider switches survive share links and stored state
+ using URL-safe stable provider IDs and a shared boolean-switch codec; existing
+ Mapillary links keep their meaning. Explicit provider toggles reach durable
+ state without adopting unrelated live switch changes.
+
+- Directions and Recent Imagery accept replaceable acquisition sources, including
+ route transport, imagery catalog, thumbnails, tile templates and attribution.
+ Explicitly removing either source leaves its layer unavailable; standalone
+ defaults preserve the existing providers.
+
+- Catalog source availability no longer modifies layer objects; unavailable
+ tooltips use layer display names.
+
+- Street Level with no providers uses the shared unavailable state and refuses
+ activation before initialization, while keeping its layer identity visible.
+
+- Application source configuration accepts replacements and explicit removal.
+ Missing catalog feeds leave their layers unavailable without blocking startup
+ or making fallback requests; malformed supplied feeds remain configuration errors.
+
+- Street Level providers are optional in application composition. Omitting a
+ Mapillary source no longer prevents startup; callers can inject a different
+ provider list or an empty list without changing layer identities or share links.
+
+- Vector tile sources take their allowed tile origin from the configured
+ `tileJsonUrl`, so repointing only that URL at a mirror loads tiles from the
+ mirror or fails visibly instead of silently using OpenFreeMap. Thanks to
+ [daikaginza](https://github.com/daikaginza) (#935).
+
+- MCP setup examples use the app's default port, `4173`. Thanks to
+ [Huanyi Xie](https://github.com/xiehuanyi) (#934) and
+ [shirunjies8-png](https://github.com/shirunjies8-png) (#959) for the same fix.
+
+- Voice revisits numbered analyst records through current layer snapshots and
+ preserves resolved pin coordinates. Requested analyst lists and rankings can
+ speak returned items alongside the exact count and coverage caveats. Failed
+ actions settle the voice card without reviving cancelled turns. Coverage
+ benchmarks reject invalid calls before accepting semantic matches.
+
+- Street Level is included in the voice layer manifest for visibility toggles;
+ imagery is not exposed as countable analyst records.
+
+- Voice debug logs omit complete tool-result bodies and serialized function
+ outputs by default, including location queries and generated spoken replies.
+ `GEV_VOICE_LOG_CONTENT=1` retains them for explicit content debugging.
+
+- Unqualified Alps searches use the bundled European range for navigation
+ and annotations instead of a same-named peak returned by a geocoder.
+ Coordinates, supplied presets and geographically qualified place names
+ keep their precedence; annotation proximity and containment guards remain.
+- Contacts analyst follow-ups retain the snapshot's distance ranking rather
+ than switching to the camera. Requested radii and explicit scopes remain
+ authoritative, and a request for a list still requires an analyst query.
+ Analyst speech names stale or degraded feeds and withholds authoritative
+ counts when unavailable. The existing NOTES disclosure preserves complete,
+ deduplicated coverage and source caveats, including crowded results.
+- The voice card shows analyst counts ("At least 250,000 aircraft …") with
+ their scope, feed window and caveats, and marks partial answers. Analyst
+ speech uses the same deterministic headline, preserves lower bounds and says
+ when an answer is partial. Follow-ups retain unanswered-layer coverage, and
+ keep the original scope label when they narrow a remembered result.
+ Contacts-window answers read one immutable panel snapshot, mark a 20,000-row
+ layer cap as a lower bound, and commit that displayed cohort only after
+ cancellation checks pass. Concurrent analyst calls from one model response
+ remain independent; a stopped or replaced user turn still cancels them.
+- Voice answers are shorter and fill slow work instead of going silent. Slow
+ or multi-step requests may open with one short spoken plan; instant ones
+ just happen. Tool results carry a code-built `say` line and a `display`
+ card: sources and notes stay on screen, a caveat is spoken only when it
+ changes the answer, and "at least" and partial answers are always said. A card above the mic shows captions, the plan's steps and the
+ result. When a tool that reports steps (layers, place lookups, outlines,
+ nearest-aircraft) is still running after 1.5 s its current step is spoken
+ once; others say "still working" once after 8 s. A soft
+ tick plays in push-to-talk. Holding Space while the assistant talks cuts it
+ off. Each turn writes latency and speech figures to the debug log
+ (`node scripts/voice-turn-spans.mjs`). Captions of what you say are
+ counted in the voice cost meter; the debug log leaves out what was said
+ unless `GEV_VOICE_LOG_CONTENT=1`.
+- Point and ask: hold Space with the cursor on an aircraft, ship or spot on
+ the map and say "what's that", "track that one" or "how many flights
+ around here". A reticle marks the point and the voice card shows what
+ "this" meant. In open mic it works when you have just moved the cursor
+ there. "The second one" picks from the last numbered list. Asking about
+ "the last one" resolves to the last of the five rows actually shown, and
+ referent-only tracking calls are accepted while empty targets are rejected.
+ Asking about
+ bare ground up close gives the assistant a small crop of the map around the
+ point. A slow render gets one bounded second frame request, while hidden,
+ stale, moved-camera, invalid and oversized captures still fail closed.
+
## [Unreleased]
+- Add an experimental ChatGPT/Codex OAuth option for cloud voice in Provider
+ Settings. API-key voice remains the default. Local sign-in can start from
+ the auth button and reports completion, failure, timeout, or expired
+ credentials. OAuth credentials stay on the server; local routes require
+ loopback access and same-site requests. OAuth sessions retain usage data
+ and show unknown cost; caption transcriptions are counted rather than
+ priced into the API-key cap. The auth button keeps keyboard focus after a
+ mode change, and pending sign-in does not discard text in the key fields.
+ Thanks to [Shayan Khan](https://github.com/devv-shayan) for #621 and
+ [TheSmokeDev](https://github.com/TheSmokeDev) for the credential handling
+ from #653.
+
+- Stdio servers from one install share a panel key so a page read from one
+ process can make requests through another. Concurrent malformed-key repairs
+ now select one winner; unavailable storage or a busy repair retains the
+ logged per-process fallback. Thanks to [Jibran Tahir](https://github.com/jibraaan)
+ for #958, [MarvinNL046](https://github.com/MarvinNL046) for the report and
+ proposed diagnosis in #927, and [kvnloo](https://github.com/kvnloo) for identifying
+ the repair race. Windows/Claude Desktop Cowork confirmation remains outstanding.
+
- Street Level: a street-level imagery layer modelled on the iD editor's photo
overlay, with Mapillary as its first provider (free client token, CC BY-SA 4.0
imagery). One right-rail panel holds a chip per provider, shared 360°/flat and
@@ -326,6 +439,32 @@
(`src/ui/imagerySplit.js`, `src/maps/imageryComparison.js`), and
`MapSourceController.subscribe()` reports every settled map switch.
+## Unreleased — voice reaches every layer; honest analyst answers
+
+- Voice can switch on every shipped data layer (the two scene-driven
+ Bhote Koshi layers stay scene-only), including transit, Recent
+ Imagery, wind, the weather layers, cyclones and mapped installations, and
+ understands their spoken names ("buses", "hurricanes", "military bases").
+ One voice manifest (`src/voice/layerManifest.js`) generates the tool enums
+ and aliases, and a unit test fails when a new layer has no entry.
+- `analyst_query` counts up to 250,000 loaded records per layer instead of the
+ first 2,000, and says when a layer went past that (`complete: false`). It
+ also answers over bikeshare stations, transit vehicles, rocket launches,
+ cyclones, ALPR cameras, mapped installations, local ADS-B aircraft and fire perimeters; fire
+ records carry their sensor (VIIRS or MODIS).
+- Unknown fields, operators, units, value types, scope kinds and invalid
+ centres are refused with the allowed values instead of answering zero or widening to anywhere. A
+ layer that is off, still loading or unavailable is reported as such, not
+ as a count of zero; a partly answerable query is marked `partial`.
+ Distance sorts honour `sortDir`, a written unit such as `altitudeM(ft)`
+ converts, and large rankings no longer sort the whole set.
+- Results carry `lat`/`lon` for each item and a `display` object with the
+ scope, the feed window (earthquakes and fires cover the last 24 h) and any
+ caveat for the screen. The model is asked to answer in one short phrase.
+- The weather and Recent Imagery panels can be opened by voice.
+
+## Unreleased — local receiver feeds
+
### Local receiver feeds
- The Local ADS-B layer also reads local 1090 MHz and 978 MHz UAT decoder
@@ -468,6 +607,7 @@
Add feed provenance to analyst/view answers and HUD context while retaining existing response fields and runner ownership (Matt Van Horn, #347).
Analyst records for loaded satellites, datacenters and dams, with explicit bounded count/rank coverage (Matt Van Horn, #351).
+
- New Fire Perimeters layer (Events group): live NIFC WFIGS interagency
wildfire incident perimeters as ground-clamped polygons with a
containment-colored fire line, refreshed every 5 minutes from the public
@@ -744,7 +884,6 @@ of current runtime behavior, see [`docs/CURRENT-STATE.md`](docs/CURRENT-STATE.md
### Other changes
-
- Add ECMWF IFS model selection to Wind (#464, thanks @beneduzi), with model-scoped forecast-step caches, cancellation of replaced requests, and separate issue/valid timestamps.
- Add bounded Director feature actions with accessible controls, explicit camera/layer admission and cancellation; restore pack geometry on same-shot seek. Preserve existing scenes and content attribution.
@@ -1553,4 +1692,3 @@ represent previously published GitHub Releases.
## [0.1.0] — 2026-02-09
- Initial project version.
-
diff --git a/README.md b/README.md
index 1c2f375b5..5d97dfb2f 100644
--- a/README.md
+++ b/README.md
@@ -235,7 +235,13 @@ _Why cockpit mode exists: you're riding a real aircraft over real terrain — an
## 🎙️ Talk to It
-> Voice needs an **OpenAI key**. Without one the entire app still runs — the mic button just reports voice is unavailable. The same key drives the **AI HUD summary**: a terse, five-word intelligence-style readout of the current view that regenerates as you move.
+> Voice can use an **OpenAI API key** or, on the same machine, an opt-in signed-in **ChatGPT/Codex OAuth** session. The API-key path remains the default. Selecting ChatGPT OAuth in Provider Settings reuses an existing local sign-in or starts the Codex browser sign-in flow and selects OAuth when it completes. The OpenAI key still drives the **AI HUD summary**: a terse, five-word intelligence-style readout of the current view that regenerates as you move.
+
+The OAuth option is experimental. OpenAI documents ChatGPT plan usage for the [Responses API](https://developers.openai.com/siwc/token-sharing-open-source/models-and-inference); a supported third-party Realtime contract for reusing Codex tokens has not been confirmed. A successful token mint does not establish billing or support terms.
+
+GEV reads Codex's file credentials but does not write or refresh them. It checks token shape and expiry, and reports expired sign-in as requiring re-authentication. The login button runs `codex login`, permits one pending attempt, and reports exit failures or a two-minute timeout. `CODEX_HOME` must match the directory that Codex uses. If `CODEX_AUTH_JSON` points elsewhere, sign in separately or fix that mismatch before using the button. Codex credential storage must use `auth.json`; Keychain-only and in-memory storage are not readable by GEV. On Windows, the login button may not be able to start an npm-installed Codex; run `codex login` yourself, then select OAuth. See [Codex credential storage](https://learn.chatgpt.com/docs/auth#credential-storage).
+
+For a live voice check, run `node scripts/qa-voice-auth.mjs http://127.0.0.1:4189 oauth microphone.wav` against your local dev server. Use a WAV with three seconds of leading silence followed by a short spoken reply request. Replace `oauth` with `api-key` to check the API-key path. The check verifies audio input, a completed spoken reply, usage, resource cleanup, and reconnect. It saves screenshots and credential-free results under `.gev-logs/voice-auth/`.
Click **GEV MIC**, grant the microphone, and just talk. This is more than a voice-controlled remote:
@@ -243,9 +249,9 @@ Click **GEV MIC**, grant the microphone, and just talk. This is more than a voic
- **🎯 Entity Q&A.** Click any plane, ship, or datacenter and ask _"what's this?"_ It answers using the object's live telemetry.
- **👁️ Visual grounding.** At street level, it reads a viewport screenshot to identify legible signage and building names, and is instructed never to hallucinate labels.
- **🎬 Cinematic framing.** _"Show me the planes overhead"_ pulls the camera back, angles it, and frames the live traffic like a director.
-- **🔒 Honest and secure.** The agent only confirms actions that succeeded. Your `OPENAI_API_KEY` never touches the browser; the client only gets a short-lived session token.
+- **🔒 Honest and secure.** The agent only confirms actions that succeeded. Your `OPENAI_API_KEY` never touches the browser; the client only gets a short-lived session token. If you choose **USE CHATGPT OAUTH** in Provider Settings, the local server uses that sign-in only to mint the same kind of short-lived Realtime credential, and refuses the OAuth path for non-loopback clients.
-Twenty-nine tools, four jobs — the commands below come straight from the product's voice test suite and tool playbook:
+Thirty tools, four jobs — the commands below come straight from the product's voice test suite and tool playbook:
**🎥 Direct it** — drone-operator camera verbs:
@@ -395,7 +401,7 @@ src/
├── hud.js # Intelligence HUD + AI scene summary
├── keySetup.js # POWER UP panel — in-app provider keys (dev server only)
├── mapStackController.js # Basemap switching — Google 3D / Esri / OSM / ion stacks
-├── voice/ # OpenAI Realtime session + 29 voice tools
+├── voice/ # OpenAI Realtime session + 30 voice tools
├── layers/ # Layer components — weather, wind, cyclones, transit, ALPR, …
├── data/ # One module per layer + orchestration + context store
│ ├── iconOrientation.js # Screen-projected headings + horizon cull
@@ -429,7 +435,7 @@ Seven keys. Five have a free tier, and the two 🔴 ones are metered:
| --- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 🟡 | **Cesium ion** | 🗺️ Google Photorealistic 3D, world terrain, and additional ion-hosted imagery stacks. The free Community plan is for eligible individual, personal/non-commercial use and has quotas | [cesium.com/ion](https://cesium.com/ion) — use a public `assets:read` token and check current [pricing/eligibility](https://cesium.com/platform/cesium-ion/pricing/) |
| 🔴 | **Google Maps** | Direct Google Photorealistic 3D + Google place search ([Map Tiles API](https://developers.google.com/maps/documentation/tile)) | [Google Cloud Console](https://console.cloud.google.com/) — URL-restrict it |
-| 🔴 | **OpenAI** | 🎙️ The voice experience + AI HUD summary. The mini model works; the standard model is noticeably smarter. Want Gemini or another provider behind the mic? PRs welcome | [platform.openai.com](https://platform.openai.com) — metered, see costs below |
+| 🔴 | **OpenAI** | 🎙️ Voice + AI HUD summary through an API key. Provider Settings also offers **USE CHATGPT OAUTH** for cloud voice without removing the API-key option | [platform.openai.com](https://platform.openai.com) — API-key usage is metered, see costs below |
| 🟡 | **AISStream** | 🚢 Live global ships | [aisstream.io](https://aisstream.io) — free signup |
| 🟡 | **NASA FIRMS** | 🔥 Live active fires | [firms.modaps.eosdis.nasa.gov](https://firms.modaps.eosdis.nasa.gov/api/map_key/) — free |
| 🟡 | **TomTom** | 🚦 Live flow speeds and congestion colors for the simulated traffic layer | [my.tomtom.com](https://my.tomtom.com/keys) — free tier available |
@@ -508,7 +514,7 @@ Honest numbers, roughly, as of mid-2026 — always check the provider pricing pa
| **🟢 Most layers** | **$0, no signup.** OpenSky anon, USGS, CelesTrak, adsb.lol, city CCTV, Radio Browser, GBFS, Launch Library 2, transit feeds, OSRM routing, NOAA and ECMWF weather, OpenStreetMap ALPR mapping, bundled datasets. |
| **🟡 The free-key tier** | **$0 with a signup.** AISStream, FIRMS, TomTom, OpenSky, plus Cesium ion for eligible personal/non-commercial use. Provider quotas and eligibility still apply. |
| **🗺️ Google 3D tiles** | **Free through an eligible Cesium ion Community account within its quota; metered through a direct Google key.** Use the direct route for Google place search or commercial deployment, verify current provider terms, and set budget alerts where billing is enabled. |
-| **🔴 OpenAI voice** | **The one that costs real money — so the app meters it for you.** Realtime audio runs a few cents per active minute; an evening of heavy use is single-digit dollars. A live session-spend readout sits next to the mic, with an STD/MINI model toggle, a $2 warning, and a **$5 hard cap that ends the session**. The voice context window is kept deliberately short too. |
+| **🔴 OpenAI voice** | API-key sessions keep the live spend readout, STD/MINI model toggle, $2 warning, and **$5 hard cap**. ChatGPT OAuth is an explicit choice in Provider Settings. Its usage is tracked without an API-dollar estimate or spend cap. The voice context window stays deliberately short. |
Google's direct 3D route is surprisingly generous: the first 1,000 Photorealistic
3D Tiles sessions each month are currently free, and one root request supports
diff --git a/SECURITY.md b/SECURITY.md
index eb5fcd6e0..53437a83c 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -18,6 +18,7 @@ The golden rule: **secret-bearing API keys stay on the server side.** The dev/pr
| Key | Where it lives | How the browser uses it |
|-----|----------------|--------------------------|
| `OPENAI_API_KEY` | Server only | Browser fetches a short-lived **ephemeral** Realtime session token from `/api/realtime/token`; the real key never ships |
+| ChatGPT sign-in through Codex (optional, experimental) | Codex's own `auth.json` (`CODEX_HOME`, default `~/.codex`); GEV reads it server-side and never writes or refreshes it | Only after **USE CHATGPT OAUTH** is selected in Provider Settings: the server mints the same ephemeral Realtime token with the ChatGPT access token, for loopback clients only |
| `AISSTREAM_API_KEY` | Server only | Server holds the AISStream websocket; browser polls the same-origin `/api/vessels` cache |
| OpenSky OAuth (`OPENSKY_CLIENT_ID/SECRET`) | Server only | Server mints + refreshes the token behind `/api/flights` |
| `GOOGLE_MAPS_SERVER_API_KEY` (optional, #33) | Server only | Server calls Places (`/api/google/nearby-places`, `/api/google/text-search`) and the Street View fallback with this key; falls back to `GOOGLE_MAPS_API_KEY` when unset |
@@ -74,6 +75,7 @@ The data proxies under `server/providers/` are written so the browser cannot tur
- **Redacted debug logging.** The voice debug log (`.gev-logs/`, gitignored) strips API keys, bearer tokens, client secrets, and image data URLs before writing.
- **Cross-site gate on cost/log endpoints.** The cost-bearing (`/api/realtime/token`, `/api/openai/hud-summary`, `/api/google/nearby-places`, `/api/google/text-search`), quota-bearing (`/api/mapillary/*`, with a per-IP limit on tiles) and log (`/api/realtime/debug-log`) endpoints refuse cross-site browser requests: a foreign or opaque `Origin`, a `Sec-Fetch-Site` other than `same-origin`/`none` (this is what blocks an `
`/navigation that carries no `Origin`), or any reverse-proxy/CDN forwarding header returns `403`. Non-browser loopback tools (the repo QA harnesses POST the token endpoint with no `Origin`) and the explicit `HOST=0.0.0.0` LAN opt-in keep working — the gate deliberately does not require a loopback remote address; that stricter requirement belongs only to the credential panel (`admitKeySetupRequest`). The Host check (`allowedHosts`, applied by `hostCheckPlugin` ahead of every route, since Vite's own check runs only after plugin middleware) rejects a rebinding `Host` before any handler runs, in every mode, and the gate is defense in depth behind it: the LAN opt-in keeps that check and accepts only IP addresses, `localhost` and names listed in `GEV_ALLOWED_HOSTS`. **Limit:** a name you list there is trusted like the app's own address, so list only names you control.
- **Full Content-Security-Policy.** The dev and preview servers send a real CSP (`script-src 'self' 'unsafe-eval' blob:` plus the YouTube, Facebook and X scripts the Nepal event's embedded media loads, with a matching `frame-src` for their players and posts: no inline script and no other script origin; `blob:` covers the object URLs Cesium's bundled workers bootstrap through in the built app), alongside the existing `X-Frame-Options: DENY` and `frame-ancestors 'none'`. `'unsafe-eval'` stays because Knockout, bundled inside Cesium's widgets package, resolves the global object with `eval` at load time and the globe does not initialize without it; styles permit `'unsafe-inline'` and Google Fonts because Vite's dev client injects `