// 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();
if (page === 'jobs') loadJobs();
if (page === 'watchlist') loadWatchlist();
});
});
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)}
View
`).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'}
View
`).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.');
}
}
let currentPlatform = 'All';
let currentSort = 'default';
let sortDirection = -1;
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();
else if (activePage === 'page-watchlist') loadWatchlist();
}
// ─── API Helpers ───
async function api(endpoint, options = {}) {
try {
const res = await fetch(`${API_BASE}${endpoint}`, options);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const text = await res.text();
return text ? JSON.parse(text) : true;
} 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%';
const n = Number(v);
const color = n > 55 ? 'var(--pnl-positive)' : n < 45 ? 'var(--pnl-negative)' : 'var(--text)';
return `${n.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() {
return {
text: '#8B93A7',
grid: 'rgba(255,255,255,0.07)',
bg: 'rgba(255,255,255,0.045)'
};
}
function getAvatarBg(id) {
const gradients = [
'linear-gradient(135deg,#1652F0,#4c8dff)',
'linear-gradient(135deg,#12D48A,#0a8f5f)',
'linear-gradient(135deg,#7c5cff,#4c8dff)',
'linear-gradient(135deg,#ff9d4c,#ff6b8a)',
'linear-gradient(135deg,#F6465D,#ff6b8a)'
];
return gradients[id % gradients.length];
}
function getTraitClass(trait) {
if (!trait) return 'tier-unknown';
const lower = trait.toLowerCase();
if (lower.includes('winrate') || lower.includes('wins') || lower.includes('farming') || lower.includes('insider')) return 'trait-winrate';
if (lower.includes('volume') || lower.includes('amounts') || lower.includes('sizes') || lower.includes('stake')) return 'trait-volume';
if (lower.includes('whale') || lower.includes('scalper') || lower.includes('martingale')) return 'trait-whales';
if (lower.includes('sentiment') || lower.includes('cadence') || lower.includes('24_7') || lower.includes('copyable') || lower.includes('wallet')) return 'trait-sentiment';
return 'tier-unknown';
}
const TraitMetadata = {
"sub_second_cadence": {
name: "Sub-Second Cadence",
desc: "Executes trades in sub-second intervals, highly indicative of algorithmic execution."
},
"always_on_24_7": {
name: "24/7 Activity",
desc: "Trades at all hours of the day and night with very short gaps, indicating bot operation."
},
"uniform_sizes": {
name: "Uniform Sizes",
desc: "Executes trades with identical position sizes, suggesting a systematic layout."
},
"round_amounts": {
name: "Round Amounts",
desc: "Executes trades with round amounts (e.g. 100, 500, 1000 USD), common for manual traders."
},
"uses_split_merge": {
name: "Split & Merge",
desc: "Splits large positions into smaller orders or merges multiple positions to optimize slippage."
},
"both_sides_same_market": {
name: "Two-Sided Market Maker",
desc: "Buys and sells in the same market, extracting spreads or resolving inventory."
},
"resolution_farming": {
name: "Resolution Farmer",
desc: "Buys options at high probabilities (e.g., >93%) to collect predictable small returns."
},
"longshot_buyer": {
name: "Longshot Buyer",
desc: "Buys options at low probabilities (e.g., <10%), seeking rare high-payoff events."
},
"scalper": {
name: "Scalper",
desc: "Holds positions for very short periods (typically <1 hour) to lock in quick profits."
},
"holds_to_resolution": {
name: "Holds to Resolution",
desc: "Keeps positions open until the market officially resolves, avoiding early exits."
},
"fresh_wallet": {
name: "Fresh Wallet",
desc: "Wallet was created recently (less than 30 days of active history)."
},
"stable_stake_fraction": {
name: "Stable Stake",
desc: "Risk per trade is a highly stable fraction of estimated total bankroll."
},
"possible_insider": {
name: "Possible Insider",
desc: "Consistently wins low-probability bets with very low volume, indicating asymmetric information."
},
"thin_margin_wins": {
name: "Thin Margin Winner",
desc: "Consistently resolves trades with small margins of win (low ROI)."
},
"high_payoff_wins": {
name: "High Payoff Winner",
desc: "Consistently resolves trades with high margins of win (high ROI)."
},
"sells_at_loss": {
name: "Sells at Loss",
desc: "Uses strict stop-loss rules, selling options at a loss when the market goes against them."
},
"days_active": {
name: "Days Active",
desc: "The number of days this wallet has been active on-chain."
},
"trades_last_30_days": {
name: "Trades Last 30d",
desc: "The total number of trades executed in the last 30 days."
},
"martingale_pattern": {
name: "Martingale Trader",
desc: "Increases bet size after losses to attempt to recoup losses, typical of high-risk strategies."
},
"not_copyable_hf": {
name: "Uncopyable (High Freq)",
desc: "Trades at frequencies too high to replicate manually or with standard copytrading setups."
}
};
function getTraitDisplayName(traitKey) {
const meta = TraitMetadata[traitKey];
return meta ? meta.name : traitKey.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
}
function getTraitDescription(traitKey) {
const meta = TraitMetadata[traitKey];
return meta ? meta.desc : 'No description available.';
}
// ─── 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)}
${t.trades30d} | ${t.totalTrades}
`).join('');
// Recent Trades table
const rBody = document.getElementById('recentTradesBody');
rBody.innerHTML = data.recentTrades.map(t => `
${fmt.time(t.executedAt)}
${t.traderName}
${fmt.side(t.side)}
${Number(t.price).toFixed(2)}
${fmt.num(t.size)}
`).join('');
const headerVol = document.getElementById('headerVolumeBadge');
if (headerVol) {
headerVol.textContent = `24H VOL: ${fmt.usd(data.volume24h)}`;
}
// 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 : ['Keine Daten'], datasets: [{ data: pData.length ? pData : [1],
backgroundColor: ['#1652F0', '#12D48A', '#7c5cff', '#ff9d4c', '#F6465D', '#5b9dff'],
borderWidth: 0 }] },
options: { responsive: true, maintainAspectRatio: false, cutout: '70%',
plugins: { legend: { position: 'bottom', labels: { color: cc.text, padding: 16, font: { family: "'Manrope'", 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) : ['Keine Daten'];
const tData = Object.values(tierData).length ? Object.values(tierData) : [1];
tierChart = new Chart(document.getElementById('tierChart'), {
type: 'bar',
data: { labels: tLabels, datasets: [{ label: 'Trader', data: tData,
backgroundColor: '#1652F0', borderRadius: 6, barThickness: 32 }] },
options: { responsive: true, maintainAspectRatio: false,
scales: { x: { grid: { display: false }, ticks: { color: cc.text, font: { family: "'Manrope'" } } },
y: { grid: { color: cc.grid }, ticks: { color: cc.text, font: { family: "'Manrope'" } } } },
plugins: { legend: { display: false } } }
});
}
function setTraderSort(field) {
if (currentSort === field) {
sortDirection *= -1;
} else {
currentSort = field;
sortDirection = -1; // Default to descending
}
// Update tradersSort dropdown if it exists to match
const sortSelect = document.getElementById('tradersSort');
if (sortSelect) sortSelect.value = currentSort;
loadTraders();
}
async function loadTraders() {
let url = '/api/traders?skip=0&take=100';
if (currentPlatform !== 'All') url += `&platform=${currentPlatform}`;
const hcCheckbox = document.getElementById('chk-highly-copyable');
if (hcCheckbox && hcCheckbox.checked) {
url += `&highlyCopyable=true`;
}
const filterTrait = document.getElementById('filterTrait');
if (filterTrait && filterTrait.value) {
url += `&trait=${encodeURIComponent(filterTrait.value)}`;
}
let data = await api(url);
const tbody = document.getElementById('allTradersBody');
if (!data || !data.length) { tbody.innerHTML = ' '; return; }
const sortSelect = document.getElementById('tradersSort');
if (sortSelect && sortSelect.value !== currentSort) currentSort = sortSelect.value;
const filterWinrateMin = parseFloat(document.getElementById('filterWinrateMin')?.value);
const filterCopyabilityMin = parseFloat(document.getElementById('filterCopyabilityMin')?.value);
// Filtering
if (!isNaN(filterWinrateMin)) data = data.filter(t => t.winRate >= filterWinrateMin);
if (!isNaN(filterCopyabilityMin)) data = data.filter(t => t.copytradingCopyabilityScore >= filterCopyabilityMin);
// Sorting
data.sort((a, b) => {
let valA, valB;
if (currentSort === 'score') { valA = a.combinedScore; valB = b.combinedScore; }
else if (currentSort === 'quality') { valA = a.copytradingQualityScore || 0; valB = b.copytradingQualityScore || 0; }
else if (currentSort === 'copyability') { valA = a.copytradingCopyabilityScore || 0; valB = b.copytradingCopyabilityScore || 0; }
else if (currentSort === 'winrate') { valA = a.winRate; valB = b.winRate; }
else if (currentSort === 'pnl') { valA = a.totalPnl; valB = b.totalPnl; }
else if (currentSort === 'trades') { valA = a.trades30d; valB = b.trades30d; }
else if (currentSort === 'name') { return a.displayName.localeCompare(b.displayName) * sortDirection; }
else if (currentSort === 'platform') { return a.platform.localeCompare(b.platform) * sortDirection; }
else { valA = a.combinedScore; valB = b.combinedScore; }
return (valA < valB ? -1 : valA > valB ? 1 : 0) * sortDirection;
});
tbody.innerHTML = data.map((t, i) => `
${i + 1}
${t.displayName} ${t.isSuspectedBot ? ' 🤖' : ''}
${t.platform}
${Number(t.combinedScore).toFixed(1)}
${Number(t.copytradingQualityScore || 0).toFixed(1)}
${Number(t.copytradingCopyabilityScore || 0).toFixed(1)}
${fmt.pct(t.winRate)}
${fmt.pnl(t.totalPnl)}
${t.trades30d} | ${t.totalTrades}
${t.strategy || '—'}
${t.traits ? '' + t.traits.map(tr => {
const rawKey = tr.trait || tr;
const name = getTraitDisplayName(rawKey);
const desc = getTraitDescription(rawKey);
return `${name} `;
}).join('') + '
' : ''}
`).join('');
}
async function loadWatchlist() {
const tbody = document.getElementById('watchlistBody');
const data = await api(`/api/watchlist`);
if (!Array.isArray(data)) {
// api() returns null on HTTP errors — show it instead of a silently empty page.
tbody.innerHTML = '⚠ Failed to load watchlist (see browser console / server log). ';
return;
}
tbody.innerHTML = data.map(w => `
${(w.displayName || '?').substring(0, 2).toUpperCase()}
${w.displayName || '?'}
${(w.platformUserId || '').substring(0, 8)}...
${w.platform ?? 'Unknown'}
${Number(w.copytradingScore || 0).toFixed(1)}
${fmt.pct(w.winRate)}
${fmt.pnl(w.totalPnl)}
${w.label || ''}
${new Date(w.createdAt).toLocaleDateString()}
View
Remove
`).join('') || 'Your watchlist is empty. ';
}
async function removeFromWatchlist(id) {
if (confirm('Remove this trader from watchlist?')) {
await api(`/api/traders/${id}/watchlist`, { method: 'DELETE' });
loadWatchlist();
}
}
// ─── 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}`;
const catSelect = document.getElementById('marketsCategory');
if (catSelect && catSelect.value !== 'All') {
url += `&category=${catSelect.value}`;
}
const searchInput = document.getElementById('marketsSearchInput');
if (searchInput && searchInput.value) {
url += `&query=${encodeURIComponent(searchInput.value)}`;
}
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) {
window.currentTraderId = id;
navigateTo('trader-detail');
const t = await api(`/api/traders/${id}`);
if (!t) return;
// Reset tabs to default (Analytics)
switchTraderTab('td-tab-analytics');
document.getElementById('td-name').textContent = `Trader-Profil: ${t.displayName}`;
document.getElementById('td-displayName').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;
const avatarEl = document.getElementById('td-avatar');
if (avatarEl) {
avatarEl.textContent = t.displayName ? t.displayName[0].toUpperCase() : 'P';
avatarEl.style.background = getAvatarBg(id);
}
const traitsContainer = document.getElementById('td-traits-container');
const traitsEl = document.getElementById('td-traits');
if (t.traits && t.traits.length > 0) {
traitsContainer.style.display = 'block';
traitsEl.innerHTML = t.traits.map(tr => {
const rawKey = tr.trait || tr;
const name = getTraitDisplayName(rawKey);
const desc = getTraitDescription(rawKey);
const scoreVal = tr.value !== undefined ? ` (Value: ${Number(tr.value).toFixed(4)})` : '';
return `${name} `;
}).join('');
} else {
traitsContainer.style.display = 'none';
traitsEl.innerHTML = '';
}
document.getElementById('td-winrate').innerHTML = fmt.pct(t.winRate);
document.getElementById('td-winrate30d').innerHTML = fmt.pct(t.winRate30d);
document.getElementById('td-pnl').innerHTML = fmt.pnl(t.totalPnl);
document.getElementById('td-pnl30d').innerHTML = fmt.pnl(t.pnL30d);
document.getElementById('td-trades').textContent = fmt.num(t.totalTrades);
document.getElementById('td-bankroll').textContent = fmt.usd(t.estimatedBankroll);
document.getElementById('td-score').textContent = Number(t.combinedScore).toFixed(1);
document.getElementById('td-quality-score').textContent = Number(t.copytradingQualityScore || 0).toFixed(1);
document.getElementById('td-copyability-score').textContent = Number(t.copytradingCopyabilityScore || 0).toFixed(1);
const winRateDec = t.winRate / 100;
const medianWin = t.medianWinReturnPct || 0;
const medianLoss = t.medianLossReturnPct || 0;
const expectancy = (winRateDec * medianWin) - ((1 - winRateDec) * Math.abs(medianLoss));
document.getElementById('td-median-win-loss').innerHTML = `+${Number(medianWin).toFixed(1)}% / ${Number(medianLoss).toFixed(1)}% `;
document.getElementById('td-profit-factor').textContent = t.profitFactor ? Number(t.profitFactor).toFixed(2) : '—';
document.getElementById('td-expectancy').innerHTML = expectancy > 0 ? `+${expectancy.toFixed(1)}% ` : `${expectancy.toFixed(1)}% `;
document.getElementById('td-ai-summary').textContent = t.aiStrategySummary || 'Bisher keine KI-Strategieanalyse durchgeführt.';
const syncBtn = document.getElementById('btn-sync-trader');
if (syncBtn) {
syncBtn.onclick = () => queueHistorySync(id);
}
const deepSyncBtn = document.getElementById('btn-deep-resync-trader');
if (deepSyncBtn) {
deepSyncBtn.onclick = () => {
if (confirm("Bist du sicher? Dies löscht alle komprimierten Trades, setzt die Positionen zurück und lädt den gesamten Verlauf neu.")) {
queueDeepResync(id);
}
};
}
const analyzeBtn = document.getElementById('btn-analyze-trader');
if (analyzeBtn) {
analyzeBtn.onclick = () => queueTraderAnalysis(id);
}
const aiBtn = document.getElementById('btn-ai-analysis');
if (window.capabilities && !window.capabilities.canControl) {
aiBtn.style.display = 'none';
} else {
aiBtn.style.display = 'inline-block';
aiBtn.onclick = () => triggerAiAnalysis(id, true);
}
const wlBtn = document.getElementById('btn-toggle-watchlist');
wlBtn.textContent = t.isOnWatchlist ? 'Watchlist (Remove)' : 'Watchlist (Add)';
wlBtn.onclick = async () => {
const method = t.isOnWatchlist ? 'DELETE' : 'POST';
await api(`/api/traders/${id}/watchlist`, { method });
viewTrader(id); // Reload to update UI
};
const openBtn = document.getElementById('btn-open-platform');
if (t.platform === 'Polymarket') {
openBtn.style.display = 'inline-block';
openBtn.textContent = 'Open on Polymarket';
openBtn.onclick = () => window.open(`https://polymarket.com/profile/${t.platformUserId}`, '_blank');
} else {
openBtn.style.display = 'none';
}
const catBody = document.getElementById('td-categoryBody');
if (t.categoryPerformances && t.categoryPerformances.length > 0) {
catBody.innerHTML = t.categoryPerformances.map(c => `
${c.category}
${c.subcategory || '-'}
${fmt.pct(c.winRate)}
${fmt.pnl(c.totalPnL)}
${fmt.usd(c.totalVolume)}
${(c.avgReturnPct || 0).toFixed(1)}%
${fmt.num(c.totalTrades)}
`).join('');
} else {
catBody.innerHTML = 'No category data available ';
}
const tbody = document.getElementById('td-tradesBody');
tbody.innerHTML = t.recentTrades.map(tr => `
${fmt.time(tr.executedAt)}
${tr.marketName || (tr.marketId.length > 20 ? tr.marketId.substring(0,20)+'...' : tr.marketId)}
${fmt.side(tr.side)}
${Number(tr.price).toFixed(2)}
${fmt.num(tr.size)}
${fmt.usd(tr.amount)}
`).join('');
loadTraderPositions(id);
}
async function loadTraderPositions(id) {
const tbody = document.getElementById('td-positionsBody');
tbody.innerHTML = 'Loading... ';
try {
const data = await api(`/api/traders/${id}/positions`);
if (!data || !data.length) {
tbody.innerHTML = ' ';
return;
}
tbody.innerHTML = data.map(p => `
${p.marketName || p.marketId}
${p.category || 'Other'} · ${p.outcomeToken || 'Unknown'}
${fmt.num(p.sharesHeld)}
${Number(p.avgCost).toFixed(2)}
${Number(p.currentPrice).toFixed(2)}
${fmt.pnl(p.realizedPnl)}
${fmt.pnl(p.unrealizedPnl)}
`).join('');
} catch (e) {
tbody.innerHTML = 'Error loading positions. ';
}
}
function switchTraderTab(tabId) {
// Hide all tabs
document.querySelectorAll('.tab-content').forEach(el => el.style.display = 'none');
// Deactivate all buttons
document.querySelectorAll('.btn-tab').forEach(el => el.classList.remove('active'));
// Show active tab
document.getElementById(tabId).style.display = 'block';
// Activate clicked button
const btn = document.querySelector(`.btn-tab[data-tab="${tabId}"]`);
if(btn) btn.classList.add('active');
}
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';
document.getElementById('md-bot-activity').textContent = m.botActivityScore ? Number(m.botActivityScore).toFixed(1) : '0';
document.getElementById('md-unique-traders').textContent = fmt.num(m.uniqueTradersCount);
document.getElementById('md-avg-trade-size').textContent = fmt.usd(m.averageTradeSize);
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);
async function triggerAiAnalysis(id, manual) {
const btn = document.getElementById('btn-ai-analysis');
const oldText = btn.textContent;
btn.textContent = 'Analyzing...';
btn.disabled = true;
try {
const res = await api(`/api/traders/${id}/ai-analysis?manual=${manual}`, { method: 'POST' });
if (res && res.summary) {
document.getElementById('td-ai-summary').textContent = res.summary;
} else {
alert('Analysis failed or returned empty.');
}
} catch (e) {
console.error(e);
alert('Analysis error: ' + e);
} finally {
btn.textContent = oldText;
btn.disabled = false;
}
}
// ─── Jobs Page ───
async function loadJobs() {
const data = await api('/api/jobs');
const tbody = document.getElementById('jobsBody');
if (!data || !data.length) {
tbody.innerHTML = ' ';
return;
}
tbody.innerHTML = data.map(j => `
${j.id}
${j.jobType}
${j.status === 'Completed' ? 'Completed ' : j.status === 'Failed' ? 'Failed ' : j.status}
${j.traderName || j.traderId}
${fmt.time(j.createdAt)}
${j.startedAt ? fmt.time(j.startedAt) : '—'}
${j.completedAt ? fmt.time(j.completedAt) : '—'}
${j.errorMessage || '—'}
View Trader
`).join('');
}
async function queueHistorySync(id) {
const res = await fetch(`/api/jobs/sync/${id}`, { method: 'POST' });
if (res.ok) {
alert('History Sync job queued successfully.');
} else {
alert('Failed to queue history sync.');
}
}
async function queueDeepResync(id) {
const res = await fetch(`/api/jobs/deep-resync/${id}`, { method: 'POST' });
if (res.ok) {
alert('Deep Resync job queued successfully.');
} else {
alert('Failed to queue deep resync.');
}
}
async function queueTraderAnalysis(id) {
const res = await fetch(`/api/jobs/analyze/${id}`, { method: 'POST' });
if (res.ok) {
alert('Trader Analysis job queued successfully.');
} else {
alert('Failed to queue trader analysis.');
}
}
async function queueBacklogAnalysis() {
const res = await fetch(`/api/jobs/analyze-backlog?take=50`, { method: 'POST' });
if (res.ok) {
const data = await res.json();
alert(`Successfully queued ${data.queued} traders for analysis from backlog.`);
loadJobs();
} else {
alert('Failed to queue backlog analysis.');
}
}
// Initialize Dashboard
document.addEventListener('DOMContentLoaded', async () => {
// Load capabilities first
try {
const caps = await api('/api/capabilities');
if (caps) {
window.capabilities = caps;
if (!caps.canControl) {
// Hide control-mode guarded elements
const jobsNav = document.getElementById('nav-jobs-container');
if (jobsNav) jobsNav.style.display = 'none';
const addTraderPanel = document.getElementById('control-add-trader-panel');
if (addTraderPanel) addTraderPanel.style.display = 'none';
const traderActions = document.getElementById('control-trader-actions');
if (traderActions) traderActions.style.display = 'none';
}
}
} catch (e) {
console.error('Failed to load capabilities', e);
}
// Load traits for filter
try {
const traits = await api('/api/traders/traits');
if (traits && Array.isArray(traits)) {
const filterTrait = document.getElementById('filterTrait');
if (filterTrait) {
traits.forEach(t => {
const opt = document.createElement('option');
opt.value = t;
opt.textContent = getTraitDisplayName(t);
filterTrait.appendChild(opt);
});
}
}
} catch (e) {
console.error('Failed to load traits', e);
}
loadDashboard();
});