Make dashboard alerts short with click-to-expand full details
Publish Container / publish (push) Successful in 2m50s

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Nate EmmertandClaude Sonnet 5.5 committed 2026-09-30 11:45:50 -04:00
1 parent fc93079109
commit 528dec753d
2 files changed
+38 -8

No files matched your search

+36 -6
View File
@@ -166,6 +166,16 @@ a { color: var(--cool); text-decoration: none; }
.alert-card.sev-unknown { background: var(--paper-dark); border-color: var(--border); } .alert-card.sev-unknown { background: var(--paper-dark); border-color: var(--border); }
.alert-title { font-weight: 700; margin-bottom: 0.15rem; } .alert-title { font-weight: 700; margin-bottom: 0.15rem; }
.alert-meta { font-size: 0.75rem; color: var(--ink-light); } .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 */ /* Trend arrows */
.stat-trend { font-size: 0.85rem; margin-left: 0.25rem; } .stat-trend { font-size: 0.85rem; margin-left: 0.25rem; }
.trend-up { color: #e07848; } .trend-up { color: #e07848; }
@@ -1326,6 +1336,7 @@ function updateTodayBar(readings) {
} }
// ── NWS Alerts ──────────────────────────────────────────────────────────────── // ── NWS Alerts ────────────────────────────────────────────────────────────────
const esc = s => String(s ?? '').replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
const SEV_CLASS = { Extreme: 'sev-extreme', Severe: 'sev-severe', Moderate: 'sev-moderate', Minor: 'sev-minor' }; const SEV_CLASS = { Extreme: 'sev-extreme', Severe: 'sev-severe', Moderate: 'sev-moderate', Minor: 'sev-minor' };
async function loadAlerts() { async function loadAlerts() {
const section = document.getElementById('alertsSection'); const section = document.getElementById('alertsSection');
@@ -1347,10 +1358,19 @@ async function loadAlerts() {
const sev = p.severity || 'unknown'; const sev = p.severity || 'unknown';
const cls = SEV_CLASS[sev] || 'sev-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' }) : ''; const ends = p.ends ? new Date(p.ends).toLocaleString([], { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }) : '';
return `<div class="alert-card ${cls}"> const desc = (p.description || '').trim();
<div class="alert-title">${p.event || 'Alert'}</div> const firstSentence = (p.headline && !desc) ? p.headline : (desc.replace(/\s+/g, ' ').match(/^.*?[.!?](\s|$)/) || [desc.replace(/\s+/g, ' ')])[0].trim();
<div class="alert-meta">${p.areaDesc || ''}${ends ? ' · until ' + ends : ''}</div> const short = firstSentence.length > 140 ? firstSentence.slice(0, 137) + '…' : firstSentence;
</div>`; const instr = (p.instruction || '').trim();
return `<div class="alert-card ${cls}"><details>
<summary>
<div class="alert-title">${esc(p.event || 'Alert')}</div>
<div class="alert-meta">${esc(p.areaDesc || '')}${ends ? ' · until ' + ends : ''}</div>
${short ? `<div class="alert-summary">${esc(short)}</div>` : ''}
<div class="alert-hint">Click for full details ▾</div>
</summary>
<div class="alert-full">${esc(desc || p.headline || 'No further details.')}${instr ? `<h4>What to do</h4>${esc(instr)}` : ''}</div>
</details></div>`;
}).join(''); }).join('');
} catch {} } catch {}
} }
@@ -1410,16 +1430,26 @@ async function loadAlertHistory(days) {
const badge = `<span class="ah-badge ${isNws ? 'nws' : 'threshold'}">${isNws ? 'NWS' : 'Threshold'}</span>`; const badge = `<span class="ah-badge ${isNws ? 'nws' : 'threshold'}">${isNws ? 'NWS' : 'Threshold'}</span>`;
const label = isNws ? (it.event || 'Alert') : (TAG_LABELS[it.tag] || it.tag || '—'); const label = isNws ? (it.event || 'Alert') : (TAG_LABELS[it.tag] || it.tag || '—');
const detail = isNws ? (it.headline || '') : (it.body || ''); 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' }); const time = new Date(it.ts).toLocaleString([], { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
return `<tr> return `<tr class="ah-row" data-full="${esc(full)}" title="Click to expand">
<td style="white-space:nowrap;color:var(--ink-light);font-size:0.78rem">${time}</td> <td style="white-space:nowrap;color:var(--ink-light);font-size:0.78rem">${time}</td>
<td>${badge}</td> <td>${badge}</td>
<td style="font-weight:600">${label}</td> <td style="font-weight:600">${label}</td>
<td><div class="ah-body">${detail}</div></td> <td><div class="ah-body">${esc(detail)}</div></td>
</tr>`; </tr>`;
}).join('')}</tbody> }).join('')}</tbody>
</table></div>` </table></div>`
}`; }`;
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 => { body.querySelectorAll('[data-ahdays]').forEach(btn => {
btn.addEventListener('click', () => loadAlertHistory(parseInt(btn.dataset.ahdays))); btn.addEventListener('click', () => loadAlertHistory(parseInt(btn.dataset.ahdays)));
}); });
+2 -2
View File
@@ -827,7 +827,7 @@ async function alertWatcherCron() {
if (alertId && !loggedAlertIds.has(alertId)) { if (alertId && !loggedAlertIds.has(alertId)) {
loggedAlertIds.add(alertId); loggedAlertIds.add(alertId);
const p = a.properties || {}; 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; const alertId = a.id || p.id;
if (alertId && !loggedAlertIds.has(alertId)) { if (alertId && !loggedAlertIds.has(alertId)) {
loggedAlertIds.add(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); sub.seenAlertIds = [...new Set([...sub.seenAlertIds, ...newAlerts.map(f => f.id || f.properties?.id)])].slice(-200);