feat(storm-intel): replace Hail Recon per-pin history with keyless IEM LSR + NOAA SWDI storm history (multi-type, no API key)

This commit is contained in:
Satyam Rastogi
2026-05-30 06:12:48 +05:30
parent 6a3e4afd4e
commit 034ed1d180
3 changed files with 198 additions and 32 deletions
+32 -32
View File
@@ -1206,28 +1206,21 @@ const FilterChip = ({ active, onClick, children }) => (
</button>
);
// ─── Hail Recon response parser ──────────────────────────────────────────────
// ─── Storm-history response summarizer (IEM LSR + NOAA SWDI) ─────────────────
function parseHailReconData(data) {
function summarizeStormHistory(data) {
if (data == null) return null;
let events = null;
if (Array.isArray(data)) {
events = data;
} else if (Array.isArray(data?.ImpactDates)) {
events = data.ImpactDates;
} else if (Array.isArray(data?.Events)) {
events = data.Events;
}
if (!events) return { count: null, maxSize: null, lastDate: null, unknown: true };
if (events.length === 0) return { count: 0, maxSize: null, lastDate: null };
const maxSize = events.length
? Math.max(...events.map(d => parseFloat(d.MaxSize ?? d.size ?? d.magnitude ?? 0) || 0))
: 0;
const dates = events.map(d => d.Date || d.date || d.ImpactDate || d.impact_date || null).filter(Boolean);
const lastDate = dates.length
? new Date(Math.max(...dates.map(s => new Date(s).getTime())))
: null;
return { count: events.length, maxSize: maxSize > 0 ? maxSize : null, lastDate };
const s = data.summary;
if (!s) return { count: null, unknown: true };
const lastDate = s.lastEvent?.date ? new Date(s.lastEvent.date) : null;
return {
count: s.total ?? 0,
byType: s.byType || {},
maxHail: s.maxHail ?? null,
lastEvent: s.lastEvent || null,
lastDate,
radarHailDays: s.radarHailDays ?? 0,
};
}
// ─── Main page ────────────────────────────────────────────────────────────────
@@ -1348,14 +1341,14 @@ const StormIntelPage = () => {
});
}, [reports, selectedStorm]);
// Tier 3: fetch Hail Recon data for each dropped pin
// Tier 3: fetch multi-source storm history (IEM LSR + NOAA SWDI) for each dropped pin
useEffect(() => {
if (!pins.length) { setPinHailData({}); return; }
pins.forEach((pin, idx) => {
if (pinHailData[idx] !== undefined) return;
const [lat, lng] = pin;
setPinHailData(prev => ({ ...prev, [idx]: { loading: true, data: null, error: null } }));
fetch(`/api/hail-proxy?endpoint=ImpactDatesForLatLong&Lat=${lat}&Long=${lng}&Months=60`)
fetch(`/api/storm-history?lat=${lat}&lng=${lng}&months=60&radiusMi=20`)
.then(async r => {
const ct = r.headers.get('content-type') || '';
if (!ct.includes('json')) throw new Error('proxy_unavailable');
@@ -1827,7 +1820,7 @@ const StormIntelPage = () => {
{/* Per-pin hail history — Tier 3 */}
{pins.length > 0 && (
<div className="pt-0.5 space-y-0.5">
<p className="text-[9px] font-black uppercase tracking-wider text-violet-400 mb-0.5">Hail History (5yr)</p>
<p className="text-[9px] font-black uppercase tracking-wider text-violet-400 mb-0.5">Storm History (5yr)</p>
{pins.map((_, idx) => {
const entry = pinHailData[idx];
let line;
@@ -1843,22 +1836,29 @@ const StormIntelPage = () => {
? <span className="text-[10px] text-violet-300/70 italic">Not available in dev mode</span>
: <span className="text-[10px] text-red-400">{entry.error}</span>;
} else {
const parsed = parseHailReconData(entry.data);
if (!parsed) {
const sum = summarizeStormHistory(entry.data);
if (!sum) {
line = <span className="text-[10px] text-violet-300">No data</span>;
} else if (parsed.unknown) {
} else if (sum.unknown) {
line = <span className="text-[10px] text-violet-300">Unknown response format</span>;
} else if (parsed.count === 0) {
line = <span className="text-[10px] text-violet-300">No hail events found</span>;
} else if (sum.count === 0 && !sum.radarHailDays) {
line = <span className="text-[10px] text-violet-300">No storm events found</span>;
} else {
const lastStr = parsed.lastDate
? parsed.lastDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })
const typeStr = Object.entries(sum.byType)
.sort((a, b) => b[1] - a[1])
.slice(0, 4)
.map(([t, n]) => `${n} ${t}`)
.join(' · ');
const lastStr = sum.lastDate
? sum.lastDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })
: null;
line = (
<span className="text-[10px] text-violet-200 font-semibold">
{parsed.count} event{parsed.count !== 1 ? 's' : ''}
{parsed.maxSize ? ` · max ${parsed.maxSize}"` : ''}
{sum.count} report{sum.count !== 1 ? 's' : ''}
{typeStr ? ` · ${typeStr}` : ''}
{sum.maxHail ? ` · max ${sum.maxHail}"` : ''}
{lastStr ? ` · last ${lastStr}` : ''}
{sum.radarHailDays ? ` · radar ${sum.radarHailDays}d/30` : ''}
</span>
);
}