Alerts tab: fix hurricane/tropical storm bugs; rich alert cards matching the dashboard
Publish Container / publish (push) Successful in 2m44s
Publish Container / publish (push) Successful in 2m44s
Hurricane / tropical storm fixes - NHC intensity is in knots: show real mph (80 kt = 92 mph) instead of mislabelled 'mph' - Real Saffir-Simpson category from wind (was always cat 1); subtropical, post-tropical and potential-cyclone types no longer shown as 'depression'; each storm shows movement (direction + mph, previously a non-existent field), distance from you, update time, advisory link - Forecast track points coloured by their own forecast strength (not the current class) - Map click handlers registered once (popups stacked on every refresh); overlays wait for the map to settle instead of silently not drawing while tiles load; stale renders superseded - Subtropical/tropical advice added to alert guidance; escaped storm names Alert cards (active alerts only) - What it's for, area, window, time remaining, plain-language meaning, what to do, and a Full details expander (headline, text, instructions, severity/urgency/certainty, issuer) - No longer drops alerts beyond 6 per severity group; expired alerts are filtered out - Shared plain-language alert glossary (alert-info.js, 64 products incl. tropical and surge) now used by both the app and the dashboard history Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8252017417
commit
72a4a3fe38
8 files changed
+359
-196
No files matched your search
+12
-5
@@ -831,7 +831,7 @@ app.get('/api/alerts', async (req, res) => {
|
||||
return res.status(400).json({ error: 'Invalid lat/lon' });
|
||||
}
|
||||
try {
|
||||
const r = await fetchWithRetry(`https://api.weather.gov/alerts/active?point=${lat},${lon}`, {
|
||||
const r = await fetchWithRetry(`${NWS_BASE}/alerts/active?point=${lat},${lon}`, {
|
||||
attempts: 1,
|
||||
timeoutMs: 12000,
|
||||
fetchOptions: {
|
||||
@@ -928,10 +928,11 @@ app.post('/api/push-unsubscribe', (req, res) => {
|
||||
|
||||
// ── Hurricane tracking proxy (NHC) ────────────────────────────────────────────
|
||||
const hCache = { data: null, at: 0 };
|
||||
const NHC_BASE = process.env.NHC_BASE || 'https://www.nhc.noaa.gov';
|
||||
app.get('/api/hurricanes', async (_req, res) => {
|
||||
if (hCache.data && Date.now() - hCache.at < 5 * 60 * 1000) return res.json(hCache.data);
|
||||
try {
|
||||
const r = await fetchWithRetry('https://www.nhc.noaa.gov/CurrentStorms.json', {
|
||||
const r = await fetchWithRetry(`${NHC_BASE}/CurrentStorms.json`, {
|
||||
attempts: 2, timeoutMs: 10000,
|
||||
fetchOptions: { headers: { 'User-Agent': 'weather-app-v2 (nate@necloud.us)', Accept: 'application/json' } },
|
||||
});
|
||||
@@ -945,6 +946,11 @@ app.get('/api/hurricanes', async (_req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Strength class for a point on the forecast track (falls back to the storm's own class when wind is unknown)
|
||||
function typeForKt(kt, fallback) {
|
||||
if (!kt) return fallback || 'TS';
|
||||
return kt >= 64 ? 'HU' : kt >= 34 ? 'TS' : 'TD';
|
||||
}
|
||||
app.get('/api/hurricanes/track', async (req, res) => {
|
||||
const { stormId } = req.query;
|
||||
if (!stormId || !/^[a-z]{2}\d{6}$/i.test(stormId)) {
|
||||
@@ -954,7 +960,7 @@ app.get('/api/hurricanes/track', async (req, res) => {
|
||||
const sid = stormId.toLowerCase();
|
||||
let storm = (hCache.data?.storms || []).find(s => s.id?.toLowerCase() === sid);
|
||||
if (!storm) {
|
||||
const r = await fetchWithRetry('https://www.nhc.noaa.gov/CurrentStorms.json', {
|
||||
const r = await fetchWithRetry(`${NHC_BASE}/CurrentStorms.json`, {
|
||||
attempts: 2, timeoutMs: 10000, fetchOptions: { headers: { 'User-Agent': 'weather-app-v2 (nate@necloud.us)' } }
|
||||
});
|
||||
storm = ((await r.json()).activeStorms || []).find(s => s.id?.toLowerCase() === sid);
|
||||
@@ -992,7 +998,7 @@ app.get('/api/hurricanes/track', async (req, res) => {
|
||||
lat: parseFloat(initM[1]) * (initM[2] === 'S' ? -1 : 1),
|
||||
lon: parseFloat(initM[3]) * (initM[4] === 'W' ? -1 : 1),
|
||||
windKt: curWindM ? parseInt(curWindM[1]) : 0,
|
||||
type: storm?.classification || 'TS',
|
||||
type: typeForKt(curWindM ? parseInt(curWindM[1]) : 0, storm?.classification),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1004,7 +1010,8 @@ app.get('/api/hurricanes/track', async (req, res) => {
|
||||
const lat = parseFloat(m[3]) * (m[4] === 'S' ? -1 : 1);
|
||||
const lon = parseFloat(m[5]) * (m[6] === 'W' ? -1 : 1);
|
||||
const windM = text.substring(m.index).match(/MAX WIND\s+(\d+)\s+KT/);
|
||||
positions.push({ tau, lat, lon, windKt: windM ? parseInt(windM[1]) : 0, type: storm?.classification || 'TS' });
|
||||
const kt = windM ? parseInt(windM[1]) : 0;
|
||||
positions.push({ tau, lat, lon, windKt: kt, type: typeForKt(kt, storm?.classification) });
|
||||
}
|
||||
|
||||
if (!positions.length) return res.json({ lin: null, pts: null, pgn: null });
|
||||
|
||||
Reference in new issue
Block a user