let clientsChart = null; let commandsChart = null; let countryMap = null; let countriesLayer = null; let countryCounts = {}; let maxCountryCount = 0; let latestByCountry = {}; const GEOJSON_URL = "https://cdn.jsdelivr.net/gh/datasets/geo-countries@master/data/countries.geojson"; if (typeof Chart !== "undefined") { Chart.defaults.color = "#cbd5e1"; Chart.defaults.borderColor = "rgba(100, 116, 139, 0.25)"; Chart.defaults.font.family = "Inter, system-ui, sans-serif"; } function initCharts() { const clientsCtx = document.getElementById("clients-chart"); const commandsCtx = document.getElementById("commands-chart"); clientsChart = new Chart(clientsCtx, { type: "line", data: { labels: [], datasets: [ { label: "Clients Online", data: [], borderColor: "rgb(96, 165, 250)", backgroundColor: "rgba(96, 165, 250, 0.1)", fill: true, tension: 0.4, }, ], }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false }, tooltip: { backgroundColor: "#0f172a", titleColor: "#e2e8f0", bodyColor: "#cbd5e1", borderColor: "#334155", borderWidth: 1, }, }, scales: { y: { beginAtZero: true, ticks: { color: "#94a3b8", stepSize: 1 }, grid: { color: "rgba(100, 116, 139, 0.1)" }, }, x: { ticks: { color: "#94a3b8", maxTicksLimit: 10 }, grid: { color: "rgba(100, 116, 139, 0.1)" }, }, }, }, }); commandsChart = new Chart(commandsCtx, { type: "line", data: { labels: [], datasets: [ { label: "Commands/Min", data: [], borderColor: "rgb(192, 132, 252)", backgroundColor: "rgba(192, 132, 252, 0.1)", fill: true, tension: 0.4, }, ], }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false }, tooltip: { backgroundColor: "#0f172a", titleColor: "#e2e8f0", bodyColor: "#cbd5e1", borderColor: "#334155", borderWidth: 1, }, }, scales: { y: { beginAtZero: true, ticks: { color: "#94a3b8" }, grid: { color: "rgba(100, 116, 139, 0.1)" }, }, x: { ticks: { color: "#94a3b8", maxTicksLimit: 10 }, grid: { color: "rgba(100, 116, 139, 0.1)" }, }, }, }, }); } function formatBytes(bytes) { if (bytes === 0) return "0 B"; const k = 1024; const sizes = ["B", "KB", "MB", "GB", "TB"]; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i]; } function escapeHtml(text) { const div = document.createElement("div"); div.textContent = text == null ? "" : String(text); return div.innerHTML; } function formatDuration(ms) { const seconds = Math.floor(ms / 1000); const minutes = Math.floor(seconds / 60); const hours = Math.floor(minutes / 60); const days = Math.floor(hours / 24); if (days > 0) return `${days}d ${hours % 24}h`; if (hours > 0) return `${hours}h ${minutes % 60}m`; if (minutes > 0) return `${minutes}m ${seconds % 60}s`; return `${seconds}s`; } function formatMs(value) { if (!Number.isFinite(value)) return "0 ms"; return `${Math.round(value)} ms`; } function formatTime(timestamp) { const date = new Date(timestamp); return date.toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit", }); } function animateCounter(element, newValue, duration = 800) { const oldValue = parseInt(element.textContent.replace(/,/g, "")) || 0; if (oldValue === newValue) return; const startTime = performance.now(); const diff = newValue - oldValue; function update(currentTime) { const elapsed = currentTime - startTime; const progress = Math.min(elapsed / duration, 1); const easeOutQuad = progress * (2 - progress); const current = Math.round(oldValue + diff * easeOutQuad); element.textContent = current.toLocaleString(); element.classList.add("counter-animate"); if (progress < 1) { requestAnimationFrame(update); } else { setTimeout(() => element.classList.remove("counter-animate"), 500); } } requestAnimationFrame(update); } function updateMetrics(data, debug) { animateCounter( document.getElementById("clients-online"), data.clients.online, ); animateCounter(document.getElementById("clients-total"), data.clients.total); const totalSessions = data.sessions.console + data.sessions.remoteDesktop + data.sessions.fileBrowser + data.sessions.process; animateCounter(document.getElementById("active-sessions"), totalSessions); animateCounter( document.getElementById("commands-hour"), data.commands.lastHour, ); animateCounter( document.getElementById("commands-minute"), data.commands.lastMinute, ); animateCounter( document.getElementById("commands-total"), data.commands.total, ); const totalRate = data.bandwidth.sentPerSecond + data.bandwidth.receivedPerSecond; document.getElementById("bandwidth-rate").textContent = formatBytes(totalRate) + "/s"; document.getElementById("bandwidth-sent").textContent = formatBytes( data.bandwidth.sent, ); document.getElementById("bandwidth-received").textContent = formatBytes( data.bandwidth.received, ); document.getElementById("server-uptime").textContent = formatDuration( data.server.uptime, ); document.getElementById("server-memory").textContent = formatBytes( data.server.memoryUsage.heapUsed, ); const serverRssEl = document.getElementById("server-rss"); if (serverRssEl) { serverRssEl.textContent = formatBytes(data.server.memoryUsage.rss); } const serverSystemMemEl = document.getElementById("server-system-memory"); if (serverSystemMemEl && data.server.systemMemory) { const used = data.server.systemMemory.used || 0; const total = data.server.systemMemory.total || 0; const percent = data.server.systemMemory.usedPercent || 0; serverSystemMemEl.textContent = `${formatBytes(used)} / ${formatBytes(total)} (${Math.round(percent)}%)`; } const serverCpuEl = document.getElementById("server-cpu-load"); if (serverCpuEl && data.server.cpu) { const [l1, l5, l15] = data.server.cpu.loadAvg || [0, 0, 0]; const cores = data.server.cpu.cores || 0; serverCpuEl.textContent = `${Number(l1).toFixed(2)} / ${Number(l5).toFixed(2)} / ${Number(l15).toFixed(2)}${cores ? ` (${cores} cores)` : ""}`; } animateCounter( document.getElementById("total-connections"), data.connections.totalConnections, ); const httpRequestsEl = document.getElementById("http-requests-minute"); if (httpRequestsEl) { animateCounter(httpRequestsEl, data.http.lastMinute || 0); } const httpErrorsEl = document.getElementById("http-errors-minute"); if (httpErrorsEl) { animateCounter(httpErrorsEl, data.http.lastMinuteErrors || 0); } const httpLatencyAvgEl = document.getElementById("http-latency-avg"); if (httpLatencyAvgEl) { httpLatencyAvgEl.textContent = formatMs(data.http.latencyAvg || 0); } const httpLatencyP95El = document.getElementById("http-latency-p95"); if (httpLatencyP95El) { httpLatencyP95El.textContent = formatMs(data.http.latencyP95 || 0); } const eventLoopAvgEl = document.getElementById("event-loop-avg"); if (eventLoopAvgEl) { eventLoopAvgEl.textContent = formatMs(data.eventLoop.avg || 0); } const eventLoopP95El = document.getElementById("event-loop-p95"); if (eventLoopP95El) { eventLoopP95El.textContent = formatMs(data.eventLoop.p95 || 0); } const eventLoopMaxEl = document.getElementById("event-loop-max"); if (eventLoopMaxEl) { eventLoopMaxEl.textContent = formatMs(data.eventLoop.max || 0); } if (data.ping.count > 0) { document.getElementById("ping-avg").textContent = Math.round(data.ping.avg) + " ms"; document.getElementById("ping-min").textContent = Math.round(data.ping.min) + " ms"; document.getElementById("ping-max").textContent = Math.round(data.ping.max) + " ms"; animateCounter(document.getElementById("ping-count"), data.ping.count); } else { document.getElementById("ping-avg").textContent = "-"; document.getElementById("ping-min").textContent = "-"; document.getElementById("ping-max").textContent = "-"; document.getElementById("ping-count").textContent = "0"; } const osList = document.getElementById("clients-by-os"); if (Object.keys(data.clients.byOS).length > 0) { osList.innerHTML = Object.entries(data.clients.byOS) .sort((a, b) => b[1] - a[1]) .map( ([os, count]) => `