// ========== MONITORING WIDGETS ========== // Embeds a compact system-resource + health summary panel directly on the // main dashboard. Replaces the need for a separate monitoring-dashboard.html // page β€” quick at-a-glance stats where you already are. (function () { // ----- Style injection (scoped to .dc-monitor so it doesn't leak) ----- const styleEl = document.createElement('style'); styleEl.textContent = ` .dc-monitor { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 12px; margin-bottom: 16px; padding: 12px 16px; background: var(--card-base); border: 1px solid var(--border); border-radius: var(--radius); } .dc-monitor-card { padding: 10px 12px; background: var(--card-bg, rgba(255,255,255,0.04)); border-radius: 8px; border: 1px solid var(--border); } .dc-monitor-label { font-size: 0.7rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px; } .dc-monitor-value { font-size: 1.4rem; font-weight: 600; color: var(--fg); } .dc-monitor-sub { font-size: 0.7rem; color: var(--muted); margin-top: 4px; } .dc-monitor-bar { margin-top: 6px; width: 100%; height: 4px; background: color-mix(in srgb, var(--muted) 20%, transparent); border-radius: 2px; overflow: hidden; } .dc-monitor-bar-fill { height: 100%; width: 0%; background: var(--ok-fg, #27ae60); transition: width 0.3s ease, background 0.3s ease; } .dc-monitor-bar-fill.warn { background: #f39c12; } .dc-monitor-bar-fill.bad { background: #e74c3c; } .dc-monitor-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; } .dc-monitor-title { font-size: 0.85rem; font-weight: 500; color: var(--muted); display: flex; align-items: center; gap: 6px; } .dc-monitor-pill { display: inline-flex; align-items: center; gap: 4px; padding: 2px 8px; border-radius: 999px; font-size: 0.7rem; font-weight: 500; } .dc-monitor-pill.ok { background: color-mix(in srgb, #27ae60 20%, transparent); color: #27ae60; } .dc-monitor-pill.warn { background: color-mix(in srgb, #f39c12 20%, transparent); color: #f39c12; } .dc-monitor-pill.bad { background: color-mix(in srgb, #e74c3c 20%, transparent); color: #e74c3c; } .dc-monitor-refresh { font-size: 0.7rem; color: var(--muted); opacity: 0.7; } `; document.head.appendChild(styleEl); // ----- Container element (inserted above service-filter-bar) ----- const filterBar = document.getElementById('service-filter-bar'); if (!filterBar) return; const panel = document.createElement('div'); panel.className = 'dc-monitor'; panel.id = 'dc-monitor-panel'; panel.innerHTML = `
πŸ“Š System Overview
β€”
Services
β€”
loading…
Containers Up
β€”
loading…
Avg CPU
β€”
Avg Memory
β€”
Health
β€”
β€”
`; // Insert ABOVE the filter bar filterBar.parentNode.insertBefore(panel, filterBar); // ----- Helpers ----- function setBar(id, pct) { const el = document.getElementById(id); if (!el) return; const p = Math.max(0, Math.min(100, Number(pct) || 0)); el.style.width = p + '%'; el.classList.remove('warn', 'bad'); if (p >= 85) el.classList.add('bad'); else if (p >= 65) el.classList.add('warn'); } function fmtPct(v) { if (v == null || isNaN(v)) return 'β€”'; return (Math.round(v * 10) / 10) + '%'; } function fmtBytes(b) { if (b == null || isNaN(b)) return 'β€”'; const units = ['B', 'KB', 'MB', 'GB', 'TB']; let i = 0; while (b >= 1024 && i < units.length - 1) { b /= 1024; i++; } return b.toFixed(1) + ' ' + units[i]; } // ----- Robust services count ----- // Read from multiple sources so we always have a number: // 1. window.APPS (populated by grid.js after loadServices) // 2. #cards .card elements (post-buildGrid) // 3. live fetch /api/v1/services (last-resort fallback if grid hasn't run) async function fetchServicesCount() { // Source 1+2: window.APPS / DOM cards if (Array.isArray(window.APPS) && window.APPS.length > 0) { const up = document.querySelectorAll('#cards .card[data-status="on"]').length; return { total: window.APPS.length, up, source: 'APPS' }; } const cards = document.querySelectorAll('#cards .card'); if (cards.length > 0) { const up = Array.from(cards).filter(c => c.dataset.status === 'on').length; return { total: cards.length, up, source: 'DOM' }; } // Source 3: fetch live (endpoints may return {success, services:[...]} OR raw array) try { const r = await fetch('/api/v1/services', { cache: 'no-store' }); if (!r.ok) return { total: 0, up: 0, source: 'fetch-fail' }; const body = await r.json(); const list = (body && Array.isArray(body.services)) ? body.services : (Array.isArray(body)) ? body : []; // Persist for the grid so this fallback only fires once if (Array.isArray(window.APPS) || typeof window.APPS === 'undefined') window.APPS = list; const up = document.querySelectorAll('#cards .card[data-status="on"]').length; return { total: list.length, up, source: 'fetch' }; } catch (_) { return { total: 0, up: 0, source: 'fetch-error' }; } } async function setServicesCard() { const { total, up } = await fetchServicesCount(); const el = document.getElementById('dc-monitor-services'); const sub = document.getElementById('dc-monitor-services-sub'); if (el) el.textContent = `${up} / ${total}`; if (sub) sub.textContent = total === 0 ? 'no services yet' : `${up} online Β· ${total - up} offline`; } function applyHealthSummary(data) { const el = document.getElementById('dc-monitor-health'); const sub = document.getElementById('dc-monitor-health-sub'); if (!el) return; if (!data || data.summary == null) { el.textContent = 'β€”'; if (sub) sub.textContent = 'no data'; return; } const s = data.summary; const healthy = s.healthy ?? s.up ?? 0; const unhealthy = s.unhealthy ?? s.down ?? 0; const total = s.total ?? (healthy + unhealthy); el.textContent = `${healthy}/${total}`; if (sub) { if (unhealthy === 0) { sub.innerHTML = '● all healthy'; } else if (unhealthy <= 2) { sub.innerHTML = `● ${unhealthy} degraded`; } else { sub.innerHTML = `● ${unhealthy} down`; } } } // ----- Data fetches ----- async function fetchStats() { try { const r = await fetch('/api/v1/monitoring/stats', { cache: 'no-store' }); if (!r.ok) return null; const data = await r.json(); return (data && data.stats) ? data.stats : null; } catch (_) { return null; } } async function fetchHealth() { try { const r = await fetch('/api/v1/health-checks/status', { cache: 'no-store' }); if (!r.ok) return null; return await r.json(); } catch (_) { return null; } } function applyStats(stats) { const containers = document.getElementById('dc-monitor-containers'); const containersSub = document.getElementById('dc-monitor-containers-sub'); const cpuEl = document.getElementById('dc-monitor-cpu'); const memEl = document.getElementById('dc-monitor-mem'); if (!stats) { if (containers) containers.textContent = 'β€”'; if (cpuEl) cpuEl.textContent = 'β€”'; if (memEl) memEl.textContent = 'β€”'; return; } const entries = Object.values(stats); if (entries.length === 0) { if (containers) containers.textContent = '0'; if (containersSub) containersSub.textContent = 'no containers reporting'; if (cpuEl) cpuEl.textContent = '0%'; if (memEl) memEl.textContent = '0%'; setBar('dc-monitor-cpu-bar', 0); setBar('dc-monitor-mem-bar', 0); return; } let cpuSum = 0, memSum = 0, memBytes = 0, cpuCount = 0, memCount = 0; entries.forEach(s => { // CPU may be percentage (0-100) or fraction (0-1) β€” handle both if (s.cpu != null) { const cpu = Number(s.cpu); if (!isNaN(cpu)) { cpuSum += cpu > 1 ? cpu : cpu * 100; cpuCount++; } } if (s.memory != null) { const mem = Number(s.memory); if (!isNaN(mem)) { memSum += mem; memBytes += Number(s.memoryUsage || 0); memCount++; } } }); const avgCpu = cpuCount ? cpuSum / cpuCount : 0; const avgMem = memCount ? memSum / memCount : 0; if (containers) containers.textContent = String(entries.length); if (containersSub) { const memTxt = memBytes ? ` Β· ${fmtBytes(memBytes)} RAM` : ''; containersSub.textContent = `running${memTxt}`; } if (cpuEl) cpuEl.textContent = fmtPct(avgCpu); if (memEl) memEl.textContent = fmtPct(avgMem); setBar('dc-monitor-cpu-bar', avgCpu); setBar('dc-monitor-mem-bar', avgMem); } // ----- Public refresh function ----- let inFlight = false; async function refresh() { if (inFlight) return; inFlight = true; try { setServicesCard(); const [stats, health] = await Promise.all([fetchStats(), fetchHealth()]); applyStats(stats); applyHealthSummary(health); const stamp = document.getElementById('dc-monitor-refresh-stamp'); if (stamp) { const now = new Date(); stamp.textContent = `updated ${now.toLocaleTimeString()}`; } } finally { inFlight = false; } } // Expose for init.js to call once and re-call after each refreshAll cycle window.refreshMonitoringWidgets = refresh; // Auto-refresh on the STATS interval (separate from full DASHBOARD refresh) setInterval(refresh, (typeof DC !== 'undefined' && DC.POLL && DC.POLL.STATS) || 5000); // Refresh once on first script load (init.js also calls this; double-call is harmless) setTimeout(refresh, 200); })();