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>
303 lines
11 KiB
JavaScript
303 lines
11 KiB
JavaScript
// Live aircraft (ADS-B via OpenSky) as a BillboardCollection primitive.
|
|
// Thousands of billboards rebuilt wholesale each poll; entities would crawl.
|
|
// SPEC §6.3.
|
|
//
|
|
// Wave 2: when the clock is scrubbed off wall-clock now, instead of just
|
|
// hiding, the layer REPLAYS actual recorded traffic from history/aircraft?t=
|
|
// (served by serve.py from record.py's SQLite db). Replayed aircraft show with
|
|
// an amber status. Where no recorder ran (prod), the endpoint 404s and the
|
|
// layer degrades to the original hide-when-scrubbed behavior.
|
|
|
|
export default function create(ctx) {
|
|
const { viewer, CONFIG, lib, ui, Cesium } = ctx;
|
|
|
|
const bc = viewer.scene.primitives.add(new Cesium.BillboardCollection());
|
|
const glyph = lib.aircraftGlyph(); // shared canvas, tinted per-billboard
|
|
|
|
let enabled = true; // HUD checkbox
|
|
let isLive = true; // clock at wall-clock now
|
|
let inFlight = false; // a live fetch is currently running
|
|
let timer = null; // pending setTimeout handle
|
|
let delay = CONFIG.aircraft.pollMs; // current cadence (grows on 429)
|
|
|
|
// Replay state (only used when scrubbed off-live).
|
|
let lastReplayT = null; // epoch_s of the last snapshot we requested (debounce)
|
|
let replayInFlight = false;
|
|
let pendingReplayT = null; // newest target requested while a fetch is in flight
|
|
|
|
ui.addLayer('aircraft', 'Aircraft (live ADS-B)', true, (on) => {
|
|
enabled = on;
|
|
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
|
|
// render. Military callsigns are always tinted, filter on or off.
|
|
let militaryOnly = false;
|
|
let lastPlanes = [];
|
|
ui.addLayer('aircraft-mil', 'Military filter', false, (on) => {
|
|
militaryOnly = on;
|
|
renderPlanes(lastPlanes); // instant re-render of the current frame's planes
|
|
});
|
|
|
|
const MIL_PREFIXES = CONFIG.aircraft.militaryPrefixes || [];
|
|
function isMilitary(callsign) {
|
|
if (!callsign) return false;
|
|
const cs = callsign.toUpperCase();
|
|
return MIL_PREFIXES.some((p) => cs.startsWith(p));
|
|
}
|
|
|
|
// ---- normalized plane shape: {icao24, callsign, country, lon, lat, alt, vel, track} ----
|
|
function stateToPlane(s) {
|
|
return {
|
|
icao24: s[0], callsign: (s[1] || '').trim(), country: s[2],
|
|
lon: s[5], lat: s[6], alt: s[7], vel: s[9], track: s[10],
|
|
};
|
|
}
|
|
// Recorded snapshot plane: [icao24, callsign, lon, lat, baro_alt, track, vel].
|
|
function snapPlaneToPlane(a) {
|
|
return {
|
|
icao24: a[0], callsign: a[1], country: '—',
|
|
lon: a[2], lat: a[3], alt: a[4], vel: a[6], track: a[5],
|
|
};
|
|
}
|
|
|
|
// Factored build loop — rebuilds the whole collection from a plane array.
|
|
// Shared by the live poll and by replay. Returns the feed total (the aircraft
|
|
// row shows the total; the Military-filter row shows "N military of M"), so
|
|
// the aircraft count stays stable whether or not the filter is engaged.
|
|
function renderPlanes(planes) {
|
|
lastPlanes = planes; // remembered so toggling the filter re-renders instantly
|
|
bc.removeAll();
|
|
let total = 0;
|
|
let mil = 0;
|
|
for (const p of planes) {
|
|
if (p.lon == null || p.lat == null) continue;
|
|
total++;
|
|
const military = isMilitary(p.callsign);
|
|
if (military) mil++;
|
|
if (militaryOnly && !military) continue;
|
|
bc.add({
|
|
position: Cesium.Cartesian3.fromDegrees(p.lon, p.lat, p.alt || 0),
|
|
image: glyph,
|
|
// Military callsigns tint red regardless of altitude band; civil use the band.
|
|
color: lib.cz(military ? CONFIG.colors.aircraftMil : lib.altitudeColor(CONFIG, p.alt)),
|
|
rotation: lib.headingToRotation(p.track), // lib negates — do NOT negate again
|
|
scale: military ? 0.72 : 0.55, // bump military so it stands out
|
|
id: {
|
|
layer: 'aircraft',
|
|
icao24: p.icao24,
|
|
callsign: p.callsign,
|
|
country: p.country,
|
|
alt: p.alt,
|
|
vel: p.vel,
|
|
track: p.track,
|
|
military,
|
|
},
|
|
});
|
|
}
|
|
ui.setStatus('aircraft-mil', `${mil} military of ${total}`, militaryOnly ? 'ok' : 'off');
|
|
return total;
|
|
}
|
|
|
|
// ---- live poll loop -------------------------------------------------------
|
|
// Poll only while enabled, live, and the tab is visible.
|
|
function canPoll() {
|
|
return enabled && isLive && !document.hidden;
|
|
}
|
|
|
|
function schedule(ms) {
|
|
if (timer) clearTimeout(timer);
|
|
timer = setTimeout(tick, ms);
|
|
}
|
|
|
|
function kick() {
|
|
if (!canPoll() || timer || inFlight) return;
|
|
schedule(0);
|
|
}
|
|
|
|
async function tick() {
|
|
timer = null;
|
|
if (!canPoll() || inFlight) return;
|
|
inFlight = true;
|
|
try {
|
|
await poll();
|
|
} finally {
|
|
inFlight = false;
|
|
}
|
|
if (canPoll()) schedule(delay);
|
|
}
|
|
|
|
function buildUrl() {
|
|
let url = CONFIG.proxy.opensky; // RELATIVE — works at "/" and "/godsigh/"
|
|
const bb = CONFIG.aircraft.bbox;
|
|
if (bb) {
|
|
const q = new URLSearchParams({
|
|
lamin: bb.lamin, lomin: bb.lomin, lamax: bb.lamax, lomax: bb.lomax,
|
|
});
|
|
url = `${url}?${q.toString()}`;
|
|
}
|
|
return url;
|
|
}
|
|
|
|
async function poll() {
|
|
let res;
|
|
try {
|
|
res = await fetch(buildUrl());
|
|
} catch (err) {
|
|
ui.setStatus('aircraft', 'network error — retrying', 'warn');
|
|
return;
|
|
}
|
|
|
|
if (res.status === 429) {
|
|
delay = delay * 4; // back off
|
|
ui.setStatus('aircraft', 'rate-limited — anonymous OpenSky quota', 'warn');
|
|
return;
|
|
}
|
|
if (!res.ok) {
|
|
ui.setStatus('aircraft', `HTTP ${res.status} — retrying`, 'warn');
|
|
return;
|
|
}
|
|
|
|
const remaining = res.headers.get('X-Rate-Limit-Remaining');
|
|
|
|
let data;
|
|
try {
|
|
data = await res.json();
|
|
} catch (err) {
|
|
ui.setStatus('aircraft', 'bad response — retrying', 'warn');
|
|
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) {
|
|
if (!s) continue;
|
|
if (s[5] == null || s[6] == null || s[8]) continue; // no position or on-ground
|
|
planes.push(stateToPlane(s));
|
|
}
|
|
const count = renderPlanes(planes);
|
|
|
|
delay = CONFIG.aircraft.pollMs; // healthy response — reset cadence
|
|
const stamp = data.time ? new Date(data.time * 1000).toLocaleTimeString() : '—';
|
|
const quota = remaining != null ? ` · quota ${remaining}` : '';
|
|
ui.setStatus('aircraft', `${count} aircraft · ${stamp}${quota}`, 'ok');
|
|
}
|
|
|
|
// ---- replay (scrubbed off-live) -------------------------------------------
|
|
// Requests coalesce: while a history fetch is in flight, the newest requested
|
|
// time is stashed and processed on completion — so a scrub that lands (even
|
|
// paused, where onClockTick stops firing) still gets its final snapshot.
|
|
function requestReplay(tSec) {
|
|
pendingReplayT = tSec;
|
|
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;
|
|
pendingReplayT = null;
|
|
replayInFlight = true;
|
|
lastReplayT = tSec;
|
|
try {
|
|
const res = await fetch(`history/aircraft?t=${tSec}`);
|
|
if (isLive) return; // clock snapped back to live mid-fetch — discard
|
|
if (res.status === 404) {
|
|
bc.removeAll();
|
|
bc.show = false;
|
|
ui.setStatus('aircraft', 'no history for this time', 'warn');
|
|
return;
|
|
}
|
|
if (!res.ok) {
|
|
ui.setStatus('aircraft', `history HTTP ${res.status}`, 'warn');
|
|
scheduleReplayRetry(tSec);
|
|
return;
|
|
}
|
|
let snap;
|
|
try {
|
|
snap = await res.json();
|
|
} catch (err) {
|
|
ui.setStatus('aircraft', 'bad history response', 'warn');
|
|
scheduleReplayRetry(tSec);
|
|
return;
|
|
}
|
|
if (isLive) return;
|
|
const count = renderPlanes((snap.planes || []).map(snapPlaneToPlane));
|
|
bc.show = enabled;
|
|
const stamp = snap.t ? new Date(snap.t * 1000).toLocaleTimeString() : '—';
|
|
// Amber = replayed traffic, not live (the LIVE/SCRUBBED chip already says SCRUBBED).
|
|
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).
|
|
if (pendingReplayT !== null && !isLive) drainReplay();
|
|
}
|
|
}
|
|
|
|
function onClockTick(currentTime, live) {
|
|
isLive = live;
|
|
if (isLive) {
|
|
lastReplayT = null; // leaving replay — reset the debounce
|
|
pendingReplayT = null;
|
|
bc.show = enabled;
|
|
if (enabled) kick(); // resume the live poll loop exactly as before
|
|
return;
|
|
}
|
|
// Not live → replay recorded traffic (or gracefully hide if none recorded).
|
|
if (!enabled) { bc.show = false; return; }
|
|
const tSec = Math.round(Cesium.JulianDate.toDate(currentTime).getTime() / 1000);
|
|
// Debounce on requested-time DISTANCE, not wall time: a scrub gesture fires
|
|
// many ticks — only refetch when the target moves >60 s from the last snapshot.
|
|
if (lastReplayT !== null && Math.abs(tSec - lastReplayT) <= 60) return;
|
|
requestReplay(tSec);
|
|
}
|
|
|
|
function handlePick(picked) {
|
|
if (picked?.id?.layer !== 'aircraft') return false;
|
|
const id = picked.id;
|
|
ui.showPickOverlay((id.military ? '✈⬤ ' : '✈ ') + (id.callsign || id.icao24), [
|
|
['Class', id.military ? 'Military (callsign)' : 'Civil'],
|
|
['Country', id.country || '—'],
|
|
['Altitude', `${Math.round(id.alt || 0)} m / ${Math.round((id.alt || 0) * 3.28084)} ft`],
|
|
['Speed', `${Math.round((id.vel || 0) * 1.94384)} kt`],
|
|
['Heading', `${Math.round(id.track || 0)}°`],
|
|
]);
|
|
return true;
|
|
}
|
|
|
|
function clearPick() {
|
|
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.
|
|
kick();
|
|
|
|
return { id: 'aircraft', onClockTick, handlePick, clearPick };
|
|
}
|