Private
Public Access
1
0

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> </div>
<!-- Network Traffic Sections --> <!-- 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 --> <!-- 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"> <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"> <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 src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
<script> <script>
(() => { (() => {
// State variables
let pollInFlight = false; let pollInFlight = false;
let pollingEndpoint = null; let pollingEndpoint = null;
let networkData = null; let networkData = null;
let charts = {}; let charts = {};
let peakRates = {}; let peakRates = {};
let pollInterval = null;
let isInitialized = false;
// Utility functions
const formatBytes = (bytes, decimals = 2) => { const formatBytes = (bytes, decimals = 2) => {
if (bytes === 0) return '0 B'; if (bytes === 0) return '0 B';
const k = 1024; const k = 1024;
@@ -1483,12 +1487,11 @@
}); });
}; };
// Simplified poll - renders once, then only updates values
const poll = async () => { const poll = async () => {
if (pollInFlight || !pollingEndpoint) return; if (pollInFlight || !pollingEndpoint) return;
pollInFlight = true; pollInFlight = true;
console.log('Polling network data from:', pollingEndpoint);
try { try {
const csrfElement = document.querySelector('meta[name="csrf-token"]'); const csrfElement = document.querySelector('meta[name="csrf-token"]');
const csrfToken = csrfElement ? csrfElement.content : ''; const csrfToken = csrfElement ? csrfElement.content : '';
@@ -1503,125 +1506,47 @@
credentials: 'same-origin', credentials: 'same-origin',
}); });
console.log('Network Traffic Response status:', response.status);
if (response.ok) { if (response.ok) {
const data = await response.json(); const data = await response.json();
console.log('Network Traffic Data received:', data);
const container = document.getElementById('network-traffic-container'); 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 if (!container) {
const showsPlaceholder = container && ( pollInFlight = false;
container.querySelector('#network-traffic-placeholder') !== null || return;
container.querySelector('.animate-spin') !== null }
);
// Store data globally
networkData = data; networkData = data;
// Store globally for export access
window.networkTrafficData = data; window.networkTrafficData = data;
// Render interfaces only if: // Use data-initialized attribute to track first load
// 1. This is the first load (showing placeholder), OR const needsRender = container.dataset.initialized !== 'true';
// 2. No existing interfaces found in container
// Never re-render once we have data - just update values
if (data.interfaces && data.interfaces.length > 0) { if (data.interfaces && data.interfaces.length > 0) {
if (showsPlaceholder || !hasExistingInterfaces) { if (needsRender) {
console.log('Rendering interfaces - showsPlaceholder:', showsPlaceholder, 'hasExistingInterfaces:', hasExistingInterfaces); // First load - render interfaces and mark as initialized
// Render immediately - no delay
renderInterfaces(); renderInterfaces();
// Also update data immediately after render to ensure values are set container.dataset.initialized = 'true';
requestAnimationFrame(() => { requestAnimationFrame(() => {
data.interfaces.forEach(updateInterfaceData); data.interfaces.forEach(updateInterfaceData);
}); });
} else { } else {
// Update existing interfaces immediately (just numbers, no re-render) // Already initialized - just update values (no re-render)
data.interfaces.forEach(updateInterfaceData); 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) { } 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'); const container = document.getElementById('network-traffic-container');
// Only show error if still showing initial placeholder (no data loaded yet) // Only show error if never initialized
// Never replace existing network data with error state if (container && container.dataset.initialized !== 'true') {
const showsPlaceholder = container && (
container.querySelector('#network-traffic-placeholder') !== null ||
container.querySelector('.animate-spin') !== null
);
if (container && showsPlaceholder) {
container.innerHTML = ` container.innerHTML = `
<div class="text-center py-6 text-gray-500 dark:text-gray-400 text-sm"> <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"> <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> <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> </svg>
Failed to load network data Failed to load network data
<p class="text-xs text-gray-400 mt-1">${error.message}</p>
</div> </div>
`; `;
} }
@@ -1630,213 +1555,49 @@
} }
}; };
let pollInterval = null; // Simplified bootstrap - initializes once
const bootstrap = () => { 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) { if (pollInterval) {
clearInterval(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); // Poll immediately and set up interval
console.log('Network Traffic Bootstrap: Endpoint:', pollingEndpoint); poll();
pollInterval = setInterval(poll, 30000);
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');
}
}
}; };
// Make bootstrap accessible globally for re-initialization window.initNetworkTraffic = () => {
window.initNetworkTraffic = bootstrap; isInitialized = false;
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;
}; };
// Try immediately // Initialize
if (!tryBootstrap()) { if (document.readyState === 'loading') {
// If DOM is loading, wait for it document.addEventListener('DOMContentLoaded', bootstrap);
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);
}
} else { } else {
// Successfully bootstrapped, also try immediate poll as backup setTimeout(bootstrap, 10);
console.log('Network Traffic: Bootstrap successful, ensuring immediate poll...');
} }
// Re-initialize on Livewire lifecycle events (immediate) // Handle Livewire navigation
document.addEventListener('livewire:init', () => {
setTimeout(tryBootstrap, 10);
});
document.addEventListener('livewire:load', () => {
setTimeout(tryBootstrap, 10);
});
// Re-initialize when Livewire navigates (tab switches)
document.addEventListener('livewire:navigated', () => { document.addEventListener('livewire:navigated', () => {
// Clear charts when navigating (DOM will be replaced) Object.keys(charts).forEach(id => { try { charts[id]?.destroy(); } catch(e) {} });
Object.keys(charts).forEach(interfaceId => { charts = {};
try { isInitialized = false;
if (charts[interfaceId] && typeof charts[interfaceId].destroy === 'function') { setTimeout(bootstrap, 50);
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);
}); });
// 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', () => { window.addEventListener('beforeunload', () => {
clearInterval(visibilityCheck); if (pollInterval) clearInterval(pollInterval);
if (pollInterval) {
clearInterval(pollInterval);
}
}); });
})(); })();