const numberFormatter = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
maximumFractionDigits: 0,
});
const compactNumber = new Intl.NumberFormat('en-US', {
notation: 'compact',
maximumFractionDigits: 1,
});
const $ = (id) => document.getElementById(id);
const feedback = () => window.UIFeedback || {};
function renderTopPrices(data = [], source = 'live') {
const tbody = $('top-prices-table');
if (!tbody) return;
if (!data.length) {
feedback().fadeReplace?.(
tbody,
'
| No price data available. |
',
);
return;
}
const rows = data
.map((item) => {
const change = Number(item.price_change_percentage_24h ?? 0);
const tone = change >= 0 ? 'success' : 'danger';
return `
| ${item.symbol} |
${numberFormatter.format(item.current_price || item.price || 0)} |
${change.toFixed(2)}% |
${compactNumber.format(item.total_volume || item.volume_24h || 0)} |
`;
})
.join('');
feedback().fadeReplace?.(tbody, rows);
feedback().setBadge?.(
$('top-prices-source'),
`Source: ${source}`,
source === 'local-fallback' ? 'warning' : 'success',
);
}
function renderMarketOverview(payload) {
if (!payload) return;
$('metric-market-cap').textContent = numberFormatter.format(payload.total_market_cap || 0);
$('metric-volume').textContent = numberFormatter.format(payload.total_volume_24h || 0);
$('metric-btc-dom').textContent = `${(payload.btc_dominance || 0).toFixed(2)}%`;
$('metric-cap-source').textContent = `Assets: ${payload.top_by_volume?.length || 0}`;
$('metric-volume-source').textContent = `Markets: ${payload.markets || 0}`;
const gainers = payload.top_gainers?.slice(0, 3) || [];
const losers = payload.top_losers?.slice(0, 3) || [];
$('market-overview-list').innerHTML = `
Top Gainers${gainers
.map((g) => `${g.symbol} ${g.price_change_percentage_24h?.toFixed(1) ?? 0}%`)
.join(', ')}
Top Losers${losers
.map((g) => `${g.symbol} ${g.price_change_percentage_24h?.toFixed(1) ?? 0}%`)
.join(', ')}
Liquidity Leaders${payload.top_by_volume
?.slice(0, 3)
.map((p) => p.symbol)
.join(', ')}
`;
$('intro-source').textContent = payload.source === 'local-fallback' ? 'Source: Local Fallback JSON' : 'Source: Live Providers';
feedback().setBadge?.(
$('market-overview-source'),
`Source: ${payload.source || 'live'}`,
payload.source === 'local-fallback' ? 'warning' : 'info',
);
}
function renderSystemStatus(health, status, rateLimits, config) {
if (health) {
const tone =
health.status === 'healthy' ? 'success' : health.status === 'degraded' ? 'warning' : 'danger';
$('metric-health').textContent = health.status.toUpperCase();
$('metric-health-details').textContent = `${(health.services?.market_data?.status || 'n/a').toUpperCase()} MARKET | ${(health.services?.news?.status || 'n/a').toUpperCase()} NEWS`;
$('system-health-status').textContent = `Providers loaded: ${
health.providers_loaded || health.services?.providers?.count || 0
}`;
feedback().setBadge?.($('system-status-source'), `/health: ${health.status}`, tone);
}
if (status) {
$('system-status-list').innerHTML = `
Providers online${status.providers_online || 0}
Cache size${status.cache_size || 0}
Uptime${Math.round(status.uptime_seconds || 0)}s
`;
}
if (config) {
const configEntries = [
['Version', config.version || '--'],
['API Version', config.api_version || '--'],
['Symbols', (config.supported_symbols || []).slice(0, 5).join(', ') || '--'],
['Intervals', (config.supported_intervals || []).join(', ') || '--'],
];
$('system-config-list').innerHTML = configEntries
.map(([label, value]) => `${label}${value}`)
.join('');
} else {
$('system-config-list').innerHTML = 'No configuration loaded.';
}
if (rateLimits) {
$('rate-limits-list').innerHTML =
rateLimits.rate_limits
?.map((rule) => `${rule.endpoint}${rule.limit}/${rule.window}`)
.join('') || 'No limits configured';
}
}
function renderHFWidget(health, registry) {
if (health) {
const tone =
health.status === 'healthy' ? 'success' : health.status === 'degraded' ? 'warning' : 'danger';
feedback().setBadge?.($('hf-health-status'), `HF ${health.status}`, tone);
$('hf-widget-summary').textContent = `Config ready: ${
health.services?.config ? 'Yes' : 'No'
} | Models: ${registry?.items?.length || 0}`;
}
const items = registry?.items?.slice(0, 4) || [];
$('hf-registry-list').innerHTML =
items
.map((item) => `${item}Model`)
.join('') || 'No registry data.';
}
function pushStream(payload) {
const stream = $('ws-stream');
if (!stream) return;
const node = document.createElement('div');
node.className = 'stream-item fade-in';
const topCoin = payload.market_data?.[0]?.symbol || 'n/a';
const sentiment = payload.sentiment
? `${payload.sentiment.label || payload.sentiment.result || ''} (${(
payload.sentiment.confidence || 0
).toFixed?.(2) || payload.sentiment.confidence || ''})`
: 'n/a';
node.innerHTML = `${new Date().toLocaleTimeString()}
${topCoin} | Sentiment: ${sentiment}
${
(payload.market_data || [])
.slice(0, 3)
.map(
(coin) => `${coin.symbol} ${coin.price_change_percentage_24h?.toFixed(1) || 0}%`,
)
.join('') || 'Awaiting data'
}
`;
stream.prepend(node);
while (stream.children.length > 6) stream.removeChild(stream.lastChild);
}
function connectWebSocket() {
const badge = $('ws-status');
const url = `${window.location.protocol === 'https:' ? 'wss' : 'ws'}://${window.location.host}/ws`;
try {
const socket = new WebSocket(url);
socket.addEventListener('open', () => feedback().setBadge?.(badge, 'Connected', 'success'));
socket.addEventListener('message', (event) => {
try {
const message = JSON.parse(event.data);
if (message.type === 'connected') {
feedback().setBadge?.(badge, `Client ${message.client_id.slice(0, 6)}...`, 'info');
}
if (message.type === 'update') pushStream(message.payload);
} catch (err) {
feedback().toast?.('error', 'WS parse error', err.message);
}
});
socket.addEventListener('close', () => feedback().setBadge?.(badge, 'Disconnected', 'warning'));
} catch (err) {
feedback().toast?.('error', 'WebSocket failed', err.message);
feedback().setBadge?.(badge, 'Unavailable', 'danger');
}
}
async function initDashboard() {
feedback().showLoading?.($('top-prices-table'), 'Loading market data...');
feedback().showLoading?.($('market-overview-list'), 'Loading overview...');
try {
const [{ data: topData, source }, overview] = await Promise.all([
feedback().fetchJSON?.('/api/crypto/prices/top?limit=8', {}, 'Top prices'),
feedback().fetchJSON?.('/api/crypto/market-overview', {}, 'Market overview'),
]);
renderTopPrices(topData, source);
renderMarketOverview(overview);
} catch {
renderTopPrices([], 'local-fallback');
}
try {
const [health, status, rateLimits, config] = await Promise.all([
feedback().fetchJSON?.('/health', {}, 'Health'),
feedback().fetchJSON?.('/api/system/status', {}, 'System status'),
feedback().fetchJSON?.('/api/rate-limits', {}, 'Rate limits'),
feedback().fetchJSON?.('/api/system/config', {}, 'System config'),
]);
renderSystemStatus(health, status, rateLimits, config);
} catch {}
try {
const [hfHealth, hfRegistry] = await Promise.all([
feedback().fetchJSON?.('/api/hf/health', {}, 'HF health'),
feedback().fetchJSON?.('/api/hf/registry?kind=models', {}, 'HF registry'),
]);
renderHFWidget(hfHealth, hfRegistry);
} catch {
feedback().setBadge?.($('hf-health-status'), 'HF unavailable', 'warning');
}
connectWebSocket();
}
document.addEventListener('DOMContentLoaded', initDashboard);