// WebSocket functionality for real-time updates class WebSocketManager { constructor() { this.socket = null; this.reconnectAttempts = 0; this.maxReconnectAttempts = 5; this.reconnectDelay = 1000; this.isConnected = false; this.messageHandlers = new Map(); this.init(); } init() { this.connect(); this.setupEventListeners(); } connect() { try { // Initialize Socket.IO connection this.socket = io('/logs', { transports: ['websocket', 'polling'], upgrade: true, rememberUpgrade: true }); this.setupSocketEvents(); } catch (error) { console.error('Failed to initialize WebSocket connection:', error); this.scheduleReconnect(); } } setupSocketEvents() { this.socket.on('connect', () => { console.log('WebSocket connected'); this.isConnected = true; this.reconnectAttempts = 0; this.updateConnectionStatus('connected'); // Emit any pending events this.emitPendingEvents(); }); this.socket.on('disconnect', (reason) => { console.log('WebSocket disconnected:', reason); this.isConnected = false; this.updateConnectionStatus('disconnected'); // Attempt to reconnect unless it was a manual disconnect if (reason !== 'io client disconnect') { this.scheduleReconnect(); } }); this.socket.on('connect_error', (error) => { console.error('WebSocket connection error:', error); this.isConnected = false; this.updateConnectionStatus('error'); this.scheduleReconnect(); }); // Script execution events this.socket.on('script_started', (data) => { this.handleScriptEvent('started', data); }); this.socket.on('script_completed', (data) => { this.handleScriptEvent('completed', data); }); this.socket.on('script_failed', (data) => { this.handleScriptEvent('failed', data); }); this.socket.on('script_output', (data) => { this.handleScriptOutput(data); }); // System events this.socket.on('system_notification', (data) => { this.handleSystemNotification(data); }); // Custom message handling this.socket.on('custom_message', (data) => { this.handleCustomMessage(data); }); } scheduleReconnect() { if (this.reconnectAttempts < this.maxReconnectAttempts) { this.reconnectAttempts++; const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1); console.log(`Attempting to reconnect in ${delay}ms (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`); setTimeout(() => { this.connect(); }, delay); } else { console.error('Max reconnection attempts reached'); this.updateConnectionStatus('failed'); } } updateConnectionStatus(status) { // Update UI connection indicator const statusIndicator = document.getElementById('connection-status'); if (statusIndicator) { statusIndicator.className = `connection-status ${status}`; statusIndicator.title = `Connection status: ${status}`; } // Dispatch connection status event window.dispatchEvent(new CustomEvent('connectionStatusChanged', { detail: { status, isConnected: this.isConnected } })); } handleScriptEvent(eventType, data) { console.log(`Script ${eventType}:`, data); // Update script status in UI const scriptElements = document.querySelectorAll(`[data-script-id="${data.script_id}"]`); scriptElements.forEach(element => { const statusElement = element.querySelector('.script-status'); if (statusElement) { statusElement.textContent = eventType; statusElement.className = `script-status status-${eventType}`; } }); // Show notification if (eventType === 'completed') { this.showNotification(`Script completed successfully`, 'success'); } else if (eventType === 'failed') { this.showNotification(`Script execution failed`, 'error'); } // Call registered handlers const handlers = this.messageHandlers.get('script_event') || []; handlers.forEach(handler => handler(eventType, data)); } handleScriptOutput(data) { // Update log displays const logContainers = document.querySelectorAll('.log-container[data-script-id=\"${data.script_id}\"]'); logContainers.forEach(container => { this.appendLogEntry(container, data); }); // Call registered handlers const handlers = this.messageHandlers.get('script_output') || []; handlers.forEach(handler => handler(data)); } appendLogEntry(container, data) { const logEntry = document.createElement('div'); logEntry.className = `log-entry log-${data.level || 'info'}`; const timestamp = new Date(data.timestamp).toLocaleTimeString(); logEntry.innerHTML = ` ${this.escapeHtml(data.message)} `; container.appendChild(logEntry); // Auto-scroll to bottom container.scrollTop = container.scrollHeight; // Limit log entries to prevent memory issues const maxEntries = 1000; while (container.children.length > maxEntries) { container.removeChild(container.firstChild); } } handleSystemNotification(data) { this.showNotification(data.message, data.type || 'info'); // Call registered handlers const handlers = this.messageHandlers.get('system_notification') || []; handlers.forEach(handler => handler(data)); } handleCustomMessage(data) { // Call registered handlers const handlers = this.messageHandlers.get(data.type) || []; handlers.forEach(handler => handler(data)); } showNotification(message, type = 'info') { if (typeof ScriptsManager !== 'undefined' && ScriptsManager.showNotification) { ScriptsManager.showNotification(message, type); } else { console.log(`${type.toUpperCase()}: ${message}`); } } escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } setupEventListeners() { // Listen for page visibility changes to manage connection document.addEventListener('visibilitychange', () => { if (document.hidden) { // Page is hidden, reduce activity this.emit('page_hidden'); } else { // Page is visible, resume normal activity this.emit('page_visible'); } }); // Handle page unload window.addEventListener('beforeunload', () => { if (this.socket) { this.socket.disconnect(); } }); } emitPendingEvents() { // Emit any events that were queued while disconnected // This could be implemented to store events in localStorage // and replay them when reconnected } // Public API on(eventType, handler) { if (!this.messageHandlers.has(eventType)) { this.messageHandlers.set(eventType, []); } this.messageHandlers.get(eventType).push(handler); } off(eventType, handler) { const handlers = this.messageHandlers.get(eventType); if (handlers) { const index = handlers.indexOf(handler); if (index > -1) { handlers.splice(index, 1); } } } emit(eventType, data = {}) { if (this.socket && this.isConnected) { this.socket.emit(eventType, data); } else { console.warn('Cannot emit event - WebSocket not connected'); } } joinRoom(room) { this.emit('join_room', { room }); } leaveRoom(room) { this.emit('leave_room', { room }); } subscribeToScript(scriptId) { this.emit('subscribe_script', { script_id: scriptId }); } unsubscribeFromScript(scriptId) { this.emit('unsubscribe_script', { script_id: scriptId }); } disconnect() { if (this.socket) { this.socket.disconnect(); } } isConnectedToSocket() { return this.isConnected; } } // Log display component class LogDisplay { constructor(containerId, scriptId = null) { this.container = document.getElementById(containerId); this.scriptId = scriptId; this.maxLines = 1000; this.autoScroll = true; this.init(); } init() { if (!this.container) { console.error(`Log container not found: ${containerId}`); return; } this.container.classList.add('log-container'); if (this.scriptId) { this.container.dataset.scriptId = this.scriptId; } this.setupControls(); this.setupEventListeners(); } setupControls() { const controls = document.createElement('div'); controls.className = 'log-controls mb-2'; controls.innerHTML = `