632 lines
27 KiB
JavaScript
632 lines
27 KiB
JavaScript
// 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();
|
|
});
|
|
});
|
|
|
|
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) => `
|
|
<tr>
|
|
<td>${i + 1}</td>
|
|
<td><strong>${t.displayName}</strong></td>
|
|
<td>${t.platform}</td>
|
|
<td>${Number(t.combinedScore).toFixed(1)}</td>
|
|
<td>${fmt.tier(t.tier)}</td>
|
|
<td><button class="btn-sm" onclick="viewTrader(${t.id})">View</button></td>
|
|
</tr>
|
|
`).join('') || '<tr><td colspan="6">No traders found</td></tr>';
|
|
|
|
mBody.innerHTML = data.markets.map(m => `
|
|
<tr>
|
|
<td>${m.platform}</td>
|
|
<td title="${m.question}">${m.question.substring(0, 50)}...</td>
|
|
<td>${fmt.usd(m.volume)}</td>
|
|
<td>${m.isResolved ? 'Resolved' : 'Active'}</td>
|
|
<td><button class="btn-sm" onclick="viewMarket(${m.id})">View</button></td>
|
|
</tr>
|
|
`).join('') || '<tr><td colspan="5">No markets found</td></tr>';
|
|
}
|
|
|
|
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';
|
|
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();
|
|
}
|
|
|
|
// ─── 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%';
|
|
const n = Number(v);
|
|
const color = n > 55 ? 'var(--pnl-positive)' : n < 45 ? 'var(--pnl-negative)' : 'var(--text)';
|
|
return `<span style="color:${color};font-weight:${n>55||n<45?'600':'normal'};">${n.toFixed(1)}%</span>`;
|
|
},
|
|
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 `<span class="tier-badge ${cls}">${t}</span>`; },
|
|
side: s => { if (!s) return '—'; return `<span class="side-${s.toString().toLowerCase()}">${s}</span>`; },
|
|
pnl: v => { if (v === null || v === undefined) return '$0'; const n = Number(v); return `<span class="${n >= 0 ? 'pnl-positive' : 'pnl-negative'}">${fmt.usd(Math.abs(n))}${n >= 0 ? ' ▲' : ' ▼'}</span>`; }
|
|
};
|
|
|
|
// ─── 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) => `
|
|
<tr>
|
|
<td onclick="viewTrader(${t.id})" style="cursor:pointer">${i + 1}</td>
|
|
<td><strong><a href="#" onclick="viewTrader(${t.id}); return false;" style="color:var(--primary);text-decoration:none;">${t.displayName}</a></strong></td>
|
|
<td onclick="viewTrader(${t.id})" style="cursor:pointer">${t.platform}</td>
|
|
<td onclick="viewTrader(${t.id})" style="cursor:pointer"><strong>${Number(t.combinedScore).toFixed(1)}</strong></td>
|
|
<td onclick="viewTrader(${t.id})" style="cursor:pointer">${fmt.pct(t.winRate)}</td>
|
|
<td onclick="viewTrader(${t.id})" style="cursor:pointer">${fmt.pnl(t.totalPnl)}</td>
|
|
<td onclick="viewTrader(${t.id})" style="cursor:pointer">${fmt.tier(t.tier)}</td>
|
|
<td onclick="viewTrader(${t.id})" style="cursor:pointer">${fmt.num(t.totalTrades)}</td>
|
|
</tr>
|
|
`).join('');
|
|
|
|
// Recent Trades table
|
|
const rBody = document.getElementById('recentTradesBody');
|
|
rBody.innerHTML = data.recentTrades.map(t => `
|
|
<tr>
|
|
<td>${fmt.time(t.executedAt)}</td>
|
|
<td onclick="viewTrader(${t.traderId})" style="cursor:pointer; color:var(--primary)">${t.traderName}</td>
|
|
<td onclick="${t.dbMarketId ? `viewMarket(${t.dbMarketId})` : `''`}" style="cursor:${t.dbMarketId ? 'pointer' : 'default'}" title="Market ID: ${t.marketId}">
|
|
${t.marketName || (t.marketId.length > 20 ? t.marketId.substring(0,20)+'...' : t.marketId)}
|
|
</td>
|
|
<td>${fmt.side(t.side)}</td>
|
|
<td>${Number(t.price).toFixed(2)}</td>
|
|
<td>${fmt.num(t.size)}</td>
|
|
<td>${fmt.usd(t.amount)}</td>
|
|
</tr>
|
|
`).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 } } }
|
|
});
|
|
}
|
|
|
|
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`;
|
|
}
|
|
|
|
let data = await api(url);
|
|
const tbody = document.getElementById('allTradersBody');
|
|
if (!data || !data.length) { tbody.innerHTML = '<tr><td colspan="11"><div class="empty-state"><p>No traders tracked yet.</p></div></td></tr>'; 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 === '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) => `
|
|
<tr>
|
|
<td>${i + 1}</td>
|
|
<td><strong><a href="#" onclick="viewTrader(${t.id}); return false;" style="color:var(--primary);text-decoration:none;">${t.displayName}</a></strong>${t.isSuspectedBot ? ' 🤖' : ''}</td>
|
|
<td>${t.platform}</td>
|
|
<td><strong>${Number(t.combinedScore).toFixed(1)}</strong></td>
|
|
<td>${Number(t.copytradingQualityScore || 0).toFixed(1)}</td>
|
|
<td>${Number(t.copytradingCopyabilityScore || 0).toFixed(1)}</td>
|
|
<td>${fmt.pct(t.winRate)}</td>
|
|
<td>${fmt.pnl(t.totalPnl)}</td>
|
|
<td>${fmt.tier(t.tier)}</td>
|
|
<td>${t.strategy}</td>
|
|
<td>
|
|
<div style="display:flex; gap:4px;">
|
|
<button class="btn-sm" onclick="viewTrader(${t.id})">Details</button>
|
|
<button class="btn-sm" onclick="queueHistorySync(${t.id})" style="background:var(--bg-input); border:1px solid var(--border); color:var(--text)">Sync</button>
|
|
<button class="btn-sm" onclick="queueTraderAnalysis(${t.id})" style="background:var(--bg-input); border:1px solid var(--border); color:var(--text)">Analyze</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
`).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 = '<div class="empty-state"><p>No alerts yet.</p></div>'; return; }
|
|
el.innerHTML = data.map(a => `
|
|
<div class="alert-item ${a.isRead ? '' : 'alert-unread'}">
|
|
<div class="alert-icon alert-severity-${a.severity}">🔔</div>
|
|
<div class="alert-content">
|
|
<div class="alert-title">${a.title}</div>
|
|
<div class="alert-message">${a.message}</div>
|
|
</div>
|
|
<div class="alert-time">${fmt.time(a.createdAt)}</div>
|
|
</div>
|
|
`).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 = '<tr><td colspan="6"><div class="empty-state"><p>No markets found.</p></div></td></tr>'; 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 => `
|
|
<tr onclick="viewMarket(${m.id})" style="cursor:pointer">
|
|
<td>${m.platform}</td>
|
|
<td title="${m.question}"><strong>${m.question.length > 60 ? m.question.substring(0, 60) + '...' : m.question}</strong></td>
|
|
<td>${fmt.usd(m.volume)}</td>
|
|
<td>${fmt.usd(m.liquidity)}</td>
|
|
<td>${m.endDate ? new Date(m.endDate).toLocaleDateString() : '—'}</td>
|
|
<td>${m.isResolved ? '<span class="side-sell">Resolved</span>' : '<span class="side-buy">Active</span>'}</td>
|
|
</tr>
|
|
`).join('');
|
|
}
|
|
|
|
async function viewTrader(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 = 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').innerHTML = 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);
|
|
document.getElementById('td-quality-score').textContent = Number(t.copytradingQualityScore || 0).toFixed(1);
|
|
document.getElementById('td-copyability-score').textContent = Number(t.copytradingCopyabilityScore || 0).toFixed(1);
|
|
document.getElementById('td-ai-summary').textContent = t.aiStrategySummary || 'Not analyzed yet.';
|
|
|
|
const refreshBtn = document.getElementById('btn-refresh-trader');
|
|
refreshBtn.onclick = () => manualUpdateTrader(id);
|
|
|
|
const forceBtn = document.getElementById('btn-force-analyze');
|
|
if(forceBtn) {
|
|
forceBtn.onclick = async () => {
|
|
forceBtn.disabled = true;
|
|
forceBtn.textContent = '...';
|
|
try {
|
|
await api(`/api/traders/${id}/force-analyze`, { method: 'POST' });
|
|
alert('Deep Analysis queued! Please wait a moment and then refresh.');
|
|
} finally {
|
|
forceBtn.disabled = false;
|
|
forceBtn.textContent = 'Recalculate';
|
|
}
|
|
};
|
|
}
|
|
|
|
const aiBtn = document.getElementById('btn-ai-analysis');
|
|
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 => `
|
|
<tr>
|
|
<td>${c.category}</td>
|
|
<td>${c.subcategory || '-'}</td>
|
|
<td>${fmt.pct(c.winRate)}</td>
|
|
<td>${fmt.pnl(c.totalPnL)}</td>
|
|
<td>${fmt.usd(c.totalVolume)}</td>
|
|
<td>${fmt.num(c.totalTrades)}</td>
|
|
</tr>
|
|
`).join('');
|
|
} else {
|
|
catBody.innerHTML = '<tr><td colspan="6" style="text-align:center; padding:16px; color:var(--text-muted)">No category data available</td></tr>';
|
|
}
|
|
|
|
const tbody = document.getElementById('td-tradesBody');
|
|
tbody.innerHTML = t.recentTrades.map(tr => `
|
|
<tr>
|
|
<td>${fmt.time(tr.executedAt)}</td>
|
|
<td onclick="${tr.dbMarketId ? `viewMarket(${tr.dbMarketId})` : `''`}" style="cursor:${tr.dbMarketId ? 'pointer' : 'default'}; color:${tr.dbMarketId ? 'var(--primary)' : 'inherit'}" title="Market ID: ${tr.marketId}">
|
|
${tr.marketName || (tr.marketId.length > 20 ? tr.marketId.substring(0,20)+'...' : tr.marketId)}
|
|
</td>
|
|
<td>${fmt.side(tr.side)}</td>
|
|
<td>${Number(tr.price).toFixed(2)}</td>
|
|
<td>${fmt.num(tr.size)}</td>
|
|
<td>${fmt.usd(tr.amount)}</td>
|
|
</tr>
|
|
`).join('');
|
|
}
|
|
|
|
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';
|
|
|
|
const imgContainer = document.getElementById('md-image');
|
|
if (m.imageUrl) imgContainer.innerHTML = `<img src="${m.imageUrl}" alt="Market" style="width:100%; border-radius:8px; margin-bottom:16px;">`;
|
|
else imgContainer.innerHTML = '';
|
|
|
|
const oList = document.getElementById('md-outcomes');
|
|
oList.innerHTML = m.outcomes.map(o => `
|
|
<div class="outcome-row">
|
|
<span>${o.name}</span>
|
|
<strong>${(o.price * 100).toFixed(1)}¢</strong>
|
|
<div class="progress-bar"><div class="progress-fill" style="width:${o.price * 100}%"></div></div>
|
|
</div>
|
|
`).join('');
|
|
|
|
const tbody = document.getElementById('md-tradesBody');
|
|
tbody.innerHTML = m.recentTrades.map(tr => `
|
|
<tr>
|
|
<td>${fmt.time(tr.executedAt)}</td>
|
|
<td onclick="viewTrader(${tr.traderId})" style="cursor:pointer; color:var(--primary)">${tr.traderName}</td>
|
|
<td>${fmt.side(tr.side)}</td>
|
|
<td>${Number(tr.price).toFixed(2)}</td>
|
|
<td>${fmt.num(tr.size)}</td>
|
|
<td>${fmt.usd(tr.amount)}</td>
|
|
</tr>
|
|
`).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 = '<tr><td colspan="9"><div class="empty-state"><p>No jobs found.</p></div></td></tr>';
|
|
return;
|
|
}
|
|
|
|
tbody.innerHTML = data.map(j => `
|
|
<tr>
|
|
<td>${j.id}</td>
|
|
<td><strong>${j.jobType}</strong></td>
|
|
<td>${j.status === 'Completed' ? '<span style="color:var(--pnl-positive)">Completed</span>' : j.status === 'Failed' ? '<span style="color:var(--pnl-negative)">Failed</span>' : j.status}</td>
|
|
<td><a href="#" onclick="viewTrader(${j.traderId}); return false;" style="color:var(--primary);text-decoration:none;">${j.traderName || j.traderId}</a></td>
|
|
<td>${fmt.time(j.createdAt)}</td>
|
|
<td>${j.startedAt ? fmt.time(j.startedAt) : '—'}</td>
|
|
<td>${j.completedAt ? fmt.time(j.completedAt) : '—'}</td>
|
|
<td style="color:var(--pnl-negative); max-width:200px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;" title="${j.errorMessage || ''}">${j.errorMessage || '—'}</td>
|
|
<td><button class="btn-sm" onclick="viewTrader(${j.traderId})">View Trader</button></td>
|
|
</tr>
|
|
`).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 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
|
|
loadDashboard();
|