Skip to content

ReconGrunt/Vantage

Repository files navigation

Vantage

License: CC BY-NC-SA 4.0

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.

Why there is a second backend in this repo

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_ms silently discarded a trailing ±HH:MM, so every NWS alert was stamped seven hours early. Nine call sites went through it.
  • The NWS expires field 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.

Tech stack — and why it's the right one

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.

Install

Download Vantage_<version>_x64-setup.exe from the latest release. It installs per-user (no admin prompt) and updates itself from then on.

Windows will warn you, and here is exactly why

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 SHA256

Compare it against the release's SHA256SUMS.txt. If they differ, do not run it.

Updates are signed, separately and always

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.

If it crashes

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.

Run it

Vantage for Windows (the app)

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/

Development server (not a deployment)

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:3000

Tests

npm 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 VALUES

smoke: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.

How the two backends are kept in step

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.

API

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.

Privacy

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 returns 503 without contacting anyone, and the app asks you to type coordinates instead. Honoured by both backends.

On first launch, Windows will ask for your location

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.).

What it shows

  • 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.

Data sources (all free, no API key)

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.

Rebuilding the star catalogue

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

How positions work

  • 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.

Project layout

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

Aircraft realism

  • 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.

Situational-awareness tools

  • 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.

Ground / City domain — the all-domain picture extends to the street

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.gov is an ASP.NET page, not an API, so it's driven like a browser (viewstate → POST LACC → 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 own pubDate (RFC-822) so old posts read as old.
  • 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 eventsNWS 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 airwatch reasons 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 sums severityWeight · 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).

Settings

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 upscalingOff / 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.

Opt-in sources (default OFF)

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.php no longer returns that payload and the viewer has moved to api.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.

Free-key feeds (set an env var to activate)

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).

WATER domain — live vessels (AISSTREAM_KEY)

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.

Guardrails (non-negotiable)

  1. 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).
  2. 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.

One command shell, every view

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 &kiosk to 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.

Realtime performance

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.

VR & passthrough AR (Oculus Quest) — experimental, unsupported

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 ARpassthrough 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.

3D model credits (CC-BY 3.0)

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.

Where it runs

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:3000 against 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). See roku/README.md.

License

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.

About

Vantage — open source all-source intelligence: a real-time common operating picture fusing free public feeds into one live picture of your sky and your city.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages