// ========== RESOURCE MONITOR (Enhanced) ========== (function() { // Inject modal HTML injectModal('stats-modal', `

๐Ÿ“Š Resource Monitor

Loading container stats...
๐Ÿ“ˆ Loading 24-hour aggregated metrics...
๐Ÿ“Š Choose a container and time range to view history.
๐Ÿ”” Loading alert configurations...
`); const modal = document.getElementById('stats-modal'); const openBtn = document.getElementById('container-stats-btn'); const cancelBtn = document.getElementById('stats-cancel'); const refreshBtn = document.getElementById('stats-refresh-btn'); const autoRefreshCheckbox = document.getElementById('stats-auto-refresh'); const container = document.getElementById('stats-container'); const aggregatedContainer = document.getElementById('stats-aggregated-container'); const alertsContainer = document.getElementById('stats-alerts-container'); const lastUpdateSpan = document.getElementById('stats-last-update'); let refreshInterval = null; let cachedMonitoringData = null; function formatBytes(bytes) { if (bytes === 0 || !bytes) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]; } function getCpuColor(percent) { if (percent < 30) return '#2ecc71'; if (percent < 70) return '#f39c12'; return '#e74c3c'; } function getMemColor(percent) { if (percent < 50) return '#2ecc71'; if (percent < 80) return '#f39c12'; return '#e74c3c'; } async function loadStats() { try { // Try new monitoring API first, fall back to old let stats = null; let isNewApi = false; try { const res = await fetch('/api/v1/monitoring/stats'); const data = await res.json(); if (data.success && data.stats) { stats = data.stats; isNewApi = true; cachedMonitoringData = data.stats; } } catch (_) {} if (!isNewApi) { const response = await fetch('/api/v1/stats/containers'); const data = await response.json(); if (data.success && data.stats) { // Convert array format to object format stats = {}; for (const s of data.stats) { stats[s.name] = { name: s.name, current: { cpu: s.cpu, memory: { percent: s.memory.percent, usage: s.memory.used, limit: s.memory.limit, usageMB: Math.round(s.memory.used / 1048576), limitMB: Math.round(s.memory.limit / 1048576) }, network: { rxBytes: s.network.rx, txBytes: s.network.tx, rxMB: (s.network.rx / 1048576).toFixed(1), txMB: (s.network.tx / 1048576).toFixed(1) }, disk: { readMB: 0, writeMB: 0 } }, status: s.status }; } cachedMonitoringData = stats; } } if (!stats || Object.keys(stats).length === 0) { container.innerHTML = '
No running containers found
'; return; } let html = '
'; for (const [id, info] of Object.entries(stats)) { const cur = info.current || info; const cpu = cur.cpu?.percent || 0; const mem = cur.memory?.percent || 0; const cpuColor = getCpuColor(cpu); const memColor = getMemColor(mem); const memUsed = cur.memory?.usage || cur.memory?.used || 0; const memLimit = cur.memory?.limit || 0; const netRx = cur.network?.rxBytes || cur.network?.rx || 0; const netTx = cur.network?.txBytes || cur.network?.tx || 0; const agg = info.aggregated; html += `
${info.name || id} ${agg ? `avg ${agg.cpu?.avg?.toFixed(0) || 0}% cpu` : ''} ${info.status || 'running'}
CPU
${cpu.toFixed(1)}%
Memory
${mem.toFixed(1)}%
${formatBytes(memUsed)} / ${formatBytes(memLimit)}
Network
โ†“ ${formatBytes(netRx)} / โ†‘ ${formatBytes(netTx)}
`; } html += '
'; container.innerHTML = html; lastUpdateSpan.textContent = 'Updated: ' + new Date().toLocaleTimeString(); } catch (e) { container.innerHTML = `
โŒ Failed to load stats: ${escapeHtml(e.message)}
`; } } // === 24h Aggregated Tab === async function loadAggregated() { if (!aggregatedContainer) return; const data = cachedMonitoringData; if (!data || Object.keys(data).length === 0) { aggregatedContainer.innerHTML = '
๐Ÿ“ˆNo monitoring data available. Open the Live Stats tab first.
'; return; } let html = '
'; for (const [id, info] of Object.entries(data)) { const agg = info.aggregated; if (!agg) continue; html += `
${info.name || id}
${agg.cpu?.avg?.toFixed(1) || 0}%Avg CPU
${agg.cpu?.max?.toFixed(1) || 0}%Max CPU
${agg.memory?.avg?.toFixed(1) || 0}%Avg Mem
${agg.memory?.max?.toFixed(1) || 0}%Max Mem
${agg.dataPoints ? `
${agg.dataPoints} data points over ${agg.timeRange || 24}h
` : ''}
`; } html += '
'; aggregatedContainer.innerHTML = html; } // === Alerts Tab === async function loadAlerts() { if (!alertsContainer) return; alertsContainer.innerHTML = '
Loading alerts...
'; const data = cachedMonitoringData; if (!data || Object.keys(data).length === 0) { alertsContainer.innerHTML = '
๐Ÿ””No containers found. Open the Live Stats tab first.
'; return; } let html = '
'; for (const [id, info] of Object.entries(data)) { const alertCfg = info.alertConfig || {}; html += `
${info.name || id}
`; } html += '
'; alertsContainer.innerHTML = html; // Wire up save buttons alertsContainer.querySelectorAll('.alert-save-btn').forEach(btn => { btn.addEventListener('click', async () => { const cId = btn.dataset.container; const enabled = alertsContainer.querySelector(`.alert-enabled[data-container="${cId}"]`)?.checked || false; const cpuThreshold = parseInt(alertsContainer.querySelector(`.alert-cpu[data-container="${cId}"]`)?.value) || 80; const memoryThreshold = parseInt(alertsContainer.querySelector(`.alert-mem[data-container="${cId}"]`)?.value) || 85; const cooldownMinutes = parseInt(alertsContainer.querySelector(`.alert-cooldown[data-container="${cId}"]`)?.value) || 15; const autoRestart = alertsContainer.querySelector(`.alert-autorestart[data-container="${cId}"]`)?.checked || false; try { const res = await secureFetch(`/api/v1/monitoring/alerts/${cId}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ enabled, cpuThreshold, memoryThreshold, cooldownMinutes, autoRestart }) }); const data = await res.json(); btn.textContent = data.success ? 'โœ… Saved' : 'โš ๏ธ Failed'; setTimeout(() => { btn.textContent = 'Save'; }, 2000); } catch (e) { btn.textContent = 'โŒ Error'; setTimeout(() => { btn.textContent = 'Save'; }, 2000); } }); }); } function startAutoRefresh() { if (refreshInterval) clearInterval(refreshInterval); if (autoRefreshCheckbox?.checked) { refreshInterval = setInterval(loadStats, DC.POLL.STATS); } } function stopAutoRefresh() { if (refreshInterval) { clearInterval(refreshInterval); refreshInterval = null; } } // Open modal openBtn?.addEventListener('click', () => { modal.classList.add('show'); loadStats(); startAutoRefresh(); }); // Close modal cancelBtn?.addEventListener('click', () => { modal.classList.remove('show'); stopAutoRefresh(); }); modal?.addEventListener('click', (e) => { if (e.target === modal) { modal.classList.remove('show'); stopAutoRefresh(); } }); refreshBtn?.addEventListener('click', loadStats); autoRefreshCheckbox?.addEventListener('change', () => { if (autoRefreshCheckbox.checked) startAutoRefresh(); else stopAutoRefresh(); }); // Lazy-load tabs document.querySelector('[data-panel="stats-aggregated"]')?.addEventListener('click', loadAggregated); document.querySelector('[data-panel="stats-alerts"]')?.addEventListener('click', loadAlerts); // === History Tab === const historyContainerSelect = document.getElementById('stats-history-container'); const historyArea = document.getElementById('stats-history-container-area'); const rangeButtons = document.querySelectorAll('.stats-range-btn'); let currentRange = '1h'; function rangeToMs(range) { switch (range) { case '1h': return 60 * 60 * 1000; case '24h': return 24 * 60 * 60 * 1000; case '7d': return 7 * 24 * 60 * 60 * 1000; case '30d': return 30 * 24 * 60 * 60 * 1000; case '1y': return 365 * 24 * 60 * 60 * 1000; default: return 60 * 60 * 1000; } } function tierLabel(tier) { if (tier === 'raw') return 'live (10s samples)'; if (tier === 'hourly') return 'hourly average'; if (tier === 'daily') return 'daily average'; return tier; } // Simple SVG sparkline โ€” no external lib function renderSparkline(samples, accessor, color, label, unit) { if (!samples || samples.length === 0) { return `
No data for ${escapeHtml(label)}
`; } const values = samples.map(accessor).filter(v => v != null); if (values.length === 0) { return `
No data for ${escapeHtml(label)}
`; } const max = Math.max(...values, 1); const min = Math.min(...values, 0); const range = max - min || 1; const w = 600, h = 80, pad = 4; const stepX = (w - pad * 2) / Math.max(values.length - 1, 1); const points = values.map((v, i) => { const x = pad + i * stepX; const y = h - pad - ((v - min) / range) * (h - pad * 2); return `${x.toFixed(1)},${y.toFixed(1)}`; }).join(' '); const last = values[values.length - 1]; const avg = values.reduce((a, b) => a + b, 0) / values.length; return `
${escapeHtml(label)} last ${last.toFixed(1)}${unit} ยท avg ${avg.toFixed(1)}${unit} ยท max ${max.toFixed(1)}${unit}
`; } function populateHistoryContainerSelect() { if (!historyContainerSelect) return; const data = cachedMonitoringData || {}; const previous = historyContainerSelect.value; const entries = Object.entries(data); if (entries.length === 0) { historyContainerSelect.innerHTML = ''; return; } historyContainerSelect.innerHTML = entries .map(([id, info]) => ``) .join(''); if (previous && data[previous]) historyContainerSelect.value = previous; } async function loadHistory() { if (!historyArea || !historyContainerSelect) return; const containerId = historyContainerSelect.value; if (!containerId) { historyArea.innerHTML = '
๐Ÿ“ŠNo container selected.
'; return; } const endTime = Date.now(); const startTime = endTime - rangeToMs(currentRange); historyArea.innerHTML = '
Loading history...
'; try { const res = await fetch(`/api/v1/monitoring/history/${encodeURIComponent(containerId)}?startTime=${startTime}&endTime=${endTime}`); const data = await res.json(); if (!data.success) throw new Error(data.error || 'Failed to load history'); const samples = data.samples || []; const tier = data.tier || 'raw'; if (samples.length === 0) { historyArea.innerHTML = `
๐Ÿ“ŠNo data for the last ${currentRange}. Tier: ${tierLabel(tier)}.
`; return; } // Different shape between raw vs rolled-up samples const isRaw = tier === 'raw'; const cpuAccessor = isRaw ? (s) => s.cpu?.percent : (s) => s.cpu?.avg; const memAccessor = isRaw ? (s) => s.memory?.percent : (s) => s.memory?.avgPercent; const netRxAccessor = isRaw ? (s) => (s.network?.rxMB || 0) : (s) => (s.network?.rxMB || 0); const netTxAccessor = isRaw ? (s) => (s.network?.txMB || 0) : (s) => (s.network?.txMB || 0); let html = `
${samples.length} samples ยท ${escapeHtml(tierLabel(tier))} ยท ${new Date(startTime).toLocaleString()} โ†’ ${new Date(endTime).toLocaleString()}
`; html += renderSparkline(samples, cpuAccessor, '#2ecc71', 'CPU', '%'); html += renderSparkline(samples, memAccessor, '#3498db', 'Memory', '%'); html += renderSparkline(samples, netRxAccessor, '#9b59b6', 'Network RX', ' MB'); html += renderSparkline(samples, netTxAccessor, '#e67e22', 'Network TX', ' MB'); historyArea.innerHTML = html; } catch (e) { historyArea.innerHTML = `
โš ๏ธFailed to load history: ${escapeHtml(e.message)}
`; } } rangeButtons.forEach(btn => { btn.addEventListener('click', () => { rangeButtons.forEach(b => b.classList.remove('active')); btn.classList.add('active'); currentRange = btn.dataset.range; loadHistory(); }); }); historyContainerSelect?.addEventListener('change', loadHistory); document.querySelector('[data-panel="stats-history"]')?.addEventListener('click', () => { populateHistoryContainerSelect(); loadHistory(); }); })();