336 lines
14 KiB
JavaScript
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();
|
|
}
|