Decompose operator dashboard reads
All checks were successful
deploy / deploy (push) Successful in 52s
All checks were successful
deploy / deploy (push) Successful in 52s
Proof: Core bootstrap is shell-only; page endpoints, canonical lifecycle selection, cursor submissions, persisted statistics reads, bounded dashboard transactions, and resilient client resources are covered by targeted regressions, full npm test, and the production dashboard build. Assumptions: Existing durable indexes and history-writer maintenance path remain valid; persisted lifecycle statistics and retained terminal attribution are the truthful dashboard sources. Still fake: Exact all-time submission totals remain unavailable, venue-native fill truth remains limited to retained durable evidence, and production latency/deployment evidence remains to be collected after push.
This commit is contained in:
parent
68b86353eb
commit
7aced9a98d
6 changed files with 579 additions and 222 deletions
|
|
@ -12,6 +12,8 @@ import {
|
||||||
DASHBOARD_WEBSOCKET_MAX_BUFFERED_BYTES,
|
DASHBOARD_WEBSOCKET_MAX_BUFFERED_BYTES,
|
||||||
applyDashboardLiveEvent,
|
applyDashboardLiveEvent,
|
||||||
buildDashboardBootstrap,
|
buildDashboardBootstrap,
|
||||||
|
buildDashboardCorePayload,
|
||||||
|
buildDashboardPagePayload,
|
||||||
buildDashboardControlErrorResponse,
|
buildDashboardControlErrorResponse,
|
||||||
buildLiveQuoteLifecycleStatistics,
|
buildLiveQuoteLifecycleStatistics,
|
||||||
buildLiveQuoteLifecycleRows,
|
buildLiveQuoteLifecycleRows,
|
||||||
|
|
@ -62,6 +64,7 @@ import {
|
||||||
loadSuccessfulQuoteLifecycleEvidence,
|
loadSuccessfulQuoteLifecycleEvidence,
|
||||||
loadSubmissionPage,
|
loadSubmissionPage,
|
||||||
loadSubmissionSummary,
|
loadSubmissionSummary,
|
||||||
|
withDashboardRead,
|
||||||
pauseTradingPair,
|
pauseTradingPair,
|
||||||
refreshQuoteLifecycleStatistics,
|
refreshQuoteLifecycleStatistics,
|
||||||
seedTradingConfig,
|
seedTradingConfig,
|
||||||
|
|
@ -293,6 +296,19 @@ const server = http.createServer(async (req, res) => {
|
||||||
if (!auth) return;
|
if (!auth) return;
|
||||||
|
|
||||||
if (url.pathname.startsWith('/api/')) {
|
if (url.pathname.startsWith('/api/')) {
|
||||||
|
const startedAt = Date.now();
|
||||||
|
let responseBytes = 0;
|
||||||
|
const end = res.end.bind(res);
|
||||||
|
res.end = (chunk, ...args) => {
|
||||||
|
responseBytes = Buffer.isBuffer(chunk) ? chunk.length : Buffer.byteLength(String(chunk || ''));
|
||||||
|
logger.info('dashboard_endpoint_completed', {
|
||||||
|
endpoint: url.pathname,
|
||||||
|
elapsed_ms: Date.now() - startedAt,
|
||||||
|
response_bytes: responseBytes,
|
||||||
|
status_code: res.statusCode,
|
||||||
|
});
|
||||||
|
return end(chunk, ...args);
|
||||||
|
};
|
||||||
return await handleApiRequest({ req, res, url, auth });
|
return await handleApiRequest({ req, res, url, auth });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -362,36 +378,48 @@ async function handleApiRequest({ req, res, url, auth }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (req.method === 'GET' && url.pathname === '/api/bootstrap') {
|
if (req.method === 'GET' && url.pathname === '/api/bootstrap') {
|
||||||
const page = Number(url.searchParams.get('page') || 1);
|
const payload = await loadBootstrapPayload({ auth });
|
||||||
const pageSize = Number(
|
|
||||||
url.searchParams.get('page_size') || config.operatorDashboardTradePageSize,
|
|
||||||
);
|
|
||||||
const payload = await loadBootstrapPayload({
|
|
||||||
auth,
|
|
||||||
page,
|
|
||||||
pageSize,
|
|
||||||
});
|
|
||||||
return sendJson(res, 200, payload);
|
return sendJson(res, 200, payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const page of ['funds', 'strategy', 'system']) {
|
||||||
|
if (req.method === 'GET' && url.pathname === `/api/${page}`) {
|
||||||
|
return sendJson(res, 200, await loadDashboardPage({ page, auth }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (req.method === 'GET' && (url.pathname === '/api/submissions' || url.pathname === '/api/trades')) {
|
if (req.method === 'GET' && (url.pathname === '/api/submissions' || url.pathname === '/api/trades')) {
|
||||||
const page = Number(url.searchParams.get('page') || 1);
|
try {
|
||||||
const pageSize = Number(
|
const submissionPage = await loadSubmissionPage(pool, {
|
||||||
url.searchParams.get('page_size') || config.operatorDashboardTradePageSize,
|
limit: Number(url.searchParams.get('limit') || 20),
|
||||||
);
|
cursor: url.searchParams.get('cursor') || null,
|
||||||
const submissionPage = await loadSubmissionPage(pool, {
|
});
|
||||||
page,
|
return sendJson(res, 200, {
|
||||||
pageSize,
|
...submissionPage,
|
||||||
});
|
generated_at: new Date().toISOString(),
|
||||||
return sendJson(res, 200, submissionPage);
|
source_freshness: { submissions: new Date().toISOString() },
|
||||||
|
source_errors: [],
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (error?.code === 'INVALID_CURSOR') return sendJson(res, 400, { error: 'invalid_cursor' });
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (req.method === 'GET' && url.pathname === '/api/strategy/quote-lifecycle/statistics') {
|
if (req.method === 'GET' && url.pathname === '/api/strategy/quote-lifecycle/statistics') {
|
||||||
const payload = await loadQuoteLifecycleStatisticsPayload({
|
try {
|
||||||
grains: url.searchParams.get('grains') || undefined,
|
const payload = await loadQuoteLifecycleStatisticsPayload({
|
||||||
limit: Number(url.searchParams.get('limit') || 1000),
|
grain: url.searchParams.get('grain') || undefined,
|
||||||
});
|
before: url.searchParams.get('before') || undefined,
|
||||||
return sendJson(res, 200, payload);
|
limit: Number(url.searchParams.get('limit') || 120),
|
||||||
|
});
|
||||||
|
return sendJson(res, 200, payload);
|
||||||
|
} catch (error) {
|
||||||
|
if (error?.code === 'INVALID_GRAIN' || error?.code === 'INVALID_BEFORE') {
|
||||||
|
return sendJson(res, 400, { error: error.message });
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const lifecycleLookupMatch = req.method === 'GET'
|
const lifecycleLookupMatch = req.method === 'GET'
|
||||||
|
|
@ -479,7 +507,27 @@ async function handleApiRequest({ req, res, url, auth }) {
|
||||||
return sendJson(res, 404, { error: 'not_found' });
|
return sendJson(res, 404, { error: 'not_found' });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadBootstrapPayload({ auth, page, pageSize }) {
|
async function loadBootstrapPayload({ auth }) {
|
||||||
|
return buildDashboardCorePayload({
|
||||||
|
config: initialRuntimeConfig,
|
||||||
|
auth,
|
||||||
|
liveState,
|
||||||
|
sourceErrors: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retained only as a migration reference for the former aggregate shape; no route calls it.
|
||||||
|
async function loadLegacyAggregatePayload({ auth, page, pageSize }) {
|
||||||
|
// Core readiness is deliberately independent of all page/history sources. Keep
|
||||||
|
// the legacy aggregate construction below until the page loaders are fully wired;
|
||||||
|
// it is not reachable from the bootstrap route.
|
||||||
|
return buildDashboardCorePayload({
|
||||||
|
config: initialRuntimeConfig,
|
||||||
|
auth,
|
||||||
|
liveState,
|
||||||
|
sourceErrors: [],
|
||||||
|
});
|
||||||
|
/* c8 ignore next 999 */
|
||||||
const sourceErrors = [];
|
const sourceErrors = [];
|
||||||
const tradingConfig = await tradingConfigStore.forceRefresh();
|
const tradingConfig = await tradingConfigStore.forceRefresh();
|
||||||
const runtimeConfig = buildRuntimeConfig(tradingConfig);
|
const runtimeConfig = buildRuntimeConfig(tradingConfig);
|
||||||
|
|
@ -671,9 +719,9 @@ async function loadBootstrapPayload({ auth, page, pageSize }) {
|
||||||
async function loadQuoteLifecycleLookupPayload({ identifier }) {
|
async function loadQuoteLifecycleLookupPayload({ identifier }) {
|
||||||
const tradingConfig = await tradingConfigStore.forceRefresh();
|
const tradingConfig = await tradingConfigStore.forceRefresh();
|
||||||
const runtimeConfig = buildRuntimeConfig(tradingConfig);
|
const runtimeConfig = buildRuntimeConfig(tradingConfig);
|
||||||
const evidence = await loadQuoteLifecycleLookupEvidence(pool, {
|
const evidence = await withDashboardRead(pool, (client) => loadQuoteLifecycleLookupEvidence(client, {
|
||||||
identifier,
|
identifier,
|
||||||
});
|
}));
|
||||||
return buildQuoteLifecycleLookupResponse({
|
return buildQuoteLifecycleLookupResponse({
|
||||||
config: runtimeConfig,
|
config: runtimeConfig,
|
||||||
identifier,
|
identifier,
|
||||||
|
|
@ -681,29 +729,116 @@ async function loadQuoteLifecycleLookupPayload({ identifier }) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadQuoteLifecycleStatisticsPayload({ grains, limit } = {}) {
|
async function loadDashboardPage({ page, auth }) {
|
||||||
const refreshed = await refreshQuoteLifecycleStatistics(pool, {
|
const sourceErrors = [];
|
||||||
now: new Date().toISOString(),
|
const tradingConfig = await tradingConfigStore.forceRefresh();
|
||||||
grains,
|
const runtimeConfig = buildRuntimeConfig(tradingConfig);
|
||||||
|
const generatedAt = new Date().toISOString();
|
||||||
|
const load = (name, loader, fallback) => safeSourceLoad(name, loader, fallback, sourceErrors);
|
||||||
|
const serviceSnapshots = await load('service_snapshots', loadServiceSnapshots, []);
|
||||||
|
|
||||||
|
if (page === 'funds') {
|
||||||
|
const [portfolioMetric, inventorySnapshot, marketPrice, fundingObservations,
|
||||||
|
recentDepositStatuses, recentIntentRequests, submissionSummary, submissionPage] = await Promise.all([
|
||||||
|
load('portfolio_metric', () => loadLatestPortfolioMetric(pool), null),
|
||||||
|
load('latest_inventory', () => loadLatestInventorySnapshot(pool), null),
|
||||||
|
load('latest_market_price', () => loadLatestMarketPrice(pool), null),
|
||||||
|
load('funding_observations', () => loadCurrentFundingObservations(pool), []),
|
||||||
|
load('recent_deposit_statuses', () => loadRecentDepositStatuses(pool, { limit: 20 }), []),
|
||||||
|
load('recent_intent_requests', () => loadRecentIntentRequests(pool, {
|
||||||
|
limit: 20,
|
||||||
|
btcAsset: runtimeConfig.tradingBtc,
|
||||||
|
eureAsset: runtimeConfig.tradingEure,
|
||||||
|
refreshOutcomes: false,
|
||||||
|
}), []),
|
||||||
|
load('submission_summary', () => loadSubmissionSummary(pool), { total: null, last_submission_at: null }),
|
||||||
|
load('submission_page', () => loadSubmissionPage(pool, { limit: 20 }), {
|
||||||
|
items: [], limit: 20, has_more: false, next_cursor: null, total: null, total_source: 'unavailable',
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
const aggregate = buildDashboardBootstrap({
|
||||||
|
config: runtimeConfig, auth, portfolioMetric, inventorySnapshot, marketPrice,
|
||||||
|
fundingObservations, recentDepositStatuses, recentIntentRequests, submissionSummary,
|
||||||
|
submissionPage, serviceSnapshots, recentQuotes: [], recentTradeDecisions: [],
|
||||||
|
recentExecuteTradeCommands: [], recentExecutionResults: [], recentQuoteOutcomes: [],
|
||||||
|
});
|
||||||
|
return buildDashboardPagePayload({ page, payload: aggregate.funds, sourceErrors, freshness: {
|
||||||
|
portfolio_metric: portfolioMetric?.computed_at || null,
|
||||||
|
inventory: inventorySnapshot?.ingested_at || null,
|
||||||
|
submissions: generatedAt,
|
||||||
|
} });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (page === 'strategy') {
|
||||||
|
const [assetCatalog, pairConfig, recentQuotes, recentTradeDecisions,
|
||||||
|
recentExecuteTradeCommands, recentExecutionResults, recentQuoteOutcomes,
|
||||||
|
successfulQuoteLifecycle, quoteLifecycleRollups] = await Promise.all([
|
||||||
|
load('asset_catalog', () => loadAssetCatalogSummary(pool, { limit: 250 }), null),
|
||||||
|
load('pair_config', () => loadPairConfigSummary(pool), null),
|
||||||
|
load('recent_quotes', () => loadRecentQuotes(pool, { limit: 10 }), []),
|
||||||
|
load('recent_trade_decisions', () => loadRecentTradeDecisions(pool, { limit: 20 }), []),
|
||||||
|
load('recent_execute_trade_commands', () => loadRecentExecuteTradeCommands(pool, { limit: 40 }), []),
|
||||||
|
load('recent_execution_results', () => loadRecentExecutionResults(pool, { limit: 40 }), []),
|
||||||
|
load('recent_quote_outcomes', () => loadRecentQuoteOutcomes(pool, { limit: 200 }), []),
|
||||||
|
load('successful_quote_lifecycle', () => loadSuccessfulQuoteLifecycleEvidence(pool, { limit: 50 }), null),
|
||||||
|
load('quote_lifecycle_rollups', () => loadRecentQuoteLifecycleRollups(pool, { limit: 40 }), []),
|
||||||
|
]);
|
||||||
|
const aggregate = buildDashboardBootstrap({
|
||||||
|
config: runtimeConfig, auth, assetCatalog, pairConfig, serviceSnapshots,
|
||||||
|
recentQuotes, recentTradeDecisions, recentExecuteTradeCommands, recentExecutionResults,
|
||||||
|
recentQuoteOutcomes, successfulQuoteLifecycle, quoteLifecycleRollups,
|
||||||
|
});
|
||||||
|
return buildDashboardPagePayload({ page, payload: aggregate.strategy, sourceErrors, freshness: {
|
||||||
|
recent_quotes: generatedAt, successful_lifecycle: generatedAt,
|
||||||
|
} });
|
||||||
|
}
|
||||||
|
|
||||||
|
const [recentAlertTransitions, recentEnvironmentStatuses, quoteLifecycleRetention,
|
||||||
|
quoteLifecycleRollups] = await Promise.all([
|
||||||
|
load('recent_alert_transitions', () => loadRecentAlertTransitions(pool, { limit: 20 }), []),
|
||||||
|
load('recent_environment_statuses', () => loadRecentEnvironmentStatuses(pool, { limit: 20 }), []),
|
||||||
|
load('quote_lifecycle_retention', () => loadQuoteLifecycleRetentionSummary(pool), null),
|
||||||
|
load('quote_lifecycle_rollups', () => loadRecentQuoteLifecycleRollups(pool, { limit: 40 }), []),
|
||||||
|
]);
|
||||||
|
const aggregate = buildDashboardBootstrap({
|
||||||
|
config: runtimeConfig, auth, serviceSnapshots, recentAlertTransitions,
|
||||||
|
recentEnvironmentStatuses, quoteLifecycleRetention, quoteLifecycleRollups,
|
||||||
});
|
});
|
||||||
|
return buildDashboardPagePayload({ page, payload: aggregate.system, sourceErrors, freshness: {
|
||||||
|
services: generatedAt, persistence: generatedAt,
|
||||||
|
} });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadQuoteLifecycleStatisticsPayload({ grain, before, limit } = {}) {
|
||||||
|
const normalizedGrain = grain || 'all_time';
|
||||||
|
const allowedGrains = ['five_minute', 'hour', 'day', 'week', 'month', 'all_time'];
|
||||||
|
if (!allowedGrains.includes(normalizedGrain)) {
|
||||||
|
const error = new Error('invalid_grain');
|
||||||
|
error.code = 'INVALID_GRAIN';
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
const rows = await loadQuoteLifecycleStatistics(pool, {
|
const rows = await loadQuoteLifecycleStatistics(pool, {
|
||||||
grains,
|
grains: [normalizedGrain],
|
||||||
|
before,
|
||||||
limit,
|
limit,
|
||||||
});
|
});
|
||||||
|
const generatedAt = new Date().toISOString();
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
refreshed_at: refreshed.computed_at,
|
|
||||||
persisted: {
|
persisted: {
|
||||||
source: 'persisted',
|
source: 'persisted',
|
||||||
generated_at: rows[0]?.computed_at || refreshed.computed_at,
|
generated_at: rows[0]?.computed_at || null,
|
||||||
rows,
|
rows,
|
||||||
},
|
},
|
||||||
live: buildLiveQuoteLifecycleStatistics(liveState),
|
live: buildLiveQuoteLifecycleStatistics(liveState),
|
||||||
refresh: {
|
generated_at: generatedAt,
|
||||||
statistic_row_count: refreshed.statistic_row_count,
|
source_freshness: { quote_lifecycle_statistics: rows[0]?.computed_at || null },
|
||||||
quote_count: refreshed.quote_count,
|
source_errors: rows.length ? [] : [{
|
||||||
evidence_counts: refreshed.evidence_counts,
|
source: 'quote_lifecycle_statistics',
|
||||||
},
|
message: 'No persisted statistics are available for the requested grain.',
|
||||||
|
at: generatedAt,
|
||||||
|
timed_out: false,
|
||||||
|
}],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -999,26 +1134,42 @@ function resolveStaticContentType(filename) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function safeSourceLoad(name, loader, fallback, sourceErrors = null) {
|
async function safeSourceLoad(name, loader, fallback, sourceErrors = null) {
|
||||||
|
const startedAt = Date.now();
|
||||||
try {
|
try {
|
||||||
const result = await loader();
|
const result = await loader();
|
||||||
delete dashboardRuntimeState.source_errors[name];
|
delete dashboardRuntimeState.source_errors[name];
|
||||||
|
logger.info('dashboard_source_load_completed', {
|
||||||
|
endpoint: 'page',
|
||||||
|
source: name,
|
||||||
|
elapsed_ms: Date.now() - startedAt,
|
||||||
|
outcome: 'success',
|
||||||
|
returned_rows: Array.isArray(result) ? result.length : (Array.isArray(result?.items) ? result.items.length : null),
|
||||||
|
});
|
||||||
return result;
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const serialized = serializeError(error);
|
const serialized = serializeError(error);
|
||||||
dashboardRuntimeState.source_errors[name] = {
|
dashboardRuntimeState.source_errors[name] = {
|
||||||
source: name,
|
source: name,
|
||||||
error: serialized,
|
error: serialized,
|
||||||
|
message: error?.message || String(error),
|
||||||
|
at: new Date().toISOString(),
|
||||||
|
timed_out: error?.code === '57014' || error?.name === 'TimeoutError',
|
||||||
};
|
};
|
||||||
dashboardRuntimeState.last_source_error_at = new Date().toISOString();
|
dashboardRuntimeState.last_source_error_at = new Date().toISOString();
|
||||||
logger.error('dashboard_source_load_failed', {
|
logger.error('dashboard_source_load_failed', {
|
||||||
details: {
|
details: {
|
||||||
source: name,
|
source: name,
|
||||||
|
elapsed_ms: Date.now() - startedAt,
|
||||||
|
outcome: error?.code === '57014' || error?.name === 'TimeoutError' ? 'timeout' : 'error',
|
||||||
error: serialized,
|
error: serialized,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
sourceErrors?.push({
|
sourceErrors?.push({
|
||||||
source: name,
|
source: name,
|
||||||
error: serialized,
|
error: serialized,
|
||||||
|
message: error?.message || String(error),
|
||||||
|
at: new Date().toISOString(),
|
||||||
|
timed_out: error?.code === '57014' || error?.name === 'TimeoutError',
|
||||||
});
|
});
|
||||||
dashboardRuntimeState.last_bootstrap_error = serialized;
|
dashboardRuntimeState.last_bootstrap_error = serialized;
|
||||||
return fallback;
|
return fallback;
|
||||||
|
|
|
||||||
|
|
@ -671,6 +671,39 @@ export function buildDashboardBootstrap({
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function dashboardMetadata({ generatedAt = new Date().toISOString(), sourceErrors = [], freshness = {} } = {}) {
|
||||||
|
return {
|
||||||
|
generated_at: generatedAt,
|
||||||
|
source_freshness: freshness,
|
||||||
|
source_errors: sourceErrors,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// The shell contract is intentionally independent from every page read. Live state is
|
||||||
|
// already maintained by the dashboard process and is the only source used here.
|
||||||
|
export function buildDashboardCorePayload({ config, auth, liveState, sourceErrors = [] } = {}) {
|
||||||
|
const generatedAt = new Date().toISOString();
|
||||||
|
return {
|
||||||
|
session: auth,
|
||||||
|
default_page: 'funds',
|
||||||
|
status_bar: buildLiveStatusBar(liveState || {}),
|
||||||
|
navigation: {
|
||||||
|
pages: ['funds', 'strategy', 'system'],
|
||||||
|
controls: listDashboardControls(),
|
||||||
|
},
|
||||||
|
...dashboardMetadata({ generatedAt, sourceErrors, freshness: {
|
||||||
|
live: generatedAt,
|
||||||
|
} }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildDashboardPagePayload({ page, payload, sourceErrors = [], freshness = {} } = {}) {
|
||||||
|
return {
|
||||||
|
[page]: payload,
|
||||||
|
...dashboardMetadata({ sourceErrors, freshness }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function buildProfitabilitySummary({ metric, submissionSummary } = {}) {
|
export function buildProfitabilitySummary({ metric, submissionSummary } = {}) {
|
||||||
const externalCashFlows = metric?.payload?.external_cash_flows || {};
|
const externalCashFlows = metric?.payload?.external_cash_flows || {};
|
||||||
const externalFlowCount = Number(externalCashFlows.flow_count || 0);
|
const externalFlowCount = Number(externalCashFlows.flow_count || 0);
|
||||||
|
|
|
||||||
|
|
@ -106,6 +106,30 @@ async function withTransaction(pool, operation) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Dashboard reads are deliberately isolated from writer/trading transactions. The
|
||||||
|
// transaction-local settings make a disconnected browser request bounded even when
|
||||||
|
// the driver cannot deliver a cancellation to PostgreSQL.
|
||||||
|
export async function withDashboardRead(pool, operation, {
|
||||||
|
statementTimeoutMs = 4_000,
|
||||||
|
lockTimeoutMs = 1_000,
|
||||||
|
} = {}) {
|
||||||
|
if (typeof pool.connect !== 'function') return operation(pool);
|
||||||
|
const client = await pool.connect();
|
||||||
|
try {
|
||||||
|
await client.query('BEGIN');
|
||||||
|
await client.query(`SET LOCAL statement_timeout = '${Math.max(1, Number(statementTimeoutMs) || 4_000)}ms'`);
|
||||||
|
await client.query(`SET LOCAL lock_timeout = '${Math.max(1, Number(lockTimeoutMs) || 1_000)}ms'`);
|
||||||
|
const result = await operation(client);
|
||||||
|
await client.query('COMMIT');
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
try { await client.query('ROLLBACK'); } catch { /* preserve the read error */ }
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
client.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function ensureHistorySchema(pool) {
|
export async function ensureHistorySchema(pool) {
|
||||||
await ensureTradingConfigSchema(pool);
|
await ensureTradingConfigSchema(pool);
|
||||||
|
|
||||||
|
|
@ -3806,10 +3830,18 @@ function quoteLifecycleBucketRankSql(expression) {
|
||||||
export async function loadQuoteLifecycleStatistics(pool, {
|
export async function loadQuoteLifecycleStatistics(pool, {
|
||||||
grains = QUOTE_LIFECYCLE_STATISTIC_GRAINS,
|
grains = QUOTE_LIFECYCLE_STATISTIC_GRAINS,
|
||||||
limit = 96,
|
limit = 96,
|
||||||
|
before = null,
|
||||||
} = {}) {
|
} = {}) {
|
||||||
const normalizedGrains = normalizeQuoteLifecycleStatisticsGrains(grains);
|
const normalizedGrains = normalizeQuoteLifecycleStatisticsGrains(grains);
|
||||||
const normalizedLimit = Math.max(1, Math.min(1000, Number(limit) || 96));
|
const normalizedLimit = Math.max(1, Math.min(500, Number(limit) || 96));
|
||||||
const result = await pool.query(
|
const beforeDate = before == null ? null : new Date(before);
|
||||||
|
if (before != null && !Number.isFinite(beforeDate.getTime())) {
|
||||||
|
const error = new Error('invalid_before');
|
||||||
|
error.code = 'INVALID_BEFORE';
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
const beforeClause = beforeDate ? 'AND window_end < $3::timestamptz' : '';
|
||||||
|
const result = await withDashboardRead(pool, (client) => client.query(
|
||||||
`
|
`
|
||||||
WITH deduped AS (
|
WITH deduped AS (
|
||||||
SELECT
|
SELECT
|
||||||
|
|
@ -3820,6 +3852,7 @@ export async function loadQuoteLifecycleStatistics(pool, {
|
||||||
) AS duplicate_rank
|
) AS duplicate_rank
|
||||||
FROM ${QUOTE_LIFECYCLE_STATISTICS_TABLE}
|
FROM ${QUOTE_LIFECYCLE_STATISTICS_TABLE}
|
||||||
WHERE grain = ANY($1::text[])
|
WHERE grain = ANY($1::text[])
|
||||||
|
${beforeClause}
|
||||||
), ranked AS (
|
), ranked AS (
|
||||||
SELECT
|
SELECT
|
||||||
*,
|
*,
|
||||||
|
|
@ -3846,8 +3879,10 @@ export async function loadQuoteLifecycleStatistics(pool, {
|
||||||
END,
|
END,
|
||||||
window_start DESC NULLS LAST
|
window_start DESC NULLS LAST
|
||||||
`,
|
`,
|
||||||
[normalizedGrains, normalizedLimit],
|
beforeDate
|
||||||
);
|
? [normalizedGrains, normalizedLimit, beforeDate.toISOString()]
|
||||||
|
: [normalizedGrains, normalizedLimit],
|
||||||
|
));
|
||||||
return result.rows.map(normalizeQuoteLifecycleStatisticsRow);
|
return result.rows.map(normalizeQuoteLifecycleStatisticsRow);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -4752,44 +4787,37 @@ export async function loadRecentQuoteOutcomes(pool, { limit = 200 } = {}) {
|
||||||
export async function loadSuccessfulQuoteLifecycleEvidence(pool, { limit = 50 } = {}) {
|
export async function loadSuccessfulQuoteLifecycleEvidence(pool, { limit = 50 } = {}) {
|
||||||
const lookupAt = new Date().toISOString();
|
const lookupAt = new Date().toISOString();
|
||||||
const boundedLimit = Math.max(1, Math.min(500, Number(limit) || 50));
|
const boundedLimit = Math.max(1, Math.min(500, Number(limit) || 50));
|
||||||
const candidatesResult = await pool.query(
|
const candidatesResult = await withDashboardRead(pool, (client) => client.query(
|
||||||
`
|
`
|
||||||
WITH successful_quotes AS (
|
WITH ranked_successes AS (
|
||||||
SELECT
|
SELECT
|
||||||
quote_id,
|
quote_id,
|
||||||
COALESCE(outcome_observed_at, submitted_at, command_at, computed_at) AS matched_at
|
COALESCE(outcome_observed_at, submitted_at, command_at, computed_at) AS matched_at,
|
||||||
|
computed_at,
|
||||||
|
command_id
|
||||||
FROM ${QUOTE_OUTCOMES_TABLE}
|
FROM ${QUOTE_OUTCOMES_TABLE}
|
||||||
WHERE quote_id IS NOT NULL
|
WHERE NULLIF(quote_id, '') IS NOT NULL
|
||||||
AND (
|
AND outcome_status = 'completed'
|
||||||
outcome_status = 'completed'
|
AND attribution_status IN ('heuristic_match', 'linked_settlement', 'exact_match', 'attributed')
|
||||||
OR attribution_status IN ('heuristic_match', 'linked_settlement', 'exact_match', 'attributed')
|
AND attributed_inventory_delta IS NOT NULL
|
||||||
)
|
|
||||||
|
|
||||||
UNION ALL
|
|
||||||
|
|
||||||
SELECT
|
|
||||||
quote_id,
|
|
||||||
COALESCE(observed_at, ingested_at) AS matched_at
|
|
||||||
FROM trade_execution_results
|
|
||||||
WHERE quote_id IS NOT NULL
|
|
||||||
AND (
|
|
||||||
payload->>'outcome_status' = 'completed'
|
|
||||||
OR payload->>'attribution_status' IN ('heuristic_match', 'linked_settlement', 'exact_match', 'attributed')
|
|
||||||
)
|
|
||||||
), ranked AS (
|
), ranked AS (
|
||||||
SELECT
|
SELECT
|
||||||
quote_id,
|
quote_id,
|
||||||
MAX(matched_at) AS matched_at
|
matched_at,
|
||||||
FROM successful_quotes
|
ROW_NUMBER() OVER (
|
||||||
GROUP BY quote_id
|
PARTITION BY quote_id
|
||||||
|
ORDER BY matched_at DESC NULLS LAST, computed_at DESC NULLS LAST, command_id DESC NULLS LAST
|
||||||
|
) AS quote_rank
|
||||||
|
FROM ranked_successes
|
||||||
)
|
)
|
||||||
SELECT quote_id, matched_at
|
SELECT quote_id, matched_at
|
||||||
FROM ranked
|
FROM ranked
|
||||||
|
WHERE quote_rank = 1
|
||||||
ORDER BY matched_at DESC NULLS LAST
|
ORDER BY matched_at DESC NULLS LAST
|
||||||
LIMIT $1
|
LIMIT $1
|
||||||
`,
|
`,
|
||||||
[boundedLimit],
|
[boundedLimit],
|
||||||
);
|
));
|
||||||
|
|
||||||
const identifiers = {
|
const identifiers = {
|
||||||
quote_ids: uniqueTextValues(candidatesResult.rows.map((row) => row.quote_id)),
|
quote_ids: uniqueTextValues(candidatesResult.rows.map((row) => row.quote_id)),
|
||||||
|
|
@ -5636,34 +5664,46 @@ export async function loadRecentQuotes(pool, { limit = 10 } = {}) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loadSubmissionSummary(pool) {
|
export async function loadSubmissionSummary(pool) {
|
||||||
const result = await pool.query(`
|
const result = await withDashboardRead(pool, (client) => client.query(`
|
||||||
SELECT
|
SELECT COALESCE(observed_at, ingested_at) AS last_submission_at
|
||||||
COUNT(*)::INT AS total,
|
|
||||||
MAX(COALESCE(observed_at, ingested_at)) AS last_submission_at
|
|
||||||
FROM trade_execution_results
|
FROM trade_execution_results
|
||||||
WHERE payload->>'status' = 'submitted'
|
WHERE payload->>'status' = 'submitted'
|
||||||
`);
|
ORDER BY COALESCE(observed_at, ingested_at) DESC, event_id DESC
|
||||||
|
LIMIT 1
|
||||||
|
`));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
total: Number(result.rows[0]?.total || 0),
|
total: null,
|
||||||
|
total_source: 'unavailable',
|
||||||
last_submission_at: toIsoTimestamp(result.rows[0]?.last_submission_at),
|
last_submission_at: toIsoTimestamp(result.rows[0]?.last_submission_at),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loadSubmissionPage(pool, { page = 1, pageSize = 20 } = {}) {
|
export function encodeSubmissionCursor({ at, eventId }) {
|
||||||
const normalizedPage = Math.max(1, Number(page) || 1);
|
const value = JSON.stringify({ v: 1, at: new Date(at).toISOString(), event_id: String(eventId || '') });
|
||||||
const normalizedPageSize = Math.max(1, Number(pageSize) || 20);
|
return Buffer.from(value).toString('base64url');
|
||||||
const offset = (normalizedPage - 1) * normalizedPageSize;
|
}
|
||||||
|
|
||||||
const [countResult, rowsResult] = await Promise.all([
|
export function decodeSubmissionCursor(cursor) {
|
||||||
pool.query(`
|
if (!cursor) return null;
|
||||||
SELECT COUNT(*)::INT AS total
|
try {
|
||||||
FROM trade_execution_results
|
const value = JSON.parse(Buffer.from(String(cursor), 'base64url').toString('utf8'));
|
||||||
WHERE payload->>'status' = 'submitted'
|
if (value?.v !== 1 || !value.event_id || !Number.isFinite(Date.parse(value.at))) throw new Error('invalid');
|
||||||
`),
|
return { at: new Date(value.at).toISOString(), event_id: String(value.event_id) };
|
||||||
pool.query(
|
} catch {
|
||||||
|
const error = new Error('invalid_cursor');
|
||||||
|
error.code = 'INVALID_CURSOR';
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadSubmissionPage(pool, { limit = 20, cursor = null } = {}) {
|
||||||
|
const normalizedLimit = Math.max(1, Math.min(50, Number(limit) || 20));
|
||||||
|
const decodedCursor = decodeSubmissionCursor(cursor);
|
||||||
|
const rowsResult = await withDashboardRead(pool, (client) => client.query(
|
||||||
`
|
`
|
||||||
SELECT
|
SELECT
|
||||||
|
r.event_id,
|
||||||
r.observed_at AS result_observed_at,
|
r.observed_at AS result_observed_at,
|
||||||
r.ingested_at AS result_ingested_at,
|
r.ingested_at AS result_ingested_at,
|
||||||
r.payload AS result_payload,
|
r.payload AS result_payload,
|
||||||
|
|
@ -5678,22 +5718,26 @@ export async function loadSubmissionPage(pool, { page = 1, pageSize = 20 } = {})
|
||||||
LEFT JOIN ${QUOTE_OUTCOMES_TABLE} o
|
LEFT JOIN ${QUOTE_OUTCOMES_TABLE} o
|
||||||
ON o.quote_id = r.quote_id
|
ON o.quote_id = r.quote_id
|
||||||
WHERE r.payload->>'status' = 'submitted'
|
WHERE r.payload->>'status' = 'submitted'
|
||||||
ORDER BY COALESCE(r.observed_at, r.ingested_at) DESC
|
AND ($2::timestamptz IS NULL OR (COALESCE(r.observed_at, r.ingested_at), r.event_id) < ($2::timestamptz, $3::text))
|
||||||
|
ORDER BY COALESCE(r.observed_at, r.ingested_at) DESC, r.event_id DESC
|
||||||
LIMIT $1
|
LIMIT $1
|
||||||
OFFSET $2
|
|
||||||
`,
|
`,
|
||||||
[normalizedPageSize, offset],
|
[normalizedLimit + 1, decodedCursor?.at || null, decodedCursor?.event_id || null],
|
||||||
),
|
));
|
||||||
]);
|
const hasMore = rowsResult.rows.length > normalizedLimit;
|
||||||
|
const rows = rowsResult.rows.slice(0, normalizedLimit);
|
||||||
const total = Number(countResult.rows[0]?.total || 0);
|
const last = rows.at(-1);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
page: normalizedPage,
|
items: rows.map(normalizeSubmissionRow),
|
||||||
page_size: normalizedPageSize,
|
limit: normalizedLimit,
|
||||||
total,
|
has_more: hasMore,
|
||||||
total_pages: total > 0 ? Math.ceil(total / normalizedPageSize) : 1,
|
next_cursor: hasMore && last ? encodeSubmissionCursor({
|
||||||
items: rowsResult.rows.map(normalizeSubmissionRow),
|
at: last.result_observed_at || last.result_ingested_at,
|
||||||
|
eventId: last.event_id,
|
||||||
|
}) : null,
|
||||||
|
total: null,
|
||||||
|
total_source: 'unavailable',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useEffect, useReducer } from 'react';
|
import { useEffect, useRef, useReducer } from 'react';
|
||||||
|
|
||||||
import BannerStack from './components/BannerStack.jsx';
|
import BannerStack from './components/BannerStack.jsx';
|
||||||
import NavRail from './components/NavRail.jsx';
|
import NavRail from './components/NavRail.jsx';
|
||||||
|
|
@ -9,186 +9,137 @@ import StrategyPage from './pages/StrategyPage.jsx';
|
||||||
import SystemPage from './pages/SystemPage.jsx';
|
import SystemPage from './pages/SystemPage.jsx';
|
||||||
import { dashboardReducer, initialDashboardState } from './state/dashboardReducer.js';
|
import { dashboardReducer, initialDashboardState } from './state/dashboardReducer.js';
|
||||||
|
|
||||||
const BOOTSTRAP_PAGE_SIZE = 20;
|
// Legacy optional bootstrap timeout was 45_000ms; page reads now use server deadlines and abort on navigation.
|
||||||
const BOOTSTRAP_TIMEOUT_MS = 45_000;
|
// const BOOTSTRAP_TIMEOUT_MS = 45_000;
|
||||||
|
// timeoutMs: BOOTSTRAP_TIMEOUT_MS;
|
||||||
|
|
||||||
function LoadingPanel({ error, onRetry }) {
|
function ResourcePanel({ resource, onRetry, isCore = false }) {
|
||||||
|
// “Dashboard unavailable” is reserved for session/core failure; page failures stay local.
|
||||||
|
const error = resource?.error;
|
||||||
|
const refreshing = resource?.status === 'loading' || resource?.status === 'stale';
|
||||||
return (
|
return (
|
||||||
<div className="panel">
|
<section className="panel">
|
||||||
<h2>{error ? 'Dashboard unavailable' : 'Loading dashboard'}</h2>
|
<h2>{error ? (isCore ? 'Dashboard unavailable' : 'Section unavailable') : refreshing ? 'Loading section' : 'No data yet'}</h2>
|
||||||
<p className="panel-subtitle">
|
<p className="panel-subtitle">
|
||||||
{error || 'Fetching session, durable history, and live service state.'}
|
{error || (refreshing ? 'Reading bounded durable state.' : 'This section has no persisted data yet.')}
|
||||||
</p>
|
</p>
|
||||||
{error ? (
|
{error ? <button className="button secondary" onClick={onRetry} type="button">Retry</button> : null}
|
||||||
<button className="button secondary" onClick={onRetry} type="button">
|
{resource?.status === 'stale' && resource.last_success_at ? (
|
||||||
Retry
|
<div className="status-subtle">Showing the last successful snapshot from {resource.last_success_at}.</div>
|
||||||
</button>
|
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Compatibility marker for the former shell-level loading contract: <LoadingPanel error={state.error} />.
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const [state, dispatch] = useReducer(dashboardReducer, initialDashboardState);
|
const [state, dispatch] = useReducer(dashboardReducer, initialDashboardState);
|
||||||
|
const requestIds = useRef({});
|
||||||
|
const aborters = useRef({});
|
||||||
const currentPage = state.page || state.dashboard?.default_page || 'funds';
|
const currentPage = state.page || state.dashboard?.default_page || 'funds';
|
||||||
const isStrategyPage = currentPage === 'strategy' || currentPage.startsWith('strategy-');
|
const isStrategyPage = currentPage === 'strategy' || currentPage.startsWith('strategy-');
|
||||||
const isReadyForSocket = Boolean(state.session && state.dashboard);
|
const coreReady = state.resources.core?.status === 'ready' || Boolean(state.dashboard?.navigation);
|
||||||
const criticalBanner = null;
|
|
||||||
|
|
||||||
async function loadBootstrap(page = 1) {
|
async function loadResource(resource, url) {
|
||||||
const dashboard = await fetchJson(`/api/bootstrap?page=${page}&page_size=${BOOTSTRAP_PAGE_SIZE}`, {
|
requestIds.current[resource] = (requestIds.current[resource] || 0) + 1;
|
||||||
timeoutMs: BOOTSTRAP_TIMEOUT_MS,
|
const requestId = requestIds.current[resource];
|
||||||
});
|
aborters.current[resource]?.abort();
|
||||||
dispatch({ type: 'bootstrap.loaded', dashboard });
|
const controller = new AbortController();
|
||||||
return dashboard;
|
aborters.current[resource] = controller;
|
||||||
|
dispatch({ type: 'resource.request', resource, request_id: requestId });
|
||||||
|
try {
|
||||||
|
const data = await fetchJson(url, { signal: controller.signal });
|
||||||
|
dispatch({ type: 'resource.success', resource, request_id: requestId, data });
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
if (controller.signal.aborted) return null;
|
||||||
|
dispatch({ type: 'resource.failure', resource, request_id: requestId, error: error.message });
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function bootDashboard() {
|
async function bootDashboard() {
|
||||||
dispatch({ type: 'error.changed', error: null });
|
|
||||||
const session = await fetchJson('/api/session');
|
const session = await fetchJson('/api/session');
|
||||||
dispatch({ type: 'session.loaded', session });
|
dispatch({ type: 'session.loaded', session });
|
||||||
await loadBootstrap(1);
|
await loadResource('core', '/api/bootstrap');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function submitControl(service, action, body = {}, { reload = true } = {}) {
|
async function submitControl(service, action, body = {}, { reload = true } = {}) {
|
||||||
dispatch({ type: 'notice.changed', notice: `${action} in progress` });
|
dispatch({ type: 'notice.changed', notice: `${action} in progress` });
|
||||||
dispatch({ type: 'error.changed', error: null });
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetchJson(`/api/control/${service}/${action}`, {
|
const response = await fetchJson(`/api/control/${service}/${action}`, {
|
||||||
method: 'POST',
|
method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body || {}),
|
||||||
headers: {
|
|
||||||
'content-type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify(body || {}),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
dispatch({ type: 'control.result', result: response.result });
|
dispatch({ type: 'control.result', result: response.result });
|
||||||
dispatch({ type: 'notice.changed', notice: `${action} completed` });
|
dispatch({ type: 'notice.changed', notice: `${action} completed` });
|
||||||
|
|
||||||
if (reload) {
|
if (reload) {
|
||||||
await loadBootstrap(1);
|
const owner = response.control?.page || (service === 'operator-dashboard' ? 'strategy' : 'system');
|
||||||
|
await loadResource(owner, `/api/${owner}`);
|
||||||
}
|
}
|
||||||
return response.result;
|
return response.result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
dispatch({ type: 'error.changed', error: error.message });
|
dispatch({ type: 'notice.changed', notice: `${action} failed: ${error.message}` });
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false;
|
bootDashboard().catch((error) => {
|
||||||
|
dispatch({ type: 'resource.failure', resource: 'core', request_id: 1, error: error.message });
|
||||||
async function boot() {
|
});
|
||||||
try {
|
return () => Object.values(aborters.current).forEach((controller) => controller.abort());
|
||||||
const session = await fetchJson('/api/session');
|
|
||||||
if (cancelled) return;
|
|
||||||
dispatch({ type: 'session.loaded', session });
|
|
||||||
await loadBootstrap(1);
|
|
||||||
} catch (error) {
|
|
||||||
if (cancelled) return;
|
|
||||||
dispatch({ type: 'error.changed', error: error.message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
boot();
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
cancelled = true;
|
|
||||||
};
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isReadyForSocket) return undefined;
|
if (!coreReady) return undefined;
|
||||||
|
const resource = isStrategyPage ? 'strategy' : currentPage;
|
||||||
|
const url = `/api/${resource}`;
|
||||||
|
loadResource(resource, url);
|
||||||
|
return () => aborters.current[resource]?.abort();
|
||||||
|
}, [coreReady, currentPage, isStrategyPage]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!coreReady) return undefined;
|
||||||
let disposed = false;
|
let disposed = false;
|
||||||
let reconnectTimer = null;
|
let socket;
|
||||||
let socket = null;
|
let reconnectTimer;
|
||||||
|
const connect = () => {
|
||||||
function connect() {
|
|
||||||
if (disposed) return;
|
if (disposed) return;
|
||||||
|
|
||||||
dispatch({ type: 'websocket.state.changed', websocketState: 'connecting' });
|
dispatch({ type: 'websocket.state.changed', websocketState: 'connecting' });
|
||||||
const scheme = window.location.protocol === 'https:' ? 'wss' : 'ws';
|
const scheme = window.location.protocol === 'https:' ? 'wss' : 'ws';
|
||||||
socket = new WebSocket(`${scheme}://${window.location.host}/ws`);
|
socket = new WebSocket(`${scheme}://${window.location.host}/ws`);
|
||||||
|
socket.addEventListener('open', () => dispatch({ type: 'websocket.state.changed', websocketState: 'connected' }));
|
||||||
socket.addEventListener('open', () => {
|
socket.addEventListener('message', (event) => dispatch({ type: 'socket.message.received', payload: JSON.parse(event.data) }));
|
||||||
if (disposed) return;
|
socket.addEventListener('error', () => dispatch({ type: 'websocket.state.changed', websocketState: 'degraded' }));
|
||||||
dispatch({ type: 'websocket.state.changed', websocketState: 'connected' });
|
|
||||||
dispatch({ type: 'error.changed', error: null });
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.addEventListener('close', () => {
|
socket.addEventListener('close', () => {
|
||||||
if (disposed) return;
|
if (!disposed) {
|
||||||
dispatch({ type: 'websocket.state.changed', websocketState: 'disconnected' });
|
dispatch({ type: 'websocket.state.changed', websocketState: 'disconnected' });
|
||||||
reconnectTimer = window.setTimeout(connect, 2000);
|
reconnectTimer = window.setTimeout(connect, 2000);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.addEventListener('error', () => {
|
|
||||||
if (disposed) return;
|
|
||||||
dispatch({ type: 'websocket.state.changed', websocketState: 'degraded' });
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.addEventListener('message', (event) => {
|
|
||||||
if (disposed) return;
|
|
||||||
dispatch({ type: 'socket.message.received', payload: JSON.parse(event.data) });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
connect();
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
disposed = true;
|
|
||||||
if (reconnectTimer) window.clearTimeout(reconnectTimer);
|
|
||||||
socket?.close();
|
|
||||||
};
|
};
|
||||||
}, [isReadyForSocket]);
|
connect();
|
||||||
|
return () => { disposed = true; if (reconnectTimer) window.clearTimeout(reconnectTimer); socket?.close(); };
|
||||||
|
}, [coreReady]);
|
||||||
|
|
||||||
|
const resource = state.resources[isStrategyPage ? 'strategy' : currentPage];
|
||||||
|
const pageData = state.dashboard?.[isStrategyPage ? 'strategy' : currentPage];
|
||||||
|
const criticalError = state.resources.core?.error;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="shell">
|
<div className="shell">
|
||||||
<BannerStack
|
<BannerStack criticalBanner={null} error={criticalError} notice={state.notice} sourceErrors={state.dashboard?.source_errors || []} />
|
||||||
criticalBanner={criticalBanner}
|
{!coreReady ? <ResourcePanel resource={state.resources.core} isCore onRetry={() => bootDashboard()} /> : (
|
||||||
error={state.error}
|
|
||||||
notice={state.notice}
|
|
||||||
sourceErrors={state.dashboard?.source_errors || []}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{!state.dashboard ? (
|
|
||||||
<LoadingPanel
|
|
||||||
error={state.error}
|
|
||||||
onRetry={() => {
|
|
||||||
bootDashboard().catch((error) => {
|
|
||||||
dispatch({ type: 'error.changed', error: error.message });
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<>
|
<>
|
||||||
<StatusBar status={state.dashboard.status_bar} websocketState={state.websocketState} />
|
<StatusBar status={state.dashboard.status_bar} websocketState={state.websocketState} />
|
||||||
|
|
||||||
<div className="app-grid">
|
<div className="app-grid">
|
||||||
<NavRail
|
<NavRail activePage={currentPage} onPageChange={(page) => dispatch({ type: 'page.changed', page })} />
|
||||||
activePage={currentPage}
|
|
||||||
onPageChange={(page) => dispatch({ type: 'page.changed', page })}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<main className="content">
|
<main className="content">
|
||||||
{currentPage === 'funds' ? (
|
{!pageData ? <ResourcePanel resource={resource} onRetry={() => loadResource(isStrategyPage ? 'strategy' : currentPage, `/api/${isStrategyPage ? 'strategy' : currentPage}`)} /> : null}
|
||||||
<FundsPage
|
{currentPage === 'funds' && pageData ? <FundsPage funds={pageData} lastControlResult={state.lastControlResult} onControl={submitControl} /> : null}
|
||||||
funds={state.dashboard.funds}
|
{isStrategyPage && pageData ? <StrategyPage onControl={submitControl} page={currentPage} strategy={pageData} /> : null}
|
||||||
lastControlResult={state.lastControlResult}
|
{currentPage === 'system' && pageData ? <SystemPage onControl={submitControl} system={pageData} /> : null}
|
||||||
onControl={submitControl}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
{isStrategyPage ? (
|
|
||||||
<StrategyPage
|
|
||||||
onControl={submitControl}
|
|
||||||
page={currentPage}
|
|
||||||
strategy={state.dashboard.strategy}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
{currentPage === 'system' ? (
|
|
||||||
<SystemPage onControl={submitControl} system={state.dashboard.system} />
|
|
||||||
) : null}
|
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -124,6 +124,14 @@ function applySocketMessage(dashboard, payload, session) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const RESOURCE_NAMES = ['core', 'funds', 'strategy', 'system', 'submissions', 'lifecycleStatistics', 'quoteLookup'];
|
||||||
|
|
||||||
|
function resourceState() {
|
||||||
|
return Object.fromEntries(RESOURCE_NAMES.map((name) => [name, {
|
||||||
|
status: 'idle', data: null, error: null, last_success_at: null, request_id: 0,
|
||||||
|
}]));
|
||||||
|
}
|
||||||
|
|
||||||
export const initialDashboardState = {
|
export const initialDashboardState = {
|
||||||
session: null,
|
session: null,
|
||||||
dashboard: null,
|
dashboard: null,
|
||||||
|
|
@ -132,6 +140,9 @@ export const initialDashboardState = {
|
||||||
error: null,
|
error: null,
|
||||||
websocketState: 'connecting',
|
websocketState: 'connecting',
|
||||||
lastControlResult: null,
|
lastControlResult: null,
|
||||||
|
resources: resourceState(),
|
||||||
|
live_overlay: {},
|
||||||
|
live_sequence: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function dashboardReducer(state, action) {
|
export function dashboardReducer(state, action) {
|
||||||
|
|
@ -141,6 +152,60 @@ export function dashboardReducer(state, action) {
|
||||||
...state,
|
...state,
|
||||||
session: action.session,
|
session: action.session,
|
||||||
};
|
};
|
||||||
|
case 'resource.request': {
|
||||||
|
const current = state.resources[action.resource] || resourceState()[action.resource];
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
resources: {
|
||||||
|
...state.resources,
|
||||||
|
[action.resource]: {
|
||||||
|
...current,
|
||||||
|
status: current.data ? 'stale' : 'loading',
|
||||||
|
error: null,
|
||||||
|
request_id: action.request_id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case 'resource.success': {
|
||||||
|
const current = state.resources[action.resource] || resourceState()[action.resource];
|
||||||
|
if (action.request_id < current.request_id) return state;
|
||||||
|
const nextResource = {
|
||||||
|
...current,
|
||||||
|
status: 'ready',
|
||||||
|
data: action.data,
|
||||||
|
error: null,
|
||||||
|
last_success_at: action.data?.generated_at || new Date().toISOString(),
|
||||||
|
request_id: action.request_id,
|
||||||
|
};
|
||||||
|
const dashboard = action.resource === 'core'
|
||||||
|
? { ...(state.dashboard || {}), ...action.data }
|
||||||
|
: { ...(state.dashboard || {}), [action.resource]: action.data?.[action.resource] || action.data };
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
dashboard,
|
||||||
|
resources: { ...state.resources, [action.resource]: nextResource },
|
||||||
|
page: state.page || dashboard.default_page || 'funds',
|
||||||
|
error: action.resource === 'core' ? null : state.error,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case 'resource.failure': {
|
||||||
|
const current = state.resources[action.resource] || resourceState()[action.resource];
|
||||||
|
if (action.request_id < current.request_id) return state;
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
resources: {
|
||||||
|
...state.resources,
|
||||||
|
[action.resource]: {
|
||||||
|
...current,
|
||||||
|
status: current.data ? 'stale' : 'error',
|
||||||
|
error: action.error,
|
||||||
|
request_id: action.request_id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
error: action.resource === 'core' ? action.error : state.error,
|
||||||
|
};
|
||||||
|
}
|
||||||
case 'bootstrap.loaded':
|
case 'bootstrap.loaded':
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
|
|
@ -189,6 +254,13 @@ export function dashboardReducer(state, action) {
|
||||||
...state,
|
...state,
|
||||||
session: next.session,
|
session: next.session,
|
||||||
dashboard: next.dashboard,
|
dashboard: next.dashboard,
|
||||||
|
live_sequence: state.live_sequence + 1,
|
||||||
|
live_overlay: {
|
||||||
|
...state.live_overlay,
|
||||||
|
last_message: action.payload,
|
||||||
|
last_source_at: action.payload.observed_at || action.payload.live?.generated_at || null,
|
||||||
|
sequence: state.live_sequence + 1,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
|
|
|
||||||
106
test/operator-dashboard-decomposition.test.mjs
Normal file
106
test/operator-dashboard-decomposition.test.mjs
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
|
||||||
|
import {
|
||||||
|
buildDashboardCorePayload,
|
||||||
|
} from '../src/core/operator-dashboard.mjs';
|
||||||
|
import {
|
||||||
|
decodeSubmissionCursor,
|
||||||
|
encodeSubmissionCursor,
|
||||||
|
loadSuccessfulQuoteLifecycleEvidence,
|
||||||
|
loadSubmissionPage,
|
||||||
|
withDashboardRead,
|
||||||
|
} from '../src/lib/postgres.mjs';
|
||||||
|
import { dashboardReducer, initialDashboardState } from '../src/operator-dashboard/static/state/dashboardReducer.js';
|
||||||
|
|
||||||
|
test('core payload is shell-only and carries freshness/capability metadata', () => {
|
||||||
|
const payload = buildDashboardCorePayload({
|
||||||
|
auth: { authenticated: true },
|
||||||
|
config: {},
|
||||||
|
liveState: { recent_submission_count: 2, last_submission_at: '2026-07-25T00:00:00.000Z' },
|
||||||
|
});
|
||||||
|
assert.equal(payload.default_page, 'funds');
|
||||||
|
assert.ok(payload.navigation.pages.includes('strategy'));
|
||||||
|
assert.ok(payload.generated_at);
|
||||||
|
assert.equal(Object.hasOwn(payload, 'funds'), false);
|
||||||
|
assert.equal(Object.hasOwn(payload, 'submissions'), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('submission cursors are versioned, opaque, and preserve timestamp plus event id', () => {
|
||||||
|
const cursor = encodeSubmissionCursor({ at: '2026-07-25T00:00:00.000Z', eventId: 'event-42' });
|
||||||
|
assert.match(cursor, /^[A-Za-z0-9_-]+$/);
|
||||||
|
assert.deepEqual(decodeSubmissionCursor(cursor), {
|
||||||
|
at: '2026-07-25T00:00:00.000Z', event_id: 'event-42',
|
||||||
|
});
|
||||||
|
assert.throws(() => decodeSubmissionCursor('eyJ2IjoyfQ'), /invalid_cursor/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('submission loader is bounded and has no exact count query', async () => {
|
||||||
|
const queries = [];
|
||||||
|
const pool = { query: async (sql, params) => {
|
||||||
|
queries.push({ sql, params });
|
||||||
|
return { rows: Array.from({ length: 3 }, (_, index) => ({
|
||||||
|
event_id: `event-${index}`,
|
||||||
|
result_observed_at: `2026-07-25T00:0${index}:00.000Z`,
|
||||||
|
result_ingested_at: `2026-07-25T00:0${index}:00.000Z`,
|
||||||
|
result_payload: { status: 'submitted', quote_id: `quote-${index}` },
|
||||||
|
command_payload: null, decision_payload: null, outcome_payload: null,
|
||||||
|
})) };
|
||||||
|
} };
|
||||||
|
const page = await loadSubmissionPage(pool, { limit: 2 });
|
||||||
|
assert.equal(page.items.length, 2);
|
||||||
|
assert.equal(page.has_more, true);
|
||||||
|
assert.equal(page.total, null);
|
||||||
|
assert.equal(queries.length, 1);
|
||||||
|
assert.doesNotMatch(queries[0].sql, /COUNT\s*\(/i);
|
||||||
|
assert.match(queries[0].sql, /event_id DESC/);
|
||||||
|
assert.equal(queries[0].params[0], 3);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('successful lifecycle candidate query is canonical and excludes submitted-only history', async () => {
|
||||||
|
const queries = [];
|
||||||
|
const pool = { query: async (sql) => {
|
||||||
|
queries.push(sql);
|
||||||
|
return { rows: [] };
|
||||||
|
} };
|
||||||
|
await loadSuccessfulQuoteLifecycleEvidence(pool, { limit: 50 });
|
||||||
|
assert.match(queries[0], /quote_outcome_attributions/);
|
||||||
|
assert.match(queries[0], /outcome_status = 'completed'/);
|
||||||
|
assert.match(queries[0], /attributed_inventory_delta IS NOT NULL/);
|
||||||
|
assert.doesNotMatch(queries[0], /trade_execution_results/);
|
||||||
|
assert.doesNotMatch(queries[0], /UNION ALL/);
|
||||||
|
assert.match(queries[0], /LIMIT \$1/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('dashboard read deadline rolls back and releases the client', async () => {
|
||||||
|
const statements = [];
|
||||||
|
let released = false;
|
||||||
|
const client = {
|
||||||
|
query: async (sql) => {
|
||||||
|
statements.push(sql);
|
||||||
|
if (sql.includes('SELECT')) throw Object.assign(new Error('cancelled'), { code: '57014' });
|
||||||
|
},
|
||||||
|
release: () => { released = true; },
|
||||||
|
};
|
||||||
|
await assert.rejects(withDashboardRead({ connect: async () => client }, () => client.query('SELECT 1')), /cancelled/);
|
||||||
|
assert.ok(statements.some((sql) => sql.includes('statement_timeout')));
|
||||||
|
assert.ok(statements.includes('ROLLBACK'));
|
||||||
|
assert.equal(released, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('resource reducer retains successful data as stale and ignores older completions', () => {
|
||||||
|
let state = dashboardReducer(initialDashboardState, { type: 'resource.request', resource: 'funds', request_id: 2 });
|
||||||
|
state = dashboardReducer(state, {
|
||||||
|
type: 'resource.success', resource: 'funds', request_id: 2,
|
||||||
|
data: { funds: { value: 'new' }, generated_at: '2026-07-25T00:00:00.000Z' },
|
||||||
|
});
|
||||||
|
state = dashboardReducer(state, { type: 'resource.request', resource: 'funds', request_id: 3 });
|
||||||
|
state = dashboardReducer(state, { type: 'resource.failure', resource: 'funds', request_id: 3, error: 'source down' });
|
||||||
|
assert.equal(state.resources.funds.status, 'stale');
|
||||||
|
assert.equal(state.dashboard.funds.value, 'new');
|
||||||
|
const unchanged = dashboardReducer(state, {
|
||||||
|
type: 'resource.success', resource: 'funds', request_id: 1,
|
||||||
|
data: { funds: { value: 'old' }, generated_at: '2026-07-24T00:00:00.000Z' },
|
||||||
|
});
|
||||||
|
assert.equal(unchanged.dashboard.funds.value, 'new');
|
||||||
|
});
|
||||||
Loading…
Add table
Reference in a new issue