Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 58 additions & 32 deletions dashboard/src/app/(dashboard)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,27 @@ import { getAppDisplayName } from '@/lib/branding';
import Link from 'next/link';

const NAV_ITEMS = [
{ label: 'OVERVIEW', href: '/overview' },
{ label: 'CRYPTO', href: '/crypto' },
{ label: 'SPORTS', href: '/sports' },
{ label: 'POSITIONS', href: '/positions' },
{ label: 'SYSTEM', href: '/system' },
{ num: '01', label: 'Overview', href: '/overview' },
{ num: '02', label: 'Crypto', href: '/crypto' },
{ num: '03', label: 'Sports', href: '/sports' },
{ num: '04', label: 'Positions', href: '/positions' },
{ num: '05', label: 'System', href: '/system' },
] as const;

// Mini service health sidebar component (server-rendered, static labels only
// — real-time dots are driven by client state on each page)
const SIDEBAR_SERVICES = ['redis', 'ingestion', 'signal-core', 'execution', 'settlement'];

function SidebarHealthPlaceholder() {
const services = ['redis', 'ingestion', 'signal-core', 'execution', 'settlement'];
return (
<div className="px-3 py-2 border-t border-bb-border">
<div className="text-[10px] uppercase tracking-wider text-bb-dim mb-1.5">Services</div>
<div className="flex flex-col gap-1">
{services.map((s) => (
<div key={s} className="flex items-center gap-1">
<span className="w-1.5 h-1.5 bg-bb-muted" />
<span className="text-[10px] text-bb-dim uppercase">{s}</span>
<div className="px-4 py-4 border-t border-bb-border">
<div className="flex items-baseline gap-1.5 mb-2.5">
<span className="display text-bb-dim text-[12px] leading-none">§</span>
<span className="text-[9px] uppercase tracking-wider3 text-bb-dim">Services</span>
</div>
<div className="flex flex-col gap-1.5">
{SIDEBAR_SERVICES.map((s) => (
<div key={s} className="flex items-center gap-2">
<span className="w-1 h-1 bg-bb-muted" />
<span className="text-[10px] text-bb-dim uppercase tracking-wider">{s}</span>
</div>
))}
</div>
Expand All @@ -33,42 +35,66 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
const appName = getAppDisplayName();
return (
<div className="flex h-screen overflow-hidden">
{/* Left sidebar */}
<aside className="w-48 flex flex-col border-r border-bb-border bg-bb-bg shrink-0">
{/* Top bar branding + logout */}
<div className="px-3 py-3 border-b border-bb-border flex items-start justify-between gap-2">
<div className="min-w-0">
<div className="text-[11px] font-bold text-bb-orange tracking-wider">{appName}</div>
<div className="text-[10px] text-bb-dim mt-0.5">Polymarket</div>
<aside className="w-56 flex flex-col border-r border-bb-border bg-bb-panel/40 shrink-0 backdrop-blur-sm">
<div className="px-4 pt-5 pb-4 border-b border-bb-border">
<div className="flex items-start justify-between gap-2">
<div className="min-w-0">
<div className="flex items-baseline gap-1.5">
<span className="display text-bb-orange text-[14px] leading-none">№</span>
<span className="text-[9px] uppercase tracking-wider3 text-bb-dim">
Edition I
</span>
</div>
<div className="display text-bb-paper text-[26px] leading-[0.95] mt-1.5 -ml-0.5">
{appName}
</div>
<div className="flex items-center gap-1.5 mt-2">
<span className="text-[9px] uppercase tracking-wider3 text-bb-dim">
Polymarket
</span>
<span className="w-1 h-1 bg-bb-muted" />
<span className="text-[9px] uppercase tracking-wider3 text-bb-dim">
Quant Lab
</span>
</div>
</div>
<LogoutButton className="shrink-0 px-1.5 py-1 text-[9px] uppercase tracking-wider3 text-bb-dim hover:text-bb-red transition-colors" />
</div>
<LogoutButton className="shrink-0 px-2 py-1 text-[10px] uppercase tracking-wider text-bb-dim hover:text-bb-red border border-transparent hover:border-bb-border disabled:opacity-50" />
</div>

{/* Navigation */}
<nav className="flex-1 py-2">
<nav className="flex-1 py-4">
{NAV_ITEMS.map((item) => (
<Link
key={item.href}
href={item.href}
className="
block px-3 py-1.5
text-[10px] uppercase tracking-wider text-bb-dim
hover:text-bb-text hover:bg-bb-border/30
transition-colors
group block px-4 py-2
border-l-2 border-transparent
hover:border-bb-orange
transition-colors
"
>
{item.label}
<div className="flex items-baseline gap-2.5">
<span className="display text-bb-dim text-[11px] group-hover:text-bb-orange transition-colors leading-none">
{item.num}
</span>
<span className="text-[11px] uppercase tracking-wider2 text-bb-paper group-hover:text-bb-paper transition-colors">
{item.label}
</span>
</div>
</Link>
))}
</nav>

{/* Bottom: service health placeholders */}
<SidebarHealthPlaceholder />

<div className="px-4 py-3 border-t border-bb-border">
<div className="text-[8px] uppercase tracking-wider3 text-bb-muted leading-relaxed">
All trading is paper-mode by default. Live execution requires explicit kill-switch enable.
</div>
</div>
</aside>

{/* Main content area */}
<main className="flex-1 overflow-auto bg-bb-bg">
{children}
</main>
Expand Down
147 changes: 75 additions & 72 deletions dashboard/src/app/(dashboard)/overview/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,17 @@ const DEFAULT_SERVICES: ServiceHealth = {
'btc-5m-momentum': { status: 'down', metric: '' },
};

function formatNow(): string {
return new Date().toLocaleString([], {
weekday: 'short',
day: '2-digit',
month: 'short',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
});
}

/* ─── Overview Page ───────────────────────────────────────────────────────── */

export default function OverviewPage() {
Expand All @@ -53,56 +64,41 @@ export default function OverviewPage() {
const [connected, setConnected] = useState(false);
const [stale, setStale] = useState(false);
const [selectedMarket, setSelectedMarket] = useState<string | null>(null);
const [now, setNow] = useState<string>(formatNow());
const backfilled = useRef(false);

useEffect(() => {
const es = new EventSource('/api/stream');
const t = setInterval(() => setNow(formatNow()), 30_000);
return () => clearInterval(t);
}, []);

es.onopen = () => {
setConnected(true);
setStale(false);
};
useEffect(() => {
const es = new EventSource('/api/stream');

es.onerror = () => {
setConnected(false);
setStale(true);
};
es.onopen = () => { setConnected(true); setStale(false); };
es.onerror = () => { setConnected(false); setStale(true); };

// Named event listeners using the new SSE format
es.addEventListener('stats_tick', (e: MessageEvent) => {
try {
const data = JSON.parse(e.data);
if (data.stale) {
setStale(true);
return;
}
if (data.stale) { setStale(true); return; }
setStale(false);

// Update core state
if (data.balance !== undefined) {
setCore({
balance: data.balance ?? 0,
killSwitches: data.killSwitches ?? {},
configOverrides: data.configOverrides ?? { btc5mMaxPosition: null, btc5mMomentumMaxBet: null, maxSlippageBps: null },
});
}

// Update dashboard stats
if (data.stats) {
setStats(data.stats);
}
} catch {
// Ignore parse errors
}
if (data.stats) setStats(data.stats);
} catch { /* noop */ }
});

es.addEventListener('trade_event', (e: MessageEvent) => {
try {
const trade: TradeEvent = JSON.parse(e.data);
setTrades((prev) => [trade, ...prev].slice(0, 100));
} catch {
// Ignore parse errors
}
} catch { /* noop */ }
});

es.addEventListener('trade_backfill', (e: MessageEvent) => {
Expand All @@ -113,36 +109,26 @@ export default function OverviewPage() {
setTrades(data);
backfilled.current = true;
}
} catch {
// Ignore parse errors
}
} catch { /* noop */ }
});

es.addEventListener('market_snapshot', (e: MessageEvent) => {
try {
const data = JSON.parse(e.data);
if (data.markets) {
setMarkets(data.markets);
// Auto-select first market (functional setter avoids stale closure)
setSelectedMarket((prev) => prev ?? data.markets[0].id);
}
} catch {
// Ignore parse errors
}
} catch { /* noop */ }
});

es.addEventListener('service_health', (e: MessageEvent) => {
try {
const data = JSON.parse(e.data);
if (data.services) {
setServices(data.services);
}
} catch {
// Ignore parse errors
}
if (data.services) setServices(data.services);
} catch { /* noop */ }
});

// Also listen for legacy unnamed messages for backwards compatibility
es.onmessage = (event: MessageEvent) => {
try {
const msg = JSON.parse(event.data);
Expand All @@ -154,25 +140,18 @@ export default function OverviewPage() {
if (msg.data.stats) setStats(msg.data.stats);
if (msg.data.services) setServices(msg.data.services);
if (msg.data.markets) setMarkets(msg.data.markets);
// Backfill trades from legacy format
if (msg.data.trades && !backfilled.current) {
setTrades(msg.data.trades);
backfilled.current = true;
}
// Extract core from legacy DashboardState
if (msg.data.stats?.balance !== undefined) {
setCore((prev) => ({
...prev,
balance: msg.data.stats.balance,
}));
setCore((prev) => ({ ...prev, balance: msg.data.stats.balance }));
}
}
} else if (msg.type === 'trade' && msg.data) {
setTrades((prev) => [msg.data, ...prev].slice(0, 100));
}
} catch {
// Ignore
}
} catch { /* noop */ }
};

return () => es.close();
Expand All @@ -182,46 +161,70 @@ export default function OverviewPage() {
setSelectedMarket(id);
}, []);

const upServices = Object.values(services).filter((s) => s.status === 'up').length;
const totalServices = Object.values(services).length;

return (
<div className="flex flex-col gap-1 p-1 h-full">
{/* Connection status bar */}
<div className="flex items-center justify-between px-2 py-0.5 border-b border-bb-border">
<div className="flex items-center gap-3">
<StatusDot status={connected ? 'up' : 'down'} label={connected ? 'CONNECTED' : 'DISCONNECTED'} />
<div className="flex flex-col gap-3 p-4 h-full">
{/* Editorial masthead */}
<header className="flex items-end justify-between border-b-2 border-bb-paper pb-2">
<div className="flex items-baseline gap-3">
<span className="display text-bb-orange text-[28px] leading-none">№</span>
<div>
<div className="text-[9px] uppercase tracking-wider3 text-bb-dim leading-none mb-1">
The Quant Lab Daily — Vol I, Edition I
</div>
<h1 className="display-roman text-bb-paper text-[28px] leading-[0.95]">
Overview <span className="display text-bb-dim italic">— live tape</span>
</h1>
</div>
</div>
<div className="flex items-center gap-4 pb-1">
<span className="text-[9px] uppercase tracking-wider3 text-bb-dim num">{now}</span>
<span className="w-px h-4 bg-bb-muted" />
<StatusDot
status={connected ? 'up' : 'down'}
label={connected ? 'Connected' : 'Disconnected'}
/>
{stale && (
<span className="text-[10px] text-bb-yellow uppercase">DATA STALE</span>
<span className="text-[9px] uppercase tracking-wider3 text-bb-yellow flex items-center gap-1.5">
<span className="w-1.5 h-1.5 bg-bb-yellow bb-blink" />
Data stale
</span>
)}
<span className="w-px h-4 bg-bb-muted" />
<span className="text-[9px] uppercase tracking-wider3 text-bb-dim num">
{upServices}<span className="text-bb-muted">/{totalServices}</span> services
</span>
</div>
<div className="flex items-center gap-2">
{Object.entries(services).slice(0, 5).map(([name, { status }]) => (
<StatusDot
key={name}
status={status === 'up' ? 'up' : 'down'}
label={name}
/>
))}
</div>
</div>
</header>

{/* Top row: Stats strip */}
{/* Hero stats strip */}
<StatsStrip core={core} stats={stats} />

{/* Middle row: Equity curve (60%) + Trade feed (40%) */}
<div className="flex gap-1 flex-1 min-h-0">
<div className="w-[60%]">
{/* Middle: Equity curve + Trade feed */}
<div className="flex gap-3 flex-1 min-h-[320px]">
<div className="w-[62%] flex flex-col">
<EquityCurve trades={trades} />
</div>
<div className="w-[40%] flex flex-col min-h-0">
<div className="w-[38%] flex flex-col min-h-0">
<TradeFeed trades={trades} />
</div>
</div>

{/* Bottom row: Arb markets table */}
{/* Bottom: arb markets */}
<ArbMarketsTable
markets={markets}
selectedMarket={selectedMarket ?? undefined}
onSelectMarket={handleSelectMarket}
/>

{/* Footer */}
<footer className="flex items-center justify-between text-[8px] uppercase tracking-wider3 text-bb-muted pt-1 border-t border-bb-rule">
<span>Open-source · MIT · Paper-mode default</span>
<span className="display text-bb-muted text-[12px] -mt-0.5">— fin —</span>
<span>github.com/dantraynor/algorithmic-trading-polymarket</span>
</footer>
</div>
);
}
Loading
Loading