Worldview is a tactical intelligence 3D globe viewer built with Next.js 16.2.0, React 19.2.4, CesiumJS (via Resium), and Tailwind CSS v4.
npm run dev # start dev server on http://localhost:3000
npm run build # production build
npm run start # run production server
npm run lint # ESLint (eslint-config-next/core-web-vitals + typescript)
npm run proxy # optional standalone Express proxy on http://localhost:3001
npm run copy-cesium-assets # manually sync Cesium assets to public/cesium (also runs on postinstall)app/ # Next.js App Router — entry points only
layout.tsx # Root layout (Geist fonts, h-full body)
page.tsx # Main page — mounts GlobeViewer dynamically (ssr: false)
globals.css # Tailwind v4 import + wv-* design tokens
api/
flights/route.ts # Global flights (FR24 primary, adsb.fi fallback) + route registry seed
flights/live/route.ts # Regional adsb.fi feed (lat/lon/dist query)
satellites/route.ts # TLE proxy (primary API + CelesTrak fallback)
ships/route.ts # AISStream burst collection + moving filter
cctv/route.ts # Multi-source CCTV aggregation (TfL, Austin, NSW)
cctv/image/route.ts # CORS-safe image proxy with short TTL cache
geolocation/route.ts # IP geolocation fallback endpoint
traffic/roads/route.ts # Road network endpoint (Overpass + static Sydney fallback)
health/route.ts # Lightweight health/uptime probe
src/ # All application source (NOT inside app/)
globe/
GlobeViewer.tsx # Cesium <Viewer> root; ShaderManager inner component
EntityClickHandler.tsx # drillPick → entity classification → camera tracking
hooks/ # Data/utility hooks (telemetry hooks accept `enabled: boolean`)
useFlights.ts # Global aircraft (20 s poll → /api/flights)
useFlightsLive.ts # Regional aircraft (5 s poll → /api/flights/live, adsb.fi)
useSatellites.ts # TLE propagation via satellite.js (→ /api/satellites)
useShips.ts # AIS vessels (30 s poll → /api/ships)
useEarthquakes.ts # USGS direct fetch (60 s poll, no proxy)
useTraffic.ts # Road network + animated vehicles (→ /api/traffic/roads)
useCameras.ts # CCTV feeds (5 min poll → /api/cctv)
useGeolocation.ts # Browser GPS → IP fallback (→ /api/geolocation)
useAudio.ts # Wraps tacticalAudio singleton from src/lib/audio.ts
useIsMobile.ts # matchMedia breakpoint (768 px)
lib/server/
flightRouteRegistry.ts # In-memory route metadata bridge: /api/flights -> /api/flights/live
layers/ # Cesium layer renderers (children of <Viewer>)
FlightLayer.tsx # BillboardCollection + LabelCollection + PolylineCollection
ShipLayer.tsx # Same imperative pattern as FlightLayer
SatelliteLayer.tsx # Mix: React <Entity> per satellite + useCesium() for occluder
EarthquakeLayer.tsx # React <Entity> with memo() — small dataset, declarative OK
CCTVLayer.tsx # BillboardCollection for camera icons
TrafficLayer.tsx # PolylineCollection (roads) + PointPrimitiveCollection (vehicles)
shaders/
postprocess.ts # GLSL strings for CRT / NVG / FLIR PostProcessStage
types/
camera.ts # CameraFeed, CameraApiResponse, CameraCountry
ui/ # HUD overlay components (all purely client-side)
OperationsPanel.tsx # Layer toggles, shader picker, map tile switcher
IntelFeed.tsx # Scrolling event log (flight/seismic/satellite/cctv/ship)
StatusBar.tsx # UTC clock, camera lat/lon/alt/heading
TrackedEntityPanel.tsx # Entity details when tracking (aircraft/ship/satellite/…)
CCTVPanel.tsx # Camera thumbnails via /api/cctv/image proxy
SplashScreen.tsx # Boot sequence with typewriter effect + audio cues
MobileModal.tsx # Full-screen modal for compact viewports
AudioToggle.tsx # Mute/unmute button
Crosshair.tsx # Centre reticle overlay
FilmGrain.tsx # Canvas-based analogue noise at ~12 fps
server/
.env # Server-side API credentials (see Environment Variables)
@/* resolves to the project root, not src/. Files inside src/ use relative imports between each other (e.g. ../../hooks/useFlights). Only import from @/src/... when crossing from app/ into src/.
- Always dynamic-import
GlobeViewerinapp/page.tsxwith{ ssr: false }— Cesium requireswindow. - Layer components (
FlightLayer,ShipLayer, etc.) must be rendered as children of<Viewer>souseCesium()works. - Access the raw viewer via
const { viewer } = useCesium()inside any component that lives inside<Viewer>. EllipsoidalOccluderexists at runtime but is absent from Cesium's TypeScript declarations — cast it:const EllipsoidalOccluder = (Cesium as any).EllipsoidalOccluder as new (...) => ....
For large datasets (flights, ships, CCTV, traffic), do not use React <Entity> components — they freeze the UI at ~1,000+ objects. Use imperative Primitive Collections:
// ✅ correct — imperative, bypasses React reconciliation
const { viewer } = useCesium();
useEffect(() => {
const billboards = new BillboardCollection();
viewer.scene.primitives.add(billboards);
// populate with billboards.add(...)
return () => { viewer.scene.primitives.remove(billboards); };
}, [viewer, data]);
// ❌ wrong for large datasets
data.map(item => <Entity key={item.id} ... />)EarthquakeLayer (typically < 200 entities) is the only layer that uses declarative <Entity> + memo().
Telemetry hooks (useFlights, useSatellites, useShips, useEarthquakes, useCameras) typically:
- Take
enabled: booleanand clear state/return early whenfalse. - Return domain data plus
feedItems: IntelFeedItem[]forIntelFeed. - Import
IntelFeedItemfrom../ui/IntelFeed.
Polling strategy is hook-specific in the current codebase:
setTimeoutrecursion with exponential backoff:useFlights,useFlightsLive,useShips.setIntervalcadence:useEarthquakes,useSatellites,useCameras.useTrafficis on-demand by camera bbox and returns{ roads, vehicles, loading, shouldFetchRoads }(nofeedItems).
Polling intervals: flights 20 s · flights-live 5 s · ships 30 s · satellites TLE 2 h (positions 2 s) · earthquakes 60 s · traffic on-demand · cameras 5 min.
useFlightsLive only activates when camera altitude < 5,000,000 m or an entity is being tracked. Camera lat/lon is snapped to 0.5° grid to minimise cache misses.
// app/api/<feature>/route.ts
const cache = new Map();
const CACHE_TTL = 30_000;
export async function GET(request: Request) {
const cached = cache.get('key');
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
return NextResponse.json(cached.data);
}
// fetch upstream, cache, return
}Route handlers are implemented for /api/flights, /api/flights/live, /api/satellites, /api/ships, /api/cctv, /api/cctv/image, /api/geolocation, /api/traffic/roads, and /api/health.
Note: traffic clients should call /api/traffic/roads (there is no app/api/traffic/route.ts).
Server-side credentials live in server/.env (not .env.local). They must be read in route handlers via process.env.*. For Next.js route handlers to pick them up, the file must be loaded explicitly or the variables copied to .env.local.
Client-accessible vars need the NEXT_PUBLIC_ prefix. src/globe/GlobeViewer.tsx reads process.env.NEXT_PUBLIC_GOOGLE_API_KEY and process.env.NEXT_PUBLIC_CESIUM_ION_TOKEN.
Optional UI version override: src/ui/StatusBar.tsx reads process.env.NEXT_PUBLIC_APP_VERSION. If unset, next.config.ts derives it from git tags at build time.
Key credentials available in server/.env:
OPENSKY_CLIENT_ID/OPENSKY_CLIENT_SECRET— OpenSky Network OAuth2 (flight data)AISSTREAM_API_KEY— AISStream.io WebSocket API (ship AIS data)NSW_TRANSPORT_API_KEY— Transport for NSW camera API (CCTV AU source)
Uses the v4 API — different from what most training data knows:
/* globals.css — v4 syntax */
@import "tailwindcss"; /* NOT @tailwind base/components/utilities */
@theme inline {
--color-wv-black: #0a0a0f;
--color-wv-cyan: #00D4FF;
/* etc. */
}PostCSS plugin is @tailwindcss/postcss (not tailwindcss directly). Custom wv-* colour tokens (wv-black, wv-cyan, wv-green, wv-amber, wv-muted, wv-text, wv-border, wv-dark, wv-red) are used throughout components but must be declared in globals.css under @theme. The panel-glass utility class is also used and must be defined there.
None currently. Previously missing modules now exist:
| Import path | Notes |
|---|---|
src/lib/audio.ts |
Tactical audio singleton used by useAudio.ts and SplashScreen.tsx |
src/data/airports.ts |
Airport coordinate lookup used by FlightLayer.tsx |
Migration leftovers documented in older status files have been addressed in current source:
- Hooks import
IntelFeedItemfrom../ui/IntelFeed. app/page.tsximports UI/components from@/src/....src/globe/GlobeViewer.tsxusesprocess.env.NEXT_PUBLIC_*(notimport.meta.env).package.jsonincludescesium,resium, andsatellite.js.
src/shaders/postprocess.ts exports GLSL strings (CRT_SHADER, NVG_SHADER, FLIR_SHADER) and a ShaderMode type. Applied via Cesium's PostProcessStage inside ShaderManager (an inner component of GlobeViewer that calls useCesium()). Stages must be removed before adding a new one to prevent accumulation.
EntityClickHandler uses viewer.scene.drillPick() (limit 10) to pierce 3D tiles, then falls back to scene.pick(). Entities are classified by inspecting their description HTML for keywords (callsign/icao24 → aircraft, mmsi/imo → ship, norad → satellite, magnitude/depth → earthquake). Camera follow uses Cesium's viewer.trackedEntity; ESC or clicking empty space calls viewer.trackedEntity = undefined.
IntelFeedItem— defined and exported fromsrc/ui/IntelFeed.tsx. Telemetry hooks returnfeedItems: IntelFeedItem[]. Shape:{ id, time, type, message }wheretypeis one of'flight' | 'seismic' | 'satellite' | 'system' | 'cctv' | 'ship'.