'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 = `
Alerts that fired more than once
| Alert | Target | Times | Total time | Last started | |
${rows.map(r => `
| ${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)} |
|
`).join('')}
`;
}
function renderTable(data) {
document.getElementById('ev-count').textContent = data.total;
const wrap = document.getElementById('ev-table');
if (!data.events.length) {
wrap.innerHTML = '';
document.getElementById('ev-pager').innerHTML = '';
return;
}
wrap.innerHTML = `
Alert history
| State | Type | Target | Started | Lasted |
Checks | Ticket | Description |
${data.events.map(e => {
const open = !e.resolved_at;
return `
| ${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 || '')} |
`;
}).join('')}
`;
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();