Match Horizon mockup structure; restore qualified-first radar
Deploy / lint (push) Successful in 7s
Deploy / test (push) Successful in 1m8s
Deploy / deploy (push) Successful in 35s

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 <noreply@anthropic.com>
This commit is contained in:
2026-07-10 07:44:55 +02:00
co-authored by Claude Fable 5
parent 20f6981712
commit 02cf9d1cba
8 changed files with 321 additions and 214 deletions
+20 -7
View File
@@ -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 ? '↓' : '↑'}
</text>
)}
<path d={path} fill="none" stroke={col} strokeWidth="2" strokeLinejoin="round" strokeLinecap="round" />
{contextPath && (
<path d={contextPath} fill="none" stroke="var(--ink-3)" strokeWidth="2" strokeLinejoin="round" strokeLinecap="round" opacity="0.7" />
)}
<path d={tradePath} fill="none" stroke={col} strokeWidth="2" strokeLinejoin="round" strokeLinecap="round" />
<circle cx={px(entryIdx)} cy={py(entry)} r="3.5" fill="var(--ink-2)" stroke="var(--surface)" strokeWidth="1.5" />
<circle cx={px(series.length - 1)} cy={py(last)} r="4" fill={col} stroke="var(--surface)" strokeWidth="2" />
</svg>
);