diff --git a/app/public/dashboard.html b/app/public/dashboard.html
index e977ff6..3f11ad6 100644
--- a/app/public/dashboard.html
+++ b/app/public/dashboard.html
@@ -166,6 +166,16 @@ a { color: var(--cool); text-decoration: none; }
.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); }
+.alert-card summary { cursor: pointer; list-style: none; }
+.alert-card summary::-webkit-details-marker { display: none; }
+.alert-summary { margin-top: 0.2rem; line-height: 1.35; }
+.alert-hint { font-size: 0.72rem; color: var(--ink-light); margin-top: 0.2rem; }
+.alert-card details[open] .alert-hint { display: none; }
+.alert-full { margin-top: 0.5rem; white-space: pre-wrap; line-height: 1.45; font-size: 0.82rem; }
+.alert-full h4 { margin: 0.6rem 0 0.15rem; font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--ink-light); }
+.ah-row { cursor: pointer; }
+.ah-row .ah-body { display: -webkit-box; -webkit-line-clamp: 1; -webkit-box-orient: vertical; overflow: hidden; }
+.ah-row.open .ah-body { display: block; white-space: pre-wrap; }
/* Trend arrows */
.stat-trend { font-size: 0.85rem; margin-left: 0.25rem; }
.trend-up { color: #e07848; }
@@ -1326,6 +1336,7 @@ function updateTodayBar(readings) {
}
// ── NWS Alerts ────────────────────────────────────────────────────────────────
+const esc = s => String(s ?? '').replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
const SEV_CLASS = { Extreme: 'sev-extreme', Severe: 'sev-severe', Moderate: 'sev-moderate', Minor: 'sev-minor' };
async function loadAlerts() {
const section = document.getElementById('alertsSection');
@@ -1347,10 +1358,19 @@ async function loadAlerts() {
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 : ''}
-
`;
+ const desc = (p.description || '').trim();
+ const firstSentence = (p.headline && !desc) ? p.headline : (desc.replace(/\s+/g, ' ').match(/^.*?[.!?](\s|$)/) || [desc.replace(/\s+/g, ' ')])[0].trim();
+ const short = firstSentence.length > 140 ? firstSentence.slice(0, 137) + '…' : firstSentence;
+ const instr = (p.instruction || '').trim();
+ return `
+
+ ${esc(p.event || 'Alert')}
+ ${esc(p.areaDesc || '')}${ends ? ' · until ' + ends : ''}
+ ${short ? `${esc(short)}
` : ''}
+ Click for full details ▾
+
+ ${esc(desc || p.headline || 'No further details.')}${instr ? `
What to do
${esc(instr)}` : ''}
+ `;
}).join('');
} catch {}
}
@@ -1410,16 +1430,26 @@ async function loadAlertHistory(days) {
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 full = isNws ? [it.headline, it.description, it.instruction && ('What to do: ' + it.instruction)].filter(Boolean).join('\n\n') : detail;
const time = new Date(it.ts).toLocaleString([], { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
- return `
+ return `
| ${time} |
${badge} |
${label} |
- ${detail} |
+ ${esc(detail)} |
`;
}).join('')}
`
}`;
+ body.querySelectorAll('.ah-row').forEach(row => {
+ row.addEventListener('click', () => {
+ const el = row.querySelector('.ah-body');
+ const open = row.classList.toggle('open');
+ el.textContent = open ? row.dataset.full : el.dataset.short;
+ });
+ const el = row.querySelector('.ah-body');
+ el.dataset.short = el.textContent;
+ });
body.querySelectorAll('[data-ahdays]').forEach(btn => {
btn.addEventListener('click', () => loadAlertHistory(parseInt(btn.dataset.ahdays)));
});
diff --git a/app/server.js b/app/server.js
index 05ba7d7..b67857e 100644
--- a/app/server.js
+++ b/app/server.js
@@ -827,7 +827,7 @@ async function alertWatcherCron() {
if (alertId && !loggedAlertIds.has(alertId)) {
loggedAlertIds.add(alertId);
const p = a.properties || {};
- recordAlertLog({ type: 'nws', alertId, event: p.event, headline: p.headline, effective: p.effective, expires: p.expires, lat: dashLat, lon: dashLon });
+ recordAlertLog({ type: 'nws', alertId, event: p.event, headline: p.headline, description: p.description, instruction: p.instruction, effective: p.effective, expires: p.expires, lat: dashLat, lon: dashLon });
}
}
}
@@ -853,7 +853,7 @@ async function alertWatcherCron() {
const alertId = a.id || p.id;
if (alertId && !loggedAlertIds.has(alertId)) {
loggedAlertIds.add(alertId);
- recordAlertLog({ type: 'nws', alertId, event: p.event, headline: p.headline, effective: p.effective, expires: p.expires, lat: sub.lat, lon: sub.lon });
+ recordAlertLog({ type: 'nws', alertId, event: p.event, headline: p.headline, description: p.description, instruction: p.instruction, effective: p.effective, expires: p.expires, lat: sub.lat, lon: sub.lon });
}
}
sub.seenAlertIds = [...new Set([...sub.seenAlertIds, ...newAlerts.map(f => f.id || f.properties?.id)])].slice(-200);