Add quote lifecycle display controls
All checks were successful
deploy / deploy (push) Successful in 47s
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:
parent
6ff3f55b0f
commit
6f31879480
3 changed files with 149 additions and 62 deletions
|
|
@ -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,6 +187,32 @@ function QuoteLifecycleTable({ items }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
<div className="quote-lifecycle-controls">
|
||||||
|
<label className="toggle-field">
|
||||||
|
<input
|
||||||
|
checked={showStrategyRejected}
|
||||||
|
onChange={(event) => setShowStrategyRejected(event.target.checked)}
|
||||||
|
type="checkbox"
|
||||||
|
/>
|
||||||
|
<span>{`Rejected by strategy (${rejectedCount})`}</span>
|
||||||
|
</label>
|
||||||
|
<button
|
||||||
|
aria-pressed={quoteDisplayPaused}
|
||||||
|
className="button secondary"
|
||||||
|
onClick={() => setQuoteDisplayPaused((paused) => !paused)}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
{quoteDisplayPaused ? 'Resume display' : 'Pause display'}
|
||||||
|
</button>
|
||||||
|
{quoteDisplayPaused ? <Pill label="Display paused" stateLabel="warning" /> : null}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!displayItems.length ? (
|
||||||
|
<EmptyState>No quote lifecycle evidence has been observed yet.</EmptyState>
|
||||||
|
) : !visibleItems.length ? (
|
||||||
|
<EmptyState>No quote lifecycle rows match the current filters.</EmptyState>
|
||||||
|
) : (
|
||||||
<TableFrame>
|
<TableFrame>
|
||||||
<table className="quote-lifecycle-table">
|
<table className="quote-lifecycle-table">
|
||||||
<thead>
|
<thead>
|
||||||
|
|
@ -176,7 +228,7 @@ function QuoteLifecycleTable({ items }) {
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{items.map((item, index) => {
|
{visibleItems.map((item, index) => {
|
||||||
const rowKey = item.quote_id || item.decision_id || item.command_id || item.latest_stage_at || String(index);
|
const rowKey = item.quote_id || item.decision_id || item.command_id || item.latest_stage_at || String(index);
|
||||||
const isExpanded = expanded.has(rowKey);
|
const isExpanded = expanded.has(rowKey);
|
||||||
const quoteTime = item.quote_activity_at || item.latest_stage_at;
|
const quoteTime = item.quote_activity_at || item.latest_stage_at;
|
||||||
|
|
@ -185,9 +237,9 @@ function QuoteLifecycleTable({ items }) {
|
||||||
<tr className={item.live_flash_at ? 'quote-row-flash' : undefined} key={`${rowKey}:row`}>
|
<tr className={item.live_flash_at ? 'quote-row-flash' : undefined} key={`${rowKey}:row`}>
|
||||||
<td>
|
<td>
|
||||||
<div>{formatTimestamp(quoteTime)}</div>
|
<div>{formatTimestamp(quoteTime)}</div>
|
||||||
<div className="status-subtle quote-age">{formatRelativeAge(quoteTime, now)}</div>
|
<div className="status-subtle quote-age">{formatRelativeAge(quoteTime, displayNow)}</div>
|
||||||
{item.latest_stage_at && item.latest_stage_at !== item.quote_activity_at ? (
|
{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>
|
<div className="status-subtle">Updated {formatTimestamp(item.latest_stage_at)} · {formatRelativeAge(item.latest_stage_at, displayNow)}</div>
|
||||||
) : null}
|
) : null}
|
||||||
</td>
|
</td>
|
||||||
<td><IdentifierRow label="Quote" value={item.quote_id} /></td>
|
<td><IdentifierRow label="Quote" value={item.quote_id} /></td>
|
||||||
|
|
@ -222,6 +274,8 @@ function QuoteLifecycleTable({ items }) {
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</TableFrame>
|
</TableFrame>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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/);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue