From c6f4314fc8147b3b3ae27b0029c62fbf32e0a290 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 7 Nov 2025 08:45:40 +0000 Subject: [PATCH 1/6] Initial plan From 1c1d3de6cb98ffafeccc8c97a0231e5fcb29de91 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 7 Nov 2025 08:51:41 +0000 Subject: [PATCH 2/6] Optimize performance: cache modules, improve string operations, reduce duplication Co-authored-by: MNDL-27 <96876555+MNDL-27@users.noreply.github.com> --- public/app.js | 154 ++++++++++++++++++++-------------- server/docker/engineClient.js | 5 +- server/index.js | 3 +- server/routes/containers.js | 14 ++-- server/sockets/logs.js | 49 +++++++++-- 5 files changed, 140 insertions(+), 85 deletions(-) diff --git a/public/app.js b/public/app.js index 9442ad0..c3e2814 100644 --- a/public/app.js +++ b/public/app.js @@ -1,6 +1,7 @@ let expandedContainer = null; // Only one expanded at a time let statIntervals = {}; // Track intervals for live updates per container let charts = {}; +let actionInProgress = false; // Prevent multiple simultaneous actions function getStatusColor(status) { if (status && status.includes('Up') && status.includes('healthy')) return 'bg-green-200 text-green-800'; @@ -13,6 +14,40 @@ function getStatusText(status) { return 'Stopped'; } +// Helper function to parse Docker stats +function parseDockerStats(stats) { + let cpu = 0, ram = 0, ramTotal = 1, rx = 0, tx = 0; + + // CPU calculation + if (stats.cpu_stats && stats.precpu_stats) { + const cpuDelta = stats.cpu_stats.cpu_usage.total_usage - stats.precpu_stats.cpu_usage.total_usage; + const sysDelta = stats.cpu_stats.system_cpu_usage - stats.precpu_stats.system_cpu_usage; + const cpuCount = stats.cpu_stats.online_cpus || 1; + if (cpuDelta > 0 && sysDelta > 0) { + cpu = ((cpuDelta / sysDelta) * cpuCount * 100); + } + } + + // Memory calculation + if (stats.memory_stats && stats.memory_stats.usage) { + ram = stats.memory_stats.usage / (1024 * 1024 * 1024); + ramTotal = stats.memory_stats.limit / (1024 * 1024 * 1024); + } + + // Network calculation + if (stats.networks) { + let totalRx = 0, totalTx = 0; + Object.values(stats.networks).forEach(nw => { + totalRx += nw.rx_bytes || 0; + totalTx += nw.tx_bytes || 0; + }); + rx = totalRx / 1024; + tx = totalTx / 1024; + } + + return { cpu, ram, ramTotal, rx, tx }; +} + function stopStatsUpdates(id) { if (statIntervals[id]) { clearInterval(statIntervals[id]); @@ -96,13 +131,20 @@ window.expandContainer = function(id) { }; window.containerAction = function(id, action) { + if (actionInProgress) { + return; // Prevent multiple simultaneous actions + } + actionInProgress = true; fetch(`/api/containers/${id}/${action}`, { method: 'POST' }) .then(res => res.json()) .then(() => { fetchStatsAndRender(); alert(`Action ${action} on container ${id} successful!`); }) - .catch(err => alert(`Failed to ${action}: ${err}`)); + .catch(err => alert(`Failed to ${action}: ${err}`)) + .finally(() => { + actionInProgress = false; + }); }; function startStatsUpdates(id) { @@ -111,74 +153,56 @@ function startStatsUpdates(id) { // history for chart let cpuH=[], ramH=[], netH=[]; async function pollStats() { - // poll stats - const res = await fetch(`/api/containers/${id}/stats`); - const stats = await res.json(); - // Parse stats (your Docker parse logic as before) - let cpu = 0, ram=0, ramTotal=1, rx=0, tx=0; try { - if (stats.cpu_stats && stats.precpu_stats) { - const cpuDelta = stats.cpu_stats.cpu_usage.total_usage - stats.precpu_stats.cpu_usage.total_usage; - const sysDelta = stats.cpu_stats.system_cpu_usage - stats.precpu_stats.system_cpu_usage; - const cpuCount = stats.cpu_stats.online_cpus || 1; - if (cpuDelta > 0 && sysDelta > 0) { - cpu = ((cpuDelta / sysDelta) * cpuCount * 100); - } - } - if (stats.memory_stats && stats.memory_stats.usage) { - ram = stats.memory_stats.usage / 1024 / 1024 / 1024; - ramTotal = stats.memory_stats.limit / 1024 / 1024 / 1024; - } - if (stats.networks) { - let totalRx = 0, totalTx = 0; - Object.values(stats.networks).forEach(nw => { - totalRx += nw.rx_bytes || 0; - totalTx += nw.tx_bytes || 0; - }); - rx = totalRx / 1024; - tx = totalTx / 1024; - } - } catch(e) {} + // poll stats + const res = await fetch(`/api/containers/${id}/stats`); + const stats = await res.json(); + + // Parse stats using helper function + const { cpu, ram, ramTotal, rx, tx } = parseDockerStats(stats); - // keep short history - cpuH.push(cpu); if (cpuH.length>15) cpuH.shift(); - ramH.push(ram); if (ramH.length>15) ramH.shift(); - netH.push(rx); if (netH.length>15) netH.shift(); + // keep short history + cpuH.push(cpu); if (cpuH.length>15) cpuH.shift(); + ramH.push(ram); if (ramH.length>15) ramH.shift(); + netH.push(rx); if (netH.length>15) netH.shift(); - // Update stats UI - const cpuEL = document.getElementById(`cpu-${id}`); - const ramEL = document.getElementById(`ram-${id}`); - const netEL = document.getElementById(`net-${id}`); - if(cpuEL) cpuEL.textContent = `${cpu.toFixed(1)}%`; - if(ramEL) ramEL.textContent = `${ram.toFixed(2)} / ${ramTotal.toFixed(2)} GB`; - if(netEL) netEL.textContent = `↑ ${rx.toFixed(1)} KB/s ↓ ${tx.toFixed(1)} KB/s`; + // Update stats UI + const cpuEL = document.getElementById(`cpu-${id}`); + const ramEL = document.getElementById(`ram-${id}`); + const netEL = document.getElementById(`net-${id}`); + if(cpuEL) cpuEL.textContent = `${cpu.toFixed(1)}%`; + if(ramEL) ramEL.textContent = `${ram.toFixed(2)} / ${ramTotal.toFixed(2)} GB`; + if(netEL) netEL.textContent = `↑ ${rx.toFixed(1)} KB/s ↓ ${tx.toFixed(1)} KB/s`; - // Render charts (handle repeated creation) - if(!charts[id]) { - charts[id] = { - cpu: new Chart(document.getElementById(`cpuChart-${id}`), { - type: 'line', - data: { labels:Array(cpuH.length).fill(''), datasets:[{label:"CPU",data:cpuH,borderColor:"#3b82f6",backgroundColor:"rgba(59,130,246,.11)",fill:true,tension:.45}] }, - options: {responsive:true, plugins:{legend:{display:false}}, scales:{x:{display:false},y:{display:false,min:0,max:100}}} - }), - ram: new Chart(document.getElementById(`ramChart-${id}`), { - type: 'line', - data: { labels:Array(ramH.length).fill(''), datasets:[{label:"RAM",data:ramH,borderColor:"#10b981",backgroundColor:"rgba(16,185,129,.15)",fill:true,tension:.45}] }, - options:{responsive:true,plugins:{legend:{display:false}},scales:{x:{display:false},y:{display:false,min:0}}} - }), - net: new Chart(document.getElementById(`netChart-${id}`), { - type: 'line', - data:{labels:Array(netH.length).fill(''),datasets:[{label:"NET",data:netH,borderColor:"#fde047",backgroundColor:"rgba(250,204,21,.09)",fill:true,tension:.45}]}, - options:{responsive:true,plugins:{legend:{display:false}},scales:{x:{display:false},y:{display:false,min:0}}} - }), - }; - } else { - charts[id].cpu.data.datasets[0].data = cpuH; - charts[id].cpu.update('none'); - charts[id].ram.data.datasets[0].data = ramH; - charts[id].ram.update('none'); - charts[id].net.data.datasets[0].data = netH; - charts[id].net.update('none'); + // Render charts (handle repeated creation) + if(!charts[id]) { + charts[id] = { + cpu: new Chart(document.getElementById(`cpuChart-${id}`), { + type: 'line', + data: { labels:Array(cpuH.length).fill(''), datasets:[{label:"CPU",data:cpuH,borderColor:"#3b82f6",backgroundColor:"rgba(59,130,246,.11)",fill:true,tension:.45}] }, + options: {responsive:true, plugins:{legend:{display:false}}, scales:{x:{display:false},y:{display:false,min:0,max:100}}} + }), + ram: new Chart(document.getElementById(`ramChart-${id}`), { + type: 'line', + data: { labels:Array(ramH.length).fill(''), datasets:[{label:"RAM",data:ramH,borderColor:"#10b981",backgroundColor:"rgba(16,185,129,.15)",fill:true,tension:.45}] }, + options:{responsive:true,plugins:{legend:{display:false}},scales:{x:{display:false},y:{display:false,min:0}}} + }), + net: new Chart(document.getElementById(`netChart-${id}`), { + type: 'line', + data:{labels:Array(netH.length).fill(''),datasets:[{label:"NET",data:netH,borderColor:"#fde047",backgroundColor:"rgba(250,204,21,.09)",fill:true,tension:.45}]}, + options:{responsive:true,plugins:{legend:{display:false}},scales:{x:{display:false},y:{display:false,min:0}}} + }), + }; + } else { + charts[id].cpu.data.datasets[0].data = cpuH; + charts[id].cpu.update('none'); + charts[id].ram.data.datasets[0].data = ramH; + charts[id].ram.update('none'); + charts[id].net.data.datasets[0].data = netH; + charts[id].net.update('none'); + } + } catch(e) { + console.error('Error polling stats:', e); } } pollStats(); // initial diff --git a/server/docker/engineClient.js b/server/docker/engineClient.js index 09af2ab..5883252 100644 --- a/server/docker/engineClient.js +++ b/server/docker/engineClient.js @@ -12,9 +12,10 @@ function dockerRequest(path, method = 'GET') { timeout, }; const req = http.request(options, res => { - let data = ''; - res.on('data', chunk => data += chunk); + const chunks = []; + res.on('data', chunk => chunks.push(chunk)); res.on('end', () => { + const data = Buffer.concat(chunks).toString(); try { resolve({ status: res.statusCode, data: JSON.parse(data) }); } catch { diff --git a/server/index.js b/server/index.js index 94fcf3a..a233b33 100644 --- a/server/index.js +++ b/server/index.js @@ -13,6 +13,7 @@ const authRoute = require('./routes/auth'); const { requireAuth, isAuthEnabled } = require('./middleware/auth'); const wsHandlers = require('./sockets'); const path = require('path'); +const crypto = require('crypto'); const config = require('./config/defaults'); const { validateEnv } = require('./config/schema'); try { @@ -72,7 +73,7 @@ app.use('/api/', apiLimiter); // Basic request logging with request ID app.use((req, res, next) => { - req.id = require('crypto').randomUUID(); + req.id = crypto.randomUUID(); res.setHeader('X-Request-ID', req.id); console.log(`[${req.id}] [${new Date().toISOString()}] ${req.method} ${req.url}`); next(); diff --git a/server/routes/containers.js b/server/routes/containers.js index e585197..3804c57 100644 --- a/server/routes/containers.js +++ b/server/routes/containers.js @@ -1,6 +1,7 @@ const express = require('express'); const router = express.Router(); const proxy = require('../docker/proxy'); +const { calculateCPUPercent } = require('../utils/cpu'); const Docker = require('dockerode'); const docker = new Docker({ socketPath: process.env.DOCKER_SOCKET || '/var/run/docker.sock' }); @@ -11,15 +12,10 @@ const metricsHistory = {}; // { [containerId]: [{ time, cpu, ram, rx, tx, disk, function saveContainerMetrics(id, stats, info) { let cpu=0, ram=0, rx=0, tx=0, disk=0, ramTotal=1; try { - if(stats.cpu_stats && stats.precpu_stats) { - const cpuDelta = stats.cpu_stats.cpu_usage.total_usage - stats.precpu_stats.cpu_usage.total_usage; - const sysDelta = stats.cpu_stats.system_cpu_usage - stats.precpu_stats.system_cpu_usage; - const cpuCount = stats.cpu_stats.online_cpus||1; - if(cpuDelta > 0 && sysDelta > 0) cpu = ((cpuDelta/sysDelta)*cpuCount*100); - } + cpu = calculateCPUPercent(stats); if(stats.memory_stats && stats.memory_stats.usage) { - ram = stats.memory_stats.usage / 1024 / 1024 / 1024; - ramTotal = stats.memory_stats.limit / 1024 / 1024 / 1024; + ram = stats.memory_stats.usage / (1024 * 1024 * 1024); + ramTotal = stats.memory_stats.limit / (1024 * 1024 * 1024); } if(stats.networks) { for (const nw of Object.values(stats.networks)) { @@ -27,7 +23,7 @@ function saveContainerMetrics(id, stats, info) { tx += nw.tx_bytes || 0; } } - if(info && info.SizeRw) disk = info.SizeRw / 1024 / 1024 / 1024; + if(info && info.SizeRw) disk = info.SizeRw / (1024 * 1024 * 1024); } catch(e) {} if(!metricsHistory[id]) metricsHistory[id] = []; metricsHistory[id].push({ diff --git a/server/sockets/logs.js b/server/sockets/logs.js index 52aac6a..47906fd 100644 --- a/server/sockets/logs.js +++ b/server/sockets/logs.js @@ -4,7 +4,16 @@ const engine = require('../docker/engineClient'); function streamLogs(ws, id) { let closed = false; let reqHandle = null; - ws.on('close', () => { closed = true; if (reqHandle && reqHandle.abort) try { reqHandle.abort(); } catch (e) {} }); + ws.on('close', () => { + closed = true; + if (reqHandle && reqHandle.abort) { + try { + reqHandle.abort(); + } catch (e) { + // Ignore abort errors + } + } + }); // Try using engineClient streaming which forwards chunks as they arrive try { @@ -14,17 +23,35 @@ function streamLogs(ws, id) { try { // chunk may be a Buffer; convert and split into lines const text = chunk.toString('utf8'); - text.split('\n').forEach(line => { + const lines = text.split('\n'); + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; if (line && line.trim()) { - try { ws.send(JSON.stringify({ log: line })); } catch (e) {} + ws.send(JSON.stringify({ log: line })); } - }); + } } catch (e) { - try { ws.send(JSON.stringify({ error: 'Failed to process log chunk' })); } catch (e) {} + ws.send(JSON.stringify({ error: 'Failed to process log chunk' })); + } + }, + onEnd: () => { + if (!closed) { + try { + ws.send(JSON.stringify({ info: 'log stream ended' })); + } catch (e) { + // Ignore send errors + } } }, - onEnd: () => { if (!closed) try { ws.send(JSON.stringify({ info: 'log stream ended' })); } catch (e) {} }, - onError: (err) => { if (!closed) try { ws.send(JSON.stringify({ error: String(err && err.message ? err.message : err) })); } catch (e) {} } + onError: (err) => { + if (!closed) { + try { + ws.send(JSON.stringify({ error: String(err && err.message ? err.message : err) })); + } catch (e) { + // Ignore send errors + } + } + } }); } catch (err) { // Fallback: try a single request via proxy (non-follow) @@ -32,7 +59,13 @@ function streamLogs(ws, id) { try { const result = await proxy.requestDockerAPI(`/containers/${id}/logs?stdout=1&stderr=1&tail=200`); if (typeof result.data === 'string') { - result.data.split('\n').forEach(line => { if (!closed && line.trim()) ws.send(JSON.stringify({ log: line })); }); + const lines = result.data.split('\n'); + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + if (!closed && line.trim()) { + ws.send(JSON.stringify({ log: line })); + } + } } else { if (!closed) ws.send(JSON.stringify({ log: result.data })); } From 942f6edd99fe282c1becbf06ab2a37b582434054 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 7 Nov 2025 08:53:47 +0000 Subject: [PATCH 3/6] Apply same performance optimizations to docs/app.js Co-authored-by: MNDL-27 <96876555+MNDL-27@users.noreply.github.com> --- docs/app.js | 154 ++++++++++++++++++++++++++++++---------------------- 1 file changed, 89 insertions(+), 65 deletions(-) diff --git a/docs/app.js b/docs/app.js index 9442ad0..c3e2814 100644 --- a/docs/app.js +++ b/docs/app.js @@ -1,6 +1,7 @@ let expandedContainer = null; // Only one expanded at a time let statIntervals = {}; // Track intervals for live updates per container let charts = {}; +let actionInProgress = false; // Prevent multiple simultaneous actions function getStatusColor(status) { if (status && status.includes('Up') && status.includes('healthy')) return 'bg-green-200 text-green-800'; @@ -13,6 +14,40 @@ function getStatusText(status) { return 'Stopped'; } +// Helper function to parse Docker stats +function parseDockerStats(stats) { + let cpu = 0, ram = 0, ramTotal = 1, rx = 0, tx = 0; + + // CPU calculation + if (stats.cpu_stats && stats.precpu_stats) { + const cpuDelta = stats.cpu_stats.cpu_usage.total_usage - stats.precpu_stats.cpu_usage.total_usage; + const sysDelta = stats.cpu_stats.system_cpu_usage - stats.precpu_stats.system_cpu_usage; + const cpuCount = stats.cpu_stats.online_cpus || 1; + if (cpuDelta > 0 && sysDelta > 0) { + cpu = ((cpuDelta / sysDelta) * cpuCount * 100); + } + } + + // Memory calculation + if (stats.memory_stats && stats.memory_stats.usage) { + ram = stats.memory_stats.usage / (1024 * 1024 * 1024); + ramTotal = stats.memory_stats.limit / (1024 * 1024 * 1024); + } + + // Network calculation + if (stats.networks) { + let totalRx = 0, totalTx = 0; + Object.values(stats.networks).forEach(nw => { + totalRx += nw.rx_bytes || 0; + totalTx += nw.tx_bytes || 0; + }); + rx = totalRx / 1024; + tx = totalTx / 1024; + } + + return { cpu, ram, ramTotal, rx, tx }; +} + function stopStatsUpdates(id) { if (statIntervals[id]) { clearInterval(statIntervals[id]); @@ -96,13 +131,20 @@ window.expandContainer = function(id) { }; window.containerAction = function(id, action) { + if (actionInProgress) { + return; // Prevent multiple simultaneous actions + } + actionInProgress = true; fetch(`/api/containers/${id}/${action}`, { method: 'POST' }) .then(res => res.json()) .then(() => { fetchStatsAndRender(); alert(`Action ${action} on container ${id} successful!`); }) - .catch(err => alert(`Failed to ${action}: ${err}`)); + .catch(err => alert(`Failed to ${action}: ${err}`)) + .finally(() => { + actionInProgress = false; + }); }; function startStatsUpdates(id) { @@ -111,74 +153,56 @@ function startStatsUpdates(id) { // history for chart let cpuH=[], ramH=[], netH=[]; async function pollStats() { - // poll stats - const res = await fetch(`/api/containers/${id}/stats`); - const stats = await res.json(); - // Parse stats (your Docker parse logic as before) - let cpu = 0, ram=0, ramTotal=1, rx=0, tx=0; try { - if (stats.cpu_stats && stats.precpu_stats) { - const cpuDelta = stats.cpu_stats.cpu_usage.total_usage - stats.precpu_stats.cpu_usage.total_usage; - const sysDelta = stats.cpu_stats.system_cpu_usage - stats.precpu_stats.system_cpu_usage; - const cpuCount = stats.cpu_stats.online_cpus || 1; - if (cpuDelta > 0 && sysDelta > 0) { - cpu = ((cpuDelta / sysDelta) * cpuCount * 100); - } - } - if (stats.memory_stats && stats.memory_stats.usage) { - ram = stats.memory_stats.usage / 1024 / 1024 / 1024; - ramTotal = stats.memory_stats.limit / 1024 / 1024 / 1024; - } - if (stats.networks) { - let totalRx = 0, totalTx = 0; - Object.values(stats.networks).forEach(nw => { - totalRx += nw.rx_bytes || 0; - totalTx += nw.tx_bytes || 0; - }); - rx = totalRx / 1024; - tx = totalTx / 1024; - } - } catch(e) {} + // poll stats + const res = await fetch(`/api/containers/${id}/stats`); + const stats = await res.json(); + + // Parse stats using helper function + const { cpu, ram, ramTotal, rx, tx } = parseDockerStats(stats); - // keep short history - cpuH.push(cpu); if (cpuH.length>15) cpuH.shift(); - ramH.push(ram); if (ramH.length>15) ramH.shift(); - netH.push(rx); if (netH.length>15) netH.shift(); + // keep short history + cpuH.push(cpu); if (cpuH.length>15) cpuH.shift(); + ramH.push(ram); if (ramH.length>15) ramH.shift(); + netH.push(rx); if (netH.length>15) netH.shift(); - // Update stats UI - const cpuEL = document.getElementById(`cpu-${id}`); - const ramEL = document.getElementById(`ram-${id}`); - const netEL = document.getElementById(`net-${id}`); - if(cpuEL) cpuEL.textContent = `${cpu.toFixed(1)}%`; - if(ramEL) ramEL.textContent = `${ram.toFixed(2)} / ${ramTotal.toFixed(2)} GB`; - if(netEL) netEL.textContent = `↑ ${rx.toFixed(1)} KB/s ↓ ${tx.toFixed(1)} KB/s`; + // Update stats UI + const cpuEL = document.getElementById(`cpu-${id}`); + const ramEL = document.getElementById(`ram-${id}`); + const netEL = document.getElementById(`net-${id}`); + if(cpuEL) cpuEL.textContent = `${cpu.toFixed(1)}%`; + if(ramEL) ramEL.textContent = `${ram.toFixed(2)} / ${ramTotal.toFixed(2)} GB`; + if(netEL) netEL.textContent = `↑ ${rx.toFixed(1)} KB/s ↓ ${tx.toFixed(1)} KB/s`; - // Render charts (handle repeated creation) - if(!charts[id]) { - charts[id] = { - cpu: new Chart(document.getElementById(`cpuChart-${id}`), { - type: 'line', - data: { labels:Array(cpuH.length).fill(''), datasets:[{label:"CPU",data:cpuH,borderColor:"#3b82f6",backgroundColor:"rgba(59,130,246,.11)",fill:true,tension:.45}] }, - options: {responsive:true, plugins:{legend:{display:false}}, scales:{x:{display:false},y:{display:false,min:0,max:100}}} - }), - ram: new Chart(document.getElementById(`ramChart-${id}`), { - type: 'line', - data: { labels:Array(ramH.length).fill(''), datasets:[{label:"RAM",data:ramH,borderColor:"#10b981",backgroundColor:"rgba(16,185,129,.15)",fill:true,tension:.45}] }, - options:{responsive:true,plugins:{legend:{display:false}},scales:{x:{display:false},y:{display:false,min:0}}} - }), - net: new Chart(document.getElementById(`netChart-${id}`), { - type: 'line', - data:{labels:Array(netH.length).fill(''),datasets:[{label:"NET",data:netH,borderColor:"#fde047",backgroundColor:"rgba(250,204,21,.09)",fill:true,tension:.45}]}, - options:{responsive:true,plugins:{legend:{display:false}},scales:{x:{display:false},y:{display:false,min:0}}} - }), - }; - } else { - charts[id].cpu.data.datasets[0].data = cpuH; - charts[id].cpu.update('none'); - charts[id].ram.data.datasets[0].data = ramH; - charts[id].ram.update('none'); - charts[id].net.data.datasets[0].data = netH; - charts[id].net.update('none'); + // Render charts (handle repeated creation) + if(!charts[id]) { + charts[id] = { + cpu: new Chart(document.getElementById(`cpuChart-${id}`), { + type: 'line', + data: { labels:Array(cpuH.length).fill(''), datasets:[{label:"CPU",data:cpuH,borderColor:"#3b82f6",backgroundColor:"rgba(59,130,246,.11)",fill:true,tension:.45}] }, + options: {responsive:true, plugins:{legend:{display:false}}, scales:{x:{display:false},y:{display:false,min:0,max:100}}} + }), + ram: new Chart(document.getElementById(`ramChart-${id}`), { + type: 'line', + data: { labels:Array(ramH.length).fill(''), datasets:[{label:"RAM",data:ramH,borderColor:"#10b981",backgroundColor:"rgba(16,185,129,.15)",fill:true,tension:.45}] }, + options:{responsive:true,plugins:{legend:{display:false}},scales:{x:{display:false},y:{display:false,min:0}}} + }), + net: new Chart(document.getElementById(`netChart-${id}`), { + type: 'line', + data:{labels:Array(netH.length).fill(''),datasets:[{label:"NET",data:netH,borderColor:"#fde047",backgroundColor:"rgba(250,204,21,.09)",fill:true,tension:.45}]}, + options:{responsive:true,plugins:{legend:{display:false}},scales:{x:{display:false},y:{display:false,min:0}}} + }), + }; + } else { + charts[id].cpu.data.datasets[0].data = cpuH; + charts[id].cpu.update('none'); + charts[id].ram.data.datasets[0].data = ramH; + charts[id].ram.update('none'); + charts[id].net.data.datasets[0].data = netH; + charts[id].net.update('none'); + } + } catch(e) { + console.error('Error polling stats:', e); } } pollStats(); // initial From 79e074231d5aa2e43e59418da47bfcc49ca2ccf8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 7 Nov 2025 08:56:04 +0000 Subject: [PATCH 4/6] Add retry limits and optimize buffer handling in API clients Co-authored-by: MNDL-27 <96876555+MNDL-27@users.noreply.github.com> --- server/docker/portainerClient.js | 5 +++-- server/docker/qbittorrentClient.js | 13 ++++++++++--- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/server/docker/portainerClient.js b/server/docker/portainerClient.js index 454b46a..1eba6c3 100644 --- a/server/docker/portainerClient.js +++ b/server/docker/portainerClient.js @@ -21,9 +21,10 @@ function portainerRequest(path, method = 'GET') { timeout: 10000, }; const req = https.request(options, res => { - let data = ''; - res.on('data', chunk => data += chunk); + const chunks = []; + res.on('data', chunk => chunks.push(chunk)); res.on('end', () => { + const data = Buffer.concat(chunks).toString(); try { resolve({ status: res.statusCode, data: JSON.parse(data) }); } catch { diff --git a/server/docker/qbittorrentClient.js b/server/docker/qbittorrentClient.js index 0ae81ee..d8c5da4 100644 --- a/server/docker/qbittorrentClient.js +++ b/server/docker/qbittorrentClient.js @@ -7,6 +7,8 @@ class QBittorrentClient { this.username = username || 'admin'; this.password = password || 'adminadmin'; this.cookie = null; + this.loginAttempts = 0; + this.maxLoginAttempts = 2; } /** @@ -28,6 +30,7 @@ class QBittorrentClient { const cookies = response.headers['set-cookie']; if (cookies && cookies.length > 0) { this.cookie = cookies[0].split(';')[0]; + this.loginAttempts = 0; // Reset on successful login return true; } return false; @@ -72,12 +75,14 @@ class QBittorrentClient { }, }; } catch (error) { - // If unauthorized, try to login again - if (error.response && error.response.status === 403) { + // If unauthorized, try to login again (with retry limit) + if (error.response && error.response.status === 403 && this.loginAttempts < this.maxLoginAttempts) { this.cookie = null; + this.loginAttempts++; return this.getTransferInfo(); } + this.loginAttempts = 0; // Reset counter console.error('Failed to get qBittorrent transfer info:', error.message); return { success: false, @@ -109,11 +114,13 @@ class QBittorrentClient { data: response.data, }; } catch (error) { - if (error.response && error.response.status === 403) { + if (error.response && error.response.status === 403 && this.loginAttempts < this.maxLoginAttempts) { this.cookie = null; + this.loginAttempts++; return this.getTorrents(); } + this.loginAttempts = 0; // Reset counter console.error('Failed to get qBittorrent torrents:', error.message); return { success: false, From da3c1d93d298d1691e4bf601da3e7d4a3365656b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 7 Nov 2025 08:58:34 +0000 Subject: [PATCH 5/6] Add proper error handling for WebSocket send operations Co-authored-by: MNDL-27 <96876555+MNDL-27@users.noreply.github.com> --- server/sockets/logs.js | 34 +++++++++++++++++++++++++++++----- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/server/sockets/logs.js b/server/sockets/logs.js index 47906fd..efaa33e 100644 --- a/server/sockets/logs.js +++ b/server/sockets/logs.js @@ -27,11 +27,19 @@ function streamLogs(ws, id) { for (let i = 0; i < lines.length; i++) { const line = lines[i]; if (line && line.trim()) { - ws.send(JSON.stringify({ log: line })); + try { + ws.send(JSON.stringify({ log: line })); + } catch (sendErr) { + // Connection closed, ignore + } } } } catch (e) { - ws.send(JSON.stringify({ error: 'Failed to process log chunk' })); + try { + ws.send(JSON.stringify({ error: 'Failed to process log chunk' })); + } catch (sendErr) { + // Connection closed, ignore + } } }, onEnd: () => { @@ -63,14 +71,30 @@ function streamLogs(ws, id) { for (let i = 0; i < lines.length; i++) { const line = lines[i]; if (!closed && line.trim()) { - ws.send(JSON.stringify({ log: line })); + try { + ws.send(JSON.stringify({ log: line })); + } catch (sendErr) { + // Connection closed, ignore + } } } } else { - if (!closed) ws.send(JSON.stringify({ log: result.data })); + if (!closed) { + try { + ws.send(JSON.stringify({ log: result.data })); + } catch (sendErr) { + // Connection closed, ignore + } + } } } catch (err2) { - if (!closed) ws.send(JSON.stringify({ error: err2.message })); + if (!closed) { + try { + ws.send(JSON.stringify({ error: err2.message })); + } catch (sendErr) { + // Connection closed, ignore + } + } } })(); } From a9be271e98659d4ef85eb6937ab65c0af7aaea0f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 7 Nov 2025 09:00:34 +0000 Subject: [PATCH 6/6] Add constant for byte-to-GB conversion to avoid repeated calculation Co-authored-by: MNDL-27 <96876555+MNDL-27@users.noreply.github.com> --- server/routes/containers.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/server/routes/containers.js b/server/routes/containers.js index 3804c57..1b4a668 100644 --- a/server/routes/containers.js +++ b/server/routes/containers.js @@ -7,6 +7,7 @@ const docker = new Docker({ socketPath: process.env.DOCKER_SOCKET || '/var/run/d // --- METRICS HISTORY SUPPORT --- const METRICS_WINDOW = 300; // e.g. 300 points = 10 minutes if polled every 2s +const BYTES_TO_GB = 1024 * 1024 * 1024; // Constant for byte to GB conversion const metricsHistory = {}; // { [containerId]: [{ time, cpu, ram, rx, tx, disk, ramTotal }, ...] } function saveContainerMetrics(id, stats, info) { @@ -14,8 +15,8 @@ function saveContainerMetrics(id, stats, info) { try { cpu = calculateCPUPercent(stats); if(stats.memory_stats && stats.memory_stats.usage) { - ram = stats.memory_stats.usage / (1024 * 1024 * 1024); - ramTotal = stats.memory_stats.limit / (1024 * 1024 * 1024); + ram = stats.memory_stats.usage / BYTES_TO_GB; + ramTotal = stats.memory_stats.limit / BYTES_TO_GB; } if(stats.networks) { for (const nw of Object.values(stats.networks)) { @@ -23,7 +24,7 @@ function saveContainerMetrics(id, stats, info) { tx += nw.tx_bytes || 0; } } - if(info && info.SizeRw) disk = info.SizeRw / (1024 * 1024 * 1024); + if(info && info.SizeRw) disk = info.SizeRw / BYTES_TO_GB; } catch(e) {} if(!metricsHistory[id]) metricsHistory[id] = []; metricsHistory[id].push({