Stream quote lifecycle rows to dashboard
All checks were successful
deploy / deploy (push) Successful in 34s

Proof: npm test; npm run operator-dashboard:build; PYTHONPATH=. python3 test/render_release_manifest_test.py; PYTHONPATH=. python3 test/repo_deployments_test.py

Assumptions: Kafka live topics carry normalized quote, decision, command, and execution result envelopes; durable quote outcomes still refresh through history/bootstrap when inventory attribution is recomputed.

Still fake: Venue-native terminal fill events and fee-complete realized PnL remain unavailable; submitted and relay-accepted evidence still cannot prove settlement without durable inventory movement.
This commit is contained in:
philipp 2026-04-15 17:04:21 +02:00
parent 51461a25bc
commit ddb360a34f
9 changed files with 475 additions and 33 deletions

View file

@ -11,6 +11,7 @@ import {
applyDashboardLiveEvent,
buildDashboardBootstrap,
buildDashboardControlErrorResponse,
buildLiveQuoteLifecycleRows,
buildLiveStatusBar,
createDashboardLiveState,
listDashboardServices,
@ -102,10 +103,34 @@ const initialInventory = await safeSourceLoad(
() => loadLatestInventorySnapshot(pool),
null,
);
const initialRecentTradeDecisions = await safeSourceLoad(
'recent_trade_decisions',
() => loadRecentTradeDecisions(pool, { limit: 20 }),
[],
);
const initialRecentExecuteTradeCommands = await safeSourceLoad(
'recent_execute_trade_commands',
() => loadRecentExecuteTradeCommands(pool, { limit: 40 }),
[],
);
const initialRecentExecutionResults = await safeSourceLoad(
'recent_execution_results',
() => loadRecentExecutionResults(pool, { limit: 40 }),
[],
);
const initialRecentQuoteOutcomes = await safeSourceLoad(
'recent_quote_outcomes',
() => loadRecentQuoteOutcomes(pool, { limit: 200 }),
[],
);
const liveState = createDashboardLiveState({
config,
recentQuotes: initialRecentQuotes,
recentTradeDecisions: initialRecentTradeDecisions,
recentExecuteTradeCommands: initialRecentExecuteTradeCommands,
recentExecutionResults: initialRecentExecutionResults,
recentQuoteOutcomes: initialRecentQuoteOutcomes,
latestMarketPrice: initialMarketPrice,
latestInventory: initialInventory,
recentSubmissionCount: initialSubmissionSummary.total,
@ -124,6 +149,8 @@ const liveConsumer = await createConsumer({
const liveTopics = [
config.kafkaTopicNormSwapDemand,
config.kafkaTopicDecisionTradeDecision,
config.kafkaTopicCmdExecuteTrade,
config.kafkaTopicRefMarketPrice,
config.kafkaTopicStateIntentInventory,
config.kafkaTopicOpsAlert,
@ -169,6 +196,7 @@ webSocketServer.on('connection', (socket, _req, authContext) => {
session: authContext,
live: {
recent_quotes: liveState.recent_quotes,
recent_lifecycle_rows: buildLiveQuoteLifecycleRows(liveState),
status_bar: buildLiveStatusBar(liveState),
},
}));

View file

@ -5,6 +5,7 @@ import { TERMINAL_SETTLEMENT_ATTRIBUTION_STATUSES } from './quote-outcomes.mjs';
import { inferServiceFreshnessTimestamp as inferRuntimeFreshnessTimestamp } from './runtime-health.mjs';
export const DASHBOARD_LIVE_QUOTE_LIMIT = 10;
export const DASHBOARD_LIVE_LIFECYCLE_LIMIT = 20;
const DECIMAL_SCALE = 18;
const DECIMAL_FACTOR = 10n ** BigInt(DECIMAL_SCALE);
@ -292,6 +293,10 @@ export function listDashboardServices(config) {
export function createDashboardLiveState({
config,
recentQuotes = [],
recentTradeDecisions = [],
recentExecuteTradeCommands = [],
recentExecutionResults = [],
recentQuoteOutcomes = [],
latestMarketPrice = null,
latestInventory = null,
recentSubmissionCount = 0,
@ -299,11 +304,17 @@ export function createDashboardLiveState({
activeAlerts = [],
} = {}) {
const state = {
config,
active_pair: config.activePair,
btc_asset: config.tradingBtc,
eure_asset: config.tradingEure,
quote_limit: config.operatorDashboardQuoteLimit || DASHBOARD_LIVE_QUOTE_LIMIT,
lifecycle_limit: config.operatorDashboardLifecycleLimit || DASHBOARD_LIVE_LIFECYCLE_LIMIT,
recent_quotes: recentQuotes.slice(0, config.operatorDashboardQuoteLimit || DASHBOARD_LIVE_QUOTE_LIMIT),
recent_trade_decisions: recentTradeDecisions.slice(0, config.operatorDashboardLifecycleLimit || DASHBOARD_LIVE_LIFECYCLE_LIMIT),
recent_execute_trade_commands: recentExecuteTradeCommands.slice(0, config.operatorDashboardLifecycleLimit || DASHBOARD_LIVE_LIFECYCLE_LIMIT),
recent_execution_results: recentExecutionResults.slice(0, config.operatorDashboardLifecycleLimit || DASHBOARD_LIVE_LIFECYCLE_LIMIT),
recent_quote_outcomes: recentQuoteOutcomes.slice(0, config.operatorDashboardLifecycleLimit || DASHBOARD_LIVE_LIFECYCLE_LIMIT),
latest_market_price: latestMarketPrice?.payload || latestMarketPrice || null,
latest_inventory: latestInventory?.payload || latestInventory || null,
recent_submission_count: Number(recentSubmissionCount || 0),
@ -319,18 +330,62 @@ export function createDashboardLiveState({
return state;
}
export function buildLiveQuoteLifecycleRows(state, { flashQuoteId = null, flashAt = null } = {}) {
const rows = deriveQuoteLifecycleRows({
recentQuotes: state.recent_quotes,
recentTradeDecisions: state.recent_trade_decisions,
recentExecuteTradeCommands: state.recent_execute_trade_commands,
recentExecutionResults: state.recent_execution_results,
recentQuoteOutcomes: state.recent_quote_outcomes,
limit: state.lifecycle_limit,
}).map((row) => enrichLifecycleRowForUi({ config: state.config, row }));
if (!flashQuoteId) return rows;
const highlightedAt = flashAt || new Date().toISOString();
return rows.map((row) => (
row.quote_id === flashQuoteId
? { ...row, live_flash_at: highlightedAt }
: row
));
}
export function applyDashboardLiveEvent(state, { topic, event }) {
if (!event?.payload) return [];
switch (topic) {
switch (normalizeDashboardLiveTopic(state, topic)) {
case 'norm.swap_demand': {
const quote = normalizeLiveQuote(event.payload, event);
if (!quote) return [];
state.recent_quotes = appendUniqueRecentQuote(state.recent_quotes, quote, state.quote_limit);
return [{
type: 'quotes.recent',
recent_quotes: state.recent_quotes,
}];
return [
{
type: 'quotes.recent',
recent_quotes: state.recent_quotes,
},
buildQuoteLifecycleUpdate(state, { flashQuoteId: quote.quote_id }),
];
}
case 'decision.trade_decision': {
const decision = normalizeLiveDecision(event.payload, event);
if (!decision) return [];
state.recent_trade_decisions = appendUniqueRecentEvent(
state.recent_trade_decisions,
decision,
state.lifecycle_limit,
(entry) => livePayloadKey(entry, ['decision_id', 'quote_id']),
);
return [buildQuoteLifecycleUpdate(state, { flashQuoteId: decision.payload?.quote_id })];
}
case 'cmd.execute_trade': {
const command = normalizeLiveCommand(event.payload, event);
if (!command) return [];
state.recent_execute_trade_commands = appendUniqueRecentEvent(
state.recent_execute_trade_commands,
command,
state.lifecycle_limit,
(entry) => livePayloadKey(entry, ['command_id', 'decision_id', 'quote_id']),
);
return [buildQuoteLifecycleUpdate(state, { flashQuoteId: command.payload?.quote_id })];
}
case 'ref.market_price':
state.latest_market_price = {
@ -355,14 +410,27 @@ export function applyDashboardLiveEvent(state, { topic, event }) {
case 'ops.alert': {
return [];
}
case 'exec.trade_result':
if (event.payload.status !== 'submitted') return [];
state.recent_submission_count += 1;
state.last_submission_at = event.observed_at || event.ingested_at || new Date().toISOString();
return [{
type: 'status_bar.updated',
status_bar: buildLiveStatusBar(state),
}];
case 'exec.trade_result': {
const execution = normalizeLiveExecutionResult(event.payload, event);
if (!execution) return [];
state.recent_execution_results = appendUniqueRecentEvent(
state.recent_execution_results,
execution,
state.lifecycle_limit,
(entry) => livePayloadKey(entry, ['command_id', 'decision_id', 'quote_id', 'result_at']),
);
const updates = [];
if (event.payload.status === 'submitted') {
state.recent_submission_count += 1;
state.last_submission_at = event.observed_at || event.ingested_at || new Date().toISOString();
updates.push({
type: 'status_bar.updated',
status_bar: buildLiveStatusBar(state),
});
}
updates.push(buildQuoteLifecycleUpdate(state, { flashQuoteId: execution.quote_id }));
return updates;
}
default:
return [];
}
@ -1819,6 +1887,98 @@ function summarizeRecentAlertTransitions(alerts) {
));
}
function normalizeDashboardLiveTopic(state, topic) {
const config = state?.config || {};
const aliases = new Map([
[config.kafkaTopicNormSwapDemand, 'norm.swap_demand'],
[config.kafkaTopicDecisionTradeDecision, 'decision.trade_decision'],
[config.kafkaTopicCmdExecuteTrade, 'cmd.execute_trade'],
[config.kafkaTopicRefMarketPrice, 'ref.market_price'],
[config.kafkaTopicStateIntentInventory, 'state.intent_inventory'],
[config.kafkaTopicOpsAlert, 'ops.alert'],
[config.kafkaTopicExecTradeResult, 'exec.trade_result'],
]);
return aliases.get(topic) || topic;
}
function buildQuoteLifecycleUpdate(state, { flashQuoteId = null } = {}) {
const receivedAt = new Date().toISOString();
return {
type: 'quote_lifecycle.updated',
recent_lifecycle_rows: buildLiveQuoteLifecycleRows(state, {
flashQuoteId,
flashAt: receivedAt,
}),
flash_quote_id: flashQuoteId || null,
received_at: receivedAt,
};
}
function appendUniqueRecentEvent(items, nextItem, limit, keyFn) {
const nextKey = keyFn(nextItem);
const deduped = [
nextItem,
...(items || []).filter((item) => keyFn(item) !== nextKey),
];
return deduped.slice(0, limit);
}
function livePayloadKey(entry, fields) {
const payload = entry?.payload || entry || {};
for (const field of fields) {
if (payload[field] != null && payload[field] !== '') return `${field}:${payload[field]}`;
}
return `event:${entry?.observed_at || entry?.ingested_at || JSON.stringify(payload)}`;
}
function normalizeLiveDecision(payload, event) {
if (!payload?.decision_id && !payload?.quote_id) return null;
const decisionAt = payload.decision_at || event.observed_at || event.ingested_at || null;
return {
observed_at: event.observed_at || decisionAt,
ingested_at: event.ingested_at || null,
payload: {
...payload,
decision_at: decisionAt,
},
};
}
function normalizeLiveCommand(payload, event) {
if (!payload?.command_id && !payload?.decision_id && !payload?.quote_id) return null;
return {
observed_at: event.observed_at || event.ingested_at || null,
ingested_at: event.ingested_at || null,
payload: {
...payload,
amount_in: payload.quote_output?.amount_in ?? payload.proposed_amount_in ?? payload.amount_in ?? null,
amount_out: payload.quote_output?.amount_out ?? payload.proposed_amount_out ?? payload.amount_out ?? null,
},
};
}
function normalizeLiveExecutionResult(payload, event) {
if (!payload?.command_id && !payload?.decision_id && !payload?.quote_id) return null;
return {
command_id: payload.command_id || null,
decision_id: payload.decision_id || null,
execution_key: payload.execution_key || null,
quote_id: payload.quote_id || null,
pair: payload.pair || null,
result_at: event.observed_at || event.ingested_at || new Date().toISOString(),
status: payload.status || null,
result_code: payload.result_code || null,
outcome_status: payload.outcome_status || payload.venue_outcome_status || payload.trade_outcome_status || null,
outcome_reason: payload.outcome_reason || payload.venue_outcome_reason || payload.trade_outcome_reason || null,
attribution_status: payload.attribution_status || null,
attribution_method: payload.attribution_method || null,
attributed_inventory_delta: payload.attributed_inventory_delta || null,
venue_response: payload.venue_response || null,
error_message: payload.error?.message || null,
note: payload.note || null,
};
}
function appendUniqueRecentQuote(quotes, nextQuote, limit) {
const deduped = [nextQuote, ...quotes.filter((quote) => quote.quote_id !== nextQuote.quote_id)];
return deduped.slice(0, limit);

View file

@ -1,5 +1,5 @@
import Pill from './Pill.jsx';
import { formatAge, formatBoolean } from '../lib/format.js';
import { formatAge, formatBoolean, formatTimestamp } from '../lib/format.js';
export default function ServiceCard({ service }) {
const healthLabel = service.health_label || service.health_status || (service.reachable ? 'online' : 'offline');
@ -14,7 +14,8 @@ export default function ServiceCard({ service }) {
<div>{`Reachable ${formatBoolean(service.reachable)}`}</div>
<div>{`Paused ${formatBoolean(service.paused)}`}</div>
<div>{`Armed ${formatBoolean(service.armed)}`}</div>
<div>{`Freshness ${formatAge(service.freshness_age_ms)}`}</div>
<div>{`Freshness ${formatAge(service.freshness_age_ms)}${service.freshness_age_ms == null ? '' : ' ago'}`}</div>
<div>{`Freshness at ${formatTimestamp(service.freshness_at)}`}</div>
<div className="mono">{service.base_url}</div>
{service.last_error ? <div>{JSON.stringify(service.last_error)}</div> : null}
</div>

View file

@ -12,10 +12,36 @@ export function formatTimestamp(value) {
export function formatAge(value) {
if (value == null) return 'Unavailable';
if (value < 1000) return `${value} ms`;
if (value < 60_000) return `${(value / 1000).toFixed(1)} s`;
if (value < 3_600_000) return `${(value / 60_000).toFixed(1)} min`;
return `${(value / 3_600_000).toFixed(1)} h`;
const numeric = Number(value);
if (!Number.isFinite(numeric)) return 'Unavailable';
const ageMs = Math.max(0, Math.floor(numeric));
if (ageMs < 1000) return `${ageMs} ms`;
const seconds = Math.floor(ageMs / 1000);
if (seconds < 60) return `${seconds}s`;
const minutes = Math.floor(seconds / 60);
const remainingSeconds = seconds % 60;
if (minutes < 60) {
return remainingSeconds > 0 ? `${minutes}m ${remainingSeconds}s` : `${minutes}m`;
}
const hours = Math.floor(minutes / 60);
const remainingMinutes = minutes % 60;
if (hours < 24) {
return remainingMinutes > 0 ? `${hours}h ${remainingMinutes}m` : `${hours}h`;
}
const days = Math.floor(hours / 24);
const remainingHours = hours % 24;
return remainingHours > 0 ? `${days}d ${remainingHours}h` : `${days}d`;
}
export function formatAgeFromTimestamp(value, now = Date.now()) {
if (!value) return 'Unavailable';
const timestamp = new Date(value).getTime();
if (Number.isNaN(timestamp)) return 'Unavailable';
return formatAge(now - timestamp);
}
export function formatEur(value) {

View file

@ -1,10 +1,10 @@
import { Fragment, useState } from 'react';
import { Fragment, useEffect, useState } from 'react';
import EmptyState from '../components/EmptyState.jsx';
import MetricCard from '../components/MetricCard.jsx';
import Pill from '../components/Pill.jsx';
import TableFrame from '../components/TableFrame.jsx';
import { formatBoolean, formatEur, formatTimestamp, truncateMiddle } from '../lib/format.js';
import { formatAgeFromTimestamp, formatBoolean, formatEur, formatTimestamp, truncateMiddle } from '../lib/format.js';
const RESPONDED_STATES = new Set(['submitted', 'awaiting_outcome', 'not_filled', 'completed']);
@ -17,6 +17,22 @@ async function copyIdentifier(value) {
}
}
function useNow(intervalMs = 1000) {
const [now, setNow] = useState(() => Date.now());
useEffect(() => {
const timer = window.setInterval(() => setNow(Date.now()), intervalMs);
return () => window.clearInterval(timer);
}, [intervalMs]);
return now;
}
function formatRelativeAge(value, now) {
const age = formatAgeFromTimestamp(value, now);
return age === 'Unavailable' ? 'Age unavailable' : `${age} ago`;
}
function IdentifierRow({ label, value }) {
if (!value) return <div className="status-subtle">{`${label}: unavailable`}</div>;
@ -123,6 +139,7 @@ function LifecycleDetails({ item }) {
function QuoteLifecycleTable({ items }) {
const [expanded, setExpanded] = useState(() => new Set());
const now = useNow();
if (!items?.length) return <EmptyState>No quote lifecycle evidence has been observed yet.</EmptyState>;
function toggle(rowKey) {
@ -153,13 +170,15 @@ function QuoteLifecycleTable({ items }) {
{items.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 key={`${rowKey}:row`}>
<tr className={item.live_flash_at ? 'quote-row-flash' : undefined} key={`${rowKey}:row`}>
<td>
<div>{formatTimestamp(item.quote_activity_at || item.latest_stage_at)}</div>
<div>{formatTimestamp(quoteTime)}</div>
<div className="status-subtle quote-age">{formatRelativeAge(quoteTime, now)}</div>
{item.latest_stage_at && item.latest_stage_at !== item.quote_activity_at ? (
<div className="status-subtle">Updated {formatTimestamp(item.latest_stage_at)}</div>
<div className="status-subtle">Updated {formatTimestamp(item.latest_stage_at)} · {formatRelativeAge(item.latest_stage_at, now)}</div>
) : null}
</td>
<td><IdentifierRow label="Quote" value={item.quote_id} /></td>

View file

@ -11,6 +11,13 @@ function applySocketMessage(dashboard, payload, session) {
...dashboard.funds,
recent_quotes: payload.live?.recent_quotes || dashboard.funds.recent_quotes,
},
strategy: payload.live?.recent_lifecycle_rows ? {
...dashboard.strategy,
strategy_state: {
...dashboard.strategy.strategy_state,
recent_lifecycle_rows: payload.live.recent_lifecycle_rows,
},
} : dashboard.strategy,
status_bar: {
...dashboard.status_bar,
...(payload.live?.status_bar || {}),
@ -28,6 +35,22 @@ function applySocketMessage(dashboard, payload, session) {
},
},
};
case 'quote_lifecycle.updated':
return {
session,
dashboard: {
...dashboard,
strategy: {
...dashboard.strategy,
strategy_state: {
...dashboard.strategy.strategy_state,
recent_lifecycle_rows:
payload.recent_lifecycle_rows
|| dashboard.strategy.strategy_state.recent_lifecycle_rows,
},
},
},
};
case 'status_bar.updated':
return {
session,

View file

@ -549,6 +549,26 @@ table.lifecycle-table th:nth-child(5) {
width: 150px;
}
.quote-age {
font-variant-numeric: tabular-nums;
}
.quote-row-flash td {
animation: quote-row-flash-cell 1800ms ease-out;
}
@keyframes quote-row-flash-cell {
0% {
background: rgba(31, 122, 90, 0.24);
box-shadow: inset 4px 0 0 rgba(31, 122, 90, 0.72);
}
100% {
background: transparent;
box-shadow: inset 0 0 0 rgba(31, 122, 90, 0);
}
}
.quote-lifecycle-table th:nth-child(2),
.quote-lifecycle-table td:nth-child(2) {
width: 260px;

View file

@ -5,6 +5,7 @@ import { readFileSync } from 'node:fs';
const strategySource = readFileSync(new URL('../src/operator-dashboard/static/pages/StrategyPage.jsx', import.meta.url), 'utf8');
const fundsSource = readFileSync(new URL('../src/operator-dashboard/static/pages/FundsPage.jsx', import.meta.url), 'utf8');
const stylesSource = readFileSync(new URL('../src/operator-dashboard/static/styles.css', import.meta.url), 'utf8');
const serviceCardSource = readFileSync(new URL('../src/operator-dashboard/static/components/ServiceCard.jsx', import.meta.url), 'utf8');
test('strategy page owns consolidated quote lifecycle and successful trade tables', () => {
assert.match(strategySource, /Quote lifecycle/);
@ -15,6 +16,8 @@ test('strategy page owns consolidated quote lifecycle and successful trade table
assert.match(strategySource, /successful_trade_gross_edge_estimate_eure/);
assert.match(strategySource, /before fees/);
assert.match(strategySource, /Show lifecycle/);
assert.match(strategySource, /formatAgeFromTimestamp/);
assert.match(strategySource, /quote-row-flash/);
assert.match(strategySource, /Submitted means the relay accepted the response; it does not prove a trade\./);
assert.doesNotMatch(strategySource, /Actionable|actionable/);
});
@ -26,6 +29,13 @@ test('funds page no longer renders duplicate quote and submission tables', () =>
assert.doesNotMatch(fundsSource, /Durable ledger/);
});
test('dashboard freshness surfaces show age and exact timestamp evidence', () => {
assert.match(serviceCardSource, /formatTimestamp\(service\.freshness_at\)/);
assert.match(serviceCardSource, /Freshness at/);
assert.match(stylesSource, /\.quote-row-flash td/);
assert.match(stylesSource, /@keyframes quote-row-flash-cell/);
});
test('mobile status bar uses normal document flow instead of sticky viewport positioning', () => {
assert.match(
stylesSource,

View file

@ -12,6 +12,8 @@ import {
resolveDashboardControl,
resolveDashboardControlTimeoutMs,
} from '../src/core/operator-dashboard.mjs';
import { formatAge, formatAgeFromTimestamp } from '../src/operator-dashboard/static/lib/format.js';
import { dashboardReducer } from '../src/operator-dashboard/static/state/dashboardReducer.js';
import {
buildDashboardSessionToken,
parseBasicAuthorizationHeader,
@ -35,6 +37,13 @@ function buildConfig() {
return {
activePair: `${tradingBtc.assetId}->${tradingEure.assetId}`,
operatorDashboardQuoteLimit: 10,
kafkaTopicNormSwapDemand: 'norm.swap_demand',
kafkaTopicDecisionTradeDecision: 'decision.trade_decision',
kafkaTopicCmdExecuteTrade: 'cmd.execute_trade',
kafkaTopicRefMarketPrice: 'ref.market_price',
kafkaTopicStateIntentInventory: 'state.intent_inventory',
kafkaTopicOpsAlert: 'ops.alert',
kafkaTopicExecTradeResult: 'exec.trade_result',
tradingBtc,
tradingEure,
assetRegistry: new Map([
@ -212,7 +221,7 @@ test('basic auth resolves operator identity and reuses a session cookie', () =>
assert.equal(second.via, 'session_cookie');
});
test('live quote updates stay capped at ten items and submitted results update live counters', () => {
test('live quote updates stay capped and publish lifecycle rows without refresh', () => {
const config = buildConfig();
const state = createDashboardLiveState({
config,
@ -220,9 +229,10 @@ test('live quote updates stay capped at ten items and submitted results update l
lastSubmissionAt: '2026-04-04T08:00:00.000Z',
});
let latestQuoteUpdates = [];
for (let index = 0; index < 11; index += 1) {
applyDashboardLiveEvent(state, {
topic: 'norm.swap_demand',
latestQuoteUpdates = applyDashboardLiveEvent(state, {
topic: config.kafkaTopicNormSwapDemand,
event: {
observed_at: `2026-04-04T08:00:${String(index).padStart(2, '0')}.000Z`,
ingested_at: `2026-04-04T08:00:${String(index).padStart(2, '0')}.000Z`,
@ -239,23 +249,168 @@ test('live quote updates stay capped at ten items and submitted results update l
});
}
const updates = applyDashboardLiveEvent(state, {
topic: 'exec.trade_result',
const quoteLifecycleUpdate = latestQuoteUpdates.find((update) => update.type === 'quote_lifecycle.updated');
assert.equal(state.recent_quotes.length, 10);
assert.equal(state.recent_quotes[0].quote_id, 'quote-10');
assert.equal(state.recent_quotes.at(-1).quote_id, 'quote-1');
assert.equal(latestQuoteUpdates[0].type, 'quotes.recent');
assert.equal(quoteLifecycleUpdate.recent_lifecycle_rows[0].quote_id, 'quote-10');
assert.equal(quoteLifecycleUpdate.recent_lifecycle_rows[0].lifecycle_state, 'observed');
assert.ok(quoteLifecycleUpdate.recent_lifecycle_rows[0].live_flash_at);
const submittedUpdates = applyDashboardLiveEvent(state, {
topic: config.kafkaTopicExecTradeResult,
event: {
observed_at: '2026-04-04T08:30:00.000Z',
ingested_at: '2026-04-04T08:30:00.000Z',
payload: {
command_id: 'cmd-quote-10',
decision_id: 'decision-quote-10',
quote_id: 'quote-10',
pair: config.activePair,
status: 'submitted',
result_code: 'quote_response_ok',
},
},
});
assert.equal(state.recent_quotes.length, 10);
assert.equal(state.recent_quotes[0].quote_id, 'quote-10');
assert.equal(state.recent_quotes.at(-1).quote_id, 'quote-1');
const submittedLifecycleUpdate = submittedUpdates.find((update) => update.type === 'quote_lifecycle.updated');
assert.equal(state.recent_submission_count, 3);
assert.equal(state.last_submission_at, '2026-04-04T08:30:00.000Z');
assert.equal(updates[0].type, 'status_bar.updated');
assert.ok(submittedUpdates.find((update) => update.type === 'status_bar.updated'));
assert.equal(submittedLifecycleUpdate.recent_lifecycle_rows[0].quote_id, 'quote-10');
assert.equal(submittedLifecycleUpdate.recent_lifecycle_rows[0].lifecycle_state, 'submitted');
assert.doesNotMatch(
`${submittedLifecycleUpdate.recent_lifecycle_rows[0].lifecycle_label} ${submittedLifecycleUpdate.recent_lifecycle_rows[0].reason_text}`,
/completed|successful trade|asset delta/i,
);
});
test('live decision, command, and executor result events advance lifecycle rows without bootstrap reload', () => {
const config = buildConfig();
const state = createDashboardLiveState({ config });
applyDashboardLiveEvent(state, {
topic: config.kafkaTopicNormSwapDemand,
event: {
observed_at: '2026-04-04T09:00:00.000Z',
ingested_at: '2026-04-04T09:00:00.000Z',
payload: {
quote_id: 'quote-live',
pair: config.activePair,
asset_in: config.tradingBtc.assetId,
asset_out: config.tradingEure.assetId,
amount_in: '100',
amount_out: '200',
},
},
});
const decisionUpdates = applyDashboardLiveEvent(state, {
topic: config.kafkaTopicDecisionTradeDecision,
event: {
observed_at: '2026-04-04T09:00:01.000Z',
ingested_at: '2026-04-04T09:00:01.000Z',
payload: {
decision_id: 'decision-live',
quote_id: 'quote-live',
pair: config.activePair,
decision: 'actionable',
decision_reason: 'actionable',
gross_edge_pct: '0.49',
eure_notional: '5',
},
},
});
assert.equal(decisionUpdates[0].recent_lifecycle_rows[0].lifecycle_state, 'evaluated');
assert.equal(decisionUpdates[0].recent_lifecycle_rows[0].reason_code, 'strategy_approved');
const commandUpdates = applyDashboardLiveEvent(state, {
topic: config.kafkaTopicCmdExecuteTrade,
event: {
observed_at: '2026-04-04T09:00:02.000Z',
ingested_at: '2026-04-04T09:00:02.000Z',
payload: {
command_id: 'cmd-live',
decision_id: 'decision-live',
quote_id: 'quote-live',
pair: config.activePair,
asset_in: config.tradingBtc.assetId,
asset_out: config.tradingEure.assetId,
quote_output: {
amount_in: '101',
amount_out: '201',
},
},
},
});
assert.equal(commandUpdates[0].recent_lifecycle_rows[0].lifecycle_state, 'command_emitted');
assert.equal(commandUpdates[0].recent_lifecycle_rows[0].submitted_terms.amount_in_units, '101');
const blockedUpdates = applyDashboardLiveEvent(state, {
topic: config.kafkaTopicExecTradeResult,
event: {
observed_at: '2026-04-04T09:00:03.000Z',
ingested_at: '2026-04-04T09:00:03.000Z',
payload: {
command_id: 'cmd-live',
decision_id: 'decision-live',
quote_id: 'quote-live',
pair: config.activePair,
status: 'rejected',
result_code: 'executor_disarmed',
note: 'executor is disarmed',
},
},
});
assert.equal(blockedUpdates.length, 1);
assert.equal(blockedUpdates[0].type, 'quote_lifecycle.updated');
assert.equal(blockedUpdates[0].recent_lifecycle_rows[0].lifecycle_state, 'blocked');
assert.equal(blockedUpdates[0].recent_lifecycle_rows[0].lifecycle_label, 'Blocked before submit');
assert.notEqual(blockedUpdates[0].recent_lifecycle_rows[0].lifecycle_label, 'Rejected by strategy');
});
test('socket lifecycle messages replace strategy rows without page refresh', () => {
const dashboard = {
funds: { recent_quotes: [] },
status_bar: {},
strategy: {
strategy_state: {
recent_lifecycle_rows: [],
},
},
};
const state = {
dashboard,
session: { authenticated: true },
page: 'strategy',
};
const next = dashboardReducer(state, {
type: 'socket.message.received',
payload: {
type: 'quote_lifecycle.updated',
recent_lifecycle_rows: [{
quote_id: 'quote-live',
lifecycle_state: 'observed',
live_flash_at: '2026-04-04T09:00:00.000Z',
}],
},
});
assert.equal(next.dashboard.strategy.strategy_state.recent_lifecycle_rows[0].quote_id, 'quote-live');
assert.equal(next.dashboard.strategy.strategy_state.recent_lifecycle_rows[0].live_flash_at, '2026-04-04T09:00:00.000Z');
});
test('dashboard age formatting uses seconds first and exact timestamp deltas', () => {
assert.equal(formatAge(999), '999 ms');
assert.equal(formatAge(1_500), '1s');
assert.equal(formatAge(65_000), '1m 5s');
assert.equal(
formatAgeFromTimestamp('2026-04-04T08:00:00.000Z', Date.parse('2026-04-04T08:01:05.000Z')),
'1m 5s',
);
});
test('live dashboard ignores ops alert events so alert severity cannot re-enter operator state', () => {