Implement quote lifecycle investigator
All checks were successful
deploy / deploy (push) Successful in 57s
All checks were successful
deploy / deploy (push) Successful in 57s
Proof: Adds durable quote lifecycle lookup by quote_id, decision_id, or command_id through the authenticated dashboard API, pins selected Quote lifecycle evidence outside the rolling table, and covers the Awaiting executor follow-up regression with targeted tests. Assumptions: Existing swap demand, trade decision, execute command, executor result, and quote outcome history rows are sufficient to reconstruct the selected quote lifecycle; this turn stays read-side and does not alter strategy, pricing, limits, arming, signer identity, pair enablement, relay, or executor behavior. Still fake: Venue-native final fill truth, fee-aware realized PnL, already-pruned historical detail, and the broader quote analytics workbench remain incomplete.
This commit is contained in:
parent
0902cb2c93
commit
cb87910c95
9 changed files with 1332 additions and 14 deletions
|
|
@ -14,6 +14,7 @@ import {
|
|||
buildDashboardBootstrap,
|
||||
buildDashboardControlErrorResponse,
|
||||
buildLiveQuoteLifecycleRows,
|
||||
buildQuoteLifecycleLookupResponse,
|
||||
buildLiveStatusBar,
|
||||
createDashboardLiveState,
|
||||
isDashboardWebSocketBackpressured,
|
||||
|
|
@ -44,6 +45,7 @@ import {
|
|||
loadLatestMarketPrice,
|
||||
loadLatestPortfolioMetric,
|
||||
loadPairConfigSummary,
|
||||
loadQuoteLifecycleLookupEvidence,
|
||||
loadQuoteLifecycleRetentionSummary,
|
||||
loadRecentAlertTransitions,
|
||||
loadRecentDepositStatuses,
|
||||
|
|
@ -378,6 +380,38 @@ async function handleApiRequest({ req, res, url, auth }) {
|
|||
return sendJson(res, 200, submissionPage);
|
||||
}
|
||||
|
||||
const lifecycleLookupMatch = req.method === 'GET'
|
||||
? url.pathname.match(/^\/api\/strategy\/quote-lifecycle\/(.+)$/)
|
||||
: null;
|
||||
if (lifecycleLookupMatch) {
|
||||
const identifier = decodeURIComponent(lifecycleLookupMatch[1] || '').trim();
|
||||
if (!identifier) {
|
||||
return sendJson(res, 400, {
|
||||
ok: false,
|
||||
error: 'quote_lifecycle_identifier_required',
|
||||
lifecycle_row: null,
|
||||
lookup: {
|
||||
requested_identifier: '',
|
||||
matched_identifier_type: null,
|
||||
found: false,
|
||||
lookup_at: new Date().toISOString(),
|
||||
latest_stage_at: null,
|
||||
matched_identifiers: {
|
||||
quote_ids: [],
|
||||
decision_ids: [],
|
||||
command_ids: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const payload = await loadQuoteLifecycleLookupPayload({ identifier });
|
||||
return sendJson(res, payload.ok ? 200 : 404, {
|
||||
...payload,
|
||||
...(payload.ok ? {} : { error: 'quote_lifecycle_not_found' }),
|
||||
});
|
||||
}
|
||||
|
||||
const controlMatch = req.method === 'POST'
|
||||
? url.pathname.match(/^\/api\/control\/([^/]+)\/([^/]+)$/)
|
||||
: null;
|
||||
|
|
@ -597,6 +631,19 @@ async function loadBootstrapPayload({ auth, page, pageSize }) {
|
|||
return payload;
|
||||
}
|
||||
|
||||
async function loadQuoteLifecycleLookupPayload({ identifier }) {
|
||||
const tradingConfig = await tradingConfigStore.forceRefresh();
|
||||
const runtimeConfig = buildRuntimeConfig(tradingConfig);
|
||||
const evidence = await loadQuoteLifecycleLookupEvidence(pool, {
|
||||
identifier,
|
||||
});
|
||||
return buildQuoteLifecycleLookupResponse({
|
||||
config: runtimeConfig,
|
||||
identifier,
|
||||
evidence,
|
||||
});
|
||||
}
|
||||
|
||||
async function loadServiceSnapshots() {
|
||||
const services = listDashboardServices(config);
|
||||
return Promise.all(services.map((service) => loadServiceSnapshot(service)));
|
||||
|
|
|
|||
|
|
@ -1130,6 +1130,7 @@ const HUMAN_REASON_TEXT = {
|
|||
pending_deposit_not_credited: 'Funding is not credited yet.',
|
||||
pending_outbound_reserved: 'Pending outbound inventory is reserved.',
|
||||
quote_expired: 'Quote expired.',
|
||||
quote_not_found_or_finished: 'Relay reported the quote as not found or already finished.',
|
||||
quote_response_ack: 'Quote response acknowledged by the relay.',
|
||||
quote_response_ok: 'Quote response accepted by the relay.',
|
||||
awaiting_outcome: 'No durable venue outcome is recorded yet.',
|
||||
|
|
@ -1425,7 +1426,11 @@ function finalizeLifecycleRow(row) {
|
|||
} else if (execution?.status === 'failed') {
|
||||
lifecycle_state = 'failed';
|
||||
lifecycle_label = 'Submission failed';
|
||||
reason_code = normalizeLifecycleToken(execution?.result_code || 'submission_failed');
|
||||
reason_code = normalizeLifecycleToken(
|
||||
execution?.failure_category
|
||||
|| execution?.result_code
|
||||
|| 'submission_failed',
|
||||
);
|
||||
reason_text = buildExecutionFailureText(execution, reason_code);
|
||||
} else if (execution?.status === 'rejected') {
|
||||
lifecycle_state = 'blocked';
|
||||
|
|
@ -1504,6 +1509,100 @@ function finalizeLifecycleRow(row) {
|
|||
};
|
||||
}
|
||||
|
||||
export function buildQuoteLifecycleLookupResponse({
|
||||
config,
|
||||
identifier,
|
||||
evidence = {},
|
||||
now = new Date().toISOString(),
|
||||
} = {}) {
|
||||
const requestedIdentifier = String(identifier || '').trim();
|
||||
const rows = deriveQuoteLifecycleRows({
|
||||
recentQuotes: evidence.recentQuotes || [],
|
||||
recentTradeDecisions: evidence.recentTradeDecisions || [],
|
||||
recentExecuteTradeCommands: evidence.recentExecuteTradeCommands || [],
|
||||
recentExecutionResults: evidence.recentExecutionResults || [],
|
||||
recentQuoteOutcomes: evidence.recentQuoteOutcomes || [],
|
||||
limit: null,
|
||||
}).map((row) => enrichLifecycleRowForUi({ config, row }));
|
||||
const lifecycleRow = selectLookupLifecycleRow(rows, requestedIdentifier);
|
||||
const found = Boolean(lifecycleRow);
|
||||
const lookup = {
|
||||
requested_identifier: requestedIdentifier,
|
||||
matched_identifier_type: found
|
||||
? inferLookupIdentifierType(requestedIdentifier, lifecycleRow, evidence)
|
||||
: null,
|
||||
found,
|
||||
lookup_at: evidence.lookup_at || now,
|
||||
latest_stage_at: lifecycleRow?.latest_stage_at || null,
|
||||
matched_identifiers: found
|
||||
? collectLifecycleIdentifiers(lifecycleRow, evidence.matched_identifiers)
|
||||
: normalizeMatchedIdentifiers(evidence.matched_identifiers),
|
||||
};
|
||||
|
||||
return {
|
||||
ok: found,
|
||||
lifecycle_row: lifecycleRow || null,
|
||||
lookup,
|
||||
};
|
||||
}
|
||||
|
||||
function selectLookupLifecycleRow(rows, identifier) {
|
||||
if (!rows.length) return null;
|
||||
const normalizedIdentifier = String(identifier || '').trim();
|
||||
if (!normalizedIdentifier) return rows[0] || null;
|
||||
return rows.find((row) => lifecycleRowHasIdentifier(row, normalizedIdentifier))
|
||||
|| rows[0]
|
||||
|| null;
|
||||
}
|
||||
|
||||
function lifecycleRowHasIdentifier(row, identifier) {
|
||||
return row?.quote_id === identifier
|
||||
|| row?.decision_id === identifier
|
||||
|| row?.command_id === identifier;
|
||||
}
|
||||
|
||||
function inferLookupIdentifierType(identifier, row, evidence = {}) {
|
||||
if (evidence.matched_identifier_type) return evidence.matched_identifier_type;
|
||||
if (row?.quote_id === identifier) return 'quote_id';
|
||||
if (row?.decision_id === identifier) return 'decision_id';
|
||||
if (row?.command_id === identifier) return 'command_id';
|
||||
return 'linked_identifier';
|
||||
}
|
||||
|
||||
function collectLifecycleIdentifiers(row, matchedIdentifiers = null) {
|
||||
const normalized = normalizeMatchedIdentifiers(matchedIdentifiers);
|
||||
return {
|
||||
quote_ids: uniqueStrings([
|
||||
...normalized.quote_ids,
|
||||
row?.quote_id,
|
||||
]),
|
||||
decision_ids: uniqueStrings([
|
||||
...normalized.decision_ids,
|
||||
row?.decision_id,
|
||||
]),
|
||||
command_ids: uniqueStrings([
|
||||
...normalized.command_ids,
|
||||
row?.command_id,
|
||||
]),
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeMatchedIdentifiers(value = null) {
|
||||
return {
|
||||
quote_ids: uniqueStrings(value?.quote_ids),
|
||||
decision_ids: uniqueStrings(value?.decision_ids),
|
||||
command_ids: uniqueStrings(value?.command_ids),
|
||||
};
|
||||
}
|
||||
|
||||
function uniqueStrings(values = []) {
|
||||
return [...new Set(
|
||||
(values || [])
|
||||
.map((value) => String(value || '').trim())
|
||||
.filter(Boolean),
|
||||
)];
|
||||
}
|
||||
|
||||
function buildPolicySkipText(decision, reasonCode) {
|
||||
const base = humanizeReasonCode(reasonCode, 'Policy skipped the response.');
|
||||
const policy = decision?.response_policy || {};
|
||||
|
|
|
|||
|
|
@ -3868,6 +3868,354 @@ export async function loadRecentQuoteOutcomes(pool, { limit = 200 } = {}) {
|
|||
return result.rows.map(normalizeQuoteOutcomeRow);
|
||||
}
|
||||
|
||||
export async function loadQuoteLifecycleLookupEvidence(pool, { identifier } = {}) {
|
||||
const requestedIdentifier = String(identifier || '').trim();
|
||||
const lookupAt = new Date().toISOString();
|
||||
if (!requestedIdentifier) {
|
||||
return buildQuoteLifecycleLookupEvidence({
|
||||
requestedIdentifier,
|
||||
lookupAt,
|
||||
});
|
||||
}
|
||||
|
||||
const candidatesResult = await pool.query(
|
||||
`
|
||||
WITH candidates AS (
|
||||
SELECT
|
||||
'quote_id' AS matched_identifier_type,
|
||||
COALESCE(quote_id, payload->>'quote_id') AS quote_id,
|
||||
payload->>'decision_id' AS decision_id,
|
||||
payload->>'command_id' AS command_id,
|
||||
COALESCE(observed_at, ingested_at) AS matched_at
|
||||
FROM swap_demand_events
|
||||
WHERE quote_id = $1
|
||||
OR payload->>'quote_id' = $1
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT
|
||||
CASE
|
||||
WHEN quote_id = $1 OR payload->>'quote_id' = $1 THEN 'quote_id'
|
||||
WHEN decision_key = $1 OR payload->>'decision_id' = $1 THEN 'decision_id'
|
||||
WHEN payload->>'command_id' = $1 THEN 'command_id'
|
||||
ELSE 'linked_identifier'
|
||||
END AS matched_identifier_type,
|
||||
COALESCE(quote_id, payload->>'quote_id') AS quote_id,
|
||||
COALESCE(payload->>'decision_id', decision_key) AS decision_id,
|
||||
payload->>'command_id' AS command_id,
|
||||
COALESCE(observed_at, ingested_at) AS matched_at
|
||||
FROM trade_decisions
|
||||
WHERE quote_id = $1
|
||||
OR decision_key = $1
|
||||
OR payload->>'quote_id' = $1
|
||||
OR payload->>'decision_id' = $1
|
||||
OR payload->>'command_id' = $1
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT
|
||||
CASE
|
||||
WHEN quote_id = $1 OR payload->>'quote_id' = $1 THEN 'quote_id'
|
||||
WHEN payload->>'decision_id' = $1 THEN 'decision_id'
|
||||
WHEN decision_key = $1 OR payload->>'command_id' = $1 THEN 'command_id'
|
||||
ELSE 'linked_identifier'
|
||||
END AS matched_identifier_type,
|
||||
COALESCE(quote_id, payload->>'quote_id') AS quote_id,
|
||||
payload->>'decision_id' AS decision_id,
|
||||
COALESCE(payload->>'command_id', decision_key) AS command_id,
|
||||
COALESCE(observed_at, ingested_at) AS matched_at
|
||||
FROM execute_trade_commands
|
||||
WHERE quote_id = $1
|
||||
OR decision_key = $1
|
||||
OR payload->>'quote_id' = $1
|
||||
OR payload->>'decision_id' = $1
|
||||
OR payload->>'command_id' = $1
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT
|
||||
CASE
|
||||
WHEN quote_id = $1 OR payload->>'quote_id' = $1 THEN 'quote_id'
|
||||
WHEN payload->>'decision_id' = $1 THEN 'decision_id'
|
||||
WHEN decision_key = $1 OR payload->>'command_id' = $1 THEN 'command_id'
|
||||
ELSE 'linked_identifier'
|
||||
END AS matched_identifier_type,
|
||||
COALESCE(quote_id, payload->>'quote_id') AS quote_id,
|
||||
payload->>'decision_id' AS decision_id,
|
||||
COALESCE(payload->>'command_id', decision_key) AS command_id,
|
||||
COALESCE(observed_at, ingested_at) AS matched_at
|
||||
FROM trade_execution_results
|
||||
WHERE quote_id = $1
|
||||
OR decision_key = $1
|
||||
OR payload->>'quote_id' = $1
|
||||
OR payload->>'decision_id' = $1
|
||||
OR payload->>'command_id' = $1
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT
|
||||
CASE
|
||||
WHEN quote_id = $1 OR payload->>'quote_id' = $1 THEN 'quote_id'
|
||||
WHEN decision_id = $1 OR payload->>'decision_id' = $1 THEN 'decision_id'
|
||||
WHEN command_id = $1 OR payload->>'command_id' = $1 THEN 'command_id'
|
||||
ELSE 'linked_identifier'
|
||||
END AS matched_identifier_type,
|
||||
COALESCE(quote_id, payload->>'quote_id') AS quote_id,
|
||||
COALESCE(decision_id, payload->>'decision_id') AS decision_id,
|
||||
COALESCE(command_id, payload->>'command_id') AS command_id,
|
||||
COALESCE(outcome_observed_at, submitted_at, command_at, computed_at) AS matched_at
|
||||
FROM ${QUOTE_OUTCOMES_TABLE}
|
||||
WHERE quote_id = $1
|
||||
OR decision_id = $1
|
||||
OR command_id = $1
|
||||
OR payload->>'quote_id' = $1
|
||||
OR payload->>'decision_id' = $1
|
||||
OR payload->>'command_id' = $1
|
||||
)
|
||||
SELECT *
|
||||
FROM candidates
|
||||
ORDER BY matched_at DESC NULLS LAST
|
||||
LIMIT 100
|
||||
`,
|
||||
[requestedIdentifier],
|
||||
);
|
||||
|
||||
const identifiers = collectQuoteLifecycleLookupIdentifiers(candidatesResult.rows);
|
||||
if (!hasLookupIdentifiers(identifiers)) {
|
||||
return buildQuoteLifecycleLookupEvidence({
|
||||
requestedIdentifier,
|
||||
lookupAt,
|
||||
matchedIdentifierType: null,
|
||||
matchedIdentifiers: identifiers,
|
||||
});
|
||||
}
|
||||
|
||||
const [
|
||||
recentQuotes,
|
||||
recentTradeDecisions,
|
||||
recentExecuteTradeCommands,
|
||||
recentExecutionResults,
|
||||
recentQuoteOutcomes,
|
||||
] = await Promise.all([
|
||||
loadLifecycleLookupQuotes(pool, identifiers),
|
||||
loadLifecycleLookupTradeDecisions(pool, identifiers),
|
||||
loadLifecycleLookupExecuteTradeCommands(pool, identifiers),
|
||||
loadLifecycleLookupExecutionResults(pool, identifiers),
|
||||
loadLifecycleLookupQuoteOutcomes(pool, identifiers),
|
||||
]);
|
||||
|
||||
return buildQuoteLifecycleLookupEvidence({
|
||||
requestedIdentifier,
|
||||
lookupAt,
|
||||
matchedIdentifierType: inferLookupMatchedIdentifierType(
|
||||
requestedIdentifier,
|
||||
candidatesResult.rows,
|
||||
),
|
||||
matchedIdentifiers: identifiers,
|
||||
recentQuotes,
|
||||
recentTradeDecisions,
|
||||
recentExecuteTradeCommands,
|
||||
recentExecutionResults,
|
||||
recentQuoteOutcomes,
|
||||
});
|
||||
}
|
||||
|
||||
function buildQuoteLifecycleLookupEvidence({
|
||||
requestedIdentifier,
|
||||
lookupAt,
|
||||
matchedIdentifierType = null,
|
||||
matchedIdentifiers = null,
|
||||
recentQuotes = [],
|
||||
recentTradeDecisions = [],
|
||||
recentExecuteTradeCommands = [],
|
||||
recentExecutionResults = [],
|
||||
recentQuoteOutcomes = [],
|
||||
} = {}) {
|
||||
return {
|
||||
found: Boolean(
|
||||
recentQuotes.length
|
||||
|| recentTradeDecisions.length
|
||||
|| recentExecuteTradeCommands.length
|
||||
|| recentExecutionResults.length
|
||||
|| recentQuoteOutcomes.length,
|
||||
),
|
||||
requested_identifier: requestedIdentifier,
|
||||
matched_identifier_type: matchedIdentifierType,
|
||||
matched_identifiers: normalizeQuoteLifecycleLookupIdentifiers(matchedIdentifiers),
|
||||
lookup_at: lookupAt,
|
||||
recentQuotes,
|
||||
recentTradeDecisions,
|
||||
recentExecuteTradeCommands,
|
||||
recentExecutionResults,
|
||||
recentQuoteOutcomes,
|
||||
};
|
||||
}
|
||||
|
||||
function collectQuoteLifecycleLookupIdentifiers(rows = []) {
|
||||
return normalizeQuoteLifecycleLookupIdentifiers({
|
||||
quote_ids: rows.map((row) => row.quote_id),
|
||||
decision_ids: rows.map((row) => row.decision_id),
|
||||
command_ids: rows.map((row) => row.command_id),
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeQuoteLifecycleLookupIdentifiers(value = null) {
|
||||
return {
|
||||
quote_ids: uniqueTextValues(value?.quote_ids),
|
||||
decision_ids: uniqueTextValues(value?.decision_ids),
|
||||
command_ids: uniqueTextValues(value?.command_ids),
|
||||
};
|
||||
}
|
||||
|
||||
function uniqueTextValues(values = []) {
|
||||
return [...new Set(
|
||||
(values || [])
|
||||
.map((value) => String(value || '').trim())
|
||||
.filter(Boolean),
|
||||
)];
|
||||
}
|
||||
|
||||
function hasLookupIdentifiers(identifiers) {
|
||||
return Boolean(
|
||||
identifiers?.quote_ids?.length
|
||||
|| identifiers?.decision_ids?.length
|
||||
|| identifiers?.command_ids?.length,
|
||||
);
|
||||
}
|
||||
|
||||
function inferLookupMatchedIdentifierType(identifier, rows = []) {
|
||||
const requestedIdentifier = String(identifier || '').trim();
|
||||
const direct = rows.find((row) => (
|
||||
row.matched_identifier_type
|
||||
&& (
|
||||
row.quote_id === requestedIdentifier
|
||||
|| row.decision_id === requestedIdentifier
|
||||
|| row.command_id === requestedIdentifier
|
||||
)
|
||||
));
|
||||
return direct?.matched_identifier_type || rows[0]?.matched_identifier_type || null;
|
||||
}
|
||||
|
||||
async function loadLifecycleLookupQuotes(pool, identifiers) {
|
||||
if (!identifiers.quote_ids.length) return [];
|
||||
const result = await pool.query(
|
||||
`
|
||||
SELECT observed_at, ingested_at, payload
|
||||
FROM swap_demand_events
|
||||
WHERE quote_id = ANY($1::text[])
|
||||
OR payload->>'quote_id' = ANY($1::text[])
|
||||
ORDER BY COALESCE(observed_at, ingested_at) DESC
|
||||
LIMIT 20
|
||||
`,
|
||||
[identifiers.quote_ids],
|
||||
);
|
||||
return result.rows.map(normalizeRecentQuoteRow);
|
||||
}
|
||||
|
||||
async function loadLifecycleLookupTradeDecisions(pool, identifiers) {
|
||||
const result = await pool.query(
|
||||
`
|
||||
SELECT observed_at, ingested_at, payload
|
||||
FROM trade_decisions
|
||||
WHERE quote_id = ANY($1::text[])
|
||||
OR payload->>'quote_id' = ANY($1::text[])
|
||||
OR decision_key = ANY($2::text[])
|
||||
OR payload->>'decision_id' = ANY($2::text[])
|
||||
ORDER BY COALESCE(observed_at, ingested_at) DESC
|
||||
LIMIT 20
|
||||
`,
|
||||
[identifiers.quote_ids, identifiers.decision_ids],
|
||||
);
|
||||
return result.rows.map((row) => ({
|
||||
observed_at: toIsoTimestamp(row.observed_at),
|
||||
ingested_at: toIsoTimestamp(row.ingested_at),
|
||||
payload: row.payload,
|
||||
}));
|
||||
}
|
||||
|
||||
async function loadLifecycleLookupExecuteTradeCommands(pool, identifiers) {
|
||||
const result = await pool.query(
|
||||
`
|
||||
SELECT observed_at, ingested_at, payload
|
||||
FROM execute_trade_commands
|
||||
WHERE quote_id = ANY($1::text[])
|
||||
OR payload->>'quote_id' = ANY($1::text[])
|
||||
OR payload->>'decision_id' = ANY($2::text[])
|
||||
OR decision_key = ANY($3::text[])
|
||||
OR payload->>'command_id' = ANY($3::text[])
|
||||
ORDER BY COALESCE(observed_at, ingested_at) DESC
|
||||
LIMIT 20
|
||||
`,
|
||||
[identifiers.quote_ids, identifiers.decision_ids, identifiers.command_ids],
|
||||
);
|
||||
return result.rows.map((row) => normalizeExecuteTradeCommandRow(row));
|
||||
}
|
||||
|
||||
async function loadLifecycleLookupExecutionResults(pool, identifiers) {
|
||||
const result = await pool.query(
|
||||
`
|
||||
SELECT
|
||||
r.observed_at AS result_observed_at,
|
||||
r.ingested_at AS result_ingested_at,
|
||||
r.payload AS result_payload,
|
||||
c.ingested_at AS command_ingested_at,
|
||||
c.payload AS command_payload,
|
||||
d.payload AS decision_payload,
|
||||
o.payload AS outcome_payload
|
||||
FROM trade_execution_results r
|
||||
LEFT JOIN execute_trade_commands c
|
||||
ON c.decision_key = r.decision_key
|
||||
LEFT JOIN trade_decisions d
|
||||
ON d.decision_key = COALESCE(c.payload->>'decision_id', r.payload->>'decision_id')
|
||||
LEFT JOIN ${QUOTE_OUTCOMES_TABLE} o
|
||||
ON o.quote_id = r.quote_id
|
||||
WHERE r.quote_id = ANY($1::text[])
|
||||
OR r.payload->>'quote_id' = ANY($1::text[])
|
||||
OR r.payload->>'decision_id' = ANY($2::text[])
|
||||
OR r.decision_key = ANY($3::text[])
|
||||
OR r.payload->>'command_id' = ANY($3::text[])
|
||||
ORDER BY COALESCE(r.observed_at, r.ingested_at) DESC
|
||||
LIMIT 20
|
||||
`,
|
||||
[identifiers.quote_ids, identifiers.decision_ids, identifiers.command_ids],
|
||||
);
|
||||
return result.rows.map((row) => normalizeExecutionResultRow(row));
|
||||
}
|
||||
|
||||
async function loadLifecycleLookupQuoteOutcomes(pool, identifiers) {
|
||||
const result = await pool.query(
|
||||
`
|
||||
SELECT
|
||||
quote_id,
|
||||
decision_id,
|
||||
command_id,
|
||||
execution_result_status,
|
||||
execution_result_code,
|
||||
submitted_at,
|
||||
command_at,
|
||||
outcome_status,
|
||||
outcome_observed_at,
|
||||
outcome_source,
|
||||
attribution_status,
|
||||
attribution_method,
|
||||
attributed_inventory_delta,
|
||||
computed_at,
|
||||
payload
|
||||
FROM ${QUOTE_OUTCOMES_TABLE}
|
||||
WHERE quote_id = ANY($1::text[])
|
||||
OR payload->>'quote_id' = ANY($1::text[])
|
||||
OR decision_id = ANY($2::text[])
|
||||
OR payload->>'decision_id' = ANY($2::text[])
|
||||
OR command_id = ANY($3::text[])
|
||||
OR payload->>'command_id' = ANY($3::text[])
|
||||
ORDER BY COALESCE(outcome_observed_at, submitted_at, command_at, computed_at) DESC
|
||||
LIMIT 20
|
||||
`,
|
||||
[identifiers.quote_ids, identifiers.decision_ids, identifiers.command_ids],
|
||||
);
|
||||
return result.rows.map(normalizeQuoteOutcomeRow);
|
||||
}
|
||||
|
||||
export async function loadIntentRequestPreflightByIdOrKey(pool, {
|
||||
requestId = null,
|
||||
idempotencyKey = null,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,91 @@
|
|||
export function lifecycleRowIdentifier(row = null) {
|
||||
return row?.quote_id || row?.decision_id || row?.command_id || null;
|
||||
}
|
||||
|
||||
export function findLifecycleRowByIdentifier(rows = [], identifier = '') {
|
||||
const normalizedIdentifier = String(identifier || '').trim();
|
||||
if (!normalizedIdentifier) return null;
|
||||
return (rows || []).find((row) => (
|
||||
row?.quote_id === normalizedIdentifier
|
||||
|| row?.decision_id === normalizedIdentifier
|
||||
|| row?.command_id === normalizedIdentifier
|
||||
)) || null;
|
||||
}
|
||||
|
||||
export function createSelectedLifecycleInvestigation(row, {
|
||||
source = 'live_row',
|
||||
lookup = null,
|
||||
now = Date.now(),
|
||||
} = {}) {
|
||||
const identifier = lifecycleRowIdentifier(row);
|
||||
if (!identifier) return null;
|
||||
const refreshedAt = toIsoTimestamp(now);
|
||||
return {
|
||||
identifier,
|
||||
row,
|
||||
source,
|
||||
lookup,
|
||||
selected_at: refreshedAt,
|
||||
refreshed_at: refreshedAt,
|
||||
error: null,
|
||||
};
|
||||
}
|
||||
|
||||
export function updateSelectedLifecycleFromLiveRows(selected, rows = [], {
|
||||
now = Date.now(),
|
||||
} = {}) {
|
||||
if (!selected?.identifier) return selected || null;
|
||||
const liveRow = findLifecycleRowByIdentifier(rows, selected.identifier);
|
||||
if (!liveRow) return selected;
|
||||
return {
|
||||
...selected,
|
||||
row: liveRow,
|
||||
source: 'live_row',
|
||||
refreshed_at: toIsoTimestamp(now),
|
||||
error: null,
|
||||
};
|
||||
}
|
||||
|
||||
export function shouldRefreshSelectedFromDurable(selected, rows = []) {
|
||||
if (!selected?.identifier) return false;
|
||||
return !findLifecycleRowByIdentifier(rows, selected.identifier);
|
||||
}
|
||||
|
||||
export function updateSelectedLifecycleFromLookup(selected, payload, {
|
||||
identifier = null,
|
||||
now = Date.now(),
|
||||
} = {}) {
|
||||
const requestedIdentifier = identifier || selected?.identifier || payload?.lookup?.requested_identifier || null;
|
||||
const refreshedAt = toIsoTimestamp(now);
|
||||
if (payload?.ok && payload.lifecycle_row) {
|
||||
return {
|
||||
...(selected || {}),
|
||||
identifier: lifecycleRowIdentifier(payload.lifecycle_row) || requestedIdentifier,
|
||||
row: payload.lifecycle_row,
|
||||
source: 'durable_lookup',
|
||||
lookup: payload.lookup || null,
|
||||
selected_at: selected?.selected_at || refreshedAt,
|
||||
refreshed_at: refreshedAt,
|
||||
error: null,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...(selected || {}),
|
||||
identifier: requestedIdentifier,
|
||||
row: selected?.row || null,
|
||||
source: payload?.error === 'quote_lifecycle_not_found' || payload?.lookup?.found === false
|
||||
? 'not_found'
|
||||
: 'lookup_error',
|
||||
lookup: payload?.lookup || null,
|
||||
selected_at: selected?.selected_at || refreshedAt,
|
||||
refreshed_at: refreshedAt,
|
||||
error: payload?.error || null,
|
||||
};
|
||||
}
|
||||
|
||||
function toIsoTimestamp(value) {
|
||||
const timestamp = typeof value === 'number' ? value : Date.parse(value);
|
||||
if (!Number.isFinite(timestamp)) return new Date().toISOString();
|
||||
return new Date(timestamp).toISOString();
|
||||
}
|
||||
|
|
@ -5,6 +5,13 @@ import MetricCard from '../components/MetricCard.jsx';
|
|||
import Pill from '../components/Pill.jsx';
|
||||
import TableFrame from '../components/TableFrame.jsx';
|
||||
import { formatAgeFromTimestamp, formatBoolean, formatEur, formatTimestamp, truncateMiddle } from '../lib/format.js';
|
||||
import {
|
||||
createSelectedLifecycleInvestigation,
|
||||
findLifecycleRowByIdentifier,
|
||||
shouldRefreshSelectedFromDurable,
|
||||
updateSelectedLifecycleFromLiveRows,
|
||||
updateSelectedLifecycleFromLookup,
|
||||
} from '../lib/quoteLifecycleInvestigator.js';
|
||||
|
||||
const RESPONDED_STATES = new Set(['submitted', 'awaiting_outcome', 'not_filled', 'completed']);
|
||||
const TRADING_PAIR_MODES = new Set(['maker', 'taker', 'both']);
|
||||
|
|
@ -13,6 +20,17 @@ const COMPETITIVENESS_DETAIL_ROW_COUNT = 8;
|
|||
const COMPETITIVENESS_LATENCY_ROW_COUNT = 6;
|
||||
const COMPETITIVENESS_ROLLUP_ROW_COUNT = 8;
|
||||
const QUOTE_LIFECYCLE_ROW_COUNT = 20;
|
||||
const QUOTE_LIFECYCLE_LOOKUP_DEBOUNCE_MS = 750;
|
||||
const QUOTE_LIFECYCLE_PROBLEM_FILTERS = [
|
||||
['all', 'All states'],
|
||||
['awaiting_executor', 'Awaiting executor'],
|
||||
['approved_no_command', 'Approved, no command'],
|
||||
['blocked_before_submit', 'Blocked before submit'],
|
||||
['submission_failed', 'Submission failed'],
|
||||
['quote_not_found_or_finished', 'Quote not found or finished'],
|
||||
['strategy_rejected', 'Strategy rejected'],
|
||||
['submitted_awaiting_outcome', 'Submitted or awaiting outcome'],
|
||||
];
|
||||
|
||||
async function copyIdentifier(value) {
|
||||
if (!value || !navigator?.clipboard?.writeText) return;
|
||||
|
|
@ -23,6 +41,22 @@ async function copyIdentifier(value) {
|
|||
}
|
||||
}
|
||||
|
||||
async function fetchQuoteLifecycleLookup(identifier) {
|
||||
const response = await fetch(`/api/strategy/quote-lifecycle/${encodeURIComponent(identifier)}`, {
|
||||
headers: {
|
||||
accept: 'application/json',
|
||||
},
|
||||
});
|
||||
const text = await response.text();
|
||||
const payload = text.trim() ? JSON.parse(text) : null;
|
||||
|
||||
if (response.status === 404 && payload?.error === 'quote_lifecycle_not_found') return payload;
|
||||
if (!response.ok) {
|
||||
throw new Error(payload?.error || text.trim() || `HTTP ${response.status}`);
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
|
||||
function useNow(intervalMs = 1000) {
|
||||
const [now, setNow] = useState(() => Date.now());
|
||||
|
||||
|
|
@ -179,6 +213,86 @@ function isStrategyRejected(item) {
|
|||
|| String(item?.lifecycle_label || '').toLowerCase() === 'rejected by strategy';
|
||||
}
|
||||
|
||||
function isQuoteNotFoundOrFinished(item) {
|
||||
return [
|
||||
item?.reason_code,
|
||||
item?.execution?.result_code,
|
||||
item?.execution?.failure_category,
|
||||
item?.execution?.outcome_reason,
|
||||
item?.outcome?.outcome_reason,
|
||||
item?.outcome_reason,
|
||||
].some((value) => String(value || '').toLowerCase() === 'quote_not_found_or_finished');
|
||||
}
|
||||
|
||||
function matchesLifecycleProblemFilter(item, filter) {
|
||||
switch (filter) {
|
||||
case 'awaiting_executor':
|
||||
return item?.lifecycle_state === 'command_emitted';
|
||||
case 'approved_no_command':
|
||||
return item?.lifecycle_state === 'evaluated';
|
||||
case 'blocked_before_submit':
|
||||
return item?.lifecycle_state === 'blocked';
|
||||
case 'submission_failed':
|
||||
return item?.lifecycle_state === 'failed';
|
||||
case 'quote_not_found_or_finished':
|
||||
return isQuoteNotFoundOrFinished(item);
|
||||
case 'strategy_rejected':
|
||||
return isStrategyRejected(item);
|
||||
case 'submitted_awaiting_outcome':
|
||||
return item?.lifecycle_state === 'submitted' || item?.lifecycle_state === 'awaiting_outcome';
|
||||
case 'all':
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
function sourceLabel(source) {
|
||||
if (source === 'live_row') return 'Live row';
|
||||
if (source === 'durable_lookup') return 'Durable lookup';
|
||||
if (source === 'not_found') return 'Not found';
|
||||
if (source === 'lookup_error') return 'Lookup error';
|
||||
if (source === 'loading') return 'Lookup loading';
|
||||
return 'No selection';
|
||||
}
|
||||
|
||||
function sourceTone(source) {
|
||||
if (source === 'not_found') return 'warning';
|
||||
if (source === 'lookup_error') return 'critical';
|
||||
if (source === 'live_row' || source === 'durable_lookup') return 'healthy';
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
function formatCommandAge(item, now) {
|
||||
const age = formatAgeFromTimestamp(item?.command_at, now);
|
||||
return age === 'Unavailable' ? 'Unavailable' : `${age} old`;
|
||||
}
|
||||
|
||||
function executorResultState(item) {
|
||||
if (item?.execution?.status) return `Recorded: ${plainCodeLabel(item.execution.status)}`;
|
||||
if (item?.execution_result_at) return 'Recorded';
|
||||
if (item?.lifecycle_state === 'command_emitted') return 'Not recorded yet';
|
||||
return 'Unavailable';
|
||||
}
|
||||
|
||||
function nextExpectedStage(item) {
|
||||
if (item?.lifecycle_state === 'command_emitted') {
|
||||
return 'Executor result, then relay or outcome evidence';
|
||||
}
|
||||
if (item?.lifecycle_state === 'submitted' || item?.lifecycle_state === 'awaiting_outcome') {
|
||||
return 'Durable outcome and settlement attribution';
|
||||
}
|
||||
if (['failed', 'blocked', 'not_filled', 'completed', 'rejected'].includes(item?.lifecycle_state)) {
|
||||
return 'No later stage is recorded as expected from current evidence';
|
||||
}
|
||||
return 'Strategy decision or execute command';
|
||||
}
|
||||
|
||||
function awaitingExecutorBrief(item, now) {
|
||||
if (item?.lifecycle_state !== 'command_emitted') return null;
|
||||
const command = item.command_id ? truncateMiddle(item.command_id, 24) : 'Unavailable';
|
||||
return `Command ${command}; age ${formatCommandAge(item, now)}; executor result ${executorResultState(item)}; next ${nextExpectedStage(item)}`;
|
||||
}
|
||||
|
||||
function StageCard({ title, at, status, children }) {
|
||||
return (
|
||||
<div className="lifecycle-stage-card">
|
||||
|
|
@ -239,7 +353,7 @@ function TimingWaterfall({ timing }) {
|
|||
);
|
||||
}
|
||||
|
||||
function LifecycleDetails({ item }) {
|
||||
function LifecycleDetails({ item, now = Date.now() }) {
|
||||
const executionTiming = formatExecutionTiming(item.execution?.timing);
|
||||
const makerTerms = formatMakerTerms(item.maker_terms);
|
||||
const inventoryCheck = formatInventoryCheck(item.inventory_check);
|
||||
|
|
@ -262,12 +376,17 @@ function LifecycleDetails({ item }) {
|
|||
|
||||
<StageCard at={item.command_at} status={item.command_id ? 'Command recorded' : 'No command'} title="3. Executor command">
|
||||
<IdentifierRow label="Command" value={item.command_id} />
|
||||
<div className="status-subtle">{`Command timestamp: ${formatTimestamp(item.command_at)}`}</div>
|
||||
<div className="status-subtle">{`Command age: ${formatCommandAge(item, now)}`}</div>
|
||||
<div className="status-subtle">{`Executor result: ${executorResultState(item)}`}</div>
|
||||
<div className="status-subtle">{`Next expected: ${nextExpectedStage(item)}`}</div>
|
||||
<div>{makerTerms || formatTerms(item.submitted_terms)}</div>
|
||||
{makerTerms ? <div className="status-subtle">{formatTerms(item.submitted_terms)}</div> : null}
|
||||
</StageCard>
|
||||
|
||||
<StageCard at={item.execution_result_at} status={item.execution?.status || 'No relay result'} title="4. Relay response">
|
||||
<div>{item.execution?.result_code || 'No executor result code stored'}</div>
|
||||
{item.execution?.failure_category ? <div className="status-subtle">{plainCodeLabel(item.execution.failure_category)}</div> : null}
|
||||
{item.execution?.error_message ? <div className="status-subtle">{item.execution.error_message}</div> : null}
|
||||
{!item.execution?.error_message && item.execution?.note ? <div className="status-subtle">{item.execution.note}</div> : null}
|
||||
{executionTiming ? <div className="status-subtle">{executionTiming}</div> : null}
|
||||
|
|
@ -294,6 +413,81 @@ function LifecycleDetails({ item }) {
|
|||
);
|
||||
}
|
||||
|
||||
function QuoteLifecycleInvestigator({
|
||||
selected,
|
||||
lookupState,
|
||||
lookupError,
|
||||
now,
|
||||
onRefresh,
|
||||
}) {
|
||||
const row = selected?.row || null;
|
||||
const source = lookupState === 'loading' ? 'loading' : selected?.source;
|
||||
|
||||
return (
|
||||
<div className="quote-investigator-panel">
|
||||
<div className="quote-investigator-head">
|
||||
<div>
|
||||
<div className="eyebrow">Pinned quote investigator</div>
|
||||
<h4>{row?.quote_id ? truncateMiddle(row.quote_id, 46) : selected?.identifier ? truncateMiddle(selected.identifier, 46) : 'No quote selected'}</h4>
|
||||
<div className="panel-subtitle">
|
||||
{row
|
||||
? `${row.lifecycle_label || 'Unavailable'} - ${row.reason_text || 'Unavailable'}`
|
||||
: selected?.source === 'not_found'
|
||||
? 'No durable lifecycle evidence matched that identifier.'
|
||||
: 'Select a row or look up an identifier to inspect durable lifecycle evidence.'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="quote-investigator-actions">
|
||||
<Pill label={sourceLabel(source)} stateLabel={sourceTone(source)} />
|
||||
{selected?.refreshed_at ? <Pill label={`Refreshed ${formatRelativeAge(selected.refreshed_at, now)}`} stateLabel="info" /> : null}
|
||||
<button
|
||||
className="button secondary"
|
||||
disabled={!selected?.identifier || lookupState === 'loading'}
|
||||
onClick={() => onRefresh?.(selected.identifier)}
|
||||
type="button"
|
||||
>
|
||||
Refresh lookup
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{lookupError ? <div className="banner error">{lookupError}</div> : null}
|
||||
|
||||
{row ? (
|
||||
<>
|
||||
<div className="quote-investigator-facts">
|
||||
<div>
|
||||
<div className="status-label">Lifecycle</div>
|
||||
<div className="status-value">{row.lifecycle_label || 'Unavailable'}</div>
|
||||
<div className="status-subtle mono">{row.reason_code || 'Unavailable'}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="status-label">Command age</div>
|
||||
<div className="status-value">{formatCommandAge(row, now)}</div>
|
||||
<div className="status-subtle mono">{row.command_id || 'Unavailable'}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="status-label">Executor result</div>
|
||||
<div className="status-value">{executorResultState(row)}</div>
|
||||
<div className="status-subtle">{formatTimestamp(row.execution_result_at)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="status-label">Next expected</div>
|
||||
<div className="status-value quote-investigator-next">{nextExpectedStage(row)}</div>
|
||||
<div className="status-subtle">{formatTimestamp(row.latest_stage_at)}</div>
|
||||
</div>
|
||||
</div>
|
||||
<LifecycleDetails item={row} now={now} />
|
||||
</>
|
||||
) : (
|
||||
<EmptyState>
|
||||
Durable lifecycle evidence is unavailable for the current identifier.
|
||||
</EmptyState>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function pairDisplayLabel(pairId, pairConfig) {
|
||||
const pair = (pairConfig?.pairs || []).find((entry) => (
|
||||
(entry.pair_id || entry.pairId || entry.pair) === pairId
|
||||
|
|
@ -661,15 +855,45 @@ function MakerCompetitivenessSection({ summary, pairConfig }) {
|
|||
function QuoteLifecycleTable({ items }) {
|
||||
const [expanded, setExpanded] = useState(() => new Set());
|
||||
const [showStrategyRejected, setShowStrategyRejected] = useState(true);
|
||||
const [problemFilter, setProblemFilter] = useState('all');
|
||||
const latestItemsRef = useRef(items || []);
|
||||
const [quoteDisplayPaused, setQuoteDisplayPaused] = useState(false);
|
||||
const [displayItems, setDisplayItems] = useState(() => items || []);
|
||||
const [lookupInput, setLookupInput] = useState('');
|
||||
const [lookupState, setLookupState] = useState('idle');
|
||||
const [lookupError, setLookupError] = useState(null);
|
||||
const [selectedInvestigation, setSelectedInvestigation] = useState(null);
|
||||
const selectedInvestigationRef = useRef(null);
|
||||
const lookupTimerRef = useRef(null);
|
||||
const liveNow = useNow();
|
||||
const [displayNow, setDisplayNow] = useState(() => Date.now());
|
||||
|
||||
useEffect(() => {
|
||||
selectedInvestigationRef.current = selectedInvestigation;
|
||||
}, [selectedInvestigation]);
|
||||
|
||||
useEffect(() => () => {
|
||||
if (lookupTimerRef.current) window.clearTimeout(lookupTimerRef.current);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
latestItemsRef.current = items || [];
|
||||
if (!quoteDisplayPaused) setDisplayItems(items || []);
|
||||
|
||||
const selected = selectedInvestigationRef.current;
|
||||
if (!selected?.identifier) return;
|
||||
const updated = updateSelectedLifecycleFromLiveRows(selected, items || [], {
|
||||
now: Date.now(),
|
||||
});
|
||||
if (updated !== selected) {
|
||||
setSelectedInvestigation(updated);
|
||||
setLookupState('live');
|
||||
setLookupError(null);
|
||||
return;
|
||||
}
|
||||
if (shouldRefreshSelectedFromDurable(selected, items || [])) {
|
||||
scheduleDurableLookup(selected.identifier);
|
||||
}
|
||||
}, [items, quoteDisplayPaused]);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -680,9 +904,17 @@ function QuoteLifecycleTable({ items }) {
|
|||
() => displayItems.filter((item) => isStrategyRejected(item)).length,
|
||||
[displayItems],
|
||||
);
|
||||
const problemFilteredItems = useMemo(
|
||||
() => displayItems.filter((item) => matchesLifecycleProblemFilter(item, problemFilter)),
|
||||
[displayItems, problemFilter],
|
||||
);
|
||||
const visibleItems = useMemo(
|
||||
() => (showStrategyRejected ? displayItems : displayItems.filter((item) => !isStrategyRejected(item))),
|
||||
[displayItems, showStrategyRejected],
|
||||
() => (
|
||||
showStrategyRejected || problemFilter === 'strategy_rejected'
|
||||
? problemFilteredItems
|
||||
: problemFilteredItems.filter((item) => !isStrategyRejected(item))
|
||||
),
|
||||
[problemFilteredItems, problemFilter, showStrategyRejected],
|
||||
);
|
||||
const visibleRows = fixedRows(visibleItems, QUOTE_LIFECYCLE_ROW_COUNT);
|
||||
const emptyRowsMessage = !displayItems.length
|
||||
|
|
@ -700,6 +932,77 @@ function QuoteLifecycleTable({ items }) {
|
|||
});
|
||||
}
|
||||
|
||||
async function performLookup(identifier, { silent = false } = {}) {
|
||||
const trimmed = String(identifier || '').trim();
|
||||
if (!trimmed) return;
|
||||
if (lookupTimerRef.current) {
|
||||
window.clearTimeout(lookupTimerRef.current);
|
||||
lookupTimerRef.current = null;
|
||||
}
|
||||
if (!silent) setLookupState('loading');
|
||||
setLookupError(null);
|
||||
|
||||
try {
|
||||
const payload = await fetchQuoteLifecycleLookup(trimmed);
|
||||
setSelectedInvestigation((current) => updateSelectedLifecycleFromLookup(
|
||||
current?.identifier === trimmed ? current : { identifier: trimmed, row: null },
|
||||
payload,
|
||||
{ identifier: trimmed, now: Date.now() },
|
||||
));
|
||||
setLookupState(payload?.ok ? 'found' : 'not_found');
|
||||
setLookupError(null);
|
||||
} catch (error) {
|
||||
const message = error?.message || 'Lookup failed';
|
||||
setSelectedInvestigation((current) => updateSelectedLifecycleFromLookup(
|
||||
current?.identifier === trimmed ? current : { identifier: trimmed, row: null },
|
||||
{
|
||||
error: message,
|
||||
lookup: {
|
||||
requested_identifier: trimmed,
|
||||
matched_identifier_type: null,
|
||||
found: false,
|
||||
lookup_at: new Date().toISOString(),
|
||||
latest_stage_at: null,
|
||||
matched_identifiers: {
|
||||
quote_ids: [],
|
||||
decision_ids: [],
|
||||
command_ids: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
{ identifier: trimmed, now: Date.now() },
|
||||
));
|
||||
setLookupState('error');
|
||||
setLookupError(message);
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleDurableLookup(identifier) {
|
||||
if (!identifier) return;
|
||||
if (lookupTimerRef.current) window.clearTimeout(lookupTimerRef.current);
|
||||
lookupTimerRef.current = window.setTimeout(() => {
|
||||
performLookup(identifier, { silent: true }).catch(() => {});
|
||||
}, QUOTE_LIFECYCLE_LOOKUP_DEBOUNCE_MS);
|
||||
}
|
||||
|
||||
function inspectRow(item) {
|
||||
const next = createSelectedLifecycleInvestigation(item, {
|
||||
source: 'live_row',
|
||||
now: Date.now(),
|
||||
});
|
||||
if (!next) return;
|
||||
setSelectedInvestigation(next);
|
||||
setLookupInput(next.identifier);
|
||||
setLookupState('live');
|
||||
setLookupError(null);
|
||||
scheduleDurableLookup(next.identifier);
|
||||
}
|
||||
|
||||
function submitLookup(event) {
|
||||
event.preventDefault();
|
||||
performLookup(lookupInput).catch(() => {});
|
||||
}
|
||||
|
||||
function applyLatestLifecycleDisplay() {
|
||||
setDisplayItems(latestItemsRef.current || []);
|
||||
setDisplayNow(Date.now());
|
||||
|
|
@ -712,7 +1015,29 @@ function QuoteLifecycleTable({ items }) {
|
|||
|
||||
return (
|
||||
<>
|
||||
<form className="quote-lifecycle-lookup" onSubmit={submitLookup}>
|
||||
<label className="field quote-lifecycle-lookup-field">
|
||||
<span>Lookup identifier</span>
|
||||
<input
|
||||
onChange={(event) => setLookupInput(event.target.value)}
|
||||
placeholder="quote_id, decision_id, or command_id"
|
||||
type="search"
|
||||
value={lookupInput}
|
||||
/>
|
||||
</label>
|
||||
<button className="button" disabled={!lookupInput.trim() || lookupState === 'loading'} type="submit">
|
||||
Lookup
|
||||
</button>
|
||||
</form>
|
||||
<div className="quote-lifecycle-controls">
|
||||
<label className="field quote-lifecycle-filter-field">
|
||||
<span>Problem state</span>
|
||||
<select value={problemFilter} onChange={(event) => setProblemFilter(event.target.value)}>
|
||||
{QUOTE_LIFECYCLE_PROBLEM_FILTERS.map(([value, label]) => (
|
||||
<option key={value} value={value}>{label}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label className="toggle-field">
|
||||
<input
|
||||
checked={showStrategyRejected}
|
||||
|
|
@ -732,9 +1057,17 @@ function QuoteLifecycleTable({ items }) {
|
|||
<Pill label={quoteDisplayPaused ? 'Display paused' : 'Live updates'} stateLabel={quoteDisplayPaused ? 'warning' : 'healthy'} />
|
||||
</div>
|
||||
<div className="status-subtle quote-lifecycle-snapshot-note">
|
||||
Live rows update as they arrive. Fixed row slots and clamped cells keep the table height stable.
|
||||
Pause freezes only the rolling table. The pinned quote investigator can still refresh from durable lookup.
|
||||
</div>
|
||||
|
||||
<QuoteLifecycleInvestigator
|
||||
lookupError={lookupError}
|
||||
lookupState={lookupState}
|
||||
now={liveNow}
|
||||
onRefresh={(identifier) => performLookup(identifier).catch(() => {})}
|
||||
selected={selectedInvestigation}
|
||||
/>
|
||||
|
||||
<TableFrame className="quote-lifecycle-frame">
|
||||
<table className="quote-lifecycle-table">
|
||||
<thead>
|
||||
|
|
@ -760,13 +1093,18 @@ function QuoteLifecycleTable({ items }) {
|
|||
}
|
||||
const rowKey = item.quote_id || item.decision_id || item.command_id || item.latest_stage_at || String(index);
|
||||
const isExpanded = expanded.has(rowKey);
|
||||
const isSelected = Boolean(
|
||||
selectedInvestigation?.identifier
|
||||
&& findLifecycleRowByIdentifier([item], selectedInvestigation.identifier),
|
||||
);
|
||||
const quoteTime = item.quote_activity_at || item.latest_stage_at;
|
||||
const updatedText = item.latest_stage_at && item.latest_stage_at !== item.quote_activity_at
|
||||
? `Updated ${formatTimestamp(item.latest_stage_at)} - ${formatRelativeAge(item.latest_stage_at, displayNow)}`
|
||||
: '';
|
||||
const awaitingBrief = awaitingExecutorBrief(item, displayNow);
|
||||
return (
|
||||
<Fragment key={rowKey}>
|
||||
<tr className={`quote-lifecycle-row${item.live_flash_at ? ' quote-row-flash' : ''}`} key={`${rowKey}:row`}>
|
||||
<tr className={`quote-lifecycle-row${item.live_flash_at ? ' quote-row-flash' : ''}${isSelected ? ' is-selected' : ''}`} key={`${rowKey}:row`}>
|
||||
<td>
|
||||
<div className="quote-lifecycle-cell">
|
||||
<div className="lifecycle-line lifecycle-clamp-one">{formatTimestamp(quoteTime)}</div>
|
||||
|
|
@ -788,8 +1126,9 @@ function QuoteLifecycleTable({ items }) {
|
|||
<td><Pill label={item.lifecycle_label} stateLabel={item.lifecycle_tone} /></td>
|
||||
<td>
|
||||
<div className="quote-lifecycle-cell">
|
||||
<div className="lifecycle-line lifecycle-clamp-two">{item.reason_text}</div>
|
||||
<div className="status-subtle mono lifecycle-line lifecycle-clamp-one">{item.reason_code || 'reason_unknown'}</div>
|
||||
<div className="lifecycle-line lifecycle-clamp-two">{item.reason_text || 'Unavailable'}</div>
|
||||
<div className="status-subtle mono lifecycle-line lifecycle-clamp-one">{item.reason_code || 'Unavailable'}</div>
|
||||
{awaitingBrief ? <div className="status-subtle lifecycle-line lifecycle-clamp-two">{awaitingBrief}</div> : null}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
|
|
@ -800,14 +1139,19 @@ function QuoteLifecycleTable({ items }) {
|
|||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div className="lifecycle-action-stack">
|
||||
<button className="button secondary" onClick={() => inspectRow(item)} type="button">
|
||||
Inspect quote
|
||||
</button>
|
||||
<button className="button secondary" onClick={() => toggle(rowKey)} type="button">
|
||||
{isExpanded ? 'Hide lifecycle' : 'Show lifecycle'}
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{isExpanded ? (
|
||||
<tr className="lifecycle-expanded-row" key={`${rowKey}:details`}>
|
||||
<td colSpan={8}><LifecycleDetails item={item} /></td>
|
||||
<td colSpan={8}><LifecycleDetails item={item} now={displayNow} /></td>
|
||||
</tr>
|
||||
) : null}
|
||||
</Fragment>
|
||||
|
|
|
|||
|
|
@ -618,10 +618,85 @@ table.lifecycle-table th:nth-child(5) {
|
|||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.quote-lifecycle-lookup {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: end;
|
||||
gap: 10px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.quote-lifecycle-lookup-field {
|
||||
flex: 1 1 320px;
|
||||
}
|
||||
|
||||
.quote-lifecycle-filter-field {
|
||||
min-width: 230px;
|
||||
}
|
||||
|
||||
.quote-lifecycle-lookup-field span,
|
||||
.quote-lifecycle-filter-field span {
|
||||
font-size: 0.84rem;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.quote-lifecycle-snapshot-note {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.quote-investigator-panel {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
margin-bottom: 14px;
|
||||
padding: 16px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 16px;
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.quote-investigator-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.quote-investigator-head h4 {
|
||||
margin: 4px 0 0;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.quote-investigator-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.quote-investigator-facts {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
|
||||
}
|
||||
|
||||
.quote-investigator-facts > div {
|
||||
min-width: 0;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 14px;
|
||||
background: var(--panel-2);
|
||||
}
|
||||
|
||||
.quote-investigator-facts .status-value {
|
||||
margin-top: 6px;
|
||||
font-size: 1rem;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.quote-investigator-next {
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.toggle-field {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
|
@ -651,6 +726,11 @@ table.lifecycle-table th:nth-child(5) {
|
|||
height: 112px;
|
||||
}
|
||||
|
||||
.quote-lifecycle-table tbody tr.quote-lifecycle-row.is-selected td {
|
||||
box-shadow: inset 3px 0 0 rgba(31, 122, 90, 0.76);
|
||||
background: rgba(31, 122, 90, 0.08);
|
||||
}
|
||||
|
||||
.quote-lifecycle-table td {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
|
@ -752,7 +832,12 @@ table.lifecycle-table th:nth-child(5) {
|
|||
.quote-lifecycle-table td:nth-child(8),
|
||||
.successful-trades-table th:nth-child(7),
|
||||
.successful-trades-table td:nth-child(7) {
|
||||
width: 150px;
|
||||
width: 170px;
|
||||
}
|
||||
|
||||
.lifecycle-action-stack {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.successful-trades-table {
|
||||
|
|
|
|||
|
|
@ -37,6 +37,16 @@ test('operator dashboard API auth failures are JSON for frontend fetches', () =>
|
|||
assert.match(source, /sendJson\(res, 401, \{ error: 'authentication_required' \}\)/);
|
||||
});
|
||||
|
||||
test('operator dashboard exposes authenticated durable quote lifecycle lookup API', () => {
|
||||
assert.match(source, /quote-lifecycle/);
|
||||
assert.match(source, /lifecycleLookupMatch/);
|
||||
assert.match(source, /loadQuoteLifecycleLookupEvidence/);
|
||||
assert.match(source, /buildQuoteLifecycleLookupResponse/);
|
||||
assert.match(source, /quote_lifecycle_identifier_required/);
|
||||
assert.match(source, /quote_lifecycle_not_found/);
|
||||
assert.match(source, /payload\.ok \? 200 : 404/);
|
||||
});
|
||||
|
||||
test('operator dashboard bootstrap does not synchronously refresh intent request outcomes', () => {
|
||||
assert.match(source, /loadRecentIntentRequests\(pool, \{[\s\S]*refreshOutcomes: false/);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -32,9 +32,29 @@ test('strategy page owns consolidated quote lifecycle and successful trade table
|
|||
assert.match(strategySource, /setDisplayItems\(items \|\| \[\]\)/);
|
||||
assert.match(strategySource, /useNow\(\)/);
|
||||
assert.match(strategySource, /Live updates/);
|
||||
assert.match(strategySource, /Live rows update as they arrive/);
|
||||
assert.match(strategySource, /Pause freezes only the rolling table/);
|
||||
assert.match(strategySource, /Pinned quote investigator/);
|
||||
assert.match(strategySource, /Lookup identifier/);
|
||||
assert.match(strategySource, /Inspect quote/);
|
||||
assert.match(strategySource, /Refresh lookup/);
|
||||
assert.match(strategySource, /fetchQuoteLifecycleLookup/);
|
||||
assert.match(strategySource, /\/api\/strategy\/quote-lifecycle\//);
|
||||
assert.match(strategySource, /QUOTE_LIFECYCLE_PROBLEM_FILTERS/);
|
||||
assert.match(strategySource, /awaiting_executor/);
|
||||
assert.match(strategySource, /quote_not_found_or_finished/);
|
||||
assert.match(strategySource, /approved_no_command/);
|
||||
assert.match(strategySource, /submitted_awaiting_outcome/);
|
||||
assert.match(strategySource, /updateSelectedLifecycleFromLiveRows/);
|
||||
assert.match(strategySource, /shouldRefreshSelectedFromDurable/);
|
||||
assert.match(strategySource, /updateSelectedLifecycleFromLookup/);
|
||||
assert.match(strategySource, /Command age:/);
|
||||
assert.match(strategySource, /Executor result:/);
|
||||
assert.match(strategySource, /Next expected:/);
|
||||
assert.match(strategySource, /visibleRows/);
|
||||
assert.match(strategySource, /quote-lifecycle-placeholder-row/);
|
||||
assert.match(stylesSource, /\.quote-investigator-panel/);
|
||||
assert.match(stylesSource, /\.quote-lifecycle-lookup/);
|
||||
assert.match(stylesSource, /\.quote-lifecycle-table tbody tr\.quote-lifecycle-row\.is-selected/);
|
||||
assert.match(stylesSource, /\.shell \{[\s\S]*?width: 100%;[\s\S]*?margin: 0;[\s\S]*?\}/);
|
||||
assert.doesNotMatch(stylesSource, /^\s*max-width\s*:/m);
|
||||
assert.doesNotMatch(strategySource, /maxWidth:/);
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import {
|
|||
buildDashboardBootstrap,
|
||||
buildDashboardControlErrorResponse,
|
||||
buildLiveStatusBar,
|
||||
buildQuoteLifecycleLookupResponse,
|
||||
buildProfitabilitySummary,
|
||||
createDashboardLiveState,
|
||||
deriveQuoteLifecycleRows,
|
||||
|
|
@ -15,12 +16,19 @@ import {
|
|||
resolveDashboardControlTimeoutMs,
|
||||
} from '../src/core/operator-dashboard.mjs';
|
||||
import { formatAge, formatAgeFromTimestamp } from '../src/operator-dashboard/static/lib/format.js';
|
||||
import {
|
||||
createSelectedLifecycleInvestigation,
|
||||
shouldRefreshSelectedFromDurable,
|
||||
updateSelectedLifecycleFromLiveRows,
|
||||
updateSelectedLifecycleFromLookup,
|
||||
} from '../src/operator-dashboard/static/lib/quoteLifecycleInvestigator.js';
|
||||
import { dashboardReducer } from '../src/operator-dashboard/static/state/dashboardReducer.js';
|
||||
import {
|
||||
buildDashboardSessionToken,
|
||||
parseBasicAuthorizationHeader,
|
||||
resolveDashboardRequestAuth,
|
||||
} from '../src/core/operator-dashboard-auth.mjs';
|
||||
import { loadQuoteLifecycleLookupEvidence } from '../src/lib/postgres.mjs';
|
||||
|
||||
function buildConfig() {
|
||||
const tradingBtc = {
|
||||
|
|
@ -564,6 +572,272 @@ test('socket lifecycle messages replace strategy rows without page refresh', ()
|
|||
assert.equal(next.dashboard.strategy.strategy_state.recent_lifecycle_rows[0].live_flash_at, '2026-04-04T09:00:00.000Z');
|
||||
});
|
||||
|
||||
test('selected awaiting executor quote remains inspectable after live rows move and refreshes from durable result', () => {
|
||||
const config = buildConfig();
|
||||
const awaiting = buildQuoteLifecycleLookupResponse({
|
||||
config,
|
||||
identifier: 'quote-follow',
|
||||
evidence: {
|
||||
lookup_at: '2026-06-12T10:00:00.000Z',
|
||||
matched_identifier_type: 'quote_id',
|
||||
matched_identifiers: {
|
||||
quote_ids: ['quote-follow'],
|
||||
decision_ids: ['decision-follow'],
|
||||
command_ids: ['cmd-follow'],
|
||||
},
|
||||
recentTradeDecisions: [{
|
||||
observed_at: '2026-06-12T09:59:58.000Z',
|
||||
payload: {
|
||||
quote_id: 'quote-follow',
|
||||
decision_id: 'decision-follow',
|
||||
pair: config.activePair,
|
||||
decision: 'actionable',
|
||||
decision_reason: 'actionable',
|
||||
},
|
||||
}],
|
||||
recentExecuteTradeCommands: [{
|
||||
observed_at: '2026-06-12T09:59:59.000Z',
|
||||
command_id: 'cmd-follow',
|
||||
decision_id: 'decision-follow',
|
||||
quote_id: 'quote-follow',
|
||||
pair: config.activePair,
|
||||
}],
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(awaiting.ok, true);
|
||||
assert.equal(awaiting.lifecycle_row.lifecycle_state, 'command_emitted');
|
||||
assert.equal(awaiting.lifecycle_row.lifecycle_label, 'Awaiting executor');
|
||||
|
||||
const selected = createSelectedLifecycleInvestigation(awaiting.lifecycle_row, {
|
||||
now: '2026-06-12T10:00:00.000Z',
|
||||
});
|
||||
const afterLiveMove = updateSelectedLifecycleFromLiveRows(selected, [{
|
||||
quote_id: 'quote-newer',
|
||||
lifecycle_state: 'observed',
|
||||
}], {
|
||||
now: '2026-06-12T10:00:02.000Z',
|
||||
});
|
||||
|
||||
assert.equal(afterLiveMove.row.quote_id, 'quote-follow');
|
||||
assert.equal(afterLiveMove.row.lifecycle_state, 'command_emitted');
|
||||
assert.equal(shouldRefreshSelectedFromDurable(afterLiveMove, [{ quote_id: 'quote-newer' }]), true);
|
||||
|
||||
const later = buildQuoteLifecycleLookupResponse({
|
||||
config,
|
||||
identifier: 'cmd-follow',
|
||||
evidence: {
|
||||
lookup_at: '2026-06-12T10:00:04.000Z',
|
||||
matched_identifier_type: 'command_id',
|
||||
matched_identifiers: {
|
||||
quote_ids: ['quote-follow'],
|
||||
decision_ids: ['decision-follow'],
|
||||
command_ids: ['cmd-follow'],
|
||||
},
|
||||
recentExecuteTradeCommands: [{
|
||||
observed_at: '2026-06-12T09:59:59.000Z',
|
||||
command_id: 'cmd-follow',
|
||||
decision_id: 'decision-follow',
|
||||
quote_id: 'quote-follow',
|
||||
pair: config.activePair,
|
||||
}],
|
||||
recentExecutionResults: [{
|
||||
command_id: 'cmd-follow',
|
||||
decision_id: 'decision-follow',
|
||||
quote_id: 'quote-follow',
|
||||
pair: config.activePair,
|
||||
result_at: '2026-06-12T10:00:03.000Z',
|
||||
status: 'failed',
|
||||
result_code: 'submission_failed',
|
||||
failure_category: 'quote_not_found_or_finished',
|
||||
note: 'relay no longer has the quote',
|
||||
}],
|
||||
},
|
||||
});
|
||||
const refreshed = updateSelectedLifecycleFromLookup(afterLiveMove, later, {
|
||||
identifier: 'cmd-follow',
|
||||
now: '2026-06-12T10:00:04.000Z',
|
||||
});
|
||||
|
||||
assert.equal(refreshed.source, 'durable_lookup');
|
||||
assert.equal(refreshed.row.quote_id, 'quote-follow');
|
||||
assert.equal(refreshed.row.lifecycle_state, 'failed');
|
||||
assert.equal(refreshed.row.reason_code, 'quote_not_found_or_finished');
|
||||
assert.match(refreshed.row.reason_text, /not found or already finished/i);
|
||||
});
|
||||
|
||||
test('quote lifecycle lookup response matches quote, decision, and command identifiers from durable evidence', () => {
|
||||
const config = buildConfig();
|
||||
const evidence = {
|
||||
lookup_at: '2026-06-12T10:10:00.000Z',
|
||||
matched_identifiers: {
|
||||
quote_ids: ['quote-lookup'],
|
||||
decision_ids: ['decision-lookup'],
|
||||
command_ids: ['cmd-lookup'],
|
||||
},
|
||||
recentQuotes: [{
|
||||
quote_id: 'quote-lookup',
|
||||
pair: config.activePair,
|
||||
asset_in: config.tradingBtc.assetId,
|
||||
asset_out: config.tradingEure.assetId,
|
||||
amount_in: '100',
|
||||
amount_out: '200',
|
||||
observed_at: '2026-06-12T10:09:57.000Z',
|
||||
}],
|
||||
recentTradeDecisions: [{
|
||||
observed_at: '2026-06-12T10:09:58.000Z',
|
||||
payload: {
|
||||
quote_id: 'quote-lookup',
|
||||
decision_id: 'decision-lookup',
|
||||
pair: config.activePair,
|
||||
decision: 'actionable',
|
||||
decision_reason: 'actionable',
|
||||
},
|
||||
}],
|
||||
recentExecuteTradeCommands: [{
|
||||
observed_at: '2026-06-12T10:09:59.000Z',
|
||||
command_id: 'cmd-lookup',
|
||||
decision_id: 'decision-lookup',
|
||||
quote_id: 'quote-lookup',
|
||||
pair: config.activePair,
|
||||
}],
|
||||
};
|
||||
|
||||
const byQuote = buildQuoteLifecycleLookupResponse({
|
||||
config,
|
||||
identifier: 'quote-lookup',
|
||||
evidence: { ...evidence, matched_identifier_type: 'quote_id' },
|
||||
});
|
||||
const byDecision = buildQuoteLifecycleLookupResponse({
|
||||
config,
|
||||
identifier: 'decision-lookup',
|
||||
evidence: { ...evidence, matched_identifier_type: 'decision_id' },
|
||||
});
|
||||
const byCommand = buildQuoteLifecycleLookupResponse({
|
||||
config,
|
||||
identifier: 'cmd-lookup',
|
||||
evidence: { ...evidence, matched_identifier_type: 'command_id' },
|
||||
});
|
||||
|
||||
assert.equal(byQuote.lifecycle_row.quote_id, 'quote-lookup');
|
||||
assert.equal(byDecision.lifecycle_row.decision_id, 'decision-lookup');
|
||||
assert.equal(byCommand.lifecycle_row.command_id, 'cmd-lookup');
|
||||
assert.equal(byQuote.lookup.matched_identifier_type, 'quote_id');
|
||||
assert.equal(byDecision.lookup.matched_identifier_type, 'decision_id');
|
||||
assert.equal(byCommand.lookup.matched_identifier_type, 'command_id');
|
||||
});
|
||||
|
||||
test('postgres quote lifecycle lookup loader links durable quote, decision, and command evidence', async () => {
|
||||
const queries = [];
|
||||
const pool = {
|
||||
async query(sql, params = []) {
|
||||
queries.push({ sql, params });
|
||||
if (sql.includes('WITH candidates')) {
|
||||
return {
|
||||
rows: [{
|
||||
matched_identifier_type: 'decision_id',
|
||||
quote_id: 'quote-loader',
|
||||
decision_id: 'decision-loader',
|
||||
command_id: 'cmd-loader',
|
||||
matched_at: '2026-06-12T10:20:00.000Z',
|
||||
}],
|
||||
};
|
||||
}
|
||||
if (sql.includes('FROM swap_demand_events')) {
|
||||
return {
|
||||
rows: [{
|
||||
observed_at: '2026-06-12T10:19:57.000Z',
|
||||
ingested_at: '2026-06-12T10:19:57.000Z',
|
||||
payload: {
|
||||
quote_id: 'quote-loader',
|
||||
pair: 'btc->eure',
|
||||
asset_in: 'btc',
|
||||
asset_out: 'eure',
|
||||
amount_in: '100',
|
||||
amount_out: '200',
|
||||
},
|
||||
}],
|
||||
};
|
||||
}
|
||||
if (sql.includes('FROM trade_decisions')) {
|
||||
return {
|
||||
rows: [{
|
||||
observed_at: '2026-06-12T10:19:58.000Z',
|
||||
ingested_at: '2026-06-12T10:19:58.000Z',
|
||||
payload: {
|
||||
quote_id: 'quote-loader',
|
||||
decision_id: 'decision-loader',
|
||||
decision: 'actionable',
|
||||
decision_reason: 'actionable',
|
||||
},
|
||||
}],
|
||||
};
|
||||
}
|
||||
if (sql.includes('FROM execute_trade_commands')) {
|
||||
return {
|
||||
rows: [{
|
||||
observed_at: '2026-06-12T10:19:59.000Z',
|
||||
ingested_at: '2026-06-12T10:19:59.000Z',
|
||||
payload: {
|
||||
quote_id: 'quote-loader',
|
||||
decision_id: 'decision-loader',
|
||||
command_id: 'cmd-loader',
|
||||
},
|
||||
}],
|
||||
};
|
||||
}
|
||||
if (sql.includes('FROM trade_execution_results r')) {
|
||||
return {
|
||||
rows: [{
|
||||
result_observed_at: '2026-06-12T10:20:01.000Z',
|
||||
result_ingested_at: '2026-06-12T10:20:01.000Z',
|
||||
result_payload: {
|
||||
quote_id: 'quote-loader',
|
||||
decision_id: 'decision-loader',
|
||||
command_id: 'cmd-loader',
|
||||
status: 'failed',
|
||||
result_code: 'submission_failed',
|
||||
failure_category: 'quote_not_found_or_finished',
|
||||
},
|
||||
command_ingested_at: '2026-06-12T10:19:59.000Z',
|
||||
command_payload: {
|
||||
quote_id: 'quote-loader',
|
||||
decision_id: 'decision-loader',
|
||||
command_id: 'cmd-loader',
|
||||
},
|
||||
decision_payload: {
|
||||
quote_id: 'quote-loader',
|
||||
decision_id: 'decision-loader',
|
||||
},
|
||||
outcome_payload: null,
|
||||
}],
|
||||
};
|
||||
}
|
||||
if (sql.includes('FROM quote_outcome_attributions')) {
|
||||
return { rows: [] };
|
||||
}
|
||||
return { rows: [] };
|
||||
},
|
||||
};
|
||||
|
||||
const evidence = await loadQuoteLifecycleLookupEvidence(pool, {
|
||||
identifier: 'decision-loader',
|
||||
});
|
||||
|
||||
assert.equal(evidence.found, true);
|
||||
assert.equal(evidence.matched_identifier_type, 'decision_id');
|
||||
assert.deepEqual(evidence.matched_identifiers, {
|
||||
quote_ids: ['quote-loader'],
|
||||
decision_ids: ['decision-loader'],
|
||||
command_ids: ['cmd-loader'],
|
||||
});
|
||||
assert.equal(evidence.recentQuotes[0].quote_id, 'quote-loader');
|
||||
assert.equal(evidence.recentTradeDecisions[0].payload.decision_id, 'decision-loader');
|
||||
assert.equal(evidence.recentExecuteTradeCommands[0].command_id, 'cmd-loader');
|
||||
assert.equal(evidence.recentExecutionResults[0].failure_category, 'quote_not_found_or_finished');
|
||||
assert.equal(queries[0].params[0], 'decision-loader');
|
||||
});
|
||||
|
||||
test('dashboard age formatting uses seconds first and exact timestamp deltas', () => {
|
||||
assert.equal(formatAge(999), '999 ms');
|
||||
assert.equal(formatAge(1_500), '1s');
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue