From 403884e480acd5138a9dd8b69ae0993a93b07ebe Mon Sep 17 00:00:00 2001 From: jing Date: Mon, 13 Jul 2026 17:16:24 +1000 Subject: [PATCH] wave2: adversarial-review fixes (9 confirmed findings) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ran a 7-reviewer × per-finding-verifier workflow over the Wave 2 code; 9 of 13 raw findings survived adversarial verification. Fixes: - quakes.js: refresh now UPDATES revised quakes, not just dedupes — a signature (mag|place|depth|time) per id detects USGS revisions and rebuilds that entity, so a quake revised M4.4→M5.2 no longer stays visually understated (SPEC2 §2 "update, don't duplicate"). - aircraft.js: (a) poll() drops its result if the clock scrubbed off-live mid- fetch, so late live data never paints over the replay view; (b) re-enabling the aircraft layer while scrubbed now restores the replayed frame instead of leaving it hidden; (c) transient (non-404) history errors schedule a bounded retry so a paused clock doesn't wedge on an error frame. - satellites.js: ground track centers on the VIEWER clock (not wall-clock now) so it stays under a scrubbed satellite; and it hides when the Satellites layer is toggled off (no stray dashed line). - main.js: malformed camera hash with blank tokens (#c=,,,,) is now rejected instead of applying a bogus (0,0,0) camera — falls back to the default view. - serve.py: history t-parse catches OverflowError (t=inf/1e999 → 400, not a crashed handler); stale-on-error now also covers upstream 5xx, not just 429. Verified: t=inf/-inf/1e999/nan → 400; malformed hash → default Gulf camera; ground track 427 km from the scrubbed dot (was ~2 orbits off) and hidden on layer-off; aircraft re-enable during replay restores 6062 billboards; zero console errors. Co-Authored-By: Claude Opus 4.8 --- js/layers/aircraft.js | 23 +++++++++++++++++++++-- js/layers/quakes.js | 26 ++++++++++++++++++++------ js/layers/satellites.js | 5 ++++- js/main.js | 7 +++++-- serve.py | 8 +++++--- 5 files changed, 55 insertions(+), 14 deletions(-) diff --git a/js/layers/aircraft.js b/js/layers/aircraft.js index 2339486..4f61ac9 100644 --- a/js/layers/aircraft.js +++ b/js/layers/aircraft.js @@ -27,8 +27,10 @@ export default function create(ctx) { ui.addLayer('aircraft', 'Aircraft (live ADS-B)', true, (on) => { enabled = on; - bc.show = on && isLive; - if (on && isLive) kick(); + if (!on) { bc.show = false; return; } + bc.show = true; + if (isLive) kick(); // resume live polling + else renderPlanes(lastPlanes); // scrubbed → restore the replayed frame }); // Military-callsign filter (default off): when on, only military aircraft @@ -169,6 +171,11 @@ export default function create(ctx) { return; } + // The clock may have scrubbed off-live while this fetch was in flight — if so, + // drop it so we never paint live traffic over the replay view (mirrors the + // isLive guards in drainReplay). + if (!isLive) return; + const states = Array.isArray(data && data.states) ? data.states : []; const planes = []; for (const s of states) { @@ -193,6 +200,15 @@ export default function create(ctx) { drainReplay(); } + // A transient history failure (network blip / 5xx) while the clock is PAUSED + // would otherwise wedge — onClockTick won't fire again to retry. Clear the + // debounce and re-request once shortly. (404 is intentional graceful-degrade + // and is NOT retried here.) + function scheduleReplayRetry(tSec) { + lastReplayT = null; + setTimeout(() => { if (!isLive && !replayInFlight) requestReplay(tSec); }, 4000); + } + async function drainReplay() { if (replayInFlight || pendingReplayT === null) return; const tSec = pendingReplayT; @@ -210,6 +226,7 @@ export default function create(ctx) { } if (!res.ok) { ui.setStatus('aircraft', `history HTTP ${res.status}`, 'warn'); + scheduleReplayRetry(tSec); return; } let snap; @@ -217,6 +234,7 @@ export default function create(ctx) { snap = await res.json(); } catch (err) { ui.setStatus('aircraft', 'bad history response', 'warn'); + scheduleReplayRetry(tSec); return; } if (isLive) return; @@ -227,6 +245,7 @@ export default function create(ctx) { ui.setStatus('aircraft', `replay · ${stamp} · ${count} aircraft`, 'warn'); } catch (err) { ui.setStatus('aircraft', 'replay fetch error', 'warn'); + scheduleReplayRetry(tSec); } finally { replayInFlight = false; // A newer target arrived mid-fetch — process it now (covers a paused clock). diff --git a/js/layers/quakes.js b/js/layers/quakes.js index d9e30e1..02f30d1 100644 --- a/js/layers/quakes.js +++ b/js/layers/quakes.js @@ -15,8 +15,15 @@ export default function create(ctx) { const red = lib.cz(CONFIG.colors.quakeMax); const [magLo, magHi] = Q.magRamp; - // id → entity, so refreshes update the delta instead of duplicating. + // id → { entity, sig }, so refreshes update the delta instead of duplicating. + // The sig lets us detect USGS revisions (magnitude/place/depth/time) and + // rebuild that entity, rather than leaving a stale one on screen. const byId = new Map(); + const sigOf = (f) => { + const p = f.properties || {}; + const c = (f.geometry && f.geometry.coordinates) || []; + return `${p.mag}|${p.place}|${c[2]}|${p.time}`; + }; let enabled = true; let timer = null; @@ -126,21 +133,28 @@ export default function create(ctx) { const keepIds = new Set(kept.map((f) => `quake:${f.id}`)); // Remove entities that are no longer in the kept set (revised away or capped out). - for (const [id, ent] of byId) { + for (const [id, rec] of byId) { if (!keepIds.has(id)) { - ds.entities.remove(ent); + ds.entities.remove(rec.entity); byId.delete(id); } } - // Add newcomers (existing ids stay untouched — dedupe). + // Add newcomers and rebuild any quake USGS has revised (magnitude/place/etc.); + // unchanged ones are left in place (dedupe). let maxMag = -Infinity; for (const f of kept) { if (f.properties.mag > maxMag) maxMag = f.properties.mag; const id = `quake:${f.id}`; - if (byId.has(id)) continue; + const sig = sigOf(f); + const existing = byId.get(id); + if (existing) { + if (existing.sig === sig) continue; // unchanged — keep it + ds.entities.remove(existing.entity); // revised — rebuild below + byId.delete(id); + } const ent = buildEntity(f); ds.entities.add(ent); - byId.set(id, ent); + byId.set(id, { entity: ent, sig }); } const count = byId.size; diff --git a/js/layers/satellites.js b/js/layers/satellites.js index 2f0ba3b..6413a6a 100644 --- a/js/layers/satellites.js +++ b/js/layers/satellites.js @@ -24,6 +24,7 @@ export default function create(ctx) { if (e.point) e.point.show = on; if (e.label) e.label.show = on; } + if (!on) groundTrack.show = false; // don't leave a stray track when sats are hidden }); ui.addLayer('sat-paths', 'Orbit paths', true, (on) => { showPaths = on; @@ -54,7 +55,9 @@ export default function create(ctx) { const positions = []; const half = periodSec / 2; const step = Math.max(20, periodSec / 180); // ~one orbit in ~180 samples - const nowJd = Cesium.JulianDate.now(); + // Center on the VIEWER clock, not wall-clock now, so a scrubbed timeline's + // track still passes under the (time-dynamic) satellite dot. + const nowJd = viewer.clock.currentTime.clone(); for (let dt = -half; dt <= half; dt += step) { const date = Cesium.JulianDate.toDate( Cesium.JulianDate.addSeconds(nowJd, dt, new Cesium.JulianDate())); diff --git a/js/main.js b/js/main.js index 7f44b63..00bcb46 100644 --- a/js/main.js +++ b/js/main.js @@ -97,8 +97,11 @@ function applyHashState() { const c = params.get('c'); if (c) { - const p = c.split(',').map(Number); - if (p.length === 5 && p.every(Number.isFinite)) { + const parts = c.split(','); + const p = parts.map(Number); + // Reject blank tokens too — Number('') is 0, which would silently apply a + // bogus camera from a truncated hash instead of falling back to the default. + if (parts.length === 5 && parts.every((s) => s.trim() !== '') && p.every(Number.isFinite)) { viewer.camera.setView({ destination: Cesium.Cartesian3.fromDegrees(p[0], p[1], p[2]), orientation: { heading: Cesium.Math.toRadians(p[3]), pitch: Cesium.Math.toRadians(p[4]), roll: 0 }, diff --git a/serve.py b/serve.py index 6b4d1d0..aa61b6b 100644 --- a/serve.py +++ b/serve.py @@ -143,8 +143,10 @@ class Handler(SimpleHTTPRequestHandler): write_cache(cache, body) passthrough["X-Godsigh-Cache"] = "miss" except urllib.error.HTTPError as e: - # 3) Stale-on-error: on 429, keep the client working from last-known cache. - if opensky_global and e.code == 429: + # 3) Stale-on-error: on 429 (quota) or any 5xx (upstream failure), keep + # the client working from the last-known cache. 4xx client errors are + # NOT masked with stale data. + if opensky_global and (e.code == 429 or e.code >= 500): stale = read_cache_any(cache) if stale is not None: return self._send(200, "application/json", stale, {"X-Godsigh-Cache": "stale"}) @@ -215,7 +217,7 @@ class Handler(SimpleHTTPRequestHandler): return self._send_json(400, {"error": "usage: history/aircraft?t="}) try: t = int(float(t_raw)) - except ValueError: + except (ValueError, OverflowError): # OverflowError: t=inf / 1e999 return self._send_json(400, {"error": "t must be epoch seconds"}) if not HISTORY_DB.exists(): return self._send_json(404, {"error": "no history for this time"})