From 4cb36a47a95f6c66cc8a4d69fc9f9ca86a364a2e Mon Sep 17 00:00:00 2001 From: Jared Vititoe Date: Thu, 30 Apr 2026 22:19:50 -0400 Subject: [PATCH] Add stat cards, lt-frame alert queue, and timeline for resolved alerts - Four lt-stat-card widgets (Critical, Warning, Hosts, Resolved 24h) below the status bar; Critical card pulses red when count > 0 - Clicking Critical or Warning card filters the events table by severity - Events table wrapped in lt-frame with ASCII corner ornaments and lt-section-header; filter bar moved to lt-toolbar with lt-search icon - Recently Resolved table replaced with lt-timeline component - updateStatusBar() and updateHostGrid() keep stat card values live Co-Authored-By: Claude Sonnet 4.6 --- static/app.js | 11 ++++ static/style.css | 11 ++++ templates/index.html | 123 ++++++++++++++++++++++++++++++------------- 3 files changed, 107 insertions(+), 38 deletions(-) diff --git a/static/app.js b/static/app.js index 3fe4e00..d9e4e9d 100644 --- a/static/app.js +++ b/static/app.js @@ -89,6 +89,14 @@ function updateStatusBar(summary, lastCheck, daemonOk) { alertBadge.style.display = total ? '' : 'none'; } + // Update stat cards + const scCrit = document.getElementById('stat-critical-val'); + const scWarn = document.getElementById('stat-warning-val'); + if (scCrit) scCrit.textContent = critCount; + if (scWarn) scWarn.textContent = warnCount; + const statCritCard = document.getElementById('stat-critical'); + if (statCritCard) statCritCard.classList.toggle('lt-stat-card--alert', critCount > 0); + // Stale data banner: warn if last_check is older than 15 minutes let staleBanner = document.getElementById('stale-banner'); if (lastCheck) { @@ -112,6 +120,9 @@ function updateStatusBar(summary, lastCheck, daemonOk) { } function updateHostGrid(hosts) { + const scHosts = document.getElementById('stat-hosts-val'); + if (scHosts) scHosts.textContent = Object.keys(hosts).length; + for (const [name, host] of Object.entries(hosts)) { const card = document.querySelector(`.host-card[data-host="${CSS.escape(name)}"]`); if (!card) continue; diff --git a/static/style.css b/static/style.css index a0670d3..ef97346 100644 --- a/static/style.css +++ b/static/style.css @@ -995,6 +995,17 @@ .diag-pulse-link a { color: var(--cyan); } .diag-pulse-link a:hover { text-shadow: var(--glow-cyan); } +/* ── Stat card alert variant (pulsing border when critical > 0) ─── */ +.lt-stat-card--alert { + border-color: var(--red) !important; + box-shadow: 0 0 8px rgba(255,45,85,.25) !important; + animation: topo-pulse-down 2s ease-in-out infinite; +} +.lt-stat-card--alert::before { background: var(--red); box-shadow: var(--glow-red); } + +/* ── lt-frame inside g-section: no extra bottom margin ────────────── */ +.g-section > .lt-frame { margin-bottom: 0; } + /* ── Responsive ───────────────────────────────────────────────────── */ @media (max-width: 768px) { .host-grid { grid-template-columns: 1fr; } diff --git a/templates/index.html b/templates/index.html index cefd5f9..2a9663d 100644 --- a/templates/index.html +++ b/templates/index.html @@ -26,6 +26,42 @@ + +
+
+ +
+ {{ summary.critical or 0 }} + Critical +
+
+
+ +
+ {{ summary.warning or 0 }} + Warning +
+
+
+ +
+ {{ snapshot.hosts | length }} + Hosts +
+
+
+ +
+ {{ recent_resolved | length }} + Resolved 24h +
+
+
+
@@ -282,19 +318,25 @@

Active Alerts

{{ (summary.critical or 0) + (summary.warning or 0) }} -
-
- +
+
+ +
+ + +
-
+
+ + +
Alert Queue
+
{% if events %} {% if total_active is defined and total_active > events|length %}
Showing {{ events|length }} of {{ total_active }} active alerts — view all via API
@@ -364,7 +406,8 @@
No active alerts
{% endif %} -
+
+
@@ -374,34 +417,24 @@

Recently Resolved

{{ recent_resolved | length }} in last 24h -
- - - - - - - - - - - - - - {% for e in recent_resolved %} - - - - - - - - - {% endfor %} - -
Recently resolved alerts
SevTypeTargetDetailResolvedDuration
{{ e.severity }}{{ e.event_type | replace('_', ' ') }}{{ e.target_name }}{{ e.target_detail or '–' }} - {{ e.resolved_at }} -
+
+ {% for e in recent_resolved %} + {%- set dot_cls = 'lt-timeline-item--green' if e.severity == 'info' else 'lt-timeline-item--dim' -%} +
+
+ {{ e.target_name }} + {% if e.target_detail %}· {{ e.target_detail }}{% endif %} + {{ e.resolved_at }} +
+
+ {{ e.event_type | replace('_', ' ') }} +  ·  + {{ e.severity }} +  ·  duration + +
+
+ {% endfor %}
{% endif %} @@ -529,5 +562,19 @@ // Re-apply filter after dynamic table updates new MutationObserver(applyEventsFilter) .observe(document.getElementById('events-table-wrap'), { childList: true, subtree: true }); + + // Stat card clicks — filter events table by severity + document.querySelectorAll('.lt-stat-card[data-stat-filter]').forEach(card => { + card.addEventListener('click', () => { + const sev = card.dataset.statFilter; + document.querySelectorAll('.sev-pills .pill').forEach(p => p.classList.remove('active')); + const matchPill = document.querySelector(`.sev-pills .pill[data-sev="${sev}"]`); + if (matchPill) matchPill.classList.add('active'); + _filterSev = sev; + applyEventsFilter(); + document.getElementById('events-table-wrap')?.scrollIntoView({ behavior: 'smooth', block: 'start' }); + }); + card.addEventListener('keydown', e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); card.click(); } }); + }); {% endblock %}