// ========== 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 = `
Containers Up
β
loadingβ¦
`;
// 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];
}
function setServicesCard() {
const total = (window.APPS || []).length;
let up = 0;
document.querySelectorAll('#cards .card').forEach(c => {
if (c.dataset.status === 'on') up++;
});
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);
})();