2e797c89f6
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
96 lines
3.7 KiB
JavaScript
96 lines
3.7 KiB
JavaScript
/**
|
|
* 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([]);
|
|
}
|
|
}
|