mirror of
https://github.com/multipleof4/KalBot.git
synced 2026-03-16 21:41:02 +00:00
Feat: Mobile-first dashboard for market + paper trading
This commit is contained in:
360
app/dashboard/page.js
Normal file
360
app/dashboard/page.js
Normal file
@@ -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 (
|
||||||
|
<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center">
|
||||||
|
<div className="text-[#28CC95] text-lg animate-pulse">Loading Kalbot...</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const market = data?.market;
|
||||||
|
const paper = data?.paper;
|
||||||
|
const strategies = data?.strategies || [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-[#0a0a0a] text-white font-sans pb-20">
|
||||||
|
{/* Header */}
|
||||||
|
<header className="sticky top-0 z-50 bg-[#0a0a0a]/95 backdrop-blur border-b border-white/10 px-4 py-3">
|
||||||
|
<div className="flex items-center justify-between max-w-lg mx-auto">
|
||||||
|
<h1 className="text-lg font-bold" style={{ color: GREEN }}>Kalbot</h1>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className={`w-2 h-2 rounded-full ${data?.lastUpdate ? 'bg-green-400 animate-pulse' : 'bg-red-500'}`} />
|
||||||
|
<span className="text-xs text-gray-400">
|
||||||
|
{data?.lastUpdate ? 'Live' : 'Offline'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main className="max-w-lg mx-auto px-4 pt-4 space-y-4">
|
||||||
|
{/* Market Card */}
|
||||||
|
<MarketCard market={market} />
|
||||||
|
|
||||||
|
{/* Paper Stats */}
|
||||||
|
<PaperStats paper={paper} />
|
||||||
|
|
||||||
|
{/* Tab Bar */}
|
||||||
|
<div className="flex gap-1 bg-white/5 rounded-lg p-1">
|
||||||
|
{['market', 'strategies', 'trades'].map(t => (
|
||||||
|
<button
|
||||||
|
key={t}
|
||||||
|
onClick={() => setTab(t)}
|
||||||
|
className={`flex-1 py-2 px-3 rounded-md text-sm font-medium transition-all ${
|
||||||
|
tab === t ? 'bg-white/10 text-white' : 'text-gray-500 hover:text-gray-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{t.charAt(0).toUpperCase() + t.slice(1)}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tab Content */}
|
||||||
|
{tab === 'market' && <MarketDetails market={market} />}
|
||||||
|
{tab === 'strategies' && <StrategiesView strategies={strategies} />}
|
||||||
|
{tab === 'trades' && <TradesView trades={trades} openPositions={paper?.openPositions || []} />}
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{/* Worker Uptime */}
|
||||||
|
<div className="fixed bottom-0 left-0 right-0 bg-[#0a0a0a]/95 backdrop-blur border-t border-white/5 py-2 px-4">
|
||||||
|
<div className="max-w-lg mx-auto flex justify-between text-xs text-gray-600">
|
||||||
|
<span>Worker uptime: {formatUptime(data?.workerUptime)}</span>
|
||||||
|
<span>Updated: {data?.lastUpdate ? new Date(data.lastUpdate).toLocaleTimeString() : 'never'}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function MarketCard({ market }) {
|
||||||
|
if (!market) {
|
||||||
|
return (
|
||||||
|
<div className="bg-white/5 rounded-2xl p-5 border border-white/10">
|
||||||
|
<p className="text-gray-500 text-center">No active market — waiting for next 15-min window...</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeLeft = market.closeTime ? getTimeLeft(market.closeTime) : null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-white/5 rounded-2xl p-5 border border-white/10 space-y-4">
|
||||||
|
{/* Title */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h2 className="font-bold text-base">BTC Up or Down</h2>
|
||||||
|
<p className="text-xs text-gray-400">15 minutes</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{timeLeft && (
|
||||||
|
<span className="text-xs bg-white/10 px-2 py-1 rounded-full text-gray-300">
|
||||||
|
⏱ {timeLeft}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<span className="text-2xl">₿</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Up */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm font-medium">Up</span>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<span className="text-sm text-gray-400">{market.yesOdds}x</span>
|
||||||
|
<span className="text-sm font-bold px-3 py-1 rounded-full border"
|
||||||
|
style={{ borderColor: GREEN, color: GREEN }}>
|
||||||
|
{market.yesPct}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="w-full bg-white/10 rounded-full h-2">
|
||||||
|
<div className="h-2 rounded-full transition-all duration-500"
|
||||||
|
style={{ width: `${market.yesPct}%`, backgroundColor: GREEN }} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Down */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm font-medium">Down</span>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<span className="text-sm text-gray-400">{market.noOdds}x</span>
|
||||||
|
<span className="text-sm font-bold px-3 py-1 rounded-full border"
|
||||||
|
style={{ borderColor: '#4A90D9', color: '#4A90D9' }}>
|
||||||
|
{market.noPct}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="w-full bg-white/10 rounded-full h-2">
|
||||||
|
<div className="h-2 rounded-full transition-all duration-500"
|
||||||
|
style={{ width: `${market.noPct}%`, backgroundColor: '#4A90D9' }} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Volume */}
|
||||||
|
<div className="flex justify-between text-xs text-gray-500 pt-1">
|
||||||
|
<span>${(market.volume || 0).toLocaleString()} vol</span>
|
||||||
|
<span className="font-mono text-gray-600">{market.ticker}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function PaperStats({ paper }) {
|
||||||
|
if (!paper) return null;
|
||||||
|
|
||||||
|
const pnlColor = paper.totalPnL >= 0 ? GREEN : RED;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-4 gap-2">
|
||||||
|
<StatBox label="Balance" value={`$${paper.balance}`} />
|
||||||
|
<StatBox label="PnL" value={`${paper.totalPnL >= 0 ? '+' : ''}$${paper.totalPnL}`} color={pnlColor} />
|
||||||
|
<StatBox label="Win Rate" value={`${paper.winRate}%`} color={paper.winRate >= 50 ? GREEN : RED} />
|
||||||
|
<StatBox label="Trades" value={paper.totalTrades} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function StatBox({ label, value, color }) {
|
||||||
|
return (
|
||||||
|
<div className="bg-white/5 rounded-xl p-3 border border-white/5 text-center">
|
||||||
|
<p className="text-[10px] text-gray-500 uppercase tracking-wider">{label}</p>
|
||||||
|
<p className="text-sm font-bold mt-0.5" style={color ? { color } : {}}>{value}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function MarketDetails({ market }) {
|
||||||
|
if (!market) return <p className="text-gray-500 text-sm text-center py-8">No market data</p>;
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div className="bg-white/5 rounded-xl border border-white/5 overflow-hidden">
|
||||||
|
{rows.map(([k, v], i) => (
|
||||||
|
<div key={k} className={`flex justify-between px-4 py-3 ${i < rows.length - 1 ? 'border-b border-white/5' : ''}`}>
|
||||||
|
<span className="text-sm text-gray-400">{k}</span>
|
||||||
|
<span className="text-sm font-medium">{v}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function StrategiesView({ strategies }) {
|
||||||
|
if (!strategies.length) {
|
||||||
|
return <p className="text-gray-500 text-sm text-center py-8">No strategies loaded</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{strategies.map((s, i) => (
|
||||||
|
<div key={i} className="bg-white/5 rounded-xl p-4 border border-white/5">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className={`w-2 h-2 rounded-full ${s.enabled && !s.paused ? 'bg-green-400' : 'bg-red-500'}`} />
|
||||||
|
<span className="font-bold text-sm capitalize">{s.name}</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-xs px-2 py-0.5 rounded-full bg-white/10 text-gray-400">{s.mode}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1 text-xs text-gray-400">
|
||||||
|
{s.config && Object.entries(s.config).map(([k, v]) => (
|
||||||
|
<div key={k} className="flex justify-between">
|
||||||
|
<span>{k}</span>
|
||||||
|
<span className="text-gray-300">{typeof v === 'number' ? v : String(v)}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{s.consecutiveLosses !== undefined && (
|
||||||
|
<div className="flex justify-between mt-1 pt-1 border-t border-white/5">
|
||||||
|
<span>Consecutive Losses</span>
|
||||||
|
<span className={s.consecutiveLosses > 0 ? 'text-red-400' : 'text-gray-300'}>
|
||||||
|
{s.consecutiveLosses}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{s.currentBetSize !== undefined && (
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span>Next Bet</span>
|
||||||
|
<span className="text-gray-300">${s.currentBetSize}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{s.paused && (
|
||||||
|
<div className="text-red-400 font-medium mt-1">⚠️ PAUSED — max losses reached</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TradesView({ trades, openPositions }) {
|
||||||
|
return (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{/* Open Positions */}
|
||||||
|
{openPositions.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<h3 className="text-xs text-gray-500 uppercase tracking-wider mb-2">Open Positions</h3>
|
||||||
|
{openPositions.map((t, i) => (
|
||||||
|
<TradeRow key={i} trade={t} isOpen />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Trade History */}
|
||||||
|
<div>
|
||||||
|
<h3 className="text-xs text-gray-500 uppercase tracking-wider mb-2">
|
||||||
|
History ({trades.length})
|
||||||
|
</h3>
|
||||||
|
{trades.length === 0 ? (
|
||||||
|
<p className="text-gray-600 text-sm text-center py-6">No trades yet. Strategies are watching...</p>
|
||||||
|
) : (
|
||||||
|
trades.map((t, i) => <TradeRow key={i} trade={t} />)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div className="bg-white/5 rounded-lg p-3 border border-white/5 mb-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{isOpen ? (
|
||||||
|
<span className="w-2 h-2 rounded-full bg-yellow-400 animate-pulse" />
|
||||||
|
) : (
|
||||||
|
<span>{won ? '✅' : '❌'}</span>
|
||||||
|
)}
|
||||||
|
<span className="text-sm font-medium capitalize">{trade.side}</span>
|
||||||
|
<span className="text-xs text-gray-500">@ {trade.price}¢</span>
|
||||||
|
</div>
|
||||||
|
<span className={`text-sm font-bold ${pnlColor}`}>
|
||||||
|
{trade.pnl != null ? `${trade.pnl >= 0 ? '+' : ''}$${trade.pnl}` : 'open'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between mt-1">
|
||||||
|
<span className="text-[10px] text-gray-600 capitalize">{trade.strategy}</span>
|
||||||
|
<span className="text-[10px] text-gray-600">
|
||||||
|
{trade.entryTime ? new Date(trade.entryTime).toLocaleTimeString() : ''}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{trade.reason && (
|
||||||
|
<p className="text-[10px] text-gray-600 mt-1 truncate">{trade.reason}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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`;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user