// 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 = ` ${time} ${request.client_id || 'Unknown'} ${request.provider || 'Unknown'} ${request.model || 'Unknown'} ${(request.total_tokens || request.tokens || 0)} ${request.status || 'unknown'} `; // Add to top of table tableBody.insertBefore(row, tableBody.firstChild); // Limit to 50 rows const rows = tableBody.querySelectorAll('tr'); if (rows.length > 50) { tableBody.removeChild(rows[rows.length - 1]); } } addToMonitoringStream(request) { const streamElement = document.getElementById('request-stream'); if (!streamElement) return; const entry = document.createElement('div'); entry.className = 'stream-entry'; // Format time const time = new Date().toLocaleTimeString(); // Determine icon based on status let icon = 'question-circle'; let color = 'var(--text-secondary)'; if (request.status === 'success') { icon = 'check-circle'; color = 'var(--success)'; } else if (request.status === 'error') { icon = 'exclamation-circle'; color = 'var(--danger)'; } entry.innerHTML = `
${time}
${request.client_id || 'Unknown'} → ${request.provider || 'Unknown'} (${request.model || 'Unknown'})
${(request.total_tokens || request.tokens || 0)} tokens • ${(request.duration_ms || request.duration || 0)}ms
`; // Add to top of stream streamElement.insertBefore(entry, streamElement.firstChild); // Limit to 20 entries const entries = streamElement.querySelectorAll('.stream-entry'); if (entries.length > 20) { streamElement.removeChild(entries[entries.length - 1]); } // Add highlight animation entry.classList.add('highlight'); setTimeout(() => entry.classList.remove('highlight'), 1000); } updateCharts(metric) { // This would update Chart.js charts with new data // Implementation depends on specific chart setup } updateSystemMetrics(metric) { const metricsElement = document.getElementById('system-metrics'); if (!metricsElement) return; // Update specific metric displays // This is a simplified example } addLogEntry(log) { const tableBody = document.querySelector('#logs-table tbody'); if (!tableBody) return; const row = document.createElement('tr'); // Format time const time = new Date(log.timestamp || Date.now()).toLocaleString(); // Determine log level class const levelClass = log.level || 'info'; row.innerHTML = ` ${time} ${levelClass.toUpperCase()} ${log.source || 'Unknown'} ${log.message || ''} `; // Add to top of table tableBody.insertBefore(row, tableBody.firstChild); // Limit to 100 rows const rows = tableBody.querySelectorAll('tr'); if (rows.length > 100) { tableBody.removeChild(rows[rows.length - 1]); } } addToLogStream(log) { const logStreamElement = document.getElementById('system-logs'); if (!logStreamElement) return; const entry = document.createElement('div'); entry.className = `log-entry log-${log.level || 'info'}`; // Format time const time = new Date().toLocaleTimeString(); // Determine icon based on level let icon = 'info-circle'; if (log.level === 'error') icon = 'exclamation-circle'; if (log.level === 'warn') icon = 'exclamation-triangle'; if (log.level === 'debug') icon = 'bug'; entry.innerHTML = `
${time}
${log.message || ''}
`; // Add to top of stream logStreamElement.insertBefore(entry, logStreamElement.firstChild); // Limit to 50 entries const entries = logStreamElement.querySelectorAll('.log-entry'); if (entries.length > 50) { logStreamElement.removeChild(entries[entries.length - 1]); } } updateSystemHealth(system) { const healthElement = document.getElementById('system-health'); if (!healthElement) return; // Update system health indicators // This is a simplified example } disconnect() { if (this.ws) { this.ws.close(); this.ws = null; } this.isConnected = false; this.updateStatus('disconnected'); } reconnect() { this.disconnect(); this.connect(); } } // Initialize WebSocket manager when DOM is loaded document.addEventListener('DOMContentLoaded', () => { window.wsManager = new WebSocketManager(); }); // Export for use in other modules if (typeof module !== 'undefined' && module.exports) { module.exports = WebSocketManager; }