diff --git a/src/operator-dashboard/static/App.jsx b/src/operator-dashboard/static/App.jsx index 8509a45..e8d8c1a 100644 --- a/src/operator-dashboard/static/App.jsx +++ b/src/operator-dashboard/static/App.jsx @@ -31,6 +31,7 @@ function LoadingPanel({ error, onRetry }) { export default function App() { const [state, dispatch] = useReducer(dashboardReducer, initialDashboardState); const currentPage = state.page || state.dashboard?.default_page || 'funds'; + const isStrategyPage = currentPage === 'strategy' || currentPage.startsWith('strategy-'); const isReadyForSocket = Boolean(state.session && state.dashboard); const criticalBanner = null; @@ -178,8 +179,12 @@ export default function App() { onControl={submitControl} /> ) : null} - {currentPage === 'strategy' ? ( - + {isStrategyPage ? ( + ) : null} {currentPage === 'system' ? ( diff --git a/src/operator-dashboard/static/components/NavRail.jsx b/src/operator-dashboard/static/components/NavRail.jsx index ca18a92..25bfaad 100644 --- a/src/operator-dashboard/static/components/NavRail.jsx +++ b/src/operator-dashboard/static/components/NavRail.jsx @@ -6,8 +6,33 @@ const NAV_ITEMS = [ }, { page: 'strategy', - title: 'Strategy', - description: 'Trading state, decision flow, and guarded omissions.', + title: 'Strategy overview', + description: 'Quote, response, and settlement counters.', + }, + { + page: 'strategy-pairs', + title: 'Pair config', + description: 'Directed pairs, edges, limits, and response policy.', + }, + { + page: 'strategy-competitiveness', + title: 'Competitiveness', + description: 'Timing, quote-age, relay result, and outcome summaries.', + }, + { + page: 'strategy-assets', + title: 'Asset registry', + description: 'Supported-token import and deposit addresses.', + }, + { + page: 'strategy-trades', + title: 'Successful trades', + description: 'Only quotes with proven asset movement.', + }, + { + page: 'strategy-lifecycle', + title: 'Quote lifecycle', + description: 'Incoming quotes and what happened next.', }, { page: 'system', diff --git a/src/operator-dashboard/static/pages/StrategyPage.jsx b/src/operator-dashboard/static/pages/StrategyPage.jsx index a28afa8..72ca318 100644 --- a/src/operator-dashboard/static/pages/StrategyPage.jsx +++ b/src/operator-dashboard/static/pages/StrategyPage.jsx @@ -1203,77 +1203,100 @@ function PairConfigSection({ assetCatalog, pairConfig, onControl }) { ); } -export default function StrategyPage({ strategy, onControl }) { +function StrategyOverviewSection({ strategy }) { const funnel = strategy.strategy_state.trade_funnel || {}; const counts = funnel.counts || {}; return ( - <> -
-
-
-
Trading evidence
-

Quotes, responses, and proven trades

-
- One place for quote truth: every row starts at the incoming quote, then shows whether we responded, why not, and whether any asset movement was proven. -
+
+
+
+
Trading evidence
+

Quotes, responses, and proven trades

+
+ One place for quote truth: every row starts at the incoming quote, then shows whether we responded, why not, and whether any asset movement was proven.
-
- - - - - - - -
-
- - - - - - - -
-
-
-
Successful trades only
-

Trades with proven asset movement

-
- This table excludes submitted-only quote responses. Realized PnL remains unavailable until fees and venue-native terminal fills are stored. -
-
-
- 0 ? 'healthy' : 'unknown'} /> - - 0 ? 'warning' : 'unknown'} /> -
-
- -
- -
-
-
-
Quote lifecycle
-

Incoming quotes and what happened next

-
- Full-width quote table: incoming quote, response decision, result, decisive reason, and expandable lifecycle stages. -
-
-
- -
- +
+
+ + + + + + + +
+
); } + +function SuccessfulTradesSection({ funnel, counts }) { + return ( +
+
+
+
Successful trades only
+

Trades with proven asset movement

+
+ This table excludes submitted-only quote responses. Realized PnL remains unavailable until fees and venue-native terminal fills are stored. +
+
+
+ 0 ? 'healthy' : 'unknown'} /> + + 0 ? 'warning' : 'unknown'} /> +
+
+ +
+ ); +} + +function QuoteLifecycleSection({ items }) { + return ( +
+
+
+
Quote lifecycle
+

Incoming quotes and what happened next

+
+ Full-width quote table: incoming quote, response decision, result, decisive reason, and expandable lifecycle stages. +
+
+
+ +
+ ); +} + +export default function StrategyPage({ strategy, onControl, page = 'strategy' }) { + const funnel = strategy.strategy_state.trade_funnel || {}; + const counts = funnel.counts || {}; + + switch (page) { + case 'strategy-pairs': + return ; + case 'strategy-competitiveness': + return ( + + ); + case 'strategy-assets': + return ; + case 'strategy-trades': + return ; + case 'strategy-lifecycle': + return ; + case 'strategy': + default: + return ; + } +} diff --git a/test/operator-dashboard-ui-static.test.mjs b/test/operator-dashboard-ui-static.test.mjs index c10aa0c..ae4ab6d 100644 --- a/test/operator-dashboard-ui-static.test.mjs +++ b/test/operator-dashboard-ui-static.test.mjs @@ -9,6 +9,7 @@ const serviceCardSource = readFileSync(new URL('../src/operator-dashboard/static const statusBarSource = readFileSync(new URL('../src/operator-dashboard/static/components/StatusBar.jsx', import.meta.url), 'utf8'); const systemSource = readFileSync(new URL('../src/operator-dashboard/static/pages/SystemPage.jsx', import.meta.url), 'utf8'); const appSource = readFileSync(new URL('../src/operator-dashboard/static/App.jsx', import.meta.url), 'utf8'); +const navSource = readFileSync(new URL('../src/operator-dashboard/static/components/NavRail.jsx', import.meta.url), 'utf8'); test('strategy page owns consolidated quote lifecycle and successful trade tables', () => { assert.match(strategySource, /Quote lifecycle/); @@ -143,10 +144,25 @@ test('strategy page distinguishes configured bps from gross quote edge percent', assert.doesNotMatch(strategySource, /Edge \$\{item\.edge_bps\}%/); }); -test('pair controls are rendered before the long asset catalog table', () => { - assert.ok( - strategySource.indexOf(' { + assert.match(navSource, /Strategy overview/); + assert.match(navSource, /Pair config/); + assert.match(navSource, /Competitiveness/); + assert.match(navSource, /Asset registry/); + assert.match(navSource, /Successful trades/); + assert.match(navSource, /Quote lifecycle/); + assert.match(navSource, /page: 'strategy-pairs'/); + assert.match(navSource, /page: 'strategy-competitiveness'/); + assert.match(navSource, /page: 'strategy-assets'/); + assert.match(navSource, /page: 'strategy-trades'/); + assert.match(navSource, /page: 'strategy-lifecycle'/); + assert.match(appSource, /currentPage\.startsWith\('strategy-'\)/); + assert.match(appSource, /page=\{currentPage\}/); + assert.match(strategySource, /case 'strategy-pairs'/); + assert.match(strategySource, /case 'strategy-competitiveness'/); + assert.match(strategySource, /case 'strategy-assets'/); + assert.match(strategySource, /case 'strategy-trades'/); + assert.match(strategySource, /case 'strategy-lifecycle'/); });