SPB Git forge
1commits 1branches 0releases
189.0 KBsize
maindefault branch
5 h agolast push
Python 54.7% TypeScript 42.5% SQL 1.9%
5.8 KB · 111 lines tsx
Raw Blame History
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