// Predictalytics Analytics — Dashboard Application const API_BASE = ''; // ─── Theme Toggle ─── const themeToggle = document.getElementById('themeToggle'); const html = document.documentElement; const savedTheme = localStorage.getItem('ba-theme') || 'dark'; html.setAttribute('data-theme', savedTheme); themeToggle.addEventListener('click', () => { const current = html.getAttribute('data-theme'); const next = current === 'dark' ? 'light' : 'dark'; html.setAttribute('data-theme', next); localStorage.setItem('ba-theme', next); updateChartColors(); }); // ─── Navigation ─── document.querySelectorAll('.nav-item[data-page]').forEach(item => { item.addEventListener('click', e => { e.preventDefault(); const page = item.dataset.page; document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active')); item.classList.add('active'); document.querySelectorAll('.page').forEach(p => p.classList.remove('active')); document.getElementById(`page-${page}`).classList.add('active'); if (page === 'traders') loadTraders(); if (page === 'alerts') loadAlerts(); if (page === 'markets') loadMarkets(); }); }); let pageHistory = ['dashboard']; function navigateTo(pageId) { document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active')); document.querySelectorAll('.page').forEach(p => p.classList.remove('active')); const page = document.getElementById(`page-${pageId}`); if (page) { page.classList.add('active'); pageHistory.push(pageId); } } function navigateBack() { if (pageHistory.length > 1) { pageHistory.pop(); const prev = pageHistory[pageHistory.length - 1]; document.querySelectorAll('.page').forEach(p => p.classList.remove('active')); document.getElementById(`page-${prev}`).classList.add('active'); } } // ─── Search ─── const searchInput = document.getElementById('searchInput'); searchInput?.addEventListener('keypress', e => { if (e.key === 'Enter') { const query = searchInput.value.trim(); if (query) performSearch(query); } }); async function performSearch(query) { navigateTo('search'); document.getElementById('search-title').textContent = `Search Results for "${query}"`; const data = await api(`/api/search?q=${encodeURIComponent(query)}`); const tBody = document.getElementById('searchTradersBody'); const mBody = document.getElementById('searchMarketsBody'); if (!data) return; tBody.innerHTML = data.traders.map((t, i) => ` ${i + 1} ${t.displayName} ${t.platform} ${Number(t.combinedScore).toFixed(1)} ${fmt.tier(t.tier)} `).join('') || 'No traders found'; mBody.innerHTML = data.markets.map(m => ` ${m.platform} ${m.question.substring(0, 50)}... ${fmt.usd(m.volume)} ${m.isResolved ? 'Resolved' : 'Active'} `).join('') || 'No markets found'; } async function manualAddTrader() { const platform = document.getElementById('addPlatform').value; const wallet = document.getElementById('addWallet').value.trim(); if (!wallet) return; const res = await fetch(`/api/traders?platform=${platform}&wallet=${wallet}`, { method: 'POST' }); if (res.ok) { const data = await res.json(); alert('Trader added successfully! Sync will start shortly.'); document.getElementById('addWallet').value = ''; viewTrader(data.id); } else { alert('Failed to add trader.'); } } async function manualUpdateTrader(id) { const res = await fetch(`/api/traders/${id}/refresh`, { method: 'POST' }); if (res.ok) { alert('Sync triggered manually. Data will update in a few minutes.'); } else { alert('Failed to trigger sync.'); } } let currentPlatform = 'All'; let currentSort = 'default'; document.getElementById('platformSelect')?.addEventListener('change', (e) => { currentPlatform = e.target.value; refreshActivePage(); }); document.getElementById('sortSelect')?.addEventListener('change', (e) => { currentSort = e.target.value; refreshActivePage(); }); function refreshActivePage() { const activePage = document.querySelector('.page.active')?.id; if (activePage === 'page-dashboard') loadDashboard(); else if (activePage === 'page-traders') loadTraders(); else if (activePage === 'page-markets') loadMarkets(); } // ─── API Helpers ─── async function api(endpoint) { try { const res = await fetch(`${API_BASE}${endpoint}`); if (!res.ok) throw new Error(`HTTP ${res.status}`); return await res.json(); } catch (err) { console.error(`API Error [${endpoint}]:`, err); return null; } } // ─── Format Helpers ─── const fmt = { usd: v => { if (v === null || v === undefined) return '$0'; const n = Number(v); return n >= 1000000 ? `$${(n/1000000).toFixed(1)}M` : n >= 1000 ? `$${(n/1000).toFixed(1)}K` : `$${n.toFixed(0)}`; }, pct: v => { if (v === null || v === undefined) return '0%'; return `${Number(v).toFixed(1)}%`; }, num: v => { if (v === null || v === undefined) return '0'; return Number(v).toLocaleString(); }, time: v => { if (!v) return '—'; const d = new Date(v); const now = new Date(); const diff = (now - d) / 1000; if (diff < 60) return `${Math.floor(diff)}s ago`; if (diff < 3600) return `${Math.floor(diff/60)}m ago`; if (diff < 86400) return `${Math.floor(diff/3600)}h ago`; return d.toLocaleDateString(); }, tier: t => { if (!t) return '—'; const cls = `tier-${t.toString().toLowerCase()}`; return `${t}`; }, side: s => { if (!s) return '—'; return `${s}`; }, pnl: v => { if (v === null || v === undefined) return '$0'; const n = Number(v); return `${fmt.usd(Math.abs(n))}${n >= 0 ? ' ▲' : ' ▼'}`; } }; // ─── Chart instances ─── let platformChart, tierChart; function getChartColors() { const isDark = html.getAttribute('data-theme') === 'dark'; return { text: isDark ? '#AAAAAA' : '#666666', grid: isDark ? '#2A2C33' : '#E8E9EC', bg: isDark ? '#16181D' : '#FFFFFF' }; } function updateChartColors() { const c = getChartColors(); [platformChart, tierChart].forEach(chart => { if (!chart) return; if (chart.options.plugins?.legend) chart.options.plugins.legend.labels.color = c.text; chart.update(); }); } // ─── Dashboard Load ─── async function loadDashboard() { const data = await api('/api/dashboard'); if (!data) { document.getElementById('metricTraders').textContent = '0'; document.getElementById('metricActive').textContent = '0'; document.getElementById('metricTrades').textContent = '0'; document.getElementById('metricVolume').textContent = '$0'; return; } // Metrics document.getElementById('metricTraders').textContent = fmt.num(data.totalTraders); document.getElementById('metricActive').textContent = fmt.num(data.activeTraders24h); document.getElementById('metricTrades').textContent = fmt.num(data.totalTrades); document.getElementById('metricVolume').textContent = fmt.usd(data.totalVolume24h); // Alert badge const badge = document.getElementById('alertBadge'); if (data.unreadAlerts > 0) { badge.textContent = data.unreadAlerts; badge.style.display = 'inline'; } else { badge.style.display = 'none'; } // Top Traders table const tbody = document.getElementById('topTradersBody'); tbody.innerHTML = data.topTraders.map((t, i) => ` ${i + 1} ${t.displayName} ${t.platform} ${Number(t.combinedScore).toFixed(1)} ${fmt.pct(t.winRate)} ${fmt.pnl(t.totalPnl)} ${fmt.tier(t.tier)} ${fmt.num(t.totalTrades)} `).join(''); // Recent Trades table const rBody = document.getElementById('recentTradesBody'); rBody.innerHTML = data.recentTrades.map(t => ` ${fmt.time(t.executedAt)} ${t.traderName} ${t.dbMarketId ? `Market #${t.dbMarketId}` : (t.marketId ? t.marketId.substring(0, 12) + '...' : '—')} ${fmt.side(t.side)} ${Number(t.price).toFixed(2)} ${fmt.num(t.size)} ${fmt.usd(t.amount)} `).join(''); // Platform Chart const cc = getChartColors(); const pLabels = Object.keys(data.platformBreakdown.traderCounts); const pData = Object.values(data.platformBreakdown.traderCounts); if (platformChart) platformChart.destroy(); platformChart = new Chart(document.getElementById('platformChart'), { type: 'doughnut', data: { labels: pLabels.length ? pLabels : ['No Data'], datasets: [{ data: pData.length ? pData : [1], backgroundColor: ['#FF2D55', '#5AC8FA', '#FF9500', '#34C759', '#AF52DE', '#FF6B8A', '#30D158'], borderWidth: 0 }] }, options: { responsive: true, maintainAspectRatio: false, cutout: '70%', plugins: { legend: { position: 'bottom', labels: { color: cc.text, padding: 16, font: { family: "'Inter'", size: 12 } } } } } }); // Tier chart const tierData = data.topTraders.reduce((acc, t) => { acc[t.tier] = (acc[t.tier] || 0) + 1; return acc; }, {}); if (tierChart) tierChart.destroy(); const tLabels = Object.keys(tierData).length ? Object.keys(tierData) : ['No Data']; const tData = Object.values(tierData).length ? Object.values(tierData) : [1]; tierChart = new Chart(document.getElementById('tierChart'), { type: 'bar', data: { labels: tLabels, datasets: [{ label: 'Traders', data: tData, backgroundColor: '#FF2D55', borderRadius: 6, barThickness: 32 }] }, options: { responsive: true, maintainAspectRatio: false, scales: { x: { grid: { display: false }, ticks: { color: cc.text, font: { family: "'Inter'" } } }, y: { grid: { color: cc.grid }, ticks: { color: cc.text, font: { family: "'Inter'" } } } }, plugins: { legend: { display: false } } } }); } // ─── Traders Page ─── async function loadTraders() { let url = '/api/traders?skip=0&take=100'; if (currentPlatform !== 'All') url += `&platform=${currentPlatform}`; let data = await api(url); const tbody = document.getElementById('allTradersBody'); if (!data || !data.length) { tbody.innerHTML = '

