diff --git a/src/apps/operator-dashboard.mjs b/src/apps/operator-dashboard.mjs index 7bf5031..051ebe2 100644 --- a/src/apps/operator-dashboard.mjs +++ b/src/apps/operator-dashboard.mjs @@ -735,7 +735,7 @@ async function loadDashboardPage({ page, auth }) { 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, []); + const serviceSnapshots = await load('service_snapshots', loadServiceSnapshotsBounded, initialServiceSnapshots); if (page === 'funds') { const [portfolioMetric, inventorySnapshot, marketPrice, fundingObservations, @@ -847,6 +847,30 @@ async function loadServiceSnapshots() { return Promise.all(services.map((service) => loadServiceSnapshot(service))); } +async function loadServiceSnapshotsBounded() { + const timeoutMs = Math.min(2_500, Number(config.operatorDashboardUpstreamTimeoutMs) || 3_000); + let timeout; + try { + return await Promise.race([ + loadServiceSnapshots(), + new Promise((_, reject) => { + timeout = setTimeout(() => { + const error = new Error(`service probes exceeded ${timeoutMs}ms`); + error.name = 'TimeoutError'; + reject(error); + }, timeoutMs); + }), + ]); + } catch (error) { + logger.warn('dashboard_service_probe_budget_exceeded', { + details: { timeout_ms: timeoutMs, error: serializeError(error) }, + }); + return initialServiceSnapshots; + } finally { + if (timeout) clearTimeout(timeout); + } +} + async function loadServiceSnapshot(service) { const [stateResult, healthResult] = await Promise.allSettled([ fetchUpstreamJson(`${service.base_url}/state`), diff --git a/src/core/operator-dashboard.mjs b/src/core/operator-dashboard.mjs index 8f4ed41..e1d1ace 100644 --- a/src/core/operator-dashboard.mjs +++ b/src/core/operator-dashboard.mjs @@ -1981,7 +1981,9 @@ function buildTradeFunnelSummary(lifecycleRows = []) { successful_trade_gross_edge_estimate_count: grossEdgeEstimate.count, unresolved_submission_count: unresolvedSubmissions.length, no_trade_count: noTradeRows.length, - successful_trades: successfulTrades, + // Keep the dashboard response bounded while retaining the full count and + // aggregate estimate from the bounded lifecycle input. + successful_trades: successfulTrades.slice(0, 50), unresolved_submissions: unresolvedSubmissions.slice(0, 20), no_trade_rows: [], counts,