Files
Virtnet.bond/extensions/Servers/Proxmox/views/overview-new-scripts.js

336 lines
14 KiB
JavaScript

// This is temporary to prepare the new script
const formatBytes = (bytes, decimals = 2) => {
if (bytes === 0) return '0 B';
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
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(dm)) + ' ' + sizes[i];
};
const formatRate = (bytesPerSec) => {
return formatBytes(bytesPerSec) + '/s';
};
const createInterfaceCard = (interfaceData, index) => {
const interfaceId = interfaceData.id.replace(/[^a-z0-9]/gi, '-');
const isMultiple = networkData.interfaces.length > 1;
return `
<div class="bg-background-secondary border border-neutral rounded-lg p-6 ${isMultiple ? '' : 'md:col-span-2'}">
<div class="flex items-center justify-between mb-4">
<div>
<h3 class="text-lg font-semibold flex items-center gap-2">
<svg class="w-5 h-5 text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z"></path>
</svg>
${interfaceData.name}
</h3>
<p class="text-sm/60 mt-1">Live bandwidth • Updates every 5 seconds</p>
</div>
<button class="text-base/60 hover:text-base transition-colors chart-toggle"
data-interface="${interfaceId}"
title="Toggle network graph">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"></path>
</svg>
</button>
</div>
<!-- Download Speed -->
<div class="mb-6">
<div class="flex items-center justify-between mb-2">
<div class="flex items-center gap-2">
<svg class="w-4 h-4 text-success" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M9 19l3 3m0 0l3-3m-3 3V10"></path>
</svg>
<span class="text-sm font-medium text-base/80">Download</span>
<span class="text-lg font-bold text-success" data-traffic="${interfaceId}-download-rate">0 B/s</span>
</div>
<div class="text-right">
<span class="text-xs text-base/50">Peak: </span>
<span class="text-xs font-medium text-base/70" data-traffic="${interfaceId}-download-peak">0 B/s</span>
</div>
</div>
<div class="network-flow-bar">
<div class="network-flow-fill download" data-traffic="${interfaceId}-download-bar">
<div class="network-flow-particle"></div>
</div>
</div>
</div>
<!-- Upload Speed -->
<div class="mb-6">
<div class="flex items-center justify-between mb-2">
<div class="flex items-center gap-2">
<svg class="w-4 h-4 text-info" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"></path>
</svg>
<span class="text-sm font-medium text-base/80">Upload</span>
<span class="text-lg font-bold text-info" data-traffic="${interfaceId}-upload-rate">0 B/s</span>
</div>
<div class="text-right">
<span class="text-xs text-base/50">Peak: </span>
<span class="text-xs font-medium text-base/70" data-traffic="${interfaceId}-upload-peak">0 B/s</span>
</div>
</div>
<div class="network-flow-bar">
<div class="network-flow-fill upload" data-traffic="${interfaceId}-upload-bar">
<div class="network-flow-particle"></div>
</div>
</div>
</div>
<!-- Network Graph Dropdown -->
<div class="chart-dropdown" data-chart-container="${interfaceId}">
<div class="border-t border-neutral/30 pt-4 mt-4">
<canvas id="chart-${interfaceId}" class="w-full" style="height: 200px;"></canvas>
</div>
</div>
<!-- Statistics -->
<div class="grid grid-cols-3 gap-3 text-center">
<div class="p-3 rounded bg-background-secondary/50 border border-neutral/20">
<p class="text-xs text-base/50 uppercase">Combined</p>
<p class="text-sm font-semibold" data-traffic="${interfaceId}-combined">0 B/s</p>
</div>
<div class="p-3 rounded bg-background-secondary/50 border border-neutral/20">
<p class="text-xs text-base/50 uppercase">Total In</p>
<p class="text-sm font-semibold" data-traffic="${interfaceId}-total-in">0 MB</p>
</div>
<div class="p-3 rounded bg-background-secondary/50 border border-neutral/20">
<p class="text-xs text-base/50 uppercase">Total Out</p>
<p class="text-sm font-semibold" data-traffic="${interfaceId}-total-out">0 MB</p>
</div>
</div>
</div>
`;
};
const updateInterfaceData = (interfaceData) => {
const interfaceId = interfaceData.id.replace(/[^a-z0-9]/gi, '-');
const scale = networkData.scale || (100 * 1024 * 1024);
// Update peak rates
if (!peakRates[interfaceId]) {
peakRates[interfaceId] = { download: 0, upload: 0 };
}
peakRates[interfaceId].download = Math.max(peakRates[interfaceId].download, interfaceData.downloadRate);
peakRates[interfaceId].upload = Math.max(peakRates[interfaceId].upload, interfaceData.uploadRate);
// Update text elements
const setText = (key, value) => {
const el = document.querySelector(`[data-traffic="${interfaceId}-${key}"]`);
if (el) el.textContent = value;
};
setText('download-rate', formatRate(interfaceData.downloadRate));
setText('upload-rate', formatRate(interfaceData.uploadRate));
setText('download-peak', formatRate(peakRates[interfaceId].download));
setText('upload-peak', formatRate(peakRates[interfaceId].upload));
setText('combined', formatRate(interfaceData.totalRate));
setText('total-in', formatBytes(networkData.total.downloadTotal));
setText('total-out', formatBytes(networkData.total.uploadTotal));
// Update progress bars
const downloadPercent = Math.min(100, (interfaceData.downloadRate / scale) * 100);
const uploadPercent = Math.min(100, (interfaceData.uploadRate / scale) * 100);
const downloadBar = document.querySelector(`[data-traffic="${interfaceId}-download-bar"]`);
const uploadBar = document.querySelector(`[data-traffic="${interfaceId}-upload-bar"]`);
if (downloadBar) {
downloadBar.style.width = `${downloadPercent}%`;
}
if (uploadBar) {
uploadBar.style.width = `${uploadPercent}%`;
}
// Update chart if visible
if (charts[interfaceId]) {
const chart = charts[interfaceId];
const labels = interfaceData.history.download.map((_, i) => `${i * 3}m`).slice(-20);
chart.data.labels = labels;
chart.data.datasets[0].data = interfaceData.history.download.slice(-20);
chart.data.datasets[1].data = interfaceData.history.upload.slice(-20);
chart.update('none');
}
};
const createChart = (interfaceId, interfaceData) => {
const canvas = document.getElementById(`chart-${interfaceId}`);
if (!canvas || charts[interfaceId]) return;
const ctx = canvas.getContext('2d');
const labels = interfaceData.history.download.map((_, i) => `${i * 3}m`).slice(-20);
charts[interfaceId] = new Chart(ctx, {
type: 'line',
data: {
labels: labels,
datasets: [
{
label: 'Download',
data: interfaceData.history.download.slice(-20),
borderColor: 'rgb(34, 197, 94)',
backgroundColor: 'rgba(34, 197, 94, 0.1)',
tension: 0.4,
fill: true,
},
{
label: 'Upload',
data: interfaceData.history.upload.slice(-20),
borderColor: 'rgb(59, 130, 246)',
backgroundColor: 'rgba(59, 130, 246, 0.1)',
tension: 0.4,
fill: true,
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
interaction: {
intersect: false,
mode: 'index',
},
plugins: {
legend: {
display: true,
position: 'top',
labels: {
usePointStyle: true,
padding: 10,
}
},
tooltip: {
callbacks: {
label: (context) => {
return context.dataset.label + ': ' + formatRate(context.parsed.y);
}
}
}
},
scales: {
y: {
beginAtZero: true,
ticks: {
callback: (value) => formatRate(value),
}
},
x: {
grid: {
display: false,
}
}
}
}
});
};
const renderInterfaces = () => {
const container = document.getElementById('network-traffic-container');
if (!container || !networkData) return;
if (networkData.interfaces.length === 0) {
container.innerHTML = `
<div class="bg-background-secondary border border-neutral rounded-lg p-8 text-center">
<svg class="w-12 h-12 mx-auto text-base/40 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<p class="text-base/60">No network data available</p>
</div>
`;
return;
}
const gridClass = networkData.interfaces.length > 1 ? 'grid grid-cols-1 md:grid-cols-2 gap-6' : 'grid grid-cols-1 gap-6';
container.className = gridClass;
container.innerHTML = networkData.interfaces
.map((iface, index) => createInterfaceCard(iface, index))
.join('');
// Set up chart toggles
document.querySelectorAll('.chart-toggle').forEach(button => {
button.addEventListener('click', (e) => {
const interfaceId = e.currentTarget.dataset.interface;
const container = document.querySelector(`[data-chart-container="${interfaceId}"]`);
if (container) {
container.classList.toggle('open');
// Create chart on first open
if (container.classList.contains('open') && !charts[interfaceId]) {
const interfaceData = networkData.interfaces.find(i =>
i.id.replace(/[^a-z0-9]/gi, '-') === interfaceId
);
if (interfaceData) {
setTimeout(() => createChart(interfaceId, interfaceData), 100);
}
}
}
});
});
};
const poll = async () => {
if (pollInFlight || !pollingEndpoint) return;
pollInFlight = true;
try {
const csrfElement = document.querySelector('meta[name="csrf-token"]');
const csrfToken = csrfElement ? csrfElement.content : '';
const response = await fetch(pollingEndpoint, {
method: 'GET',
headers: {
'Accept': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
'X-CSRF-TOKEN': csrfToken,
},
credentials: 'same-origin',
});
if (response.ok) {
const data = await response.json();
const wasEmpty = !networkData || networkData.interfaces.length === 0;
networkData = data;
// Render interfaces if first load or structure changed
if (wasEmpty || data.interfaces.length !== (document.querySelectorAll('.chart-toggle').length || 0)) {
renderInterfaces();
}
// Update data for all interfaces
data.interfaces.forEach(updateInterfaceData);
}
} catch (error) {
console.error('Failed to fetch live traffic data', error);
} finally {
pollInFlight = false;
}
};
const bootstrap = () => {
const container = document.getElementById('network-traffic-container');
pollingEndpoint = container?.dataset?.liveTrafficEndpoint || null;
if (pollingEndpoint) {
poll();
setInterval(poll, 5000); // Poll every 5 seconds
}
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', bootstrap);
} else {
bootstrap();
}