Station API Keys
@@ -1713,6 +1735,74 @@ document.getElementById('saveLocBtn')?.addEventListener('click', async () => {
} catch { msg.textContent = 'Error'; msg.className = 'save-msg err'; }
});
+// ── Alert History ─────────────────────────────────────────────────────────────
+const TAG_LABELS = {
+ 'nws': 'NWS Alert',
+ 'wind-threshold': 'Wind Gust',
+ 'temp-threshold': 'Low Temp',
+ 'aqi-threshold': 'Air Quality',
+ 'rain-threshold': 'Rain Rate',
+ 'temp-high': 'High Temp',
+ 'temp-low': 'Station Low',
+ 'freeze': 'Freeze',
+ 'wind-station': 'Station Wind',
+ 'station-offline': 'Station Offline',
+};
+
+let alertHistoryDays = 30;
+
+async function loadAlertHistory(days) {
+ alertHistoryDays = days;
+ const body = document.getElementById('alertHistoryBody');
+ body.innerHTML = 'Loading…
';
+ try {
+ const r = await fetch(`/api/dashboard/alert-history?days=${days}`);
+ if (!r.ok) throw new Error();
+ const items = await r.json();
+ const dayBtns = body.querySelectorAll?.('.range-btn') || [];
+ body.innerHTML = `
+
+ Range:
+ ${[7,30,90].map(d => ``).join('')}
+ ${items.length} event${items.length !== 1 ? 's' : ''}
+
+ ${items.length === 0
+ ? `No alerts logged in the last ${days} days.
`
+ : `
+ | Time | Type | Event | Details |
+ ${items.map(it => {
+ const isNws = it.type === 'nws';
+ const badge = `${isNws ? 'NWS' : 'Threshold'}`;
+ const label = isNws ? (it.event || 'Alert') : (TAG_LABELS[it.tag] || it.tag || '—');
+ const detail = isNws ? (it.headline || '') : (it.body || '');
+ const time = new Date(it.ts).toLocaleString([], { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
+ return `
+ | ${time} |
+ ${badge} |
+ ${label} |
+ ${detail} |
+
`;
+ }).join('')}
+
`
+ }`;
+ body.querySelectorAll('[data-ahdays]').forEach(btn => {
+ btn.addEventListener('click', () => loadAlertHistory(parseInt(btn.dataset.ahdays)));
+ });
+ } catch {
+ document.getElementById('alertHistoryBody').innerHTML = 'Failed to load alert history.
';
+ }
+}
+
+const alertHistoryPanel = document.getElementById('alertHistoryPanel');
+if (alertHistoryPanel) {
+ alertHistoryPanel.addEventListener('toggle', () => {
+ if (!alertHistoryPanel.open) return;
+ const body = document.getElementById('alertHistoryBody');
+ if (body.querySelector('table, .ah-empty')) return;
+ loadAlertHistory(alertHistoryDays);
+ }, { once: false });
+}
+
// ── Scroll handler: add blur shadow to sticky header ─────────────────────────
window.addEventListener('scroll', () => {
document.querySelector('.header').classList.toggle('scrolled', window.scrollY > 5);
diff --git a/app/server.js b/app/server.js
index d414e58..582292c 100644
--- a/app/server.js
+++ b/app/server.js
@@ -1404,6 +1404,26 @@ app.get('/api/station/trend', requireDashboardAuth, (_req, res) => {
});
});
+// Alert history log
+app.get('/api/dashboard/alert-history', requireDashboardAuth, (req, res) => {
+ const days = Math.min(90, Math.max(1, parseInt(req.query.days) || 30));
+ const results = [];
+ const now = Date.now();
+ for (let i = 0; i < days; i++) {
+ const day = new Date(now - i * 86400000).toISOString().slice(0, 10);
+ const file = path.join(ALERTS_DIR, `${day}.jsonl`);
+ if (!fs.existsSync(file)) continue;
+ try {
+ const lines = fs.readFileSync(file, 'utf8').split('\n').filter(Boolean);
+ for (const line of lines) {
+ try { results.push(JSON.parse(line)); } catch {}
+ }
+ } catch {}
+ }
+ results.sort((a, b) => b.ts - a.ts);
+ res.json(results);
+});
+
app.listen(PORT, () => {
console.log(`Weather app running on port ${PORT}`);
if (dashAuth) {