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 ( +
+
Loading Kalbot...
+
+ ); + } + + const market = data?.market; + const paper = data?.paper; + const strategies = data?.strategies || []; + + return ( +
+ {/* Header */} +
+
+

Kalbot

+
+ + + {data?.lastUpdate ? 'Live' : 'Offline'} + +
+
+
+ +
+ {/* 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 ( +
+

{label}

+

{value}

+
+ ); +} + +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`; +}