From 528dec753d10ea0cbb04b35f824e61052183cc9e Mon Sep 17 00:00:00 2001 From: Nate Emmert Date: Wed, 30 Sep 2026 11:45:50 -0400 Subject: [PATCH] Make dashboard alerts short with click-to-expand full details Co-Authored-By: Claude Sonnet 5.5 --- app/public/dashboard.html | 42 +++++++++++++++++++++++++++++++++------ app/server.js | 4 ++-- 2 files changed, 38 insertions(+), 8 deletions(-) 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);