refactor: enhance network traffic polling logic and initialization handling

This commit is contained in:
2025-12-17 16:53:14 -05:00
parent fdaa2bf7f1
commit 19356d05e6

View File

@@ -837,7 +837,7 @@
</div>
<!-- Network Traffic Sections -->
<div id="network-traffic-container" data-live-traffic-endpoint="{{ route('proxmox.server.live-traffic', $server) }}">
<div id="network-traffic-container" data-live-traffic-endpoint="{{ route('proxmox.server.live-traffic', $server) }}" data-initialized="false">
<!-- Initial placeholder - will be replaced with actual data on first load -->
<div class="text-center py-6 text-gray-500 dark:text-gray-400 text-sm" id="network-traffic-placeholder">
<svg class="w-8 h-8 mx-auto mb-2 animate-spin text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -981,12 +981,16 @@
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
<script>
(() => {
// State variables
let pollInFlight = false;
let pollingEndpoint = null;
let networkData = null;
let charts = {};
let peakRates = {};
let pollInterval = null;
let isInitialized = false;
// Utility functions
const formatBytes = (bytes, decimals = 2) => {
if (bytes === 0) return '0 B';
const k = 1024;
@@ -1483,12 +1487,11 @@
});
};
// Simplified poll - renders once, then only updates values
const poll = async () => {
if (pollInFlight || !pollingEndpoint) return;
pollInFlight = true;
console.log('Polling network data from:', pollingEndpoint);
try {
const csrfElement = document.querySelector('meta[name="csrf-token"]');
const csrfToken = csrfElement ? csrfElement.content : '';
@@ -1503,125 +1506,47 @@
credentials: 'same-origin',
});
console.log('Network Traffic Response status:', response.status);
if (response.ok) {
const data = await response.json();
console.log('Network Traffic Data received:', data);
const container = document.getElementById('network-traffic-container');
const wasEmpty = !networkData || !networkData.interfaces || networkData.interfaces.length === 0;
const hasExistingInterfaces = container && container.querySelectorAll('.chart-toggle').length > 0;
// Check if container shows the initial placeholder
const showsPlaceholder = container && (
container.querySelector('#network-traffic-placeholder') !== null ||
container.querySelector('.animate-spin') !== null
);
if (!container) {
pollInFlight = false;
return;
}
// Store data globally
networkData = data;
// Store globally for export access
window.networkTrafficData = data;
// Render interfaces only if:
// 1. This is the first load (showing placeholder), OR
// 2. No existing interfaces found in container
// Never re-render once we have data - just update values
// Use data-initialized attribute to track first load
const needsRender = container.dataset.initialized !== 'true';
if (data.interfaces && data.interfaces.length > 0) {
if (showsPlaceholder || !hasExistingInterfaces) {
console.log('Rendering interfaces - showsPlaceholder:', showsPlaceholder, 'hasExistingInterfaces:', hasExistingInterfaces);
// Render immediately - no delay
if (needsRender) {
// First load - render interfaces and mark as initialized
renderInterfaces();
// Also update data immediately after render to ensure values are set
container.dataset.initialized = 'true';
requestAnimationFrame(() => {
data.interfaces.forEach(updateInterfaceData);
});
} else {
// Update existing interfaces immediately (just numbers, no re-render)
// Already initialized - just update values (no re-render)
data.interfaces.forEach(updateInterfaceData);
// After tab switch, ensure chart toggle listeners are attached even if interfaces exist
// This handles the case where DOM was replaced but interfaces were re-rendered
const toggleButtons = container.querySelectorAll('.chart-toggle');
if (toggleButtons.length > 0) {
toggleButtons.forEach(button => {
// Check if button already has a data-listener attribute
if (!button.dataset.listenerAttached) {
button.dataset.listenerAttached = 'true';
const interfaceId = button.dataset.interface;
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 attemptCreateChart = (retries = 5, delay = 200) => {
const interfaceData = networkData?.interfaces?.find(i =>
i.id.replace(/[^a-z0-9]/gi, '-') === interfaceId
);
const canvas = document.getElementById(`chart-${interfaceId}`);
if (canvas && interfaceData && interfaceData.history) {
const historyLength = Math.max(
interfaceData.history.download?.length || 0,
interfaceData.history.upload?.length || 0
);
if (historyLength > 0) {
console.log('Creating chart for interface:', interfaceId, 'after tab switch');
requestAnimationFrame(() => {
setTimeout(() => {
if (!charts[interfaceId]) {
createChart(interfaceId, interfaceData);
}
}, 50);
});
return;
}
}
if (retries > 0) {
setTimeout(() => attemptCreateChart(retries - 1, delay), delay);
}
};
attemptCreateChart();
}
}
});
}
});
}
}
}
} else {
console.error('Network Traffic Response not OK:', response.status, response.statusText);
const text = await response.text();
console.error('Response body:', text);
}
} catch (error) {
console.error('Failed to fetch live traffic data:', error);
console.error('Network traffic poll error:', error);
const container = document.getElementById('network-traffic-container');
// Only show error if still showing initial placeholder (no data loaded yet)
// Never replace existing network data with error state
const showsPlaceholder = container && (
container.querySelector('#network-traffic-placeholder') !== null ||
container.querySelector('.animate-spin') !== null
);
if (container && showsPlaceholder) {
// Only show error if never initialized
if (container && container.dataset.initialized !== 'true') {
container.innerHTML = `
<div class="text-center py-6 text-gray-500 dark:text-gray-400 text-sm">
<svg class="w-8 h-8 mx-auto mb-2 text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
Failed to load network data
<p class="text-xs text-gray-400 mt-1">${error.message}</p>
</div>
`;
}
@@ -1630,213 +1555,49 @@
}
};
let pollInterval = null;
// Simplified bootstrap - initializes once
const bootstrap = () => {
// Clear any existing interval
if (isInitialized) return;
const container = document.getElementById('network-traffic-container');
if (!container) return;
pollingEndpoint = container.dataset.liveTrafficEndpoint;
if (!pollingEndpoint) return;
isInitialized = true;
if (pollInterval) {
clearInterval(pollInterval);
pollInterval = null;
}
const container = document.getElementById('network-traffic-container');
pollingEndpoint = container?.dataset?.liveTrafficEndpoint || null;
console.log('Network Traffic Bootstrap: Container found:', !!container);
console.log('Network Traffic Bootstrap: Endpoint:', pollingEndpoint);
if (pollingEndpoint && container) {
// Check if container exists and has endpoint - be less strict about visibility
// Sometimes container exists but offsetParent check fails initially
const isVisible = container.offsetParent !== null;
const containerExists = container && container.dataset.liveTrafficEndpoint;
if (isVisible || containerExists) {
// Always poll immediately on bootstrap (don't wait for interval)
console.log('Network Traffic: Polling immediately on bootstrap...');
// Poll immediately - don't wait
poll();
// Set up interval for subsequent polls (30 seconds)
// Minimal delay to avoid race condition
setTimeout(() => {
if (!pollInterval) {
pollInterval = setInterval(poll, 30000);
console.log('Network Traffic: Polling interval set up (30 seconds)');
}
}, 100);
} else {
console.log('Network Traffic: Container not ready, will retry via visibility check');
}
}
// Poll immediately and set up interval
poll();
pollInterval = setInterval(poll, 30000);
};
// Make bootstrap accessible globally for re-initialization
window.initNetworkTraffic = bootstrap;
// Initial bootstrap - try multiple times aggressively
const tryBootstrap = () => {
const container = document.getElementById('network-traffic-container');
if (container && container.dataset.liveTrafficEndpoint) {
bootstrap();
return true;
}
return false;
window.initNetworkTraffic = () => {
isInitialized = false;
bootstrap();
};
// Try immediately
if (!tryBootstrap()) {
// If DOM is loading, wait for it
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
// Try multiple times with minimal delays
setTimeout(() => tryBootstrap() || setTimeout(() => tryBootstrap() || setTimeout(tryBootstrap, 50), 50), 10);
});
} else {
// DOM ready, try with minimal delays
setTimeout(() => tryBootstrap() || setTimeout(() => tryBootstrap() || setTimeout(tryBootstrap, 50), 50), 10);
}
// Initialize
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', bootstrap);
} else {
// Successfully bootstrapped, also try immediate poll as backup
console.log('Network Traffic: Bootstrap successful, ensuring immediate poll...');
setTimeout(bootstrap, 10);
}
// Re-initialize on Livewire lifecycle events (immediate)
document.addEventListener('livewire:init', () => {
setTimeout(tryBootstrap, 10);
});
document.addEventListener('livewire:load', () => {
setTimeout(tryBootstrap, 10);
});
// Re-initialize when Livewire navigates (tab switches)
// Handle Livewire navigation
document.addEventListener('livewire:navigated', () => {
// Clear charts when navigating (DOM will be replaced)
Object.keys(charts).forEach(interfaceId => {
try {
if (charts[interfaceId] && typeof charts[interfaceId].destroy === 'function') {
charts[interfaceId].destroy();
}
} catch (e) {
console.warn('Error destroying chart during navigation:', e);
}
});
charts = {}; // Clear charts object
// Re-initialize after a short delay to ensure DOM is ready
setTimeout(() => {
tryBootstrap();
// Also ensure event listeners are re-attached if interfaces are already rendered
if (networkData && networkData.interfaces && networkData.interfaces.length > 0) {
const container = document.getElementById('network-traffic-container');
if (container && container.querySelectorAll('.chart-toggle').length > 0) {
// Re-attach chart toggle listeners
document.querySelectorAll('.chart-toggle').forEach(button => {
// Remove any existing listeners by cloning the button
const newButton = button.cloneNode(true);
button.parentNode.replaceChild(newButton, button);
// Add fresh event listener
newButton.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 attemptCreateChart = (retries = 5, delay = 200) => {
const interfaceData = networkData?.interfaces?.find(i =>
i.id.replace(/[^a-z0-9]/gi, '-') === interfaceId
);
const canvas = document.getElementById(`chart-${interfaceId}`);
if (canvas && interfaceData && interfaceData.history) {
const historyLength = Math.max(
interfaceData.history.download?.length || 0,
interfaceData.history.upload?.length || 0
);
if (historyLength > 0) {
console.log('Creating chart for interface:', interfaceId, 'with data:', {
id: interfaceData.id,
name: interfaceData.name,
historyLength: historyLength
});
requestAnimationFrame(() => {
setTimeout(() => {
if (!charts[interfaceId]) {
createChart(interfaceId, interfaceData);
}
}, 50);
});
return;
}
}
if (retries > 0) {
console.log(`Chart data not ready for ${interfaceId}, retrying... (${retries} attempts left)`);
setTimeout(() => attemptCreateChart(retries - 1, delay), delay);
} else {
console.warn('Failed to create chart for', interfaceId, 'after multiple attempts');
}
};
attemptCreateChart();
}
}
});
});
}
}
}, 100);
Object.keys(charts).forEach(id => { try { charts[id]?.destroy(); } catch(e) {} });
charts = {};
isInitialized = false;
setTimeout(bootstrap, 50);
});
// Check periodically if container is visible and needs initialization (very frequent)
const visibilityCheck = setInterval(() => {
const container = document.getElementById('network-traffic-container');
if (container) {
const isVisible = container.offsetParent !== null;
const hasEndpoint = container.dataset.liveTrafficEndpoint;
const showsPlaceholder = container.querySelector('.border-dashed') !== null ||
container.textContent.includes('Loading live network statistics');
// If visible, has endpoint, and either:
// 1. No polling interval (not initialized), OR
// 2. Shows placeholder (was reset)
if (isVisible && hasEndpoint && (showsPlaceholder || !pollInterval)) {
console.log('Network Traffic: Container needs initialization (placeholder:', showsPlaceholder, 'no interval:', !pollInterval, '), re-initializing...');
bootstrap();
}
// Also check if we have endpoint but haven't polled yet - poll immediately
if (isVisible && hasEndpoint && !pollInterval && !pollInFlight) {
// Update pollingEndpoint if it's not set
if (!pollingEndpoint) {
pollingEndpoint = hasEndpoint;
}
console.log('Network Traffic: Container visible with endpoint but no polling active, polling immediately...');
poll();
setTimeout(() => {
if (!pollInterval) {
pollInterval = setInterval(poll, 30000);
}
}, 100);
}
}
}, 200); // Check very frequently (every 200ms) for immediate response
// Clean up on page unload
window.addEventListener('beforeunload', () => {
clearInterval(visibilityCheck);
if (pollInterval) {
clearInterval(pollInterval);
}
if (pollInterval) clearInterval(pollInterval);
});
})();