GODSIGH/SPEC.md
jing 7da9eeaeb0 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>
2026-07-13 12:47:11 +10:00

252 lines
24 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# GODSIGH — Build Spec & Execution Instructions
**Audience:** Claude Opus 4.8, executing autonomously in this repo.
**Author:** Claude Fable 5, 2026-07-13, from a working session with John.
**Repo:** `ssh://git@100.71.119.27:222/monster/GODSIGH.git` (branch `main`)
**Final destination:** `https://partly.party/godsigh/` (games VPS, `forum-nginx` container, Cloudflare-fronted) — **deployment is the LAST phase and is gated on John.**
---
## 0. Mission
Build a browser-based 3D/4D OSINT "world view" globe in the spirit of Bilawal Sidhu's *God's Eye View*: a CesiumJS digital-twin globe with a space-time slider along the bottom, where independent public sensor feeds — satellite orbits (Celestrak TLEs), live aircraft (OpenSky ADS-B), ship traffic (AIS), and static infrastructure vectors (pipelines, choke points, ports) — are all synced to one clock. Two rendering skins: a dark high-contrast **Data Mode** and a satellite-imagery **Photo Mode**.
Everything uses free, public, unclassified data. No API keys are required for the MVP (AIS live data optionally takes a free aisstream.io key later).
**Done looks like:** open `http://127.0.0.1:8137`, see a dark globe over the Persian Gulf with ~60 named satellites tracing glowing orbits, thousands of live aircraft, demo tanker traffic through the Strait of Hormuz (including one flagged "dark vessel" AIS gap), pipeline/choke-point overlays, and demo event markers that appear/disappear as you scrub the timeline ±6 hours. Then, on John's go-ahead, the same thing live at `partly.party/godsigh`.
## 1. Ground truth — already verified 2026-07-13 (do not re-litigate)
These endpoints were probed with browser-like `Origin` headers from this machine:
| Feed | URL | Status | CORS | Consequence |
|---|---|---|---|---|
| Celestrak TLEs | `https://celestrak.org/NORAD/elements/gp.php?GROUP=stations&FORMAT=tle` | 200 | `ACAO: *` | Browser may fetch directly; we proxy anyway for safety |
| OpenSky ADS-B | `https://opensky-network.org/api/states/all` | 200 | `ACAO:` pinned to their own origin | **Browser CANNOT fetch directly — must go through our proxy** |
| EOX Sentinel-2 cloudless | `https://tiles.maps.eox.at/wmts/1.0.0/s2cloudless-2023_3857/default/g/{z}/{y}/{x}.jpg` | 200 | `ACAO: *` | Photo Mode basemap, no key |
| Carto dark basemap | `https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png` (subdomains ad) | 200 | `ACAO: *` | Data Mode basemap, no key |
OpenSky anonymous quota: ~400 credits/day; a **global** `states/all` call costs 4 credits (≈100 calls/day). The response carries `x-rate-limit-remaining`. Budget accordingly (see §6.3).
Classified US satellites (e.g. USA 234 / Topaz) are **not** in Celestrak's public catalog — do not waste time looking. Russian (Persona/Kosmos) and Chinese (Gaofen) military satellites **are** publicly tracked. Amateur "classfd" TLE lists are a roadmap item only.
## 2. Locked architecture decisions
1. **No build step.** Vanilla ES modules + CDN globals. No npm, no bundler, no framework. `index.html` loads CesiumJS and satellite.js as script-tag globals; our code is plain `<script type="module">` files importing each other relatively.
- Cesium: `https://cdn.jsdelivr.net/npm/cesium@1/Build/Cesium/Cesium.js` + `.../Build/Cesium/Widgets/widgets.css` (global `Cesium`; the IIFE build self-detects its base URL so workers/assets load from the CDN).
- satellite.js: `https://cdn.jsdelivr.net/npm/satellite.js@5/dist/satellite.min.js` (global `satellite`).
- Before the deploy phase, pin both to the exact resolved versions (read `Cesium.VERSION` in the console) so prod doesn't drift.
2. **Subpath-safe URLs everywhere.** The app must work at both `http://127.0.0.1:8137/` and `https://partly.party/godsigh/`. Every fetch and asset reference is **relative** (`proxy/opensky?...`, `css/style.css`) — never a leading `/`.
3. **Dev server = `serve.py`** (already in repo): stdlib-only static server + same-origin proxy at `proxy/opensky` and `proxy/celestrak`, port 8137. In production, nginx location blocks replace the proxy (§9.4) — `serve.py` is dev-only and must not ship into the web root.
4. **Cesium Entities vs Primitives split:** satellites/ships/infra/events are Cesium **entities** (few hundred, get time-dynamics + the built-in InfoBox for free). Aircraft are a **`BillboardCollection` primitive** (thousands; entities would crawl) with a custom click overlay.
5. **Time model:** Cesium clock spans **now 6h → now +6h**, `CLAMPED`, multiplier 1, starting at now, `shouldAnimate: true`. The built-in timeline + animation widgets ARE the space-time slider — do not build a custom one. Time-dynamic layers (satellites, demo ships, events) use `SampledPositionProperty`/`availability` so scrubbing "just works". Live-only layers (aircraft, live AIS) are hidden with a status note when the clock is scrubbed >10 min from wall-clock now (§6.6).
6. **No fake data masquerading as real.** Demo/synthetic layers (ships, events) are labeled `[DEMO]` in entity names, in the HUD, and in the README.
## 3. Repo workflow
- Work on `main` (John's solo repo; no PR ceremony). Commit per phase with messages like `phase 2: satellite layer — celestrak TLEs + SGP4 sampling`.
- End commit messages with: `Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>`
- Push after each phase so John can watch progress in Gitea.
- Already in repo when you start: `serve.py`, `SPEC.md` (this file), `README.md` (stub), `.claude/launch.json` (preview server config), `.gitignore`.
## 4. File map (target state)
```
GODSIGH/
├── SPEC.md # this file
├── README.md # user-facing docs; finish in phase 8
├── serve.py # dev server + CORS proxy (exists; don't ship to web root)
├── deploy.sh # phase 9 only — mirrors GAMES/* deploy doctrine
├── .claude/launch.json # preview config: python3 serve.py 8137 (exists)
├── index.html # CDN script tags, HUD skeleton, cesiumContainer
├── css/style.css # dark HUD styling
└── js/
├── config.js # all URLs, keys, poll intervals, theater camera, colors
├── main.js # viewer bootstrap, basemap modes, clock, live-gating, picking
├── ui.js # HUD panel: layer toggles, per-feed status lines, LIVE/SCRUB chip
└── layers/
├── satellites.js # Celestrak TLE → SGP4 → SampledPositionProperty entities
├── aircraft.js # OpenSky poll → BillboardCollection
├── ships.js # demo AIS fleet + dark-vessel gap + optional aisstream.io live
├── infra.js # static pipelines, choke points, facilities
└── events.js # [DEMO] time-anchored event markers
```
## 5. Phase plan
Execute in order. **Verify each phase in the browser before committing** (§7 protocol). Phases 18 need no input from John. Phase 9 is gated.
- **Phase 0 — bootstrap:** clone repo, `python3 serve.py 8137` (or preview_start with the provided launch.json), confirm the empty page serves and `proxy/opensky?lamin=24&lomin=50&lamax=27&lomax=57` returns JSON through it.
- **Phase 1 — globe core:** viewer + dual basemaps + clock/timeline + camera + HUD skeleton (§6.1).
- **Phase 2 — satellites** (§6.2).
- **Phase 3 — aircraft** (§6.3).
- **Phase 4 — ships** (§6.4).
- **Phase 5 — infrastructure + events** (§6.5).
- **Phase 6 — interaction polish:** mode toggle, live-scrub gating, picking overlay, status lines (§6.6).
- **Phase 7 — full verification pass** (§7).
- **Phase 8 — README** (§8).
- **Phase 9 — deploy to partly.party/godsigh** (§9) — **stop and confirm with John first.**
## 6. Detailed component specs
### 6.1 Globe core (`main.js`, `index.html`, `css/style.css`)
```js
const now = Cesium.JulianDate.now();
const start = Cesium.JulianDate.addHours(now, -6, new Cesium.JulianDate());
const stop = Cesium.JulianDate.addHours(now, 6, new Cesium.JulianDate());
const viewer = new Cesium.Viewer('cesiumContainer', {
baseLayer: false, // we manage imagery ourselves — avoids any Cesium ion token
baseLayerPicker: false, geocoder: false, sceneModePicker: false,
navigationHelpButton: false, homeButton: false, fullscreenButton: false,
timeline: true, animation: true, infoBox: true, selectionIndicator: true,
shouldAnimate: true,
});
```
- **Basemaps:** add two `UrlTemplateImageryProvider` layers — Carto `dark_all` (Data Mode, `subdomains:['a','b','c','d']`, credit "© OpenStreetMap contributors © CARTO") and EOX `s2cloudless-2023_3857` (Photo Mode, credit "Sentinel-2 cloudless by EOX — modified Copernicus Sentinel data 2023"). Photo layer starts `show = false`. Mode toggle flips the two `show` flags. Keep both credits — attribution is a license requirement for Carto, OSM and EOX.
- **Clock:** set `startTime/stopTime/currentTime`, `clockRange: Cesium.ClockRange.CLAMPED`, `multiplier: 1`, then `viewer.timeline.zoomTo(start, stop)`.
- **Atmosphere/lighting:** `viewer.scene.globe.enableLighting = true` (live day/night terminator — the single cheapest "wow"). Default sky box/atmosphere are fine.
- **Camera:** `viewer.camera.setView({ destination: Cesium.Cartesian3.fromDegrees(53.0, 25.5, 2_800_000) })` — opens on the Persian Gulf theater.
- **HUD:** fixed left panel (`<aside id="hud">`) over the canvas: title `GODSIGH ▸ WORLD VIEW`, mode toggle buttons, one row per layer (checkbox + name + colored status dot + status text), a `LIVE / SCRUBBED` chip, and a footer crediting data sources. Styling: near-black translucent panels (`rgba(10,14,18,.82)` + `backdrop-filter: blur(8px)`), 1px `rgba(255,255,255,.08)` borders, `ui-monospace` font, cyan `#35e0ff` accents. Don't fight Cesium's widget CSS; the default timeline/animation widgets already look the part.
### 6.2 Satellites (`layers/satellites.js`)
**Fetch** (via `proxy/celestrak?...`, dedupe by NORAD number, cap ~100 sats):
| Query | Purpose |
|---|---|
| `GROUP=resource&FORMAT=tle` | Commercial/civil EO constellation — filter names by keywords: `WORLDVIEW, LEGION, PLEIADES, CAPELLA, GAOFEN, SKYSAT, ICEYE, UMBRA, LANDSAT, SENTINEL-1, SENTINEL-2, GEOEYE, KOMPSAT, CARTOSAT, RESURS` |
| `GROUP=stations&FORMAT=tle` | Keep only `ISS (ZARYA)` and `CSS (TIANHE)` |
| `NAME=GAOFEN&FORMAT=tle` | Chinese military EO series incl. Gaofen-11 (may 404/empty — tolerate) |
| `NAME=COSMOS 2486&FORMAT=tle`, `NAME=COSMOS 2506&FORMAT=tle` | Russian Persona-class recon (URL-encode the space) |
TLE text parses as name/line1/line2 triplets. A failed source must not sink the layer — `Promise.allSettled`, report per-source counts in the status line.
**Propagation:** for each sat, `satellite.twoline2satrec(l1, l2)`, then build a `SampledPositionProperty` sampled every **60 s** across the full clock window (721 samples/sat):
```js
const date = Cesium.JulianDate.toDate(t);
const pv = satellite.propagate(satrec, date);
if (!pv || !pv.position) continue; // decayed/bad TLEs — skip sample
const gmst = satellite.gstime(date);
const geo = satellite.eciToGeodetic(pv.position, gmst); // radians + km
posProp.addSample(t, Cesium.Cartesian3.fromRadians(geo.longitude, geo.latitude, geo.height * 1000));
```
Set Lagrange interpolation (degree 5). ~100 sats × 721 samples ≈ 72k SGP4 calls — chunk the loop (e.g. 10 sats per `setTimeout(0)` tick) so first paint isn't blocked; update the status line as chunks land.
**Entity per sat:** `point` (pixelSize 6, color-coded), `label` (name, `10px ui-monospace`, offset above, `translucencyByDistance` so labels fade at distance), `path` (`width 1`, `resolution 120`, `PolylineGlowMaterialProperty` at ~0.35 alpha, `leadTime = trailTime = periodSec / 2` where `periodSec = (2π / satrec.no) * 60``satrec.no` is rad/min), `description` HTML: NORAD id, source query, period min, inclination `satrec.inclo` (rad→deg), apogee/perigee from `a = (398600.4418 / n²)^(1/3)` km with `n = satrec.no/60` rad/s, `e = satrec.ecco`.
**Colors:** GAOFEN `#ff4d4d` · COSMOS `#ff9f40` · WORLDVIEW/LEGION `#4dd2ff` · CAPELLA `#9d7bff` · PLEIADES `#7bff9d` · SENTINEL `#ffd24d` · ISS/CSS `#ffffff` · default `#66ffcc`.
Separate HUD toggles for "Satellites" and "Orbit paths" (paths ON by default).
### 6.3 Aircraft (`layers/aircraft.js`)
- **Fetch** `proxy/opensky` (optionally with `?lamin=&lomin=&lamax=&lomax=` from `config.js`; default **global** — same 4-credit cost as any bbox > 400 deg², and a fully lit planet is the demo). Response: `{ time, states: [...] }`; state array indices: `0 icao24, 1 callsign, 2 origin_country, 5 lon, 6 lat, 7 baro_altitude, 8 on_ground, 9 velocity(m/s), 10 true_track(deg)`. Skip null lon/lat and `on_ground`.
- **Render:** one shared canvas-drawn triangle glyph (32×32, white — draw `(16,2)→(28,30)→(16,23)→(4,30)`, close, fill) in a `BillboardCollection`; per-billboard `color` tint by altitude band (< 3000 m `#ffb347`, < 9000 m `#ffe74d`, else `#4dd2ff`), `rotation: -Cesium.Math.toRadians(track || 0)`, position at `baro_altitude` meters, `scale` ~0.55. Set `id: { layer:'aircraft', icao24, callsign, country, alt, vel, track }` for picking. Rebuild the collection wholesale each poll (8k billboards rebuilds fine).
- **Poll:** every **90 s** while the layer is enabled AND the clock is live 6.6) AND `!document.hidden` that's ~40% of the anonymous daily budget for a long session, acceptable. On HTTP 429, back off ×4 and show `rate-limited — anonymous OpenSky quota` in the status line. Show count + data timestamp + `x-rate-limit-remaining` when available.
- **Picking:** `ScreenSpaceEventHandler` LEFT_CLICK `scene.pick`; if `picked?.id?.layer === 'aircraft'`, fill a small fixed overlay div (callsign, country, altitude m/ft, speed kt, heading); otherwise hide it and let the entity InfoBox handle sats/ships.
### 6.4 Ships (`layers/ships.js`)
**Demo fleet (always available, everything named `… [DEMO]`):** ~8 vessels crossing the clock window as `SampledPositionProperty` waypoint tracks (`[lon, lat, hourOffset]`, linear interpolation, height 0), rendered as diamond-glyph billboards (shared canvas, like aircraft) with fixed `rotation` from the great-circle bearing of the track (`θ = atan2(sinΔλ·cosφ₂, cosφ₁·sinφ sinφ₁·cosφ₂·cosΔλ)`, rotation `= -toRadians(θ)`), plus name label and InfoBox description (flag, type, speed, route note). Include:
1. 23 eastbound + 2 westbound tankers/LNG through the Hormuz deep channel, e.g. `[[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]]` and reverses. Green `#7bff9d`.
2. One **"toll-route" LPG carrier** hugging the northern (Iranian-waters) coastline: waypoints tracking ~27.026.5 N between 54.556.5 E. Yellow `#ffe74d`, description notes the escort-route storyline.
3. One **dark vessel** `#ff4d4d`: `availability` = two `TimeInterval`s `[start, now2h]` and `[now+1h, stop]`. Between its last-seen and reacquired positions draw a **red dashed polyline** (`PolylineDashMaterialProperty`) with a mid-track label `AIS DARK GAP [DEMO]`. This demos the dark-vessel-detection concept while scrubbing.
4. 56 stationary gray tankers clustered at Fujairah anchorage (~25.1025.25 N, 56.556.7 E) the "floating parking lot".
**Live AIS (optional, later):** if `CONFIG.AISSTREAM_API_KEY` is set, open `wss://stream.aisstream.io/v0/stream`, on open send `{ APIKey, BoundingBoxes: [[[10,30],[35,70]]], FilterMessageTypes: ["PositionReport"] }` (boxes are `[lat,lon]` pairs, SW/NE), upsert one entity per MMSI (cap ~500, evict stalest). Free key from aisstream.io. Verify the actual message shape at runtime before trusting field names. WebSockets are not CORS-blocked, so no proxy needed. Demo fleet hides when live AIS is on.
### 6.5 Infrastructure + events (`layers/infra.js`, `layers/events.js`)
All coordinates approximate say so in each description. Static entities, no time dynamics (except events' availability).
**Choke points** red ring (`ellipse` ~80 km semi-axes, outline + `rgba(255,0,60,.06)` fill) + label: Strait of Hormuz `(56.5, 26.6)`, Bab el-Mandeb `(43.33, 12.58)`, Suez `(32.35, 30.45)`, Malacca `(101.0, 2.5)`.
**Pipelines** amber `#ffbf40` dashed polylines (`clampToGround: true`) + midpoint label:
- EastWest Petroline: `(49.68,25.94) → (46.7,24.6) → (44.5,24.3) → (41.5,24.2) → (38.06,24.09)` (Abqaiq Yanbu)
- HabshanFujairah: `(53.61,23.75) → (54.6,24.2) → (55.5,24.8) → (56.33,25.12)`
**Facilities** small square points + labels: Ras Tanura oil terminal `(50.16,26.64)`, Abqaiq processing `(49.68,25.94)`, Yanbu port `(38.06,24.09)`, Kharg Island terminal `(50.32,29.23)`, Fujairah oil terminal `(56.37,25.18)`, Jebel Ali desalination `(55.02,25.01)`, Ras Al-Khair desalination `(49.19,27.55)`, Al Udeid Air Base `(51.32,25.12)`, Haifa refinery `(35.05,32.79)`.
**Events `[DEMO]`** 34 pulsing markers (point `pixelSize` via `CallbackProperty` sine ~8±4) whose `availability` starts at their event time, so they *appear when the slider crosses them*: e.g. `now3h` strike marker inland Iran `(51.4, 32.6)`, `now2h` "satellite pass correlation" note at the same spot, `now+1h` tanker-incident marker in the Strait. Descriptions state clearly these are illustrative placeholders for a future real event feed.
### 6.6 Interaction glue (`main.js`, `ui.js`)
- **Live-scrub gating:** on `clock.onTick` (throttled to ~1/s), compute `|currentTime JulianDate.now()|`. If > 600 s: hide aircraft + live-AIS collections, set chip to `SCRUBBED — live layers paused`, and skip OpenSky polls (saves quota). Back within 600 s: restore. The timeline's rightmost 6 h are future — satellites keep predicting there (correct behavior — orbits are deterministic), live layers stay dark.
- **`ui.js` API:** `addLayer(id, name, defaultOn, onToggle)` and `setStatus(id, text, state)` where state ∈ ok/warn/err → green/amber/red dot. Every layer reports load counts and failures here — **no silent failures.**
- **`config.js`:** every URL, poll interval, bbox, camera start, color table, and the empty `AISSTREAM_API_KEY` slot in one file, commented.
## 7. Verification protocol (phase 7, and after every phase)
Use the preview tools (`.claude/launch.json` defines server `godsigh``python3 serve.py 8137`):
1. `preview_start`, then `preview_console_logs` (level error) — zero uncaught errors allowed.
2. `preview_network` — confirm 200s: Carto tiles, EOX tiles (toggle Photo Mode first), `proxy/celestrak…` ×45, `proxy/opensky…`. 429 from OpenSky is acceptable if the HUD shows the rate-limit status.
3. `preview_screenshot` in both modes; `preview_snapshot` to confirm HUD text/toggles.
4. Behavior checks: click a satellite → InfoBox with orbital params; click an aircraft → overlay; scrub timeline back 3 h → aircraft vanish + chip flips + demo ships/satellites move + `now3h` event marker pops in; drag to future → satellites keep orbiting; toggle every layer on/off; toggle both basemap modes.
5. Acceptance checklist: ☐ ≥40 satellites with orbit paths ☐ aircraft count in status (or explicit rate-limit message) ☐ dark-vessel gap renders with dashed red line ☐ all infra labels ☐ events appear/disappear with slider ☐ both basemaps ☐ zero console errors ☐ every fetch path relative (grep for `fetch('/` and `src="/` — must be empty).
## 8. README (phase 8)
Rewrite `README.md` for a visitor: what it is, screenshot, quickstart (`python3 serve.py 8137`), data-source table with attribution (OpenSky CC terms — non-commercial; Carto/OSM attribution; EOX Sentinel-2 attribution; Celestrak), explicit list of what is DEMO data, and the roadmap: live AIS via aisstream key · GPS-jamming layer (gpsjam.org daily H3 data) · historical recording (cron job appending OpenSky/AIS snapshots to JSONL for true time-travel — see the `jobs` skill heartbeat convention if built) · classified-sat amateur TLEs · oil-futures side panel · Cesium ion token for terrain/photorealistic 3D tiles · agentic event ingestion.
## 9. Phase 9 — Deployment (FINAL — gated)
**Stop. Confirm with John before starting this phase.** He will generate a fresh Cloudflare API token when ready. Two prerequisites from the local machine doctrine:
1. **Read `~/Documents/GAMES/CLAUDE.md`** and one existing deploy script (e.g. `GAMES/cratewars/deploy.sh`) and mirror the doctrine: bundle → rsync to `/tmp` staging on `humanjing@100.71.119.27``docker cp` into `forum-nginx` web root under `godsigh/` → curl verify. Ignore the stale `~/Documents/VPS_DEPLOYMENT_GUIDE.md` / `DEPLOYMENT_STATUS.md`.
2. **Run `/ship-check`** before exposing anything (the proxy endpoint is the only dynamic surface — upstream is pinned to OpenSky so there's no open-proxy/SSRF hole, but let ship-check confirm).
Deployment specifics:
- **Ship only** `index.html`, `css/`, `js/` (+ README). Never `serve.py`, `SPEC.md`, `.claude/`.
- **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**).
**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
resolver 1.1.1.1 8.8.8.8 valid=300s ipv6=off; # add once if absent
location = /godsigh/proxy/opensky {
set $ups_opensky opensky-network.org;
proxy_pass https://$ups_opensky/api/states/all$is_args$args;
proxy_ssl_server_name on;
proxy_set_header Host opensky-network.org;
proxy_set_header User-Agent "godsigh/1.0 (partly.party)";
add_header Cache-Control "no-store" always;
}
location = /godsigh/proxy/celestrak {
set $ups_celestrak celestrak.org;
proxy_pass https://$ups_celestrak/NORAD/elements/gp.php$is_args$args;
proxy_ssl_server_name on;
proxy_set_header Host celestrak.org;
proxy_set_header User-Agent "godsigh/1.0 (partly.party)";
add_header Cache-Control "max-age=3600" always;
}
```
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`.
- **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.
## 10. Known pitfalls
- `Viewer({ baseLayer: false })` is the modern (≥1.104) way to get a token-free viewer; do not pass `imageryProvider` (removed) and do not touch Cesium ion.
- `satellite.propagate` returns falsy/NaN positions for decayed or malformed TLEs — guard every sample; skip sats with < 50 valid samples.
- Billboard `rotation` is screen-space CCW; headings are CW-from-north always negate. Looks right top-down, slightly off at oblique tilt accepted.
- OpenSky callsigns are 8-char space-padded `.trim()`.
- Celestrak `NAME=` queries can return `No GP data found` as plain text detect (`line1[0] !== '1'`) and treat as empty, not fatal.
- Don't enable `requestRenderMode` the animating clock needs continuous render.
- Keep the Cesium credit container visible (attribution is a license condition, not decoration).
- If jsdelivr's `cesium@1` alias ever serves a breaking change mid-build, pin to the last known-good exact version and note it in the README.
## 11. Explicitly out of scope for this build
GPS-jamming heatmap, real strike/event feeds, oil-futures charts, historical data recording, AI agent ingestion, Cesium ion terrain/3D tiles, accounts/auth. List them in the README roadmap; build none of them.