← back to Ken
kalshi-dash/src/Layout.jsx
414 lines
import React, { useState, useEffect, useRef } from 'react';
import { Outlet, Link, useLocation, useNavigate } from 'react-router-dom';
import { api, checkSession, logout } from './api';
import TickerTape from './components/TickerTape';
// rAF count-up tween so the $ spent value animates on change (motion graphics).
function useCountUp(target, dur = 900) {
const [val, setVal] = useState(target);
const from = useRef(target);
useEffect(() => {
const start = performance.now(), a = from.current, b = target;
let raf;
const tick = (now) => {
const t = Math.min(1, (now - start) / dur), e = 1 - Math.pow(1 - t, 3);
setVal(a + (b - a) * e);
if (t < 1) raf = requestAnimationFrame(tick); else from.current = b;
};
raf = requestAnimationFrame(tick);
return () => cancelAnimationFrame(raf);
}, [target]);
return val;
}
// Header status-bar chip for the bounded real-money run.
function LiveRunChip({ lr }) {
const spent = useCountUp(lr && lr.active ? lr.spent_today_cents : 0);
if (!lr) return null;
if (!lr.active) {
return (
<span className="lrchip idle" title="No bounded live run armed — safe/paper (market data + paper portfolios only).">
<span className="lr-dot" /> <span className="lr-live">PAPER</span>
<span className="lr-day" style={{ opacity: .8 }}>run idle</span>
</span>
);
}
const cap = lr.daily_budget_cents || 0;
const pct = cap > 0 ? Math.min(100, (lr.spent_today_cents / cap) * 100) : 0;
const off = (() => {
const [y, m, d] = String(lr.end_date).split('-').map(Number);
return new Date(y, m - 1, d).toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
})();
return (
<span className="lrchip" title={'Bounded live run — $' + (cap / 100).toFixed(0) + '/day, auto-off after ' + lr.end_date}>
<span className="lr-dot" /> <span className="lr-live">LIVE</span>
<span className="lr-pipe">·</span>
<span className="lr-day">DAY <b>{lr.day}</b>/{lr.days}</span>
<span className="lr-pipe">·</span>
<span className="lr-amt"><b>{'$' + (spent / 100).toFixed(2)}</b>/{'$' + (cap / 100).toFixed(0)}</span>
<span className="lr-bar"><i style={{ width: pct + '%' }} /></span>
<span className="lr-pipe">·</span>
<span className="lr-off">off <b>{off}</b> · {lr.days_left}d</span>
</span>
);
}
const NAV = [
{ to: '/', label: 'Dashboard', d: 'M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6' },
{ to: '/markets', label: 'Markets', d: 'M13 7h8m0 0v8m0-8l-8 8-4-4-6 6' },
{ to: '/signals', label: 'Signals', d: 'M13 10V3L4 14h7v7l9-11h-7z' },
{ to: '/intelligence', label: 'Intel Feed', d: 'M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z' },
{ to: '/trading', label: 'Trading', d: 'M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z' },
{ to: '/positions', label: 'Positions', d: 'M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z' },
{ to: '/orders', label: 'Orders', d: 'M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4' },
{ to: '/heatmap', label: 'Heatmap', d: 'M4 5a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1H5a1 1 0 01-1-1V5zm10 0a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1V5zM4 15a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1H5a1 1 0 01-1-1v-4zm10 0a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1v-4z' },
{ to: '/orbit', label: 'Orbit', d: '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-9m-9 9a9 9 0 019-9' },
{ to: '/pulse', label: 'Pulse', d: 'M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z' },
{ to: '/weather', label: 'Weather', d: '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' },
{ to: '/risk', label: 'Portfauxlios', d: 'M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4' },
{ to: '/settings', label: 'Settings', d: 'M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z M15 12a3 3 0 11-6 0 3 3 0 016 0z' },
];
function StatusDot({ active, warn }) {
const color = warn
? 'var(--orange)'
: active
? 'var(--green)'
: 'var(--red)';
return (
<span
style={{
display: 'inline-block',
width: 6,
height: 6,
borderRadius: '50%',
background: color,
flexShrink: 0,
boxShadow: `0 0 4px ${color}`,
}}
/>
);
}
export default function Layout() {
const loc = useLocation();
const nav = useNavigate();
const [h, setH] = useState(null);
const [lr, setLr] = useState(null);
const [col, setCol] = useState(false);
const [authChecked, setAuthChecked] = useState(false);
useEffect(() => {
checkSession().then(ok => {
if (!ok) { nav('/login'); return; }
setAuthChecked(true);
const load = () => {
api('/health').then(setH).catch(() => {});
api('/live-run').then(setLr).catch(() => {});
};
load();
const i = setInterval(load, 15000);
return () => clearInterval(i);
});
}, []);
if (!authChecked) {
return (
<div className="flex items-center justify-center min-h-screen" style={{ background: 'var(--bg)' }}>
<div
className="w-8 h-8 rounded-full border-2 border-t-transparent animate-spin"
style={{ borderColor: 'var(--cyan)', borderTopColor: 'transparent' }}
/>
</div>
);
}
const sidebarW = col ? 64 : 220;
return (
<div className="flex min-h-screen" style={{ background: 'var(--bg)' }}>
{/* ── Sidebar ── */}
{/* ── CRT Scan Line Overlay ── */}
<div className="scan-overlay" aria-hidden="true" />
<aside
className="sidebar-mesh"
style={{
width: sidebarW,
minWidth: sidebarW,
borderRight: '1px solid var(--border)',
display: 'flex',
flexDirection: 'column',
transition: 'width 200ms ease, min-width 200ms ease',
overflow: 'hidden',
position: 'sticky',
top: 0,
height: '100vh',
}}
>
{/* Brand */}
<div
style={{
padding: col ? '16px 12px' : '16px 18px',
borderBottom: '1px solid var(--border)',
display: 'flex',
alignItems: 'center',
gap: 12,
minHeight: 64,
flexShrink: 0,
}}
>
{/* Orange gradient K badge */}
<div
className="logo-animated"
style={{
width: 34,
height: 34,
borderRadius: 8,
background: 'linear-gradient(135deg, var(--orange), #E5690D)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0,
boxShadow: '0 2px 10px rgba(247,147,26,0.3)',
}}
>
<span style={{ color: '#0E0E10', fontWeight: 900, fontSize: 16, fontFamily: 'var(--font-heading)' }}>K</span>
</div>
{!col && (
<span
className="kalshi-gradient heading"
style={{ fontSize: 13, fontWeight: 700, letterSpacing: '0.06em', whiteSpace: 'nowrap', filter: 'drop-shadow(0 0 8px rgba(0,240,255,0.3))' }}
>
KEN TRADER
</span>
)}
</div>
{/* Nav items */}
<nav style={{ flex: 1, padding: '10px 8px', overflowY: 'auto', overflowX: 'hidden' }}>
{NAV.map((n, idx) => {
const active = loc.pathname === n.to || (n.to !== '/' && loc.pathname.startsWith(n.to));
const isSettings = n.to === '/settings';
return (
<React.Fragment key={n.to}>
{isSettings && (
<div style={{ margin: '8px 4px', height: 1, background: 'linear-gradient(90deg, transparent, var(--border), transparent)' }} />
)}
<Link
to={n.to}
title={col ? n.label : undefined}
style={{
display: 'flex',
alignItems: 'center',
gap: col ? 0 : 10,
padding: col ? '9px 0' : '9px 10px',
justifyContent: col ? 'center' : 'flex-start',
borderRadius: 8,
marginBottom: 2,
textDecoration: 'none',
fontSize: 13,
fontWeight: active ? 600 : 400,
color: active ? 'var(--cyan)' : 'var(--text-dim)',
background: active ? 'rgba(0,240,255,0.08)' : 'transparent',
borderLeft: active ? '3px solid var(--cyan)' : '3px solid transparent',
boxShadow: active ? 'inset 0 0 16px rgba(0,240,255,0.06), inset 3px 0 8px rgba(0,240,255,0.1)' : undefined,
textShadow: active ? '0 0 8px rgba(0,240,255,0.4)' : undefined,
transition: 'all 150ms ease',
whiteSpace: 'nowrap',
overflow: 'hidden',
}}
onMouseEnter={e => {
if (!active) {
e.currentTarget.style.color = 'var(--text)';
e.currentTarget.style.background = 'rgba(0,240,255,0.04)';
e.currentTarget.style.borderLeftColor = 'rgba(0,240,255,0.3)';
}
}}
onMouseLeave={e => {
if (!active) {
e.currentTarget.style.color = 'var(--text-dim)';
e.currentTarget.style.background = 'transparent';
e.currentTarget.style.borderLeftColor = 'transparent';
}
}}
>
<svg
style={{ width: 17, height: 17, flexShrink: 0, opacity: active ? 1 : 0.7 }}
fill="none"
stroke="currentColor"
strokeWidth="1.6"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" d={n.d} />
</svg>
{!col && (
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis' }}>{n.label}</span>
)}
</Link>
</React.Fragment>
);
})}
</nav>
{/* Bottom status + controls */}
<div
style={{
borderTop: '1px solid var(--border)',
padding: col ? '12px 8px' : '12px 14px',
flexShrink: 0,
}}
>
{/* Status indicators — hidden when collapsed */}
{!col && h && (
<div style={{ marginBottom: 10, display: 'flex', flexDirection: 'column', gap: 6 }}>
{[
{ label: 'Env', value: (h.env || 'demo').toUpperCase(), warn: h.env === 'demo', active: h.env === 'prod' },
{ label: 'API', value: h.kalshi_connected ? 'CONNECTED' : 'OFF', active: h.kalshi_connected },
{ label: 'DB', value: h.db_connected ? 'OK' : 'DOWN', active: h.db_connected },
{ label: 'Safe Mode', value: h.safe_mode ? 'ON' : 'OFF', warn: h.safe_mode, active: !h.safe_mode },
].map(s => (
<div key={s.label} style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<span style={{ fontSize: 10, color: 'var(--text-muted)', fontFamily: 'var(--font-body)' }}>{s.label}</span>
<div style={{ display: 'flex', alignItems: 'center', gap: 5 }}>
<StatusDot active={s.active} warn={s.warn} />
<span
style={{
fontSize: 10,
fontFamily: 'var(--font-mono)',
fontWeight: 600,
color: s.warn ? 'var(--orange)' : s.active ? 'var(--green)' : 'var(--red)',
}}
>
{s.value}
</span>
</div>
</div>
))}
</div>
)}
{/* Collapse + Logout */}
<div style={{ display: 'flex', gap: 6, alignItems: 'center' }}>
<button
onClick={() => setCol(!col)}
title={col ? 'Expand sidebar' : 'Collapse sidebar'}
style={{
flex: 1,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '7px 0',
background: 'transparent',
border: '1px solid var(--border)',
borderRadius: 7,
cursor: 'pointer',
color: 'var(--text-muted)',
transition: 'all 150ms ease',
}}
onMouseEnter={e => { e.currentTarget.style.borderColor = 'var(--cyan)'; e.currentTarget.style.color = 'var(--cyan)'; }}
onMouseLeave={e => { e.currentTarget.style.borderColor = 'var(--border)'; e.currentTarget.style.color = 'var(--text-muted)'; }}
>
<svg
style={{ width: 14, height: 14, transition: 'transform 200ms ease', transform: col ? 'rotate(180deg)' : 'none' }}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M11 19l-7-7 7-7m8 14l-7-7 7-7" />
</svg>
</button>
{!col && (
<button
onClick={logout}
style={{
padding: '7px 10px',
background: 'transparent',
border: '1px solid var(--border)',
borderRadius: 7,
cursor: 'pointer',
color: 'var(--text-muted)',
fontSize: 11,
fontFamily: 'var(--font-body)',
fontWeight: 500,
transition: 'all 150ms ease',
whiteSpace: 'nowrap',
}}
onMouseEnter={e => { e.currentTarget.style.borderColor = 'var(--red)'; e.currentTarget.style.color = 'var(--red)'; }}
onMouseLeave={e => { e.currentTarget.style.borderColor = 'var(--border)'; e.currentTarget.style.color = 'var(--text-muted)'; }}
>
Logout
</button>
)}
</div>
</div>
</aside>
{/* ── Main content ── */}
<main style={{ flex: 1, overflowY: 'auto', overflowX: 'hidden', display: 'flex', flexDirection: 'column' }}>
{/* Ticker Tape */}
<TickerTape />
{/* Market Status Bar */}
<div className="market-status-bar">
<span style={{ color: 'var(--cyan)', fontWeight: 700, letterSpacing: '0.06em' }}>KEN TRADER</span>
<span style={{ color: 'var(--border-light)' }}>|</span>
<span>
KALSHI{' '}
<span style={{ color: h?.kalshi_connected ? 'var(--green)' : 'var(--red)', fontWeight: 700 }}>
{h?.kalshi_connected ? 'LIVE' : 'OFFLINE'}
</span>
</span>
<span style={{ color: 'var(--border-light)' }}>|</span>
<span>
DB{' '}
<span style={{ color: h?.db_connected ? 'var(--green)' : 'var(--red)', fontWeight: 700 }}>
{h?.db_connected ? 'OK' : 'DOWN'}
</span>
</span>
<span style={{ color: 'var(--border-light)' }}>|</span>
<span>
ENV{' '}
<span style={{ color: h?.env === 'prod' ? 'var(--green)' : 'var(--orange)', fontWeight: 700 }}>
{(h?.env || 'DEMO').toUpperCase()}
</span>
</span>
{h?.safe_mode && (
<>
<span style={{ color: 'var(--border-light)' }}>|</span>
<span style={{ color: 'var(--orange)', fontWeight: 700 }}>SAFE MODE ON</span>
</>
)}
<span style={{ color: 'var(--border-light)' }}>|</span>
<LiveRunChip lr={lr} />
<span style={{ marginLeft: 'auto', display: 'flex', alignItems: 'center', gap: 12 }}>
<span style={{ color: 'var(--border-light)' }}>
{new Date().toLocaleTimeString('en-US', { timeZone: 'America/Los_Angeles', hour: '2-digit', minute: '2-digit', second: '2-digit' })} PT
</span>
{/* Notification Bell */}
<button
className="notification-bell"
aria-label="Alerts (7 unread)"
title="7 unread alerts"
style={{ background: 'none', border: 'none', display: 'flex', alignItems: 'center' }}
>
<svg fill="none" stroke="currentColor" strokeWidth="1.8" viewBox="0 0 24 24" aria-hidden="true">
<path strokeLinecap="round" strokeLinejoin="round" 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>
<span className="notification-count" aria-label="7 alerts">7</span>
</button>
</span>
</div>
<div style={{ maxWidth: 1600, margin: '0 auto', padding: '24px 28px', flex: 1, width: '100%' }}>
<Outlet />
</div>
</main>
</div>
);
}