Proof: npm test passed 173/173; npm run operator-dashboard:build passed; regression tests cover omitting raw asset payloads from bootstrap rows and using an explicit 45s bootstrap timeout. Assumptions: The timeout observed by the operator is the browser-side 15000ms fetch limit on /api/bootstrap; trimming asset rows and extending only the bootstrap timeout keeps the dashboard available while preserving visible asset and pair config. Still fake: raw imported token payloads remain stored in Postgres but are not exposed through a per-asset dashboard detail view in this fix.
This commit is contained in:
parent
edfa14f37e
commit
da2202c455
4 changed files with 124 additions and 5 deletions
|
|
@ -635,8 +635,14 @@ export function createTradingConfigStore({
|
|||
|
||||
export async function loadAssetCatalogSummary(pool, { limit = 250 } = {}) {
|
||||
await ensureTradingConfigSchema(pool);
|
||||
const [snapshot, countResult] = await Promise.all([
|
||||
loadTradingConfig(pool),
|
||||
const boundedLimit = Math.max(1, Number(limit) || 50);
|
||||
const [latestImportResult, countResult, assetResult] = await Promise.all([
|
||||
pool.query(`
|
||||
SELECT *
|
||||
FROM ${SUPPORTED_ASSET_IMPORT_RUNS_TABLE}
|
||||
ORDER BY fetched_at DESC
|
||||
LIMIT 1
|
||||
`),
|
||||
pool.query(`
|
||||
SELECT
|
||||
COUNT(*)::INT AS known_count,
|
||||
|
|
@ -645,17 +651,40 @@ export async function loadAssetCatalogSummary(pool, { limit = 250 } = {}) {
|
|||
COUNT(*) FILTER (WHERE enabled_for_inventory)::INT AS inventory_enabled_count
|
||||
FROM ${TRADING_ASSETS_TABLE}
|
||||
`),
|
||||
pool.query(`
|
||||
SELECT
|
||||
asset_id,
|
||||
venue,
|
||||
symbol,
|
||||
label,
|
||||
decimals,
|
||||
blockchain,
|
||||
chain,
|
||||
contract_address,
|
||||
latest_price,
|
||||
price_updated_at,
|
||||
supported,
|
||||
retired_at,
|
||||
enabled_for_inventory,
|
||||
role,
|
||||
withdraw_address,
|
||||
raw_payload <> '{}'::jsonb AS raw_payload_available,
|
||||
updated_at
|
||||
FROM ${TRADING_ASSETS_TABLE}
|
||||
ORDER BY symbol ASC, asset_id ASC
|
||||
LIMIT $1
|
||||
`, [boundedLimit]),
|
||||
]);
|
||||
const counts = countResult.rows[0] || {};
|
||||
return {
|
||||
latest_import: snapshot.latestImportRun,
|
||||
latest_import: normalizeAssetImportRunRow(latestImportResult.rows[0] || null),
|
||||
counts: {
|
||||
known: Number(counts.known_count || 0),
|
||||
supported: Number(counts.supported_count || 0),
|
||||
retired: Number(counts.retired_count || 0),
|
||||
inventory_enabled: Number(counts.inventory_enabled_count || 0),
|
||||
},
|
||||
items: snapshot.assets.slice(0, Math.max(1, Number(limit) || 50)),
|
||||
items: assetResult.rows.map(normalizeAssetCatalogSummaryRow),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -1147,6 +1176,36 @@ function normalizeTradingAssetRow(row) {
|
|||
};
|
||||
}
|
||||
|
||||
function normalizeAssetCatalogSummaryRow(row) {
|
||||
return {
|
||||
assetId: row.asset_id,
|
||||
asset_id: row.asset_id,
|
||||
venue: row.venue,
|
||||
symbol: row.symbol,
|
||||
label: row.label || row.symbol,
|
||||
decimals: Number(row.decimals),
|
||||
blockchain: row.blockchain || null,
|
||||
chain: row.chain || row.blockchain || null,
|
||||
contractAddress: row.contract_address || null,
|
||||
contract_address: row.contract_address || null,
|
||||
latestPrice: row.latest_price == null ? null : String(row.latest_price),
|
||||
latest_price: row.latest_price == null ? null : String(row.latest_price),
|
||||
priceUpdatedAt: toIsoTimestamp(row.price_updated_at),
|
||||
price_updated_at: toIsoTimestamp(row.price_updated_at),
|
||||
supported: row.supported === true,
|
||||
retiredAt: toIsoTimestamp(row.retired_at),
|
||||
retired_at: toIsoTimestamp(row.retired_at),
|
||||
enabledForInventory: row.enabled_for_inventory === true,
|
||||
enabled_for_inventory: row.enabled_for_inventory === true,
|
||||
role: row.role || null,
|
||||
withdrawAddress: row.withdraw_address || '',
|
||||
withdraw_address: row.withdraw_address || '',
|
||||
rawPayloadAvailable: row.raw_payload_available === true,
|
||||
raw_payload_available: row.raw_payload_available === true,
|
||||
updated_at: toIsoTimestamp(row.updated_at),
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeTradingPairRow(row) {
|
||||
return {
|
||||
pairId: row.pair_id,
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import SystemPage from './pages/SystemPage.jsx';
|
|||
import { dashboardReducer, initialDashboardState } from './state/dashboardReducer.js';
|
||||
|
||||
const BOOTSTRAP_PAGE_SIZE = 20;
|
||||
const BOOTSTRAP_TIMEOUT_MS = 45_000;
|
||||
|
||||
function LoadingPanel({ error, onRetry }) {
|
||||
return (
|
||||
|
|
@ -34,7 +35,9 @@ export default function App() {
|
|||
const criticalBanner = null;
|
||||
|
||||
async function loadBootstrap(page = 1) {
|
||||
const dashboard = await fetchJson(`/api/bootstrap?page=${page}&page_size=${BOOTSTRAP_PAGE_SIZE}`);
|
||||
const dashboard = await fetchJson(`/api/bootstrap?page=${page}&page_size=${BOOTSTRAP_PAGE_SIZE}`, {
|
||||
timeoutMs: BOOTSTRAP_TIMEOUT_MS,
|
||||
});
|
||||
dispatch({ type: 'bootstrap.loaded', dashboard });
|
||||
return dashboard;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -86,3 +86,8 @@ test('dashboard loading state exposes API failures and retry action', () => {
|
|||
assert.match(appSource, /Retry/);
|
||||
assert.match(appSource, /bootDashboard\(\)\.catch/);
|
||||
});
|
||||
|
||||
test('dashboard bootstrap uses an explicit long timeout for slow live state aggregation', () => {
|
||||
assert.match(appSource, /const BOOTSTRAP_TIMEOUT_MS = 45_000/);
|
||||
assert.match(appSource, /timeoutMs: BOOTSTRAP_TIMEOUT_MS/);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import {
|
|||
createPairStrategyConfigVersion,
|
||||
enableObserveOnlyPair,
|
||||
importSupportedAssets,
|
||||
loadAssetCatalogSummary,
|
||||
loadTradingConfig,
|
||||
pauseTradingPair,
|
||||
seedTradingConfig,
|
||||
|
|
@ -72,6 +73,26 @@ test('supported token import is idempotent, does not enable inventory, and retir
|
|||
assert.equal(pool.assets.get(CURRENT_EURE_ASSET_ID).retired_at, '2026-05-12T16:32:00.000Z');
|
||||
});
|
||||
|
||||
test('asset catalog summary omits raw token payloads from bootstrap rows', async () => {
|
||||
const pool = createMemoryPool();
|
||||
await importSupportedAssets(pool, {
|
||||
fetchedAt: '2026-05-12T16:30:00.000Z',
|
||||
response: [{
|
||||
...token(CURRENT_NBTC_ASSET_ID, 'BTC', 8),
|
||||
routes: Array.from({ length: 100 }, (_, index) => ({ route: index, detail: 'not for bootstrap' })),
|
||||
}],
|
||||
});
|
||||
|
||||
const summary = await loadAssetCatalogSummary(pool, { limit: 250 });
|
||||
const [asset] = summary.items;
|
||||
|
||||
assert.equal(summary.counts.known, 1);
|
||||
assert.equal(asset.asset_id, CURRENT_NBTC_ASSET_ID);
|
||||
assert.equal(asset.raw_payload_available, true);
|
||||
assert.equal(Object.hasOwn(asset, 'raw_payload'), false);
|
||||
assert.equal(Object.hasOwn(asset, 'rawPayload'), false);
|
||||
});
|
||||
|
||||
test('seeded DB config preserves current nBTC/EURe pair, 49 bps edge, and legacy BTC tracking', async () => {
|
||||
const pool = createMemoryPool();
|
||||
await seedTradingConfig(pool, { now: '2026-05-12T16:35:00.000Z' });
|
||||
|
|
@ -327,6 +348,9 @@ function createMemoryPool() {
|
|||
async query(sql, params = []) {
|
||||
if (/CREATE TABLE|CREATE (UNIQUE )?INDEX/i.test(sql)) return { rows: [], rowCount: 0 };
|
||||
if (/SELECT \* FROM trading_assets\s*$/i.test(sql)) return rows(this.assets);
|
||||
if (/SELECT\s+asset_id,[\s\S]+raw_payload_available[\s\S]+FROM trading_assets/i.test(sql)) {
|
||||
return selectAssetCatalogRows(this, params);
|
||||
}
|
||||
if (/SELECT \*\s+FROM trading_assets\s+ORDER BY/i.test(sql)) return rows(this.assets);
|
||||
if (/INSERT INTO trading_assets/i.test(sql)) return insertAsset(this, params);
|
||||
if (/UPDATE trading_assets/i.test(sql)) return retireAssets(this, params);
|
||||
|
|
@ -389,6 +413,34 @@ function rows(map) {
|
|||
return { rows: [...map.values()], rowCount: map.size };
|
||||
}
|
||||
|
||||
function selectAssetCatalogRows(pool, params) {
|
||||
const limit = Math.max(1, Number(params[0]) || 50);
|
||||
const selected = [...pool.assets.values()]
|
||||
.sort((left, right) => String(left.symbol).localeCompare(String(right.symbol))
|
||||
|| String(left.asset_id).localeCompare(String(right.asset_id)))
|
||||
.slice(0, limit)
|
||||
.map((row) => ({
|
||||
asset_id: row.asset_id,
|
||||
venue: row.venue,
|
||||
symbol: row.symbol,
|
||||
label: row.label,
|
||||
decimals: row.decimals,
|
||||
blockchain: row.blockchain,
|
||||
chain: row.chain,
|
||||
contract_address: row.contract_address,
|
||||
latest_price: row.latest_price,
|
||||
price_updated_at: row.price_updated_at,
|
||||
supported: row.supported,
|
||||
retired_at: row.retired_at,
|
||||
enabled_for_inventory: row.enabled_for_inventory,
|
||||
role: row.role,
|
||||
withdraw_address: row.withdraw_address,
|
||||
raw_payload_available: Object.keys(row.raw_payload || {}).length > 0,
|
||||
updated_at: row.updated_at,
|
||||
}));
|
||||
return { rows: selected, rowCount: selected.length };
|
||||
}
|
||||
|
||||
function insertAsset(pool, params) {
|
||||
const seed = params.length === 16;
|
||||
const [
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue