Add quote lifecycle display controls
All checks were successful
deploy / deploy (push) Successful in 47s

Proof: npm test passes 238/238; operator-dashboard static UI test covers the rejected-by-strategy filter and pause/resume display controls; operator-dashboard bundle builds successfully.

Assumptions: pausing the quote lifecycle table should freeze only the rendered rows and relative-age display while socket state and runtime processing continue normally.

Still fake: venue-native terminal fill ids and realized fee/PnL attribution remain unavailable.
This commit is contained in:
philipp 2026-05-18 21:22:00 +02:00
parent 6ff3f55b0f
commit 6f31879480
3 changed files with 149 additions and 62 deletions

View file

@ -94,6 +94,12 @@ function strategyDecisionStatus(decision) {
return plainCodeLabel(decision?.decision, 'No strategy 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 }) { function StageCard({ title, at, status, children }) {
return ( return (
<div className="lifecycle-stage-card"> <div className="lifecycle-stage-card">
@ -148,8 +154,28 @@ function LifecycleDetails({ item }) {
function QuoteLifecycleTable({ items }) { function QuoteLifecycleTable({ items }) {
const [expanded, setExpanded] = useState(() => new Set()); const [expanded, setExpanded] = useState(() => new Set());
const now = useNow(); const [showStrategyRejected, setShowStrategyRejected] = useState(true);
if (!items?.length) return <EmptyState>No quote lifecycle evidence has been observed yet.</EmptyState>; 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) { function toggle(rowKey) {
setExpanded((current) => { setExpanded((current) => {
@ -161,67 +187,95 @@ function QuoteLifecycleTable({ items }) {
} }
return ( return (
<TableFrame> <>
<table className="quote-lifecycle-table"> <div className="quote-lifecycle-controls">
<thead> <label className="toggle-field">
<tr> <input
<th>Quote time</th> checked={showStrategyRejected}
<th>Quote id</th> onChange={(event) => setShowStrategyRejected(event.target.checked)}
<th>Request</th> type="checkbox"
<th>Responded?</th> />
<th>Result</th> <span>{`Rejected by strategy (${rejectedCount})`}</span>
<th>Reason</th> </label>
<th>Edge / notional</th> <button
<th>Lifecycle</th> aria-pressed={quoteDisplayPaused}
</tr> className="button secondary"
</thead> onClick={() => setQuoteDisplayPaused((paused) => !paused)}
<tbody> type="button"
{items.map((item, index) => { >
const rowKey = item.quote_id || item.decision_id || item.command_id || item.latest_stage_at || String(index); {quoteDisplayPaused ? 'Resume display' : 'Pause display'}
const isExpanded = expanded.has(rowKey); </button>
const quoteTime = item.quote_activity_at || item.latest_stage_at; {quoteDisplayPaused ? <Pill label="Display paused" stateLabel="warning" /> : null}
return ( </div>
<Fragment key={rowKey}>
<tr className={item.live_flash_at ? 'quote-row-flash' : undefined} key={`${rowKey}:row`}> {!displayItems.length ? (
<td> <EmptyState>No quote lifecycle evidence has been observed yet.</EmptyState>
<div>{formatTimestamp(quoteTime)}</div> ) : !visibleItems.length ? (
<div className="status-subtle quote-age">{formatRelativeAge(quoteTime, now)}</div> <EmptyState>No quote lifecycle rows match the current filters.</EmptyState>
{item.latest_stage_at && item.latest_stage_at !== item.quote_activity_at ? ( ) : (
<div className="status-subtle">Updated {formatTimestamp(item.latest_stage_at)} · {formatRelativeAge(item.latest_stage_at, now)}</div> <TableFrame>
<table className="quote-lifecycle-table">
<thead>
<tr>
<th>Quote time</th>
<th>Quote id</th>
<th>Request</th>
<th>Responded?</th>
<th>Result</th>
<th>Reason</th>
<th>Edge / notional</th>
<th>Lifecycle</th>
</tr>
</thead>
<tbody>
{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 (
<Fragment key={rowKey}>
<tr className={item.live_flash_at ? 'quote-row-flash' : undefined} key={`${rowKey}:row`}>
<td>
<div>{formatTimestamp(quoteTime)}</div>
<div className="status-subtle quote-age">{formatRelativeAge(quoteTime, displayNow)}</div>
{item.latest_stage_at && item.latest_stage_at !== item.quote_activity_at ? (
<div className="status-subtle">Updated {formatTimestamp(item.latest_stage_at)} · {formatRelativeAge(item.latest_stage_at, displayNow)}</div>
) : null}
</td>
<td><IdentifierRow label="Quote" value={item.quote_id} /></td>
<td>
<div>{formatTerms(item.request_terms || item.submitted_terms)}</div>
<div className="status-subtle mono">{truncateMiddle(item.pair || '', 34)}</div>
</td>
<td>{responseLabel(item)}</td>
<td><Pill label={item.lifecycle_label} stateLabel={item.lifecycle_tone} /></td>
<td>
<div>{item.reason_text}</div>
<div className="status-subtle mono">{item.reason_code || 'reason_unknown'}</div>
</td>
<td>
<div className={Number(item.gross_edge_pct) > 0 ? 'value-positive' : Number(item.gross_edge_pct) < 0 ? 'value-negative' : ''}>{item.gross_edge_pct ? `${item.gross_edge_pct}%` : 'Unavailable'}</div>
<div className="status-subtle">{notionalLabel(item)}</div>
</td>
<td>
<button className="button secondary" onClick={() => toggle(rowKey)} type="button">
{isExpanded ? 'Hide lifecycle' : 'Show lifecycle'}
</button>
</td>
</tr>
{isExpanded ? (
<tr className="lifecycle-expanded-row" key={`${rowKey}:details`}>
<td colSpan={8}><LifecycleDetails item={item} /></td>
</tr>
) : null} ) : null}
</td> </Fragment>
<td><IdentifierRow label="Quote" value={item.quote_id} /></td> );
<td> })}
<div>{formatTerms(item.request_terms || item.submitted_terms)}</div> </tbody>
<div className="status-subtle mono">{truncateMiddle(item.pair || '', 34)}</div> </table>
</td> </TableFrame>
<td>{responseLabel(item)}</td> )}
<td><Pill label={item.lifecycle_label} stateLabel={item.lifecycle_tone} /></td> </>
<td>
<div>{item.reason_text}</div>
<div className="status-subtle mono">{item.reason_code || 'reason_unknown'}</div>
</td>
<td>
<div className={Number(item.gross_edge_pct) > 0 ? 'value-positive' : Number(item.gross_edge_pct) < 0 ? 'value-negative' : ''}>{item.gross_edge_pct ? `${item.gross_edge_pct}%` : 'Unavailable'}</div>
<div className="status-subtle">{notionalLabel(item)}</div>
</td>
<td>
<button className="button secondary" onClick={() => toggle(rowKey)} type="button">
{isExpanded ? 'Hide lifecycle' : 'Show lifecycle'}
</button>
</td>
</tr>
{isExpanded ? (
<tr className="lifecycle-expanded-row" key={`${rowKey}:details`}>
<td colSpan={8}><LifecycleDetails item={item} /></td>
</tr>
) : null}
</Fragment>
);
})}
</tbody>
</table>
</TableFrame>
); );
} }

View file

@ -544,6 +544,33 @@ table.lifecycle-table th:nth-child(5) {
table-layout: fixed; 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 th:nth-child(1),
.quote-lifecycle-table td:nth-child(1) { .quote-lifecycle-table td:nth-child(1) {
width: 150px; width: 150px;

View file

@ -21,6 +21,12 @@ test('strategy page owns consolidated quote lifecycle and successful trade table
assert.match(strategySource, /Show lifecycle/); assert.match(strategySource, /Show lifecycle/);
assert.match(strategySource, /formatAgeFromTimestamp/); assert.match(strategySource, /formatAgeFromTimestamp/);
assert.match(strategySource, /quote-row-flash/); 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.match(strategySource, /Submitted means the relay accepted the response; it does not prove a trade\./);
assert.doesNotMatch(strategySource, /Actionable|actionable/); assert.doesNotMatch(strategySource, /Actionable|actionable/);
}); });