/** * ═══════════════════════════════════════════════════════════════════ * HTS CRYPTO DASHBOARD - UNIFIED APPLICATION * Complete JavaScript Logic with WebSocket & API Integration * ═══════════════════════════════════════════════════════════════════ */ // ═══════════════════════════════════════════════════════════════════ // CONFIGURATION // ═══════════════════════════════════════════════════════════════════ // Auto-detect environment and set backend URLs // Use relative URLs to avoid CORS issues - always use same origin const getBackendURL = () => { // Always use current origin to avoid CORS issues return window.location.origin; }; const getWebSocketURL = () => { // Use current origin for WebSocket to avoid CORS issues const protocol = window.location.protocol === "https:" ? "wss" : "ws"; const host = window.location.host; return `${protocol}://${host}/ws`; }; // Merge DASHBOARD_CONFIG if exists, but always use localhost detection for URLs const baseConfig = window.DASHBOARD_CONFIG || {}; const backendURL = getBackendURL(); const wsURL = getWebSocketURL(); const CONFIG = { ...baseConfig, // Always override URLs with localhost detection BACKEND_URL: backendURL, WS_URL: wsURL, UPDATE_INTERVAL: baseConfig.UPDATE_INTERVAL || 30000, // 30 seconds CACHE_TTL: baseConfig.CACHE_TTL || 60000, // 1 minute }; // Always use current origin to avoid CORS issues CONFIG.BACKEND_URL = window.location.origin; const wsProtocol = window.location.protocol === "https:" ? "wss" : "ws"; CONFIG.WS_URL = `${wsProtocol}://${window.location.host}/ws`; // Log configuration for debugging console.log('[Config] Backend URL:', CONFIG.BACKEND_URL); console.log('[Config] WebSocket URL:', CONFIG.WS_URL); console.log('[Config] Current hostname:', window.location.hostname); // ═══════════════════════════════════════════════════════════════════ // WEBSOCKET CLIENT // ═══════════════════════════════════════════════════════════════════ class WebSocketClient { constructor(url) { this.url = url; this.socket = null; this.status = 'disconnected'; this.reconnectAttempts = 0; this.maxReconnectAttempts = 5; this.reconnectDelay = 3000; this.listeners = new Map(); this.heartbeatInterval = null; } 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(); this.emit('connected', true); } handleMessage(event) { try { const data = JSON.parse(event.data); console.log('[WS] Message received:', data.type); if (data.type === 'heartbeat') { this.send({ type: 'pong' }); return; } 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(); // Clean up socket reference if (this.socket) { try { // Remove event listeners to prevent memory leaks this.socket.onopen = null; this.socket.onclose = null; this.socket.onerror = null; this.socket.onmessage = null; } catch (e) { // Ignore errors during cleanup } // Don't nullify socket immediately - let it close naturally // this.socket = null; // Set to null after a short delay } 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() { // Clear any existing heartbeat if (this.heartbeatInterval) { clearInterval(this.heartbeatInterval); } this.heartbeatInterval = setInterval(() => { // Double-check connection state before sending heartbeat if (this.socket && this.socket.readyState === WebSocket.OPEN) { const sent = this.send({ type: 'ping' }); if (!sent) { // If send failed, stop heartbeat and try to reconnect this.stopHeartbeat(); if (this.reconnectAttempts < this.maxReconnectAttempts) { this.scheduleReconnect(); } } } else { // Connection is not open, stop heartbeat this.stopHeartbeat(); } }, 30000); } stopHeartbeat() { if (this.heartbeatInterval) { clearInterval(this.heartbeatInterval); this.heartbeatInterval = null; } } send(data) { if (!this.socket) { console.warn('[WS] Cannot send - socket is null'); return false; } // Check if socket is in a valid state for sending if (this.socket.readyState === WebSocket.OPEN) { try { this.socket.send(JSON.stringify(data)); return true; } catch (error) { console.error('[WS] Error sending message:', error); // Mark as disconnected if send fails if (error.message && (error.message.includes('close') || error.message.includes('send'))) { this.handleClose(); } return false; } } console.warn('[WS] Cannot send - socket state:', this.socket.readyState); return false; } 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 = 'متصل'; } else if (status === 'disconnected' || status === 'error') { statusBar.classList.add('disconnected'); statusText.textContent = status === 'error' ? 'خطا در اتصال' : 'قطع شده'; } else { statusText.textContent = 'در حال اتصال...'; } } } isConnected() { return this.socket && this.socket.readyState === WebSocket.OPEN; } disconnect() { this.stopHeartbeat(); if (this.socket) { try { // Check if socket is still open before closing if (this.socket.readyState === WebSocket.OPEN || this.socket.readyState === WebSocket.CONNECTING) { this.socket.close(); } } catch (error) { console.warn('[WS] Error during disconnect:', error); } finally { // Clean up after a brief delay to allow close to complete setTimeout(() => { try { if (this.socket) { this.socket.onopen = null; this.socket.onclose = null; this.socket.onerror = null; this.socket.onmessage = null; this.socket = null; } } catch (e) { // Ignore errors during cleanup } }, 100); } } this.status = 'disconnected'; this.updateStatus('disconnected'); } } // ═══════════════════════════════════════════════════════════════════ // API CLIENT // ═══════════════════════════════════════════════════════════════════ class APIClient { constructor(baseURL) { this.baseURL = baseURL; this.cache = new Map(); } 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) { console.log('[API] Cache hit:', endpoint); return cached.data; } } try { console.log('[API] Request:', endpoint); 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; } } // Market Data async getMarket() { return this.request('/api/market', { cache: true }); } async getTrending() { return this.request('/api/trending', { cache: true }); } async getSentiment() { return this.request('/api/sentiment', { cache: true }); } async getStats() { return this.request('/api/market/stats', { cache: true }); } // News async getNews(limit = 20) { return this.request(`/api/news/latest?limit=${limit}`, { cache: true }); } // Providers async getProviders() { return this.request('/api/providers', { cache: true }); } // Chart Data async getChartData(symbol, interval = '1h', limit = 100) { return this.request(`/api/ohlcv?symbol=${symbol}&interval=${interval}&limit=${limit}`, { 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) { const date = new Date(timestamp); return date.toLocaleDateString('fa-IR', { year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit', }); }, 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 // ═══════════════════════════════════════════════════════════════════ 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: [], }; } 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(); console.log('[App] Dashboard initialized successfully'); } setupWebSocketHandlers() { this.ws.on('connected', (isConnected) => { console.log('[App] WebSocket connection status:', isConnected); if (isConnected) { this.ws.send({ type: 'subscribe', groups: ['market', 'sentiment'] }); } }); 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('stats_update', (data) => { console.log('[App] Stats update received'); this.updateOnlineUsers(data.active_connections || 0); }); } 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'); }); } // Refresh buttons const refreshCoins = document.getElementById('refresh-coins'); if (refreshCoins) { refreshCoins.addEventListener('click', () => this.loadMarketData()); } // 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); }, 300)); } // AI Tools this.setupAIToolHandlers(); } 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.loadTrendingData(), this.loadNewsData(), ]); } catch (error) { console.error('[App] Error loading initial data:', error); } this.showLoadingOverlay(false); } async loadMarketData() { try { const data = await this.api.getMarket(); this.data.market = data; this.renderMarketStats(data); this.renderCoinsTable(data.cryptocurrencies || []); } catch (error) { console.error('[App] Error loading market data:', error); } } async loadSentimentData() { try { const data = await this.api.getSentiment(); // Transform backend format (value, classification) to frontend format (bullish, neutral, bearish) const transformed = this.transformSentimentData(data); this.data.sentiment = transformed; this.renderSentiment(transformed); } catch (error) { console.error('[App] Error loading sentiment data:', error); } } transformSentimentData(data) { // Backend returns: { value: 0-100, classification: "extreme_fear"|"fear"|"neutral"|"greed"|"extreme_greed", ... } // Frontend expects: { bullish: %, neutral: %, bearish: % } if (!data) { return { bullish: 0, neutral: 100, bearish: 0 }; } const value = data.value || 50; const classification = data.classification || 'neutral'; // Convert value (0-100) to bullish/neutral/bearish distribution let bullish = 0; let neutral = 0; let bearish = 0; if (classification.includes('extreme_greed') || classification.includes('greed')) { bullish = Math.max(60, value); neutral = Math.max(20, 100 - value); bearish = 100 - bullish - neutral; } else if (classification.includes('extreme_fear') || classification.includes('fear')) { bearish = Math.max(60, 100 - value); neutral = Math.max(20, value); bullish = 100 - bearish - neutral; } else { // Neutral - distribute around center neutral = 40 + Math.abs(50 - value) * 0.4; const remaining = 100 - neutral; bullish = remaining * (value / 100); bearish = remaining - bullish; } // Ensure they sum to 100 const total = bullish + neutral + bearish; if (total > 0) { bullish = Math.round((bullish / total) * 100); neutral = Math.round((neutral / total) * 100); bearish = 100 - bullish - neutral; } return { bullish, neutral, bearish, ...data // Keep original data for reference }; } async loadTrendingData() { try { const data = await this.api.getTrending(); this.data.trending = data; } catch (error) { console.error('[App] Error loading trending data:', error); } } async loadNewsData() { try { const data = await this.api.getNews(20); this.data.news = data.news || []; this.renderNews(this.data.news); } catch (error) { console.error('[App] Error loading news data:', error); } } renderMarketStats(data) { const totalMarketCap = document.getElementById('total-market-cap'); const btcDominance = document.getElementById('btc-dominance'); const volume24h = document.getElementById('volume-24h'); if (totalMarketCap && data.total_market_cap) { totalMarketCap.textContent = Utils.formatCurrency(data.total_market_cap); } if (btcDominance && data.btc_dominance) { btcDominance.textContent = `${data.btc_dominance.toFixed(1)}%`; } if (volume24h && data.total_volume_24h) { volume24h.textContent = Utils.formatCurrency(data.total_volume_24h); } } renderCoinsTable(coins) { const tbody = document.getElementById('coins-table-body'); if (!tbody) return; if (!coins || coins.length === 0) { tbody.innerHTML = 'داده‌ای یافت نشد'; return; } tbody.innerHTML = coins.slice(0, 20).map((coin, index) => ` ${index + 1}
${coin.symbol} ${coin.name}
${Utils.formatCurrency(coin.current_price)} ${Utils.formatPercent(coin.price_change_percentage_24h)} ${Utils.formatCurrency(coin.total_volume)} ${Utils.formatCurrency(coin.market_cap)} `).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 = '

خبری یافت نشد

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

${item.title}

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

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

`).join(''); } handleMarketUpdate(data) { if (data.data) { this.renderMarketStats(data.data); if (data.data.cryptocurrencies) { this.renderCoinsTable(data.data.cryptocurrencies); } } } handleSentimentUpdate(data) { if (data.data) { this.renderSentiment(data.data); } } updateOnlineUsers(count) { const activeUsersCount = document.getElementById('active-users-count'); if (activeUsersCount) { activeUsersCount.textContent = count; } } startPeriodicUpdates() { this.updateInterval = setInterval(() => { console.log('[App] Periodic update triggered'); this.loadMarketData(); this.loadSentimentData(); }, CONFIG.UPDATE_INTERVAL); } 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 } 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 = '
در حال تحلیل...'; try { const data = await this.api.getSentiment(); aiResultsContent.innerHTML = `

