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

๐Ÿฅ Health Check Dashboard

Loading health status...
๐Ÿšจ Loading incidents...
โš™๏ธ Loading configuration...

๐ŸŒ Global Settings

Applies to all health checks. Settings are stored locally in this browser.

How often each service's health endpoint is checked.
How often container statistics (CPU/memory) are sampled.
Cap on stored history records per service.
Health history older than this is pruned.
Warn when disk usage exceeds this level.
`); 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'); // ---- Global health settings (retention, polling intervals, max entries, disk threshold) ---- const HEALTH_SETTINGS_KEY = 'dashcaddy-health-settings'; const HEALTH_DEFAULTS = { retentionDays: 30, pollingInterval: 60, statsPollingInterval: 30, maxEntriesPerService: 500, diskUsageThreshold: 80 }; const globalSaveBtn = document.getElementById('health-global-save'); const globalResetBtn = document.getElementById('health-global-reset'); const globalStatusSpan = document.getElementById('health-global-status'); const retentionInput = document.getElementById('health-setting-retention'); const intervalInput = document.getElementById('health-setting-interval'); const statsIntervalInput = document.getElementById('health-setting-stats-interval'); const maxEntriesInput = document.getElementById('health-setting-max-entries'); const diskThresholdInput = document.getElementById('health-setting-disk-threshold'); function loadHealthSettings() { try { const raw = safeGet(HEALTH_SETTINGS_KEY); const saved = raw ? JSON.parse(raw) : {}; return Object.assign({}, HEALTH_DEFAULTS, saved); } catch (_) { return Object.assign({}, HEALTH_DEFAULTS); } } function applyHealthSettingsToUI() { const s = loadHealthSettings(); if (retentionInput) retentionInput.value = s.retentionDays; if (intervalInput) intervalInput.value = s.pollingInterval; if (statsIntervalInput) statsIntervalInput.value = s.statsPollingInterval; if (maxEntriesInput) maxEntriesInput.value = s.maxEntriesPerService; if (diskThresholdInput) diskThresholdInput.value = s.diskUsageThreshold; } function saveHealthSettings() { const settings = { retentionDays: Math.max(1, Math.min(3650, parseInt(retentionInput?.value) || HEALTH_DEFAULTS.retentionDays)), pollingInterval: Math.max(5, Math.min(3600, parseInt(intervalInput?.value) || HEALTH_DEFAULTS.pollingInterval)), statsPollingInterval: Math.max(5, Math.min(3600, parseInt(statsIntervalInput?.value) || HEALTH_DEFAULTS.statsPollingInterval)), maxEntriesPerService: Math.max(10, Math.min(100000, parseInt(maxEntriesInput?.value) || HEALTH_DEFAULTS.maxEntriesPerService)), diskUsageThreshold: Math.max(50, Math.min(99, parseInt(diskThresholdInput?.value) || HEALTH_DEFAULTS.diskUsageThreshold)) }; try { safeSet(HEALTH_SETTINGS_KEY, JSON.stringify(settings)); applyHealthSettingsToUI(); if (globalStatusSpan) { globalStatusSpan.textContent = 'Saved โœ“'; globalStatusSpan.style.color = 'var(--ok-fg)'; setTimeout(() => { if (globalStatusSpan) globalStatusSpan.textContent = ''; }, 2500); } if (typeof showNotification === 'function') showNotification('Global health settings saved', 'success'); } catch (e) { if (globalStatusSpan) { globalStatusSpan.textContent = 'Save failed'; globalStatusSpan.style.color = 'var(--bad-fg)'; } if (typeof showNotification === 'function') showNotification('Failed to save settings: ' + e.message, 'error'); } } function resetHealthSettings() { try { safeSet(HEALTH_SETTINGS_KEY, JSON.stringify(HEALTH_DEFAULTS)); } catch (_) { /* ignore */ } applyHealthSettingsToUI(); if (globalStatusSpan) { globalStatusSpan.textContent = 'Reset to defaults โœ“'; globalStatusSpan.style.color = 'var(--ok-fg)'; setTimeout(() => { if (globalStatusSpan) globalStatusSpan.textContent = ''; }, 2500); } } applyHealthSettingsToUI(); globalSaveBtn?.addEventListener('click', saveHealthSettings); globalResetBtn?.addEventListener('click', resetHealthSettings); // ---- End global health settings ---- 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); })();