// ========== HEALTH CHECK DASHBOARD ========== (function() { // Inject modal HTML injectModal('health-modal', `

๐Ÿฅ Health Check Dashboard

Loading health status...
๐Ÿšจ Loading incidents...
โš™๏ธ Loading configuration...
`); 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 += ''; html += ''; html += ''; 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 += ``; html += ``; html += ``; html += ``; html += ``; html += ``; html += ''; html += ``; } html += '
ServiceStatusUptime 24hUptime 7dAvg ResponseLast Check
${escapeHtml(s.name || s.serviceId)}${isUp ? 'Up' : 'Down'}${typeof u24 === 'number' ? u24.toFixed(1) + '%' : u24}${typeof u7d === 'number' ? u7d.toFixed(1) + '%' : u7d}${avgRt}${lastCheck}
'; 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 += ''; 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 += ``; html += ``; html += ``; html += ``; html += ``; html += ``; html += ''; } html += '
ServiceTypeSeverityStatusDurationWhen
${escapeHtml(inc.serviceId)}${escapeHtml(inc.type)}${severityBadge(inc.severity)}${inc.status}${dur}${timeAgo(inc.createdAt)}
'; } 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 += ''; for (const s of services) { const isUp = s.status === 'up'; html += ``; html += ``; html += ``; html += ``; html += `'; } html += '
ServiceStatusSLA TargetActions
${escapeHtml(s.name || s.serviceId)}${isUp ? 'Up' : 'Down'}${s.sla?.target ? s.sla.target + '%' : '-'}`; 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); })();