← back to Bertha
react-dash/src/pages/Dashboard.jsx
348 lines
import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { api, post } from '../api';
import { AreaChart, Area, BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGrid, Cell, PieChart, Pie } from 'recharts';
function Stat({ label, value, color, sub, onClick }) {
return (
<div className={`card ${onClick ? 'cursor-pointer hover:border-blue-500/30' : ''}`} onClick={onClick}>
<p className="text-xs text-gray-500 uppercase tracking-wide mb-1">{label}</p>
<p className={`stat-v ${color || 'text-white'}`}>{value}</p>
{sub && <p className="text-xs text-gray-500 mt-1">{sub}</p>}
</div>
);
}
function ActionCard({ icon, title, desc, onClick, busy, color, badge }) {
return (
<button onClick={onClick} disabled={busy} className={`card text-left hover:border-${color || 'blue'}-500/40 hover:bg-${color || 'blue'}-500/5 transition-all group cursor-pointer disabled:opacity-50`}>
<div className="flex items-start gap-3">
<div className={`w-10 h-10 rounded-lg flex items-center justify-center flex-shrink-0 bg-${color || 'blue'}-500/15`}>
<svg className={`w-5 h-5 text-${color || 'blue'}-400`} fill="none" stroke="currentColor" strokeWidth="1.5" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" d={icon} /></svg>
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<p className="font-semibold text-sm group-hover:text-white transition">{title}</p>
{badge && <span className={`badge bg-${badge.color || 'blue'}`}>{badge.text}</span>}
</div>
<p className="text-xs text-gray-500 mt-0.5">{desc}</p>
</div>
{busy ? (
<div className="animate-spin w-4 h-4 border-2 border-blue-500 border-t-transparent rounded-full flex-shrink-0 mt-1" />
) : (
<svg className="w-4 h-4 text-gray-600 group-hover:text-gray-400 transition flex-shrink-0 mt-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 5l7 7-7 7" /></svg>
)}
</div>
</button>
);
}
const ChartTooltip = ({ active, payload, label, prefix = '$' }) => {
if (!active || !payload?.length) return null;
return (
<div className="bg-gray-900 border border-gray-700 rounded-lg px-3 py-2 text-xs shadow-xl">
<p className="text-gray-400 mb-1">{label}</p>
{payload.map((p, i) => (
<p key={i} style={{ color: p.color }} className="font-mono">{p.name}: {prefix}{typeof p.value === 'number' ? p.value.toFixed(2) : p.value}</p>
))}
</div>
);
};
export default function Dashboard() {
const [d, setD] = useState(null);
const [r, setR] = useState(null);
const [bankroll, setBankroll] = useState(null);
const [loading, setL] = useState(true);
const [busy, setBusy] = useState({});
const [results, setResults] = useState([]);
const nav = useNavigate();
const load = () => Promise.all([
api('/dashboard'), api('/risk'), api('/bankroll')
]).then(([dash, risk, bank]) => {
setD(dash); setR(risk?.state); setBankroll(bank); setL(false);
}).catch(() => setL(false));
useEffect(() => { load(); const i = setInterval(load, 12000); return () => clearInterval(i); }, []);
function addResult(msg, type = 'info') {
setResults(prev => [{ msg, type, ts: Date.now() }, ...prev].slice(0, 5));
}
async function runAction(key, fn) {
setBusy(b => ({ ...b, [key]: true }));
try { await fn(); load(); }
catch (e) { addResult(`Error: ${e.message}`, 'error'); }
setBusy(b => ({ ...b, [key]: false }));
}
async function ingestMarkets() {
await runAction('ingest', async () => {
const r = await post('/markets', { action: 'ingest' });
addResult(r.error ? `Ingest failed: ${r.error}` : `Ingested ${r.ingested || 0} markets from ${r.total_scanned || 0} scanned`, r.error ? 'error' : 'success');
});
}
async function fetchWeather() {
await runAction('weather', async () => {
const r = await post('/weather', { action: 'fetch_all' });
addResult(r.error ? `Weather fetch failed: ${r.error}` : `Fetched forecasts for ${r.cities_fetched || 0} cities`, r.error ? 'error' : 'success');
});
}
async function runPredictions() {
await runAction('predict', async () => {
const r = await post('/models', { action: 'run_predictions' });
addResult(r.error ? `Predictions failed: ${r.error}` : `Generated ${r.predictions_made || 0} predictions`, r.error ? 'error' : 'success');
});
}
async function runRiskScan() {
await runAction('risk', async () => {
const r = await api('/risk');
const s = r.state || {};
addResult(`Risk: $${parseFloat(s.bankroll || 50).toFixed(2)} bankroll, $${parseFloat(s.open_exposure || 0).toFixed(2)} exposure, Kill: ${s.kill_switch_active ? 'ON' : 'off'}`, 'success');
});
}
async function runFullPipeline() {
await runAction('pipeline', async () => {
addResult('Pipeline: Ingest -> Weather -> Predict...', 'info');
const m = await post('/markets', { action: 'ingest' });
addResult(`1/3 Ingested ${m.ingested || 0} markets`, m.error ? 'error' : 'info');
const w = await post('/weather', { action: 'fetch_all' });
addResult(`2/3 Weather for ${w.cities_fetched || 0} cities`, w.error ? 'error' : 'info');
const p = await post('/models', { action: 'run_predictions' });
addResult(`3/3 ${p.predictions_made || 0} predictions generated`, p.error ? 'error' : 'success');
addResult('Pipeline complete!', 'success');
});
}
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 cfg = r?.config || {};
const pnl = parseFloat(r?.daily_pnl || 0);
const hasMarkets = (d?.market_count || 0) > 0;
const hasForecasts = (d?.forecast_count || 0) > 0;
const hasPredictions = (d?.recent_predictions?.length || 0) > 0;
// Bankroll chart data
const ledgerData = (bankroll?.ledger || []).slice().reverse().map((e, i) => ({
idx: i,
time: new Date(e.ts).toLocaleDateString('en-US', { month: 'short', day: 'numeric' }),
balance: parseFloat(e.balance_after || 0),
amount: parseFloat(e.amount || 0),
event: e.event_type,
}));
// Edge distribution for predictions
const edgeData = (d?.recent_predictions || []).filter(p => p.edge != null).map(p => ({
name: (p.condition_id || '').substring(0, 8),
edge: parseFloat((p.edge * 100).toFixed(2)),
pYes: parseFloat((p.p_yes * 100).toFixed(1)),
signal: p.recommendation || 'HOLD',
}));
// Risk allocation pie
const bankrollVal = parseFloat(r?.bankroll || 50);
const exposureVal = parseFloat(r?.open_exposure || 0);
const availableVal = bankrollVal - exposureVal;
const riskPie = [
{ name: 'Available', value: Math.max(0, availableVal), fill: '#10b981' },
{ name: 'Exposed', value: exposureVal, fill: '#3b82f6' },
];
return (
<div className="space-y-6">
<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 ${cfg.paper_mode !== false ? 'bg-yellow' : 'bg-green'}`}>{cfg.paper_mode !== false ? 'PAPER MODE' : 'LIVE MODE'}</span>
{r?.kill_switch_active && <span className="badge bg-red">KILL SWITCH</span>}
<span className={`w-2.5 h-2.5 rounded-full ${r?.kill_switch_active ? 'bg-red-500' : 'bg-green-500 pulse-g'}`} />
</div>
</div>
{/* Top Stats */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<Stat label="Bankroll" value={`$${bankrollVal.toFixed(2)}`} color="text-green-400" sub="Starting: $50.00" onClick={() => nav('/bankroll')} />
<Stat label="Daily P&L" value={`$${pnl.toFixed(2)}`} color={pnl >= 0 ? 'text-green-400' : 'text-red-400'} sub={`Limit: -$${cfg.max_daily_loss_usd || 5}`} />
<Stat label="Open Exposure" value={`$${exposureVal.toFixed(2)}`} color="text-blue-400" sub={`Max: $${cfg.max_open_exposure_usd || 5}`} />
<Stat label="Max Drawdown" value={`${parseFloat(r?.max_drawdown_pct || 0).toFixed(1)}%`} color="text-purple-400" sub={`Limit: ${cfg.max_drawdown_pct || 20}%`} />
</div>
{/* Charts Row */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
{/* Bankroll Chart */}
<div className="card lg:col-span-2">
<h2 className="font-semibold mb-3">Bankroll History</h2>
{ledgerData.length > 1 ? (
<ResponsiveContainer width="100%" height={200}>
<AreaChart data={ledgerData}>
<defs>
<linearGradient id="bgGreen" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#10b981" stopOpacity={0.3} />
<stop offset="95%" stopColor="#10b981" stopOpacity={0} />
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" stroke="#1f2937" />
<XAxis dataKey="time" tick={{ fill: '#6b7280', fontSize: 11 }} axisLine={false} tickLine={false} />
<YAxis tick={{ fill: '#6b7280', fontSize: 11 }} axisLine={false} tickLine={false} domain={['dataMin - 2', 'dataMax + 2']} tickFormatter={v => `$${v}`} />
<Tooltip content={<ChartTooltip />} />
<Area type="monotone" dataKey="balance" stroke="#10b981" strokeWidth={2} fill="url(#bgGreen)" name="Balance" />
</AreaChart>
</ResponsiveContainer>
) : <p className="text-gray-500 text-sm text-center py-12">Chart appears after trades begin</p>}
</div>
{/* Risk Allocation Pie */}
<div className="card">
<h2 className="font-semibold mb-3">Capital Allocation</h2>
<ResponsiveContainer width="100%" height={200}>
<PieChart>
<Pie data={riskPie} cx="50%" cy="50%" innerRadius={50} outerRadius={80} paddingAngle={4} dataKey="value">
{riskPie.map((entry, i) => <Cell key={i} fill={entry.fill} />)}
</Pie>
<Tooltip content={<ChartTooltip />} />
</PieChart>
</ResponsiveContainer>
<div className="flex justify-center gap-4 text-xs mt-2">
<span className="flex items-center gap-1.5"><span className="w-2.5 h-2.5 rounded-full bg-green-500" />Available ${availableVal.toFixed(2)}</span>
<span className="flex items-center gap-1.5"><span className="w-2.5 h-2.5 rounded-full bg-blue-500" />Exposed ${exposureVal.toFixed(2)}</span>
</div>
</div>
</div>
{/* Edge Distribution */}
{edgeData.length > 0 && (
<div className="card">
<h2 className="font-semibold mb-3">Prediction Edge Distribution</h2>
<ResponsiveContainer width="100%" height={180}>
<BarChart data={edgeData} barSize={24}>
<CartesianGrid strokeDasharray="3 3" stroke="#1f2937" />
<XAxis dataKey="name" tick={{ fill: '#6b7280', fontSize: 10 }} axisLine={false} tickLine={false} />
<YAxis tick={{ fill: '#6b7280', fontSize: 11 }} axisLine={false} tickLine={false} tickFormatter={v => `${v}%`} />
<Tooltip content={<ChartTooltip prefix="" />} />
<Bar dataKey="edge" name="Edge %">
{edgeData.map((entry, i) => (
<Cell key={i} fill={entry.edge > 0 ? '#10b981' : '#ef4444'} fillOpacity={0.8} />
))}
</Bar>
</BarChart>
</ResponsiveContainer>
</div>
)}
{/* Quick Actions */}
<div className="card" style={{ background: 'linear-gradient(135deg, rgba(59,130,246,0.05), rgba(139,92,246,0.05))' }}>
<div className="flex items-center justify-between mb-4">
<div>
<h2 className="font-semibold">Quick Actions</h2>
<p className="text-xs text-gray-500 mt-0.5">Run pipeline steps or navigate to details</p>
</div>
<button onClick={runFullPipeline} disabled={busy.pipeline} className="btn btn-p text-xs flex items-center gap-2">
{busy.pipeline ? <span className="animate-spin w-3 h-3 border-2 border-white border-t-transparent rounded-full" /> : <svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 10V3L4 14h7v7l9-11h-7z" /></svg>}
Run Full Pipeline
</button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
<ActionCard icon="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6" title="Ingest Markets" desc="Scan Polymarket for weather events" onClick={ingestMarkets} busy={busy.ingest} color="blue" badge={hasMarkets ? { text: `${d.market_count} tracked`, color: 'blue' } : { text: 'Start here', color: 'yellow' }} />
<ActionCard icon="M3 15a4 4 0 004 4h9a5 5 0 10-.1-9.999 5.002 5.002 0 10-9.78 2.096A4.001 4.001 0 003 15z" title="Fetch Weather" desc="Pull latest NWS forecasts" onClick={fetchWeather} busy={busy.weather} color="green" badge={hasForecasts ? { text: `${d.forecast_count} runs`, color: 'green' } : null} />
<ActionCard icon="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" title="Run Predictions" desc="Generate P(YES) + edge" onClick={runPredictions} busy={busy.predict} color="purple" badge={hasPredictions ? { text: `${d.recent_predictions.length} active`, color: 'purple' } : null} />
<ActionCard icon="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" title="Risk Scan" desc="Check limits and kill switch" onClick={runRiskScan} busy={busy.risk} color="yellow" />
<ActionCard icon="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8V7m0 1v8m0 0v1" title="View Bankroll" desc="Balance history and P&L" onClick={() => nav('/bankroll')} color="green" />
<ActionCard icon="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9" title="Polymarket CLOB" desc="Live prices, orderbooks & trading" onClick={() => nav('/polymarket')} color="purple" badge={{ text: 'LIVE', color: 'purple' }} />
</div>
</div>
{/* Action Log */}
{results.length > 0 && (
<div className="card">
<div className="flex items-center justify-between mb-3">
<h2 className="font-semibold text-sm">Action Log</h2>
<button onClick={() => setResults([])} className="text-xs text-gray-500 hover:text-gray-300">Clear</button>
</div>
<div className="space-y-1.5">
{results.map((r, i) => (
<div key={r.ts + i} className={`flex items-center gap-2 text-xs px-3 py-2 rounded-lg ${r.type === 'error' ? 'bg-red-500/10 text-red-400' : r.type === 'success' ? 'bg-green-500/10 text-green-400' : 'bg-blue-500/10 text-blue-400'}`}>
<span className={`w-1.5 h-1.5 rounded-full flex-shrink-0 ${r.type === 'error' ? 'bg-red-500' : r.type === 'success' ? 'bg-green-500' : 'bg-blue-500'}`} />
<span className="flex-1">{r.msg}</span>
<span className="text-gray-600">{new Date(r.ts).toLocaleTimeString()}</span>
</div>
))}
</div>
</div>
)}
{/* Predictions & Trades */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
<div className="card">
<div className="flex items-center justify-between mb-3">
<h2 className="font-semibold">Recent Predictions</h2>
<button onClick={() => nav('/models')} className="text-xs text-blue-400 hover:text-blue-300">View All</button>
</div>
{d?.recent_predictions?.length > 0 ? (
<table><thead><tr><th>Market</th><th>P(Yes)</th><th>Edge</th><th>Signal</th></tr></thead><tbody>
{d.recent_predictions.slice(0, 6).map((p, i) => <tr key={i}>
<td className="max-w-[180px] truncate font-mono text-xs">{p.condition_id?.substring(0, 20)}</td>
<td className="font-mono text-green-400">{(p.p_yes * 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?.startsWith('BUY') ? 'bg-green' : 'bg-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>
<div className="card">
<div className="flex items-center justify-between mb-3">
<h2 className="font-semibold">Recent Trades</h2>
<button onClick={() => nav('/trades')} className="text-xs text-blue-400 hover:text-blue-300">View All</button>
</div>
{d?.recent_trades?.length > 0 ? (
<table><thead><tr><th>Time</th><th>Side</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 text-sm">{new Date(t.match_time).toLocaleString()}</td>
<td><span className={`badge ${t.side === 'buy' ? 'bg-green' : 'bg-red'}`}>{t.side?.toUpperCase()}</span></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 in paper mode.</p>}
</div>
</div>
{/* Bottom Stats */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="card cursor-pointer hover:border-blue-500/30 transition" onClick={() => nav('/markets')}>
<p className="text-xs text-gray-500 uppercase mb-1">Markets Tracked</p>
<p className="stat-v text-blue-400">{d?.market_count || 0}</p>
<p className="text-xs text-gray-500 mt-1">{d?.weather_market_count || 0} weather</p>
</div>
<div className="card cursor-pointer hover:border-purple-500/30 transition" onClick={() => nav('/weather')}>
<p className="text-xs text-gray-500 uppercase mb-1">Forecast Runs</p>
<p className="stat-v text-purple-400">{d?.forecast_count || 0}</p>
</div>
<div className="card cursor-pointer hover:border-green-500/30 transition" onClick={() => nav('/risk')}>
<p className="text-xs text-gray-500 uppercase mb-1">Risk Config</p>
<div className="space-y-1 text-sm">
<div className="flex justify-between"><span className="text-gray-500">Max Bet</span><span>${cfg.max_bet_usd || 0.50}</span></div>
<div className="flex justify-between"><span className="text-gray-500">Min Edge</span><span>{((cfg.min_edge_threshold || 0.03) * 100).toFixed(0)}%</span></div>
<div className="flex justify-between"><span className="text-gray-500">Kelly</span><span>{cfg.fractional_kelly || 0.10}x</span></div>
</div>
</div>
</div>
{/* Alerts */}
<div className="card">
<h2 className="font-semibold mb-3">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' ? 'bg-red' : a.severity === 'warning' ? 'bg-yellow' : 'bg-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 healthy.</p>}
</div>
</div>
);
}