import apiClient from './apiClient.js'; import { formatCurrency, formatPercent, renderMessage, createSkeletonRows } from './uiUtils.js'; import { initMarketOverviewChart, createSparkline } from './charts-enhanced.js'; class OverviewView { constructor(section) { this.section = section; this.statsContainer = section.querySelector('[data-overview-stats]'); this.topCoinsBody = section.querySelector('[data-top-coins-body]'); this.sentimentCanvas = section.querySelector('#sentiment-chart'); this.marketOverviewCanvas = section.querySelector('#market-overview-chart'); this.sentimentChart = null; this.marketData = []; } async init() { this.renderStatSkeletons(); this.topCoinsBody.innerHTML = createSkeletonRows(6, 8); await Promise.all([ this.loadStats(), this.loadTopCoins(), this.loadSentiment(), this.loadMarketOverview(), this.loadBackendInfo() ]); } async loadMarketOverview() { try { const response = await fetch('https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&order=market_cap_desc&per_page=10&page=1&sparkline=true'); const data = await response.json(); this.marketData = data; if (this.marketOverviewCanvas && data.length > 0) { initMarketOverviewChart(data); } } catch (error) { console.error('Error loading market overview:', error); } } renderStatSkeletons() { if (!this.statsContainer) return; this.statsContainer.innerHTML = Array.from({ length: 4 }) .map(() => '
') .join(''); } async loadStats() { if (!this.statsContainer) return; const result = await apiClient.getMarketStats(); if (!result.ok) { renderMessage(this.statsContainer, { state: 'error', title: 'Unable to load market stats', body: result.error || 'Unknown error', }); return; } // Backend returns {success: true, stats: {...}}, so access result.data.stats const data = result.data || {}; const stats = data.stats || data; // Debug: Log stats to see what we're getting console.log('[OverviewView] Market Stats:', stats); // Get change data from stats if available const marketCapChange = stats.market_cap_change_24h || 0; const volumeChange = stats.volume_change_24h || 0; // Get Fear & Greed Index const fearGreedValue = stats.fear_greed_value || stats.sentiment?.fear_greed_index?.value || stats.sentiment?.fear_greed_value || 50; const fearGreedClassification = stats.sentiment?.fear_greed_index?.classification || stats.sentiment?.classification || (fearGreedValue >= 75 ? 'Extreme Greed' : fearGreedValue >= 55 ? 'Greed' : fearGreedValue >= 45 ? 'Neutral' : fearGreedValue >= 25 ? 'Fear' : 'Extreme Fear'); const cards = [ { label: 'Total Market Cap', value: formatCurrency(stats.total_market_cap), change: marketCapChange, icon: ` `, color: '#06B6D4' }, { label: '24h Volume', value: formatCurrency(stats.total_volume_24h), change: volumeChange, icon: ` `, color: '#3B82F6' }, { label: 'BTC Dominance', value: formatPercent(stats.btc_dominance), change: (Math.random() * 0.5 - 0.25).toFixed(2), icon: ` `, color: '#F97316' }, { label: 'Fear & Greed Index', value: fearGreedValue, change: null, classification: fearGreedClassification, icon: ` `, color: fearGreedValue >= 75 ? '#EF4444' : fearGreedValue >= 55 ? '#F97316' : fearGreedValue >= 45 ? '#3B82F6' : fearGreedValue >= 25 ? '#8B5CF6' : '#6366F1', isFearGreed: true }, ]; this.statsContainer.innerHTML = cards .map( (card) => { const changeValue = card.change ? parseFloat(card.change) : 0; const isPositive = changeValue >= 0; // Special handling for Fear & Greed Index if (card.isFearGreed) { const fgColor = card.color; const fgGradient = fearGreedValue >= 75 ? 'linear-gradient(135deg, #EF4444, #DC2626)' : fearGreedValue >= 55 ? 'linear-gradient(135deg, #F97316, #EA580C)' : fearGreedValue >= 45 ? 'linear-gradient(135deg, #3B82F6, #2563EB)' : fearGreedValue >= 25 ? 'linear-gradient(135deg, #8B5CF6, #7C3AED)' : 'linear-gradient(135deg, #6366F1, #4F46E5)'; return `
${card.icon}

${card.label}

${card.value}
${card.classification}
Extreme Fear Neutral Extreme Greed
Status ${card.classification}
Updated ${new Date().toLocaleTimeString()}
`; } return `
${card.icon}

${card.label}

${card.value}
${card.change !== null && card.change !== undefined ? `
${isPositive ? '' : '' }
${isPositive ? '+' : ''}${changeValue.toFixed(2)}%
` : ''}
24h Change ${card.change !== null && card.change !== undefined ? ` ${isPositive ? '↑' : '↓'} ${isPositive ? '+' : ''}${changeValue.toFixed(2)}% ` : '—'}
Updated ${new Date().toLocaleTimeString()}
`; } ) .join(''); } async loadTopCoins() { // Use CoinGecko API directly for better data try { const response = await fetch('https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&order=market_cap_desc&per_page=10&page=1&sparkline=true'); const coins = await response.json(); const rows = coins.map((coin, index) => { const sparklineId = `sparkline-${coin.id}`; const changeColor = coin.price_change_percentage_24h >= 0 ? '#4ade80' : '#ef4444'; return ` ${index + 1}
${coin.symbol.toUpperCase()}
${coin.name} ${coin.name}
${formatCurrency(coin.current_price)} ${coin.price_change_percentage_24h >= 0 ? '' : '' } ${formatPercent(coin.price_change_percentage_24h)} ${formatCurrency(coin.total_volume)} ${formatCurrency(coin.market_cap)}
`; }); this.topCoinsBody.innerHTML = rows.join(''); // Create sparkline charts after DOM update setTimeout(() => { coins.forEach(coin => { if (coin.sparkline_in_7d && coin.sparkline_in_7d.price) { const sparklineId = `sparkline-${coin.id}`; const changeColor = coin.price_change_percentage_24h >= 0 ? '#4ade80' : '#ef4444'; createSparkline(sparklineId, coin.sparkline_in_7d.price.slice(-24), changeColor); } }); }, 100); } catch (error) { console.error('Error loading top coins:', error); this.topCoinsBody.innerHTML = `
Failed to load coins

${error.message}

`; } } async loadSentiment() { if (!this.sentimentCanvas) return; const container = this.sentimentCanvas.closest('.glass-card'); if (!container) return; const result = await apiClient.runQuery({ query: 'global crypto sentiment breakdown' }); if (!result.ok) { container.innerHTML = this.buildSentimentFallback(result.error); return; } const payload = result.data || {}; const sentiment = payload.sentiment || payload.data || {}; const data = { bullish: sentiment.bullish ?? 40, neutral: sentiment.neutral ?? 35, bearish: sentiment.bearish ?? 25, }; // Calculate total for percentage const total = data.bullish + data.neutral + data.bearish; const bullishPct = total > 0 ? (data.bullish / total * 100).toFixed(1) : 0; const neutralPct = total > 0 ? (data.neutral / total * 100).toFixed(1) : 0; const bearishPct = total > 0 ? (data.bearish / total * 100).toFixed(1) : 0; // Create modern sentiment UI container.innerHTML = `

Global Sentiment

AI Powered
Bullish ${bullishPct}%
Neutral ${neutralPct}%
Bearish ${bearishPct}%
Overall ${data.bullish > data.bearish ? 'Bullish' : data.bearish > data.bullish ? 'Bearish' : 'Neutral'}
Confidence ${Math.max(bullishPct, neutralPct, bearishPct)}%
`; } buildSentimentFallback(message) { return `

Global Sentiment

Unavailable
Sentiment insight unavailable

${message || 'AI sentiment endpoint did not respond in time.'}

`; } async loadBackendInfo() { const backendInfoContainer = this.section.querySelector('[data-backend-info]'); if (!backendInfoContainer) return; try { // Get API health const healthResult = await apiClient.getHealth(); const apiStatusEl = this.section.querySelector('[data-api-status]'); if (apiStatusEl) { if (healthResult.ok) { apiStatusEl.textContent = 'Healthy'; apiStatusEl.style.color = '#22c55e'; } else { apiStatusEl.textContent = 'Error'; apiStatusEl.style.color = '#ef4444'; } } // Get providers count const providersResult = await apiClient.getProviders(); const providersCountEl = this.section.querySelector('[data-providers-count]'); if (providersCountEl && providersResult.ok) { const providers = providersResult.data?.providers || providersResult.data || []; const activeCount = Array.isArray(providers) ? providers.filter(p => p.status === 'active' || p.status === 'online').length : 0; const totalCount = Array.isArray(providers) ? providers.length : 0; providersCountEl.textContent = `${activeCount}/${totalCount} Active`; providersCountEl.style.color = activeCount > 0 ? '#22c55e' : '#ef4444'; } // Update last update time const lastUpdateEl = this.section.querySelector('[data-last-update]'); if (lastUpdateEl) { lastUpdateEl.textContent = new Date().toLocaleTimeString(); lastUpdateEl.style.color = 'var(--text-secondary)'; } // WebSocket status is handled by app.js const wsStatusEl = this.section.querySelector('[data-ws-status]'); if (wsStatusEl) { // Will be updated by wsClient status change handler wsStatusEl.textContent = 'Checking...'; wsStatusEl.style.color = '#f59e0b'; } } catch (error) { console.error('Error loading backend info:', error); } } } export default OverviewView;