'use strict'; /* global deviceUrl, _toIso */ // Alert history page: renders /api/events/history with filters kept in the URL. const esc = s => lt.escHtml(s == null ? '' : String(s)); const FILTERS = ['target', 'range', 'type', 'severity', 'status']; const DEFAULTS = { target: '', range: '7d', type: '', severity: '', status: 'all' }; const RANGE_TEXT = { '24h': '24 hours', '7d': '7 days', '30d': '30 days', '90d': '90 days' }; let _offset = 0; let _loadSeq = 0; function typeText(t) { return String(t || '').replace(/_/g, ' '); } function duration(seconds) { let s = Math.max(0, Math.floor(seconds || 0)); if (s < 60) return `${s} s`; let m = Math.floor(s / 60); if (m < 60) return `${m} min`; let h = Math.floor(m / 60); m %= 60; if (h < 48) return m ? `${h} h ${m} min` : `${h} h`; const d = Math.floor(h / 24); h %= 24; return h ? `${d} d ${h} h` : `${d} d`; } function when(iso) { if (!iso) return '-'; const d = new Date(_toIso(iso)); if (isNaN(d)) return esc(iso); const text = d.toLocaleString([], { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }); return `${esc(text)}`; } function ticketLink(id) { if (!id) return '-'; const base = (typeof GANDALF_CONFIG !== 'undefined' && GANDALF_CONFIG.ticket_web_url) || ''; return base ? `#${esc(id)}` : `#${esc(id)}`; } function filters() { const f = {}; FILTERS.forEach(k => { f[k] = document.getElementById(`ev-${k}`).value.trim(); }); return f; } function readUrl() { const q = new URLSearchParams(location.search); FILTERS.forEach(k => { const el = document.getElementById(`ev-${k}`); const v = q.get(k); if (v == null) return; if (el.tagName === 'SELECT' && ![...el.options].some(o => o.value === v)) { // An alert type from a link may not be in the list until data arrives if (k === 'type') el.add(new Option(typeText(v), v)); else return; } el.value = v; }); } function writeUrl(f) { const q = new URLSearchParams(); FILTERS.forEach(k => { if (f[k] && f[k] !== DEFAULTS[k]) q.set(k, f[k]); }); const qs = q.toString(); history.replaceState(null, '', qs ? `?${qs}` : location.pathname); document.getElementById('ev-clear').hidden = !FILTERS.some(k => k !== 'range' && f[k] && f[k] !== DEFAULTS[k]); } function setFilter(changes) { Object.entries(changes).forEach(([k, v]) => { const el = document.getElementById(`ev-${k}`); if (el.tagName === 'SELECT' && v && ![...el.options].some(o => o.value === v)) el.add(new Option(typeText(v), v)); el.value = v; }); _offset = 0; load(); } function fillTypes(types) { const sel = document.getElementById('ev-type'); const current = sel.value; sel.innerHTML = '' + types.map(t => ``).join(''); if (current && !types.includes(current)) sel.add(new Option(typeText(current), current)); sel.value = current; } function renderSummary(data, f) { const open = data.events.filter(e => !e.resolved_at).length; const chips = [ `${data.total} alert${data.total === 1 ? '' : 's'} in the last ${esc(RANGE_TEXT[f.range] || f.range)}`, ]; if (open && data.offset === 0) chips.push(`${open} open now`); Object.entries(data.by_type || {}).forEach(([t, n]) => { const on = f.type === t; chips.push(``); }); document.getElementById('ev-summary').innerHTML = chips.join(''); } function renderFlappers(rows) { const sec = document.getElementById('ev-flappers-section'); sec.hidden = !rows.length; if (!rows.length) return; document.getElementById('ev-flappers').innerHTML = `
${rows.map(r => ` `).join('')}
Alerts that fired more than once
AlertTargetTimesTotal timeLast started
${esc(typeText(r.event_type))}${r.open_now ? ' open' : ''} ${esc(r.target_name)} ${r.target_detail ? `${esc(r.target_detail)}` : ''} ${r.n} ${esc(duration(r.total_s))} ${when(r.last_seen)}
`; } function renderTable(data) { document.getElementById('ev-count').textContent = data.total; const wrap = document.getElementById('ev-table'); if (!data.events.length) { wrap.innerHTML = '
✔
' + '
No alerts match
'; document.getElementById('ev-pager').innerHTML = ''; return; } wrap.innerHTML = `
${data.events.map(e => { const open = !e.resolved_at; return ``; }).join('')}
Alert history
StateTypeTargetStartedLasted ChecksTicketDescription
${open ? 'open' : 'cleared'} ${esc(e.target_name)} ${e.target_detail ? `${esc(e.target_detail)}` : ''} ${when(e.first_seen)} ${esc(duration(e.duration_s))}${open ? ' so far' : ''} ${esc(e.consecutive_failures)} ${ticketLink(e.ticket_id)} ${esc(e.description || '')}
`; const end = data.offset + data.events.length; const prev = data.offset > 0; const next = end < data.total; document.getElementById('ev-pager').innerHTML = (prev || next) ? ` ${data.offset + 1}-${end} of ${data.total} ` : ''; } async function load() { const f = filters(); writeUrl(f); const q = new URLSearchParams(); FILTERS.forEach(k => { if (f[k]) q.set(k, f[k]); }); if (_offset) q.set('offset', _offset); const seq = ++_loadSeq; try { const data = await lt.api.get(`/api/events/history?${q}`); if (seq !== _loadSeq) return; // a newer filter change is already loading fillTypes(data.types || []); renderSummary(data, f); renderFlappers(data.flappers || []); renderTable(data); } catch (err) { if (seq !== _loadSeq) return; document.getElementById('ev-table').innerHTML = `
Could not load alerts
${esc(err.message || err)}
`; } } let _typing = null; document.getElementById('ev-target').addEventListener('input', () => { clearTimeout(_typing); _typing = setTimeout(() => { _offset = 0; load(); }, 300); }); document.getElementById('ev-filters').addEventListener('change', e => { if (e.target.id === 'ev-target') return; _offset = 0; load(); }); document.getElementById('ev-filters').addEventListener('submit', e => { e.preventDefault(); _offset = 0; load(); }); document.getElementById('ev-clear').addEventListener('click', () => { setFilter({ target: '', type: '', severity: '', status: 'all' }); }); document.addEventListener('click', e => { const typeChip = e.target.closest('.ev-type-chip'); if (typeChip) return setFilter({ type: typeChip.dataset.type }); const show = e.target.closest('[data-show-type]'); if (show) { const changes = { type: show.dataset.showType }; if (show.dataset.showTarget) changes.target = show.dataset.showTarget; return setFilter(changes); } const page = e.target.closest('[data-page]'); if (page && !page.disabled) { _offset = Math.max(0, parseInt(page.dataset.page, 10) || 0); load(); document.getElementById('ev-count').scrollIntoView({ block: 'nearest' }); } }); readUrl(); load();