// ========== RESOURCE MONITOR (Enhanced) ==========
(function() {
// Inject modal HTML
injectModal('stats-modal', `
๐ Resource Monitor
Real-time and historical CPU, memory, network, and disk usage for containers.
Live Stats
24h Summary
History
Alerts
Container:
1h
24h
7d
30d
1y
๐
Choose a container and time range to view history.
Auto-refresh every 5s
๐ Refresh Now
`);
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'}
Memory
${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 += `
`;
}
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 = 'No containers ';
return;
}
historyContainerSelect.innerHTML = entries
.map(([id, info]) => `${escapeHtml(info.name || id)} `)
.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();
});
})();