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:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user