diff --git a/src/operator-dashboard/static/pages/StrategyPage.jsx b/src/operator-dashboard/static/pages/StrategyPage.jsx index 731a0b7..07c66f5 100644 --- a/src/operator-dashboard/static/pages/StrategyPage.jsx +++ b/src/operator-dashboard/static/pages/StrategyPage.jsx @@ -94,6 +94,12 @@ function strategyDecisionStatus(decision) { return plainCodeLabel(decision?.decision, 'No strategy decision'); } +function isStrategyRejected(item) { + return item?.lifecycle_state === 'rejected' + || item?.decision?.decision === 'rejected' + || String(item?.lifecycle_label || '').toLowerCase() === 'rejected by strategy'; +} + function StageCard({ title, at, status, children }) { return (
@@ -148,8 +154,28 @@ function LifecycleDetails({ item }) { function QuoteLifecycleTable({ items }) { const [expanded, setExpanded] = useState(() => new Set()); - const now = useNow(); - if (!items?.length) return No quote lifecycle evidence has been observed yet.; + const [showStrategyRejected, setShowStrategyRejected] = useState(true); + const [quoteDisplayPaused, setQuoteDisplayPaused] = useState(false); + const [displayItems, setDisplayItems] = useState(() => items || []); + const liveNow = useNow(); + const [displayNow, setDisplayNow] = useState(() => Date.now()); + + useEffect(() => { + if (!quoteDisplayPaused) setDisplayItems(items || []); + }, [items, quoteDisplayPaused]); + + useEffect(() => { + if (!quoteDisplayPaused) setDisplayNow(liveNow); + }, [liveNow, quoteDisplayPaused]); + + const rejectedCount = useMemo( + () => displayItems.filter((item) => isStrategyRejected(item)).length, + [displayItems], + ); + const visibleItems = useMemo( + () => (showStrategyRejected ? displayItems : displayItems.filter((item) => !isStrategyRejected(item))), + [displayItems, showStrategyRejected], + ); function toggle(rowKey) { setExpanded((current) => { @@ -161,67 +187,95 @@ function QuoteLifecycleTable({ items }) { } return ( - - - - - - - - - - - - - - - - {items.map((item, index) => { - const rowKey = item.quote_id || item.decision_id || item.command_id || item.latest_stage_at || String(index); - const isExpanded = expanded.has(rowKey); - const quoteTime = item.quote_activity_at || item.latest_stage_at; - return ( - - - +
Quote timeQuote idRequestResponded?ResultReasonEdge / notionalLifecycle
-
{formatTimestamp(quoteTime)}
-
{formatRelativeAge(quoteTime, now)}
- {item.latest_stage_at && item.latest_stage_at !== item.quote_activity_at ? ( -
Updated {formatTimestamp(item.latest_stage_at)} · {formatRelativeAge(item.latest_stage_at, now)}
+ <> +
+ + + {quoteDisplayPaused ? : null} +
+ + {!displayItems.length ? ( + No quote lifecycle evidence has been observed yet. + ) : !visibleItems.length ? ( + No quote lifecycle rows match the current filters. + ) : ( + + + + + + + + + + + + + + + + {visibleItems.map((item, index) => { + const rowKey = item.quote_id || item.decision_id || item.command_id || item.latest_stage_at || String(index); + const isExpanded = expanded.has(rowKey); + const quoteTime = item.quote_activity_at || item.latest_stage_at; + return ( + + + + + + + + + + + + {isExpanded ? ( + + + ) : null} - - - - - - - - - - {isExpanded ? ( - - - - ) : null} - - ); - })} - -
Quote timeQuote idRequestResponded?ResultReasonEdge / notionalLifecycle
+
{formatTimestamp(quoteTime)}
+
{formatRelativeAge(quoteTime, displayNow)}
+ {item.latest_stage_at && item.latest_stage_at !== item.quote_activity_at ? ( +
Updated {formatTimestamp(item.latest_stage_at)} · {formatRelativeAge(item.latest_stage_at, displayNow)}
+ ) : null} +
+
{formatTerms(item.request_terms || item.submitted_terms)}
+
{truncateMiddle(item.pair || '', 34)}
+
{responseLabel(item)} +
{item.reason_text}
+
{item.reason_code || 'reason_unknown'}
+
+
0 ? 'value-positive' : Number(item.gross_edge_pct) < 0 ? 'value-negative' : ''}>{item.gross_edge_pct ? `${item.gross_edge_pct}%` : 'Unavailable'}
+
{notionalLabel(item)}
+
+ +
-
{formatTerms(item.request_terms || item.submitted_terms)}
-
{truncateMiddle(item.pair || '', 34)}
-
{responseLabel(item)} -
{item.reason_text}
-
{item.reason_code || 'reason_unknown'}
-
-
0 ? 'value-positive' : Number(item.gross_edge_pct) < 0 ? 'value-negative' : ''}>{item.gross_edge_pct ? `${item.gross_edge_pct}%` : 'Unavailable'}
-
{notionalLabel(item)}
-
- -
-
+ + ); + })} +
+
+ )} + ); } diff --git a/src/operator-dashboard/static/styles.css b/src/operator-dashboard/static/styles.css index 248ab67..c4e975d 100644 --- a/src/operator-dashboard/static/styles.css +++ b/src/operator-dashboard/static/styles.css @@ -544,6 +544,33 @@ table.lifecycle-table th:nth-child(5) { table-layout: fixed; } +.quote-lifecycle-controls { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 10px; + margin-bottom: 12px; +} + +.toggle-field { + display: inline-flex; + align-items: center; + gap: 8px; + min-height: 43px; + padding: 10px 12px; + border: 1px solid var(--line); + border-radius: 14px; + background: rgba(24, 33, 30, 0.08); + color: var(--ink); + cursor: pointer; +} + +.toggle-field input { + width: 16px; + height: 16px; + accent-color: var(--accent); +} + .quote-lifecycle-table th:nth-child(1), .quote-lifecycle-table td:nth-child(1) { width: 150px; diff --git a/test/operator-dashboard-ui-static.test.mjs b/test/operator-dashboard-ui-static.test.mjs index 550aa24..f6a6b35 100644 --- a/test/operator-dashboard-ui-static.test.mjs +++ b/test/operator-dashboard-ui-static.test.mjs @@ -21,6 +21,12 @@ test('strategy page owns consolidated quote lifecycle and successful trade table assert.match(strategySource, /Show lifecycle/); assert.match(strategySource, /formatAgeFromTimestamp/); assert.match(strategySource, /quote-row-flash/); + assert.match(strategySource, /showStrategyRejected/); + assert.match(strategySource, /Rejected by strategy/); + assert.match(strategySource, /quoteDisplayPaused/); + assert.match(strategySource, /Pause display/); + assert.match(strategySource, /Resume display/); + assert.match(strategySource, /setDisplayItems\(items \|\| \[\]\)/); assert.match(strategySource, /Submitted means the relay accepted the response; it does not prove a trade\./); assert.doesNotMatch(strategySource, /Actionable|actionable/); });