phase 7-8: adversarial-review fixes + README
Applied 4 confirmed findings from the multi-agent review (5th was a documented deliberate deviation): - aircraft: self-healing visibilitychange listener — the live feed no longer stalls permanently after tab hide->show while the clock is paused (MAJOR) - SPEC §9: nginx proxy_pass with $is_args$args needs a resolver or prod 502s while nginx -t still passes — documented + hardened the deploy step (MAJOR) - ships: live-AIS map now evicts true least-recently-updated (move-to-end), not oldest-inserted - ships: corrected inverted Hormuz lane direction labels (tracks with decreasing longitude sail west/inbound, not east) - README: full data-source + attribution table, real-vs-DEMO breakdown, architecture, roadmap Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
960dcd0e69
commit
7da9eeaeb0
96
README.md
96
README.md
@ -1,18 +1,100 @@
|
|||||||
# GODSIGH
|
# GODSIGH ▸ World View
|
||||||
|
|
||||||
A browser-based 3D/4D OSINT world view — CesiumJS digital-twin globe fusing public sensor feeds (satellite orbits, live ADS-B aircraft, AIS shipping, infrastructure vectors) on one scrubbable timeline. Inspired by Bilawal Sidhu's *God's Eye View*.
|
A browser-based **3D/4D OSINT world view** — a CesiumJS digital-twin globe that fuses independent, public sensor feeds onto one scrubbable timeline. Satellite orbits, live aircraft, ship traffic, and energy infrastructure, all synced to a single space-time slider. Inspired by Bilawal Sidhu's *God's Eye View*.
|
||||||
|
|
||||||
**Status: pre-build.** The complete build & deployment instructions live in [SPEC.md](SPEC.md) — start there.
|
Everything runs client-side from free, public, **unclassified** data. No API keys are required. No Cesium ion token. No build step.
|
||||||
|
|
||||||
## Quickstart (dev)
|
---
|
||||||
|
|
||||||
|
## Quickstart
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
python3 serve.py 8137
|
python3 serve.py 8137
|
||||||
# open http://127.0.0.1:8137
|
# open http://127.0.0.1:8137
|
||||||
```
|
```
|
||||||
|
|
||||||
`serve.py` is a zero-dependency static server that also proxies the OpenSky ADS-B API (which blocks cross-origin browser calls) at `proxy/opensky`.
|
`serve.py` is a zero-dependency (stdlib-only) static server that also proxies the two feeds a browser can't call directly. Open the page and the globe boots over the Persian Gulf; give the satellites a few seconds to propagate.
|
||||||
|
|
||||||
## Target
|
## What you're looking at
|
||||||
|
|
||||||
`https://partly.party/godsigh/` — deployment is the final build phase, gated on John.
|
A dark digital-twin globe with the built-in Cesium **timeline + animation widget** along the bottom acting as the space-time slider — scrub it and every time-aware layer moves with the clock (now ±6 hours). Two rendering skins, toggled top-left:
|
||||||
|
|
||||||
|
- **Data Mode** — a flat, high-contrast dark basemap for asset tracking (the default).
|
||||||
|
- **Photo Mode** — Sentinel-2 satellite imagery with a live day/night terminator.
|
||||||
|
|
||||||
|
The HUD lists every layer with a live status line and an on/off toggle, plus a `LIVE / SCRUBBED` chip that shows when you've scrubbed off wall-clock-now (at which point the live-only layers pause to save quota).
|
||||||
|
|
||||||
|
## Layers & data sources
|
||||||
|
|
||||||
|
| Layer | Source | Live? | Notes |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **Satellites** (~97) | [Celestrak](https://celestrak.org) GP/TLE | ✅ deterministic | SGP4-propagated orbits with glowing paths; EO/recon birds incl. WorldView, Pléiades, Capella, SkySat, plus Chinese **Gaofen** and Russian **Cosmos** (Persona) military satellites. Click one for orbital elements. |
|
||||||
|
| **Aircraft** | [OpenSky Network](https://opensky-network.org) ADS-B | ✅ live | Thousands of live flights, colored by altitude band; click one for callsign/country/altitude/speed/heading. |
|
||||||
|
| **Ships** `[DEMO]` | synthetic | — | Illustrative tanker traffic through the Strait of Hormuz, incl. a "toll-route" carrier, a **dark-vessel AIS gap**, and the Fujairah idle anchorage. Optional live AIS — see below. |
|
||||||
|
| **Infrastructure** | static vectors | — | Choke points (Hormuz, Bab el-Mandeb, Suez, Malacca), the East–West Petroline & Habshan–Fujairah pipelines, and key oil/desal/base facilities. Coordinates approximate. |
|
||||||
|
| **Events** `[DEMO]` | synthetic | — | Time-anchored markers that appear as the slider crosses their moment — illustrative placeholders for a future real event feed. |
|
||||||
|
|
||||||
|
### Basemaps
|
||||||
|
- **Data Mode:** © OpenStreetMap contributors © CARTO (dark_all raster tiles).
|
||||||
|
- **Photo Mode:** Sentinel-2 cloudless by [EOX](https://s2maps.eu) — modified Copernicus Sentinel data 2023.
|
||||||
|
|
||||||
|
## What is real vs. DEMO
|
||||||
|
|
||||||
|
**Real, live, public data:** satellites (Celestrak TLEs) and aircraft (OpenSky ADS-B). Satellite positions are computed live via SGP4; aircraft are polled from OpenSky.
|
||||||
|
|
||||||
|
**Synthetic `[DEMO]` data:** ships and events. These are hand-authored to illustrate the *kind* of analysis the platform is built for (dark-vessel detection, choke-point pressure, strike/pass correlation) without claiming to depict a real event. Every demo entity is labeled `[DEMO]` in its name, the HUD, and its info panel.
|
||||||
|
|
||||||
|
> OpenSky's anonymous API has a small daily quota (~400 credits; a global snapshot costs 4). The aircraft layer polls every 90 s only while the tab is focused and the clock is live, and shows the remaining quota in its status line. If you hit the limit it backs off and says so.
|
||||||
|
|
||||||
|
## Optional: live AIS ship tracking
|
||||||
|
|
||||||
|
The demo fleet is the default. To show **real** ship traffic, get a free key from [aisstream.io](https://aisstream.io) and set it in [`js/config.js`](js/config.js):
|
||||||
|
|
||||||
|
```js
|
||||||
|
AISSTREAM_API_KEY: 'your-key-here',
|
||||||
|
```
|
||||||
|
|
||||||
|
On the first live position report the demo fleet hides and real vessels stream in over a WebSocket (no proxy needed).
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
No framework, no bundler — vanilla ES modules plus two CDN globals (CesiumJS, satellite.js).
|
||||||
|
|
||||||
|
```
|
||||||
|
index.html CDN script tags, HUD shell, #cesiumContainer
|
||||||
|
css/style.css dark HUD styling
|
||||||
|
js/config.js all URLs, intervals, camera, colors, keys — one file
|
||||||
|
js/lib.js shared helpers (glyphs, bearing, color bands)
|
||||||
|
js/ui.js HUD: layer rows, status lines, LIVE/SCRUBBED chip
|
||||||
|
js/main.js viewer bootstrap, basemaps, clock, gating, picking, loader
|
||||||
|
js/layers/*.js one module per layer (satellites, aircraft, ships, infra, events)
|
||||||
|
serve.py dev server + same-origin proxy (proxy/opensky, proxy/celestrak)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why the proxy?** OpenSky pins its CORS header to its own domain, so a browser can't fetch it cross-origin. `serve.py` (dev) and nginx (prod) forward `proxy/opensky` and `proxy/celestrak` from the same origin. Every URL in the app is **relative**, so it works unchanged at `/` and at `/godsigh/`.
|
||||||
|
|
||||||
|
**Entities vs. primitives:** satellites, ships, infrastructure, and events are Cesium entities (time dynamics + click InfoBox for free). Aircraft are a `BillboardCollection` primitive — thousands of them, rebuilt each poll — with a custom click overlay.
|
||||||
|
|
||||||
|
## Roadmap
|
||||||
|
|
||||||
|
- **Live AIS** by default (aisstream.io key) — implemented, opt-in today.
|
||||||
|
- **GPS-jamming heatmap** from [gpsjam.org](https://gpsjam.org) daily H3 tiles.
|
||||||
|
- **Historical recording** — a cron job appending OpenSky/AIS snapshots to JSONL for true time-travel beyond the live ±6h window.
|
||||||
|
- **Classified-sat amateur TLEs** (community "classfd" catalogs) for birds Celestrak omits.
|
||||||
|
- **Economic side panel** — Brent / WTI oil futures synced to the timeline.
|
||||||
|
- **Cesium ion terrain & photorealistic 3D tiles** (needs a token).
|
||||||
|
- **Agentic event ingestion** — LLM/agent pipelines that task, scrape, and structure OSINT into the event feed.
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
Target: `https://partly.party/godsigh/` (games VPS, behind the `forum-nginx` container and Cloudflare). Only `index.html`, `css/`, and `js/` ship; `serve.py` is replaced by nginx `location` blocks that proxy OpenSky/Celestrak. See [SPEC.md](SPEC.md) §9 for the exact deploy steps.
|
||||||
|
|
||||||
|
## Attribution & terms
|
||||||
|
|
||||||
|
- **OpenSky Network** — data under their terms; non-commercial use. https://opensky-network.org
|
||||||
|
- **Celestrak** — GP/TLE orbital data. https://celestrak.org
|
||||||
|
- **CARTO / OpenStreetMap** — basemap tiles. © OpenStreetMap contributors © CARTO.
|
||||||
|
- **EOX** — Sentinel-2 cloudless; modified Copernicus Sentinel data 2023. https://s2maps.eu
|
||||||
|
- **CesiumJS**, **satellite.js** — Apache-2.0 / MIT.
|
||||||
|
|
||||||
|
The Cesium credit display is kept visible in-app as a condition of these licenses.
|
||||||
|
|||||||
14
SPEC.md
14
SPEC.md
@ -206,18 +206,24 @@ Deployment specifics:
|
|||||||
|
|
||||||
- **Ship only** `index.html`, `css/`, `js/` (+ README). Never `serve.py`, `SPEC.md`, `.claude/`.
|
- **Ship only** `index.html`, `css/`, `js/` (+ README). Never `serve.py`, `SPEC.md`, `.claude/`.
|
||||||
- **Pin CDN versions** (§2.1) before shipping.
|
- **Pin CDN versions** (§2.1) before shipping.
|
||||||
- **Proxy in prod:** add an nginx location to the `forum-nginx` config (find it via `docker exec forum-nginx nginx -T`; **check whether the conf is a host bind-mount or container-local — if container-local, a restart wipes it, so persist it the same way the existing site config persists**):
|
- **Proxy in prod:** add an nginx location to the `forum-nginx` config (find it via `docker exec forum-nginx nginx -T`; **check whether the conf is a host bind-mount or container-local — if container-local, a restart wipes it, so persist it the same way the existing site config persists**).
|
||||||
|
|
||||||
|
**CRITICAL — resolver required.** A `proxy_pass` whose value contains a variable (here `$is_args$args`, and the `set $ups_*` host below) makes nginx resolve the upstream at **request time**, not config-load time. Without a `resolver` directive in scope, every proxied request returns **HTTP 502 "no resolver defined to resolve …"** — and `nginx -t` still PASSES, so the config test gives false confidence. Confirm one exists (`docker exec forum-nginx nginx -T | grep -i resolver`); if not, add one at `http`/`server` scope:
|
||||||
|
|
||||||
```nginx
|
```nginx
|
||||||
|
resolver 1.1.1.1 8.8.8.8 valid=300s ipv6=off; # add once if absent
|
||||||
|
|
||||||
location = /godsigh/proxy/opensky {
|
location = /godsigh/proxy/opensky {
|
||||||
proxy_pass https://opensky-network.org/api/states/all$is_args$args;
|
set $ups_opensky opensky-network.org;
|
||||||
|
proxy_pass https://$ups_opensky/api/states/all$is_args$args;
|
||||||
proxy_ssl_server_name on;
|
proxy_ssl_server_name on;
|
||||||
proxy_set_header Host opensky-network.org;
|
proxy_set_header Host opensky-network.org;
|
||||||
proxy_set_header User-Agent "godsigh/1.0 (partly.party)";
|
proxy_set_header User-Agent "godsigh/1.0 (partly.party)";
|
||||||
add_header Cache-Control "no-store" always;
|
add_header Cache-Control "no-store" always;
|
||||||
}
|
}
|
||||||
location = /godsigh/proxy/celestrak {
|
location = /godsigh/proxy/celestrak {
|
||||||
proxy_pass https://celestrak.org/NORAD/elements/gp.php$is_args$args;
|
set $ups_celestrak celestrak.org;
|
||||||
|
proxy_pass https://$ups_celestrak/NORAD/elements/gp.php$is_args$args;
|
||||||
proxy_ssl_server_name on;
|
proxy_ssl_server_name on;
|
||||||
proxy_set_header Host celestrak.org;
|
proxy_set_header Host celestrak.org;
|
||||||
proxy_set_header User-Agent "godsigh/1.0 (partly.party)";
|
proxy_set_header User-Agent "godsigh/1.0 (partly.party)";
|
||||||
@ -225,7 +231,7 @@ location = /godsigh/proxy/celestrak {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
then `docker exec forum-nginx nginx -t && docker exec forum-nginx nginx -s reload`.
|
then `docker exec forum-nginx nginx -t && docker exec forum-nginx nginx -s reload`. **`nginx -t` passing is NOT sufficient — it does not catch a missing resolver.** The proxy curl below (must return JSON, not a 502 or HTML) is the hard gate.
|
||||||
- **Cloudflare:** ask John for the fresh token at this point; purge `partly.party/godsigh/*` after deploy. Beware Cloudflare caching the proxy endpoints — the `no-store` header above matters; if aircraft data looks frozen, check `cf-cache-status`.
|
- **Cloudflare:** ask John for the fresh token at this point; purge `partly.party/godsigh/*` after deploy. Beware Cloudflare caching the proxy endpoints — the `no-store` header above matters; if aircraft data looks frozen, check `cf-cache-status`.
|
||||||
- **Verify like the deploy-map says — data endpoints, not just the index:** `curl -sI https://partly.party/godsigh/` (200), `curl -s 'https://partly.party/godsigh/proxy/opensky?lamin=24&lomin=50&lamax=27&lomax=57' | head -c 300` (JSON, not HTML), one Celestrak proxy call, and a browser load checking tiles + console. Report exact curl results to John. Non-2xx = not deployed; say so plainly.
|
- **Verify like the deploy-map says — data endpoints, not just the index:** `curl -sI https://partly.party/godsigh/` (200), `curl -s 'https://partly.party/godsigh/proxy/opensky?lamin=24&lomin=50&lamax=27&lomax=57' | head -c 300` (JSON, not HTML), one Celestrak proxy call, and a browser load checking tiles + console. Report exact curl results to John. Non-2xx = not deployed; say so plainly.
|
||||||
|
|
||||||
|
|||||||
@ -151,6 +151,13 @@ export default function create(ctx) {
|
|||||||
ui.hidePickOverlay();
|
ui.hidePickOverlay();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Self-heal when the tab regains focus. A backgrounded tab kills the poll
|
||||||
|
// loop (canPoll() is false, so tick() returns without rescheduling); if the
|
||||||
|
// clock is also paused, onClockTick never fires again to restart it (its
|
||||||
|
// currentTime is frozen), so the feed would stall permanently. kick() is
|
||||||
|
// idempotent (guarded by timer/inFlight/canPoll), so this is safe.
|
||||||
|
document.addEventListener('visibilitychange', () => { if (!document.hidden) kick(); });
|
||||||
|
|
||||||
// One immediate fetch on init.
|
// One immediate fetch on init.
|
||||||
kick();
|
kick();
|
||||||
|
|
||||||
|
|||||||
@ -92,22 +92,25 @@ export default function create(ctx) {
|
|||||||
let vessels = 0;
|
let vessels = 0;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 1) Tankers/LNG through the Hormuz deep channel — eastbound + westbound lanes.
|
// 1) Tankers/LNG through the Hormuz deep channel, two opposing lanes.
|
||||||
const eb1 = [[56.9, 25.4, -6], [56.4, 26.2, -2.5], [55.6, 26.55, 0], [54.5, 26.2, 3], [53.5, 25.7, 6]];
|
// laneIn: waypoints run 56.9°E → 53.5°E as time advances, i.e. longitude
|
||||||
const eb2 = [[56.95, 25.25, -5], [56.45, 26.05, -1.5], [55.65, 26.4, 0.5], [54.55, 26.05, 3.5], [53.55, 25.55, 6]];
|
// DECREASES → the vessel sails WEST, inbound to the Persian Gulf.
|
||||||
// Westbound lanes: reverse of the eastbound tracks, nudged north to separate lanes.
|
const laneIn1 = [[56.9, 25.4, -6], [56.4, 26.2, -2.5], [55.6, 26.55, 0], [54.5, 26.2, 3], [53.5, 25.7, 6]];
|
||||||
|
const laneIn2 = [[56.95, 25.25, -5], [56.45, 26.05, -1.5], [55.65, 26.4, 0.5], [54.55, 26.05, 3.5], [53.55, 25.55, 6]];
|
||||||
|
// Opposing lanes: reversed tracks (longitude increases → sailing EAST,
|
||||||
|
// outbound to the Gulf of Oman), nudged north to separate the two lanes.
|
||||||
const shiftLat = (wp, d) => wp.map(([lon, lat, h]) => [lon, lat + d, h]);
|
const shiftLat = (wp, d) => wp.map(([lon, lat, h]) => [lon, lat + d, h]);
|
||||||
const wb1 = shiftLat(reverse(eb1), 0.18);
|
const laneOut1 = shiftLat(reverse(laneIn1), 0.18);
|
||||||
const wb2 = shiftLat(reverse(eb2), 0.18);
|
const laneOut2 = shiftLat(reverse(laneIn2), 0.18);
|
||||||
|
|
||||||
addVessel('GULF PIONEER [DEMO]', eb1, C.shipNormal,
|
addVessel('GULF PIONEER [DEMO]', laneIn1, C.shipNormal,
|
||||||
{ flag: 'Marshall Islands', type: 'Crude oil tanker (VLCC)', speed: '12.4 kn', note: 'Eastbound, Hormuz deep channel' });
|
{ flag: 'Marshall Islands', type: 'Crude oil tanker (VLCC)', speed: '12.4 kn', note: 'Westbound — inbound to the Persian Gulf' });
|
||||||
addVessel('AL RUWAIS [DEMO]', eb2, C.shipNormal,
|
addVessel('AL RUWAIS [DEMO]', laneIn2, C.shipNormal,
|
||||||
{ flag: 'Qatar', type: 'LNG carrier (Q-Max)', speed: '15.1 kn', note: 'Eastbound, Hormuz deep channel' });
|
{ flag: 'Qatar', type: 'LNG carrier (Q-Max)', speed: '15.1 kn', note: 'Westbound — inbound to the Persian Gulf' });
|
||||||
addVessel('STAR OF MUSCAT [DEMO]', wb1, C.shipNormal,
|
addVessel('STAR OF MUSCAT [DEMO]', laneOut1, C.shipNormal,
|
||||||
{ flag: 'Panama', type: 'Product tanker (LR2)', speed: '11.8 kn', note: 'Westbound, Hormuz deep channel' });
|
{ flag: 'Panama', type: 'Product tanker (LR2)', speed: '11.8 kn', note: 'Eastbound — outbound to the Gulf of Oman' });
|
||||||
addVessel('NORDIC AURORA [DEMO]', wb2, C.shipNormal,
|
addVessel('NORDIC AURORA [DEMO]', laneOut2, C.shipNormal,
|
||||||
{ flag: 'Liberia', type: 'LNG carrier', speed: '14.6 kn', note: 'Westbound, Hormuz deep channel' });
|
{ flag: 'Liberia', type: 'LNG carrier', speed: '14.6 kn', note: 'Eastbound — outbound to the Gulf of Oman' });
|
||||||
vessels += 4;
|
vessels += 4;
|
||||||
|
|
||||||
// 2) "Toll-route" LPG carrier hugging the northern (Iranian-waters) coastline.
|
// 2) "Toll-route" LPG carrier hugging the northern (Iranian-waters) coastline.
|
||||||
@ -227,9 +230,13 @@ export default function create(ctx) {
|
|||||||
if (ent) {
|
if (ent) {
|
||||||
ent.position = pos;
|
ent.position = pos;
|
||||||
if (isFinite(pr.Cog)) ent.billboard.rotation = lib.headingToRotation(pr.Cog);
|
if (isFinite(pr.Cog)) ent.billboard.rotation = lib.headingToRotation(pr.Cog);
|
||||||
|
// Move to the end so keys().next() yields a true least-recently-updated
|
||||||
|
// victim on eviction (Map preserves insertion order).
|
||||||
|
byMMSI.delete(mmsi);
|
||||||
|
byMMSI.set(mmsi, ent);
|
||||||
} else {
|
} else {
|
||||||
if (byMMSI.size >= CAP) {
|
if (byMMSI.size >= CAP) {
|
||||||
const oldest = byMMSI.keys().next().value; // Map preserves insertion order
|
const oldest = byMMSI.keys().next().value; // least-recently-updated
|
||||||
const e = byMMSI.get(oldest);
|
const e = byMMSI.get(oldest);
|
||||||
if (e) aisDS.entities.remove(e);
|
if (e) aisDS.entities.remove(e);
|
||||||
byMMSI.delete(oldest);
|
byMMSI.delete(oldest);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user