// 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 = ` [${timestamp}] ${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 = `
`; this.container.parentNode.insertBefore(controls, this.container); // Setup control event listeners controls.querySelector('#log-clear').addEventListener('click', () => this.clear()); controls.querySelector('#log-scroll-toggle').addEventListener('click', () => this.toggleAutoScroll()); controls.querySelector('#log-download').addEventListener('click', () => this.downloadLogs()); } setupEventListeners() { // Listen for scroll events to disable auto-scroll when user scrolls up this.container.addEventListener('scroll', () => { const isScrolledToBottom = this.container.scrollHeight - this.container.clientHeight <= this.container.scrollTop + 1; if (!isScrolledToBottom && this.autoScroll) { this.autoScroll = false; this.updateScrollToggle(); } }); } appendLog(message, level = 'info', timestamp = null) { const logEntry = document.createElement('div'); logEntry.className = `log-entry log-${level}`; const time = timestamp ? new Date(timestamp) : new Date(); const timeString = time.toLocaleTimeString(); logEntry.innerHTML = ` [${timeString}] ${this.escapeHtml(message)} `; this.container.appendChild(logEntry); // Limit number of entries while (this.container.children.length > this.maxLines) { this.container.removeChild(this.container.firstChild); } // Auto-scroll if enabled if (this.autoScroll) { this.scrollToBottom(); } } clear() { this.container.innerHTML = ''; } scrollToBottom() { this.container.scrollTop = this.container.scrollHeight; } toggleAutoScroll() { this.autoScroll = !this.autoScroll; this.updateScrollToggle(); if (this.autoScroll) { this.scrollToBottom(); } } updateScrollToggle() { const toggle = document.getElementById('log-scroll-toggle'); if (toggle) { const icon = toggle.querySelector('i'); if (this.autoScroll) { icon.className = 'bi bi-arrow-down'; toggle.classList.add('active'); } else { icon.className = 'bi bi-pause'; toggle.classList.remove('active'); } } } downloadLogs() { const logs = Array.from(this.container.children).map(entry => { const timestamp = entry.querySelector('.log-timestamp').textContent; const content = entry.querySelector('.log-content').textContent; return `${timestamp} ${content}`; }).join('\n'); const blob = new Blob([logs], { type: 'text/plain' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `logs-${new Date().toISOString().slice(0, 19)}.txt`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } } // Initialize WebSocket when DOM is loaded document.addEventListener('DOMContentLoaded', function() { // Only initialize if user is authenticated if (document.body.dataset.authenticated === 'true') { window.webSocketManager = new WebSocketManager(); // Create global log display if container exists const logContainer = document.getElementById('global-logs'); if (logContainer) { window.globalLogDisplay = new LogDisplay('global-logs'); } } }); // Export for use in other modules window.WebSocketManager = WebSocketManager; window.LogDisplay = LogDisplay;