diff --git a/app/public/dashboard.html b/app/public/dashboard.html
index c988e0a..014f16b 100644
--- a/app/public/dashboard.html
+++ b/app/public/dashboard.html
@@ -110,6 +110,15 @@ a { color: var(--cool); text-decoration: none; }
.progress-bar { height: 100%; background: var(--cool); border-radius: 3px; transition: width 0.5s; width: 0%; }
.progress-text { font-size: 0.82rem; color: var(--ink-light); margin-top: 0.4rem; }
.backfill-progress { margin-top: 0.75rem; }
+/* NWS alert cards */
+.alert-card { border-radius: 10px; padding: 0.6rem 1rem; margin-bottom: 0.5rem; font-size: 0.85rem; border: 1px solid; }
+.alert-card.sev-extreme { background: rgba(120,0,0,0.12); border-color: #a00; color: var(--ink); }
+.alert-card.sev-severe { background: rgba(194,90,42,0.12); border-color: var(--accent); }
+.alert-card.sev-moderate{ background: rgba(200,160,0,0.12); border-color: #c8a000; }
+.alert-card.sev-minor { background: rgba(42,107,138,0.12); border-color: var(--cool); }
+.alert-card.sev-unknown { background: var(--paper-dark); border-color: var(--border); }
+.alert-title { font-weight: 700; margin-bottom: 0.15rem; }
+.alert-meta { font-size: 0.75rem; color: var(--ink-light); }
/* Trend arrows */
.stat-trend { font-size: 0.85rem; margin-left: 0.25rem; }
.trend-up { color: #e07848; }
@@ -153,6 +162,7 @@ a { color: var(--cool); text-decoration: none; }
+
@@ -172,6 +182,9 @@ a { color: var(--cool); text-decoration: none; }
+
+
+
@@ -181,6 +194,11 @@ a { color: var(--cool); text-decoration: none; }
+
Temp vs Normal
—
@@ -329,6 +347,19 @@ a { color: var(--cool); text-decoration: none; }
+
Station Location (for NWS Alerts)
+
Used to fetch active NWS weather alerts for your area. If you have push notifications enabled, your saved location is used automatically.
+
+
+
+
+
+
+
+
Change Password
@@ -588,6 +619,16 @@ function renderCharts(readings, range, yoy = null) {
Solar Radiation (W/m²)
+
+ ${pts.some(r => r.tempinf != null) ? `
+
+
Indoor Temperature & Dew Point (°F)
+
+
+
+
Indoor Humidity (%)
+
+
` : ''}
`;
@@ -635,6 +676,23 @@ function renderCharts(readings, range, yoy = null) {
[lineDataset('Solar W/m²', field(pts, 'solarradiation'), c.accent)],
lbls, 'W/m²', { scales: { y: { min: 0 } } });
+ if (pts.some(r => r.tempinf != null)) {
+ const indoorDp = pts.map(r => {
+ if (r.tempinf == null || r.humidityin == null) return null;
+ const Tc = (r.tempinf - 32) * 5 / 9, RH = r.humidityin;
+ const dp = (243.04 * (Math.log(RH / 100) + 17.625 * Tc / (243.04 + Tc))) /
+ (17.625 - Math.log(RH / 100) - 17.625 * Tc / (243.04 + Tc));
+ return +(dp * 9 / 5 + 32).toFixed(1);
+ });
+ makeChart('cTempIn', 'line',
+ [lineDataset('Temp In °F', field(pts, 'tempinf'), c.accent),
+ lineDataset('Dew Point In', indoorDp, c.cool, true)],
+ lbls, '°F');
+ makeChart('cHumIn', 'line',
+ [lineDataset('Humidity In %', field(pts, 'humidityin'), c.cool)],
+ lbls, '%', { scales: { y: { min: 0, max: 100 } } });
+ }
+
// Wind rose
const roseCanvas = document.getElementById('windRoseCanvas');
if (roseCanvas) {
@@ -674,6 +732,22 @@ async function updateLatest() {
set('sSolar', d.solarradiation, 0);
set('sBatt', d.battout != null ? (d.battout === 1 ? 'OK' : 'Low') : '—', 0);
+ if (d.tempinf != null) {
+ set('sTempIn', d.tempinf);
+ document.getElementById('indoorTempTile').style.display = '';
+ }
+ if (d.humidityin != null) {
+ set('sHumIn', d.humidityin, 0);
+ document.getElementById('indoorHumTile').style.display = '';
+ }
+ if (d.tempinf != null && d.humidityin != null) {
+ const Tc = (d.tempinf - 32) * 5 / 9, RH = d.humidityin;
+ const dp = (243.04 * (Math.log(RH / 100) + 17.625 * Tc / (243.04 + Tc))) /
+ (17.625 - Math.log(RH / 100) - 17.625 * Tc / (243.04 + Tc));
+ set('sDewIn', +(dp * 9 / 5 + 32).toFixed(1), 1);
+ document.getElementById('indoorDewTile').style.display = '';
+ }
+
const name = document.getElementById('stationName');
if (name && d.PASSKEY) name.textContent = '';
@@ -1143,6 +1217,74 @@ document.getElementById('saveThreshBtn')?.addEventListener('click', async () =>
} catch { msg.textContent = 'Error'; msg.className = 'save-msg err'; }
});
+// ── NWS Alerts ────────────────────────────────────────────────────────────────
+const SEV_CLASS = { Extreme: 'sev-extreme', Severe: 'sev-severe', Moderate: 'sev-moderate', Minor: 'sev-minor' };
+async function loadAlerts() {
+ const section = document.getElementById('alertsSection');
+ if (!section) return;
+ try {
+ const r = await fetch('/api/dashboard/alerts');
+ if (!r.ok) return;
+ const data = await r.json();
+ const features = data.features || [];
+ if (!features.length) { section.innerHTML = ''; section.style.marginBottom = '0'; return; }
+ section.style.marginBottom = '1rem';
+ section.innerHTML = features.map(f => {
+ const p = f.properties || {};
+ const sev = p.severity || 'unknown';
+ const cls = SEV_CLASS[sev] || 'sev-unknown';
+ const ends = p.ends ? new Date(p.ends).toLocaleString([], { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }) : '';
+ return `
+
${p.event || 'Alert'}
+
${p.areaDesc || ''}${ends ? ' · until ' + ends : ''}
+
`;
+ }).join('');
+ } catch {}
+}
+
+// ── Streak ─────────────────────────────────────────────────────────────────────
+async function loadStreak() {
+ try {
+ const r = await fetch('/api/station/streak');
+ if (!r.ok) return;
+ const d = await r.json();
+ if (!d.type || !d.count) return;
+ const tile = document.getElementById('sStreakTile');
+ tile.style.display = '';
+ document.getElementById('sStreak').textContent = d.count;
+ document.getElementById('sStreakNote').textContent = d.count === 1 ? `${d.type} day` : `consecutive ${d.type} days`;
+ tile.style.borderColor = d.type === 'wet' ? 'var(--cool)' : 'var(--border)';
+ } catch {}
+}
+
+// ── Station location ──────────────────────────────────────────────────────────
+async function loadLocation() {
+ try {
+ const r = await fetch('/api/dashboard/location');
+ if (!r.ok) return;
+ const loc = await r.json();
+ if (!loc) return;
+ const latEl = document.getElementById('inputLat');
+ const lonEl = document.getElementById('inputLon');
+ if (latEl) latEl.value = loc.lat;
+ if (lonEl) lonEl.value = loc.lon;
+ } catch {}
+}
+document.getElementById('saveLocBtn')?.addEventListener('click', async () => {
+ const msg = document.getElementById('locMsg');
+ const lat = parseFloat(document.getElementById('inputLat')?.value);
+ const lon = parseFloat(document.getElementById('inputLon')?.value);
+ if (!isFinite(lat) || !isFinite(lon) || lat < -90 || lat > 90 || lon < -180 || lon > 180) {
+ msg.textContent = 'Enter valid lat/lon'; msg.className = 'save-msg err'; return;
+ }
+ try {
+ const r = await fetch('/api/dashboard/location', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ lat, lon }) });
+ if (r.ok) { msg.textContent = 'Saved'; msg.className = 'save-msg'; loadAlerts(); }
+ else { msg.textContent = 'Error'; msg.className = 'save-msg err'; }
+ setTimeout(() => msg.textContent = '', 3000);
+ } catch { msg.textContent = 'Error'; msg.className = 'save-msg err'; }
+});
+
// ── Init all new features ─────────────────────────────────────────────────────
updateLatest();
updatePressureTrend();
@@ -1153,10 +1295,14 @@ loadHistory(currentRange);
checkKeyStatus();
loadStationName();
loadThresholds();
+loadAlerts();
+loadStreak();
+loadLocation();
setInterval(updatePressureTrend, 120000);
setInterval(updateExtStats, 300000);
setInterval(updateTrends, 120000);
+setInterval(loadAlerts, 5 * 60 * 1000);