spb/crypto-trade-bot
Public
Python 54.7%
TypeScript 42.5%
SQL 1.9%
1"use client";2import { useQuery, useQueryClient } from "@tanstack/react-query";3import { Activity, BarChart3, BookOpen, Briefcase, FlaskConical, Gauge, LayoutDashboard, LogOut, Menu, Radio, Settings, ShieldAlert, TerminalSquare, X } from "lucide-react";4import Link from "next/link";5import { usePathname } from "next/navigation";6import { useState } from "react";7import { api } from "@/lib/api";8import type { RiskStatus, SystemStatus } from "@/lib/types";9import { Badge, Button, Dot, cx } from "./ui";1011const NAV = [12 { href: "/", label: "Overview", icon: LayoutDashboard },13 { href: "/market", label: "Market Explorer", icon: Activity },14 { href: "/lab", label: "Strategy Lab", icon: FlaskConical },15 { href: "/backtests", label: "Backtest Explorer", icon: BarChart3 },16 { href: "/portfolio", label: "Portfolio Optimizer", icon: Briefcase },17 { href: "/paper", label: "Paper Trading", icon: Radio },18 { href: "/execution", label: "Execution Console", icon: TerminalSquare },19 { href: "/risk", label: "Risk Center", icon: ShieldAlert },20 { href: "/reports", label: "Research Reports", icon: BookOpen },21 { href: "/settings", label: "Settings", icon: Settings },22];2324export function useSystemStatus() {25 return useQuery({ queryKey: ["system-status"], queryFn: () => api<SystemStatus>("/api/system/status"), refetchInterval: 15_000 });26}27export function useRiskStatus() {28 return useQuery({ queryKey: ["risk-status"], queryFn: () => api<RiskStatus>("/api/risk/status"), refetchInterval: 15_000 });29}3031export function Shell({ children }: { children: React.ReactNode }) {32 const [open, setOpen] = useState(false);33 const pathname = usePathname();34 const sys = useSystemStatus();35 const risk = useRiskStatus();36 const qc = useQueryClient();37 const mode = sys.data?.mode ?? "offline";38 const ks = risk.data?.kill_switch?.active;3940 async function logout() {41 await api("/api/auth/logout", { method: "POST", headers: { Origin: window.location.origin } });42 qc.clear();43 // eslint-disable-next-line @next/next/no-location-assign-relative-destination -- full reload clears client state44 window.location.href = "/login";45 }4647 const nav = (48 <nav aria-label="Main" className="flex flex-col gap-0.5 p-2">49 {NAV.map(({ href, label, icon: Icon }) => {50 const active = href === "/" ? pathname === "/" : pathname.startsWith(href);51 return (52 <Link key={href} href={href} onClick={() => setOpen(false)} aria-current={active ? "page" : undefined} className={cx("flex items-center gap-2.5 rounded-md px-2.5 py-2 text-sm transition", active ? "bg-accent-soft text-fg" : "text-fg-muted hover:bg-bg-sub hover:text-fg")}>53 <Icon className="h-4 w-4 shrink-0" aria-hidden />54 <span>{label}</span>55 </Link>56 );57 })}58 </nav>59 );6061 return (62 <div className="flex min-h-screen">63 <aside className="no-print hidden w-56 shrink-0 border-r border-line bg-bg-elev lg:block">64 <div className="flex h-12 items-center gap-2 border-b border-line px-4">65 <Gauge className="h-4 w-4 text-accent" aria-hidden />66 <span className="font-semibold tracking-tight">ktrade</span>67 <span className="text-[10px] text-fg-dim">Kraken spot</span>68 </div>69 {nav}70 </aside>71 {open && (72 <div className="fixed inset-0 z-40 lg:hidden" onClick={() => setOpen(false)}>73 <div className="absolute inset-0 bg-black/60" />74 <aside className="absolute left-0 top-0 h-full w-64 border-r border-line bg-bg-elev" onClick={(e) => e.stopPropagation()}>75 <div className="flex h-12 items-center justify-between border-b border-line px-4">76 <span className="font-semibold">ktrade</span>77 <button aria-label="Close menu" onClick={() => setOpen(false)}><X className="h-4 w-4" /></button>78 </div>79 {nav}80 </aside>81 </div>82 )}83 <div className="flex min-w-0 flex-1 flex-col">84 {mode === "live" && (85 <div role="alert" className="bg-live px-4 py-1.5 text-center text-xs font-bold uppercase tracking-widest text-white">LIVE TRADING ACTIVE — real orders can reach Kraken</div>86 )}87 <header className="no-print flex h-12 items-center gap-3 border-b border-line bg-bg-elev px-3 sm:px-4">88 <button className="rounded p-1 text-fg-muted hover:bg-bg-sub lg:hidden" aria-label="Open menu" onClick={() => setOpen(true)}><Menu className="h-5 w-5" /></button>89 <ModeBadge mode={mode} />90 <div className="ml-1 hidden items-center gap-3 text-xs text-fg-muted sm:flex">91 <span className="flex items-center gap-1.5" title={sys.data?.kraken?.error ?? sys.data?.kraken?.status}><Dot ok={sys.data ? sys.data.kraken?.status === "online" : null} /> Kraken {sys.data?.kraken?.status ?? "…"}</span>92 <span className="flex items-center gap-1.5"><Dot ok={sys.data ? !!sys.data.stream?.connected : null} /> stream {sys.data?.stream ? (sys.data.stream.connected ? "live" : "down") : "n/a"}</span>93 {ks !== undefined && <span className="flex items-center gap-1.5"><Dot ok={!ks} /> kill switch {ks ? <span className="font-semibold text-down">ACTIVE</span> : "armed"}</span>}94 </div>95 <div className="ml-auto flex items-center gap-2">96 {ks && <Badge tone="down" className="sm:hidden">kill</Badge>}97 <Button size="sm" variant="ghost" onClick={logout} aria-label="Log out"><LogOut className="h-3.5 w-3.5" /> <span className="hidden sm:inline">Logout</span></Button>98 </div>99 </header>100 <main className="min-w-0 flex-1 p-3 sm:p-4 lg:p-6">{children}</main>101 </div>102 </div>103 );104}105106export function ModeBadge({ mode }: { mode: string }) {107 if (mode === "live") return <Badge tone="live">LIVE</Badge>;108 if (mode === "paper") return <Badge tone="accent">PAPER</Badge>;109 return <Badge tone="muted">OFFLINE</Badge>;110}111