network-performance-monitor
Version:
A comprehensive network performance monitoring tool that continuously tests and tracks your network's performance over time
1,027 lines (922 loc) • 35.1 kB
JavaScript
// Global variables
let latencyChart, speedChart, websiteChart;
let dataRange = { start: null, end: null };
let currentPeriod = '1h';
let autoRefreshInterval = null;
let autoRefreshEnabled = true;
let countdownInterval = null;
let lastRefreshTime = Date.now();
let selectedNetwork = null;
let currentNetwork = null;
// Custom plugin to draw average line labels
const averageLinePlugin = {
id: 'averageLinePlugin',
afterDraw: (chart) => {
const ctx = chart.ctx;
const annotation = chart.options.plugins.annotation?.annotations?.avgLine;
if (annotation && annotation.label && annotation.label.content && annotation.yMin !== undefined) {
const yScale = chart.scales.y;
const xScale = chart.scales.x;
const y = yScale.getPixelForValue(annotation.yMin);
// Get chart area bounds
const chartArea = chart.chartArea;
const x = chartArea.right - 10;
ctx.save();
// Set font first to measure text
ctx.font = `${annotation.label.font?.weight || 'bold'} ${annotation.label.font?.size || 12}px Arial`;
const text = annotation.label.content;
const textWidth = ctx.measureText(text).width;
const padding = annotation.label.padding || 4;
// Draw background
ctx.fillStyle = annotation.label.backgroundColor || 'rgba(0,0,0,0.8)';
ctx.fillRect(x - textWidth - padding * 2, y - 10 - padding, textWidth + padding * 2, 20 + padding * 2);
// Draw text
ctx.fillStyle = annotation.label.color || 'white';
ctx.textAlign = 'right';
ctx.textBaseline = 'middle';
ctx.fillText(text, x - padding, y);
ctx.restore();
}
}
};
// Initialize charts
function initCharts() {
// Register plugins globally but disable by default
Chart.register(ChartDataLabels);
Chart.register(averageLinePlugin);
if (typeof chartjsPluginAnnotation !== 'undefined') {
Chart.register(chartjsPluginAnnotation);
}
Chart.defaults.plugins.datalabels = {
display: false
};
const commonOptions = {
responsive: true,
maintainAspectRatio: false,
interaction: {
mode: 'index',
intersect: false,
},
plugins: {
zoom: {
zoom: {
wheel: {
enabled: true,
},
pinch: {
enabled: true
},
mode: 'x',
},
pan: {
enabled: true,
mode: 'x',
},
limits: {
x: {
min: 'original',
max: 'original'
}
}
},
tooltip: {
callbacks: {
afterLabel: function(context) {
const dataPoint = context.raw;
const details = [];
if (context.dataset.label === 'Ping Latency' && dataPoint.host) {
details.push(`Host: ${dataPoint.host}`);
if (dataPoint.packetLoss !== null && dataPoint.packetLoss !== undefined) {
details.push(`Packet Loss: ${dataPoint.packetLoss}%`);
}
}
if (context.dataset.label === 'Website Response Time' && dataPoint.url) {
details.push(`URL: ${dataPoint.url}`);
if (dataPoint.statusCode) {
details.push(`Status: ${dataPoint.statusCode}`);
}
}
if ((context.dataset.label === 'Download Speed' || context.dataset.label === 'Upload Speed') && dataPoint.speedData) {
details.push(`Server: ${dataPoint.speedData.server || 'Unknown'}`);
details.push(`Ping: ${dataPoint.speedData.ping ? dataPoint.speedData.ping.toFixed(2) + ' ms' : 'N/A'}`);
const triggerReason = dataPoint.speedData.triggerReason || 'scheduled';
const formattedTrigger = triggerReason.charAt(0).toUpperCase() + triggerReason.slice(1);
details.push(`Trigger: ${formattedTrigger}`);
if (context.dataset.label === 'Download Speed' && dataPoint.speedData.uploadSpeed) {
details.push(`Upload: ${dataPoint.speedData.uploadSpeed.toFixed(2)} Mbps`);
}
if (context.dataset.label === 'Upload Speed' && dataPoint.speedData.downloadSpeed) {
details.push(`Download: ${dataPoint.speedData.downloadSpeed.toFixed(2)} Mbps`);
}
}
return details;
}
}
}
},
scales: {
x: {
type: 'time',
time: {
displayFormats: {
minute: 'HH:mm',
hour: 'HH:mm',
day: 'MMM dd',
week: 'MMM dd',
month: 'MMM yyyy'
}
},
ticks: {
source: 'auto',
autoSkip: true,
maxRotation: 0
}
}
}
};
// Latency chart
const latencyCtx = document.getElementById('latencyChart').getContext('2d');
latencyChart = new Chart(latencyCtx, {
type: 'line',
data: {
datasets: [{
label: 'Ping Latency',
data: [],
borderColor: 'rgb(75, 192, 192)',
backgroundColor: 'rgba(75, 192, 192, 0.1)',
tension: 0.1,
pointRadius: 2,
pointHoverRadius: 5
}]
},
options: {
...commonOptions,
scales: {
...commonOptions.scales,
y: {
type: 'logarithmic',
beginAtZero: false,
min: 1,
title: {
display: true,
text: 'Latency (ms) - Log Scale'
},
ticks: {
callback: function(value) {
if (value === 1 || value === 10 || value === 100 || value === 1000) {
return value + ' ms';
}
return null;
}
}
}
},
plugins: {
...commonOptions.plugins,
annotation: {
annotations: {}
},
averageLinePlugin: {
enabled: true
}
}
}
});
// Speed chart
const speedCtx = document.getElementById('speedChart').getContext('2d');
speedChart = new Chart(speedCtx, {
type: 'line',
data: {
datasets: [{
label: 'Download Speed',
data: [],
borderColor: 'rgb(54, 162, 235)',
backgroundColor: 'rgba(54, 162, 235, 0.1)',
tension: 0.1,
pointRadius: 3,
pointHoverRadius: 6
}, {
label: 'Upload Speed',
data: [],
borderColor: 'rgb(255, 99, 132)',
backgroundColor: 'rgba(255, 99, 132, 0.1)',
tension: 0.1,
pointRadius: 3,
pointHoverRadius: 6
}]
},
options: {
...commonOptions,
scales: {
...commonOptions.scales,
y: {
beginAtZero: true,
title: {
display: true,
text: 'Speed (Mbps)'
}
}
},
plugins: {
...commonOptions.plugins,
datalabels: {
display: false
}
}
}
});
// Website chart - same format as latency chart
const websiteCtx = document.getElementById('websiteChart').getContext('2d');
websiteChart = new Chart(websiteCtx, {
type: 'line',
data: {
datasets: [{
label: 'Website Response Time',
data: [],
borderColor: 'rgb(153, 102, 255)',
backgroundColor: 'rgba(153, 102, 255, 0.1)',
tension: 0.1,
pointRadius: 2,
pointHoverRadius: 5
}]
},
options: {
...commonOptions,
scales: {
...commonOptions.scales,
y: {
type: 'logarithmic',
beginAtZero: false,
min: 1,
title: {
display: true,
text: 'Response Time (ms) - Log Scale'
},
ticks: {
callback: function(value) {
if (value === 1 || value === 10 || value === 100 || value === 1000) {
return value + ' ms';
}
return null;
}
}
}
},
plugins: {
...commonOptions.plugins,
annotation: {
annotations: {}
},
averageLinePlugin: {
enabled: true
}
}
}
});
}
// Fetch and display data
async function fetchData(period = 'all') {
let start, end;
if (period === 'all') {
start = dataRange.start;
end = dataRange.end;
} else {
end = new Date();
start = new Date();
switch(period) {
case '1h':
start.setHours(start.getHours() - 1);
break;
case '1d':
start.setDate(start.getDate() - 1);
break;
case '1w':
start.setDate(start.getDate() - 7);
break;
case '1m':
start.setMonth(start.getMonth() - 1);
break;
case '1y':
start.setFullYear(start.getFullYear() - 1);
break;
}
}
const params = new URLSearchParams();
if (start) params.append('start', start.toISOString());
if (end) params.append('end', end.toISOString());
if (selectedNetwork) params.append('network', selectedNetwork);
try {
// Fetch ping data
const pingResponse = await fetch(`/api/ping-data?${params}`);
const pingData = await pingResponse.json();
// Fetch speed data
const speedResponse = await fetch(`/api/speed-data?${params}`);
const speedData = await speedResponse.json();
// Fetch website data
const websiteResponse = await fetch(`/api/website-data?${params}`);
const websiteData = await websiteResponse.json();
updateCharts(pingData, speedData, websiteData, start, end);
} catch (error) {
console.error('Error fetching data:', error);
}
}
// Get appropriate time format based on time range
function getTimeFormat(period) {
switch(period) {
case '1h':
return {
displayFormats: {
minute: 'HH:mm',
hour: 'HH:mm'
},
unit: 'minute'
};
case '1d':
return {
displayFormats: {
hour: 'HH:00',
day: 'HH:00'
},
unit: 'hour'
};
case '1w':
return {
displayFormats: {
day: 'MMM dd',
week: 'MMM dd'
},
unit: 'day'
};
case '1m':
return {
displayFormats: {
day: 'MMM dd',
week: 'MMM dd',
month: 'MMM dd'
},
unit: 'day'
};
case '1y':
return {
displayFormats: {
month: 'MMM',
year: 'MMM'
},
unit: 'month'
};
default:
// For 'all' data, auto-detect best format
if (dataRange.start && dataRange.end) {
const diff = dataRange.end - dataRange.start;
const days = diff / (1000 * 60 * 60 * 24);
if (days <= 1) {
return getTimeFormat('1d');
} else if (days <= 7) {
return getTimeFormat('1w');
} else if (days <= 31) {
return getTimeFormat('1m');
} else {
return getTimeFormat('1y');
}
}
return {
displayFormats: {
hour: 'MMM dd HH:mm',
day: 'MMM dd',
week: 'MMM dd',
month: 'MMM yyyy'
}
};
}
}
// Update charts with new data
function updateCharts(pingData, speedData, websiteData, start, end) {
// Calculate average latency and find anomalies
let totalLatency = 0;
let maxLatencies = [];
const latencyData = pingData.map(d => {
// Handle timestamp that might be in "YYYY-MM-DD HH:mm:ss" format
// Assume the timestamp from DB is in UTC
const timestamp = d.timestamp.replace(' ', 'T') + 'Z';
const date = new Date(timestamp);
const latency = d.latency;
totalLatency += latency;
return {
x: date,
y: latency,
host: d.host,
packetLoss: d.packet_loss
};
});
const avgLatency = pingData.length > 0 ? totalLatency / pingData.length : 0;
// Find top anomalies (latencies > 2x average)
if (avgLatency > 0) {
const anomalyThreshold = avgLatency * 2;
const anomalies = latencyData
.filter(d => d.y > anomalyThreshold)
.sort((a, b) => b.y - a.y)
.slice(0, 5); // Top 5 anomalies
maxLatencies = anomalies;
}
// Update latency chart
latencyChart.data.datasets[0].data = latencyData;
// Add or update average line
if (avgLatency > 0) {
// Update annotation for average line
latencyChart.options.plugins.annotation.annotations = {
avgLine: {
type: 'line',
yMin: avgLatency,
yMax: avgLatency,
borderColor: 'rgba(255, 99, 132, 0.5)',
borderWidth: 2,
borderDash: [5, 5],
label: {
content: `Avg: ${avgLatency.toFixed(1)} ms`,
enabled: true,
position: 'end',
backgroundColor: 'rgba(255, 99, 132, 0.8)',
color: 'white',
font: {
size: 12,
weight: 'bold'
},
padding: 4,
xAdjust: -60,
yAdjust: -10
}
}
};
// Configure data labels for anomalies
latencyChart.options.plugins.datalabels = {
display: function(context) {
const value = context.dataset.data[context.dataIndex].y;
// Only show labels for anomalies
return maxLatencies.some(anomaly =>
anomaly.x.getTime() === context.dataset.data[context.dataIndex].x.getTime() &&
anomaly.y === value
);
},
align: 'top',
anchor: 'bottom',
backgroundColor: 'rgba(255, 99, 132, 0.8)',
borderRadius: 4,
color: 'white',
font: {
weight: 'bold',
size: 11
},
formatter: function(value) {
return value.y.toFixed(1) + ' ms';
},
padding: 4
};
} else {
latencyChart.options.plugins.annotation.annotations = {};
latencyChart.options.plugins.datalabels = {
display: false
};
}
// Update speed chart
speedChart.data.datasets[0].data = speedData.map(d => {
const timestamp = d.timestamp.replace(' ', 'T') + 'Z';
const date = new Date(timestamp);
return {
x: date,
y: d.download_speed,
speedData: {
downloadSpeed: d.download_speed,
uploadSpeed: d.upload_speed,
ping: d.ping,
server: d.server,
triggerReason: d.trigger_reason
}
};
});
speedChart.data.datasets[1].data = speedData.map(d => {
const timestamp = d.timestamp.replace(' ', 'T') + 'Z';
const date = new Date(timestamp);
return {
x: date,
y: d.upload_speed,
speedData: {
downloadSpeed: d.download_speed,
uploadSpeed: d.upload_speed,
ping: d.ping,
server: d.server,
triggerReason: d.trigger_reason
}
};
});
// Process website data
let totalWebsiteLatency = 0;
let maxWebsiteLatencies = [];
const websiteLatencyData = websiteData.map(d => {
const timestamp = d.timestamp.replace(' ', 'T') + 'Z';
const date = new Date(timestamp);
const latency = d.latency;
totalWebsiteLatency += latency;
return {
x: date,
y: latency,
url: d.url,
statusCode: d.status_code
};
});
const avgWebsiteLatency = websiteData.length > 0 ? totalWebsiteLatency / websiteData.length : 0;
// Find top website anomalies
if (avgWebsiteLatency > 0) {
const anomalyThreshold = avgWebsiteLatency * 2;
const anomalies = websiteLatencyData
.filter(d => d.y > anomalyThreshold)
.sort((a, b) => b.y - a.y)
.slice(0, 5);
maxWebsiteLatencies = anomalies;
}
// Update website chart
websiteChart.data.datasets[0].data = websiteLatencyData;
// Add or update average line for website chart
if (avgWebsiteLatency > 0) {
websiteChart.options.plugins.annotation.annotations = {
avgLine: {
type: 'line',
yMin: avgWebsiteLatency,
yMax: avgWebsiteLatency,
borderColor: 'rgba(153, 102, 255, 0.5)',
borderWidth: 2,
borderDash: [5, 5],
label: {
content: `Avg: ${avgWebsiteLatency.toFixed(1)} ms`,
enabled: true,
position: 'end',
backgroundColor: 'rgba(153, 102, 255, 0.8)',
color: 'white',
font: {
size: 12,
weight: 'bold'
},
padding: 4,
xAdjust: -60,
yAdjust: -10
}
}
};
// Configure data labels for website anomalies
websiteChart.options.plugins.datalabels = {
display: function(context) {
const value = context.dataset.data[context.dataIndex].y;
return maxWebsiteLatencies.some(anomaly =>
anomaly.x.getTime() === context.dataset.data[context.dataIndex].x.getTime() &&
anomaly.y === value
);
},
align: 'top',
anchor: 'bottom',
backgroundColor: 'rgba(153, 102, 255, 0.8)',
borderRadius: 4,
color: 'white',
font: {
weight: 'bold',
size: 11
},
formatter: function(value) {
return value.y.toFixed(1) + ' ms';
},
padding: 4
};
} else {
websiteChart.options.plugins.annotation.annotations = {};
websiteChart.options.plugins.datalabels = {
display: false
};
}
// Set scale limits
const scaleOptions = {
min: start || dataRange.start,
max: end || dataRange.end
};
// Update time format based on current period
const timeFormat = getTimeFormat(currentPeriod);
latencyChart.options.scales.x.time = timeFormat;
speedChart.options.scales.x.time = timeFormat;
websiteChart.options.scales.x.time = timeFormat;
latencyChart.options.scales.x.min = scaleOptions.min;
latencyChart.options.scales.x.max = scaleOptions.max;
speedChart.options.scales.x.min = scaleOptions.min;
speedChart.options.scales.x.max = scaleOptions.max;
websiteChart.options.scales.x.min = scaleOptions.min;
websiteChart.options.scales.x.max = scaleOptions.max;
// Update zoom limits
latencyChart.options.plugins.zoom.limits.x.min = dataRange.start;
latencyChart.options.plugins.zoom.limits.x.max = dataRange.end;
speedChart.options.plugins.zoom.limits.x.min = dataRange.start;
speedChart.options.plugins.zoom.limits.x.max = dataRange.end;
websiteChart.options.plugins.zoom.limits.x.min = dataRange.start;
websiteChart.options.plugins.zoom.limits.x.max = dataRange.end;
latencyChart.update();
speedChart.update();
websiteChart.update();
}
// Fetch and display averages
async function fetchAverages() {
try {
// Calculate time range based on currentPeriod
let start, end;
if (currentPeriod === 'all') {
start = dataRange.start;
end = dataRange.end;
} else {
end = new Date();
start = new Date();
switch(currentPeriod) {
case '1h':
start.setHours(start.getHours() - 1);
break;
case '1d':
start.setDate(start.getDate() - 1);
break;
case '1w':
start.setDate(start.getDate() - 7);
break;
case '1m':
start.setMonth(start.getMonth() - 1);
break;
case '1y':
start.setFullYear(start.getFullYear() - 1);
break;
}
}
// Build URL with parameters
const params = new URLSearchParams();
if (selectedNetwork) {
params.append('network', selectedNetwork);
}
if (start) {
params.append('start', start.toISOString());
}
if (end) {
params.append('end', end.toISOString());
}
const url = '/api/averages' + (params.toString() ? '?' + params.toString() : '');
const response = await fetch(url);
const averages = await response.json();
document.getElementById('avgPing').textContent =
averages.avgPingLatency ? `${averages.avgPingLatency.toFixed(2)} ms` : '--';
document.getElementById('avgWebsite').textContent =
averages.avgWebsiteLatency ? `${averages.avgWebsiteLatency.toFixed(2)} ms` : '--';
document.getElementById('avgDownload').textContent =
averages.avgDownloadSpeed ? `${averages.avgDownloadSpeed.toFixed(2)} Mbps` : '--';
document.getElementById('avgUpload').textContent =
averages.avgUploadSpeed ? `${averages.avgUploadSpeed.toFixed(2)} Mbps` : '--';
} catch (error) {
console.error('Error fetching averages:', error);
}
}
// Fetch data range
async function fetchDataRange() {
try {
const response = await fetch('/api/data-range');
const range = await response.json();
if (range.start && range.end) {
// Handle timestamp format - assume UTC from database
const startTimestamp = range.start.replace(' ', 'T') + 'Z';
const endTimestamp = range.end.replace(' ', 'T') + 'Z';
dataRange.start = new Date(startTimestamp);
dataRange.end = new Date(endTimestamp);
const formatDate = (date) => {
return date.toLocaleDateString() + ' ' + date.toLocaleTimeString();
};
document.getElementById('dataRange').textContent =
`Data available from ${formatDate(dataRange.start)} to ${formatDate(dataRange.end)}`;
}
} catch (error) {
console.error('Error fetching data range:', error);
}
}
// Auto-refresh management
function startAutoRefresh() {
if (autoRefreshInterval) {
clearInterval(autoRefreshInterval);
}
if (countdownInterval) {
clearInterval(countdownInterval);
}
if (autoRefreshEnabled) {
lastRefreshTime = Date.now();
// Main refresh interval
autoRefreshInterval = setInterval(async () => {
lastRefreshTime = Date.now();
// Update network list to detect network changes
await loadNetworks();
fetchData(currentPeriod);
fetchAverages();
}, 30000);
// Countdown update interval
countdownInterval = setInterval(() => {
updateRefreshStatus();
}, 1000);
updateRefreshStatus();
}
}
function stopAutoRefresh() {
if (autoRefreshInterval) {
clearInterval(autoRefreshInterval);
autoRefreshInterval = null;
}
if (countdownInterval) {
clearInterval(countdownInterval);
countdownInterval = null;
}
updateRefreshStatus();
}
function updateRefreshStatus() {
const statusElement = document.getElementById('refreshStatus');
if (autoRefreshEnabled) {
const elapsed = Math.floor((Date.now() - lastRefreshTime) / 1000);
const remaining = Math.max(0, 30 - elapsed);
statusElement.textContent = `(${remaining}s)`;
statusElement.style.color = remaining <= 5 ? '#007bff' : '#666';
} else {
statusElement.textContent = '(paused)';
statusElement.style.color = '#999';
}
}
// Trigger manual speed test
async function triggerManualSpeedTest() {
const btn = document.getElementById('manualSpeedTestBtn');
const originalText = btn.textContent;
try {
// Update button state
btn.disabled = true;
btn.classList.add('running');
btn.textContent = 'Running Speed Test...';
// Make API call
const response = await fetch('/api/trigger-speed-test', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Speed test failed');
}
const result = await response.json();
// Show notification
const notification = document.createElement('div');
notification.className = 'network-change-notification';
if (result.success) {
notification.style.backgroundColor = '#28a745';
notification.textContent = `Speed Test Complete: ↓ ${result.downloadSpeed?.toFixed(2) || 'N/A'} Mbps | ↑ ${result.uploadSpeed?.toFixed(2) || 'N/A'} Mbps | Ping: ${result.ping?.toFixed(2) || 'N/A'} ms`;
} else {
notification.style.backgroundColor = '#dc3545';
notification.textContent = `Speed Test Failed: ${result.error || 'Unknown error'}`;
}
document.body.appendChild(notification);
setTimeout(() => notification.remove(), 10000);
// Refresh data to show new speed test result
setTimeout(() => {
fetchData(currentPeriod);
}, 1000);
} catch (error) {
console.error('Error triggering speed test:', error);
// Show error notification
const notification = document.createElement('div');
notification.className = 'network-change-notification';
notification.style.backgroundColor = '#dc3545';
notification.textContent = `Error: ${error.message}`;
document.body.appendChild(notification);
setTimeout(() => notification.remove(), 5000);
} finally {
// Reset button state
btn.disabled = false;
btn.classList.remove('running');
btn.textContent = originalText;
}
}
// Setup event listeners
function setupEventListeners() {
// Auto-refresh toggle
const autoRefreshToggle = document.getElementById('autoRefreshToggle');
autoRefreshToggle.addEventListener('change', (e) => {
autoRefreshEnabled = e.target.checked;
if (autoRefreshEnabled) {
startAutoRefresh();
} else {
stopAutoRefresh();
}
});
// Manual speed test button
const speedTestBtn = document.getElementById('manualSpeedTestBtn');
speedTestBtn.addEventListener('click', async () => {
await triggerManualSpeedTest();
});
// Network selector
const networkSelect = document.getElementById('networkSelect');
networkSelect.addEventListener('change', (e) => {
selectedNetwork = e.target.value || null;
fetchData(currentPeriod);
fetchAverages(); // Averages may differ by network
});
// Time period buttons
document.querySelectorAll('.time-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
document.querySelectorAll('.time-btn').forEach(b => b.classList.remove('active'));
e.target.classList.add('active');
currentPeriod = e.target.dataset.period;
fetchData(currentPeriod);
fetchAverages(); // Update averages for new time period
});
});
// Sync zoom between charts
let syncing = false;
const syncZoom = (sourceChart, targetCharts) => {
if (!syncing) {
syncing = true;
targetCharts.forEach(targetChart => {
targetChart.options.scales.x.min = sourceChart.options.scales.x.min;
targetChart.options.scales.x.max = sourceChart.options.scales.x.max;
targetChart.update('none');
});
syncing = false;
}
};
// Add zoom event listeners
const latencyCanvas = document.getElementById('latencyChart');
const speedCanvas = document.getElementById('speedChart');
const websiteCanvas = document.getElementById('websiteChart');
latencyCanvas.addEventListener('wheel', () => {
setTimeout(() => syncZoom(latencyChart, [speedChart, websiteChart]), 10);
});
speedCanvas.addEventListener('wheel', () => {
setTimeout(() => syncZoom(speedChart, [latencyChart, websiteChart]), 10);
});
websiteCanvas.addEventListener('wheel', () => {
setTimeout(() => syncZoom(websiteChart, [latencyChart, speedChart]), 10);
});
}
// Load available networks
async function loadNetworks() {
try {
// Get current network info
const currentNetworkResponse = await fetch('/api/current-network');
const networkInfo = await currentNetworkResponse.json();
const previousNetwork = currentNetwork;
currentNetwork = networkInfo.name;
// Log if network changed
if (previousNetwork && previousNetwork !== currentNetwork) {
console.log(`Network changed from "${previousNetwork}" to "${currentNetwork}"`);
// Show notification if network changed
const notification = document.createElement('div');
notification.className = 'network-change-notification';
notification.textContent = `Network changed to: ${currentNetwork}`;
document.body.appendChild(notification);
setTimeout(() => notification.remove(), 5000);
}
// Get all networks
const networksResponse = await fetch('/api/networks');
const networks = await networksResponse.json();
const select = document.getElementById('networkSelect');
const previousSelection = select.value;
select.innerHTML = '';
// Add "All Networks" option
const allOption = document.createElement('option');
allOption.value = '';
allOption.textContent = 'All Networks';
select.appendChild(allOption);
// Add individual networks
networks.forEach(network => {
const option = document.createElement('option');
option.value = network;
option.textContent = network;
if (network === currentNetwork) {
option.textContent += ' (current)';
}
select.appendChild(option);
});
// Preserve selection if network still exists, otherwise default to current
if (previousSelection && networks.includes(previousSelection)) {
select.value = previousSelection;
} else if (!selectedNetwork || !networks.includes(selectedNetwork)) {
selectedNetwork = currentNetwork;
select.value = currentNetwork;
} else {
select.value = selectedNetwork;
}
} catch (error) {
console.error('Error loading networks:', error);
}
}
// Initialize everything
async function init() {
initCharts();
setupEventListeners();
await loadNetworks();
await fetchDataRange();
await fetchAverages();
await fetchData('1h');
// Start auto-refresh
startAutoRefresh();
}
// Start the app
document.addEventListener('DOMContentLoaded', init);