feat: prepare Phase 1 free local edition

This commit is contained in:
Redacted-Coder 2026-09-08 17:55:24 -04:00 committed by GitHub
parent ac927de71a
commit 57cacf92c6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 831 additions and 1339 deletions

View File

@ -1,24 +1,22 @@
# God's Eye View — environment variables
# Set to 1 to block paid Google/OpenAI services while allowing free-account keys.
GEV_FREE_ONLY=1
# Optional Photon endpoint override (server operator configured, not user input).
# GEV_PHOTON_URL=https://photon.komoot.io/api/
# Conservative local traffic tile budget. Use a provider account without paid overage.
TOMTOM_DAILY_TILE_BUDGET=5000
# Copy to .env and fill in your keys. On macOS the launcher can also read keys
# from the Keychain (see below); on Linux/Windows use this file or env vars.
#
# macOS Keychain: store any of these and ./scripts/dev-fresh.sh pulls them in.
# Each command prompts for the secret so it never lands in shell history:
# security add-generic-password -U -s "google-maps-api" -a "api-key" -w
# security add-generic-password -U -s "openai-api" -a "api-key" -w
# security add-generic-password -U -s "aisstream-api" -a "api-key" -w
# security add-generic-password -U -s "firms-map" -a "map-key" -w
# security add-generic-password -U -s "cesium-ion" -a "token" -w
# security add-generic-password -U -s "tomtom-api" -a "api-key" -w
# from the Keychain (see README); on Linux/Windows use this file or env vars.
#
# NOTE ON CLIENT-EXPOSED KEYS: GOOGLE_MAPS_API_KEY and CESIUM_ION_TOKEN are
# injected into the browser bundle by design (they're used client-side) and
# WILL be visible in devtools. Restrict/scope them rather than trying to hide
# them (see SECURITY.md). All other keys below stay server-side.
# Required: Google Maps API key (Map Tiles API must be enabled).
# Optional: Google photorealistic 3D tiles (Map Tiles API must be enabled).
# Leave blank to start with the free OpenStreetMap globe.
# CLIENT-EXPOSED — restrict it (HTTP referrer + API restriction) in Google Cloud.
GOOGLE_MAPS_API_KEY=your_google_maps_api_key_here
GOOGLE_MAPS_API_KEY=
# Optional: opt-in per-IP rate limit for the Google Places cost endpoint
# (/api/google/nearby-places), in requests per minute.
# DEFAULT IS UNLIMITED — unset (or 0) means no throttling, unchanged behavior.
@ -58,9 +56,9 @@ OPENAI_HUD_SUMMARY_MODEL=gpt-5-nano
# GEV_RATELIMIT_OPENAI_PER_MIN=30
# Optional: OpenSky auth mode (oauth | basic | auto | anon)
# Default: oauth — requires client credentials below
# Use anonymous access for the free, no-account setup.
# Set to "anon" to skip aircraft auth (rate-limited but works)
OPENSKY_AUTH_MODE=oauth
OPENSKY_AUTH_MODE=anon
# Optional: OpenSky OAuth credentials (from opensky-network.org account dashboard)
OPENSKY_CLIENT_ID=
@ -107,15 +105,15 @@ AISSTREAM_API_KEY=
# 0 disables the silence watch entirely.
# AISSTREAM_SILENCE_TIMEOUT_MS=120000
# Optional: TomTom live traffic flow (BYOK freemium — free tier is ~50,000 tile
# requests/day, get a key at https://developer.tomtom.com). SERVER-SIDE ONLY:
# Optional: TomTom live traffic flow (BYOK — check your current account
# allowance at https://developer.tomtom.com). SERVER-SIDE ONLY:
# the browser fetches same-origin /api/tomtom/* and never sees the key.
# Keyless fallback: the traffic layer runs its built-in simulation (white dots,
# hardcoded per-road-class speeds) — no key required for the layer to work.
# TOMTOM_API_KEY=
# Optional soft cap on upstream tile fetches per UTC day (default 40000).
# Optional soft cap on upstream tile fetches per UTC day (set to 5000 above).
# Over the cap the proxy serves cached/stale tiles instead of hitting upstream.
# TOMTOM_DAILY_TILE_BUDGET=40000
# TOMTOM_DAILY_TILE_BUDGET=5000
# Optional: CCTV layer tuning (advanced). Defaults are sensible — leave unset
# unless you're customizing the camera source pack.

8
.gitignore vendored
View File

@ -10,3 +10,11 @@ output/
.DS_Store
.gstack/
3d-models/
# Private configuration and local development artifacts
.env.*
!.env.example
*.pem
*.key
.codex/
.agents/

View File

@ -169,3 +169,9 @@ of current runtime behavior, see [`docs/CURRENT-STATE.md`](docs/CURRENT-STATE.md
## [0.1.0] — 2026-02-09
- Initial project version.
## Phase 1 community preview
- Add portable free-only startup and source-health diagnostics.
- Expand camera regions and enable official HLS playback.
- Add saved cities, Boston transit, global hazard reports, weather units and radar.
- Retain known grounded-aircraft regression limitations; upstream integration pending.

View File

@ -136,3 +136,14 @@ Douglas-Peucker simplification, 6-decimal rounding).
## In-app attribution
The required Google Maps / Cesium credit renders on the on-globe credit line (`#cesium-credits`, bottom-left) and must stay visible — including in clean-view and recording modes (the whole line, logo + "Google Maps" + the "Data attribution" link, stays on screen; only the GEV panels/HUD fade). The layer-specific credits (adsb.lol, TeleGeography, OSM datacenters/dams/roads, NASA FIRMS, CelesTrak, USGS, City of Austin, GBFS, Radio Browser, OpenSky, AISStream) are registered into the expandable **"Data attribution"** popover on that credit line via `viewer.creditDisplay.addStaticCredit(new Cesium.Credit(html, /* showOnScreen */ false))` — see `src/data/dataCredits.js`. When you add a new data source, add its license and attribution to this file **and** append an entry to `DATA_CREDITS` in `src/data/dataCredits.js` so it surfaces in the app.
## Phase 1 runtime additions
These connections fetch provider content at runtime; this contribution does not
bundle camera images, video recordings or weather observations.
- WSDOT camera catalog and snapshots: https://data.wsdot.wa.gov/ ; preserve WSDOT attribution and provider conditions.
- Caltrans HLS: official streaming URLs in the existing Caltrans catalog; on-demand playback, not redistributed recordings.
- MBTA vehicle positions: https://api-v3.mbta.com/ ; Boston only, subject to MBTA developer terms.
- GDACS: https://www.gdacs.org/ ; attributed published disaster reports, not comprehensive warnings.
- Open-Meteo: https://open-meteo.com/ ; CC BY 4.0 attribution; public endpoint limited to eligible noncommercial use.
- RainViewer: https://www.rainviewer.com/api.html ; free API use within provider terms, attribution retained; incomplete radar coverage.
- Photon: https://github.com/komoot/photon ; OpenStreetMap-based search, moderate public-service use; configurable self-hosted endpoint.

