diff --git "a/app.js" "b/app.js" --- "a/app.js" +++ "b/app.js" @@ -1,1395 +1,1395 @@ -/** - * ═══════════════════════════════════════════════════════════════════ - * 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 }; +/** + * ═══════════════════════════════════════════════════════════════════ + * 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 };