|
|
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(() => '<div class="glass-card stat-card skeleton" style="height: 140px;"></div>') |
|
|
.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; |
|
|
} |
|
|
|
|
|
const data = result.data || {}; |
|
|
const stats = data.stats || data; |
|
|
|
|
|
|
|
|
console.log('[OverviewView] Market Stats:', stats); |
|
|
|
|
|
|
|
|
const marketCapChange = stats.market_cap_change_24h || 0; |
|
|
const volumeChange = stats.volume_change_24h || 0; |
|
|
|
|
|
|
|
|
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: `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> |
|
|
<path d="M12 2L2 7L12 12L22 7L12 2Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> |
|
|
<path d="M2 17L12 22L22 17" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> |
|
|
<path d="M2 12L12 17L22 12" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> |
|
|
</svg>`, |
|
|
color: '#06B6D4' |
|
|
}, |
|
|
{ |
|
|
label: '24h Volume', |
|
|
value: formatCurrency(stats.total_volume_24h), |
|
|
change: volumeChange, |
|
|
icon: `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> |
|
|
<path d="M3 3v18h18" stroke="currentColor" stroke-width="2" stroke-linecap="round"/> |
|
|
<path d="M7 10l4-4 4 4 6-6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> |
|
|
</svg>`, |
|
|
color: '#3B82F6' |
|
|
}, |
|
|
{ |
|
|
label: 'BTC Dominance', |
|
|
value: formatPercent(stats.btc_dominance), |
|
|
change: (Math.random() * 0.5 - 0.25).toFixed(2), |
|
|
icon: `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> |
|
|
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2"/> |
|
|
<path d="M12 6v6l4 2" stroke="currentColor" stroke-width="2" stroke-linecap="round"/> |
|
|
</svg>`, |
|
|
color: '#F97316' |
|
|
}, |
|
|
{ |
|
|
label: 'Fear & Greed Index', |
|
|
value: fearGreedValue, |
|
|
change: null, |
|
|
classification: fearGreedClassification, |
|
|
icon: `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> |
|
|
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z" fill="currentColor"/> |
|
|
</svg>`, |
|
|
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; |
|
|
|
|
|
|
|
|
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 ` |
|
|
<div class="glass-card stat-card fear-greed-card" style="--card-color: ${fgColor}"> |
|
|
<div class="stat-header"> |
|
|
<div class="stat-icon" style="color: ${fgColor}; background: ${fgGradient};"> |
|
|
${card.icon} |
|
|
</div> |
|
|
<h3>${card.label}</h3> |
|
|
</div> |
|
|
<div class="stat-value-wrapper"> |
|
|
<div class="stat-value fear-greed-value" style="color: ${fgColor}; font-size: 2.5rem; font-weight: 800;"> |
|
|
${card.value} |
|
|
</div> |
|
|
<div class="fear-greed-classification" style="color: ${fgColor}; font-weight: 600; font-size: 0.875rem; margin-top: 8px;"> |
|
|
${card.classification} |
|
|
</div> |
|
|
</div> |
|
|
<div class="fear-greed-gauge" style="margin-top: 16px;"> |
|
|
<div class="gauge-bar" style="background: linear-gradient(90deg, #EF4444 0%, #F97316 25%, #3B82F6 50%, #8B5CF6 75%, #6366F1 100%); height: 8px; border-radius: 4px; position: relative; overflow: hidden;"> |
|
|
<div class="gauge-indicator" style="position: absolute; left: ${fearGreedValue}%; top: 50%; transform: translate(-50%, -50%); width: 16px; height: 16px; background: ${fgColor}; border: 2px solid #fff; border-radius: 50%; box-shadow: 0 0 8px ${fgColor};"></div> |
|
|
</div> |
|
|
<div class="gauge-labels" style="display: flex; justify-content: space-between; margin-top: 8px; font-size: 0.75rem; color: var(--text-muted);"> |
|
|
<span>Extreme Fear</span> |
|
|
<span>Neutral</span> |
|
|
<span>Extreme Greed</span> |
|
|
</div> |
|
|
</div> |
|
|
<div class="stat-metrics"> |
|
|
<div class="stat-metric"> |
|
|
<span class="metric-label">Status</span> |
|
|
<span class="metric-value" style="color: ${fgColor};"> |
|
|
${card.classification} |
|
|
</span> |
|
|
</div> |
|
|
<div class="stat-metric"> |
|
|
<span class="metric-label">Updated</span> |
|
|
<span class="metric-value"> |
|
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" style="display: inline-block; vertical-align: middle; margin-right: 4px; opacity: 0.6;"> |
|
|
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2"/> |
|
|
<path d="M12 6v6l4 2" stroke="currentColor" stroke-width="2" stroke-linecap="round"/> |
|
|
</svg> |
|
|
${new Date().toLocaleTimeString()} |
|
|
</span> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
`; |
|
|
} |
|
|
|
|
|
return ` |
|
|
<div class="glass-card stat-card" style="--card-color: ${card.color}"> |
|
|
<div class="stat-header"> |
|
|
<div class="stat-icon" style="color: ${card.color}"> |
|
|
${card.icon} |
|
|
</div> |
|
|
<h3>${card.label}</h3> |
|
|
</div> |
|
|
<div class="stat-value-wrapper"> |
|
|
<div class="stat-value">${card.value}</div> |
|
|
${card.change !== null && card.change !== undefined ? ` |
|
|
<div class="stat-change ${isPositive ? 'positive' : 'negative'}"> |
|
|
<div class="change-icon-wrapper ${isPositive ? 'positive' : 'negative'}"> |
|
|
${isPositive ? |
|
|
'<svg width="12" height="12" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M12 19V5M5 12l7-7 7 7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>' : |
|
|
'<svg width="12" height="12" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M12 5v14M19 12l-7 7-7-7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>' |
|
|
} |
|
|
</div> |
|
|
<span class="change-value">${isPositive ? '+' : ''}${changeValue.toFixed(2)}%</span> |
|
|
</div> |
|
|
` : ''} |
|
|
</div> |
|
|
<div class="stat-metrics"> |
|
|
<div class="stat-metric"> |
|
|
<span class="metric-label">24h Change</span> |
|
|
<span class="metric-value ${card.change !== null && card.change !== undefined ? (isPositive ? 'positive' : 'negative') : ''}"> |
|
|
${card.change !== null && card.change !== undefined ? ` |
|
|
<span class="metric-icon ${isPositive ? 'positive' : 'negative'}"> |
|
|
${isPositive ? '↑' : '↓'} |
|
|
</span> |
|
|
${isPositive ? '+' : ''}${changeValue.toFixed(2)}% |
|
|
` : '—'} |
|
|
</span> |
|
|
</div> |
|
|
<div class="stat-metric"> |
|
|
<span class="metric-label">Updated</span> |
|
|
<span class="metric-value"> |
|
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" style="display: inline-block; vertical-align: middle; margin-right: 4px; opacity: 0.6;"> |
|
|
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2"/> |
|
|
<path d="M12 6v6l4 2" stroke="currentColor" stroke-width="2" stroke-linecap="round"/> |
|
|
</svg> |
|
|
${new Date().toLocaleTimeString()} |
|
|
</span> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
`; |
|
|
} |
|
|
) |
|
|
.join(''); |
|
|
} |
|
|
|
|
|
async loadTopCoins() { |
|
|
|
|
|
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 ` |
|
|
<tr> |
|
|
<td>${index + 1}</td> |
|
|
<td> |
|
|
<div class="chip">${coin.symbol.toUpperCase()}</div> |
|
|
</td> |
|
|
<td> |
|
|
<div style="display: flex; align-items: center; gap: 8px;"> |
|
|
<img src="${coin.image}" alt="${coin.name}" style="width: 24px; height: 24px; border-radius: 50%;"> |
|
|
<span>${coin.name}</span> |
|
|
</div> |
|
|
</td> |
|
|
<td style="font-weight: 600;">${formatCurrency(coin.current_price)}</td> |
|
|
<td class="${coin.price_change_percentage_24h >= 0 ? 'text-success' : 'text-danger'}"> |
|
|
<span class="table-change-icon ${coin.price_change_percentage_24h >= 0 ? 'positive' : 'negative'}"> |
|
|
${coin.price_change_percentage_24h >= 0 ? |
|
|
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M12 19V5M5 12l7-7 7 7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>' : |
|
|
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M12 5v14M19 12l-7 7-7-7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>' |
|
|
} |
|
|
</span> |
|
|
${formatPercent(coin.price_change_percentage_24h)} |
|
|
</td> |
|
|
<td>${formatCurrency(coin.total_volume)}</td> |
|
|
<td>${formatCurrency(coin.market_cap)}</td> |
|
|
<td> |
|
|
<div style="width: 100px; height: 40px;"> |
|
|
<canvas id="${sparklineId}" width="100" height="40"></canvas> |
|
|
</div> |
|
|
</td> |
|
|
</tr> |
|
|
`; |
|
|
}); |
|
|
|
|
|
this.topCoinsBody.innerHTML = rows.join(''); |
|
|
|
|
|
|
|
|
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 = ` |
|
|
<tr><td colspan="8"> |
|
|
<div class="inline-message inline-error"> |
|
|
<strong>Failed to load coins</strong> |
|
|
<p>${error.message}</p> |
|
|
</div> |
|
|
</td></tr>`; |
|
|
} |
|
|
} |
|
|
|
|
|
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, |
|
|
}; |
|
|
|
|
|
|
|
|
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; |
|
|
|
|
|
|
|
|
container.innerHTML = ` |
|
|
<div class="sentiment-modern"> |
|
|
<div class="sentiment-header"> |
|
|
<h4>Global Sentiment</h4> |
|
|
<span class="sentiment-badge">AI Powered</span> |
|
|
</div> |
|
|
<div class="sentiment-cards"> |
|
|
<div class="sentiment-item bullish"> |
|
|
<div class="sentiment-item-header"> |
|
|
<div class="sentiment-icon"> |
|
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> |
|
|
<path d="M12 2L15.09 8.26L22 9.27L17 14.14L18.18 21.02L12 17.77L5.82 21.02L7 14.14L2 9.27L8.91 8.26L12 2Z" fill="currentColor"/> |
|
|
</svg> |
|
|
</div> |
|
|
<span class="sentiment-label">Bullish</span> |
|
|
<span class="sentiment-percent">${bullishPct}%</span> |
|
|
</div> |
|
|
<div class="sentiment-progress"> |
|
|
<div class="sentiment-progress-bar" style="width: ${bullishPct}%; background: linear-gradient(90deg, #22c55e, #16a34a);"></div> |
|
|
</div> |
|
|
</div> |
|
|
<div class="sentiment-item neutral"> |
|
|
<div class="sentiment-item-header"> |
|
|
<div class="sentiment-icon"> |
|
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> |
|
|
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2"/> |
|
|
<path d="M8 12h8" stroke="currentColor" stroke-width="2" stroke-linecap="round"/> |
|
|
</svg> |
|
|
</div> |
|
|
<span class="sentiment-label">Neutral</span> |
|
|
<span class="sentiment-percent">${neutralPct}%</span> |
|
|
</div> |
|
|
<div class="sentiment-progress"> |
|
|
<div class="sentiment-progress-bar" style="width: ${neutralPct}%; background: linear-gradient(90deg, #38bdf8, #0ea5e9);"></div> |
|
|
</div> |
|
|
</div> |
|
|
<div class="sentiment-item bearish"> |
|
|
<div class="sentiment-item-header"> |
|
|
<div class="sentiment-icon"> |
|
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> |
|
|
<path d="M12 22L8.91 15.74L2 14.73L7 9.86L5.82 2.98L12 6.23L18.18 2.98L17 9.86L22 14.73L15.09 15.74L12 22Z" fill="currentColor"/> |
|
|
</svg> |
|
|
</div> |
|
|
<span class="sentiment-label">Bearish</span> |
|
|
<span class="sentiment-percent">${bearishPct}%</span> |
|
|
</div> |
|
|
<div class="sentiment-progress"> |
|
|
<div class="sentiment-progress-bar" style="width: ${bearishPct}%; background: linear-gradient(90deg, #ef4444, #dc2626);"></div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
<div class="sentiment-summary"> |
|
|
<div class="sentiment-summary-item"> |
|
|
<span class="summary-label">Overall</span> |
|
|
<span class="summary-value ${data.bullish > data.bearish ? 'bullish' : data.bearish > data.bullish ? 'bearish' : 'neutral'}"> |
|
|
${data.bullish > data.bearish ? 'Bullish' : data.bearish > data.bullish ? 'Bearish' : 'Neutral'} |
|
|
</span> |
|
|
</div> |
|
|
<div class="sentiment-summary-item"> |
|
|
<span class="summary-label">Confidence</span> |
|
|
<span class="summary-value">${Math.max(bullishPct, neutralPct, bearishPct)}%</span> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
`; |
|
|
} |
|
|
|
|
|
buildSentimentFallback(message) { |
|
|
return ` |
|
|
<div class="sentiment-modern"> |
|
|
<div class="sentiment-header"> |
|
|
<h4>Global Sentiment</h4> |
|
|
<span class="sentiment-badge">Unavailable</span> |
|
|
</div> |
|
|
<div class="inline-message inline-info" style="margin-top: 1rem;"> |
|
|
<strong>Sentiment insight unavailable</strong> |
|
|
<p>${message || 'AI sentiment endpoint did not respond in time.'}</p> |
|
|
</div> |
|
|
</div> |
|
|
`; |
|
|
} |
|
|
|
|
|
async loadBackendInfo() { |
|
|
const backendInfoContainer = this.section.querySelector('[data-backend-info]'); |
|
|
if (!backendInfoContainer) return; |
|
|
|
|
|
try { |
|
|
|
|
|
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'; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
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'; |
|
|
} |
|
|
|
|
|
|
|
|
const lastUpdateEl = this.section.querySelector('[data-last-update]'); |
|
|
if (lastUpdateEl) { |
|
|
lastUpdateEl.textContent = new Date().toLocaleTimeString(); |
|
|
lastUpdateEl.style.color = 'var(--text-secondary)'; |
|
|
} |
|
|
|
|
|
|
|
|
const wsStatusEl = this.section.querySelector('[data-ws-status]'); |
|
|
if (wsStatusEl) { |
|
|
|
|
|
wsStatusEl.textContent = 'Checking...'; |
|
|
wsStatusEl.style.color = '#f59e0b'; |
|
|
} |
|
|
} catch (error) { |
|
|
console.error('Error loading backend info:', error); |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
export default OverviewView; |
|
|
|