192
FREE-START.md Normal file
View File

@ -0,0 +1,192 @@
# Gods Eye — free local setup
Open **Start Gods Eye.command** in this folder, then visit http://localhost:4173.
Keep the launcher window open while using the app. Press Control-C to stop it.
If Gods Eye is already running, use the existing browser page instead of starting a second copy.
This launcher selects a compatible Node runtime and enables `GEV_FREE_ONLY=1`.
Google and OpenAI credentials are blocked; optional free-account keys in `.env`
are allowed. No subscription or paid API account is configured.
Try the Locations buttons or search for a city, landmark, or address and press
Enter. Bundled places work locally; other places use Photon / OpenStreetMap
through a bounded, cached proxy. The public Photon service permits moderate
project use and has no availability guarantee: https://github.com/komoot/photon.
Set `GEV_PHOTON_URL` to use your own compatible service if needed.
Turn on Earthquakes, Flights, Satellites, or the bundled infrastructure layers.
Public feeds need internet access and can be delayed, rate-limited, or unavailable.
Traffic without a TomTom key is a labeled simulation.
Use the **◉ Source Status** button at the top to inspect connections and receipt
ages, and to refresh enabled sources. Enabling Space Missions temporarily stages
its own layers; disabling it restores the previous view. It does not mean the
other feeds stopped working.
## Free-account connections
Add your own keys to `.env`, keep `GEV_FREE_ONLY=1`, and restart the launcher.
Do not paste keys into chat or save them in browser URLs.
| Feature | Variable | Account / key |
| --- | --- | --- |
| Live ships | `AISSTREAM_API_KEY` | https://aisstream.io/ |
| Fire detections | `FIRMS_MAP_KEY` | https://firms.modaps.eosdis.nasa.gov/api/map_key/ |
| Real traffic flow | `TOMTOM_API_KEY` | https://docs.tomtom.com/pricing |
TomTom lists a free vector-traffic tile allowance. Use an account with no paid
overage enabled. This setup limits requests to 5,000 tiles/day; that local counter
cannot account for other apps sharing your provider quota. Check your account's
allowance before adding a key. Without it, traffic remains explicitly simulated.
Google photorealistic buildings and OpenAI voice/AI commands are disabled under
the no-paid-services constraint. The free map is mapped imagery and terrain,
not live satellite video. Satellites are propagated from orbital elements;
CCTV is provider snapshots or streams with limited coverage; fire detections
and aircraft reports have inherent source delays. Buildings, cables, dams,
and datacenters are reference data rather than live activity.
For a manually configured launch, use a supported Node version, run `npm ci`,
then `npm run dev -- --host localhost --port 4173`. That command uses `.env`.
The free launcher keeps Google and OpenAI disabled even if `.env` later changes.
## Verification of this setup
- Production build passed using Node 24.19.0.
- All 2,607 unit and allocation tests passed after the free-source changes.
- Browser checks confirmed OpenStreetMap rendering, live aircraft tracking,
a keyless Tokyo search, 36 earthquakes, and 834 satellites. Feed counts vary.
- The retry verified free place search for Reykjavik, 5,537 aircraft in the
server snapshot, 50 military aircraft, 25 space missions, 800 camera directory
entries, and live weather. An Austin CCTV snapshot displayed `SNAPSHOT · OK`.
Directory entries do not guarantee every camera is working. The station-only
satellite endpoint returned 21 objects; the UI's four catalogs totalled 834.
- The added AISStream, NASA FIRMS, and TomTom keys were verified against real
payloads: a live ship stream, 183,552 fire detections from three sources with
`stale: false`, and a traffic tile decoded into 870 road segments. Counts vary.
- Fixed the mapped-installations proxy to try another public mirror after
upstream access rejections. The previously failing request now returns HTTP
200 with six mapped objects in the Austin test area. Free-mode HUD summaries
now use local metrics without repeatedly calling the disabled OpenAI service.
- The 21 targeted proxy/HUD tests and production build passed after these fixes.
- Fresh browser checks showed ships and fires ON, TomTom traffic ON with 96%
coverage, CCTV and mapped layers finishing loading, and no console errors.
Visual inspection still found a provider's "Image Unavailable" camera picture
delivered with HTTP 200. The badge now says RECEIVED instead of OK and explains
that delivery does not prove camera availability. Individual cameras remain
provider-dependent; a successful directory request is not a camera health test.
- Ship client refresh is now 10 seconds (previously 60), reading the server's
existing live stream cache; no extra upstream AIS subscription was added.
- Additional UI checks loaded 750 radio directory entries, about 4,400
datacenters, 716 dams, and about 2,600 cable reference objects. Radio playback
itself was not tested. Bikeshare returned no stations at the current view;
mapped installations requested a closer view. These are not verified live feeds.
- During the retry, OpenSky switched to the existing adsb.lol regional fallback.
That preserves nearby aircraft but is not global coverage. The Source Status
panel now carries the same fallback/stale labels as the layer controls.
- Before the latest proxy fix, the broader tracking harness finished with 96 passes and 4 failures:
three checks reported one HTTP 503 from the military-installations endpoint;
one ground-model sampling-count assertion also failed. The endpoint now passes
the live payload check; this does not establish that every tracking assertion passes.
- A later full tracking-harness run was interrupted by a development-page reload
(execution context destroyed) during the camera-label change, so it is not a
completed regression result. The final production build passed.
- Dependency hardening on September 6 updated compatible packages plus Puppeteer
and Sharp to patched releases. The install audit reported zero known
vulnerabilities. This is not a security certification or a public deployment review.
## City Pulse, Storm Watch, and worldwide weather
Open **◈ City Pulse, Storm Watch & World Weather** at the top of the map.
The panel adds its own map markers; selecting a result flies to its location.
**Focus map** temporarily hides other enabled layers; **Restore other layers**
or **Stop & close** brings them back. Closing stops these feeds and clears their markers.
- **City Pulse:** Boston transit positions from MBTA (up to 200 vehicles,
refreshed every 30 seconds). Other cities retain access to the existing traffic,
camera and bikeshare controls where those providers have coverage. Transit is
not worldwide. Use **Enable city layers** and **Browse cameras** as needed.
- **Storm Watch:** global GDACS cyclone, flood, drought, wildfire and volcano
reports, refreshed every six minutes. Up to 300 reports are mapped; cyclone,
flood, drought and volcano reports take priority over wildfire overflow.
These are published event reports, not radar, comprehensive local warnings,
or river-gauge measurements. Each record shows its report date and severity.
- **Weather around the world:** an 18-city global overview plus weather search
for other places. Open-Meteo provides model-based current conditions and
three-day forecasts, labeled separately from direct station observations.
Temperature offers Fahrenheit/Celsius, wind km/h, precipitation mm, and weather times UTC.
Refresh interval is ten minutes; source model updates can be less frequent.
Server caches coalesce simultaneous requests. On provider failure, cached data
less than an hour old may be returned explicitly marked STALE; older server
cache is not returned as current. Client-retained data also gets a stale label
if refresh fails. The Open-Meteo public endpoint is intended for noncommercial
use within its free limits; no paid service or new key was configured.
Sources: https://api-v3.mbta.com/ · https://www.gdacs.org/ · https://open-meteo.com/
Verified: 18 overview weather locations, 200 Boston vehicle records, and 300
hazard reports (including 7 cyclone and 15 flood reports in the test snapshot).
Browser checks covered all three views, focused mapping, and weather search for
Reykjavik. Five targeted tests and the production build passed. Existing unrelated
grounded-aircraft regression failures have not been changed by this addition.
## More camera cities and live video
Use the **▣ Browse camera cities and live video** button at the top of the map.
Choose a city/region and camera. Entries marked **VIDEO** expose an official
Caltrans HLS stream; press **Play live video** to start it. Return to snapshot,
change cameras, or close the dialog to release the stream. Playback is on demand
and limited to one stream in this browser panel. Traffic layers can remain enabled.
The expanded catalog loads all 12 Caltrans districts and adds 300 WSDOT cameras
across 10 Washington areas. The current 900-camera cap is shared across sources
to retain geographic variety. On verification it contained 216 live-video links;
individual stream availability varies. Los Angeles video played successfully in
the browser. Washington and London are labeled snapshots, not continuous video.
Washington source documentation: https://data.wsdot.wa.gov/arcgis/rest/services/TravelInformation/TravelInfoCamerasWeather/FeatureServer/0
No additional account or paid service is required for these additions. Set
`CCTV_WSDOT_ENABLED=0` to omit Washington; `CCTV_CALTRANS_DISTRICTS` can restrict
districts. Catalogs remain cached for 15 minutes. Selected snapshot requests run
every 30 seconds; provider image-update intervals may be longer. The live player
uses free HLS.js when the browser lacks native HLS playback. New York and Singapore
have not been connected in this change.
## Source health without extra API usage
The top **◉** control now shows how many enabled sources report a fault, stale
data, or a fallback. It checks the existing layer metadata every five seconds;
it does not make extra provider requests or consume API allowance. Selected
snapshot feeds also have conservative receipt deadlines to catch stalled updates.
Reference catalogs and predicted satellite positions are explicitly distinguished.
Open the control and choose **Download health report** to save a JSON report of
current states and the latest 200 state changes observed during this page session.
The report excludes raw errors, URLs, credentials, and coordinates. It is a
diagnostic journal, not a recording of observations, and resets on page reload.
Receipt age cannot establish observation freshness. Individual camera availability
still requires inspection; a provider can successfully return a placeholder image.
Validation after dependency and health changes: all 2,611 unit and allocation
checks passed, including simulated stalled receipts and recovery, bounded journal
retention, and exclusion of raw sensitive fields. The production build passed.
The completed browser tracking run passed 87 checks and failed four: grounded
models becoming ready, tracked model zoom transitions, retained hidden-model
flooring, and shown-but-not-ready fleet-model flooring. These remain unresolved.
The same full run recorded no console errors and no HTTP 5xx responses.
### Live view preferences and radar (September 6)
City Pulse now supports place search and up to 50 favorite cities saved in this
browser. Favorites appear first in the city selector and can be removed. Weather
offers remembered Fahrenheit/Celsius units for map labels, current conditions and
daily forecasts. New preferences default to Fahrenheit.
Storm Watch uses hazard symbols, severity colors and a category filter; labels
appear closer to the ground. These are report locations, not hazard footprints.
Storm Watch and World Weather include optional RainViewer precipitation radar,
a frame selector for the past two hours and opacity control. Metadata refreshes
every five minutes while open; tiles use the documented maximum zoom of 7.
Coverage is incomplete and frame generation times differ from observation times.
This is reflectivity, not Doppler velocity. No new API key is required.
Verification: 8 focused tests passed; production build passed; world weather,
custom city search/save and radar metadata loaded in the browser; a public radar
tile returned HTTP 200 image/png. This does not certify every external feed.

