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 = `
-
- `;
- }
- },
-
- 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 = '';
- 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}
-
- ${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 => `
-
-
-
-
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 = `
+
+ `;
+ }
+ },
+
+ 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 = '';
+ 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}
+
+ ${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 => `
+
+
+
+
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 };