From 02cf9d1cba3b044513a9d399c896c456bba5918d Mon Sep 17 00:00:00 2001 From: Dennis Thiessen Date: Fri, 10 Jul 2026 07:44:55 +0200 Subject: [PATCH] Match Horizon mockup structure; restore qualified-first radar Fixes from first prod review of the redesign: - Top navigation bar (TopBar) replaces the sidebar, like the mockup: wordmark + sections + ticker search + jobs/status/logout; content in a centered max-width column. MobileNav unchanged. - Radar panel: qualified setups lead again (the actionable list, as the old Top Setups did); non-qualified sit behind an "N below the gate" toggle with per-rule reasons, auto-open only when nothing qualifies. Adds mini radar fingerprints for the top 3 qualified (dimension scores from the rankings endpoint, fixed axis order). - Dashboard layout: open positions and radar side by side (xl), as in the mockup. - ScoreCard: the radar fingerprint was inside the showComposite block, which the ticker page disables - it never rendered. Now it shows whenever >= 3 dimensions exist. - TradeChart: fresh positions with < 2 bars since entry rendered no chart; now pads with ~10 pre-entry context bars (gray) and marks the entry point. First open position auto-expands so the chart is visible without a click. Co-Authored-By: Claude Fable 5 --- frontend/src/components/charts/horizon.tsx | 27 +- .../components/dashboard/OpenTradesPanel.tsx | 12 +- frontend/src/components/layout/AppShell.tsx | 16 +- frontend/src/components/layout/Sidebar.tsx | 118 --------- frontend/src/components/layout/TopBar.tsx | 106 ++++++++ frontend/src/components/ui/ScoreCard.tsx | 13 +- frontend/src/pages/DashboardPage.tsx | 241 ++++++++++++------ frontend/tsconfig.tsbuildinfo | 2 +- 8 files changed, 321 insertions(+), 214 deletions(-) delete mode 100644 frontend/src/components/layout/Sidebar.tsx create mode 100644 frontend/src/components/layout/TopBar.tsx diff --git a/frontend/src/components/charts/horizon.tsx b/frontend/src/components/charts/horizon.tsx index 214a03b..3fbcf72 100644 --- a/frontend/src/components/charts/horizon.tsx +++ b/frontend/src/components/charts/horizon.tsx @@ -237,11 +237,14 @@ export function TradeChart({ openedAt: string; }) { const openedDate = openedAt.slice(0, 10); - const closes = bars - .filter((b) => b.date.slice(0, 10) >= openedDate) - .map((b) => b.close); - const series = [entry, ...closes]; - if (series.length < 3) return null; + const firstIdx = bars.findIndex((b) => b.date.slice(0, 10) >= openedDate); + // Fresh trades have few bars since entry — pad with context so the chart + // still reads (gray before entry, colored after). + const CONTEXT_BARS = 10; + const start = firstIdx === -1 ? Math.max(0, bars.length - CONTEXT_BARS) : Math.max(0, firstIdx - CONTEXT_BARS); + const series = bars.slice(start).map((b) => b.close); + const entryIdx = firstIdx === -1 ? series.length - 1 : firstIdx - start; + if (series.length < 2) return null; const w = 560; const h = 150; const padL = 8; const padR = 96; const padT = 10; const padB = 12; @@ -259,7 +262,13 @@ export function TradeChart({ const plotH = h - padT - padB; const px = (i: number) => padL + (i / (series.length - 1)) * plotW; const py = (v: number) => padT + plotH - ((v - lo) / (hi - lo)) * plotH; - const path = series.map((v, i) => `${i === 0 ? 'M' : 'L'}${px(i).toFixed(1)},${py(v).toFixed(1)}`).join(' '); + const seg = (from: number, to: number) => + series + .slice(from, to + 1) + .map((v, i) => `${i === 0 ? 'M' : 'L'}${px(from + i).toFixed(1)},${py(v).toFixed(1)}`) + .join(' '); + const contextPath = entryIdx > 0 ? seg(0, entryIdx) : null; + const tradePath = seg(entryIdx, series.length - 1); const last = series[series.length - 1]; const perShare = isShort ? entry - last : last - entry; const col = perShare >= 0 ? 'var(--up)' : 'var(--down)'; @@ -288,7 +297,11 @@ export function TradeChart({ target {fmt(target)} {isShort ? '↓' : '↑'} )} - + {contextPath && ( + + )} + + ); diff --git a/frontend/src/components/dashboard/OpenTradesPanel.tsx b/frontend/src/components/dashboard/OpenTradesPanel.tsx index 3984953..a70ccb8 100644 --- a/frontend/src/components/dashboard/OpenTradesPanel.tsx +++ b/frontend/src/components/dashboard/OpenTradesPanel.tsx @@ -1,4 +1,4 @@ -import { useMemo, useState } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; import type { KeyboardEvent, ReactNode } from 'react'; import { Link } from 'react-router-dom'; import { useQuery } from '@tanstack/react-query'; @@ -135,6 +135,16 @@ export function OpenTradesPanel() { const close = useClosePaperTrade(); const [expandedId, setExpandedId] = useState(null); + // Open the first row once on load so the trade chart is visible without a + // click; after that the user's expand/collapse choice wins. + const autoExpanded = useRef(false); + useEffect(() => { + if (!autoExpanded.current && trades && trades.length > 0) { + autoExpanded.current = true; + setExpandedId(trades[0].id); + } + }, [trades]); + const exitLabel = policy ? policy.mode === 'atr_trailing' ? `${(policy.atr_multiplier ?? 3).toFixed(1)}x ATR trailing stop / ${policy.hold_days}d max` diff --git a/frontend/src/components/layout/AppShell.tsx b/frontend/src/components/layout/AppShell.tsx index 1516d6b..feb0844 100644 --- a/frontend/src/components/layout/AppShell.tsx +++ b/frontend/src/components/layout/AppShell.tsx @@ -1,21 +1,19 @@ import { Outlet } from 'react-router-dom'; -import Sidebar from './Sidebar'; +import TopBar from './TopBar'; import MobileNav from './MobileNav'; export default function AppShell() { return ( -
+