42
PHASE-1.md Normal file
View File

@ -0,0 +1,42 @@
# Phase 1 — public release candidate
This community edition extends Bilawal Sidhu's MIT-licensed God's Eye View.
Release status: community preview on ModDayJob/gods-eye-view, branch codex/phase-1.
Based on upstream ac927de; integration with newer upstream changes is pending.
## Included
- Free-only launcher, OpenStreetMap and keyless place search.
- Optional AISStream, NASA FIRMS and TomTom connections; faster ship refresh.
- Source-health indicators, stale/fallback labels and downloadable diagnostics.
- Expanded Caltrans and Washington cameras, region browsing and HLS video.
- City Pulse with Boston transit, place search and 50 saved favorite cities.
- Global GDACS hazard reports, category filters and severity symbols.
- Worldwide weather, three-day forecasts and saved Fahrenheit/Celsius units.
- RainViewer precipitation radar with recent-frame selection and opacity.
- Dependency updates, public setup instructions and automated test workflow.
## Validation and limitations
Earlier local checks passed 2,611 unit/allocation tests; the latest weather change
passed eight focused checks and a production build. A prior browser regression
run passed 87 checks and failed four grounded-aircraft/model checks. Those remain
open. See FREE-START.md for specific live checks; historical counts are not a
promise of current coverage. GitHub Actions has not run yet.
Transit is Boston-only. Radar is reflectivity, not Doppler velocity. Satellites
are predicted from orbital data. Camera availability varies. Third-party data,
maps, video and models remain subject to their source terms; the code's MIT
license does not grant blanket redistribution rights for provider content.
## Publication route
Fork the original repository under the chosen GitHub account. Clone that fork
to a separate folder to preserve upstream history, then apply this edition's
changes on a codex/phase-1 branch. Compare against upstream before committing;
do not replace upstream changes blindly. Exclude private configuration, caches,
generated builds and logs. Review the exact staged files for secrets and review
bundled data/model attribution before pushing. Do not upload this entire working
folder as a ZIP with local files included.
Open a draft pull request describing Phase 1 and its known limitations.
Publish a Phase 1 release/tag after review and passing checks. Users download
the source and run npm ci followed by npm start; each supplies their own optional
keys. A shared hosted service needs a separate deployment and quota design.

