// ========== HEALTH CHECK DASHBOARD ==========
(function() {
// Inject modal HTML
injectModal('health-modal', `
๐ฅ Health Check Dashboard
Service uptime tracking, SLA monitoring, and incident management.
`);
const modal = document.getElementById('health-modal');
const openBtn = document.getElementById('health-check-btn');
const cancelBtn = document.getElementById('health-cancel');
const refreshBtn = document.getElementById('health-refresh-btn');
const statusContainer = document.getElementById('health-status-container');
const incidentsContainer = document.getElementById('health-incidents-container');
const configContainer = document.getElementById('health-config-container');
const lastUpdateSpan = document.getElementById('health-last-update');
const addBtn = document.getElementById('health-add-btn');
const formEl = document.getElementById('health-config-form');
const formTitle = document.getElementById('health-form-title');
const formCancel = document.getElementById('health-form-cancel');
const formSave = document.getElementById('health-form-save');
let editingId = null;
function uptimeColor(pct) {
if (pct >= 99.9) return 'var(--ok-fg)';
if (pct >= 95) return '#f39c12';
return 'var(--bad-fg)';
}
function severityBadge(sev) {
const colors = { critical: 'var(--bad-fg)', high: '#ff6b6b', medium: '#f39c12', low: 'var(--muted)' };
return `${sev}`;
}
async function loadStatus() {
try {
const res = await fetch('/api/v1/health-checks/status');
const data = await res.json();
if (!data.success || !data.status || Object.keys(data.status).length === 0) {
statusContainer.innerHTML = '๐ฅNo health checks configured. Go to the Configure tab to add services.
';
return;
}
const services = Object.values(data.status);
let html = '';
html += '';
html += '| Service | Status | ';
html += 'Uptime 24h | Uptime 7d | ';
html += 'Avg Response | Last Check |
';
for (const s of services) {
const isUp = s.status === 'up';
const dotColor = isUp ? 'var(--dot-ok)' : 'var(--dot-bad)';
const u24 = s.uptime?.['24h'] ?? '-';
const u7d = s.uptime?.['7d'] ?? '-';
const avgRt = s.avgResponseTime != null ? Math.round(s.avgResponseTime) + 'ms' : '-';
const lastCheck = s.timestamp ? timeAgo(s.timestamp) : '-';
html += ``;
html += `| ${escapeHtml(s.name || s.serviceId)} | `;
html += `${isUp ? 'Up' : 'Down'} | `;
html += `${typeof u24 === 'number' ? u24.toFixed(1) + '%' : u24} | `;
html += `${typeof u7d === 'number' ? u7d.toFixed(1) + '%' : u7d} | `;
html += `${avgRt} | `;
html += `${lastCheck} | `;
html += '
';
html += ` Loading details... |
`;
}
html += '
';
statusContainer.innerHTML = html;
lastUpdateSpan.textContent = 'Updated ' + new Date().toLocaleTimeString();
// Row click to expand details
statusContainer.querySelectorAll('tr[data-health-id]').forEach(row => {
row.addEventListener('click', async () => {
const id = row.dataset.healthId;
const detailRow = document.getElementById('health-detail-' + id);
if (!detailRow) return;
if (detailRow.style.display !== 'none') {
detailRow.style.display = 'none';
return;
}
detailRow.style.display = '';
try {
const r = await fetch(`/api/v1/health-checks/${id}/stats?hours=24`);
const d = await r.json();
if (d.success && d.stats) {
const st = d.stats;
const rt = st.responseTime || {};
detailRow.querySelector('td').innerHTML = `
Total Checks
${st.totalChecks || 0}
Uptime
${(st.uptime || 0).toFixed(2)}%
Avg Response
${Math.round(rt.avg || 0)}ms
P95 / P99
${Math.round(rt.p95 || 0)}ms / ${Math.round(rt.p99 || 0)}ms
Min Response
${Math.round(rt.min || 0)}ms
Max Response
${Math.round(rt.max || 0)}ms
Up Checks
${st.upChecks || 0}
Down Checks
${st.downChecks || 0}
`;
} else {
detailRow.querySelector('td').innerHTML = 'No detailed stats available for this period.
';
}
} catch (e) {
detailRow.querySelector('td').innerHTML = `Failed: ${escapeHtml(e.message)}
`;
}
});
});
} catch (e) {
statusContainer.innerHTML = `Failed to load health status: ${escapeHtml(e.message)}
`;
}
}
async function loadIncidents() {
try {
const [openRes, histRes] = await Promise.all([
fetch('/api/v1/health-checks/incidents'),
fetch('/api/v1/health-checks/incidents/history?limit=50')
]);
const openData = await openRes.json();
const histData = await histRes.json();
let html = '';
// Open incidents
const open = (openData.success && openData.incidents) ? openData.incidents : [];
if (open.length > 0) {
html += 'Open Incidents (' + open.length + ')
';
for (const inc of open) {
html += `
${escapeHtml(inc.serviceId)}
${severityBadge(inc.severity)}
${escapeHtml(inc.message)}
Started ${timeAgo(inc.createdAt)} ยท ${inc.occurrences || 1} occurrence(s)
`;
}
html += '
';
} else {
html += 'All services operational โ no open incidents
';
}
// Incident history
const history = (histData.success && histData.history) ? histData.history : [];
if (history.length > 0) {
html += 'Incident History
';
html += '';
html += '| Service | Type | Severity | Status | Duration | When |
';
for (const inc of history) {
const resolved = inc.status === 'resolved';
const dur = resolved && inc.duration ? (inc.duration < 60000 ? Math.round(inc.duration / 1000) + 's' : Math.round(inc.duration / 60000) + 'm') : '-';
html += ``;
html += `| ${escapeHtml(inc.serviceId)} | `;
html += `${escapeHtml(inc.type)} | `;
html += `${severityBadge(inc.severity)} | `;
html += `${inc.status} | `;
html += `${dur} | `;
html += `${timeAgo(inc.createdAt)} | `;
html += '
';
}
html += '
';
}
incidentsContainer.innerHTML = html || '๐จNo incidents recorded yet.
';
} catch (e) {
incidentsContainer.innerHTML = `Failed: ${escapeHtml(e.message)}
`;
}
}
async function loadConfig() {
try {
const res = await fetch('/api/v1/health-checks/status');
const data = await res.json();
const services = data.success && data.status ? Object.values(data.status) : [];
if (services.length === 0) {
configContainer.innerHTML = 'โ๏ธNo health checks configured yet. Click "Add Health Check" below.
';
return;
}
let html = '';
html += '| Service | Status | SLA Target | Actions |
';
for (const s of services) {
const isUp = s.status === 'up';
html += ``;
html += `| ${escapeHtml(s.name || s.serviceId)} | `;
html += `${isUp ? 'Up' : 'Down'} | `;
html += `${s.sla?.target ? s.sla.target + '%' : '-'} | `;
html += ``;
html += ``;
html += ``;
html += ' |
';
}
html += '
';
configContainer.innerHTML = html;
} catch (e) {
configContainer.innerHTML = `Failed: ${escapeHtml(e.message)}
`;
}
}
function showForm(id, name, url, timeout, codes, sla, slow) {
editingId = id || null;
formTitle.textContent = id ? 'Edit Health Check' : 'Add Health Check';
document.getElementById('health-form-id').value = id || '';
document.getElementById('health-form-id').disabled = !!id;
document.getElementById('health-form-name').value = name || '';
document.getElementById('health-form-url').value = url || '';
document.getElementById('health-form-timeout').value = timeout || 10000;
document.getElementById('health-form-codes').value = codes || '200';
document.getElementById('health-form-sla').value = sla || 99.9;
document.getElementById('health-form-slow').value = slow || 5000;
formEl.style.display = '';
addBtn.style.display = 'none';
}
function hideForm() {
formEl.style.display = 'none';
addBtn.style.display = '';
editingId = null;
}
addBtn?.addEventListener('click', () => showForm('', '', '', 10000, '200', 99.9, 5000));
formCancel?.addEventListener('click', hideForm);
formSave?.addEventListener('click', async () => {
const id = editingId || document.getElementById('health-form-id').value.trim();
if (!id) return showNotification('Service ID is required', 'warning');
const url = document.getElementById('health-form-url').value.trim();
if (!url) return showNotification('URL is required', 'warning');
const codes = document.getElementById('health-form-codes').value.split(',').map(c => parseInt(c.trim())).filter(Boolean);
const body = {
name: document.getElementById('health-form-name').value.trim() || id,
url,
timeout: parseInt(document.getElementById('health-form-timeout').value) || 10000,
expectedStatusCodes: codes.length ? codes : [200],
sla: { target: parseFloat(document.getElementById('health-form-sla').value) || 99.9 },
slowResponseThreshold: parseInt(document.getElementById('health-form-slow').value) || 5000
};
try {
formSave.textContent = 'Saving...';
formSave.disabled = true;
const res = await secureFetch(`/api/v1/health-checks/${encodeURIComponent(id)}/configure`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
const data = await res.json();
if (!data.success) throw new Error(data.error || 'Save failed');
hideForm();
loadConfig();
loadStatus();
} catch (e) {
showNotification('Error: ' + e.message, 'error');
} finally {
formSave.textContent = 'Save';
formSave.disabled = false;
}
});
document.addEventListener('health-edit', async (e) => {
const id = e.detail;
// Load existing config to populate form โ use current status data as fallback
showForm(id, '', '', 10000, '200', 99.9, 5000);
});
document.addEventListener('health-delete', async (e) => {
const id = e.detail;
if (!confirm(`Delete health check for "${id}"?`)) return;
try {
const res = await secureFetch(`/api/v1/health-checks/${encodeURIComponent(id)}/configure`, { method: 'DELETE' });
const data = await res.json();
if (!data.success) throw new Error(data.error);
loadConfig();
loadStatus();
} catch (err) {
showNotification('Error: ' + err.message, 'error');
}
});
openBtn?.addEventListener('click', () => {
modal?.classList.add('show');
loadStatus();
});
wireModal(modal, cancelBtn);
refreshBtn?.addEventListener('click', loadStatus);
// Lazy-load tabs
document.querySelector('[data-panel="health-incidents"]')?.addEventListener('click', loadIncidents);
document.querySelector('[data-panel="health-config"]')?.addEventListener('click', loadConfig);
})();