diff --git a/src/core/operator-dashboard.mjs b/src/core/operator-dashboard.mjs index f292cca..c36149c 100644 --- a/src/core/operator-dashboard.mjs +++ b/src/core/operator-dashboard.mjs @@ -755,11 +755,14 @@ function buildBalanceSummary({ inventorySnapshot, marketPrice, config }) { const spendable = inventory.spendable || {}; const pendingInbound = inventory.pending_inbound || {}; const pendingOutbound = inventory.pending_outbound || {}; + const balanceAssets = config.trackedAssets?.length + ? config.trackedAssets + : [...config.assetRegistry.values()]; return { synced_at: inventory.synced_at || inventorySnapshot?.ingested_at || null, reconciliation_status: inventory.reconciliation_status || null, - items: [...config.assetRegistry.values()].map((asset) => { + items: balanceAssets.map((asset) => { const spendableUnits = String(spendable[asset.assetId] || '0'); const pendingInboundUnits = String(pendingInbound[asset.assetId] || '0'); const pendingOutboundUnits = String(pendingOutbound[asset.assetId] || '0'); @@ -1540,8 +1543,8 @@ function buildTradeFunnelSummary(lifecycleRows = []) { unresolved_submission_count: unresolvedSubmissions.length, no_trade_count: noTradeRows.length, successful_trades: successfulTrades, - unresolved_submissions: unresolvedSubmissions, - no_trade_rows: noTradeRows, + unresolved_submissions: unresolvedSubmissions.slice(0, 20), + no_trade_rows: [], counts, caveat: successfulTrades.length > 0 diff --git a/test/operator-dashboard.test.mjs b/test/operator-dashboard.test.mjs index 0ab6df5..4177b27 100644 --- a/test/operator-dashboard.test.mjs +++ b/test/operator-dashboard.test.mjs @@ -847,6 +847,92 @@ test('bootstrap balances and funding handles distinguish nBTC reserve from legac assert.equal(bootstrap.funds.funding.handles[0].label, 'btc:mainnet funding handle'); }); +test('bootstrap balances exclude imported catalog assets that are not inventory-enabled', () => { + const config = buildDualBtcConfig(); + const importedOnlyAsset = { + assetId: 'nep141:sol-imported-token.near', + symbol: 'CATALOG', + label: 'Catalog-only token', + decimals: 6, + chain: 'sol', + supported: true, + enabledForInventory: false, + }; + config.assetRegistry = new Map([ + ...config.assetRegistry, + [importedOnlyAsset.assetId, importedOnlyAsset], + ]); + + const bootstrap = buildDashboardBootstrap({ + config, + inventorySnapshot: { + ingested_at: '2026-05-12T19:45:00.000Z', + payload: { + synced_at: '2026-05-12T19:45:00.000Z', + reconciliation_status: 'ok', + spendable: { + [config.tradingBtc.assetId]: '100000', + [importedOnlyAsset.assetId]: '999999', + }, + pending_inbound: {}, + pending_outbound: {}, + }, + }, + marketPrice: null, + recentQuotes: [], + submissionPage: { page: 1, page_size: 20, total: 0, total_pages: 1, items: [] }, + submissionSummary: { total: 0 }, + fundingObservations: [], + recentTradeDecisions: [], + recentExecuteTradeCommands: [], + recentExecutionResults: [], + recentAlertTransitions: [], + serviceSnapshots: [], + }); + + assert.deepEqual(bootstrap.funds.balances.items.map((item) => item.asset_id), [ + 'nep141:nbtc.bridge.near', + 'nep141:btc.omft.near', + 'nep141:eure.omft.near', + ]); + assert.equal( + bootstrap.strategy.asset_catalog.items.some((item) => item.asset_id === importedOnlyAsset.assetId), + true, + ); +}); + +test('bootstrap keeps no-trade counts without shipping non-rendered row payloads', () => { + const config = buildConfig(); + const bootstrap = buildDashboardBootstrap({ + config, + inventorySnapshot: null, + marketPrice: null, + recentQuotes: [], + submissionPage: { page: 1, page_size: 20, total: 0, total_pages: 1, items: [] }, + submissionSummary: { total: 0 }, + fundingObservations: [], + recentTradeDecisions: [{ + observed_at: '2026-05-12T19:40:00.000Z', + payload: { + decision_id: 'decision-rejected', + quote_id: 'quote-rejected', + pair: config.activePair, + decision: 'rejected', + decision_reason: 'below_threshold', + raw_solver_payload: Object.fromEntries(Array.from({ length: 100 }, (_, index) => [`field_${index}`, index])), + }, + }], + recentExecuteTradeCommands: [], + recentExecutionResults: [], + recentAlertTransitions: [], + serviceSnapshots: [], + }); + + const funnel = bootstrap.strategy.strategy_state.trade_funnel; + assert.equal(funnel.no_trade_count, 1); + assert.deepEqual(funnel.no_trade_rows, []); +}); + test('bootstrap normalizes actionable decision vocabulary before exposing it to the dashboard', () => { const config = buildConfig(); const bootstrap = buildDashboardBootstrap({