Add NHC 7-day tropical outlook for early hurricane warning

This commit is contained in:
James 2026-02-14 03:27:02 -05:00
parent 8d8bbbc476
commit 4b9483a835
2 changed files with 63 additions and 16 deletions

View File

@ -370,6 +370,9 @@ async function updateWeather() {
if (d.alerts && d.alerts.length) {
html += ' <span class="wx-alert">⚠️ ' + d.alerts.join(', ') + '</span>';
}
if (d.tropical) {
html += ' <span class="wx-alert">🌀 ' + d.tropical + '</span>';
}
document.getElementById('weather-line').innerHTML = html;
} catch(e) {}
}

View File

@ -123,24 +123,68 @@ app.get('/api/weather', async (req, res) => {
result.high = parseInt(today.maxtempF);
result.low = parseInt(today.mintempF);
// NWS severe alerts
const nws = await new Promise((resolve, reject) => {
const r = require('https').get('https://api.weather.gov/alerts/active?point=27.7676,-82.6403', {
headers: { 'User-Agent': 'james-dashboard' }
}, (resp) => {
let body = '';
resp.on('data', c => body += c);
resp.on('end', () => { try { resolve(JSON.parse(body)); } catch(e) { reject(e); } });
// NWS severe alerts (imminent)
try {
const nws = await new Promise((resolve, reject) => {
const r = require('https').get('https://api.weather.gov/alerts/active?point=27.7676,-82.6403', {
headers: { 'User-Agent': 'james-dashboard' }
}, (resp) => {
let body = '';
resp.on('data', c => body += c);
resp.on('end', () => { try { resolve(JSON.parse(body)); } catch(e) { reject(e); } });
});
r.on('error', reject);
r.setTimeout(10000, () => { r.destroy(); reject(new Error('timeout')); });
});
r.on('error', reject);
r.setTimeout(10000, () => { r.destroy(); reject(new Error('timeout')); });
});
for (const f of (nws.features || [])) {
const event = (f.properties.event || '').toLowerCase();
if (SEVERE_EVENTS.some(s => event.includes(s))) {
result.alerts.push(f.properties.event);
for (const f of (nws.features || [])) {
const event = (f.properties.event || '').toLowerCase();
if (SEVERE_EVENTS.some(s => event.includes(s))) {
result.alerts.push(f.properties.event);
}
}
}
} catch(e) {}
// NHC Tropical Weather Outlook (7-day formation, hurricane season)
try {
const twoHtml = await new Promise((resolve, reject) => {
const r = require('https').get('https://www.nhc.noaa.gov/text/MIATWOAT.shtml', {
headers: { 'User-Agent': 'james-dashboard' }
}, (resp) => {
let body = '';
resp.on('data', c => body += c);
resp.on('end', () => resolve(body));
});
r.on('error', reject);
r.setTimeout(10000, () => { r.destroy(); reject(new Error('timeout')); });
});
const preMatch = twoHtml.match(/<pre>([\s\S]*?)<\/pre>/);
if (preMatch) {
const text = preMatch[1].replace(/<[^>]+>/g, '');
// Skip if off-season / "not expected"
if (!text.includes('formation is not expected')) {
// Extract formation probabilities
const lines = text.split('\n');
const formations = [];
for (let i = 0; i < lines.length; i++) {
const pctMatch = lines[i].match(/(\d+)\s*percent/i);
if (pctMatch && parseInt(pctMatch[1]) >= 20) {
// Find nearby context about Gulf/Caribbean
const context = lines.slice(Math.max(0, i-3), i+2).join(' ');
const loc = context.match(/Gulf|Caribbean|Atlantic|Florida|Bahamas/i);
formations.push({
pct: parseInt(pctMatch[1]),
location: loc ? loc[0] : 'Atlantic',
text: lines[i].trim()
});
}
}
if (formations.length) {
const worst = formations.sort((a,b) => b.pct - a.pct)[0];
result.tropical = `${worst.pct}% tropical development (${worst.location}, 7-day)`;
}
}
}
} catch(e) {}
weatherCache = { data: result, ts: Date.now() };
res.json(result);