refactor: enhance network traffic polling logic and initialization handling
This commit is contained in:
@@ -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);
|
||||
});
|
||||
})();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user