No traders tracked yet.

'; return; } // Sorting if (currentSort === 'score') data.sort((a, b) => b.combinedScore - a.combinedScore); else if (currentSort === 'name') data.sort((a, b) => a.displayName.localeCompare(b.displayName)); else if (currentSort === 'pnl') data.sort((a, b) => b.totalPnl - a.totalPnl); tbody.innerHTML = data.map((t, i) => ` ${i + 1} ${t.displayName} ${t.platform} ${Number(t.combinedScore).toFixed(1)} ${fmt.pct(t.winRate)} ${fmt.pnl(t.totalPnl)} ${fmt.tier(t.tier)} ${t.strategy} `).join(''); } // ─── Alerts Page ─── async function loadAlerts() { const data = await api('/api/alerts?count=50'); const el = document.getElementById('alertsList'); if (!data || !data.length) { el.innerHTML = '

No alerts yet.

'; return; } el.innerHTML = data.map(a => `
🔔
${a.title}
${a.message}
${fmt.time(a.createdAt)}
`).join(''); } // ─── Markets Page ─── async function loadMarkets() { let url = '/api/markets?skip=0&take=100'; if (currentPlatform !== 'All') url += `&platform=${currentPlatform}`; let data = await api(url); const tbody = document.getElementById('allMarketsBody'); if (!data || !data.length) { tbody.innerHTML = '

No markets found.

'; return; } // Sorting if (currentSort === 'score') data.sort((a, b) => b.volume - a.volume); else if (currentSort === 'name') data.sort((a, b) => (a.endDate || '').localeCompare(b.endDate || '')); else if (currentSort === 'pnl') data.sort((a, b) => b.liquidity - a.liquidity); tbody.innerHTML = data.map(m => ` ${m.platform} ${m.question.length > 60 ? m.question.substring(0, 60) + '...' : m.question} ${fmt.usd(m.volume)} ${fmt.usd(m.liquidity)} ${m.endDate ? new Date(m.endDate).toLocaleDateString() : '—'} ${m.isResolved ? 'Resolved' : 'Active'} `).join(''); } async function viewTrader(id) { navigateTo('trader-detail'); const t = await api(`/api/traders/${id}`); if (!t) return; document.getElementById('td-name').textContent = t.displayName; document.getElementById('td-platform').textContent = t.platform; document.getElementById('td-platformId').textContent = t.platformUserId; document.getElementById('td-tier').innerHTML = fmt.tier(t.tier); document.getElementById('td-strategy').textContent = t.strategy; document.getElementById('td-winrate').textContent = fmt.pct(t.winRate); document.getElementById('td-pnl').innerHTML = fmt.pnl(t.totalPnl); document.getElementById('td-trades').textContent = fmt.num(t.totalTrades); document.getElementById('td-score').textContent = Number(t.combinedScore).toFixed(1); const refreshBtn = document.getElementById('btn-refresh-trader'); refreshBtn.onclick = () => manualUpdateTrader(id); const tbody = document.getElementById('td-tradesBody'); tbody.innerHTML = t.recentTrades.map(tr => ` ${fmt.time(tr.executedAt)} ${tr.dbMarketId ? `Market #${tr.dbMarketId}` : (tr.marketId ? tr.marketId.substring(0, 16) + '...' : '—')} ${fmt.side(tr.side)} ${Number(tr.price).toFixed(2)} ${fmt.num(tr.size)} ${fmt.usd(tr.amount)} `).join(''); } async function viewMarket(id) { navigateTo('market-detail'); const m = await api(`/api/markets/${id}`); if (!m) return; document.getElementById('md-question').textContent = m.question; document.getElementById('md-platform').textContent = m.platform; document.getElementById('md-category').textContent = m.category; document.getElementById('md-ends').textContent = m.endDate ? new Date(m.endDate).toLocaleDateString() : 'Never'; document.getElementById('md-volume').textContent = fmt.usd(m.volume); document.getElementById('md-liquidity').textContent = fmt.usd(m.liquidity); document.getElementById('md-status').textContent = m.isResolved ? 'Resolved' : 'Active'; const imgContainer = document.getElementById('md-image'); if (m.imageUrl) imgContainer.innerHTML = `Market`; else imgContainer.innerHTML = ''; const oList = document.getElementById('md-outcomes'); oList.innerHTML = m.outcomes.map(o => `
${o.name} ${(o.price * 100).toFixed(1)}¢
`).join(''); const tbody = document.getElementById('md-tradesBody'); tbody.innerHTML = m.recentTrades.map(tr => ` ${fmt.time(tr.executedAt)} ${tr.traderName} ${fmt.side(tr.side)} ${Number(tr.price).toFixed(2)} ${fmt.num(tr.size)} ${fmt.usd(tr.amount)} `).join(''); } async function viewMarketByQuestion(question) { // Legacy helper: search markets by question text const data = await api(`/api/search?q=${encodeURIComponent(question)}`); if (data && data.markets.length > 0) { viewMarket(data.markets[0].id); } } function viewMarketById(dbMarketId) { if (dbMarketId) viewMarket(dbMarketId); } // ─── Initial Load ─── loadDashboard(); // Auto-refresh every 30 seconds setInterval(() => { const activePage = document.querySelector('.page.active'); if (activePage?.id === 'page-dashboard') loadDashboard(); }, 30000);