// WebSocket Manager for Real-time Updates class WebSocketManager { constructor() { this.ws = null; this.reconnectAttempts = 0; this.maxReconnectAttempts = 5; this.reconnectDelay = 1000; this.isConnected = false; this.subscribers = new Map(); this.init(); } init() { this.connect(); this.setupStatusIndicator(); this.setupAutoReconnect(); } connect() { try { // Determine WebSocket URL const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const host = window.location.host; const wsUrl = `${protocol}//${host}/ws`; this.ws = new WebSocket(wsUrl); this.ws.onopen = () => this.onOpen(); this.ws.onclose = () => this.onClose(); this.ws.onerror = (error) => this.onError(error); this.ws.onmessage = (event) => this.onMessage(event); } catch (error) { console.error('WebSocket connection error:', error); this.scheduleReconnect(); } } onOpen() { console.log('WebSocket connected'); this.isConnected = true; this.reconnectAttempts = 0; this.updateStatus('connected'); // Notify subscribers this.notify('connection', { status: 'connected' }); // Send authentication if needed if (window.authManager && window.authManager.token) { this.send({ type: 'auth', token: window.authManager.token }); } // Subscribe to default channels this.send({ type: 'subscribe', channels: ['requests', 'metrics', 'logs'] }); } onClose() { console.log('WebSocket disconnected'); this.isConnected = false; this.updateStatus('disconnected'); // Notify subscribers this.notify('connection', { status: 'disconnected' }); // Schedule reconnection this.scheduleReconnect(); } onError(error) { console.error('WebSocket error:', error); this.updateStatus('error'); } onMessage(event) { try { const data = JSON.parse(event.data); this.handleMessage(data); } catch (error) { console.error('Error parsing WebSocket message:', error); } } handleMessage(data) { // Handle request events if (data.type === 'request') { this.notify('requests', data.payload); } // Notify specific channel subscribers const channel = data.channel || data.type; if (channel && this.subscribers.has(channel)) { this.subscribers.get(channel).forEach(callback => { try { callback(data.payload); } catch (error) { console.error('Error in WebSocket callback:', error); } }); } } handleRequest(request) { // Update request counters this.updateRequestCounters(request); // Add to recent requests if on overview page if (window.dashboard && window.dashboard.currentPage === 'overview') { this.addRecentRequest(request); } // Update monitoring stream if on monitoring page if (window.dashboard && window.dashboard.currentPage === 'monitoring') { this.addToMonitoringStream(request); } } handleMetric(metric) { // Update charts with new metric data this.updateCharts(metric); // Update system metrics display this.updateSystemMetrics(metric); } handleLog(log) { // Add to logs table if on logs page if (window.dashboard && window.dashboard.currentPage === 'logs') { this.addLogEntry(log); } // Add to monitoring logs if on monitoring page if (window.dashboard && window.dashboard.currentPage === 'monitoring') { this.addToLogStream(log); } } handleSystem(system) { // Update system health indicators this.updateSystemHealth(system); } handleError(error) { console.error('Server error:', error); // Show error toast if (window.authManager) { window.authManager.showToast(error.message || 'Server error', 'error'); } } send(data) { if (this.ws && this.ws.readyState === WebSocket.OPEN) { this.ws.send(JSON.stringify(data)); return true; } else { console.warn('WebSocket not connected, message not sent:', data); return false; } } subscribe(channel, callback) { if (!this.subscribers.has(channel)) { this.subscribers.set(channel, new Set()); } this.subscribers.get(channel).add(callback); // Send subscription to server this.send({ type: 'subscribe', channels: [channel] }); // Return unsubscribe function return () => this.unsubscribe(channel, callback); } unsubscribe(channel, callback) { if (this.subscribers.has(channel)) { this.subscribers.get(channel).delete(callback); // If no more subscribers, unsubscribe from server if (this.subscribers.get(channel).size === 0) { this.send({ type: 'unsubscribe', channels: [channel] }); } } } notify(channel, data) { if (this.subscribers.has(channel)) { this.subscribers.get(channel).forEach(callback => { try { callback(data); } catch (error) { console.error('Error in notification callback:', error); } }); } } scheduleReconnect() { if (this.reconnectAttempts >= this.maxReconnectAttempts) { console.warn('Max reconnection attempts reached'); return; } this.reconnectAttempts++; const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1); console.log(`Scheduling reconnection in ${delay}ms (attempt ${this.reconnectAttempts})`); setTimeout(() => { if (!this.isConnected) { this.connect(); } }, delay); } setupAutoReconnect() { // Reconnect when browser comes online window.addEventListener('online', () => { if (!this.isConnected) { console.log('Browser online, attempting to reconnect...'); this.connect(); } }); // Keepalive ping setInterval(() => { if (this.isConnected) { this.send({ type: 'ping' }); } }, 30000); } setupStatusIndicator() { // Status indicator is already in the HTML // This function just ensures it's properly styled } updateStatus(status) { const statusElement = document.getElementById('ws-status-nav'); if (!statusElement) return; const dot = statusElement.querySelector('.ws-dot'); const text = statusElement.querySelector('.ws-text'); if (!dot || !text) return; // Remove all status classes dot.classList.remove('connected', 'disconnected'); statusElement.classList.remove('connected', 'disconnected'); // Add new status class dot.classList.add(status); statusElement.classList.add(status); // Update text const statusText = { 'connected': 'Connected', 'disconnected': 'Disconnected', 'connecting': 'Connecting...', 'error': 'Connection Error' }; text.textContent = statusText[status] || status; } // Helper methods for updating UI updateRequestCounters(request) { // Update request counters in overview stats const requestCountElement = document.querySelector('[data-stat="total-requests"]'); if (requestCountElement) { const currentCount = parseInt(requestCountElement.textContent) || 0; requestCountElement.textContent = currentCount + 1; } // Update token counters const tokenCountElement = document.querySelector('[data-stat="total-tokens"]'); if (tokenCountElement && (request.total_tokens || request.tokens)) { const currentTokens = parseInt(tokenCountElement.textContent) || 0; tokenCountElement.textContent = currentTokens + (request.total_tokens || request.tokens); } } addRecentRequest(request) { const tableBody = document.querySelector('#recent-requests tbody'); if (!tableBody) return; const row = document.createElement('tr'); // Format time const time = new Date(request.timestamp || Date.now()).toLocaleTimeString(); // Format status badge const statusClass = request.status === 'success' ? 'success' : request.status === 'error' ? 'danger' : 'warning'; const statusIcon = request.status === 'success' ? 'check-circle' : request.status === 'error' ? 'exclamation-circle' : 'exclamation-triangle'; row.innerHTML = `