diff --git a/static/app.js b/static/app.js index aa6b526..becba3c 100644 --- a/static/app.js +++ b/static/app.js @@ -1,20 +1,18 @@ 'use strict'; // ── Auto-redirect on auth timeout ───────────────────────────────────── -// Intercept all fetch() calls: if the server returns 401 (auth expired), -// reload the page so Authelia redirects to the login screen. +// Wraps fetch so a 401 (Authelia session expired) forces a full reload. +// lt.api uses fetch internally, so this covers all API calls too. (function () { const _fetch = window.fetch; window.fetch = async function (...args) { const resp = await _fetch(...args); - if (resp.status === 401) { - window.location.reload(); - } + if (resp.status === 401) window.location.reload(); return resp; }; })(); -// ── Toast notifications — delegates to lt.toast from base.js ───────── +// ── Toast notifications — thin wrapper over lt.toast ────────────────── function showToast(msg, type = 'success') { if (type === 'error') return lt.toast.error(msg); if (type === 'warning') return lt.toast.warning(msg); @@ -22,24 +20,25 @@ function showToast(msg, type = 'success') { return lt.toast.success(msg); } +// ── Normalise UTC timestamp string for Date() parsing ───────────────── +// Server returns "2026-03-14 14:14:21 UTC"; Date() needs ISO 8601. +function _toIso(s) { + if (!s) return s; + return s.replace(' UTC', 'Z').replace(' ', 'T'); +} + // ── Dashboard auto-refresh ──────────────────────────────────────────── async function refreshAll() { try { - const [netResp, statusResp] = await Promise.all([ - fetch('/api/network'), - fetch('/api/status'), + const [net, status] = await Promise.all([ + lt.api.get('/api/network'), + lt.api.get('/api/status'), ]); - if (!netResp.ok || !statusResp.ok) return; - - const net = await netResp.json(); - const status = await statusResp.json(); - updateHostGrid(net.hosts || {}); updateUnifiTable(net.unifi || []); updateEventsTable(status.events || [], status.total_active); updateStatusBar(status.summary || {}, status.last_check || ''); updateTopology(net.hosts || {}); - } catch (e) { console.warn('Refresh failed:', e); } @@ -57,23 +56,17 @@ function updateStatusBar(summary, lastCheck) { const lc = document.getElementById('last-check'); if (lc && lastCheck) lc.textContent = lastCheck; - // Update browser tab title with alert count const critCount = summary.critical || 0; const warnCount = summary.warning || 0; - if (critCount) { - document.title = `(${critCount} CRIT) GANDALF`; - } else if (warnCount) { - document.title = `(${warnCount} WARN) GANDALF`; - } else { - document.title = 'GANDALF'; - } + if (critCount) document.title = `(${critCount} CRIT) GANDALF`; + else if (warnCount) document.title = `(${warnCount} WARN) GANDALF`; + else document.title = 'GANDALF'; // Stale data banner: warn if last_check is older than 15 minutes let staleBanner = document.getElementById('stale-banner'); if (lastCheck) { - // last_check format: "2026-03-14 14:14:21 UTC" - const checkAge = (Date.now() - new Date(lastCheck.replace(' UTC', 'Z').replace(' ', 'T'))) / 1000; - if (checkAge > 900) { // 15 minutes + const checkAge = (Date.now() - new Date(_toIso(lastCheck))) / 1000; + if (checkAge > 900) { if (!staleBanner) { staleBanner = document.createElement('div'); staleBanner.id = 'stale-banner'; @@ -94,15 +87,12 @@ function updateHostGrid(hosts) { const card = document.querySelector(`.host-card[data-host="${CSS.escape(name)}"]`); if (!card) continue; - // Update card border class card.className = card.className.replace(/host-card-(up|down|degraded|unknown)/g, ''); card.classList.add(`host-card-${host.status}`); - // Update status dot in header const dot = card.querySelector('.host-status-dot'); if (dot) dot.className = `host-status-dot dot-${host.status}`; - // Update interface rows const ifaceList = card.querySelector('.iface-list'); if (ifaceList && host.interfaces && Object.keys(host.interfaces).length > 0) { ifaceList.innerHTML = Object.entries(host.interfaces) @@ -110,7 +100,7 @@ function updateHostGrid(hosts) { .map(([iface, state]) => `
- ${escHtml(iface)} + ${lt.escHtml(iface)} ${state}
`).join(''); @@ -146,16 +136,16 @@ function updateUnifiTable(devices) { const suppressBtn = !d.connected ? `` : ''; return ` ${statusText} - ${escHtml(d.name)} - ${escHtml(d.type)} - ${escHtml(d.model)} - ${escHtml(d.ip)} + ${lt.escHtml(d.name)} + ${lt.escHtml(d.type)} + ${lt.escHtml(d.model)} + ${lt.escHtml(d.ip)} ${suppressBtn} `; }).join(''); @@ -183,25 +173,25 @@ function updateEventsTable(events, totalActive) { const ticketBase = (typeof GANDALF_CONFIG !== 'undefined' && GANDALF_CONFIG.ticket_web_url) ? GANDALF_CONFIG.ticket_web_url : 'http://t.lotusguild.org/ticket/'; const ticket = e.ticket_id - ? `#${e.ticket_id}` : '–'; return ` ${e.severity} - ${escHtml(e.event_type.replace(/_/g,' '))} - ${escHtml(e.target_name)} - ${escHtml(e.target_detail || '–')} - ${escHtml((e.description||'').substring(0,60))}${(e.description||'').length>60?'…':''} - ${fmtRelTime(e.first_seen)} - ${fmtRelTime(e.last_seen)} + ${lt.escHtml(e.event_type.replace(/_/g,' '))} + ${lt.escHtml(e.target_name)} + ${lt.escHtml(e.target_detail || '–')} + ${lt.escHtml((e.description||'').substring(0,60))}${(e.description||'').length>60?'…':''} + ${lt.time.ago(_toIso(e.first_seen))} + ${lt.time.ago(_toIso(e.last_seen))} ${e.consecutive_failures} ${ticket} + data-sup-type="${lt.escHtml(supType)}" + data-sup-name="${lt.escHtml(e.target_name)}" + data-sup-detail="${lt.escHtml(e.target_detail||'')}">🔕 `; }).join(''); @@ -222,20 +212,19 @@ function updateEventsTable(events, totalActive) { `; } -// ── Suppression modal (dashboard) ──────────────────────────────────── +// ── Suppression modal ───────────────────────────────────────────────── function openSuppressModal(type, name, detail) { const modal = document.getElementById('suppress-modal'); if (!modal) return; - document.getElementById('sup-type').value = type; - document.getElementById('sup-name').value = name; - document.getElementById('sup-detail').value = detail; - document.getElementById('sup-reason').value = ''; + document.getElementById('sup-type').value = type; + document.getElementById('sup-name').value = name; + document.getElementById('sup-detail').value = detail; + document.getElementById('sup-reason').value = ''; document.getElementById('sup-expires').value = ''; updateSuppressForm(); - modal.classList.add('is-open'); - modal.removeAttribute('aria-hidden'); + lt.modal.open('suppress-modal'); document.querySelectorAll('#suppress-modal .pill').forEach(p => p.classList.remove('active')); const manualPill = document.querySelector('#suppress-modal .pill-manual'); @@ -245,10 +234,7 @@ function openSuppressModal(type, name, detail) { } function closeSuppressModal() { - const modal = document.getElementById('suppress-modal'); - if (!modal) return; - modal.classList.remove('is-open'); - modal.setAttribute('aria-hidden', 'true'); + lt.modal.close('suppress-modal'); } function updateSuppressForm() { @@ -286,37 +272,38 @@ async function submitSuppress(e) { if (type !== 'all' && !name.trim()) { showToast('Target name is required', 'error'); return; } try { - const resp = await fetch('/api/suppressions', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - target_type: type, - target_name: name, - target_detail: detail, - reason: reason, - expires_minutes: expires ? parseInt(expires) : null, - }), + await lt.api.post('/api/suppressions', { + target_type: type, + target_name: name, + target_detail: detail, + reason, + expires_minutes: expires ? parseInt(expires) : null, }); - const data = await resp.json(); - if (data.success) { - closeSuppressModal(); - showToast('Suppression applied ✔', 'success'); - setTimeout(refreshAll, 500); - } else { - showToast(data.error || 'Failed to apply suppression', 'error'); - } + closeSuppressModal(); + showToast('Suppression applied ✔', 'success'); + setTimeout(refreshAll, 500); } catch (err) { - showToast('Network error', 'error'); + showToast(err.message || 'Failed to apply suppression', 'error'); } } -// ── Global click handler: modal backdrop + suppress button delegation ─ +// ── Global click delegation ─────────────────────────────────────────── document.addEventListener('click', e => { - // Close modal when clicking backdrop - const modal = document.getElementById('suppress-modal'); - if (modal && e.target === modal) { closeSuppressModal(); return; } + // Refresh button + if (e.target.closest('[data-action="refresh"]')) { + lt.autoRefresh.now(); + return; + } - // Suppress button via data attributes (avoids inline onclick XSS) + // Duration pills (data-duration="" = manual/forever) + const pill = e.target.closest('.pill[data-duration]'); + if (pill) { + const val = pill.dataset.duration; + setDuration(val ? parseInt(val) : null, pill); + return; + } + + // Suppress buttons const btn = e.target.closest('.btn-suppress[data-sup-type]'); if (btn) { openSuppressModal( @@ -326,25 +313,3 @@ document.addEventListener('click', e => { ); } }); - -// ── Relative time ───────────────────────────────────────────────────── -function fmtRelTime(tsStr) { - if (!tsStr) return '–'; - const d = new Date(tsStr.replace(' UTC', 'Z').replace(' ', 'T')); - if (isNaN(d)) return tsStr; - const secs = Math.floor((Date.now() - d) / 1000); - if (secs < 60) return `${secs}s ago`; - if (secs < 3600) return `${Math.floor(secs/60)}m ago`; - if (secs < 86400) return `${Math.floor(secs/3600)}h ago`; - return `${Math.floor(secs/86400)}d ago`; -} - -// ── Utility ─────────────────────────────────────────────────────────── -function escHtml(str) { - if (str === null || str === undefined) return ''; - return String(str) - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"'); -} diff --git a/static/base.css b/static/base.css index 4045b7f..c89d1ab 100644 --- a/static/base.css +++ b/static/base.css @@ -1114,6 +1114,13 @@ select option:checked { .lt-row-p3 { border-left: 2px solid var(--priority-3) !important; } .lt-row-p4 { border-left: 2px solid var(--priority-4) !important; } +/* Row state aliases (unprefixed, compatible with monitoring apps) */ +.lt-table tr.row-critical td:first-child, .lt-table tr.lt-row-critical td:first-child { border-left: 2px solid var(--accent-red); } +.lt-table tr.row-critical td { background: rgba(255,45,85,.04); } +.lt-table tr.row-warning td:first-child { border-left: 2px solid var(--accent-amber); } +.lt-table tr.row-warning td { background: rgba(255,107,0,.04); } +.lt-table tr.row-resolved td { opacity: 0.6; } + /* Compact data table */ .lt-data-table { width: 100%; @@ -1180,7 +1187,7 @@ select option:checked { .lt-p5 { color: var(--priority-5); background: rgba(62,96,122,0.09); border-color: rgba(62,96,122,0.30); } /* Chips */ -.lt-chip { +.lt-chip, .chip { display: inline-flex; align-items: center; padding: 0.15rem 0.5rem; @@ -1191,10 +1198,10 @@ select option:checked { border: 1px solid currentColor; } -.lt-chip-ok { color: var(--accent-green); background: var(--accent-green-dim); text-shadow: var(--glow-green); } -.lt-chip-warn { color: var(--accent-amber); background: var(--accent-amber-dim); } -.lt-chip-critical { color: var(--accent-red); background: var(--accent-red-dim); text-shadow: var(--glow-red); } -.lt-chip-info { color: var(--accent-cyan); background: var(--accent-cyan-dim); text-shadow: var(--glow-cyan); } +.lt-chip-ok, .chip-ok { color: var(--accent-green); background: var(--accent-green-dim); text-shadow: var(--glow-green); } +.lt-chip-warn, .chip-warning { color: var(--accent-amber); background: var(--accent-amber-dim); } +.lt-chip-critical, .chip-critical { color: var(--accent-red); background: var(--accent-red-dim); text-shadow: var(--glow-red); } +.lt-chip-info { color: var(--accent-cyan); background: var(--accent-cyan-dim); text-shadow: var(--glow-cyan); } /* Generic badges */ .lt-badge { @@ -1228,10 +1235,11 @@ select option:checked { border-radius: 50%; flex-shrink: 0; } -.lt-dot-up { background: var(--accent-green); box-shadow: 0 0 6px var(--accent-green), 0 0 14px var(--accent-green); animation: pulse-green 2.2s ease-in-out infinite; will-change: box-shadow; } -.lt-dot-down { background: var(--accent-red); box-shadow: 0 0 6px var(--accent-red), 0 0 12px var(--accent-red); } -.lt-dot-warn { background: var(--accent-amber); box-shadow: 0 0 6px var(--accent-amber), 0 0 12px var(--accent-amber); animation: pulse-amber 2.2s ease-in-out infinite; will-change: box-shadow; } -.lt-dot-idle { background: var(--text-muted); box-shadow: none; } +.lt-dot-up, .dot-up { background: var(--accent-green); box-shadow: 0 0 6px var(--accent-green), 0 0 14px var(--accent-green); animation: pulse-green 2.2s ease-in-out infinite; will-change: box-shadow; } +.lt-dot-down, .dot-down { background: var(--accent-red); box-shadow: 0 0 6px var(--accent-red), 0 0 12px var(--accent-red); } +.lt-dot-warn, .dot-degraded { background: var(--accent-amber); box-shadow: 0 0 6px var(--accent-amber), 0 0 12px var(--accent-amber); animation: pulse-amber 2.2s ease-in-out infinite; will-change: box-shadow; } +.lt-dot-idle, .dot-unknown, +.dot-initial_down { background: var(--text-muted); box-shadow: none; } /* ---------------------------------------------------------------- diff --git a/templates/base.html b/templates/base.html index 5173e95..3fd40b5 100644 --- a/templates/base.html +++ b/templates/base.html @@ -102,7 +102,12 @@
- {{ user.name or user.username }} + {% set _uname = user.name or user.username %} +
{{ _uname[0] | upper }}
+ {{ _uname }} + {% if user.groups and 'admin' in user.groups %} + admin + {% endif %}
@@ -206,12 +211,8 @@ } }); - // R key to refresh on dashboard - document.addEventListener('keydown', function(e) { - if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.isContentEditable) return; - if (e.key === 'r' || e.key === 'R') { - if (typeof refreshAll === 'function') { e.preventDefault(); refreshAll(); } - } + lt.keys.on('r', function() { + if (typeof refreshAll === 'function') refreshAll(); }); diff --git a/templates/index.html b/templates/index.html index c8b9d81..43d86d5 100644 --- a/templates/index.html +++ b/templates/index.html @@ -18,7 +18,7 @@
{{ last_check }} - +
@@ -386,7 +386,7 @@

Suppress Alert

- +
@@ -415,18 +415,18 @@
- - - - - + + + + +
Persists until manually removed.
@@ -437,23 +437,11 @@ {% block scripts %} {% endblock %}