Files
my-mc-stats-website/modal.html
T
2025-12-27 03:04:39 -05:00

968 lines
48 KiB
HTML

<!-- modal.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My-MC.Link Status - Modal</title>
<link rel="icon" href="https://minecraft.wiki/images/Favicon.png" type="image/png">
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;700;900&display=swap" rel="stylesheet">
<link href="https://my-mc.link/css/style.min.css?p=5" rel="stylesheet">
<link href="https://my-mc.link/favicon.ico" rel="icon" type="image/x-icon">
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.umd.min.js"></script>
<style>
/* Modal-specific styling */
body {
margin: 0;
padding: 0;
min-height: 100vh;
background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%);
overflow-y: auto;
}
table th,
table td {
text-align: center !important;
}
.container-table {
display: flex;
justify-content: center;
margin: 0 auto;
max-width: 100%;
}
.container-table table {
width: 100%;
max-width: 1200px;
}
.no-data {
text-align: center;
color: #fb7185;
font-size: 1.2rem;
margin: 1rem 0;
}
/* Ensure cards are visible */
.feature-card {
display: block !important;
opacity: 1 !important;
visibility: visible !important;
position: relative;
z-index: 10;
background: rgba(17, 24, 39, 0.8);
backdrop-filter: blur(10px);
border-radius: 12px;
padding: 1.5rem;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
}
.tilt-card {
display: block !important;
opacity: 1 !important;
visibility: visible !important;
transform: none !important;
}
canvas {
width: 100% !important;
height: 300px !important;
}
/* Modal layout */
.modal-content {
padding: 2rem 1rem;
min-height: 100vh;
}
/* Remove header spacing */
.modal-content > .section-bg:first-child {
margin-top: 0;
}
</style>
</head>
<body>
<!-- Particle Effects -->
<div class="particle"></div>
<div class="particle large"></div>
<div class="particle"></div>
<div class="particle large"></div>
<div class="particle"></div>
<div id="particles-js" style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: -1;"></div>
<div class="modal-content">
<main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pb-16 relative z-10">
<section class="section-bg p-8 sm:p-10 mb-12">
<h2
class="text-3xl minecraft-font mb-8 text-center bg-clip-text text-transparent bg-gradient-to-r from-teal-400 to-blue-500">
Docker Environment Overview</h2>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<div class="feature-card tilt-card">
<h3 class="text-xl minecraft-font mb-3 text-teal-400">Running Containers</h3>
<p class="text-lg opacity-90" id="running-containers">0</p>
</div>
<div class="feature-card tilt-card">
<h3 class="text-xl minecraft-font mb-3 text-teal-400">Total Containers</h3>
<p class="text-lg opacity-90" id="total-containers">0</p>
</div>
<div class="feature-card tilt-card">
<h3 class="text-xl minecraft-font mb-3 text-teal-400">Total CPU Usage</h3>
<p class="text-lg opacity-90" id="total-cpu">0%</p>
</div>
<div class="feature-card tilt-card">
<h3 class="text-xl minecraft-font mb-3 text-teal-400">Total RAM Usage</h3>
<p class="text-lg opacity-90" id="total-ram">0 GB</p>
</div>
<div class="feature-card tilt-card">
<h3 class="text-xl minecraft-font mb-3 text-teal-400">Holesail Processes</h3>
<p class="text-lg opacity-90" id="holesail-processes">0</p>
</div>
<div class="feature-card tilt-card">
<h3 class="text-xl minecraft-font mb-3 text-teal-400">Disk Usage</h3>
<p class="text-lg opacity-90" id="disk-usage">0 GB</p>
</div>
<div class="feature-card tilt-card">
<h3 class="text-xl minecraft-font mb-3 text-teal-400">Free Disk</h3>
<p class="text-lg opacity-90" id="free-disk">0 GB</p>
</div>
<div class="feature-card tilt-card">
<h3 class="text-xl minecraft-font mb-3 text-teal-400">AI Fault Monitor Count</h3>
<p class="text-lg opacity-90" id="ai-fault-count">0</p>
</div>
</div>
<div>
<h3 class="text-xl minecraft-font mb-6 text-teal-400 text-center">Docker Network Traffic</h3>
<div class="chart-container">
<canvas id="docker-net-chart"></canvas>
</div>
</div>
<h3 class="text-xl minecraft-font mb-6 text-teal-400 text-center">Minecraft Containers (Sorted by CPU)</h3>
<div class="container-table">
<table class="w-full text-sm text-gray-200 mb-8">
<thead class="text-xs uppercase bg-gray-800">
<tr>
<th class="px-6 py-3">Name</th>
<th class="px-6 py-3">CPU Usage (%)</th>
<th class="px-6 py-3">Memory Usage (GB)</th>
<th class="px-6 py-3">State</th>
</tr>
</thead>
<tbody id="cpu-table"></tbody>
</table>
</div>
<h3 class="text-xl minecraft-font mb-6 text-teal-400 text-center">Minecraft Containers (Sorted by Memory)
</h3>
<div class="container-table">
<table class="w-full text-sm text-gray-200">
<thead class="text-xs uppercase bg-gray-800">
<tr>
<th class="px-6 py-3">Name</th>
<th class="px-6 py-3">CPU Usage (%)</th>
<th class="px-6 py-3">Memory Usage (GB)</th>
<th class="px-6 py-3">State</th>
</tr>
</thead>
<tbody id="memory-table"></tbody>
</table>
</div>
</section>
<section class="section-bg p-8 sm:p-10 mb-12">
<h2
class="text-3xl minecraft-font mb-8 text-center bg-gradient-to-r from-teal-400 to-blue-500 bg-clip-text text-transparent">
Host System Metrics</h2>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 mb-8">
<div class="feature-card tilt-card">
<h3 class="text-xl minecraft-font mb-3 text-teal-400">Load Avg (1m)</h3>
<p class="text-lg opacity-90" id="host-load1">0</p>
</div>
<div class="feature-card tilt-card">
<h3 class="text-xl minecraft-font mb-3 text-teal-400">Load Avg (5m)</h3>
<p class="text-lg opacity-90" id="host-load5">0</p>
</div>
<div class="feature-card tilt-card">
<h3 class="text-xl minecraft-font mb-3 text-teal-400">Load Avg (15m)</h3>
<p class="text-lg opacity-90" id="host-load15">0</p>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
<div>
<h3 class="text-xl minecraft-font mb-6 text-teal-400 text-center">CPU Usage</h3>
<canvas id="cpu-chart" class="mb-8"></canvas>
</div>
<div>
<h3 class="text-xl minecraft-font mb-6 text-teal-400 text-center">RAM Usage</h3>
<canvas id="ram-chart" class="mb-8"></canvas>
</div>
<div>
<h3 class="text-xl minecraft-font mb-6 text-teal-400 text-center">Network Traffic</h3>
<canvas id="net-chart" class="mb-8"></canvas>
</div>
<div>
<h3 class="text-xl minecraft-font mb-6 text-teal-400 text-center">Disk I/O</h3>
<canvas id="disk-chart" class="mb-8"></canvas>
</div>
<div>
<h3 class="text-xl minecraft-font mb-6 text-teal-400 text-center">Load Averages</h3>
<canvas id="load-chart" class="mb-8"></canvas>
</div>
</div>
</section>
<section class="section-bg p-8 sm:p-10 mb-12">
<h2
class="text-3xl minecraft-font mb-8 text-center bg-gradient-to-r from-teal-400 to-blue-500 bg-clip-text text-transparent">
Jump Node Stats</h2>
<div id="jump-no-data" class="no-data" style="display: none;">No Jump Node data available</div>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 mb-6">
<div class="feature-card tilt-card">
<h3 class="text-xl minecraft-font mb-3 text-teal-400">Uptime</h3>
<p class="text-lg opacity-90" id="jump-uptime">0 days</p>
</div>
<div class="feature-card tilt-card">
<h3 class="text-xl minecraft-font mb-3 text-teal-400">AI Anomaly Count</h3>
<p class="text-lg opacity-90" id="jump-ai-anomaly-count">0</p>
</div>
<div class="feature-card tilt-card">
<h3 class="text-xl minecraft-font mb-3 text-teal-400">Disk Usage</h3>
<p class="text-lg opacity-90" id="jump-disk-usage">0 GB</p>
</div>
<div class="feature-card tilt-card">
<h3 class="text-xl minecraft-font mb-3 text-teal-400">Free Disk</h3>
<p class="text-lg opacity-90" id="jump-free-disk">0 GB</p>
</div>
</div>
<div class="flex justify-center gap-6 mb-8">
<div class="feature-card tilt-card">
<h3 class="text-xl minecraft-font mb-3 text-teal-400">Load Avg (1m)</h3>
<p class="text-lg opacity-90" id="jump-load1">0</p>
</div>
<div class="feature-card tilt-card">
<h3 class="text-xl minecraft-font mb-3 text-teal-400">Load Avg (5m)</h3>
<p class="text-lg opacity-90" id="jump-load5">0</p>
</div>
<div class="feature-card tilt-card">
<h3 class="text-xl minecraft-font mb-3 text-teal-400">Load Avg (15m)</h3>
<p class="text-lg opacity-90" id="jump-load15">0</p>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
<div>
<h3 class="text-xl minecraft-font mb-6 text-teal-400 text-center">CPU Usage</h3>
<canvas id="jump-cpu-chart" class="mb-8"></canvas>
</div>
<div>
<h3 class="text-xl minecraft-font mb-6 text-teal-400 text-center">RAM Usage</h3>
<canvas id="jump-ram-chart" class="mb-8"></canvas>
</div>
<div>
<h3 class="text-xl minecraft-font mb-6 text-teal-400 text-center">Network Traffic</h3>
<canvas id="jump-net-chart" class="mb-8"></canvas>
</div>
<div>
<h3 class="text-xl minecraft-font mb-6 text-teal-400 text-center">Disk I/O</h3>
<canvas id="jump-disk-chart" class="mb-8"></canvas>
</div>
<div>
<h3 class="text-xl minecraft-font mb-6 text-teal-400 text-center">Load Averages</h3>
<canvas id="jump-load-chart" class="mb-8"></canvas>
</div>
</div>
</section>
</main>
</div>
<script>
const ws = new WebSocket('wss://' + window.location.host);
const socket = ws;
let charts = {};
let dockerNetHistory = [];
let netHistory = [];
let diskHistory = [];
let jumpNetHistory = [];
let jumpDiskHistory = [];
let hostLoadHistory = [];
let jumpLoadHistory = [];
let jumpRamHistory = [];
const MAX_POINTS = 30;
const SMOOTHING_WINDOW = 3;
function smoothData(data, windowSize) {
if (!data || data.length < windowSize) return data || [];
return data.map((_, i) => {
const start = Math.max(0, i - windowSize + 1);
const slice = data.slice(start, i + 1);
return slice.reduce((sum, val) => sum + parseFloat(val), 0) / slice.length;
});
}
function formatBytes(bytes) {
if (bytes >= 1e9) return { value: (bytes / 1e9).toFixed(2), unit: 'GB/s' };
if (bytes >= 1e6) return { value: (bytes / 1e6).toFixed(2), unit: 'MB/s' };
if (bytes >= 1e3) return { value: (bytes / 1e3).toFixed(2), unit: 'KB/s' };
return { value: bytes.toFixed(2), unit: 'B/s' };
}
function formatUptime(seconds) {
const days = Math.floor(seconds / (60 * 60 * 24));
return `${days} days`;
}
function formatTime(timestamp) {
return new Date(timestamp * 1000).toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' });
}
// NEW: Format disk space intelligently (TB if ≥ 1024 GB, otherwise GB)
function formatDiskSpace(gbValue) {
if (gbValue >= 1024) {
return `${(gbValue / 1024).toFixed(2)} TB`;
}
return `${gbValue.toFixed(2)} GB`;
}
function initCharts() {
if (typeof Chart === 'undefined') {
console.error('Chart.js failed to load.');
return;
}
const canvases = {
dockerNet: document.getElementById('docker-net-chart'),
cpu: document.getElementById('cpu-chart'),
ram: document.getElementById('ram-chart'),
net: document.getElementById('net-chart'),
disk: document.getElementById('disk-chart'),
load: document.getElementById('load-chart'),
jumpCpu: document.getElementById('jump-cpu-chart'),
jumpRam: document.getElementById('jump-ram-chart'),
jumpNet: document.getElementById('jump-net-chart'),
jumpDisk: document.getElementById('jump-disk-chart'),
jumpLoad: document.getElementById('jump-load-chart')
};
for (const [key, canvas] of Object.entries(canvases)) {
if (!canvas || !canvas.getContext) {
console.error(`Canvas element for ${key} not found or invalid.`);
return;
}
}
const commonOptions = {
responsive: true,
maintainAspectRatio: false,
animation: { duration: 500, easing: 'linear' },
scales: {
y: { beginAtZero: true, grid: { color: 'rgba(255,255,255,0.1)' }, ticks: { color: '#e5e7eb' } },
x: {
grid: { display: false },
ticks: { color: '#e5e7eb', autoSkip: true, maxTicksLimit: 10 },
title: { display: true, text: 'Time (oldest to newest)', color: '#e5e7eb' }
}
},
plugins: {
tooltip: { mode: 'index', intersect: false },
legend: { display: true, position: 'top', labels: { color: '#e5e7eb' } }
}
};
charts.dockerNet = new Chart(canvases.dockerNet.getContext('2d'), {
type: 'line',
data: {
labels: [],
datasets: [
{ label: 'In', data: [], borderColor: '#38bdf8', backgroundColor: 'rgba(56, 189, 248, 0.5)', borderWidth: 2, fill: true, tension: 0.4 },
{ label: 'Out', data: [], borderColor: '#fb7185', backgroundColor: 'rgba(251, 113, 133, 0.5)', borderWidth: 2, fill: true, tension: 0.4 }
]
},
options: {
...commonOptions,
scales: {
y: {
...commonOptions.scales.y,
stacked: true,
title: { display: true, text: 'Speed', color: '#e5e7eb' }
},
x: commonOptions.scales.x
}
}
});
charts.cpu = new Chart(canvases.cpu.getContext('2d'), {
type: 'line',
data: {
labels: [],
datasets: [
{ label: 'User (%)', data: [], borderColor: '#38bdf8', backgroundColor: 'rgba(56, 189, 248, 0.5)', borderWidth: 2, fill: true, tension: 0.4 },
{ label: 'System (%)', data: [], borderColor: '#fb7185', backgroundColor: 'rgba(251, 113, 133, 0.5)', borderWidth: 2, fill: true, tension: 0.4 }
]
},
options: {
...commonOptions,
scales: {
y: {
...commonOptions.scales.y,
stacked: true,
max: 100,
title: { display: true, text: 'Usage (%)', color: '#e5e7eb' }
},
x: commonOptions.scales.x
}
}
});
charts.ram = new Chart(canvases.ram.getContext('2d'), {
type: 'line',
data: {
labels: [],
datasets: [
{ label: 'Used (MB)', data: [], borderColor: '#38bdf8', backgroundColor: 'rgba(56, 189, 248, 0.5)', borderWidth: 2, fill: true, tension: 0.4 },
{ label: 'Free (MB)', data: [], borderColor: '#fb7185', backgroundColor: 'rgba(251, 113, 133, 0.5)', borderWidth: 2, fill: true, tension: 0.4 }
]
},
options: {
...commonOptions,
scales: {
y: {
...commonOptions.scales.y,
stacked: true,
title: { display: true, text: 'Memory (MB)', color: '#e5e7eb' }
},
x: commonOptions.scales.x
}
}
});
charts.net = new Chart(canvases.net.getContext('2d'), {
type: 'line',
data: {
labels: [],
datasets: [
{ label: 'Received', data: [], borderColor: '#38bdf8', backgroundColor: 'rgba(56, 189, 248, 0.5)', borderWidth: 2, fill: true, tension: 0.4 },
{ label: 'Sent', data: [], borderColor: '#fb7185', backgroundColor: 'rgba(251, 113, 133, 0.5)', borderWidth: 2, fill: true, tension: 0.4 }
]
},
options: {
...commonOptions,
scales: {
y: {
...commonOptions.scales.y,
stacked: true,
title: { display: true, text: 'Speed', color: '#e5e7eb' }
},
x: commonOptions.scales.x
}
}
});
charts.disk = new Chart(canvases.disk.getContext('2d'), {
type: 'line',
data: {
labels: [],
datasets: [
{ label: 'Read', data: [], borderColor: '#38bdf8', backgroundColor: 'rgba(56, 189, 248, 0.5)', borderWidth: 2, fill: true, tension: 0.4 },
{ label: 'Write', data: [], borderColor: '#fb7185', backgroundColor: 'rgba(251, 113, 133, 0.5)', borderWidth: 2, fill: true, tension: 0.4 }
]
},
options: {
...commonOptions,
scales: {
y: {
...commonOptions.scales.y,
stacked: true,
title: { display: true, text: 'Speed', color: '#e5e7eb' }
},
x: commonOptions.scales.x
}
}
});
charts.load = new Chart(canvases.load.getContext('2d'), {
type: 'line',
data: {
labels: [],
datasets: [
{ label: '1m', data: [], borderColor: '#38bdf8', borderWidth: 2, fill: false, tension: 0.4 },
{ label: '5m', data: [], borderColor: '#fb7185', borderWidth: 2, fill: false, tension: 0.4 },
{ label: '15m', data: [], borderColor: '#10b981', borderWidth: 2, fill: false, tension: 0.4 }
]
},
options: {
...commonOptions,
scales: {
y: {
...commonOptions.scales.y,
title: { display: true, text: 'Load', color: '#e5e7eb' }
},
x: commonOptions.scales.x
}
}
});
charts.jumpCpu = new Chart(canvases.jumpCpu.getContext('2d'), {
type: 'line',
data: {
labels: [],
datasets: [
{ label: 'User (%)', data: [], borderColor: '#38bdf8', backgroundColor: 'rgba(56, 189, 248, 0.5)', borderWidth: 2, fill: true, tension: 0.4 },
{ label: 'System (%)', data: [], borderColor: '#fb7185', backgroundColor: 'rgba(251, 113, 133, 0.5)', borderWidth: 2, fill: true, tension: 0.4 }
]
},
options: {
...commonOptions,
scales: {
y: {
...commonOptions.scales.y,
stacked: true,
max: 100,
title: { display: true, text: 'Usage (%)', color: '#e5e7eb' }
},
x: commonOptions.scales.x
}
}
});
charts.jumpRam = new Chart(canvases.jumpRam.getContext('2d'), {
type: 'line',
data: {
labels: [],
datasets: [
{ label: 'Used (MB)', data: [], borderColor: '#38bdf8', backgroundColor: 'rgba(56, 189, 248, 0.5)', borderWidth: 2, fill: true, tension: 0.4 },
{ label: 'Free (MB)', data: [], borderColor: '#fb7185', backgroundColor: 'rgba(251, 113, 133, 0.5)', borderWidth: 2, fill: true, tension: 0.4 }
]
},
options: {
...commonOptions,
scales: {
y: {
...commonOptions.scales.y,
stacked: true,
title: { display: true, text: 'Memory (MB)', color: '#e5e7eb' }
},
x: commonOptions.scales.x
}
}
});
charts.jumpNet = new Chart(canvases.jumpNet.getContext('2d'), {
type: 'line',
data: {
labels: [],
datasets: [
{ label: 'Received', data: [], borderColor: '#38bdf8', backgroundColor: 'rgba(56, 189, 248, 0.5)', borderWidth: 2, fill: true, tension: 0.4 },
{ label: 'Sent', data: [], borderColor: '#fb7185', backgroundColor: 'rgba(251, 113, 133, 0.5)', borderWidth: 2, fill: true, tension: 0.4 }
]
},
options: {
...commonOptions,
scales: {
y: {
...commonOptions.scales.y,
stacked: true,
title: { display: true, text: 'Speed', color: '#e5e7eb' }
},
x: commonOptions.scales.x
}
}
});
charts.jumpDisk = new Chart(canvases.jumpDisk.getContext('2d'), {
type: 'line',
data: {
labels: [],
datasets: [
{ label: 'Read', data: [], borderColor: '#38bdf8', backgroundColor: 'rgba(56, 189, 248, 0.5)', borderWidth: 2, fill: true, tension: 0.4 },
{ label: 'Write', data: [], borderColor: '#fb7185', backgroundColor: 'rgba(251, 113, 133, 0.5)', borderWidth: 2, fill: true, tension: 0.4 }
]
},
options: {
...commonOptions,
scales: {
y: {
...commonOptions.scales.y,
stacked: true,
title: { display: true, text: 'Speed', color: '#e5e7eb' }
},
x: commonOptions.scales.x
}
}
});
charts.jumpLoad = new Chart(canvases.jumpLoad.getContext('2d'), {
type: 'line',
data: {
labels: [],
datasets: [
{ label: '1m', data: [], borderColor: '#38bdf8', borderWidth: 2, fill: false, tension: 0.4 },
{ label: '5m', data: [], borderColor: '#fb7185', borderWidth: 2, fill: false, tension: 0.4 },
{ label: '15m', data: [], borderColor: '#10b981', borderWidth: 2, fill: false, tension: 0.4 }
]
},
options: {
...commonOptions,
scales: {
y: {
...commonOptions.scales.y,
title: { display: true, text: 'Load', color: '#e5e7eb' }
},
x: commonOptions.scales.x
}
}
});
console.log('Charts initialized successfully.');
}
function updateUI(data) {
if (!data) {
console.error('Received empty WebSocket data.');
return;
}
console.debug('Received WebSocket payload:', JSON.stringify(data, null, 2));
if (!data.docker) {
console.warn('Missing docker data.');
data.docker = {};
}
if (!data.netdata) {
console.warn('Missing netdata data.');
data.netdata = { cpu: [], ram: [], net: [], disk: [], disk_space: [], anomaly: [], load: [] };
}
if (!data.jumpNode) {
console.warn('Missing jump node data.');
data.jumpNode = { cpu: [], ram: [], net: [], disk: [], anomaly: [], disk_space: [], uptime: [], load: [] };
}
const jumpNoData = document.getElementById('jump-no-data');
const hasJumpData = data.jumpNode && (
(data.jumpNode.cpu && data.jumpNode.cpu.length > 0) ||
(data.jumpNode.ram && data.jumpNode.ram.length > 0) ||
(data.jumpNode.net && data.jumpNode.net.length > 0) ||
(data.jumpNode.disk && data.jumpNode.disk.length > 0) ||
(data.jumpNode.anomaly && data.jumpNode.anomaly.length > 0) ||
(data.jumpNode.disk_space && data.jumpNode.disk_space.length > 0) ||
(data.jumpNode.uptime && data.jumpNode.uptime.length > 0) ||
(data.jumpNode.load && data.jumpNode.load.length > 0)
);
jumpNoData.style.display = hasJumpData ? 'none' : 'block';
document.getElementById('total-containers').textContent = data.docker.totalContainers || 0;
document.getElementById('running-containers').textContent = data.docker.runningContainers || 0;
document.getElementById('total-cpu').textContent = `${data.docker.totalCpu || 0}%`;
document.getElementById('total-ram').textContent = `${data.docker.totalMemory || 0} GB`;
document.getElementById('holesail-processes').textContent = data.holesailProcessCount || 0;
const diskSpaceData = data.netdata.disk_space || [];
const latestDiskSpace = diskSpaceData.length > 0 ? diskSpaceData[diskSpaceData.length - 1] : null;
document.getElementById('disk-usage').textContent = latestDiskSpace ? `${latestDiskSpace.used.toFixed(2)} GB` : '0 GB';
document.getElementById('free-disk').textContent = latestDiskSpace
? formatDiskSpace(latestDiskSpace.avail)
: '0 GB';
const anomalyData = data.netdata.anomaly || [];
const latestAnomaly = anomalyData.length > 0 ? anomalyData[anomalyData.length - 1] : null;
document.getElementById('ai-fault-count').textContent = latestAnomaly ? `${latestAnomaly.anomalous}` : '0';
const loadData = data.netdata.load || [];
const latestLoad = loadData.length > 0 ? loadData[loadData.length - 1] : null;
document.getElementById('host-load1').textContent = latestLoad ? latestLoad.load1.toFixed(2) : '0';
document.getElementById('host-load5').textContent = latestLoad ? latestLoad.load5.toFixed(2) : '0';
document.getElementById('host-load15').textContent = latestLoad ? latestLoad.load15.toFixed(2) : '0';
const jumpDiskSpaceData = data.jumpNode.disk_space || [];
const latestJumpDiskSpace = jumpDiskSpaceData.length > 0 ? jumpDiskSpaceData[jumpDiskSpaceData.length - 1] : null;
document.getElementById('jump-disk-usage').textContent = latestJumpDiskSpace ? `${latestJumpDiskSpace.used.toFixed(2)} GB` : '0 GB';
document.getElementById('jump-free-disk').textContent = latestJumpDiskSpace ? `${latestJumpDiskSpace.avail.toFixed(2)} GB` : '0 GB';
const jumpAnomalyData = data.jumpNode.anomaly || [];
const latestJumpAnomaly = jumpAnomalyData.length > 0 ? jumpAnomalyData[jumpAnomalyData.length - 1] : null;
document.getElementById('jump-ai-anomaly-count').textContent = latestJumpAnomaly ? `${latestJumpAnomaly.anomalous}` : '0';
const jumpUptimeData = data.jumpNode.uptime || [];
const latestJumpUptime = jumpUptimeData.length > 0 ? jumpUptimeData[jumpUptimeData.length - 1] : null;
document.getElementById('jump-uptime').textContent = latestJumpUptime ? formatUptime(latestJumpUptime.uptime) : '0 days';
const jumpLoadData = data.jumpNode.load || [];
const latestJumpLoad = jumpLoadData.length > 0 ? jumpLoadData[jumpLoadData.length - 1] : null;
document.getElementById('jump-load1').textContent = latestJumpLoad ? latestJumpLoad.load1.toFixed(2) : '0';
document.getElementById('jump-load5').textContent = latestJumpLoad ? latestJumpLoad.load5.toFixed(2) : '0';
document.getElementById('jump-load15').textContent = latestJumpLoad ? latestJumpLoad.load15.toFixed(2) : '0';
const cpuTable = document.getElementById('cpu-table');
const memoryTable = document.getElementById('memory-table');
cpuTable.innerHTML = data.docker.sortedByCpu?.map(c => `
<tr>
<td class="px-6 py-4">${c.name}</td>
<td class="px-6 py-4">${c.cpu}%</td>
<td class="px-6 py-4">${c.memory} GB</td>
<td class="px-6 py-4">${c.state}</td>
</tr>
`).join('') || '';
memoryTable.innerHTML = data.docker.sortedByMemory?.map(c => `
<tr>
<td class="px-6 py-4">${c.name}</td>
<td class="px-6 py-4">${c.cpu}%</td>
<td class="px-6 py-4">${c.memory} GB</td>
<td class="px-6 py-4">${c.state}</td>
</tr>
`).join('') || '';
if (data.docker.totalNetwork) {
const networkData = {
time: data.docker.totalNetwork.time || Math.floor(Date.now() / 1000),
in: parseFloat(data.docker.totalNetwork.received.value) * (data.docker.totalNetwork.received.unit === 'GB/s' ? 1e9 : data.docker.totalNetwork.received.unit === 'MB/s' ? 1e6 : data.docker.totalNetwork.received.unit === 'KB/s' ? 1e3 : 1),
out: parseFloat(data.docker.totalNetwork.sent.value) * (data.docker.totalNetwork.sent.unit === 'GB/s' ? 1e9 : data.docker.totalNetwork.sent.unit === 'MB/s' ? 1e6 : data.docker.totalNetwork.sent.unit === 'KB/s' ? 1e3 : 1),
unit: data.docker.totalNetwork.received.unit
};
dockerNetHistory.push(networkData);
if (dockerNetHistory.length > MAX_POINTS) {
dockerNetHistory.shift();
}
}
if (data.netdata.net?.length) {
const latestNet = data.netdata.net[data.netdata.net.length - 1];
netHistory.push({
time: latestNet.time,
received: Math.abs(latestNet.received),
sent: Math.abs(latestNet.sent)
});
if (netHistory.length > MAX_POINTS) {
netHistory.shift();
}
}
if (data.netdata.disk?.length) {
const latestDisk = data.netdata.disk[data.netdata.disk.length - 1];
diskHistory.push({
time: latestDisk.time,
in: Math.abs(latestDisk.in),
out: Math.abs(latestDisk.out)
});
if (diskHistory.length > MAX_POINTS) {
diskHistory.shift();
}
}
if (data.netdata.load?.length) {
const latestLoad = data.netdata.load[data.netdata.load.length - 1];
hostLoadHistory.push({
time: latestLoad.time,
load1: latestLoad.load1,
load5: latestLoad.load5,
load15: latestLoad.load15
});
if (hostLoadHistory.length > MAX_POINTS) {
hostLoadHistory.shift();
}
}
if (data.jumpNode.net?.length) {
const latestNet = data.jumpNode.net[data.jumpNode.net.length - 1];
jumpNetHistory.push({
time: latestNet.time,
received: Math.abs(latestNet.received),
sent: Math.abs(latestNet.sent)
});
if (jumpNetHistory.length > MAX_POINTS) {
jumpNetHistory.shift();
}
} else {
console.debug('No jump node network data available.');
}
if (data.jumpNode.disk?.length) {
const latestDisk = data.jumpNode.disk[data.jumpNode.disk.length - 1];
jumpDiskHistory.push({
time: latestDisk.time,
in: Math.abs(latestDisk.in),
out: Math.abs(latestDisk.out)
});
if (jumpDiskHistory.length > MAX_POINTS) {
jumpDiskHistory.shift();
}
} else {
console.debug('No jump node disk data available.');
}
if (data.jumpNode.ram?.length) {
const latestRam = data.jumpNode.ram[data.jumpNode.ram.length - 1];
jumpRamHistory.push({
time: latestRam.time,
used: Math.abs(latestRam.used),
free: Math.abs(latestRam.free)
});
if (jumpRamHistory.length > MAX_POINTS) {
jumpRamHistory.shift();
}
} else {
console.debug('No jump node RAM data available.');
}
if (data.jumpNode.load?.length) {
const latestLoad = data.jumpNode.load[data.jumpNode.load.length - 1];
jumpLoadHistory.push({
time: latestLoad.time,
load1: latestLoad.load1,
load5: latestLoad.load5,
load15: latestLoad.load15
});
if (jumpLoadHistory.length > MAX_POINTS) {
jumpLoadHistory.shift();
}
} else {
console.debug('No jump node load data available.');
}
const defaultData = [];
if (charts.dockerNet) {
const maxVal = Math.max(...dockerNetHistory.map(d => Math.max(d.in, d.out)), 1);
const unitInfo = formatBytes(maxVal);
const scale = unitInfo.unit === 'GB/s' ? 1e9 : unitInfo.unit === 'MB/s' ? 1e6 : unitInfo.unit === 'KB/s' ? 1e3 : 1;
const inData = smoothData(dockerNetHistory.map(d => d.in / scale), SMOOTHING_WINDOW);
const outData = smoothData(dockerNetHistory.map(d => d.out / scale), SMOOTHING_WINDOW);
charts.dockerNet.data.labels = dockerNetHistory.length ? dockerNetHistory.map(d => formatTime(d.time)) : [];
charts.dockerNet.data.datasets[0].data = inData.length ? inData : defaultData;
charts.dockerNet.data.datasets[1].data = outData.length ? outData : defaultData;
charts.dockerNet.options.scales.y.title.text = unitInfo.unit;
charts.dockerNet.update();
}
if (charts.cpu) {
const userData = smoothData(data.netdata.cpu?.map(d => d.user) || [], SMOOTHING_WINDOW);
const systemData = smoothData(data.netdata.cpu?.map(d => d.system) || [], SMOOTHING_WINDOW);
charts.cpu.data.labels = data.netdata.cpu?.length ? data.netdata.cpu.map(d => formatTime(d.time)) : [];
charts.cpu.data.datasets[0].data = userData.length ? userData : defaultData;
charts.cpu.data.datasets[1].data = systemData.length ? systemData : defaultData;
charts.cpu.update();
}
if (charts.ram) {
const usedData = smoothData(data.netdata.ram?.map(d => d.used) || [], SMOOTHING_WINDOW);
const freeData = smoothData(data.netdata.ram?.map(d => d.free) || [], SMOOTHING_WINDOW);
charts.ram.data.labels = data.netdata.ram?.length ? data.netdata.ram.map(d => formatTime(d.time)) : [];
charts.ram.data.datasets[0].data = usedData.length ? usedData : defaultData;
charts.ram.data.datasets[1].data = freeData.length ? freeData : defaultData;
charts.ram.update();
}
if (charts.net) {
const maxVal = Math.max(...netHistory.map(d => Math.max(d.received, d.sent)), 1);
const unitInfo = formatBytes(maxVal);
const scale = unitInfo.unit === 'GB/s' ? 1e9 : unitInfo.unit === 'MB/s' ? 1e6 : unitInfo.unit === 'KB/s' ? 1e3 : 1;
const receivedData = smoothData(netHistory.map(d => d.received / scale), SMOOTHING_WINDOW);
const sentData = smoothData(netHistory.map(d => d.sent / scale), SMOOTHING_WINDOW);
charts.net.data.labels = netHistory.length ? netHistory.map(d => formatTime(d.time)) : [];
charts.net.data.datasets[0].data = receivedData.length ? receivedData : defaultData;
charts.net.data.datasets[1].data = sentData.length ? sentData : defaultData;
charts.net.options.scales.y.title.text = unitInfo.unit;
charts.net.update();
}
if (charts.disk) {
const maxVal = Math.max(...diskHistory.map(d => Math.max(d.in, d.out)), 1);
const unitInfo = formatBytes(maxVal);
const scale = unitInfo.unit === 'GB/s' ? 1e9 : unitInfo.unit === 'MB/s' ? 1e6 : unitInfo.unit === 'KB/s' ? 1e3 : 1;
const inData = smoothData(diskHistory.map(d => d.in / scale), SMOOTHING_WINDOW);
const outData = smoothData(diskHistory.map(d => d.out / scale), SMOOTHING_WINDOW);
charts.disk.data.labels = diskHistory.length ? diskHistory.map(d => formatTime(d.time)) : [];
charts.disk.data.datasets[0].data = inData.length ? inData : defaultData;
charts.disk.data.datasets[1].data = outData.length ? outData : defaultData;
charts.disk.options.scales.y.title.text = unitInfo.unit;
charts.disk.update();
}
if (charts.load) {
const load1Data = smoothData(hostLoadHistory.map(d => d.load1), SMOOTHING_WINDOW);
const load5Data = smoothData(hostLoadHistory.map(d => d.load5), SMOOTHING_WINDOW);
const load15Data = smoothData(hostLoadHistory.map(d => d.load15), SMOOTHING_WINDOW);
charts.load.data.labels = hostLoadHistory.length ? hostLoadHistory.map(d => formatTime(d.time)) : [];
charts.load.data.datasets[0].data = load1Data.length ? load1Data : defaultData;
charts.load.data.datasets[1].data = load5Data.length ? load5Data : defaultData;
charts.load.data.datasets[2].data = load15Data.length ? load15Data : defaultData;
charts.load.update();
}
if (charts.jumpCpu) {
const userData = smoothData(data.jumpNode.cpu?.map(d => d.user) || [], SMOOTHING_WINDOW);
const systemData = smoothData(data.jumpNode.cpu?.map(d => d.system) || [], SMOOTHING_WINDOW);
charts.jumpCpu.data.labels = data.jumpNode.cpu?.length ? data.jumpNode.cpu.map(d => formatTime(d.time)) : [];
charts.jumpCpu.data.datasets[0].data = userData.length ? userData : defaultData;
charts.jumpCpu.data.datasets[1].data = systemData.length ? systemData : defaultData;
charts.jumpCpu.update();
}
if (charts.jumpRam) {
const usedData = smoothData(jumpRamHistory.map(d => d.used) || [], SMOOTHING_WINDOW);
const freeData = smoothData(jumpRamHistory.map(d => d.free) || [], SMOOTHING_WINDOW);
charts.jumpRam.data.labels = jumpRamHistory.length ? jumpRamHistory.map(d => formatTime(d.time)) : [];
charts.jumpRam.data.datasets[0].data = usedData.length ? usedData : defaultData;
charts.jumpRam.data.datasets[1].data = freeData.length ? freeData : defaultData;
charts.jumpRam.update();
}
if (charts.jumpNet) {
const maxVal = Math.max(...jumpNetHistory.map(d => Math.max(d.received, d.sent)), 1);
const unitInfo = formatBytes(maxVal);
const scale = unitInfo.unit === 'GB/s' ? 1e9 : unitInfo.unit === 'MB/s' ? 1e6 : unitInfo.unit === 'KB/s' ? 1e3 : 1;
const receivedData = smoothData(jumpNetHistory.map(d => d.received / scale), SMOOTHING_WINDOW);
const sentData = smoothData(jumpNetHistory.map(d => d.sent / scale), SMOOTHING_WINDOW);
charts.jumpNet.data.labels = jumpNetHistory.length ? jumpNetHistory.map(d => formatTime(d.time)) : [];
charts.jumpNet.data.datasets[0].data = receivedData.length ? receivedData : defaultData;
charts.jumpNet.data.datasets[1].data = sentData.length ? sentData : defaultData;
charts.jumpNet.options.scales.y.title.text = unitInfo.unit;
charts.jumpNet.update();
}
if (charts.jumpDisk) {
const maxVal = Math.max(...jumpDiskHistory.map(d => Math.max(d.in, d.out)), 1);
const unitInfo = formatBytes(maxVal);
const scale = unitInfo.unit === 'GB/s' ? 1e9 : unitInfo.unit === 'MB/s' ? 1e6 : unitInfo.unit === 'KB/s' ? 1e3 : 1;
const inData = smoothData(jumpDiskHistory.map(d => d.in / scale), SMOOTHING_WINDOW);
const outData = smoothData(jumpDiskHistory.map(d => d.out / scale), SMOOTHING_WINDOW);
charts.jumpDisk.data.labels = jumpDiskHistory.length ? jumpDiskHistory.map(d => formatTime(d.time)) : [];
charts.jumpDisk.data.datasets[0].data = inData.length ? inData : defaultData;
charts.jumpDisk.data.datasets[1].data = outData.length ? outData : defaultData;
charts.jumpDisk.options.scales.y.title.text = unitInfo.unit;
charts.jumpDisk.update();
}
if (charts.jumpLoad) {
const load1Data = smoothData(jumpLoadHistory.map(d => d.load1), SMOOTHING_WINDOW);
const load5Data = smoothData(jumpLoadHistory.map(d => d.load5), SMOOTHING_WINDOW);
const load15Data = smoothData(jumpLoadHistory.map(d => d.load15), SMOOTHING_WINDOW);
charts.jumpLoad.data.labels = jumpLoadHistory.length ? jumpLoadHistory.map(d => formatTime(d.time)) : [];
charts.jumpLoad.data.datasets[0].data = load1Data.length ? load1Data : defaultData;
charts.jumpLoad.data.datasets[1].data = load5Data.length ? load5Data : defaultData;
charts.jumpLoad.data.datasets[2].data = load15Data.length ? load15Data : defaultData;
charts.jumpLoad.update();
}
}
ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
updateUI(data);
} catch (error) {
console.error('Error parsing WebSocket data:', error);
}
};
ws.onopen = () => {
console.log('WebSocket connected');
initCharts();
};
ws.onclose = () => {
console.log('WebSocket disconnected');
};
</script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/particles.min.js"></script>
</body>
</html>