Your Name
feat: UI improvements and error suppression - Enhanced dashboard and market pages with improved header buttons, logo, and currency symbol display - Stopped animated ticker - Removed pie chart legends - Added error suppressor for external service errors (SSE, Permissions-Policy warnings) - Improved header button prominence and icon appearance - Enhanced logo with glow effects and better design - Fixed currency symbol visibility in market tables
8b7b267
| /** | |
| * WebSocket Client - Real-time Updates with Proper Cleanup | |
| * Crypto Monitor HF - Enterprise Edition | |
| */ | |
| class CryptoWebSocketClient { | |
| constructor(url = null) { | |
| this.url = url || `ws://${window.location.host}/ws`; | |
| this.ws = null; | |
| this.sessionId = null; | |
| this.isConnected = false; | |
| this.reconnectAttempts = 0; | |
| this.maxReconnectAttempts = 5; | |
| this.reconnectDelay = 3000; | |
| this.reconnectTimer = null; | |
| this.heartbeatTimer = null; | |
| // Event handlers stored for cleanup | |
| this.messageHandlers = new Map(); | |
| this.connectionCallbacks = []; | |
| // Auto-connect | |
| this.connect(); | |
| } | |
| /** | |
| * Connect to WebSocket server | |
| */ | |
| connect() { | |
| // Clean up existing connection | |
| this.disconnect(); | |
| try { | |
| console.log('[WebSocket] Connecting to:', this.url); | |
| this.ws = new WebSocket(this.url); | |
| // Bind event handlers | |
| this.ws.onopen = this.handleOpen.bind(this); | |
| this.ws.onmessage = this.handleMessage.bind(this); | |
| this.ws.onerror = this.handleError.bind(this); | |
| this.ws.onclose = this.handleClose.bind(this); | |
| } catch (error) { | |
| console.error('[WebSocket] Connection error:', error); | |
| this.scheduleReconnect(); | |
| } | |
| } | |
| /** | |
| * Disconnect and cleanup | |
| */ | |
| disconnect() { | |
| // Clear timers | |
| if (this.reconnectTimer) { | |
| clearTimeout(this.reconnectTimer); | |
| this.reconnectTimer = null; | |
| } | |
| if (this.heartbeatTimer) { | |
| clearInterval(this.heartbeatTimer); | |
| this.heartbeatTimer = null; | |
| } | |
| // Close WebSocket | |
| if (this.ws) { | |
| this.ws.onopen = null; | |
| this.ws.onmessage = null; | |
| this.ws.onerror = null; | |
| this.ws.onclose = null; | |
| if (this.ws.readyState === WebSocket.OPEN) { | |
| this.ws.close(); | |
| } | |
| this.ws = null; | |
| } | |
| this.isConnected = false; | |
| this.sessionId = null; | |
| } | |
| /** | |
| * Handle WebSocket open event | |
| */ | |
| handleOpen(event) { | |
| console.log('[WebSocket] Connected'); | |
| this.isConnected = true; | |
| this.reconnectAttempts = 0; | |
| // Notify connection callbacks | |
| this.notifyConnection(true); | |
| // Update UI | |
| this.updateConnectionStatus(true); | |
| // Start heartbeat | |
| this.startHeartbeat(); | |
| } | |
| /** | |
| * Handle WebSocket message event | |
| */ | |
| handleMessage(event) { | |
| try { | |
| const message = JSON.parse(event.data); | |
| const type = message.type; | |
| console.log('[WebSocket] Received message type:', type); | |
| // Handle system messages | |
| switch (type) { | |
| case 'welcome': | |
| this.sessionId = message.session_id; | |
| console.log('[WebSocket] Session ID:', this.sessionId); | |
| break; | |
| case 'heartbeat': | |
| this.send({ type: 'pong' }); | |
| break; | |
| case 'stats_update': | |
| this.handleStatsUpdate(message.data); | |
| break; | |
| case 'provider_stats': | |
| this.handleProviderStats(message.data); | |
| break; | |
| case 'market_update': | |
| this.handleMarketUpdate(message.data); | |
| break; | |
| case 'price_update': | |
| this.handlePriceUpdate(message.data); | |
| break; | |
| case 'alert': | |
| this.handleAlert(message.data); | |
| break; | |
| } | |
| // Call registered handler if exists | |
| const handler = this.messageHandlers.get(type); | |
| if (handler) { | |
| handler(message); | |
| } | |
| } catch (error) { | |
| console.error('[WebSocket] Error processing message:', error); | |
| } | |
| } | |
| /** | |
| * Handle WebSocket error event | |
| */ | |
| handleError(error) { | |
| console.error('[WebSocket] Error:', error); | |
| this.isConnected = false; | |
| this.updateConnectionStatus(false); | |
| } | |
| /** | |
| * Handle WebSocket close event | |
| */ | |
| handleClose(event) { | |
| console.log('[WebSocket] Disconnected'); | |
| this.isConnected = false; | |
| this.sessionId = null; | |
| // Notify connection callbacks | |
| this.notifyConnection(false); | |
| // Update UI | |
| this.updateConnectionStatus(false); | |
| // Stop heartbeat | |
| if (this.heartbeatTimer) { | |
| clearInterval(this.heartbeatTimer); | |
| this.heartbeatTimer = null; | |
| } | |
| // Schedule reconnect | |
| this.scheduleReconnect(); | |
| } | |
| /** | |
| * Schedule reconnection attempt | |
| */ | |
| scheduleReconnect() { | |
| if (this.reconnectAttempts < this.maxReconnectAttempts) { | |
| this.reconnectAttempts++; | |
| console.log(`[WebSocket] Reconnecting in ${this.reconnectDelay}ms (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`); | |
| this.reconnectTimer = setTimeout(() => { | |
| this.connect(); | |
| }, this.reconnectDelay); | |
| } else { | |
| console.error('[WebSocket] Max reconnection attempts reached'); | |
| this.showReconnectButton(); | |
| } | |
| } | |
| /** | |
| * Start heartbeat to keep connection alive | |
| */ | |
| startHeartbeat() { | |
| // Send ping every 30 seconds | |
| this.heartbeatTimer = setInterval(() => { | |
| if (this.isConnected) { | |
| this.send({ type: 'ping' }); | |
| } | |
| }, 30000); | |
| } | |
| /** | |
| * Send message to server | |
| */ | |
| send(data) { | |
| if (this.isConnected && this.ws && this.ws.readyState === WebSocket.OPEN) { | |
| this.ws.send(JSON.stringify(data)); | |
| } else { | |
| console.warn('[WebSocket] Cannot send - not connected'); | |
| } | |
| } | |
| /** | |
| * Subscribe to message group | |
| */ | |
| subscribe(group) { | |
| this.send({ | |
| type: 'subscribe', | |
| group: group | |
| }); | |
| } | |
| /** | |
| * Unsubscribe from message group | |
| */ | |
| unsubscribe(group) { | |
| this.send({ | |
| type: 'unsubscribe', | |
| group: group | |
| }); | |
| } | |
| /** | |
| * Request stats update | |
| */ | |
| requestStats() { | |
| this.send({ | |
| type: 'get_stats' | |
| }); | |
| } | |
| /** | |
| * Register message handler (with cleanup support) | |
| */ | |
| on(type, handler) { | |
| this.messageHandlers.set(type, handler); | |
| // Return cleanup function | |
| return () => { | |
| this.messageHandlers.delete(type); | |
| }; | |
| } | |
| /** | |
| * Remove message handler | |
| */ | |
| off(type) { | |
| this.messageHandlers.delete(type); | |
| } | |
| /** | |
| * Register connection callback | |
| */ | |
| onConnection(callback) { | |
| this.connectionCallbacks.push(callback); | |
| // Return cleanup function | |
| return () => { | |
| const index = this.connectionCallbacks.indexOf(callback); | |
| if (index > -1) { | |
| this.connectionCallbacks.splice(index, 1); | |
| } | |
| }; | |
| } | |
| /** | |
| * Notify connection callbacks | |
| */ | |
| notifyConnection(connected) { | |
| this.connectionCallbacks.forEach(callback => { | |
| try { | |
| callback(connected); | |
| } catch (error) { | |
| console.error('[WebSocket] Error in connection callback:', error); | |
| } | |
| }); | |
| } | |
| // ===== Message Handlers ===== | |
| handleStatsUpdate(data) { | |
| const activeConnections = data.active_connections || 0; | |
| const totalSessions = data.total_sessions || 0; | |
| this.updateOnlineUsers(activeConnections, totalSessions); | |
| if (data.client_types) { | |
| this.updateClientTypes(data.client_types); | |
| } | |
| } | |
| handleProviderStats(data) { | |
| if (window.dashboardApp && window.dashboardApp.updateProviderStats) { | |
| window.dashboardApp.updateProviderStats(data); | |
| } | |
| } | |
| handleMarketUpdate(data) { | |
| if (window.dashboardApp && window.dashboardApp.updateMarketData) { | |
| window.dashboardApp.updateMarketData(data); | |
| } | |
| } | |
| handlePriceUpdate(data) { | |
| if (window.dashboardApp && window.dashboardApp.updatePrice) { | |
| window.dashboardApp.updatePrice(data.symbol, data.price, data.change_24h); | |
| } | |
| } | |
| handleAlert(data) { | |
| this.showAlert(data.message, data.severity); | |
| } | |
| // ===== UI Updates ===== | |
| updateConnectionStatus(connected) { | |
| const statusBar = document.querySelector('.connection-status-bar'); | |
| const statusDot = document.getElementById('ws-status-dot'); | |
| const statusText = document.getElementById('ws-status-text'); | |
| if (statusBar) { | |
| if (connected) { | |
| statusBar.classList.remove('disconnected'); | |
| } else { | |
| statusBar.classList.add('disconnected'); | |
| } | |
| } | |
| if (statusDot) { | |
| statusDot.className = connected ? 'status-dot status-online' : 'status-dot status-offline'; | |
| } | |
| if (statusText) { | |
| statusText.textContent = connected ? 'Connected' : 'Disconnected'; | |
| } | |
| } | |
| updateOnlineUsers(active, total) { | |
| const activeEl = document.getElementById('active-users-count'); | |
| const totalEl = document.getElementById('total-sessions-count'); | |
| if (activeEl) { | |
| activeEl.textContent = active; | |
| activeEl.classList.add('count-updated'); | |
| setTimeout(() => activeEl.classList.remove('count-updated'), 500); | |
| } | |
| if (totalEl) { | |
| totalEl.textContent = total; | |
| } | |
| } | |
| updateClientTypes(types) { | |
| // Delegated to dashboard app if needed | |
| if (window.dashboardApp && window.dashboardApp.updateClientTypes) { | |
| window.dashboardApp.updateClientTypes(types); | |
| } | |
| } | |
| showAlert(message, severity = 'info') { | |
| const alertContainer = document.getElementById('alerts-container') || document.body; | |
| const alert = document.createElement('div'); | |
| alert.className = `alert alert-${severity}`; | |
| alert.innerHTML = ` | |
| <strong>${severity === 'error' ? '❌' : severity === 'warning' ? '⚠️' : 'ℹ️'}</strong> | |
| ${message} | |
| `; | |
| alertContainer.appendChild(alert); | |
| // Auto-remove after 5 seconds | |
| setTimeout(() => { | |
| alert.remove(); | |
| }, 5000); | |
| } | |
| showReconnectButton() { | |
| const statusBar = document.querySelector('.connection-status-bar'); | |
| if (statusBar && !document.getElementById('ws-reconnect-btn')) { | |
| const button = document.createElement('button'); | |
| button.id = 'ws-reconnect-btn'; | |
| button.className = 'btn btn-sm btn-secondary'; | |
| button.textContent = '🔄 Reconnect'; | |
| button.onclick = () => { | |
| this.reconnectAttempts = 0; | |
| this.connect(); | |
| button.remove(); | |
| }; | |
| statusBar.appendChild(button); | |
| } | |
| } | |
| /** | |
| * Cleanup method to be called when app is destroyed | |
| */ | |
| destroy() { | |
| console.log('[WebSocket] Destroying client'); | |
| this.disconnect(); | |
| this.messageHandlers.clear(); | |
| this.connectionCallbacks = []; | |
| } | |
| } | |
| // Create global instance | |
| window.wsClient = null; | |
| // Auto-initialize on DOMContentLoaded | |
| document.addEventListener('DOMContentLoaded', () => { | |
| try { | |
| window.wsClient = new CryptoWebSocketClient(); | |
| console.log('[WebSocket] Client initialized'); | |
| } catch (error) { | |
| console.error('[WebSocket] Initialization error:', error); | |
| } | |
| }); | |
| // Cleanup on page unload | |
| window.addEventListener('beforeunload', () => { | |
| if (window.wsClient) { | |
| window.wsClient.destroy(); | |
| } | |
| }); | |
| console.log('[WebSocket] Module loaded'); | |