Vantage is a real-time, multi-domain common operating picture built entirely on free, no-key data. Two domains run from one frontend codebase:
- Air / Sky — the real sky above your location: aircraft, satellites, planets, the Sun, the Moon, the Milky Way and live meteor showers, each placed at its true azimuth and altitude (real callsigns, real NORAD objects, real ephemerides), plus a top-down tactical radar scope. Point a projector straight up and it becomes an immersive ceiling.
- Ground / City — a live city activity map: incidents, hazards, traffic and public
cameras from dozens of free public feeds, fused into hotspots and an event feed
(
?display=city; see the Ground / City domain section below).
Most of this README documents the Air domain (the original core); the Ground/City domain has its own section further down.
Vantage ships as Vantage for Windows — a native desktop app (Tauri v2 + WebView2) with its own window, system tray (minimise-to-tray), single-instance, saved window position/size, and auto-update. Its proxy is a native Rust server embedded in the app — no Node, no bundled Chromium. The render loop pauses when the window is hidden so it doesn't eat resources in the background.
server/ is a Node/Express implementation of the same /api surface. It is a development
server and a test oracle, not a shipping surface — nothing in it reaches a user.
It earns its keep by being a second opinion. The two backends are diffed against each other
by scripts/parity-live.mjs, and that keeps finding defects in the
Rust proxy — the one that actually ships — which no single-implementation test can see, because
there is nothing to compare the answer to. Recent examples, all desktop-side:
parse_iso_mssilently discarded a trailing±HH:MM, so every NWS alert was stamped seven hours early. Nine call sites went through it.- The NWS
expiresfield was never read, so a lapsed alert could not be retired. - FAA event ids omitted a segment, so not one matched — selection and dedup disagreed.
Shape-level checks are blind to all of these: both sides emit a title, and the two titles are
simply different strings. Hence two layers — contract-smoke.mjs for shape, parity-live.mjs
for values.
The frontend is plain ES modules with no build step, so the dev server also gives instant reload without a ~2-minute Rust rebuild. WebXR works when driven from it, but headsets are not a supported product surface.
| Concern | Choice | Why |
|---|---|---|
| Rendering | Three.js (WebGL2) | Mature, fast, direct control of the real-time render loop. The whole app is one continuous draw loop over thousands of points + dozens of meshes — exactly what a thin WebGL layer is best at. |
| XR | WebXR (built into Three.js) | One codebase gives desktop, projector, and Oculus/Quest VR + passthrough AR for free. No native VR rewrite. |
| App code | Vanilla ES modules | No framework. A 60 fps render loop driving imperative GPU state gets nothing from React/Vue's diffing — they'd only add overhead and indirection. Modules keep it organised without a build step. |
| Desktop shell | Tauri v2 + WebView2 | Native window using the OS's existing Chromium (WebView2) — same WebGL2 renderer as Electron, without bundling a second browser. Tiny binary, low RAM. |
| Backend | Native Rust (axum), embedded in the app | Only job is to proxy/cache the free APIs (CORS + rate limits). A second Node implementation exists for development and as a differential test oracle — see above; it does not ship. |
| Data | adsb.lol / adsb.fi · CelesTrak (SGP4 via satellite.js) · astronomy-engine · Open-Meteo · adsbdb · HYG |
All free, no key; orbital + ephemeris math runs locally. |
| Deps | Vendored locally (public/vendor/: three, satellite.js, astronomy-engine) via an import-map |
No bundler and no CDN — the app boots fully offline. |
Verdict: no rewrite needed. This is already the optimal stack for "one web codebase → screen + projector + Quest." The alternatives are worse fits here:
- React / react-three-fiber — declarative reconciliation fights a manual render loop; pure overhead.
- Unity / Unreal — great native graphics but can't run in a browser/kiosk, huge runtime, and throws away the free WebXR path.
- CesiumJS — built for a geospatial globe, not a from-the-ground sky dome.
- Babylon.js — peer of Three.js; switching buys nothing.
Optional future polish (not required): add Vite + TypeScript for build-time bundling and types — a tooling upgrade, not an architecture change.
Download Vantage_<version>_x64-setup.exe from the latest release. It installs per-user (no admin prompt) and updates itself from then on.
Vantage 1.0 is not Authenticode code-signed, so SmartScreen shows "Windows protected your PC". That warning means only one thing: nobody has paid a certificate authority to vouch for this publisher. It is not a malware finding. A cert is a recurring cost that buys reputation, not safety, and this project would rather ship the release and tell you the truth than delay it or pretend the warning doesn't happen. Click More info → Run anyway if — and only if — the checksum below matches.
Verify before you run it. Every release publishes SHA256SUMS.txt beside the installer:
Get-FileHash .\Vantage_1.0.0_x64-setup.exe -Algorithm SHA256Compare it against the release's SHA256SUMS.txt. If they differ, do not run it.
Auto-updates use their own minisign key, which is free and unrelated to Authenticode. The public half is compiled into the app; the private half never leaves CI. A client refuses any update whose signature doesn't verify — so while the first install is unsigned, every update after it is cryptographically verified. Check for updates from the tray menu; it now reports the actual outcome (up to date / downloading / failed: reason) instead of doing nothing visible.
The shipped binary has no console by design, so it writes its own breadcrumbs to
%LOCALAPPDATA%\Vantage\logs\: a rolling vantage.log (startup, port binding, update and feed
errors) and one crash-<timestamp>.log per panic. Attaching those to a bug report is the
difference between a fix and a guess.
Requires the Rust toolchain (rustup) and the WebView2 runtime (preinstalled on
Windows 11). The Tauri CLI comes in as a dev dependency via npm install.
npm install
npm run tauri:dev # dev window with live devtools
npm run tauri:build # NSIS installer -> src-tauri/target/release/bundle/nsis/Serves the same frontend over the Node backend, for iterating on public/ without a Rust
rebuild. It is also the oracle half of the parity tests below.
npm start
# open http://localhost:3000npm test # standalone, no server needed
npm run smoke # contract parity — needs Node on :3000 AND the app on :47615
npm run smoke:headless # same, but starts/stops both backends itself — no window opens
npm run smoke:headless -- --fresh --parity # + refuse a stale binary, + diff live VALUESsmoke:headless uses vantage.exe --headless [--port N], which starts the embedded axum
server with no Tauri runtime, no window and no tray icon — so /api parity can be
checked against the real native backend without taking over the desktop. It reuses anything
already listening, so it's safe to run while the app is open.
| Test | Guards |
|---|---|
scripts/version-check.mjs |
package.json, Cargo.toml and tauri.conf.json agree on the version (the updater compares against the last one). |
scripts/parity-test.mjs |
The text classifiers, against shared golden fixtures in shared/fixtures/ — the same files the Rust cargo test suite runs. Plus the matching semantics and the shape of the rules file itself. |
scripts/classify-test.mjs |
Aircraft service classification (civ / law / mil / EMS). Locks in two real bugs that both silently mislabelled government aircraft as civilian — \bsheriff\b failing on the plural "SheriffS Department", and angel (Angel Flight) matching "Los Angeles". |
scripts/sgp4-testvector.mjs |
The SGP4 az/el pipeline, against a recorded vector + an independent implementation. |
scripts/projection-test.mjs |
Ceiling-mode projection: azimuth/altitude → dome position → screen. |
scripts/docs-check.mjs |
Documentation drift: every relative link resolves against git ls-files (so a gitignored target fails, as it would for anyone cloning), every env var the code reads is documented, and every /api route appears here. |
scripts/contract-smoke.mjs |
Node ⇄ Rust /api shape parity: identical key sets on every route, plus the same source catalogue with the same enabled / keyed / optin / category / kinds. |
scripts/parity-live.mjs |
Node ⇄ Rust /api value parity at three fixed observers (LA / NYC / Seattle): same kind, severity, id, title, description, sourceUrl, coordinates and timestamps, per feed. This is the layer that catches "both sides emit a title and the two titles differ" — invisible to a shape check. Release gate: zero mismatches. |
The text-classification tables are not written in either language. They live in
shared/rules/classify.json; Node parses it at module init and Rust compiles it in with
include_str!, so a malformed file fails the build. Only the ~15-line matcher exists twice,
and it is pinned by shared/fixtures/classify.jsonl, which both npm test and
cargo test execute. A rule that behaves differently in the two backends is a failing test,
not something a user finds on a map.
The same discipline now covers feed timezones, the named-zone abbreviation table and the
per-adapter time budgets: any table whose semantics both backends must share lives in
shared/, never inline in a language file. Timestamp parsing is pinned by
shared/fixtures/timestamps.json, which both languages run — note its iso cases are checked
against parseFeedTs(t,'UTC') and deliberately not Date.parse, because per the ES spec a
date-time with no offset is machine-local, which is the exact bug class being guarded.
Both backends serve the same 15 routes. Everything is GET unless noted.
| Route | Returns |
|---|---|
/api/aircraft?lat&lon&radius |
Live ADS-B (adsb.lol → adsb.fi fallback, serve-stale) |
/api/tle?group= |
Satellite elements (CelesTrak, 6 h cache) |
/api/flightinfo?callsign&icao24 |
Registry / route enrichment (adsbdb) |
/api/atc · /api/atc/{feed} |
The 8 tower feeds; the second streams audio same-origin |
/api/weather?lat&lon |
Cloud cover, visibility, wind (Open-Meteo) |
/api/incidents?lat&lon&radius |
Fused Ground/City events + per-source health |
/api/cameras?lat&lon&radius |
Public camera catalogue + per-source health |
/api/vessels?lat&lon&radius |
AIS contacts (keyed, empty without AISSTREAM_KEY) |
/api/camimg/{id} |
Camera image proxy — resolves id against the server's own catalogue, never a caller-supplied URL |
/api/sources |
The full adapter catalogue with enabled / keyed / optin |
/api/geolocate |
Coarse city-level fix from your IP — see Privacy |
/api/prefs (GET/POST) |
Free-form preference blob: observer location, domains, layers, last view |
/api/tile/{style}/{z}/{x}/{y} |
Basemap tile proxy |
/api/health |
{ok:true} |
/api/prefs is stored server-side (%APPDATA%\Vantage\prefs.json on Windows,
~/.vantage/prefs.json elsewhere) and out-ranks localStorage on boot, so your location
survives a port or origin change. An explicit ?lat=&lon= in the URL out-ranks both. Delete
that file to reset.
Vantage has no account, no telemetry, no analytics and no crash reporting. It never uploads your location. Everything it draws is public data fetched about a place you chose.
One route is different, and it is the only one: /api/geolocate. It exists because the
desktop shell renders in WebView2, which denies the browser Geolocation API unless the host app
grants the permission — so "Use my location" would otherwise dead-end. When you press that
button and the device declines to give a fix, the backend asks ipwho.is, falling back to
ipapi.co, roughly "where is this IP?". The request carries this machine's public IP,
because that is what those services answer from. No account, no identifier and nothing about
you is attached, the answer is city-level (~25 km), and it is cached 30 minutes.
- It only fires on an explicit press of Use my location. Nothing calls this route at startup.
- The UI says so, at the control itself — not only here.
- To refuse it entirely, set
VANTAGE_NO_NET_GEO=1. The route then returns503without contacting anyone, and the app asks you to type coordinates instead. Honoured by both backends.
Separately from the route above, the very first time you run Vantage — before you have set a location and before you have touched anything — it calls the browser Geolocation API, and Windows shows "Vantage wants to know your location". This is the OS asking on the app's behalf; the answer never leaves your machine.
Say Block and nothing breaks: the app falls back to its default observer (New York City, 40.7128 / −74.0060) and tells you to set your position under Your location. It never asks again once a location is saved.
Why it asks at all: this is a sky-and-city app, and every single thing it draws — which aircraft are overhead, which satellites pass, where the Sun is, which city feeds apply — is meaningless without knowing where you are standing.
Every other outbound request is to a public data feed and is about the observed location, not about you. Third-party feeds naturally see the connection's IP, as with any HTTP client; the opt-in "gray" sources are off by default and listed in the feeds panel.
The app embeds a native Rust proxy on 127.0.0.1:47615 and opens its window there,
so the same UI runs with no Node process. Closing the window minimises to the tray;
quit from the tray menu. See src-tauri/README.md for the
architecture, icon regeneration (npm run icon), and the auto-updater signing model.
On first load it asks for your location (or defaults to New York). You can also type a lat/lon/altitude in the panel.
- Drag to look around the dome · scroll to zoom (telescope-style FOV).
- Toggle the Aircraft / Satellites / Planets layers in the panel.
- Hover an aircraft or planet for live details (callsign, altitude, speed, etc.).
- A real night sky — 8,900+ naked-eye stars from the HYG catalogue, placed by their true RA/Dec and rotated into your local sky using your latitude and the live sidereal time. Star colours come from real B–V colour indices; sizes from magnitude; they twinkle, the brightest get a soft glow + diffraction glint, and bright stars can be labelled.
- The Milky Way — a procedural band painted on the true galactic plane, so it arcs across the dome and wheels overhead exactly as the real one does for your place and time (with the brighter bulge toward the galactic centre in Sagittarius).
- Meteors, true to the date and place — the major annual showers (Perseids, Geminids, Quadrantids, Lyrids, …) with their real radiants rotated into your local sky and activity that ramps around each shower's peak date; shower meteors stream out of the radiant, sporadics fall toward the horizon. Quiet between showers, busy on peak night — just like the real sky.
- 3D aircraft — each plane is the real reported flight as a per-type 3D model, lit by the real Sun position, oriented along its true track, climbing/descending per its vertical rate, trailing a fading contrail. Overhead in the ceiling view it swells into a dramatic low flyover so you can read the airframe and livery.
- Flight info (toggle) — aircraft type, registration, operator, and route (origin → destination) pulled live from adsbdb.
- Satellites glowing at their true look-angles, a phase-accurate Moon (a real Sun-lit sphere with the correct crescent/gibbous terminator + earthshine), and the Sun + planets at accurate positions.
- A sky that matches the hour — physically-flavoured twilight with a sunset glow and the pink Belt of Venus opposite the Sun, real cloud decks driven by live weather, and a day/night exposure + lighting cycle.
- Cinematic rendering — ACES tone mapping, bloom, environment reflections on the metal, and a soft-edged 180° fisheye dome with a warm horizon glow.
| Layer | Source | How |
|---|---|---|
| Aircraft | adsb.lol (adsb.fi fallback) | Live ADS-B near you, polled every ~4 s, dead-reckoned between polls for smooth motion |
| Flight info | adsbdb | Callsign → route, ICAO24 → aircraft type (enriched on demand, cached) |
| Satellites | CelesTrak | TLEs propagated locally with SGP4 (satellite.js) |
| Planets/Sun/Moon | (none) | Computed locally from ephemerides (astronomy-engine) |
| Stars | HYG catalogue | Public-domain; baked once into public/data/stars.json via scripts/build-stars.mjs |
The proxy fetches and caches these upstreams to dodge CORS and rate limits. The web
build uses a tiny Node proxy (server/index.js); the Windows app uses an equivalent
native Rust proxy (src-tauri/src/proxy/) — same routes, units, and cache windows, all
free and no-key.
public/data/stars.json is already committed. To regenerate (e.g. a different
magnitude limit), download the HYG CSV and run:
node scripts/build-stars.mjs hygdata_v41.csv public/data/stars.json- Aircraft: reported lat/lon/alt → ECEF → local ENU → azimuth/altitude
relative to you (accounts for Earth curvature). See
public/js/coords.js. - Satellites: TLE → SGP4 propagation → look angles (az/el/range) from your ground station.
- Planets: of-date RA/Dec with aberration → refracted horizontal coordinates.
Each object's direction (az/alt) is physically exact. Distances are compressed onto nested dome shells so an 8 km plane and a 500 km satellite are both legible; that radius is a display choice, not a claim about scale.
server/index.js web proxy + static host (ADS-B, CelesTrak, weather, ATC, caching)
src-tauri/ native Windows app: Tauri shell + Rust proxy (mirrors server/index.js)
public/vendor/ locally-vendored libs (three, satellite.js, astronomy-engine) — offline
public/index.html page + import map (local vendor paths, no CDN)
public/js/coords.js geodetic <-> az/alt <-> dome geometry
public/js/sky.js horizon, cardinal markers, alt/az grid, twilight backdrop
public/js/stars.js HYG star field + the Milky Way
public/js/meteors.js date/location-accurate meteor showers + sporadics
public/js/planets.js Sun, phase-accurate Moon, planets (astronomy-engine)
public/js/satellites.js TLE + SGP4 (satellite.js)
public/js/aircraft.js live planes (ADS-B) + dead reckoning + ceiling low-flyover
public/js/clouds.js live-weather cloud decks
public/js/fisheye.js 180° fisheye dome-master projection
public/js/ceiling-brush.js paint-to-reveal custom ceiling-shape mask
public/js/radar.js tactical top-down PPI scope (tracks, rings, sweep, track list)
public/js/city.js Ground/City domain map (incidents, hotspots, cameras, live aircraft, hover ID)
public/js/hotspots.js client-side hotspot engine (weighted fixed-grid density) for the city view
public/js/upscale.js optional WebGL2 GPU upscaler for the camera stills (bicubic + adaptive sharpen)
public/js/classify.js aircraft service classification (civ / law / mil / EMS) — see scripts/classify-test.mjs
public/js/sat-worker.js off-main-thread SGP4 propagation (module worker; sync fallback in satellites.js)
server/sources/ pluggable Ground/City feed adapters (CAD/911, 311, NWS, USGS, cameras; opt-in gray)
public/js/atc.js live ATC audio (LiveATC) — single-stream resolver + voice activity
public/js/towers.js nearby ATC facilities placed on the horizon
public/js/dashboard.js "Arrange" mode — drag/resize widgets per view
public/js/ui.js the ONE command menu: view switcher + per-view sections, location, clock, info
public/js/main.js scene, camera, controls, render loop, WebXR
- Service colour-coding — 🔵 blue = law enforcement, 🟢 green = military, 🔴 red = EMS/fire, white = civilian. Classified from the adsbdb operator/owner/route plus the US-military ICAO24 hex range (see
public/js/classify.js). - Helicopters render as a helicopter model (rotor, tail boom, skids), not an airliner.
- Status-driven nav lights (
public/js/navlights.js) — steady red (left) / green (right) / white (tail) position lights, a flashing red anti-collision beacon, white wingtip strobes, and landing lights that switch on only at low altitude (approach/departure), just like the real procedure. On by default.
-
Tactical radar (top-down) — a flat geographic scope (Web Mercator, pan/zoom, optional free satellite/terrain basemap), your position centred, live aircraft plotted at their true lat/lon with range rings, a rotating sweep, MIL-STD-2525-flavoured affiliation tracks, and a live track list + per-track detail. The status strip reports honest ADS-B link health — no classification theatre.
-
Live ATC audio — streamed free from LiveATC.net and proxied same-origin. One voice at a time (hovered › kept › auto-overhead) with an "on air" indicator from a WebAudio voice-activity detector. Comms follow you — change your location and out-of-range feeds drop, so you always hear the sky above where you actually are.
Coverage, stated plainly: 8 US tower feeds — KATL, KDAL, KDTW, KEWR, KJFK, KLAX, KLGA, KSFO — and a facility is only claimed within 280 km of you. These are tower frequencies, not approach or centre, and a tower frequency is not a specific cockpit: you hear the facility working the flight, not the aircraft you clicked. Outside those metros the panel says there is no feed in range, because there isn't one.
-
Distress + air-incident log — aircraft squawking 7500 (hijack), 7600 (radio fail) or 7700 (emergency) surface in a distress panel (shown in every view) and are self-logged into a per-UTC-day incident list, since no free public air-incident feed exists.
-
Arrange mode — drag / resize / show-hide any on-screen widget on a snap grid (hit Arrange layout, or press E). Layouts save per view family, not per view: the three dome views (ceiling / fisheye / free) share one arrangement, while radar and city each keep their own.
Vantage is a multi-domain common operating picture. Alongside the Air/Sky domain it now
has a Ground / City view (the City button, or ?display=city): a top-down geographic
map of what's happening on the ground right now, fusing many free, public feeds into live
hotspots, an event list, and clickable public cameras.
- Live incidents — official CAD/911 dispatch + crime + 311 open data (Seattle Fire, SF Police/Fire real-time, DC MPD, Chicago, Cincinnati, NYC), normalized into one Event model and classified by kind (fire · medical · police · traffic · hazard · civic).
- Los Angeles — LA publishes no real-time police/fire dispatch, so the LA picture is
assembled from the best sources that do exist, each labelled with its real precision
and lag (nothing is presented as fresher or finer than it is):
- CHP CAD (
chp-cad) — the one genuinely real-time dispatch feed for the region.cad.chp.ca.govis an ASP.NET page, not an API, so it's driven like a browser (viewstate → POSTLACC→ parse). The list carries no coordinates, so each incident is placed at its dispatching CHP area-office centroid — the precision the data has. - Caltrans LCS/CMS — live lane closures + message signs (the real-time road backbone).
- LAPD calls for service (
lapd-calls) — closest thing to LAPD dispatch, but it runs ~5–7 days behind and has no coordinates, only the LAPD geographic division, so calls sit at their division centroid and are ranked as background, not "happening now". - LAPD news/alerts RSS (
lapd-news) — official press releases; near-real-time for major incidents, dated from the feed's ownpubDate(RFC-822) so old posts read as old.
- CHP CAD (
- Airspace disruption (
faa-nas) — FAA NAS status: ground stops, ground-delay programs, arrival/departure delays and airport closures, live. Closure entries are NOTAMs, so the embedded window is parsed — a standing year-long restriction reads as old and is flagged (standing restriction) rather than posing as breaking news. Works in any major US metro. - Air→ground correlation (
airwatch) — a helicopter holding a low, tight orbit is one of the most reliable live cues that something is happening beneath it (police perimeter, fire, medical LZ), and it's derived from the ADS-B feed the Air domain already consumes. Reported as an inferred indicator, never a confirmed incident, and only the aircraft's position is used — never a person. - Hazards & natural events — NWS alerts, USGS quakes + volcanoes, NWS storm reports (IEM), NASA EONET, GDACS global disasters, and NWPS flood gauges — all free, no key.
- Public cameras — officially-published DOT cameras (Caltrans CWWP2, NYC DOT,
FL511, TfL JamCams) plus ALERTCalifornia (UC San Diego): ~2,100 public PTZ
wildfire cameras statewide, ~160 around LA. Co-located feeds group into one site that opens
a live 2-up grid, each cell with its own freshness/offline state, and cameras reporting a
PTZ bearing draw a view-cone showing where they're actually looking. All images are
served through
/api/camimg(never hotlinked), with the last-good frame kept on a blip. No private/unsecured cameras — ever. - Live aircraft on the ground map — the city view is an all-domain picture, so the same
ADS-B traffic the dome flies is plotted here too, as recognisable airframes rather than
generic markers: fixed wing (fuselage + swept wings), helicopter (rotor disc + tail
boom) and drone/UAV (quad-X), each rotated to its real track. Affiliation is colour and
frame shape, so it reads in mono or peripherally — military green/square, law
blue/rectangle, EMS · fire · rescue red/diamond; civil traffic stays plain and unframed
because it's the majority and framing it would bury the movements that matter. A low
helicopter holding station gets a pulse ring — the same cue
airwatchreasons about. - Hover to identify — every object on the map says what it is: aircraft give affiliation, airframe + type, registration, registered-to owner, operator, route and altitude (looked up on demand, and it says "no registry match" or flags a relayed TIS-B target rather than pretending it's still loading); cameras give feed count + provider; incidents give kind, severity, age and which feed reported them.
- Layers where you're looking — a Google/Apple-Maps-style layers box on the map (click-away or Esc) carrying aircraft affiliation (Government vs Civilian), every incident kind with its legend colour (crime/police, traffic, fire, medical, wildfire, hazard, weather, quake, events/311, outage, social) and the camera/view-cone/heat overlays. It is the same state as the command panel — change either and the other follows.
- Vessels (WATER domain) — AIS contacts drawn as plan-view hulls rotated to course: bulk/tanker, passenger, tug/fishing, and fast/naval silhouettes, with military reusing the same green as military aircraft so affiliation means one thing in every domain. Course over ground is used while making way and true heading at rest; with neither reported the contact is drawn as a plain circle rather than a hull pointing somewhere nobody claimed. Hover gives MMSI, callsign, speed, course-vs-heading when they differ, and destination — labelled as crew-entered, because on AIS it is. Keyed and off by default.
- Hotspots — a client-side weighted fixed-grid density engine (
public/js/hotspots.js) ranks activity by severity × recency × density, so a working fire from minutes ago outweighs a hundred day-old calls. Events are binned into fixed cells and each cell sumsseverityWeight · exp(-age/halfLife); it is a weighted histogram, deliberately not a smoothed kernel estimate. The heat overlay + "top hotspots" board update on every poll, not per frame. - Honest feed health — a per-source status list shows which feeds are live, empty, or offline right now (the same no-theatre ethos as the radar link-health strip).
App-wide preferences live in a Settings dialog (the gear in the panel header), separate from the per-view controls in the command panel:
- GPU camera upscaling — Off / 2× / 4×. Agency cameras publish small JPEGs (many 320×240), and the browser's default stretch is plain bilinear. This reconstructs the frame on the GPU instead (WebGL2: Catmull-Rom bicubic + contrast-adaptive sharpening, clamped to the local range so it can't ring or amplify JPEG blocking). It falls back to the original image if WebGL2 is missing — and says so in the dialog rather than offering a switch that silently does nothing.
Feeds auto-activate by location (live CAD/911 where a city publishes it). Adding a feed is
one file: each source is a small adapter in server/sources/* that turns an upstream
record into the shared Event/Camera model and degrades to empty on failure — one dead
feed never breaks the map. Every adapter — keyless, free-key and opt-in alike — is
mirrored feed-for-feed in the native Rust proxy (src-tauri/src/proxy/sources/) — the proxy
the app actually ships. The Node adapters are the reference implementation the shipped ones are
diffed against. /api/incidents, /api/cameras, /api/camimg, /api/vessels and
/api/sources are guarded by scripts/contract-smoke.mjs for shape (same source catalog, same
enabled state) and scripts/parity-live.mjs for values.
Snap Map, Citizen, PulsePoint, scanner and social have no official free (mappable) feed, so
they're opt-in, pluggable adapters — enable with VANTAGE_ENABLE_CITIZEN=1,
VANTAGE_ENABLE_SNAPMAP=1, VANTAGE_ENABLE_PULSEPOINT=1 (+ VANTAGE_PULSEPOINT_AGENCIES=…),
VANTAGE_ENABLE_SCANNER=1 (+ VANTAGE_SCANNER_SYSTEMS=…), or VANTAGE_BLUESKY_QUERY="<place>"
(a free, unauthenticated Bluesky search). They stay strictly place/event-centric:
aggregate activity at a location, never a person.
PulsePoint is currently unavailable, by upstream change. It used to publish live fire/EMS dispatch in the clear; as of a 2026-07-25 check
web.pulsepoint.org/DB/giba.phpno longer returns that payload and the viewer has moved toapi.pulsepoint.org/v1, which answers 401 without credentials. The adapter is kept registered (so the catalog and Node/Rust parity are unchanged) and reports that reason honestly in the feeds panel — it is not a decryption bug, and we don't bypass authentication.
Trivially-free keys unlock national/global breadth. Each stays off until its key is set;
GET /api/sources (and the City feeds panel) shows every feed's state — live, key, or opt-in.
| Feed | Adds | Env var | Where |
|---|---|---|---|
| EPA AirNow | Air-quality (AQI) hazards, US | AIRNOW_KEY |
airnowapi.org |
| NASA FIRMS | Wildfire thermal hotspots, global | FIRMS_MAP_KEY |
firms.modaps.eosdis.nasa.gov |
| Windy Webcams | Global public webcams | WINDY_KEY |
api.windy.com |
| WSDOT | Washington traffic alerts + cameras | WSDOT_KEY |
wsdot.wa.gov |
| 511 SF Bay | Bay Area traffic incidents / closures | FIVE11_SF_TOKEN |
511.org/open-data |
| Ticketmaster | Public events (crowd context) | TICKETMASTER_KEY |
developer.ticketmaster.com |
| Socrata | (optional) lifts the anonymous rate limit | SOCRATA_APP_TOKEN |
portal dev settings |
| aisstream.io | Live vessels (WATER domain) — AIS positions | AISSTREAM_KEY |
aisstream.io |
Keyless additions shipped this phase (no key needed): FL511 (Florida DOT cameras) and
TfL JamCams (London traffic cameras — an optional TFL_APP_KEY just lifts the rate).
GET /api/vessels?lat&lon&radius returns normalized AIS contacts (MMSI, name, callsign,
course/heading/speed, ship type, destination). It is keyed and off by default, and that
is a data-availability fact, not a preference: there is no keyless real-time AIS. NOAA and
the USCG publish only historical bulk CSV/GeoParquet, and aisstream.io — the one free live
feed — requires a (free) key, speaks WebSocket only, and blocks browser CORS.
That makes it the only streaming source in an otherwise request/response codebase, so it
gets a deliberately different internal shape: one background task owns a single WebSocket and
maintains an in-memory MMSI → last position map; the route just serves a snapshot of it, so
the adapter contract stays identical to every other feed. Contacts that stop reporting are
dropped after 12 minutes, not held — a silent vessel is genuinely out of range or has its
transponder off, and drawing its last position would be inventing a track.
- Public / authorized feeds only — official agency feeds, legal aggregators, open APIs. Never unauthorized access to private or unsecured cameras, never auth-bypass or ToS-circumvention. The camera-image proxy resolves an id against the server's own catalog, never a caller-supplied URL (no open proxy / SSRF surface).
- Places & events, never persons — the picture is of the city (incidents, hazards, hotspots), not any individual. Social/place-heat sources are ingested as aggregates only.
Vantage is a docked command-and-control shell: a persistent global command bar across the top (brand · DOMAIN · VIEW · alerts · ATC transport · feed health · clock), a left rail holding the command panel, a right rail for the live detail stack, and a bottom tray for the flightboard. The map canvases stay full-bleed underneath, so the chrome frames the picture instead of floating over it in fixed positions.
Domain and view are separate axes. View is how you look (ceiling / fisheye / free / radar / city); domain is what you track — GROUND, WATER, AIR — and you can run more than one at once. A domain the current view can't render is shown disabled, not hidden, and clicking it switches to the view that can: pick what you want to see, and the system picks the display. Domains gate drawing, not just controls, and persist across launches.
Shared controls (aircraft service filter, ATC, the air-incident log, your location) stay one click away everywhere; view-specific controls appear only where they apply — dome layers / star labels / projection tuning / ceiling-shape paint in the projector views, range rings / basemap / sweep in radar. Pick a view in the bar, or via URL for kiosk auto-launch:
| View | Use | URL |
|---|---|---|
| Ceiling skylight | Projector roughly above the viewer; a round window of overhead sky, zenith-centred — correct for a flat roof | ?display=ceiling |
| Fisheye dome 180° | True dome master — projector straight up at a flat ceiling; zenith = centre, horizon = disc edge (correct on a curved planetarium dome) | ?display=fisheye |
| Free look | A normal screen — drag to look around | ?display=free |
| Tactical radar | Top-down geographic PPI scope: ownship centred, North up, live tracks by real lat/lon, range rings, sweep, MIL-STD-2525 tracks + track list | ?display=radar |
| City activity | Ground/City common-operating-picture: live incidents, hazards, hotspots + public cameras on a geo map, live aircraft overhead and (keyed) AIS vessels, with a per-source feed-health readout | ?display=city (e.g. &lat=37.78&lon=-122.42) |
- North at top slider aligns the projection to your room (
&north=90). - Fullscreen / kiosk slims the UI — it does not hide it. The command panel stays,
reduced to its essential sections, and the compass, info card and pass strip remain. Add
&kioskto the URL to start that way; collapse the panel separately if you want the rail gone entirely. - Example projector launch:
http://localhost:3000/?display=fisheye&north=120&kiosk
Every URL parameter the app reads:
| Param | Effect |
|---|---|
?display= |
ceiling · fisheye · free · radar · city (table above) |
?lat= ?lon= |
Observer position. Out-ranks both localStorage and the saved prefs file |
?alt= |
Observer altitude in metres (default 10) |
?north= |
Rotate the projection so this compass bearing is at the top of the image |
?span= |
Ceiling/fisheye only — width of the sky cone mapped to the disc, in degrees. Larger = more sky, smaller objects |
?skyonly= |
Draw the sky alone: no ground plane, no horizon furniture. For projecting onto a ceiling where the lower half would spill onto walls |
?kiosk |
Start with the UI slimmed (see above). Value ignored — presence is enough |
Keyboard / pointer shortcuts:
| Input | Does |
|---|---|
E |
Toggle Arrange layout mode (ignored with Ctrl/Alt/Cmd, or while typing in a field) |
Esc |
Close the Settings dialog, or the City layers menu |
← / → |
Nudge north at top by 1° — only while the compass is editable, and not while a field has focus |
| Wheel over the compass | Nudge north by 1° · hold Shift for a coarser 5° step |
- Ceiling shape (paint mask) — real ceilings aren't clean rectangles (beams, crown molding, a round medallion, an alcove). Under Display / Projection → Ceiling shape, turn on Custom ceiling shape, hit Paint, and brush directly on the projected image to Reveal sky in your ceiling's true shape (or Black out the spill onto the walls). The painted mask is saved automatically, so a kiosk keeps it across reloads. Hit Paint again to stop painting and look around.
The whole scene is one continuous draw loop, tuned to stay smooth on a 24/7 projector kiosk:
- The 180° fisheye dome renders only the 5 cube faces it actually samples — the down-facing face is never seen at ≤180°, so it's skipped (~17% off the dome's GPU cost, with zero change to any pixel).
- The cloud shader uses a trimmed multi-octave noise (≈half the samples) since the dome re-renders the scene per cube face.
- Satellites upload only the points currently above the horizon; the ephemeris (Sun/Moon/planets) solves at ~1 Hz; per-frame allocations in the hot paths are zero.
Each of these is measurable with the browser's own profiler against a 24/7 kiosk session; the working notes behind them are kept out of the repo.
Vantage ships as a Windows desktop app; headsets are not a supported surface and are not tested against a release. The WebXR path below works when the frontend is driven from the development server, and is kept because it costs nothing to keep.
Open the dev server in the Quest browser and use the on-screen buttons:
- ENTER VR — full surrounding planetarium dome.
- START AR — passthrough overlay: the real room stays visible (the opaque sky dome + ground are hidden) while live aircraft, satellites, planets, and stars float in your actual space.
WebXR is only enabled while a session is active, so desktop/projector rendering is unaffected.
Per-type aircraft and satellite models are self-hosted in public/models/,
sourced from Poly Pizza under CC-BY 3.0 (plus a low-poly ISS). Attribution:
"Airplane" (airliner, also used for bizjets), "Satellite", low-poly "International Space Station" — Poly by Google
"Boeing 747" — Miha Lunar
"Jet" (fighter) — jeremy
"Helicopter" — jeremy
"Small plane" (cessna) — Eik Røgeberg
all via Poly Pizza (https://poly.pizza), licensed under CC-BY 3.0
Aircraft are matched to a model by ICAO type code (see modelKeyFor in
public/js/aircraft.js): airliner, 747 jumbo, business jet, GA prop, helicopter,
or stealth fighter (military). Satellites use a generic comms-sat model, with the
ISS getting its own model. public/model-view.html is a dev page that lays out
every model for orientation calibration.
Supported: Windows, as the native app (src-tauri/). That is the product.
The frontend is a plain web app, so it also renders anywhere a modern browser does. Those paths are development conveniences, not supported deployments — they are not release-tested:
- Projector / kiosk — Chromium fullscreen at
localhost:3000against the dev server; point it straight up for the ceiling dome. - Oculus / WebXR — the in-page VR button on a headset browser (see above).
- Roku — a separate native SceneGraph/BrightScript channel lives in
roku/(Roku has no WebGL, so it re-implements the radar view against the same coordinate math). Seeroku/README.md.
Vantage © 2026 ReconGrunt — CC BY-NC-SA 4.0
(SPDX-License-Identifier: CC-BY-NC-SA-4.0)
Rework it, learn from it, build on it — just credit the author and don't sell it.
| ✅ Allowed | Use it, study it, run it. Modify / adapt / remix it. Share it and share your reworked versions. |
| 📝 BY — credit required | Give appropriate credit to ReconGrunt, link back to this repo and the license, and state what you changed. |
| 🚫 NC — no commercial use | Do not sell it, bundle it into a paid product or app, or otherwise monetise it or anything derived from it. |
| 🔁 SA — same license | Any adaptation must be released under CC BY-NC-SA 4.0, so these terms can't be stripped off downstream. |
Two things worth being precise about:
- This is "source available", not OSI "open source". The NonCommercial term is by definition not an open-source license. That's deliberate — it's the only way to permit reworking while barring resale.
- Third-party material is NOT relicensed by this. The vendored libraries (three.js, satellite.js, astronomy-engine — all MIT), the CC-BY 3.0 3D models, the public-domain star catalogue and every live data feed keep their own terms. See THIRD-PARTY-NOTICES.md; honour those alongside this license.
The NonCommercial restriction binds everyone who receives the work — it does not bind the copyright holder. ReconGrunt retains full commercial rights and may sell, dual-license or relicense Vantage at any time (relevant to the planned paid Roku channel, which is unaffected).
For commercial use, ask.