During the nginx outage the proxy URLs briefly served the landing page WITH cacheable headers; browsers cached that HTML for the proxy URLs and kept reading it after the origin recovered (satellites 'no satellites', empty aircraft). no-store on every feed fetch means the browser cache can never serve (or store) a poisoned body for data endpoints again. The proxies are no-store server-side anyway — this closes the outage-window loophole. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
91 lines
3.8 KiB
JavaScript
91 lines
3.8 KiB
JavaScript
// NASA FIRMS layer (Wave 6.1) — raw VIIRS S-NPP satellite fire detections,
|
|
// worldwide, last 24 h. This is the sensor-level truth EONET curates from:
|
|
// every thermal anomaly the satellite saw, colored/sized by fire radiative
|
|
// power. CSV comes via proxy/firms (key injected server-side, 1 h cache).
|
|
|
|
export default function create(ctx) {
|
|
const { viewer, Cesium, ui } = ctx;
|
|
const pts = viewer.scene.primitives.add(new Cesium.PointPrimitiveCollection());
|
|
pts.show = false;
|
|
|
|
let enabled = false;
|
|
let loaded = false;
|
|
let timer = null;
|
|
const REFRESH_MS = 1800000; // 30 min; server caches 1 h anyway
|
|
|
|
const DIM = Cesium.Color.fromCssColorString('#b3572c').withAlpha(0.8);
|
|
const MID = Cesium.Color.fromCssColorString('#ff7a2f').withAlpha(0.9);
|
|
const HOT = Cesium.Color.fromCssColorString('#ffd24a');
|
|
const OUTLINE = Cesium.Color.BLACK;
|
|
|
|
ui.addLayer('firms', 'Fire hotspots (NASA FIRMS)', false, (on) => {
|
|
enabled = on;
|
|
pts.show = on;
|
|
if (on && !loaded) refresh();
|
|
if (on && !timer) timer = setInterval(refresh, REFRESH_MS);
|
|
if (!on && timer) { clearInterval(timer); timer = null; }
|
|
if (!on) { ui.hidePickOverlay(); ui.setStatus('firms', '', 'ok'); }
|
|
else if (loaded) ui.setStatus('firms', statusText(), 'ok');
|
|
}, 'Earth & hazards');
|
|
|
|
function statusText() { return `${pts.length} detections · 24h · VIIRS`; }
|
|
|
|
async function refresh() {
|
|
if (!loaded) ui.setStatus('firms', 'fetching detections…', 'warn');
|
|
let res;
|
|
try {
|
|
res = await fetch('proxy/firms', { cache: 'no-store' });
|
|
} catch {
|
|
ui.setStatus('firms', 'network error', 'err'); return;
|
|
}
|
|
if (res.status === 503) { ui.setStatus('firms', 'no key — add firmscredentials.json', 'err'); return; }
|
|
if (!res.ok) { ui.setStatus('firms', `HTTP ${res.status}`, 'warn'); return; }
|
|
const text = await res.text();
|
|
const lines = text.trim().split('\n');
|
|
const head = (lines.shift() || '').split(',');
|
|
const col = (n) => head.indexOf(n);
|
|
const iLat = col('latitude'), iLon = col('longitude'), iFrp = col('frp');
|
|
const iBright = col('bright_ti4'), iDate = col('acq_date'), iTime = col('acq_time');
|
|
const iSat = col('satellite'), iDn = col('daynight'), iConf = col('confidence');
|
|
if (iLat < 0 || iLon < 0) { ui.setStatus('firms', 'unexpected CSV shape', 'err'); return; }
|
|
pts.removeAll();
|
|
for (const line of lines) {
|
|
const c = line.split(',');
|
|
const lat = +c[iLat], lon = +c[iLon], frp = +c[iFrp] || 0;
|
|
if (!isFinite(lat) || !isFinite(lon)) continue;
|
|
pts.add({
|
|
position: Cesium.Cartesian3.fromDegrees(lon, lat),
|
|
pixelSize: frp > 50 ? 6 : frp > 5 ? 4 : 3,
|
|
color: frp > 50 ? HOT : frp > 5 ? MID : DIM,
|
|
outlineColor: OUTLINE,
|
|
outlineWidth: 1,
|
|
disableDepthTestDistance: 50000,
|
|
id: {
|
|
layer: 'firms', frp,
|
|
bright: +c[iBright] || null, date: c[iDate], time: c[iTime],
|
|
sat: c[iSat], dn: c[iDn], conf: c[iConf],
|
|
},
|
|
});
|
|
}
|
|
loaded = true;
|
|
if (enabled) ui.setStatus('firms', statusText(), 'ok');
|
|
}
|
|
|
|
function handlePick(picked) {
|
|
if (!enabled || picked?.id?.layer !== 'firms') return false;
|
|
const d = picked.id;
|
|
const hhmm = String(d.time || '').padStart(4, '0');
|
|
ui.showPickOverlay('🛰 VIIRS fire detection', [
|
|
['FRP', d.frp ? `${d.frp.toFixed(1)} MW` : '—'],
|
|
['Brightness', d.bright ? `${d.bright.toFixed(0)} K` : '—'],
|
|
['Seen', `${d.date || '—'} ${hhmm.slice(0, 2)}:${hhmm.slice(2)} UTC`],
|
|
['Satellite', `${d.sat === 'N' ? 'Suomi NPP' : d.sat || '—'} · ${d.dn === 'D' ? 'day' : 'night'}`],
|
|
['Confidence', { l: 'low', n: 'nominal', h: 'high' }[d.conf] || d.conf || '—'],
|
|
]);
|
|
return true;
|
|
}
|
|
function clearPick() { ui.hidePickOverlay(); }
|
|
|
|
return { id: 'firms', handlePick, clearPick };
|
|
}
|