feat(storm-intel): Storm Intelligence Hub — Phase 1
Adds a new Storm Intel section to the sidebar and routes for ADMIN and OWNER roles. - StormIntelPage: full-height layout with Leaflet map + storm polygon overlays, severity-coded color system, horizontal filter bar (date range / severity / sort), and a scrollable storm event list - Storm detail drawer slides in from right with hail size, est. homes, days ago, conversion window indicator (Hot Zone / Warm / Cold / Too Fresh), intel text, and quick-action buttons (Create Lead, Open in Dispatch, Assign Canvassers stub) - Clicking a storm card or map polygon zooms the map to that polygon and opens the drawer - useStormEvents hook with USE_MOCK flag, date-range cutoff filter, type/severity filter, sort by date or severity, and conversionWindow() helper - api/storm-events.js: NWS api.weather.gov proxy (free, no auth needed), parses GeoJSON polygons and hail size from alert descriptions - MOCK_STORM_EVENTS: 7 realistic Plano TX storm events with accurate polygon coords - Sidebar: CloudLightning "Storm Intel" entry added for OWNER and ADMIN nav
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* Proxies active NWS severe weather alerts for Texas to the frontend.
|
||||
* No auth required — api.weather.gov is a free public endpoint.
|
||||
*
|
||||
* For historical data beyond 7 days, integrate NOAA Storm Events CSV API
|
||||
* or SWDI (https://www.ncei.noaa.gov/pub/data/swdi/) in a future iteration.
|
||||
*
|
||||
* Response: array of normalised storm event objects (same shape as MOCK_STORM_EVENTS)
|
||||
*/
|
||||
|
||||
const NWS_HEADERS = {
|
||||
'User-Agent': 'PlanoRealtyCRM/1.0 (admin@plano-realty.com)',
|
||||
Accept: 'application/geo+json',
|
||||
};
|
||||
|
||||
function severityFromSize(inches) {
|
||||
if (!inches || inches < 0.75) return 'none';
|
||||
if (inches < 1.00) return 'trace';
|
||||
if (inches < 1.50) return 'moderate';
|
||||
if (inches < 2.00) return 'significant';
|
||||
return 'severe';
|
||||
}
|
||||
|
||||
function parseHailSize(text = '') {
|
||||
const m = text.match(/(\d+\.?\d*)\s*inch/i) || text.match(/(\d+\.?\d*)"/) ;
|
||||
return m ? parseFloat(m[1]) : null;
|
||||
}
|
||||
|
||||
export default async function handler(req, res) {
|
||||
res.setHeader('Cache-Control', 's-maxage=3600, stale-while-revalidate=86400');
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
|
||||
if (req.method !== 'GET') {
|
||||
return res.status(405).json({ error: 'Method not allowed' });
|
||||
}
|
||||
|
||||
try {
|
||||
// NWS active severe weather alerts for TX — hail + thunderstorm
|
||||
const url = 'https://api.weather.gov/alerts/active?area=TX&event=Hail,Severe+Thunderstorm,Tornado';
|
||||
const nwsRes = await fetch(url, { headers: NWS_HEADERS });
|
||||
|
||||
if (!nwsRes.ok) {
|
||||
console.warn('[storm-events] NWS returned', nwsRes.status);
|
||||
return res.status(200).json([]);
|
||||
}
|
||||
|
||||
const geojson = await nwsRes.json();
|
||||
const features = (geojson.features || []);
|
||||
|
||||
const events = features
|
||||
.filter(f => {
|
||||
const evt = (f.properties?.event || '').toLowerCase();
|
||||
return evt.includes('hail') || evt.includes('thunderstorm') || evt.includes('tornado');
|
||||
})
|
||||
.map((f, i) => {
|
||||
const p = f.properties || {};
|
||||
const desc = p.description || p.headline || '';
|
||||
const hailSize = parseHailSize(desc) ?? parseHailSize(p.parameters?.hailSize?.[0] ?? '');
|
||||
|
||||
// NWS GeoJSON polygon coordinates are [lng, lat] — swap for Leaflet
|
||||
let polygon = [];
|
||||
const geom = f.geometry;
|
||||
if (geom?.type === 'Polygon' && geom.coordinates?.[0]) {
|
||||
polygon = geom.coordinates[0].map(([lng, lat]) => [lat, lng]);
|
||||
} else if (geom?.type === 'MultiPolygon' && geom.coordinates?.[0]?.[0]) {
|
||||
polygon = geom.coordinates[0][0].map(([lng, lat]) => [lat, lng]);
|
||||
}
|
||||
|
||||
const typeStr = (p.event || '').toLowerCase();
|
||||
const type = typeStr.includes('tornado') ? 'tornado'
|
||||
: typeStr.includes('hail') ? 'hail'
|
||||
: 'wind';
|
||||
|
||||
return {
|
||||
id: `NWS-${p.id || i}`,
|
||||
date: p.onset || p.sent || new Date().toISOString(),
|
||||
type,
|
||||
severity: severityFromSize(hailSize),
|
||||
maxHailSize: hailSize,
|
||||
areaName: p.areaDesc || 'Unknown Area',
|
||||
county: 'Collin',
|
||||
estimatedHomes: null,
|
||||
description: p.headline || desc.slice(0, 300),
|
||||
source: 'NWS',
|
||||
polygon,
|
||||
};
|
||||
})
|
||||
.filter(e => e.polygon.length >= 3); // require a valid polygon
|
||||
|
||||
return res.status(200).json(events);
|
||||
} catch (err) {
|
||||
console.error('[storm-events] error:', err.message);
|
||||
return res.status(200).json([]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user