← back to Ken
src/app/dashboard/page.js
218 lines
'use client';
import { useState, useEffect } from 'react';
function StatCard({ label, value, sub, color, icon }) {
return (
<div className="card">
<div className="flex items-start justify-between">
<div>
<p className="text-xs text-gray-500 uppercase tracking-wide mb-1">{label}</p>
<p className={`stat-value ${color || 'text-white'}`}>{value}</p>
{sub && <p className="text-xs text-gray-500 mt-1">{sub}</p>}
</div>
{icon && <div className="text-gray-600">{icon}</div>}
</div>
</div>
);
}
export default function DashboardOverview() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function load() {
try {
const [dash, risk, bankroll] = await Promise.all([
fetch('/api/dashboard').then(r => r.json()),
fetch('/api/risk').then(r => r.json()),
fetch('/api/bankroll').then(r => r.json()),
]);
setData({ dash, risk, bankroll });
} catch (err) {
console.error('Dashboard load error:', err);
}
setLoading(false);
}
load();
const interval = setInterval(load, 15000);
return () => clearInterval(interval);
}, []);
if (loading) return (
<div className="flex items-center justify-center h-64">
<div className="animate-spin w-8 h-8 border-2 border-blue-500 border-t-transparent rounded-full" />
</div>
);
const d = data?.dash || {};
const r = data?.risk?.state || {};
const config = r.config || {};
const b = data?.bankroll || {};
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold">Dashboard</h1>
<p className="text-gray-500 text-sm mt-0.5">Weather micro-betting system overview</p>
</div>
<div className="flex items-center gap-3">
<span className={`badge ${config.paper_mode !== false ? 'badge-yellow' : 'badge-green'}`}>
{config.paper_mode !== false ? 'PAPER MODE' : 'LIVE MODE'}
</span>
{r.kill_switch_active && (
<span className="badge badge-red">KILL SWITCH ON</span>
)}
<span className={`w-2.5 h-2.5 rounded-full ${r.kill_switch_active ? 'bg-red-500' : 'bg-green-500 pulse-green'}`} />
</div>
</div>
{/* Stats Grid */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<StatCard label="Bankroll" value={`$${parseFloat(r.bankroll || 50).toFixed(2)}`} color="text-green-400" sub="Starting: $50.00" />
<StatCard label="Daily P&L" value={`$${parseFloat(r.daily_pnl || 0).toFixed(2)}`} color={parseFloat(r.daily_pnl || 0) >= 0 ? 'text-green-400' : 'text-red-400'} sub={`Limit: -$${config.max_daily_loss_usd || 5}`} />
<StatCard label="Open Exposure" value={`$${parseFloat(r.open_exposure || 0).toFixed(2)}`} color="text-blue-400" sub={`Max: $${config.max_open_exposure_usd || 5}`} />
<StatCard label="Max Drawdown" value={`${parseFloat(r.max_drawdown_pct || 0).toFixed(1)}%`} color="text-purple-400" sub={`Limit: ${config.max_drawdown_pct || 20}%`} />
</div>
{/* Trading + Markets Row */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{/* Recent Predictions */}
<div className="card">
<h2 className="font-semibold mb-3 flex items-center gap-2">
<svg className="w-4 h-4 text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z" />
</svg>
Recent Predictions
</h2>
{d.recent_predictions?.length > 0 ? (
<table>
<thead>
<tr>
<th>Market</th>
<th>P(Yes)</th>
<th>Mid</th>
<th>Edge</th>
<th>Signal</th>
</tr>
</thead>
<tbody>
{d.recent_predictions.map((p, i) => (
<tr key={i}>
<td className="max-w-[180px] truncate">{p.condition_id?.substring(0, 12)}...</td>
<td className="font-mono">{(p.p_yes * 100).toFixed(1)}%</td>
<td className="font-mono">{p.market_mid ? (p.market_mid * 100).toFixed(1) + '%' : '-'}</td>
<td className={`font-mono ${p.edge > 0 ? 'text-green-400' : 'text-red-400'}`}>
{p.edge ? (p.edge * 100).toFixed(1) + '%' : '-'}
</td>
<td>
<span className={`badge ${p.recommendation === 'BUY_YES' || p.recommendation === 'BUY_NO' ? 'badge-green' : 'badge-yellow'}`}>
{p.recommendation || 'HOLD'}
</span>
</td>
</tr>
))}
</tbody>
</table>
) : (
<p className="text-gray-500 text-sm">No predictions yet. Ingest markets to begin.</p>
)}
</div>
{/* Recent Trades */}
<div className="card">
<h2 className="font-semibold mb-3 flex items-center gap-2">
<svg className="w-4 h-4 text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4" />
</svg>
Recent Trades
</h2>
{d.recent_trades?.length > 0 ? (
<table>
<thead>
<tr>
<th>Time</th>
<th>Side</th>
<th>Price</th>
<th>Size</th>
<th>P&L</th>
</tr>
</thead>
<tbody>
{d.recent_trades.map((t, i) => (
<tr key={i}>
<td className="text-gray-400">{new Date(t.match_time).toLocaleString()}</td>
<td><span className={`badge ${t.side === 'buy' ? 'badge-green' : 'badge-red'}`}>{t.side?.toUpperCase()}</span></td>
<td className="font-mono">${parseFloat(t.price).toFixed(4)}</td>
<td className="font-mono">${parseFloat(t.size_usd || 0).toFixed(2)}</td>
<td className={`font-mono ${parseFloat(t.pnl || 0) >= 0 ? 'text-green-400' : 'text-red-400'}`}>
${parseFloat(t.pnl || 0).toFixed(2)}
</td>
</tr>
))}
</tbody>
</table>
) : (
<p className="text-gray-500 text-sm">No trades yet. System is in paper mode.</p>
)}
</div>
</div>
{/* System Info Row */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="card">
<h3 className="text-xs text-gray-500 uppercase mb-2">Markets Tracked</h3>
<p className="stat-value text-blue-400">{d.market_count || 0}</p>
<p className="text-xs text-gray-500 mt-1">{d.weather_market_count || 0} weather markets</p>
</div>
<div className="card">
<h3 className="text-xs text-gray-500 uppercase mb-2">Forecast Runs</h3>
<p className="stat-value text-purple-400">{d.forecast_count || 0}</p>
<p className="text-xs text-gray-500 mt-1">NWS + GEFS sources</p>
</div>
<div className="card">
<h3 className="text-xs text-gray-500 uppercase mb-2">Risk Config</h3>
<div className="space-y-1 text-sm">
<div className="flex justify-between"><span className="text-gray-500">Max Bet</span><span>${config.max_bet_usd || 0.50}</span></div>
<div className="flex justify-between"><span className="text-gray-500">Min Edge</span><span>{((config.min_edge_threshold || 0.03) * 100).toFixed(0)}%</span></div>
<div className="flex justify-between"><span className="text-gray-500">Kelly Fraction</span><span>{config.fractional_kelly || 0.10}x</span></div>
</div>
</div>
</div>
{/* Alerts */}
<div className="card">
<h2 className="font-semibold mb-3 flex items-center gap-2">
<svg className="w-4 h-4 text-yellow-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" />
</svg>
System Alerts
</h2>
{d.recent_alerts?.length > 0 ? (
<div className="space-y-2">
{d.recent_alerts.map((a, i) => (
<div key={i} className={`flex items-start gap-3 p-3 rounded-lg ${
a.severity === 'critical' ? 'bg-red-500/10' :
a.severity === 'warning' ? 'bg-yellow-500/10' : 'bg-blue-500/10'
}`}>
<span className={`badge ${
a.severity === 'critical' ? 'badge-red' :
a.severity === 'warning' ? 'badge-yellow' : 'badge-blue'
}`}>{a.severity}</span>
<div>
<p className="text-sm">{a.message}</p>
<p className="text-xs text-gray-500 mt-0.5">{new Date(a.ts).toLocaleString()}</p>
</div>
</div>
))}
</div>
) : (
<p className="text-gray-500 text-sm">No alerts. System is healthy.</p>
)}
</div>
</div>
);
}