diff --git a/.env.example b/.env.example
index 2b7ff95ab..c89680b30 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,
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 107eed48e..8ce4cf231 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,37 @@
# 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.
@@ -63,6 +95,18 @@
## [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
diff --git a/README.md b/README.md
index 5a4ab8b24..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,7 +249,7 @@ 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.
Thirty tools, four jobs β the commands below come straight from the product's voice test suite and tool playbook:
@@ -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 `