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 }) { - + @@ -156,7 +156,12 @@ function QuoteLifecycleTable({ items }) { return ( - +
TimeQuote time Quote id Request Responded?
{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({