diff --git a/src/core/operator-dashboard.mjs b/src/core/operator-dashboard.mjs index 7570c7d..b09ace2 100644 --- a/src/core/operator-dashboard.mjs +++ b/src/core/operator-dashboard.mjs @@ -961,13 +961,15 @@ export function deriveQuoteLifecycleRows({ gross_edge_pct: outcome?.gross_edge_pct || null, eure_notional: outcome?.eure_notional || null, outcome, + command_at: outcome?.command_at || null, + execution_result_at: outcome?.submitted_at || null, outcome_observed_at: outcome?.outcome_observed_at || outcome?.submitted_at || null, }); } const finalized = [...rowsByKey.values()] .map((row) => finalizeLifecycleRow(row)) - .sort((left, right) => sortTimestamps(right.latest_stage_at, left.latest_stage_at)); + .sort((left, right) => sortTimestamps(right.quote_activity_at, left.quote_activity_at)); return limit == null ? finalized : finalized.slice(0, limit); } @@ -1101,6 +1103,13 @@ function finalizeLifecycleRow(row) { || row.decision_at || row.quote_observed_at || null, + quote_activity_at: + row.quote_observed_at + || row.decision_at + || row.command_at + || row.execution_result_at + || row.outcome_observed_at + || null, stage_details: { quote_observed_at: row.quote_observed_at, decision_at: row.decision_at, diff --git a/src/operator-dashboard/static/pages/StrategyPage.jsx b/src/operator-dashboard/static/pages/StrategyPage.jsx index df09e21..b9a82be 100644 --- a/src/operator-dashboard/static/pages/StrategyPage.jsx +++ b/src/operator-dashboard/static/pages/StrategyPage.jsx @@ -139,7 +139,7 @@ function QuoteLifecycleTable({ items }) {
| Time | +Quote time | Quote id | Request | Responded? | @@ -156,7 +156,12 @@ function QuoteLifecycleTable({ items }) { return (||||
|---|---|---|---|---|---|---|---|---|
| {formatTimestamp(item.latest_stage_at)} | +
+ {formatTimestamp(item.quote_activity_at || item.latest_stage_at)}
+ {item.latest_stage_at && item.latest_stage_at !== item.quote_activity_at ? (
+ Updated {formatTimestamp(item.latest_stage_at)}
+ ) : null}
+ |
{formatTerms(item.request_terms || item.submitted_terms)}
diff --git a/test/operator-dashboard.test.mjs b/test/operator-dashboard.test.mjs
index 2dcd9e3..8838806 100644
--- a/test/operator-dashboard.test.mjs
+++ b/test/operator-dashboard.test.mjs
@@ -670,6 +670,56 @@ test('submitted lifecycle evidence never becomes completed by itself', () => {
assert.doesNotMatch(`${submitted[0].lifecycle_label} ${submitted[0].reason_text}`, /completed|successful trade|asset delta/i);
});
+test('quote lifecycle recency is anchored to quote or submission time, not later outcome recompute', () => {
+ const rows = deriveQuoteLifecycleRows({
+ recentTradeDecisions: [{
+ observed_at: '2026-04-13T22:33:18.000Z',
+ payload: {
+ decision_id: 'decision-current',
+ quote_id: 'quote-current',
+ pair: 'btc->eure',
+ decision: 'actionable',
+ decision_reason: 'actionable',
+ gross_edge_pct: '0.490000',
+ },
+ }],
+ recentExecutionResults: [{
+ command_id: 'cmd-current',
+ decision_id: 'decision-current',
+ quote_id: 'quote-current',
+ pair: 'btc->eure',
+ result_at: '2026-04-13T22:33:19.000Z',
+ status: 'submitted',
+ result_code: 'quote_response_ok',
+ }],
+ recentQuoteOutcomes: [{
+ command_id: 'cmd-old',
+ decision_id: 'decision-old',
+ quote_id: 'quote-old-two-percent',
+ pair: 'btc->eure',
+ gross_edge_pct: '2.000000',
+ eure_notional: '75.58',
+ submitted_at: '2026-04-09T23:36:35.566Z',
+ command_at: '2026-04-09T23:36:35.352Z',
+ outcome_status: 'not_filled',
+ outcome_reason: 'deadline_elapsed_without_settlement',
+ outcome_observed_at: '2026-04-13T22:35:33.295Z',
+ outcome_source: 'submission_deadline_and_inventory_snapshots',
+ attribution_status: 'unattributed',
+ attributed_inventory_delta: null,
+ }],
+ limit: null,
+ });
+
+ assert.equal(rows[0].quote_id, 'quote-current');
+ assert.equal(rows[0].gross_edge_pct, '0.490000');
+
+ const oldRow = rows.find((row) => row.quote_id === 'quote-old-two-percent');
+ assert.equal(oldRow.quote_activity_at, '2026-04-09T23:36:35.352Z');
+ assert.equal(oldRow.latest_stage_at, '2026-04-13T22:35:33.295Z');
+ assert.equal(oldRow.lifecycle_state, 'not_filled');
+});
+
test('successful trade rows require completed outcome with linked settled inventory evidence', () => {
const config = buildConfig();
const bootstrap = buildDashboardBootstrap({
|