wave2: adversarial-review fixes (9 confirmed findings)
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 <noreply@anthropic.com>
This commit is contained in:
parent
8f3acf9470
commit
403884e480
@ -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).
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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()));
|
||||
|
||||
@ -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 },
|
||||
|
||||
8
serve.py
8
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=<epoch_s>"})
|
||||
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"})
|
||||
|
||||
Loading…
Reference in New Issue
Block a user