/** * ═══════════════════════════════════════════════════════════════════ * 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 = `
Loading...
`; } }, showError(element, message) { if (element) { element.innerHTML = `
${message}
`; } }, 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 = 'No data available'; return; } tbody.innerHTML = coins.slice(0, CONFIG.MAX_COINS_DISPLAY || 20).map((coin, index) => ` ${coin.rank || index + 1}
${coin.symbol || coin.name} ${coin.name || ''}
${Utils.formatCurrency(coin.price || coin.current_price)} ${Utils.formatPercent(coin.change_24h || coin.price_change_percentage_24h)} ${Utils.formatCurrency(coin.volume_24h || coin.total_volume)} ${Utils.formatCurrency(coin.market_cap)} `).join(''); } renderCoinsGrid(coins) { const coinsGrid = document.getElementById('coins-grid-compact'); if (!coinsGrid) return; if (!coins || coins.length === 0) { coinsGrid.innerHTML = '

No data available

'; 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 `
${icon}
${symbol}
${Utils.formatCurrency(coin.price || coin.current_price)}
${change >= 0 ? ` ` : ` `} ${Utils.formatPercent(change)}
`; }).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 = '

No news available

'; return; } newsGrid.innerHTML = news.map(item => `
${item.image ? `${item.title}` : ''}

${item.title}

${Utils.formatDate(item.published_at || item.published_on)} ${item.source || 'Unknown'}

${item.description || item.body || item.summary || ''}

${item.url ? `Read More` : ''}
`).join(''); } renderProviders(providers) { const providersGrid = document.getElementById('providers-grid'); if (!providersGrid) return; if (!providers || providers.length === 0) { providersGrid.innerHTML = '

No providers available

'; return; } providersGrid.innerHTML = providers.map(provider => `

${provider.name || provider.provider_id}

${provider.status || 'Unknown'}

Category: ${provider.category || 'N/A'}

${provider.latency_ms ? `

Latency: ${provider.latency_ms}ms

` : ''}
`).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 = '
Analyzing...'; try { const data = await this.api.getSentiment(); aiResultsContent.innerHTML = `

Sentiment Analysis Results

Bullish
${data.bullish || 0}%
Neutral
${data.neutral || 0}%
Bearish
${data.bearish || 0}%

${data.summary || 'Market sentiment analysis based on aggregated data from multiple sources'}

`; } catch (error) { aiResultsContent.innerHTML = `
Error in analysis: ${error.message}
`; } } 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 = '
Summarizing...'; setTimeout(() => { aiResultsContent.innerHTML = `

News Summary

News summarization feature will be available soon.

This feature uses Hugging Face models for text summarization.

`; }, 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 = '
Predicting...'; setTimeout(() => { aiResultsContent.innerHTML = `

Price Prediction

Price prediction feature will be available soon.

This feature uses machine learning models to predict price trends.

`; }, 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 = '
Detecting patterns...'; setTimeout(() => { aiResultsContent.innerHTML = `

Pattern Detection

Pattern detection feature will be available soon.

This feature detects candlestick patterns and technical analysis indicators.

`; }, 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 };