// 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 = ' |
'; 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 = ''; return; }
el.innerHTML = data.map(a => `
🔔
${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 = ' |
'; 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 = `
`;
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);