'use client';
import { useState, useEffect } from 'react';
const GREEN = '#28CC95';
const RED = '#FF6B6B';
export default function Dashboard() {
const [data, setData] = useState(null);
const [trades, setTrades] = useState([]);
const [loading, setLoading] = useState(true);
const [tab, setTab] = useState('market');
useEffect(() => {
const fetchState = async () => {
try {
const res = await fetch('/api/state');
const json = await res.json();
setData(json);
setLoading(false);
} catch (e) {
console.error('State fetch error:', e);
}
};
const fetchTrades = async () => {
try {
const res = await fetch('/api/trades');
const json = await res.json();
setTrades(json.trades || []);
} catch (e) {
console.error('Trades fetch error:', e);
}
};
fetchState();
fetchTrades();
const interval = setInterval(fetchState, 2000);
const tradesInterval = setInterval(fetchTrades, 10000);
return () => {
clearInterval(interval);
clearInterval(tradesInterval);
};
}, []);
if (loading) {
return (
);
}
const market = data?.market;
const paper = data?.paper;
const strategies = data?.strategies || [];
return (
{/* Header */}
{/* Market Card */}
{/* Paper Stats */}
{/* Tab Bar */}
{['market', 'strategies', 'trades'].map(t => (
))}
{/* Tab Content */}
{tab === 'market' && }
{tab === 'strategies' && }
{tab === 'trades' && }
{/* Worker Uptime */}
Worker uptime: {formatUptime(data?.workerUptime)}
Updated: {data?.lastUpdate ? new Date(data.lastUpdate).toLocaleTimeString() : 'never'}
);
}
function MarketCard({ market }) {
if (!market) {
return (
No active market — waiting for next 15-min window...
);
}
const timeLeft = market.closeTime ? getTimeLeft(market.closeTime) : null;
return (
{/* Title */}
BTC Up or Down
15 minutes
{timeLeft && (
⏱ {timeLeft}
)}
₿
{/* Up */}
Up
{market.yesOdds}x
{market.yesPct}%
{/* Down */}
Down
{market.noOdds}x
{market.noPct}%
{/* Volume */}
${(market.volume || 0).toLocaleString()} vol
{market.ticker}
);
}
function PaperStats({ paper }) {
if (!paper) return null;
const pnlColor = paper.totalPnL >= 0 ? GREEN : RED;
return (
= 0 ? '+' : ''}$${paper.totalPnL}`} color={pnlColor} />
= 50 ? GREEN : RED} />
);
}
function StatBox({ label, value, color }) {
return (
);
}
function MarketDetails({ market }) {
if (!market) return No market data
;
const rows = [
['Yes Bid / Ask', `${market.yesBid || '-'}¢ / ${market.yesAsk || '-'}¢`],
['No Bid / Ask', `${market.noBid || '-'}¢ / ${market.noAsk || '-'}¢`],
['Last Price', `${market.lastPrice || '-'}¢`],
['Volume 24h', `$${(market.volume24h || 0).toLocaleString()}`],
['Open Interest', (market.openInterest || 0).toLocaleString()],
['Status', market.status || 'unknown'],
['Closes', market.closeTime ? new Date(market.closeTime).toLocaleTimeString() : '-'],
];
return (
{rows.map(([k, v], i) => (
{k}
{v}
))}
);
}
function StrategiesView({ strategies }) {
if (!strategies.length) {
return No strategies loaded
;
}
return (
{strategies.map((s, i) => (
{s.config && Object.entries(s.config).map(([k, v]) => (
{k}
{typeof v === 'number' ? v : String(v)}
))}
{s.consecutiveLosses !== undefined && (
Consecutive Losses
0 ? 'text-red-400' : 'text-gray-300'}>
{s.consecutiveLosses}
)}
{s.currentBetSize !== undefined && (
Next Bet
${s.currentBetSize}
)}
{s.paused && (
⚠️ PAUSED — max losses reached
)}
))}
);
}
function TradesView({ trades, openPositions }) {
return (
{/* Open Positions */}
{openPositions.length > 0 && (
Open Positions
{openPositions.map((t, i) => (
))}
)}
{/* Trade History */}
History ({trades.length})
{trades.length === 0 ? (
No trades yet. Strategies are watching...
) : (
trades.map((t, i) =>
)
)}
);
}
function TradeRow({ trade, isOpen }) {
const won = trade.pnl > 0;
const pnlColor = trade.pnl == null ? 'text-gray-400' : won ? 'text-green-400' : 'text-red-400';
return (
{isOpen ? (
) : (
{won ? '✅' : '❌'}
)}
{trade.side}
@ {trade.price}¢
{trade.pnl != null ? `${trade.pnl >= 0 ? '+' : ''}$${trade.pnl}` : 'open'}
{trade.strategy}
{trade.entryTime ? new Date(trade.entryTime).toLocaleTimeString() : ''}
{trade.reason && (
{trade.reason}
)}
);
}
function getTimeLeft(closeTime) {
const diff = new Date(closeTime).getTime() - Date.now();
if (diff <= 0) return 'Closing...';
const mins = Math.floor(diff / 60000);
const secs = Math.floor((diff % 60000) / 1000);
return `${mins}:${secs.toString().padStart(2, '0')}`;
}
function formatUptime(seconds) {
if (!seconds) return '0s';
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
const s = Math.floor(seconds % 60);
if (h > 0) return `${h}h ${m}m`;
if (m > 0) return `${m}m ${s}s`;
return `${s}s`;
}