Files
pulse/public/js/socketHandler.js
T
rcourtman f53550fc6c fix: simplify frontend update channel switching for better UX
- Remove confusing "preview mode" - channels switch and save immediately
- Eliminate complex localStorage channel persistence logic
- Reduce update reload time from 10+ seconds to 3 seconds
- Remove redundant "Switch to X & Update" buttons
- Make channel switching instant with immediate feedback

Users can now simply change the dropdown to switch channels instantly,
then click Apply Update for a fast 3-second reload experience.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-06-14 11:01:03 +01:00

532 lines
19 KiB
JavaScript

PulseApp.socketHandler = (() => {
let socket = null;
let isConnected = false;
let reconnectAttempts = 0;
const maxReconnectAttempts = 10;
const reconnectDelay = 2000; // 2 seconds
function init() {
createSocket();
}
function createSocket() {
if (socket) {
socket.removeAllListeners();
socket.disconnect();
}
socket = io();
window.socket = socket; // Make socket available globally for alerts
setupEventListeners();
}
function setupEventListeners() {
socket.on('connect', handleConnect);
socket.on('disconnect', handleDisconnect);
socket.on('rawData', handleRawData);
socket.on('initialState', handleInitialState);
socket.on('requestError', handleRequestError);
// Enhanced monitoring events
// Development features
socket.on('hotReload', handleHotReload);
// Configuration reload events
socket.on('configurationReloaded', handleConfigurationReloaded);
socket.on('configurationError', handleConfigurationError);
// Handle connection errors
socket.on('connect_error', handleConnectError);
socket.on('reconnect', handleReconnect);
socket.on('reconnect_error', handleReconnectError);
socket.on('reconnect_failed', handleReconnectFailed);
}
function handleConnect() {
isConnected = true;
reconnectAttempts = 0;
updateConnectionStatus('connected');
// Request initial data
socket.emit('requestData');
}
function handleDisconnect(reason) {
isConnected = false;
updateConnectionStatus('disconnected');
// If it's not a planned disconnect, try to reconnect
if (reason !== 'io client disconnect') {
attemptReconnect();
}
}
function handleRawData(data) {
try {
// Update main application state
if (PulseApp.state) {
PulseApp.state.updateState(data);
}
// Update alerts system with new state
if (PulseApp.alerts && data.alerts) {
PulseApp.alerts.updateAlertsFromState(data);
}
// Process UI updates based on tab
updateUIFromData(data);
} catch (error) {
console.error('[Socket] Error processing raw data:', error);
}
}
function handleInitialState(data) {
try {
if (PulseApp.state) {
PulseApp.state.updateState(data);
}
updateUIFromData(data);
} catch (error) {
console.error('[Socket] Error processing initial state:', error);
}
}
function handleRequestError(error) {
console.error('[Socket] Request error:', error);
updateConnectionStatus('error');
}
function handleHotReload() {
// Check if we're in development mode (hot reload is only enabled in dev)
// Since this is client-side code, we can't access process.env directly
const isDevelopment = window.location.hostname === 'localhost' ||
window.location.hostname === '127.0.0.1' ||
window.location.port === '7655';
if (isDevelopment) {
window.location.reload();
}
}
function handleConfigurationReloaded(data) {
// Show notification to user
if (PulseApp.alerts && PulseApp.alerts.showNotification) {
PulseApp.alerts.showNotification({
message: 'Configuration has been updated and reloaded',
severity: 'info'
});
}
// Request fresh data with new configuration
socket.emit('requestData');
}
function handleConfigurationError(data) {
console.error('[Socket] Configuration reload error:', data.error);
// Show error notification to user
if (PulseApp.alerts && PulseApp.alerts.showNotification) {
PulseApp.alerts.showNotification({
message: 'Failed to reload configuration: ' + data.error,
severity: 'critical'
});
}
}
function handleConnectError(error) {
console.error('[Socket] Connection error:', error);
updateConnectionStatus('error');
}
function handleReconnect() {
reconnectAttempts = 0;
updateConnectionStatus('connected');
}
function handleReconnectError(error) {
console.error('[Socket] Reconnection error:', error);
reconnectAttempts++;
updateConnectionStatus('reconnecting');
}
function handleReconnectFailed() {
console.error('[Socket] Reconnection failed - max attempts reached');
updateConnectionStatus('failed');
}
function attemptReconnect() {
if (reconnectAttempts < maxReconnectAttempts) {
reconnectAttempts++;
updateConnectionStatus('reconnecting');
setTimeout(() => {
socket.connect();
}, reconnectDelay * reconnectAttempts); // Exponential backoff
} else {
updateConnectionStatus('failed');
}
}
function updateConnectionStatus(status) {
const statusElement = document.getElementById('connection-status');
if (!statusElement) return;
// Clear previous classes
statusElement.className = statusElement.className
.replace(/\b(connected|disconnected|reconnecting|error|failed)\b/g, '')
.trim();
let statusText, statusClass;
switch (status) {
case 'connected':
statusText = 'Connected';
statusClass = 'connected text-xs px-2 py-1 rounded-full bg-green-100 dark:bg-green-900 text-green-600 dark:text-green-400';
break;
case 'disconnected':
statusText = 'Disconnected';
statusClass = 'disconnected text-xs px-2 py-1 rounded-full bg-gray-200 dark:bg-gray-700 text-gray-600 dark:text-gray-400';
break;
case 'reconnecting':
statusText = `Reconnecting... (${reconnectAttempts}/${maxReconnectAttempts})`;
statusClass = 'reconnecting text-xs px-2 py-1 rounded-full bg-yellow-100 dark:bg-yellow-900 text-yellow-600 dark:text-yellow-400 animate-pulse';
break;
case 'error':
statusText = 'Connection Error';
statusClass = 'error text-xs px-2 py-1 rounded-full bg-red-100 dark:bg-red-900 text-red-600 dark:text-red-400';
break;
case 'failed':
statusText = 'Connection Failed';
statusClass = 'failed text-xs px-2 py-1 rounded-full bg-red-100 dark:bg-red-900 text-red-600 dark:text-red-400';
break;
default:
statusText = 'Unknown';
statusClass = 'text-xs px-2 py-1 rounded-full bg-gray-200 dark:bg-gray-700 text-gray-600 dark:text-gray-400';
}
statusElement.textContent = statusText;
statusElement.className = statusClass;
}
function updateUIFromData(data) {
try {
// Hide loading overlay when we receive data
const loadingOverlay = document.getElementById('loading-overlay');
if (loadingOverlay && (data.nodes || data.vms || data.containers || data.pbs)) {
loadingOverlay.style.display = 'none';
}
// Update different UI sections based on current tab
const activeTab = document.querySelector('.tab.active');
if (!activeTab) return;
const tabName = activeTab.getAttribute('data-tab');
switch (tabName) {
case 'main':
updateMainTab(data);
break;
case 'storage':
updateStorageTab(data);
break;
case 'pbs':
updatePbsTab(data);
break;
case 'backups':
updateBackupsTab(data);
break;
}
// Update performance indicators if available
updatePerformanceIndicators(data);
} catch (error) {
console.error('[Socket] Error updating UI from data:', error);
}
}
function updateMainTab(data) {
try {
// Capture scroll position of main table container before updates
const mainTableContainer = document.querySelector('.table-container');
let savedScrollTop = 0;
let savedScrollLeft = 0;
if (mainTableContainer) {
savedScrollTop = mainTableContainer.scrollTop;
savedScrollLeft = mainTableContainer.scrollLeft;
}
// Update node summary cards
if (PulseApp.ui && PulseApp.ui.nodes && data.nodes) {
PulseApp.ui.nodes.updateNodeSummaryCards(data.nodes);
}
// Update main dashboard
if (PulseApp.ui && PulseApp.ui.dashboard) {
PulseApp.ui.dashboard.updateDashboardTable();
}
// Restore scroll position after updates
if (mainTableContainer && (savedScrollTop > 0 || savedScrollLeft > 0)) {
const restoreScroll = () => {
mainTableContainer.scrollTop = savedScrollTop;
mainTableContainer.scrollLeft = savedScrollLeft;
};
// Multiple restoration attempts
setTimeout(restoreScroll, 0);
setTimeout(restoreScroll, 16);
setTimeout(restoreScroll, 50);
setTimeout(restoreScroll, 100);
requestAnimationFrame(restoreScroll);
// Final verification
setTimeout(() => {
if (Math.abs(mainTableContainer.scrollTop - savedScrollTop) > 10) {
mainTableContainer.scrollTo({
top: savedScrollTop,
left: savedScrollLeft,
behavior: 'instant'
});
} else {
}
}, 200);
}
} catch (error) {
console.error('[Socket] Error updating main tab:', error);
}
}
function updateStorageTab(data) {
try {
if (PulseApp.ui && PulseApp.ui.storage && data.nodes) {
PulseApp.ui.storage.updateStorageInfo();
}
} catch (error) {
console.error('[Socket] Error updating storage tab:', error);
}
}
function updatePbsTab(data) {
try {
// Check if mobile view
const isMobile = window.innerWidth < 768;
// Capture scroll positions of various containers before updates
const pbsTab = document.getElementById('pbsTab');
const pbsInstancesContainer = document.getElementById('pbs-instances-container');
const pbsTableContainer = document.querySelector('#pbsTab .table-container');
const pbsTaskTableContainer = document.querySelector('#pbs-tasks-table-container');
const mobileContainers = document.querySelectorAll('.mobile-single-instance, .mobile-pbs-accordion, .mobile-layout');
// Store all scroll positions
const scrollPositions = new Map();
// Capture main tab scroll
if (pbsTab) {
scrollPositions.set(pbsTab, {
top: pbsTab.scrollTop,
left: pbsTab.scrollLeft
});
}
// Capture instances container scroll
if (pbsInstancesContainer) {
scrollPositions.set(pbsInstancesContainer, {
top: pbsInstancesContainer.scrollTop,
left: pbsInstancesContainer.scrollLeft
});
}
// Capture table container scrolls
if (pbsTableContainer) {
scrollPositions.set(pbsTableContainer, {
top: pbsTableContainer.scrollTop,
left: pbsTableContainer.scrollLeft
});
}
if (pbsTaskTableContainer) {
scrollPositions.set(pbsTaskTableContainer, {
top: pbsTaskTableContainer.scrollTop,
left: pbsTaskTableContainer.scrollLeft
});
}
// Capture mobile container scrolls
mobileContainers.forEach((container, index) => {
scrollPositions.set(container, {
top: container.scrollTop,
left: container.scrollLeft
});
});
// Also capture body scroll for mobile
if (isMobile) {
scrollPositions.set(document.body, {
top: window.pageYOffset || document.documentElement.scrollTop,
left: window.pageXOffset || document.documentElement.scrollLeft
});
}
// Function to restore scroll positions
const restoreScroll = () => {
scrollPositions.forEach((position, element) => {
if (position.top > 0 || position.left > 0) {
if (element === document.body) {
window.scrollTo(position.left, position.top);
} else if (element && element.parentNode) {
element.scrollTop = position.top;
element.scrollLeft = position.left;
}
}
});
};
// Check if the new PBS data structure keys exist in the received data
// data.pbs is the array of PBS instances
// data.allPbsTasks and data.aggregatedPbsTaskSummary are top-level in the state
if (PulseApp.ui && PulseApp.ui.pbs && data.pbs) { // Ensure data.pbs exists
// PulseApp.ui.pbs.updatePbsInfo expects the array of PBS instances.
// Task data (allPbsTasks, aggregatedPbsTaskSummary) is available in PulseApp.state if needed by UI components directly.
// For now, updatePbsInfo primarily works with the pbs array.
PulseApp.ui.pbs.updatePbsInfo(data.pbs);
// Restore scroll positions with multiple timing strategies
setTimeout(restoreScroll, 0);
setTimeout(restoreScroll, 16);
setTimeout(restoreScroll, 50);
setTimeout(restoreScroll, 100);
requestAnimationFrame(restoreScroll);
} else {
// Optional: Log if critical data for PBS tab is missing
if (!data.pbs) {
console.warn('[Socket - PBS Tab] PBS data (data.pbs) not found in received socket data.');
}
}
} catch (error) {
console.error('[Socket] Error updating PBS tab:', error);
}
}
function updateBackupsTab(data) {
try {
// Only update if backups tab is active and we have relevant data
const backupsTab = document.querySelector('[data-tab="backups"]');
const isBackupsTabActive = backupsTab && backupsTab.classList.contains('active');
if (PulseApp.ui && PulseApp.ui.backups && (data.pbs || data.pveBackups) && isBackupsTabActive) {
PulseApp.ui.backups.updateBackupsTab(false); // Mark as API update, not user action
}
} catch (error) {
console.error('[Socket] Error updating backups tab:', error);
}
}
function updatePerformanceIndicators(data) {
try {
// Update performance stats if available
if (data.stats) {
updateStatsDisplay(data.stats);
}
// Update any health indicators
if (data.performance) {
updateHealthDisplay(data.performance);
}
} catch (error) {
console.error('[Socket] Error updating performance indicators:', error);
}
}
function updateStatsDisplay(stats) {
// Update various stats in the UI
try {
const statusText = document.getElementById('dashboard-status-text');
if (statusText && stats.totalGuests !== undefined) {
const runningText = stats.runningGuests ? `${stats.runningGuests} running` : '0 running';
const stoppedText = stats.stoppedGuests ? `${stats.stoppedGuests} stopped` : '0 stopped';
statusText.textContent = `${stats.totalGuests} total guests (${runningText}, ${stoppedText})`;
}
} catch (error) {
console.error('[Socket] Error updating stats display:', error);
}
}
function updateHealthDisplay(performance) {
// Update health indicators in the UI
try {
// Could add health indicators to the header or other parts of the UI
// For now, this is just a placeholder for future enhancements
} catch (error) {
console.error('[Socket] Error updating health display:', error);
}
}
// Manual data request
function requestData() {
if (socket && isConnected) {
socket.emit('requestData');
} else {
console.warn('[Socket] Cannot request data - not connected');
}
}
// Get connection status
function getConnectionStatus() {
return {
connected: isConnected,
reconnectAttempts,
socket: socket ? socket.id : null
};
}
// Manual reconnect
function reconnect() {
if (socket) {
reconnectAttempts = 0;
socket.disconnect();
socket.connect();
}
}
// Cleanup
function destroy() {
if (socket) {
socket.removeAllListeners();
socket.disconnect();
socket = null;
}
isConnected = false;
window.socket = null;
}
// Public API
return {
init,
requestData,
getConnectionStatus,
reconnect,
destroy
};
})();
// Auto-initialize when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', PulseApp.socketHandler.init);
} else {
PulseApp.socketHandler.init();
}