# 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 a–d) | 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 1–8 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. 2–3 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.0–26.5 N between 54.5–56.5 E. Yellow `#ffe74d`, description notes the escort-route storyline.
3. One **dark vessel** `#ff4d4d`: `availability` = two `TimeInterval`s — `[start, now−2h]` 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. 5–6 stationary gray tankers clustered at Fujairah anchorage (~25.10–25.25 N, 56.5–56.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:
- East–West Petroline: `(49.68,25.94) → (46.7,24.6) → (44.5,24.3) → (41.5,24.2) → (38.06,24.09)` (Abqaiq → Yanbu)
- Habshan–Fujairah: `(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]`** — 3–4 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. `now−3h` strike marker inland Iran `(51.4, 32.6)`, `now−2h` "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…` ×4–5, `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 + `now−3h` 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.