129
README.md
View File

@ -1,3 +1,36 @@
# Phase 1 — free local edition
Built on [Bilawal Sidhu's God's Eye View](https://github.com/bilawalsidhu/gods-eye-view).
The original MIT license and attribution are preserved.
## Start Phase 1
1. Install Node.js **24.14+ within 24.x** (recommended) or **26.x**.
2. Download this repository using **Code → Download ZIP**, extract it, and open a terminal in the extracted folder.
3. Run:
```sh
npm ci
npm start
```
The app opens at **http://localhost:4173**. Keep the terminal open; Ctrl+C stops it.
This start command enables free-only mode on Windows, macOS and Linux.
No API keys are needed to explore the free map, weather, radar and public feeds.
Optional ship, fire and traffic keys go in your own ignored `.env` file;
copy `.env.example` first. Never publish that file.
Phase 1 adds source-health reporting, expanded cameras and live video, city
favorites, Boston transit, global hazard reports, weather and precipitation radar.
Read [the setup and limitations](FREE-START.md) and [Phase 1 release notes](PHASE-1.md).
This is a local application with a server, not a standalone GitHub Pages site.
Public data providers have their own usage limits and coverage. Free-only mode
disables Google photorealistic tiles and OpenAI voice; the original documentation
below also describes those optional upstream features.
---
<div align="center">
# 🌐 God's Eye View
@ -22,7 +55,7 @@ Photorealistic 3D globe. Live aircraft, ships, satellites, earthquakes, traffic,
<div align="center">
**[Quick Start](#-quick-start) · [First Five Minutes](#-the-first-five-minutes) · [Talk to It](#-talk-to-it) · [What's Live](#-whats-on-the-globe) · [Under the Hood](#-under-the-hood) · [Keys](#-api-keys) · [Costs](#-what-it-actually-costs)**
**[Quick Start](#-quick-start) · [First Five Minutes](#-the-first-five-minutes) · [Talk to It](#-talk-to-it) · [What's Live](#-whats-on-the-globe) · [Under the Hood](#-under-the-hood) · [Keys & Costs](#-api-keys)**
</div>
@ -42,14 +75,6 @@ The live layers are grounded in public feeds: the airliner crossing your screen
## 🎛️ What This Thing Does
<div align="center">
[![YouTube video about the God's Eye View open source release](https://img.youtube.com/vi/GRJaKcXZS94/maxresdefault.jpg)](https://www.youtube.com/watch?v=GRJaKcXZS94)
▶️ **[The full walkthrough of everything below, on YouTube](https://www.youtube.com/watch?v=GRJaKcXZS94)**
</div>
- **🛩️ Cockpit view:** Ride inside a tracked flight — the camera holds the terrain under you all the way down.
- **📡 Contacts:** A 250 km roster of everything near your target — step through live aircraft and drop into any cockpit.
- **🎯 Click-to-track anything:** Camera locks on, draws a fading trail, surfaces full metadata — and a tracked fire or vessel hands you off to the nearest live camera in one click.
@ -69,7 +94,7 @@ The live layers are grounded in public feeds: the airliner crossing your screen
Requires Node.js 24.14.x or 26.x (enforced by `package.json`).
1. Copy `.env.example``.env` and set `GOOGLE_MAPS_API_KEY`.
1. Copy `.env.example``.env`. Leave keys blank for the free OpenStreetMap globe, or set `GOOGLE_MAPS_API_KEY` for optional photorealistic tiles.
2. Install and run:
```bash
@ -79,22 +104,9 @@ npm run dev -- --host localhost --port 4173
3. Open **`http://localhost:4173`**. Cold start settles in under two seconds on a recent laptop (median 1.86 s in a point-in-time M5/Chrome capture — [docs/PERFORMANCE.md](docs/PERFORMANCE.md); a comparison baseline, not a hardware requirement). A first-run card offers to stage a mission for you — **Live Contacts**, **Space Missions**, **Environmental** — or leaves you to explore manually.
> [!TIP]
> **Not a coder? Have an AI do this whole page for you.** A one-click installer is in the works — until then, install a coding agent ([Claude Code](https://claude.com/claude-code), [Codex](https://openai.com/codex/), [Cursor](https://cursor.com), or [Antigravity](https://antigravity.google)) and paste this:
>
> ```text
> Clone https://github.com/bilawalsidhu/gods-eye-view and set it up on my machine.
> Install everything it needs, walk me through getting the required Google Maps API
> key step by step (plus any optional free keys I want), put the keys in .env, and
> help me set a billing alert and a usage quota on the Google key so I can't
> overspend. Then start the dev server and open it in my browser. I'm not a
> developer — explain what you're doing as you go, and ask me before any step
> that could cost money.
> ```
**Start free with no keys.** OpenStreetMap and keyless terrain provide the globe; Google Maps optionally adds photorealistic buildings. Everything in this README is color-coded — 🟢 needs nothing · 🟡 free key · 🔴 metered. See [Free local setup](FREE-START.md) and [Keys & Costs](#-api-keys).
**That one key is the whole entry fee.** Everything in this README is color-coded — 🟢 needs nothing · 🟡 free key · 🔴 metered — and Google Maps is the only 🔴 you need: it buys the photorealistic planet, and most of the globe lights up 🟢 from there. For typical solo exploring, expect **$0 on most layers** and pocket change on the metered two: Google currently gives **1,000 free 3D-tile sessions a month** — each good for up to three hours of rendering, which is very hard for one person to exhaust — and voice carries a built-in $5 session cap. Full map in [Keys & Costs](#-api-keys), full honest breakdown in [What it actually costs](#-what-it-actually-costs).
The dev server binds to **localhost** — your keys stay on your machine. Sharing on a LAN safely is covered in [Sharing an instance](#-sharing-an-instance) and [SECURITY.md](SECURITY.md).
The dev server binds to **localhost** — your keys stay on your machine. Sharing on a LAN and the cost rails live in [Keys & Costs](#-api-keys) and [SECURITY.md](SECURITY.md).
**macOS shortcut:** `./scripts/dev-fresh.sh` clears the Vite cache and pulls your keys straight from the Keychain.
@ -105,12 +117,10 @@ The dev server binds to **localhost** — your keys stay on your machine. Sharin
No account, no signup. The first-run card will offer to stage a mission for you — or run this gauntlet yourself. Somewhere in these five minutes it stops feeling like a demo:
1. **Light up the sky.** Take the **Live Contacts** mission (or turn on **Flights** yourself) — thousands of live aircraft, gliding on real telemetry, detection mesh already reading the scene. Click one: the camera locks on, a trail draws behind it, and its live telemetry card comes up.
2. **Take the controls.** Hit **COCKPIT** on your tracked plane and ride it down, switching sensors mid-flight: NVG into Ironbow FLIR. The cockpit carries its own briefing strip — nearby live signals, regional headlines, and real local weather, with an opt-in **WX** mode that renders volumetric clouds from actual observations around your aircraft — and **Contacts** keeps the 250 km roster one click (or one sentence) away: jump plane to plane and fall straight into the next cockpit.
2. **Take the controls.** Hit **COCKPIT** on your tracked plane and ride it down, switching sensors mid-flight: NVG into Ironbow FLIR.
![Riding with a live aircraft in cockpit view while switching sensor modes](docs/media/06-cockpit-ar.gif)
![Jumping between live aircraft and falling straight into a cockpit view](docs/media/12-switch-aircraft-cockpit.gif)
3. **Drop into a busy airport.** Search one and descend to the taxiways with **3D** aircraft on — grounded contacts, taxi trails, the whole apron working in real time.
![Moving from a full airport overhead down to close taxiway inspection with 3D flight models](docs/media/start-here/airport-ground-traffic-google-3d.gif)
@ -119,25 +129,37 @@ No account, no signup. The first-run card will offer to stage a mission for you
![Diving into an Austin intersection with a live public camera projected into the 3D scene](docs/media/03-austin-cctv.gif)
5. **Paint the streets with rush hour.** Turn on **Traffic** and dive below ~8 km — per-vehicle flow colors to the real jams (with a TomTom key; keyless it's a labeled simulation). Then hit **NEAREST** in the CCTV panel and watch the jam through the camera pointed at it.
![Diving from city-scale live congestion straight into an intersection's public camera](docs/media/05-traffic-to-cctv.gif)
6. **Track something in orbit.** Turn on **Satellites** and click the ISS — you ride along at orbital distance, orbit ring and all.
5. **Track something in orbit.** Turn on **Satellites** and click the ISS — you ride along at orbital distance, orbit ring and all.
![Tracking the ISS along its orbital path as it crosses over Ukraine](docs/media/14-iss-over-ukraine.gif)
7. **Switch the optics.** Tap `1``7` — CRT, NVG, FLIR — and the whole live planet re-renders through a different sensor.
6. **Switch the optics.** Tap `1``7` — CRT, NVG, FLIR — and the whole live planet re-renders through a different sensor.
![Cycling a dense live globe through CRT, FLIR, and NVG in one continuous view](docs/media/01-style-sweep.gif)
8. **Talk to it** *(needs an OpenAI key)*: *"Take me to LAX and select the nearest airborne aircraft."*
9. **Come home.** Hit **Reset Globe** — or just say *"zoom out to a globe view."*
7. **Talk to it** *(needs an OpenAI key)*: *"Take me to LAX and select the nearest airborne aircraft."*
8. **Come home.** Hit **Reset Globe** — or just say *"zoom out to a globe view."*
**Keyboard:** `1``7` visual styles · `H` HUD · `D` detection · `C` cockpit · `Esc` out.
---
## 🛩️ The Cockpit
> Every plane should let you do this.
Real-time cockpit mode, built from live flight data: the camera rides your contact with real terrain holding underneath, all the way down — sensor styles come along for the ride, and **Contacts** keeps the 250 km roster one click away: jump plane to plane and fall straight into the next cockpit.
![Jumping between live aircraft and falling straight into a cockpit view](docs/media/12-switch-aircraft-cockpit.gif)
The cockpit even carries its own briefing strip: nearby live signals, regional headlines, and real local weather — with an opt-in **WX** mode that renders volumetric clouds from actual observations around your aircraft.
![A live military contact ridden through Normal, NVG, and Ironbow FLIR with dense detection](docs/media/start-here/military-cockpit-dense-google-3d.gif)
*Why cockpit mode exists: you're riding a real aircraft over real terrain — and you get to pick which sensor you see the world through.*
---
## 🎙️ 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.
@ -179,7 +201,7 @@ Twenty-eight tools, four jobs — the commands below come straight from the prod
## 🛰️ What's on the Globe
Thirteen live layers. **Ten of them need nothing at all** — no key, no account, no signup.
Thirteen live layers. **Ten of them need nothing at all** — no key, no account, no signup. (🟢 nothing · 🟡 free key · 🔴 metered.)
| Layer | What you get | Source | Auth |
|-------|--------------|--------|------|
@ -197,8 +219,14 @@ Thirteen live layers. **Ten of them need nothing at all** — no key, no account
| 🚀 **Space Missions** | Rolling 30-day launches with payload, stage, and recovery detail | Launch Library 2 | 🟢 (🟡 optional token raises the allowance) |
| 🎖️ **Mapped Installations** | Viewport-bounded military-site context from community mapping — incomplete by nature, and labeled that way | OpenStreetMap | 🟢 |
![A reconstructed Falcon 9 ascent climbing and curving into its projected orbit](docs/media/08-falcon9-replay.gif)
*The Space Missions layer replaying a Falcon 9 ascent — labeled `RECONSTRUCTED ESTIMATE`, scrubbable 0.25×4×.*
**Also on the globe:** neighborhood overlays · an optional cockpit WX cloud effect. **Bundled static infrastructure:** Datacenters (4,351), Dams (704), and Submarine Cables (712).
![Diving into the Bahamas and revealing labeled submarine cable routes beneath the globe](docs/media/09-undersea-cables.gif)
**Missing a layer you want?** Open an issue — or add it and send the PR.
---
@ -220,7 +248,6 @@ Once the basics click, run these:
| **🚀 Launch replay** | Open **Space Missions**, pick a launch from the last 30 days, and ride the T-minus countdown through ascent to orbit — scrub it at 0.25×4×. Labeled `RECONSTRUCTED ESTIMATE`, because it is one. |
| **🪦 Walk the boneyard** | Fly from regional context down into dense, fully resolved rows of retired aircraft. |
| **🏗️ Orbit Three Gorges** | Sweep the dam and its terrain at a glance — then flip on the **Dams** layer and find 703 more. |
| **🌊 Trace the backbone** | Dive to the Bahamas with **Submarine Cables** on — labeled routes reveal beneath the water, 712 of them worldwide. |
*🎙️ = voice missions — they need an OpenAI key.*
@ -236,14 +263,6 @@ Once the basics click, run these:
*Walk the boneyard: rows of retired airframes, fully resolved in 3D.*
![A reconstructed Falcon 9 ascent climbing and curving into its projected orbit](docs/media/08-falcon9-replay.gif)
*Launch replay: a Falcon 9 ascent, labeled `RECONSTRUCTED ESTIMATE`, scrubbable 0.25×4×.*
![Diving into the Bahamas and revealing labeled submarine cable routes beneath the globe](docs/media/09-undersea-cables.gif)
*Trace the backbone: the submarine cable routes under the Bahamas.*
---
## 🔧 Under the Hood
@ -287,13 +306,15 @@ Five keys cover the fully keyed experience. Three currently offer no-cost develo
| | Key | Why | Get it |
|---|-----|-----|--------|
| 🔴 | **Google Maps** *(required)* | The photorealistic 3D planet ([Map Tiles API](https://developers.google.com/maps/documentation/tile)) | [Google Cloud Console](https://console.cloud.google.com/) — metered; [check current pricing](https://developers.google.com/maps/billing-and-pricing/pricing) and URL-restrict it |
| 🔴 | **Google Maps** *(optional)* | Photorealistic 3D buildings instead of the free OpenStreetMap globe ([Map Tiles API](https://developers.google.com/maps/documentation/tile)) | [Google Cloud Console](https://console.cloud.google.com/) — metered; [check current pricing](https://developers.google.com/maps/billing-and-pricing/pricing) and URL-restrict it |
| 🔴 | **OpenAI** | 🎙️ The voice experience + AI HUD summary. Want another provider behind the mic? PRs welcome | [platform.openai.com](https://platform.openai.com) — metered; [check current API pricing](https://openai.com/api/pricing/) |
| 🟡 | **AISStream** | 🚢 Live global ships | [aisstream.io](https://aisstream.io) — free, seriously, it's a two-minute signup |
| 🟡 | **NASA FIRMS** | 🔥 Live active fires | [firms.modaps.eosdis.nasa.gov](https://firms.modaps.eosdis.nasa.gov/api/map_key/) — free |
| 🟡 | **TomTom** | 🚦 Real traffic instead of an approximate simulation | [developer.tomtom.com](https://developer.tomtom.com) — check the current developer allowance for your account |
*What the TomTom key buys you: step 5 of [The First Five Minutes](#-the-first-five-minutes) for real — actual rush-hour density painted on the city instead of an approximate simulation.*
![Diving from city-scale live congestion straight into an intersection's public camera](docs/media/05-traffic-to-cctv.gif)
*What the TomTom key buys you: rush-hour density painted on the city — then dive from the jam straight into the camera watching it.*
### Cherry on top
@ -308,9 +329,15 @@ All of them are worth getting. None of them are required to start.
```bash
# Put keys in .env (see .env.example), or pass them as env vars:
OPENAI_API_KEY="…" AISSTREAM_API_KEY="…" npm run dev -- --host localhost --port 4173
```
On macOS you can also keep any key in the Keychain and `./scripts/dev-fresh.sh` pulls them in — the `security add-generic-password` service names are documented in `.env.example`.
# On macOS, store any of them in the Keychain and dev-fresh.sh pulls them in:
security add-generic-password -U -s "google-maps-api" -a "api-key" -w
security add-generic-password -U -s "openai-api" -a "api-key" -w
security add-generic-password -U -s "aisstream-api" -a "api-key" -w
security add-generic-password -U -s "firms-map" -a "map-key" -w
security add-generic-password -U -s "cesium-ion" -a "token" -w
security add-generic-password -U -s "tomtom-api" -a "api-key" -w
```
OpenSky can run fully anonymous (`OPENSKY_AUTH_MODE=anon`), or import OAuth credentials with `./scripts/opensky-import-client.sh /path/to/credentials.json`.
@ -322,7 +349,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, bundled datasets. |
| **🟡 Optional developer access** | AISStream, FIRMS, TomTom, Cesium ion, and authenticated OpenSky may offer no-cost access, but limits and permitted uses differ. Cesium ion and OpenSky in particular have plan or use restrictions; verify the current provider terms for your deployment. |
| **🔴 Google 3D tiles** | More generous than you'd guess: billing counts **root tileset requests** — one buys up to **three hours** of unlimited tile rendering — and the first **1,000 per month are free**, then about **$6 per 1,000** (US pricing; [check the current page](https://developers.google.com/maps/billing-and-pricing/pricing), rates vary by billing region). A solo user rarely leaves the free tier. Still: restrict the key, set quotas, and configure a budget alert before sustained use. |
| **🔴 Google 3D tiles** | Map Tiles usage is billed by session, with current prices and free-usage caps varying by billing region. Check Google's pricing page, restrict the key, set quotas, and configure a budget alert before sustained use. |
| **🔴 OpenAI voice** | Realtime audio is usage-metered and the total depends on the selected model, conversation length, and audio volume. The app shows a live session estimate, warns at $2, and applies a **$5 in-app session cap**; provider-side usage limits remain the billing backstop. |
### 🧗 The floor is low on purpose

29
Start Gods Eye.command Normal file
View File

@ -0,0 +1,29 @@
#!/bin/bash
set -euo pipefail
cd "$(dirname "$0")"
# Prefer a supported installed Node, then the runtime bundled with Codex.
node_bin="$(command -v node || true)"
supported_node() {
[[ -n "$1" ]] && "$1" -e 'const [major, minor] = process.versions.node.split(".").map(Number); process.exit((major === 24 && minor >= 14) || major === 26 ? 0 : 1)' 2>/dev/null
}
if ! supported_node "$node_bin"; then
node_bin="$HOME/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/bin/node"
fi
if ! supported_node "$node_bin"; then
echo 'Gods Eye needs Node 24.14+ (version 24) or Node 26. Install it, then reopen this launcher.'
read -r -p 'Press Enter to close.'
exit 1
fi
if [[ ! -f node_modules/vite/bin/vite.js ]]; then
echo 'Dependencies are missing. Run npm ci with a supported Node version first.'
read -r -p 'Press Enter to close.'
exit 1
fi
# Block metered services, while allowing optional free-account keys in .env.
export GEV_FREE_ONLY=1 GOOGLE_MAPS_API_KEY='' OPENAI_API_KEY=''
export OPENSKY_AUTH_MODE=anon HOST=localhost PORT=4173
echo 'Gods Eye — free services mode: http://localhost:4173'
echo 'Keep this window open. Press Control-C to stop.'
exec "$node_bin" node_modules/vite/bin/vite.js --host localhost --port 4173 --strictPort --open

1624
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -2,7 +2,7 @@
"name": "gods-eye-view",
"private": true,
"version": "0.1.0",
"description": "A real-time intelligence console for planet Earth photorealistic 3D globe, live aircraft/ships/satellites/earthquakes/CCTV, and hands-free voice control. Runs in a browser.",
"description": "A real-time intelligence console for planet Earth \u2014 photorealistic 3D globe, live aircraft/ships/satellites/earthquakes/CCTV, and hands-free voice control. Runs in a browser.",
"type": "module",
"license": "MIT",
"author": "Bilawal Sidhu",
@ -39,19 +39,22 @@
"preview": "vite preview",
"test": "node scripts/run-unit-tests.mjs",
"test:track": "node scripts/track-regression.mjs",
"qa:map-source-tray": "node scripts/qa-map-source-tray.mjs"
"qa:map-source-tray": "node scripts/qa-map-source-tray.mjs",
"start": "node scripts/start-free.mjs",
"test:phase1": "node --test server/*.test.mjs src/liveViewPreferences.test.mjs"
},
"dependencies": {
"@mapbox/vector-tile": "^3.0.0",
"cesium": "^1.124.0",
"egm96-universal": "^1.1.1",
"hls.js": "^1.7.2",
"mgrs": "^2.1.0",
"pbf": "^5.1.2",
"satellite.js": "^6.0.2"
},
"devDependencies": {
"puppeteer": "^24.37.5",
"sharp": "^0.34.5",
"puppeteer": "^25.10.0",
"sharp": "^0.35.4",
"vite": "^6.0.0",
"vite-plugin-cesium": "^1.2.23",
"ws": "^8.21.0"

View File

@ -9161,3 +9161,50 @@ body.scene-playback-mode #first-run-launcher {
/* Scrolling a tile into view must not animate either. */
.first-run-choices { scroll-behavior: auto; }
}
/* Source health is on demand; no extra polling or panel work while closed. */
#source-status-dialog {
margin: auto;
box-sizing: border-box;
width: min(680px, calc(100vw - 40px));
max-height: 80vh;
overflow: auto;
color: #d8f4f3;
background: #0b171f;
border: 1px solid #46777d;
border-radius: 14px;
padding: 24px;
font: 14px/1.5 system-ui, sans-serif;
}
#source-status-dialog::backdrop { background: #000b; }
#source-status-dialog h2 { font-size: 22px; margin: 0 0 12px; }
#source-status-dialog section { padding: 10px 0; border-bottom: 1px solid #29414b; }
#source-status-dialog p { margin: 6px 0 14px; color: #b5ced3; }
#source-status-dialog section p { margin: 4px 0 0; font-size: 12px; }
#source-status-dialog a { color: #86e5e8; }
#source-status-dialog button { padding: 8px 14px; margin: 0 8px 10px 0; border: 1px solid #568d97; border-radius: 6px; color: #e1ffff; background: #163b46; cursor: pointer; }
#source-status-dialog button:disabled { opacity: .55; cursor: wait; }
#camera-browser-dialog h2 { margin: 0 0 12px; font-size: 24px; }
#camera-browser-dialog p { margin: 12px 0; line-height: 1.5; }
#camera-browser-dialog button, #camera-browser-dialog select {
font: inherit; color: #e1edf2; background: #19323e;
border: 1px solid #547783; border-radius: 6px; padding: 8px 12px;
margin: 4px 8px 4px 0;
}
#camera-browser-dialog button { cursor: pointer; }
#camera-browser-dialog button:disabled { opacity: .5; cursor: default; }
#camera-browser-dialog::backdrop { background: rgba(0,0,0,.55); }
#live-views-panel { position:fixed;z-index:10000;right:16px;top:85px;width:min(390px,90vw);max-height:78vh;overflow:auto;padding:20px;background:#0b1922;color:#e1edf2;border:1px solid #547783;border-radius:12px;font:14px/1.5 sans-serif; }
#live-views-panel h2 { font-size:22px;margin:0 0 12px; }
#live-views-panel button,#live-views-panel select,#live-views-panel input { color:#e1edf2;background:#19323e;border:1px solid #547783;border-radius:6px;padding:8px;margin:4px;max-width:100%;font:inherit; }
#live-views-panel button { cursor:pointer; }
#live-views-panel section { border-top:1px solid #34505c;padding:8px 0; }
#live-views-panel p { margin:8px 0; }
#live-views-panel fieldset { border:1px solid #39526e; border-radius:10px; margin:12px 0; padding:12px; background:#101c30; }
#live-views-panel fieldset legend { color:#8fd9ff; font-weight:600; }
#live-views-panel fieldset label { display:flex; align-items:center; gap:8px; }
#live-views-panel fieldset input[type=checkbox] { width:auto; }
#live-views-panel fieldset input[type=range] { width:100%; }
#live-views-panel a { color:#8fd9ff; }
#live-views-panel > div > section { border:1px solid #2e4158; border-radius:9px; padding:10px; margin:8px 0; background:#101a28; }
#live-views-panel [hidden] { display:none!important; }

View File

@ -26,6 +26,9 @@
*/
import fs from 'node:fs';
import { liveViewsPlugin } from './server/liveViews.js';
import { loadWashingtonCameras, balanceCameraCities, publicVideoUrl } from './server/cameraExpansion.js';
import { freeServicesPlugin } from './server/freeServices.js';
import { promises as fsp } from 'node:fs';
import { createHash, randomUUID } from 'node:crypto';
import path from 'node:path';
@ -2508,6 +2511,13 @@ function sendOverpassResponse(res, payload, cacheStatus = 'MISS') {
* @param {number} [maxResponseBytes] Endpoint-specific response cap.
* @returns {Promise<{status:number,body:string,contentType:string,endpoint:string,rateLimited:boolean}>}
*/
export function shouldRetryOverpassMirror(status) {
// A mirror can reject this host/request at its edge (observed HTTP 406)
// while other public mirrors remain available. Preserve HTTP 400 as a
// query error instead of sending invalid queries to every provider.
return [403, 406, 408].includes(status) || status >= 500;
}
async function fetchOverpassPayload(body, maxResponseBytes = OVERPASS_MAX_RESPONSE_BYTES) {
let lastError = null;
let lastRateLimitPayload = null;
@ -2551,7 +2561,7 @@ async function fetchOverpassPayload(body, maxResponseBytes = OVERPASS_MAX_RESPON
lastError = new Error(`Overpass runtime error (${endpoint})`);
continue;
}
if (status >= 500) {
if (shouldRetryOverpassMirror(status)) {
lastError = new Error(`Overpass upstream returned ${status} (${endpoint})`);
continue;
}
@ -3426,7 +3436,7 @@ const AUSTIN_DOWNTOWN = { lat: 30.2672, lon: -97.7431 };
const CALTRANS_CCTV_URL = (district) =>
`https://cwwp2.dot.ca.gov/data/d${district}/cctv/cctvStatusD${String(district).padStart(2, '0')}.json`;
/** Districts fetched by default: SF Bay (4), LA (7), San Diego (11), Sacramento (3). */
const DEFAULT_CALTRANS_DISTRICTS = '4,7,11,3';
const DEFAULT_CALTRANS_DISTRICTS = '1,2,3,4,5,6,7,8,9,10,11,12';
const DEFAULT_CALTRANS_MAX_SOURCES = 300;
/** Prioritization anchors: downtown cores of the four default metros. */
const CALTRANS_ANCHORS = [
@ -3951,6 +3961,7 @@ async function loadCaltransSourcesFromOpenData() {
city: String(loc.nearbyPlace || `Caltrans D${district}`),
cityId: `ca-d${district}`,
provider: 'Caltrans',
liveVideoUrl: publicVideoUrl(cctv.imageData?.streamingVideoURL),
lat,
lon,
headingDeg: hasHeading ? heading : fallbackHeadingFromId(cameraId),
@ -3984,7 +3995,7 @@ async function loadCaltransSourcesFromOpenData() {
const maxRaw = Number(process.env.CCTV_CALTRANS_MAX_SOURCES || DEFAULT_CALTRANS_MAX_SOURCES);
const maxCount = Number.isFinite(maxRaw) ? Math.max(8, Math.min(600, Math.floor(maxRaw))) : DEFAULT_CALTRANS_MAX_SOURCES;
const prioritized = prioritizeSources(cameras, maxCount, CALTRANS_ANCHORS);
const prioritized = balanceCameraCities(cameras, maxCount, source => source.cityId);
console.log(`[CCTV] Loaded Caltrans camera sources: ${cameras.length} inService (using nearest ${prioritized.length})`);
return prioritized;
}
@ -4090,6 +4101,7 @@ function normalizeSourceItem(item) {
feedType: normalizeFeedType(item.feedType || item.type || ''),
url: typeof item.url === 'string' ? item.url : '',
snapshotUrl: typeof item.snapshotUrl === 'string' ? item.snapshotUrl : '',
liveVideoUrl: publicVideoUrl(item.liveVideoUrl),
license: String(item.license || item.licenseNote || ''),
sourceKind: String(item.sourceKind || item.kind || 'configured'),
// Optional CAL badge input (cctv-v2 design §3b/§9.2, additive-only per the
@ -4145,18 +4157,21 @@ async function refreshCctvSources() {
let fromAustin = [];
let fromCaltrans = [];
let fromTfl = [];
let fromWashington = [];
if (needsLiveSources) {
const [austinResult, caltransResult, tflResult] = await Promise.allSettled([
const [austinResult, caltransResult, tflResult, washingtonResult] = await Promise.allSettled([
loadAustinSourcesFromOpenData(),
loadCaltransSourcesFromOpenData(),
tflEnabled ? loadTflSourcesFromOpenData() : Promise.resolve([]),
loadWashingtonCameras(),
]);
fromAustin = austinResult.status === 'fulfilled' ? austinResult.value : [];
fromCaltrans = caltransResult.status === 'fulfilled' ? caltransResult.value : [];
fromTfl = tflResult.status === 'fulfilled' ? tflResult.value : [];
fromWashington = washingtonResult.status === 'fulfilled' ? washingtonResult.value : [];
}
// Live sources first so file/env overrides win on duplicate IDs (Map last-write).
const merged = [...fromAustin, ...fromCaltrans, ...fromTfl, ...fromFile, ...fromEnv];
const merged = [...fromAustin, ...fromCaltrans, ...fromTfl, ...fromWashington, ...fromFile, ...fromEnv];
// Deduplicate by camera ID (last-write wins because of Map.set)
const byId = new Map();
@ -4173,7 +4188,7 @@ async function refreshCctvSources() {
if (mergedSources.length > maxCount) {
console.warn(`[CCTV] source catalog ${mergedSources.length} exceeds cap ${maxCount}; keeping the first ${maxCount} (raise CCTV_MAX_SOURCES or lower a per-pack cap to change which).`);
}
const capped = mergedSources.length > maxCount ? mergedSources.slice(0, maxCount) : mergedSources;
const capped = mergedSources.length > maxCount ? balanceCameraCities(mergedSources, maxCount) : mergedSources;
if (capped.length > 0 || _cctvSourceCache.length === 0) {
_cctvSourceCache = capped;
} else {
@ -4518,6 +4533,7 @@ function cctvProxy() {
sourceKind: source.sourceKind || (source.url ? 'configured' : 'fallback'),
poseSource: source.poseSource,
license: source.license,
liveVideoUrl: source.liveVideoUrl || '',
})),
};
res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
@ -7337,9 +7353,17 @@ export default defineConfig(({ mode }) => {
for (const [key, val] of Object.entries(loaded)) {
if (process.env[key] === undefined) process.env[key] = val;
}
// Explicit free mode suppresses metered Google/OpenAI credentials even when
// inherited from a shell. Optional free-account keys remain server-side.
if (process.env.GEV_FREE_ONLY === '1') {
process.env.GOOGLE_MAPS_API_KEY = '';
process.env.OPENAI_API_KEY = '';
}
const env = { ...process.env };
return {
plugins: [
freeServicesPlugin({ fetchJson: fetchRegionalJson }),
liveViewsPlugin({ fetchJson: fetchRegionalJson }),
cesium(),
openSkyProxy(),
celestrakProxy(),
@ -7371,6 +7395,7 @@ export default defineConfig(({ mode }) => {
},
// Expose selected API keys to the browser via import.meta.env.*
define: {
'import.meta.env.GEV_FREE_ONLY': JSON.stringify(env.GEV_FREE_ONLY === '1'),
'import.meta.env.GOOGLE_MAPS_API_KEY': JSON.stringify(env.GOOGLE_MAPS_API_KEY),
'import.meta.env.CESIUM_ION_TOKEN': JSON.stringify(env.CESIUM_ION_TOKEN),
},