From 9c6a0a72fadc04842485b6db78671f155599911d Mon Sep 17 00:00:00 2001 From: Dennis Thiessen Date: Fri, 12 Jun 2026 14:42:05 +0200 Subject: [PATCH] Redesign: phosphor-terminal identity and simplified 4-page structure Information architecture (6 nav destinations -> 4): - New Overview home: metric strip (live setups, high confidence, hit rate, expectancy), top-5 setups, watchlist pulse - Market = Watchlist + Rankings merged as tabs; scoring weights moved into a collapsible disclosure - Signals = Scanner + Performance merged as tabs (Setups | Track Record) with actions inside the panels - Legacy routes redirect (/watchlist, /rankings, /scanner, /performance) Visual identity: - Warm ash-green dark palette replaces cold navy; citron lime accent replaces blue (Tailwind gray/blue remapped at config level so all components reskin) - Primary buttons: lime with ink text; long/short stays emerald/red - Typography: Bricolage Grotesque display, Instrument Sans body, IBM Plex Mono for all numerals incl. chart canvas labels - Atmosphere: graph-paper grid + citron glow + film grain; pulsing brand dot; mono-numbered nav Co-Authored-By: Claude Fable 5 --- frontend/index.html | 9 +- frontend/src/App.tsx | 20 +- .../src/components/admin/SettingsForm.tsx | 2 +- .../src/components/auth/ProtectedRoute.tsx | 2 +- .../components/charts/CandlestickChart.tsx | 10 +- frontend/src/components/layout/MobileNav.tsx | 22 +- frontend/src/components/layout/Sidebar.tsx | 57 ++--- .../signals/SetupsPanel.tsx} | 43 ++-- .../signals/TrackRecordPanel.tsx} | 82 +++---- frontend/src/components/ui/Badge.tsx | 2 +- frontend/src/components/ui/Field.tsx | 2 +- frontend/src/pages/DashboardPage.tsx | 217 ++++++++++++++++++ frontend/src/pages/LoginPage.tsx | 4 +- .../{WatchlistPage.tsx => MarketPage.tsx} | 69 +++++- frontend/src/pages/RankingsPage.tsx | 29 --- frontend/src/pages/RegisterPage.tsx | 2 +- frontend/src/pages/SignalsPage.tsx | 32 +++ frontend/src/styles/globals.css | 108 ++++++--- frontend/tailwind.config.ts | 47 +++- frontend/tsconfig.tsbuildinfo | 2 +- 20 files changed, 548 insertions(+), 213 deletions(-) rename frontend/src/{pages/ScannerPage.tsx => components/signals/SetupsPanel.tsx} (90%) rename frontend/src/{pages/PerformancePage.tsx => components/signals/TrackRecordPanel.tsx} (65%) create mode 100644 frontend/src/pages/DashboardPage.tsx rename frontend/src/pages/{WatchlistPage.tsx => MarketPage.tsx} (59%) delete mode 100644 frontend/src/pages/RankingsPage.tsx create mode 100644 frontend/src/pages/SignalsPage.tsx diff --git a/frontend/index.html b/frontend/index.html index b87ca08..e86f721 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -3,12 +3,15 @@ - Signal Dashboard + Signal — Trading Intelligence - + - +
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index d903022..873eac7 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -3,11 +3,10 @@ import ProtectedRoute from './components/auth/ProtectedRoute'; import AppShell from './components/layout/AppShell'; import LoginPage from './pages/LoginPage'; import RegisterPage from './pages/RegisterPage'; -import WatchlistPage from './pages/WatchlistPage'; +import DashboardPage from './pages/DashboardPage'; +import MarketPage from './pages/MarketPage'; +import SignalsPage from './pages/SignalsPage'; import TickerDetailPage from './pages/TickerDetailPage'; -import ScannerPage from './pages/ScannerPage'; -import RankingsPage from './pages/RankingsPage'; -import PerformancePage from './pages/PerformancePage'; import AdminPage from './pages/AdminPage'; export default function App() { @@ -17,12 +16,15 @@ export default function App() { } /> }> }> - } /> - } /> + } /> + } /> + } /> } /> - } /> - } /> - } /> + {/* Legacy routes from the old 6-page layout */} + } /> + } /> + } /> + } /> }> } /> diff --git a/frontend/src/components/admin/SettingsForm.tsx b/frontend/src/components/admin/SettingsForm.tsx index 6a04d30..5e4f49b 100644 --- a/frontend/src/components/admin/SettingsForm.tsx +++ b/frontend/src/components/admin/SettingsForm.tsx @@ -43,7 +43,7 @@ export function SettingsForm() { type="button" onClick={() => handleToggleRegistration(setting.value)} disabled={updateSetting.isPending} - className={`relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:ring-offset-[#0a0e1a] disabled:opacity-50 ${ + className={`relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:ring-offset-[#0e120f] disabled:opacity-50 ${ setting.value === 'true' ? 'bg-gradient-to-r from-blue-600 to-sky-500' : 'bg-white/[0.1]' }`} role="switch" diff --git a/frontend/src/components/auth/ProtectedRoute.tsx b/frontend/src/components/auth/ProtectedRoute.tsx index 968f655..192f5a5 100644 --- a/frontend/src/components/auth/ProtectedRoute.tsx +++ b/frontend/src/components/auth/ProtectedRoute.tsx @@ -14,7 +14,7 @@ export default function ProtectedRoute({ requireAdmin }: ProtectedRouteProps) { } if (requireAdmin && role !== 'admin') { - return ; + return ; } return ; diff --git a/frontend/src/components/charts/CandlestickChart.tsx b/frontend/src/components/charts/CandlestickChart.tsx index 8080691..27fd6b2 100644 --- a/frontend/src/components/charts/CandlestickChart.tsx +++ b/frontend/src/components/charts/CandlestickChart.tsx @@ -100,7 +100,7 @@ export function CandlestickChart({ data, srLevels = [], maxSRLevels = 6, zones = ctx.strokeStyle = 'rgba(255,255,255,0.04)'; ctx.lineWidth = 1; ctx.fillStyle = '#6b7280'; - ctx.font = '11px Inter, system-ui, sans-serif'; + ctx.font = '11px "IBM Plex Mono", ui-monospace, monospace'; ctx.textAlign = 'right'; for (let i = 0; i <= nTicks; i++) { const v = lo + ((hi - lo) * i) / nTicks; @@ -141,7 +141,7 @@ export function CandlestickChart({ data, srLevels = [], maxSRLevels = 6, zones = // Label ctx.fillStyle = color; - ctx.font = '10px Inter, system-ui, sans-serif'; + ctx.font = '10px "IBM Plex Mono", ui-monospace, monospace'; ctx.textAlign = 'left'; ctx.fillText( `${level.type[0].toUpperCase()} ${formatPrice(level.price_level)}`, @@ -182,7 +182,7 @@ export function CandlestickChart({ data, srLevels = [], maxSRLevels = 6, zones = // Label with midpoint price and strength score const yMid = yTop + rectHeight / 2; ctx.fillStyle = labelColor; - ctx.font = '10px Inter, system-ui, sans-serif'; + ctx.font = '10px "IBM Plex Mono", ui-monospace, monospace'; ctx.textAlign = 'left'; ctx.fillText( `${zone.type[0].toUpperCase()} ${formatPrice(zone.midpoint)} (${zone.strength})`, @@ -238,7 +238,7 @@ export function CandlestickChart({ data, srLevels = [], maxSRLevels = 6, zones = ctx.setLineDash([]); // Labels on right side - ctx.font = '10px Inter, system-ui, sans-serif'; + ctx.font = '10px "IBM Plex Mono", ui-monospace, monospace'; ctx.textAlign = 'left'; ctx.fillStyle = 'rgba(96, 165, 250, 0.9)'; ctx.fillText(`Entry ${formatPrice(tradeSetup.entry_price)}`, ml + cw + 4, entryY + 3); @@ -334,7 +334,7 @@ export function CandlestickChart({ data, srLevels = [], maxSRLevels = 6, zones = // Price label on y-axis (right side) const price = hi - ((cy - mt) / ch) * (hi - lo); const priceText = formatPrice(price); - ctx.font = '11px Inter, system-ui, sans-serif'; + ctx.font = '11px "IBM Plex Mono", ui-monospace, monospace'; const priceMetrics = ctx.measureText(priceText); const labelPadX = 5; const labelPadY = 3; diff --git a/frontend/src/components/layout/MobileNav.tsx b/frontend/src/components/layout/MobileNav.tsx index b260535..0ce5ac9 100644 --- a/frontend/src/components/layout/MobileNav.tsx +++ b/frontend/src/components/layout/MobileNav.tsx @@ -3,10 +3,9 @@ import { NavLink } from 'react-router-dom'; import { useAuthStore } from '../../stores/authStore'; const navItems = [ - { to: '/watchlist', label: 'Watchlist' }, - { to: '/scanner', label: 'Scanner' }, - { to: '/rankings', label: 'Rankings' }, - { to: '/performance', label: 'Performance' }, + { to: '/', label: 'Overview', end: true }, + { to: '/market', label: 'Market', end: false }, + { to: '/signals', label: 'Signals', end: false }, ]; export default function MobileNav() { @@ -16,7 +15,13 @@ export default function MobileNav() { return (
-

Signal Dashboard

+
+ + + + +

Signal

+
diff --git a/frontend/src/pages/ScannerPage.tsx b/frontend/src/components/signals/SetupsPanel.tsx similarity index 90% rename from frontend/src/pages/ScannerPage.tsx rename to frontend/src/components/signals/SetupsPanel.tsx index b5e44dc..35f69d4 100644 --- a/frontend/src/pages/ScannerPage.tsx +++ b/frontend/src/components/signals/SetupsPanel.tsx @@ -1,17 +1,16 @@ import { useMemo, useState } from 'react'; import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { useTrades } from '../hooks/useTrades'; -import { TradeTable, type SortColumn, type SortDirection, computeTradeAnalysis } from '../components/scanner/TradeTable'; -import { SkeletonTable } from '../components/ui/Skeleton'; -import { useToast } from '../components/ui/Toast'; -import { Button } from '../components/ui/Button'; -import { Callout } from '../components/ui/Callout'; -import { Disclosure } from '../components/ui/Disclosure'; -import { Field, Input, Select } from '../components/ui/Field'; -import { PageHeader } from '../components/ui/PageHeader'; -import { triggerJob } from '../api/admin'; -import type { TradeSetup } from '../lib/types'; -import { RECOMMENDATION_ACTION_GLOSSARY, RECOMMENDATION_ACTION_LABELS } from '../lib/recommendation'; +import { useTrades } from '../../hooks/useTrades'; +import { TradeTable, type SortColumn, type SortDirection, computeTradeAnalysis } from '../scanner/TradeTable'; +import { SkeletonTable } from '../ui/Skeleton'; +import { useToast } from '../ui/Toast'; +import { Button } from '../ui/Button'; +import { Callout } from '../ui/Callout'; +import { Disclosure } from '../ui/Disclosure'; +import { Field, Input, Select } from '../ui/Field'; +import { triggerJob } from '../../api/admin'; +import type { TradeSetup } from '../../lib/types'; +import { RECOMMENDATION_ACTION_GLOSSARY, RECOMMENDATION_ACTION_LABELS } from '../../lib/recommendation'; type DirectionFilter = 'both' | 'long' | 'short'; type ActionFilter = 'all' | 'LONG_HIGH' | 'LONG_MODERATE' | 'SHORT_HIGH' | 'SHORT_MODERATE' | 'NEUTRAL'; @@ -93,7 +92,7 @@ function sortTrades( return sorted; } -export default function ScannerPage() { +export function SetupsPanel() { const { data: trades, isLoading, isError, error } = useTrades(); const queryClient = useQueryClient(); const toast = useToast(); @@ -132,17 +131,7 @@ export default function ScannerPage() { }, [trades, minRR, directionFilter, minConfidence, actionFilter, sortColumn, sortDirection]); return ( -
- scanMutation.mutate()} loading={scanMutation.isPending}> - {scanMutation.isPending ? 'Scanning…' : 'Run Scanner'} - - } - /> - +
{/* Filter toolbar */}
@@ -196,6 +185,11 @@ export default function ScannerPage() { +
+ +
@@ -215,7 +209,6 @@ export default function ScannerPage() {
- {/* Content */} {isLoading && } {isError && ( diff --git a/frontend/src/pages/PerformancePage.tsx b/frontend/src/components/signals/TrackRecordPanel.tsx similarity index 65% rename from frontend/src/pages/PerformancePage.tsx rename to frontend/src/components/signals/TrackRecordPanel.tsx index 9c0befc..e0d7b80 100644 --- a/frontend/src/pages/PerformancePage.tsx +++ b/frontend/src/components/signals/TrackRecordPanel.tsx @@ -1,15 +1,14 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { usePerformance } from '../hooks/usePerformance'; -import { triggerJob } from '../api/admin'; -import { Button } from '../components/ui/Button'; -import { Callout } from '../components/ui/Callout'; -import { Disclosure } from '../components/ui/Disclosure'; -import { PageHeader } from '../components/ui/PageHeader'; -import { Section } from '../components/ui/Section'; -import { SkeletonCard } from '../components/ui/Skeleton'; -import { useToast } from '../components/ui/Toast'; -import { RECOMMENDATION_ACTION_LABELS } from '../lib/recommendation'; -import type { OutcomeBucketStats } from '../lib/types'; +import { usePerformance } from '../../hooks/usePerformance'; +import { triggerJob } from '../../api/admin'; +import { Button } from '../ui/Button'; +import { Callout } from '../ui/Callout'; +import { Disclosure } from '../ui/Disclosure'; +import { Section } from '../ui/Section'; +import { SkeletonCard } from '../ui/Skeleton'; +import { useToast } from '../ui/Toast'; +import { RECOMMENDATION_ACTION_LABELS } from '../../lib/recommendation'; +import type { OutcomeBucketStats } from '../../lib/types'; function fmtR(value: number | null): string { if (value === null) return '—'; @@ -35,8 +34,8 @@ function StatCard({ label, value, valueClass = 'text-gray-100', sub }: { }) { return (
-

{label}

-

{value}

+

{label}

+

{value}

{sub &&

{sub}

}
); @@ -74,13 +73,13 @@ function BreakdownTable({ rows, labelHeader, mapLabel }: { {entries.map(([key, stats]) => ( {mapLabel ? mapLabel(key) : key} - {stats.total} - {stats.wins} - {stats.losses} - {stats.expired} - {fmtPct(stats.hit_rate)} - {fmtR(stats.avg_r)} - {fmtR(stats.total_r)} + {stats.total} + {stats.wins} + {stats.losses} + {stats.expired} + {fmtPct(stats.hit_rate)} + {fmtR(stats.avg_r)} + {fmtR(stats.total_r)} ))} @@ -89,7 +88,7 @@ function BreakdownTable({ rows, labelHeader, mapLabel }: { ); } -export default function PerformancePage() { +export function TrackRecordPanel() { const { data, isLoading, isError, error } = usePerformance(); const queryClient = useQueryClient(); const toast = useToast(); @@ -106,29 +105,24 @@ export default function PerformancePage() { }); return ( -
- evaluateMutation.mutate()} loading={evaluateMutation.isPending}> - {evaluateMutation.isPending ? 'Evaluating…' : 'Evaluate Now'} - - } - /> - - -

- Each setup is replayed against the daily bars after its detection: a{' '} - win means the target was reached before the - stop, a loss means the stop was hit first (bars - where both levels fall inside the same day count conservatively as losses). Setups with - neither level hit within 30 trading days expire at - 0R. Avg R is the expectancy per trade: wins earn their R:R ratio, losses cost −1R — a - positive value means the signals have been profitable on a risk-adjusted basis. The - evaluator runs nightly after OHLCV collection. -

-
+
+
+ +

+ Each setup is replayed against the daily bars after its detection: a{' '} + win means the target was reached before the + stop, a loss means the stop was hit first (bars + where both levels fall inside the same day count conservatively as losses). Setups with + neither level hit within 30 trading days expire at + 0R. Avg R is the expectancy per trade: wins earn their R:R ratio, losses cost −1R — a + positive value means the signals have been profitable on a risk-adjusted basis. The + evaluator runs nightly after OHLCV collection. +

+
+ +
{isLoading && (
diff --git a/frontend/src/components/ui/Badge.tsx b/frontend/src/components/ui/Badge.tsx index 203b7e4..b86d814 100644 --- a/frontend/src/components/ui/Badge.tsx +++ b/frontend/src/components/ui/Badge.tsx @@ -1,6 +1,6 @@ const variantStyles: Record = { auto: 'bg-blue-500/15 text-blue-400 border-blue-500/20', - manual: 'bg-sky-500/15 text-sky-400 border-sky-500/20', + manual: 'bg-amber-500/15 text-amber-400 border-amber-500/20', default: 'bg-white/[0.06] text-gray-400 border-white/[0.08]', }; diff --git a/frontend/src/components/ui/Field.tsx b/frontend/src/components/ui/Field.tsx index 9ca880e..9811e41 100644 --- a/frontend/src/components/ui/Field.tsx +++ b/frontend/src/components/ui/Field.tsx @@ -24,7 +24,7 @@ export function Input({ className = '', ...rest }: InputHTMLAttributes) { return ( - option]:bg-[#151911] ${className}`} {...rest}> {children} ); diff --git a/frontend/src/pages/DashboardPage.tsx b/frontend/src/pages/DashboardPage.tsx new file mode 100644 index 0000000..e4d2e11 --- /dev/null +++ b/frontend/src/pages/DashboardPage.tsx @@ -0,0 +1,217 @@ +import { useMemo } from 'react'; +import { Link } from 'react-router-dom'; +import { useTrades } from '../hooks/useTrades'; +import { useWatchlist } from '../hooks/useWatchlist'; +import { usePerformance } from '../hooks/usePerformance'; +import { Callout } from '../components/ui/Callout'; +import { Section } from '../components/ui/Section'; +import { SkeletonCard, SkeletonTable } from '../components/ui/Skeleton'; +import { formatPrice } from '../lib/format'; +import { recommendationActionLabel } from '../lib/recommendation'; +import type { TradeSetup } from '../lib/types'; + +function fmtR(value: number | null): string { + if (value === null) return '—'; + return `${value > 0 ? '+' : ''}${value.toFixed(2)}R`; +} + +function rColor(value: number | null): string { + if (value === null) return 'text-gray-400'; + if (value > 0) return 'text-emerald-400'; + if (value < 0) return 'text-red-400'; + return 'text-gray-300'; +} + +function Metric({ label, value, sub, valueClass = 'text-gray-100' }: { + label: string; + value: string; + sub?: string; + valueClass?: string; +}) { + return ( +
+

{label}

+

{value}

+ {sub &&

{sub}

} +
+ ); +} + +function DirectionTag({ direction }: { direction: string }) { + const isLong = direction === 'long'; + return ( + + {direction} + + ); +} + +export default function DashboardPage() { + const trades = useTrades(); + const watchlist = useWatchlist(); + const performance = usePerformance(); + + const topSetups: TradeSetup[] = useMemo( + () => (trades.data ?? []).slice(0, 5), + [trades.data], + ); + + const highConfidenceCount = useMemo( + () => (trades.data ?? []).filter((t) => (t.confidence_score ?? 0) >= 70).length, + [trades.data], + ); + + const topWatchlist = useMemo( + () => + [...(watchlist.data ?? [])] + .sort((a, b) => (b.composite_score ?? -1) - (a.composite_score ?? -1)) + .slice(0, 6), + [watchlist.data], + ); + + const today = new Date().toLocaleDateString('en-US', { + weekday: 'long', month: 'long', day: 'numeric', + }); + + const stats = performance.data?.overall; + + return ( +
+ {/* Hero */} +
+

{today}

+

+ Market overview +

+
+ + {/* Metric strip */} + {(trades.isLoading || performance.isLoading) ? ( +
+ +
+ ) : ( +
+ + 0 ? 'text-blue-300' : 'text-gray-100'} + /> + + +
+ )} + +
+ {/* Top setups */} +
+
+ {trades.isLoading && } + {trades.isError && Failed to load setups} + {trades.data && topSetups.length === 0 && ( + No active setups. Run the scanner from the Signals page. + )} + {topSetups.length > 0 && ( +
+ + + + + + + + + + + + + {topSetups.map((setup) => ( + + + + + + + + + ))} + +
TickerDirEntryR:RConf.Action
+ + {setup.symbol} + + {formatPrice(setup.entry_price)}{setup.rr_ratio.toFixed(1)}:1 + {setup.confidence_score != null ? `${Math.round(setup.confidence_score)}%` : '—'} + + {recommendationActionLabel(setup.recommended_action)} +
+
+ + All setups → + +
+
+ )} +
+
+ + {/* Watchlist pulse */} +
+
+ {watchlist.isLoading && } + {watchlist.isError && Failed to load watchlist} + {watchlist.data && topWatchlist.length === 0 && ( + Watchlist is empty — add tickers on the Market page. + )} + {topWatchlist.length > 0 && ( +
+
    + {topWatchlist.map((entry) => ( +
  • + + {entry.symbol} + + {entry.rr_ratio != null && ( + {entry.rr_ratio.toFixed(1)}:1 + )} + + {entry.composite_score != null ? entry.composite_score.toFixed(0) : '—'} + + + +
  • + ))} +
+
+ + Full watchlist → + +
+
+ )} +
+
+
+
+ ); +} diff --git a/frontend/src/pages/LoginPage.tsx b/frontend/src/pages/LoginPage.tsx index 1b699c7..75c8a14 100644 --- a/frontend/src/pages/LoginPage.tsx +++ b/frontend/src/pages/LoginPage.tsx @@ -15,7 +15,7 @@ export default function LoginPage() { login.mutate( { username, password }, { - onSuccess: () => navigate('/watchlist'), + onSuccess: () => navigate('/'), onError: (err) => setError(err instanceof Error ? err.message : 'Login failed'), }, ); @@ -25,7 +25,7 @@ export default function LoginPage() {
{/* Ambient glow orbs */}
-
+
diff --git a/frontend/src/pages/WatchlistPage.tsx b/frontend/src/pages/MarketPage.tsx similarity index 59% rename from frontend/src/pages/WatchlistPage.tsx rename to frontend/src/pages/MarketPage.tsx index 1a2dcfe..0c1ca5f 100644 --- a/frontend/src/pages/WatchlistPage.tsx +++ b/frontend/src/pages/MarketPage.tsx @@ -1,13 +1,22 @@ import { useMemo, useState } from 'react'; +import { useSearchParams } from 'react-router-dom'; import { useWatchlist } from '../hooks/useWatchlist'; +import { useRankings } from '../hooks/useScores'; import { WatchlistTable } from '../components/watchlist/WatchlistTable'; import { AddTickerForm } from '../components/watchlist/AddTickerForm'; +import { RankingsTable } from '../components/rankings/RankingsTable'; +import { WeightsForm } from '../components/rankings/WeightsForm'; import { Callout } from '../components/ui/Callout'; +import { Disclosure } from '../components/ui/Disclosure'; import { Select } from '../components/ui/Field'; import { PageHeader } from '../components/ui/PageHeader'; import { SkeletonTable } from '../components/ui/Skeleton'; +import { Tabs } from '../components/ui/Tabs'; import type { WatchlistEntry } from '../lib/types'; +const tabs = ['Watchlist', 'Rankings'] as const; +type Tab = (typeof tabs)[number]; + type SortMode = 'name_asc' | 'name_desc' | 'score_desc' | 'score_asc'; function sortEntries(entries: WatchlistEntry[], mode: SortMode): WatchlistEntry[] { @@ -42,7 +51,7 @@ function sortEntries(entries: WatchlistEntry[], mode: SortMode): WatchlistEntry[ return sorted; } -export default function WatchlistPage() { +function WatchlistPanel() { const { data, isLoading, isError, error } = useWatchlist(); const [sortMode, setSortMode] = useState('score_desc'); @@ -52,16 +61,15 @@ export default function WatchlistPage() { ); return ( -
- } /> - +
{isLoading && } {isError && {error?.message || 'Failed to load watchlist'}} {data && ( -
-
+ <> +
+