diff --git a/app/dashboard/page.js b/app/dashboard/page.js
new file mode 100644
index 0000000..0e54909
--- /dev/null
+++ b/app/dashboard/page.js
@@ -0,0 +1,360 @@
+'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.name}
+
+
{s.mode}
+
+
+
+ {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`;
+}