Spaces:
Sleeping
Sleeping
| /** | |
| * βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| * HTS CRYPTO DASHBOARD - UNIFIED APPLICATION | |
| * Complete JavaScript Logic with WebSocket & API Integration | |
| * Integrated with Backend: aggregator.py, websocket_service.py, hf_client.py | |
| * βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| */ | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // CONFIGURATION | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const CONFIG = window.DASHBOARD_CONFIG || { | |
| BACKEND_URL: window.location.origin || 'https://really-amin-datasourceforcryptocurrency.hf.space', | |
| WS_URL: (window.location.origin || 'https://really-amin-datasourceforcryptocurrency.hf.space').replace('http://', 'ws://').replace('https://', 'wss://') + '/ws', | |
| UPDATE_INTERVAL: 30000, | |
| CACHE_TTL: 60000, | |
| ENDPOINTS: {}, | |
| WS_EVENTS: {}, | |
| }; | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // WEBSOCKET CLIENT (Enhanced with Backend Integration) | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class WebSocketClient { | |
| constructor(url) { | |
| this.url = url; | |
| this.socket = null; | |
| this.status = 'disconnected'; | |
| this.reconnectAttempts = 0; | |
| this.maxReconnectAttempts = CONFIG.MAX_RECONNECT_ATTEMPTS || 5; | |
| this.reconnectDelay = CONFIG.RECONNECT_DELAY || 3000; | |
| this.listeners = new Map(); | |
| this.heartbeatInterval = null; | |
| this.clientId = `client_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; | |
| this.subscriptions = new Set(); | |
| } | |
| connect() { | |
| if (this.socket && this.socket.readyState === WebSocket.OPEN) { | |
| console.log('[WS] Already connected'); | |
| return; | |
| } | |
| try { | |
| console.log('[WS] Connecting to:', this.url); | |
| this.socket = new WebSocket(this.url); | |
| this.socket.onopen = this.handleOpen.bind(this); | |
| this.socket.onmessage = this.handleMessage.bind(this); | |
| this.socket.onerror = this.handleError.bind(this); | |
| this.socket.onclose = this.handleClose.bind(this); | |
| this.updateStatus('connecting'); | |
| } catch (error) { | |
| console.error('[WS] Connection error:', error); | |
| this.scheduleReconnect(); | |
| } | |
| } | |
| handleOpen() { | |
| console.log('[WS] Connected successfully'); | |
| this.status = 'connected'; | |
| this.reconnectAttempts = 0; | |
| this.updateStatus('connected'); | |
| this.startHeartbeat(); | |
| // Send client identification | |
| this.send({ | |
| type: 'identify', | |
| client_id: this.clientId, | |
| metadata: { | |
| user_agent: navigator.userAgent, | |
| timestamp: new Date().toISOString() | |
| } | |
| }); | |
| // Subscribe to default services | |
| this.subscribe('market_data'); | |
| this.subscribe('sentiment'); | |
| this.subscribe('news'); | |
| this.emit('connected', true); | |
| } | |
| handleMessage(event) { | |
| try { | |
| const data = JSON.parse(event.data); | |
| if (CONFIG.DEBUG?.SHOW_WS_MESSAGES) { | |
| console.log('[WS] Message received:', data.type, data); | |
| } | |
| // Handle different message types from backend | |
| switch (data.type) { | |
| case 'heartbeat': | |
| case 'ping': | |
| this.send({ type: 'pong' }); | |
| return; | |
| case 'welcome': | |
| if (data.session_id) { | |
| this.clientId = data.session_id; | |
| } | |
| break; | |
| case 'api_update': | |
| this.emit('api_update', data); | |
| this.emit('market_update', data); | |
| break; | |
| case 'status_update': | |
| this.emit('status_update', data); | |
| break; | |
| case 'schedule_update': | |
| this.emit('schedule_update', data); | |
| break; | |
| case 'subscribed': | |
| case 'unsubscribed': | |
| console.log(`[WS] ${data.type} to ${data.api_id || data.service}`); | |
| break; | |
| } | |
| // Emit generic event | |
| this.emit(data.type, data); | |
| this.emit('message', data); | |
| } catch (error) { | |
| console.error('[WS] Message parse error:', error); | |
| } | |
| } | |
| handleError(error) { | |
| // WebSocket error events don't provide detailed error info | |
| // Check socket state to provide better error context | |
| const socketState = this.socket ? this.socket.readyState : 'null'; | |
| const stateNames = { | |
| 0: 'CONNECTING', | |
| 1: 'OPEN', | |
| 2: 'CLOSING', | |
| 3: 'CLOSED' | |
| }; | |
| const stateName = stateNames[socketState] || `UNKNOWN(${socketState})`; | |
| // Only log error once to prevent spam | |
| if (!this._errorLogged) { | |
| console.error('[WS] Connection error:', { | |
| url: this.url, | |
| state: stateName, | |
| readyState: socketState, | |
| message: 'WebSocket connection failed. Check if server is running and URL is correct.' | |
| }); | |
| this._errorLogged = true; | |
| // Reset error flag after a delay to allow logging if error persists | |
| setTimeout(() => { | |
| this._errorLogged = false; | |
| }, 5000); | |
| } | |
| this.updateStatus('error'); | |
| // Attempt reconnection if not already scheduled | |
| if (this.socket && this.socket.readyState === WebSocket.CLOSED && | |
| this.reconnectAttempts < this.maxReconnectAttempts) { | |
| this.scheduleReconnect(); | |
| } | |
| } | |
| handleClose() { | |
| console.log('[WS] Connection closed'); | |
| this.status = 'disconnected'; | |
| this.updateStatus('disconnected'); | |
| this.stopHeartbeat(); | |
| this.emit('connected', false); | |
| this.scheduleReconnect(); | |
| } | |
| scheduleReconnect() { | |
| if (this.reconnectAttempts >= this.maxReconnectAttempts) { | |
| console.error('[WS] Max reconnection attempts reached'); | |
| return; | |
| } | |
| this.reconnectAttempts++; | |
| console.log(`[WS] Reconnecting in ${this.reconnectDelay}ms (attempt ${this.reconnectAttempts})`); | |
| setTimeout(() => this.connect(), this.reconnectDelay); | |
| } | |
| startHeartbeat() { | |
| this.heartbeatInterval = setInterval(() => { | |
| if (this.isConnected()) { | |
| this.send({ type: 'ping' }); | |
| } | |
| }, CONFIG.HEARTBEAT_INTERVAL || 30000); | |
| } | |
| stopHeartbeat() { | |
| if (this.heartbeatInterval) { | |
| clearInterval(this.heartbeatInterval); | |
| this.heartbeatInterval = null; | |
| } | |
| } | |
| send(data) { | |
| if (this.isConnected()) { | |
| this.socket.send(JSON.stringify(data)); | |
| return true; | |
| } | |
| console.warn('[WS] Cannot send - not connected'); | |
| return false; | |
| } | |
| subscribe(service) { | |
| if (!this.subscriptions.has(service)) { | |
| this.subscriptions.add(service); | |
| this.send({ | |
| type: 'subscribe', | |
| service: service, | |
| api_id: service | |
| }); | |
| } | |
| } | |
| unsubscribe(service) { | |
| if (this.subscriptions.has(service)) { | |
| this.subscriptions.delete(service); | |
| this.send({ | |
| type: 'unsubscribe', | |
| service: service, | |
| api_id: service | |
| }); | |
| } | |
| } | |
| on(event, callback) { | |
| if (!this.listeners.has(event)) { | |
| this.listeners.set(event, []); | |
| } | |
| this.listeners.get(event).push(callback); | |
| } | |
| emit(event, data) { | |
| if (this.listeners.has(event)) { | |
| this.listeners.get(event).forEach(callback => callback(data)); | |
| } | |
| } | |
| updateStatus(status) { | |
| this.status = status; | |
| const statusBar = document.getElementById('connection-status-bar'); | |
| const statusDot = document.getElementById('ws-status-dot'); | |
| const statusText = document.getElementById('ws-status-text'); | |
| if (statusBar && statusDot && statusText) { | |
| if (status === 'connected') { | |
| statusBar.classList.remove('disconnected'); | |
| statusText.textContent = 'Connected'; | |
| } else if (status === 'disconnected' || status === 'error') { | |
| statusBar.classList.add('disconnected'); | |
| statusText.textContent = status === 'error' ? 'Connection Error' : 'Disconnected'; | |
| } else { | |
| statusText.textContent = 'Connecting...'; | |
| } | |
| } | |
| } | |
| isConnected() { | |
| return this.socket && this.socket.readyState === WebSocket.OPEN; | |
| } | |
| disconnect() { | |
| if (this.socket) { | |
| this.socket.close(); | |
| } | |
| this.stopHeartbeat(); | |
| } | |
| } | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // API CLIENT (Enhanced with All Backend Endpoints) | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class APIClient { | |
| constructor(baseURL) { | |
| this.baseURL = baseURL || CONFIG.BACKEND_URL; | |
| this.cache = new Map(); | |
| this.endpoints = CONFIG.ENDPOINTS || {}; | |
| } | |
| async request(endpoint, options = {}) { | |
| const url = `${this.baseURL}${endpoint}`; | |
| const cacheKey = `${options.method || 'GET'}:${url}`; | |
| // Check cache | |
| if (options.cache && this.cache.has(cacheKey)) { | |
| const cached = this.cache.get(cacheKey); | |
| if (Date.now() - cached.timestamp < CONFIG.CACHE_TTL) { | |
| if (CONFIG.DEBUG?.SHOW_API_REQUESTS) { | |
| console.log('[API] Cache hit:', endpoint); | |
| } | |
| return cached.data; | |
| } | |
| } | |
| try { | |
| if (CONFIG.DEBUG?.SHOW_API_REQUESTS) { | |
| console.log('[API] Request:', endpoint, options); | |
| } | |
| const response = await fetch(url, { | |
| method: options.method || 'GET', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| ...options.headers, | |
| }, | |
| body: options.body ? JSON.stringify(options.body) : undefined, | |
| }); | |
| if (!response.ok) { | |
| throw new Error(`HTTP ${response.status}: ${response.statusText}`); | |
| } | |
| const data = await response.json(); | |
| // Cache successful GET requests | |
| if (!options.method || options.method === 'GET') { | |
| this.cache.set(cacheKey, { | |
| data, | |
| timestamp: Date.now(), | |
| }); | |
| } | |
| return data; | |
| } catch (error) { | |
| console.error('[API] Error:', endpoint, error); | |
| throw error; | |
| } | |
| } | |
| // Health & Status | |
| async getHealth() { | |
| return this.request(this.endpoints.HEALTH || '/api/health', { cache: true }); | |
| } | |
| async getSystemStatus() { | |
| return this.request(this.endpoints.SYSTEM_STATUS || '/api/system/status', { cache: true }); | |
| } | |
| // Market Data (from aggregator.py) | |
| async getMarketStats() { | |
| return this.request(this.endpoints.MARKET || '/api/market/stats', { cache: true }); | |
| } | |
| async getMarketPrices(limit = 50) { | |
| return this.request(`${this.endpoints.MARKET_PRICES || '/api/market/prices'}?limit=${limit}`, { cache: true }); | |
| } | |
| async getTopCoins(limit = 20) { | |
| return this.request(`${this.endpoints.COINS_TOP || '/api/coins/top'}?limit=${limit}`, { cache: true }); | |
| } | |
| async getCoinDetails(symbol) { | |
| return this.request(`${this.endpoints.COIN_DETAILS || '/api/coins'}/${symbol}`, { cache: true }); | |
| } | |
| async getOHLCV(symbol, interval = '1h', limit = 100) { | |
| const endpoint = this.endpoints.OHLCV || '/api/ohlcv'; | |
| return this.request(`${endpoint}?symbol=${symbol}&interval=${interval}&limit=${limit}`, { cache: true }); | |
| } | |
| // Chart Data | |
| async getChartData(symbol, interval = '1h', limit = 100) { | |
| const endpoint = this.endpoints.CHART_HISTORY || '/api/charts/price'; | |
| return this.request(`${endpoint}/${symbol}?interval=${interval}&limit=${limit}`, { cache: true }); | |
| } | |
| async analyzeChart(symbol, interval = '1h') { | |
| return this.request(this.endpoints.CHART_ANALYZE || '/api/charts/analyze', { | |
| method: 'POST', | |
| body: { symbol, interval } | |
| }); | |
| } | |
| // Sentiment (from hf_client.py) | |
| async getSentiment() { | |
| return this.request(this.endpoints.SENTIMENT || '/api/sentiment', { cache: true }); | |
| } | |
| async analyzeSentiment(texts) { | |
| return this.request(this.endpoints.SENTIMENT_ANALYZE || '/api/sentiment/analyze', { | |
| method: 'POST', | |
| body: { texts } | |
| }); | |
| } | |
| // News (from aggregator.py) | |
| async getNews(limit = 20) { | |
| return this.request(`${this.endpoints.NEWS || '/api/news/latest'}?limit=${limit}`, { cache: true }); | |
| } | |
| async summarizeNews(articleUrl) { | |
| return this.request(this.endpoints.NEWS_SUMMARIZE || '/api/news/summarize', { | |
| method: 'POST', | |
| body: { url: articleUrl } | |
| }); | |
| } | |
| // Providers (from aggregator.py) | |
| async getProviders() { | |
| return this.request(this.endpoints.PROVIDERS || '/api/providers', { cache: true }); | |
| } | |
| async getProviderStatus() { | |
| return this.request(this.endpoints.PROVIDER_STATUS || '/api/providers/status', { cache: true }); | |
| } | |
| // HuggingFace (from hf_client.py) | |
| async getHFHealth() { | |
| return this.request(this.endpoints.HF_HEALTH || '/api/hf/health', { cache: true }); | |
| } | |
| async getHFRegistry() { | |
| return this.request(this.endpoints.HF_REGISTRY || '/api/hf/registry', { cache: true }); | |
| } | |
| async runSentimentAnalysis(texts, model = null) { | |
| return this.request(this.endpoints.HF_SENTIMENT || '/api/hf/run-sentiment', { | |
| method: 'POST', | |
| body: { texts, model } | |
| }); | |
| } | |
| // Datasets & Models | |
| async getDatasets() { | |
| return this.request(this.endpoints.DATASETS || '/api/datasets/list', { cache: true }); | |
| } | |
| async getModels() { | |
| return this.request(this.endpoints.MODELS || '/api/models/list', { cache: true }); | |
| } | |
| async testModel(modelName, input) { | |
| return this.request(this.endpoints.MODELS_TEST || '/api/models/test', { | |
| method: 'POST', | |
| body: { model: modelName, input } | |
| }); | |
| } | |
| // Query (NLP) | |
| async query(text) { | |
| return this.request(this.endpoints.QUERY || '/api/query', { | |
| method: 'POST', | |
| body: { query: text } | |
| }); | |
| } | |
| // System | |
| async getCategories() { | |
| return this.request(this.endpoints.CATEGORIES || '/api/categories', { cache: true }); | |
| } | |
| async getRateLimits() { | |
| return this.request(this.endpoints.RATE_LIMITS || '/api/rate-limits', { cache: true }); | |
| } | |
| async getLogs(logType = 'recent') { | |
| return this.request(`${this.endpoints.LOGS || '/api/logs'}/${logType}`, { cache: true }); | |
| } | |
| async getAlerts() { | |
| return this.request(this.endpoints.ALERTS || '/api/alerts', { cache: true }); | |
| } | |
| } | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // UTILITY FUNCTIONS | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const Utils = { | |
| formatCurrency(value) { | |
| if (value === null || value === undefined || isNaN(value)) { | |
| return 'β'; | |
| } | |
| const num = Number(value); | |
| if (Math.abs(num) >= 1e12) { | |
| return `$${(num / 1e12).toFixed(2)}T`; | |
| } | |
| if (Math.abs(num) >= 1e9) { | |
| return `$${(num / 1e9).toFixed(2)}B`; | |
| } | |
| if (Math.abs(num) >= 1e6) { | |
| return `$${(num / 1e6).toFixed(2)}M`; | |
| } | |
| if (Math.abs(num) >= 1e3) { | |
| return `$${(num / 1e3).toFixed(2)}K`; | |
| } | |
| return `$${num.toLocaleString(undefined, { | |
| minimumFractionDigits: 2, | |
| maximumFractionDigits: 2 | |
| })}`; | |
| }, | |
| formatPercent(value) { | |
| if (value === null || value === undefined || isNaN(value)) { | |
| return 'β'; | |
| } | |
| const num = Number(value); | |
| const sign = num >= 0 ? '+' : ''; | |
| return `${sign}${num.toFixed(2)}%`; | |
| }, | |
| formatNumber(value) { | |
| if (value === null || value === undefined || isNaN(value)) { | |
| return 'β'; | |
| } | |
| return Number(value).toLocaleString(); | |
| }, | |
| formatDate(timestamp) { | |
| if (!timestamp) return 'β'; | |
| const date = new Date(timestamp); | |
| const options = CONFIG.FORMATS?.DATE?.OPTIONS || { | |
| year: 'numeric', | |
| month: 'long', | |
| day: 'numeric', | |
| hour: '2-digit', | |
| minute: '2-digit', | |
| }; | |
| return date.toLocaleDateString(CONFIG.FORMATS?.DATE?.LOCALE || 'en-US', options); | |
| }, | |
| getChangeClass(value) { | |
| if (value > 0) return 'positive'; | |
| if (value < 0) return 'negative'; | |
| return 'neutral'; | |
| }, | |
| showLoader(element) { | |
| if (element) { | |
| element.innerHTML = ` | |
| <div class="loading-cell"> | |
| <div class="loader"></div> | |
| Loading... | |
| </div> | |
| `; | |
| } | |
| }, | |
| showError(element, message) { | |
| if (element) { | |
| element.innerHTML = ` | |
| <div class="error-message"> | |
| <i class="fas fa-exclamation-circle"></i> | |
| ${message} | |
| </div> | |
| `; | |
| } | |
| }, | |
| debounce(func, wait) { | |
| let timeout; | |
| return function executedFunction(...args) { | |
| const later = () => { | |
| clearTimeout(timeout); | |
| func(...args); | |
| }; | |
| clearTimeout(timeout); | |
| timeout = setTimeout(later, wait); | |
| }; | |
| }, | |
| }; | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // VIEW MANAGER | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class ViewManager { | |
| constructor() { | |
| this.currentView = 'overview'; | |
| this.views = new Map(); | |
| this.init(); | |
| } | |
| init() { | |
| // Desktop navigation | |
| document.querySelectorAll('.nav-tab-btn').forEach(btn => { | |
| btn.addEventListener('click', (e) => { | |
| const view = btn.dataset.view; | |
| this.switchView(view); | |
| }); | |
| }); | |
| // Mobile navigation | |
| document.querySelectorAll('.mobile-nav-tab-btn').forEach(btn => { | |
| btn.addEventListener('click', (e) => { | |
| const view = btn.dataset.view; | |
| this.switchView(view); | |
| }); | |
| }); | |
| } | |
| switchView(viewName) { | |
| if (this.currentView === viewName) return; | |
| // Hide all views | |
| document.querySelectorAll('.view-section').forEach(section => { | |
| section.classList.remove('active'); | |
| }); | |
| // Show selected view | |
| const viewSection = document.getElementById(`view-${viewName}`); | |
| if (viewSection) { | |
| viewSection.classList.add('active'); | |
| } | |
| // Update navigation buttons | |
| document.querySelectorAll('.nav-tab-btn, .mobile-nav-tab-btn').forEach(btn => { | |
| btn.classList.remove('active'); | |
| if (btn.dataset.view === viewName) { | |
| btn.classList.add('active'); | |
| } | |
| }); | |
| this.currentView = viewName; | |
| console.log('[View] Switched to:', viewName); | |
| // Trigger view-specific updates | |
| this.triggerViewUpdate(viewName); | |
| } | |
| triggerViewUpdate(viewName) { | |
| const event = new CustomEvent('viewChange', { detail: { view: viewName } }); | |
| document.dispatchEvent(event); | |
| } | |
| } | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // DASHBOARD APPLICATION (Enhanced with Full Backend Integration) | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class DashboardApp { | |
| constructor() { | |
| this.ws = new WebSocketClient(CONFIG.WS_URL); | |
| this.api = new APIClient(CONFIG.BACKEND_URL); | |
| this.viewManager = new ViewManager(); | |
| this.updateInterval = null; | |
| this.data = { | |
| market: null, | |
| sentiment: null, | |
| trending: null, | |
| news: [], | |
| providers: [], | |
| }; | |
| } | |
| async init() { | |
| console.log('[App] Initializing dashboard...'); | |
| // Connect WebSocket | |
| this.ws.connect(); | |
| this.setupWebSocketHandlers(); | |
| // Setup UI handlers | |
| this.setupUIHandlers(); | |
| // Load initial data | |
| await this.loadInitialData(); | |
| // Start periodic updates | |
| this.startPeriodicUpdates(); | |
| // Listen for view changes | |
| document.addEventListener('viewChange', (e) => { | |
| this.handleViewChange(e.detail.view); | |
| }); | |
| console.log('[App] Dashboard initialized successfully'); | |
| } | |
| setupWebSocketHandlers() { | |
| this.ws.on('connected', (isConnected) => { | |
| console.log('[App] WebSocket connection status:', isConnected); | |
| }); | |
| this.ws.on('api_update', (data) => { | |
| console.log('[App] API update received'); | |
| if (data.api_id === 'market_data' || data.service === 'market_data') { | |
| this.handleMarketUpdate(data); | |
| } | |
| }); | |
| this.ws.on('market_update', (data) => { | |
| console.log('[App] Market update received'); | |
| this.handleMarketUpdate(data); | |
| }); | |
| this.ws.on('sentiment_update', (data) => { | |
| console.log('[App] Sentiment update received'); | |
| this.handleSentimentUpdate(data); | |
| }); | |
| this.ws.on('status_update', (data) => { | |
| console.log('[App] Status update received'); | |
| if (data.status?.active_connections !== undefined) { | |
| this.updateOnlineUsers(data.status.active_connections); | |
| } | |
| }); | |
| } | |
| setupUIHandlers() { | |
| // Theme toggle | |
| const themeToggle = document.getElementById('theme-toggle'); | |
| if (themeToggle) { | |
| themeToggle.addEventListener('click', () => this.toggleTheme()); | |
| } | |
| // Notifications | |
| const notificationsBtn = document.getElementById('notifications-btn'); | |
| const notificationsPanel = document.getElementById('notifications-panel'); | |
| const closeNotifications = document.getElementById('close-notifications'); | |
| if (notificationsBtn && notificationsPanel) { | |
| notificationsBtn.addEventListener('click', () => { | |
| notificationsPanel.classList.toggle('active'); | |
| }); | |
| } | |
| if (closeNotifications && notificationsPanel) { | |
| closeNotifications.addEventListener('click', () => { | |
| notificationsPanel.classList.remove('active'); | |
| }); | |
| } | |
| // Settings | |
| const settingsBtn = document.getElementById('settings-btn'); | |
| const settingsModal = document.getElementById('settings-modal'); | |
| const closeSettings = document.getElementById('close-settings'); | |
| if (settingsBtn && settingsModal) { | |
| settingsBtn.addEventListener('click', () => { | |
| settingsModal.classList.add('active'); | |
| }); | |
| } | |
| if (closeSettings && settingsModal) { | |
| closeSettings.addEventListener('click', () => { | |
| settingsModal.classList.remove('active'); | |
| }); | |
| } | |
| // Refresh buttons | |
| const refreshCoins = document.getElementById('refresh-coins'); | |
| if (refreshCoins) { | |
| refreshCoins.addEventListener('click', () => this.loadMarketData()); | |
| } | |
| const refreshProviders = document.getElementById('refresh-providers'); | |
| if (refreshProviders) { | |
| refreshProviders.addEventListener('click', () => this.loadProviders()); | |
| } | |
| // Floating stats minimize | |
| const minimizeStats = document.getElementById('minimize-stats'); | |
| const floatingStats = document.getElementById('floating-stats'); | |
| if (minimizeStats && floatingStats) { | |
| minimizeStats.addEventListener('click', () => { | |
| floatingStats.classList.toggle('minimized'); | |
| }); | |
| } | |
| // Global search | |
| const globalSearch = document.getElementById('global-search'); | |
| if (globalSearch) { | |
| globalSearch.addEventListener('input', Utils.debounce((e) => { | |
| this.handleSearch(e.target.value); | |
| }, CONFIG.RATE_LIMITS?.SEARCH_DEBOUNCE_MS || 300)); | |
| } | |
| // AI Tools | |
| this.setupAIToolHandlers(); | |
| // Market filters | |
| const marketFilter = document.getElementById('market-filter'); | |
| if (marketFilter) { | |
| marketFilter.addEventListener('change', (e) => { | |
| this.filterMarket(e.target.value); | |
| }); | |
| } | |
| } | |
| setupAIToolHandlers() { | |
| const sentimentBtn = document.getElementById('sentiment-analysis-btn'); | |
| const summaryBtn = document.getElementById('news-summary-btn'); | |
| const predictionBtn = document.getElementById('price-prediction-btn'); | |
| const patternBtn = document.getElementById('pattern-detection-btn'); | |
| if (sentimentBtn) { | |
| sentimentBtn.addEventListener('click', () => this.runSentimentAnalysis()); | |
| } | |
| if (summaryBtn) { | |
| summaryBtn.addEventListener('click', () => this.runNewsSummary()); | |
| } | |
| if (predictionBtn) { | |
| predictionBtn.addEventListener('click', () => this.runPricePrediction()); | |
| } | |
| if (patternBtn) { | |
| patternBtn.addEventListener('click', () => this.runPatternDetection()); | |
| } | |
| const clearResults = document.getElementById('clear-results'); | |
| const aiResults = document.getElementById('ai-results'); | |
| if (clearResults && aiResults) { | |
| clearResults.addEventListener('click', () => { | |
| aiResults.style.display = 'none'; | |
| }); | |
| } | |
| } | |
| async loadInitialData() { | |
| this.showLoadingOverlay(true); | |
| try { | |
| await Promise.all([ | |
| this.loadMarketData(), | |
| this.loadSentimentData(), | |
| this.loadNewsData(), | |
| ]); | |
| } catch (error) { | |
| console.error('[App] Error loading initial data:', error); | |
| } | |
| this.showLoadingOverlay(false); | |
| } | |
| async loadMarketData() { | |
| try { | |
| const [stats, coins] = await Promise.all([ | |
| this.api.getMarketStats(), | |
| this.api.getTopCoins(CONFIG.MAX_COINS_DISPLAY || 20) | |
| ]); | |
| this.data.market = { stats, coins }; | |
| const coinsList = coins?.coins || coins || []; | |
| this.renderMarketStats(stats?.stats || stats); | |
| this.renderCoinsTable(coinsList); | |
| this.renderCoinsGrid(coinsList); | |
| } catch (error) { | |
| console.error('[App] Error loading market data:', error); | |
| Utils.showError(document.getElementById('coins-table-body'), 'Failed to load market data'); | |
| } | |
| } | |
| async loadSentimentData() { | |
| try { | |
| const data = await this.api.getSentiment(); | |
| this.data.sentiment = data; | |
| this.renderSentiment(data); | |
| } catch (error) { | |
| console.error('[App] Error loading sentiment data:', error); | |
| } | |
| } | |
| async loadNewsData() { | |
| try { | |
| const data = await this.api.getNews(CONFIG.MAX_NEWS_DISPLAY || 20); | |
| this.data.news = data.news || data || []; | |
| this.renderNews(this.data.news); | |
| } catch (error) { | |
| console.error('[App] Error loading news data:', error); | |
| } | |
| } | |
| async loadProviders() { | |
| try { | |
| const providers = await this.api.getProviders(); | |
| this.data.providers = providers.providers || providers || []; | |
| this.renderProviders(this.data.providers); | |
| } catch (error) { | |
| console.error('[App] Error loading providers:', error); | |
| } | |
| } | |
| renderMarketStats(data) { | |
| // Main metrics (3 main cards) | |
| const totalMarketCap = document.getElementById('total-market-cap'); | |
| const volume24h = document.getElementById('volume-24h'); | |
| const marketTrend = document.getElementById('market-trend'); | |
| const activeCryptos = document.getElementById('active-cryptocurrencies'); | |
| const marketsCount = document.getElementById('markets-count'); | |
| const fearGreed = document.getElementById('fear-greed-index'); | |
| const marketCapChange24h = document.getElementById('market-cap-change-24h'); | |
| const top10Share = document.getElementById('top10-share'); | |
| const btcPrice = document.getElementById('btc-price'); | |
| const ethPrice = document.getElementById('eth-price'); | |
| if (totalMarketCap && data?.total_market_cap) { | |
| totalMarketCap.textContent = Utils.formatCurrency(data.total_market_cap); | |
| const marketCapChange = document.getElementById('market-cap-change'); | |
| if (marketCapChange && data.market_cap_change_percentage_24h !== undefined) { | |
| const changeEl = marketCapChange.querySelector('span'); | |
| if (changeEl) { | |
| changeEl.textContent = Utils.formatPercent(data.market_cap_change_percentage_24h); | |
| } | |
| } | |
| } | |
| if (volume24h && data?.total_volume_24h) { | |
| volume24h.textContent = Utils.formatCurrency(data.total_volume_24h); | |
| const volumeChange = document.getElementById('volume-change'); | |
| if (volumeChange) { | |
| // Volume change would need to be calculated or provided | |
| } | |
| } | |
| if (marketTrend && data?.market_cap_change_percentage_24h !== undefined) { | |
| const change = data.market_cap_change_percentage_24h; | |
| marketTrend.textContent = change > 0 ? 'Bullish' : change < 0 ? 'Bearish' : 'Neutral'; | |
| const trendChangeEl = document.getElementById('trend-change'); | |
| if (trendChangeEl) { | |
| const changeSpan = trendChangeEl.querySelector('span'); | |
| if (changeSpan) { | |
| changeSpan.textContent = Utils.formatPercent(change); | |
| } | |
| } | |
| } | |
| // Additional metrics (if elements exist) | |
| const activeCryptos = document.getElementById('active-cryptocurrencies'); | |
| const marketsCount = document.getElementById('markets-count'); | |
| const fearGreed = document.getElementById('fear-greed-index'); | |
| const marketCapChange24h = document.getElementById('market-cap-change-24h'); | |
| const top10Share = document.getElementById('top10-share'); | |
| const btcPrice = document.getElementById('btc-price'); | |
| const ethPrice = document.getElementById('eth-price'); | |
| const btcDominance = document.getElementById('btc-dominance'); | |
| const ethDominance = document.getElementById('eth-dominance'); | |
| if (activeCryptos && data?.active_cryptocurrencies) { | |
| activeCryptos.textContent = Utils.formatNumber(data.active_cryptocurrencies); | |
| } | |
| if (marketsCount && data?.markets) { | |
| marketsCount.textContent = Utils.formatNumber(data.markets); | |
| } | |
| if (fearGreed && data?.fear_greed_index !== undefined) { | |
| fearGreed.textContent = data.fear_greed_index || 'N/A'; | |
| const fearGreedChange = document.getElementById('fear-greed-change'); | |
| if (fearGreedChange) { | |
| const index = data.fear_greed_index || 50; | |
| if (index >= 75) fearGreedChange.textContent = 'Extreme Greed'; | |
| else if (index >= 55) fearGreedChange.textContent = 'Greed'; | |
| else if (index >= 45) fearGreedChange.textContent = 'Neutral'; | |
| else if (index >= 25) fearGreedChange.textContent = 'Fear'; | |
| else fearGreedChange.textContent = 'Extreme Fear'; | |
| } | |
| } | |
| if (btcDominance && data?.btc_dominance) { | |
| document.getElementById('btc-dominance').textContent = `${data.btc_dominance.toFixed(1)}%`; | |
| } | |
| if (ethDominance && data?.eth_dominance) { | |
| ethDominance.textContent = `${data.eth_dominance.toFixed(1)}%`; | |
| } | |
| } | |
| renderCoinsTable(coins) { | |
| const tbody = document.getElementById('coins-table-body'); | |
| if (!tbody) return; | |
| if (!coins || coins.length === 0) { | |
| tbody.innerHTML = '<tr><td colspan="7">No data available</td></tr>'; | |
| return; | |
| } | |
| tbody.innerHTML = coins.slice(0, CONFIG.MAX_COINS_DISPLAY || 20).map((coin, index) => ` | |
| <tr> | |
| <td>${coin.rank || index + 1}</td> | |
| <td> | |
| <div style="display: flex; align-items: center; gap: 8px;"> | |
| <strong>${coin.symbol || coin.name}</strong> | |
| <span style="color: var(--text-muted); font-size: 0.875rem;">${coin.name || ''}</span> | |
| </div> | |
| </td> | |
| <td style="font-family: var(--font-mono);">${Utils.formatCurrency(coin.price || coin.current_price)}</td> | |
| <td> | |
| <span class="stat-change ${Utils.getChangeClass(coin.change_24h || coin.price_change_percentage_24h)}"> | |
| ${Utils.formatPercent(coin.change_24h || coin.price_change_percentage_24h)} | |
| </span> | |
| </td> | |
| <td>${Utils.formatCurrency(coin.volume_24h || coin.total_volume)}</td> | |
| <td>${Utils.formatCurrency(coin.market_cap)}</td> | |
| <td> | |
| <button class="btn-ghost" onclick="app.viewCoinDetails('${coin.symbol || coin.name}')"> | |
| <i class="fas fa-chart-line"></i> | |
| </button> | |
| </td> | |
| </tr> | |
| `).join(''); | |
| } | |
| renderCoinsGrid(coins) { | |
| const coinsGrid = document.getElementById('coins-grid-compact'); | |
| if (!coinsGrid) return; | |
| if (!coins || coins.length === 0) { | |
| coinsGrid.innerHTML = '<div class="coin-card-compact"><p>No data available</p></div>'; | |
| return; | |
| } | |
| // Get top 12 coins | |
| const topCoins = coins.slice(0, 12); | |
| // Icon mapping for popular coins | |
| const coinIcons = { | |
| 'BTC': 'βΏ', | |
| 'ETH': 'Ξ', | |
| 'BNB': 'BNB', | |
| 'SOL': 'β', | |
| 'ADA': 'β³', | |
| 'XRP': 'β', | |
| 'DOT': 'β', | |
| 'DOGE': 'Γ', | |
| 'MATIC': 'β¬', | |
| 'AVAX': 'β²', | |
| 'LINK': '⬑', | |
| 'UNI': 'π¦' | |
| }; | |
| coinsGrid.innerHTML = topCoins.map((coin) => { | |
| const symbol = (coin.symbol || '').toUpperCase(); | |
| const change = coin.change_24h || coin.price_change_percentage_24h || 0; | |
| const changeClass = Utils.getChangeClass(change); | |
| const icon = coinIcons[symbol] || symbol.charAt(0); | |
| return ` | |
| <div class="coin-card-compact" onclick="app.viewCoinDetails('${symbol}')"> | |
| <div class="coin-icon-compact">${icon}</div> | |
| <div class="coin-symbol-compact">${symbol}</div> | |
| <div class="coin-price-compact">${Utils.formatCurrency(coin.price || coin.current_price)}</div> | |
| <div class="coin-change-compact ${changeClass}"> | |
| ${change >= 0 ? ` | |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"> | |
| <polyline points="18 15 12 9 6 15"></polyline> | |
| </svg> | |
| ` : ` | |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"> | |
| <polyline points="6 9 12 15 18 9"></polyline> | |
| </svg> | |
| `} | |
| <span>${Utils.formatPercent(change)}</span> | |
| </div> | |
| </div> | |
| `; | |
| }).join(''); | |
| } | |
| renderSentiment(data) { | |
| if (!data) return; | |
| const bullish = data.bullish || 0; | |
| const neutral = data.neutral || 0; | |
| const bearish = data.bearish || 0; | |
| const bullishPercent = document.getElementById('bullish-percent'); | |
| const neutralPercent = document.getElementById('neutral-percent'); | |
| const bearishPercent = document.getElementById('bearish-percent'); | |
| if (bullishPercent) bullishPercent.textContent = `${bullish}%`; | |
| if (neutralPercent) neutralPercent.textContent = `${neutral}%`; | |
| if (bearishPercent) bearishPercent.textContent = `${bearish}%`; | |
| // Update progress bars | |
| const progressBars = document.querySelectorAll('.sentiment-progress-bar'); | |
| progressBars.forEach(bar => { | |
| if (bar.classList.contains('bullish')) { | |
| bar.style.width = `${bullish}%`; | |
| } else if (bar.classList.contains('neutral')) { | |
| bar.style.width = `${neutral}%`; | |
| } else if (bar.classList.contains('bearish')) { | |
| bar.style.width = `${bearish}%`; | |
| } | |
| }); | |
| } | |
| renderNews(news) { | |
| const newsGrid = document.getElementById('news-grid'); | |
| if (!newsGrid) return; | |
| if (!news || news.length === 0) { | |
| newsGrid.innerHTML = '<p>No news available</p>'; | |
| return; | |
| } | |
| newsGrid.innerHTML = news.map(item => ` | |
| <div class="news-card"> | |
| ${item.image ? `<img src="${item.image}" alt="${item.title}" class="news-card-image">` : ''} | |
| <div class="news-card-content"> | |
| <h3 class="news-card-title">${item.title}</h3> | |
| <div class="news-card-meta"> | |
| <span><i class="fas fa-clock"></i> ${Utils.formatDate(item.published_at || item.published_on)}</span> | |
| <span><i class="fas fa-newspaper"></i> ${item.source || 'Unknown'}</span> | |
| </div> | |
| <p class="news-card-excerpt">${item.description || item.body || item.summary || ''}</p> | |
| ${item.url ? `<a href="${item.url}" target="_blank" class="btn-ghost">Read More</a>` : ''} | |
| </div> | |
| </div> | |
| `).join(''); | |
| } | |
| renderProviders(providers) { | |
| const providersGrid = document.getElementById('providers-grid'); | |
| if (!providersGrid) return; | |
| if (!providers || providers.length === 0) { | |
| providersGrid.innerHTML = '<p>No providers available</p>'; | |
| return; | |
| } | |
| providersGrid.innerHTML = providers.map(provider => ` | |
| <div class="provider-card"> | |
| <div class="provider-header"> | |
| <h3>${provider.name || provider.provider_id}</h3> | |
| <span class="status-badge ${provider.status || 'unknown'}">${provider.status || 'Unknown'}</span> | |
| </div> | |
| <div class="provider-info"> | |
| <p><strong>Category:</strong> ${provider.category || 'N/A'}</p> | |
| ${provider.latency_ms ? `<p><strong>Latency:</strong> ${provider.latency_ms}ms</p>` : ''} | |
| </div> | |
| </div> | |
| `).join(''); | |
| } | |
| handleMarketUpdate(data) { | |
| if (data.data) { | |
| this.renderMarketStats(data.data); | |
| if (data.data.cryptocurrencies || data.data.coins) { | |
| this.renderCoinsTable(data.data.cryptocurrencies || data.data.coins); | |
| } | |
| } | |
| } | |
| handleSentimentUpdate(data) { | |
| if (data.data) { | |
| this.renderSentiment(data.data); | |
| } | |
| } | |
| updateOnlineUsers(count) { | |
| const activeUsersCount = document.getElementById('active-users-count'); | |
| if (activeUsersCount) { | |
| activeUsersCount.textContent = count; | |
| } | |
| } | |
| handleViewChange(view) { | |
| console.log('[App] View changed to:', view); | |
| // Load data for specific views | |
| switch (view) { | |
| case 'providers': | |
| this.loadProviders(); | |
| break; | |
| case 'news': | |
| this.loadNewsData(); | |
| break; | |
| case 'market': | |
| this.loadMarketData(); | |
| break; | |
| } | |
| } | |
| startPeriodicUpdates() { | |
| this.updateInterval = setInterval(() => { | |
| if (CONFIG.DEBUG?.ENABLE_CONSOLE_LOGS) { | |
| console.log('[App] Periodic update triggered'); | |
| } | |
| this.loadMarketData(); | |
| this.loadSentimentData(); | |
| }, CONFIG.UPDATE_INTERVAL || 30000); | |
| } | |
| stopPeriodicUpdates() { | |
| if (this.updateInterval) { | |
| clearInterval(this.updateInterval); | |
| this.updateInterval = null; | |
| } | |
| } | |
| toggleTheme() { | |
| document.body.classList.toggle('light-theme'); | |
| const icon = document.querySelector('#theme-toggle i'); | |
| if (icon) { | |
| icon.classList.toggle('fa-moon'); | |
| icon.classList.toggle('fa-sun'); | |
| } | |
| } | |
| handleSearch(query) { | |
| console.log('[App] Search query:', query); | |
| // Implement search functionality | |
| } | |
| filterMarket(filter) { | |
| console.log('[App] Filter market:', filter); | |
| // Implement filter functionality | |
| } | |
| viewCoinDetails(symbol) { | |
| console.log('[App] View coin details:', symbol); | |
| // Switch to charts view and load coin data | |
| this.viewManager.switchView('charts'); | |
| } | |
| showLoadingOverlay(show) { | |
| const overlay = document.getElementById('loading-overlay'); | |
| if (overlay) { | |
| if (show) { | |
| overlay.classList.add('active'); | |
| } else { | |
| overlay.classList.remove('active'); | |
| } | |
| } | |
| } | |
| // AI Tool Methods | |
| async runSentimentAnalysis() { | |
| const aiResults = document.getElementById('ai-results'); | |
| const aiResultsContent = document.getElementById('ai-results-content'); | |
| if (!aiResults || !aiResultsContent) return; | |
| aiResults.style.display = 'block'; | |
| aiResultsContent.innerHTML = '<div class="loader"></div> Analyzing...'; | |
| try { | |
| const data = await this.api.getSentiment(); | |
| aiResultsContent.innerHTML = ` | |
| <div class="ai-result-card"> | |
| <h4>Sentiment Analysis Results</h4> | |
| <div class="sentiment-summary"> | |
| <div class="sentiment-summary-item"> | |
| <div class="summary-label">Bullish</div> | |
| <div class="summary-value bullish">${data.bullish || 0}%</div> | |
| </div> | |
| <div class="sentiment-summary-item"> | |
| <div class="summary-label">Neutral</div> | |
| <div class="summary-value neutral">${data.neutral || 0}%</div> | |
| </div> | |
| <div class="sentiment-summary-item"> | |
| <div class="summary-label">Bearish</div> | |
| <div class="summary-value bearish">${data.bearish || 0}%</div> | |
| </div> | |
| </div> | |
| <p style="margin-top: 1rem; color: var(--text-muted);"> | |
| ${data.summary || 'Market sentiment analysis based on aggregated data from multiple sources'} | |
| </p> | |
| </div> | |
| `; | |
| } catch (error) { | |
| aiResultsContent.innerHTML = ` | |
| <div class="error-message"> | |
| <i class="fas fa-exclamation-circle"></i> | |
| Error in analysis: ${error.message} | |
| </div> | |
| `; | |
| } | |
| } | |
| async runNewsSummary() { | |
| const aiResults = document.getElementById('ai-results'); | |
| const aiResultsContent = document.getElementById('ai-results-content'); | |
| if (!aiResults || !aiResultsContent) return; | |
| aiResults.style.display = 'block'; | |
| aiResultsContent.innerHTML = '<div class="loader"></div> Summarizing...'; | |
| setTimeout(() => { | |
| aiResultsContent.innerHTML = ` | |
| <div class="ai-result-card"> | |
| <h4>News Summary</h4> | |
| <p>News summarization feature will be available soon.</p> | |
| <p style="color: var(--text-muted); font-size: 0.875rem;"> | |
| This feature uses Hugging Face models for text summarization. | |
| </p> | |
| </div> | |
| `; | |
| }, 1000); | |
| } | |
| async runPricePrediction() { | |
| const aiResults = document.getElementById('ai-results'); | |
| const aiResultsContent = document.getElementById('ai-results-content'); | |
| if (!aiResults || !aiResultsContent) return; | |
| aiResults.style.display = 'block'; | |
| aiResultsContent.innerHTML = '<div class="loader"></div> Predicting...'; | |
| setTimeout(() => { | |
| aiResultsContent.innerHTML = ` | |
| <div class="ai-result-card"> | |
| <h4>Price Prediction</h4> | |
| <p>Price prediction feature will be available soon.</p> | |
| <p style="color: var(--text-muted); font-size: 0.875rem;"> | |
| This feature uses machine learning models to predict price trends. | |
| </p> | |
| </div> | |
| `; | |
| }, 1000); | |
| } | |
| async runPatternDetection() { | |
| const aiResults = document.getElementById('ai-results'); | |
| const aiResultsContent = document.getElementById('ai-results-content'); | |
| if (!aiResults || !aiResultsContent) return; | |
| aiResults.style.display = 'block'; | |
| aiResultsContent.innerHTML = '<div class="loader"></div> Detecting patterns...'; | |
| setTimeout(() => { | |
| aiResultsContent.innerHTML = ` | |
| <div class="ai-result-card"> | |
| <h4>Pattern Detection</h4> | |
| <p>Pattern detection feature will be available soon.</p> | |
| <p style="color: var(--text-muted); font-size: 0.875rem;"> | |
| This feature detects candlestick patterns and technical analysis indicators. | |
| </p> | |
| </div> | |
| `; | |
| }, 1000); | |
| } | |
| destroy() { | |
| this.stopPeriodicUpdates(); | |
| this.ws.disconnect(); | |
| console.log('[App] Dashboard destroyed'); | |
| } | |
| } | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // INITIALIZATION | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| let app; | |
| document.addEventListener('DOMContentLoaded', () => { | |
| console.log('[Main] DOM loaded, initializing application...'); | |
| app = new DashboardApp(); | |
| app.init(); | |
| // Make app globally accessible for debugging | |
| window.app = app; | |
| console.log('[Main] Application ready'); | |
| }); | |
| // Cleanup on page unload | |
| window.addEventListener('beforeunload', () => { | |
| if (app) { | |
| app.destroy(); | |
| } | |
| }); | |
| // Handle visibility change to pause/resume updates | |
| document.addEventListener('visibilitychange', () => { | |
| if (document.hidden) { | |
| console.log('[Main] Page hidden, pausing updates'); | |
| if (app) app.stopPeriodicUpdates(); | |
| } else { | |
| console.log('[Main] Page visible, resuming updates'); | |
| if (app) { | |
| app.startPeriodicUpdates(); | |
| app.loadMarketData(); | |
| } | |
| } | |
| }); | |
| // Export for module usage | |
| export { DashboardApp, APIClient, WebSocketClient, Utils }; | |