نتایج تحلیل احساسات

صعودی
${data.bullish}%
خنثی
${data.neutral}%
نزولی
${data.bearish}%

${data.summary || 'تحلیل احساسات بازار بر اساس داده‌های جمع‌آوری شده از منابع مختلف'}

`; } catch (error) { aiResultsContent.innerHTML = `
خطا در تحلیل: ${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 = '
در حال خلاصه‌سازی...'; setTimeout(() => { aiResultsContent.innerHTML = `

خلاصه اخبار

قابلیت خلاصه‌سازی اخبار به زودی اضافه خواهد شد.

این قابلیت از مدل‌های Hugging Face برای خلاصه‌سازی متن استفاده می‌کند.

`; }, 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 = '
در حال پیش‌بینی...'; setTimeout(() => { aiResultsContent.innerHTML = `

پیش‌بینی قیمت

قابلیت پیش‌بینی قیمت به زودی اضافه خواهد شد.

این قابلیت از مدل‌های یادگیری ماشین برای پیش‌بینی روند قیمت استفاده می‌کند.

`; }, 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 = '
در حال تشخیص الگو...'; setTimeout(() => { aiResultsContent.innerHTML = `

تشخیص الگو

قابلیت تشخیص الگو به زودی اضافه خواهد شد.

این قابلیت الگوهای کندل استیک و تحلیل تکنیکال را شناسایی می‌کند.

`; }, 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'); app.stopPeriodicUpdates(); } else { console.log('[Main] Page visible, resuming updates'); app.startPeriodicUpdates(); app.loadMarketData(); } }); // Export for module usage export { DashboardApp, APIClient, WebSocketClient, Utils };