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 } = {}) {
|
export async function loadAssetCatalogSummary(pool, { limit = 250 } = {}) {
|
||||||
await ensureTradingConfigSchema(pool);
|
await ensureTradingConfigSchema(pool);
|
||||||
const [snapshot, countResult] = await Promise.all([
|
const boundedLimit = Math.max(1, Number(limit) || 50);
|
||||||
loadTradingConfig(pool),
|
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(`
|
pool.query(`
|
||||||
SELECT
|
SELECT
|
||||||
COUNT(*)::INT AS known_count,
|
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
|
COUNT(*) FILTER (WHERE enabled_for_inventory)::INT AS inventory_enabled_count
|
||||||
FROM ${TRADING_ASSETS_TABLE}
|
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] || {};
|
const counts = countResult.rows[0] || {};
|
||||||
return {
|
return {
|
||||||
latest_import: snapshot.latestImportRun,
|
latest_import: normalizeAssetImportRunRow(latestImportResult.rows[0] || null),
|
||||||
counts: {
|
counts: {
|
||||||
known: Number(counts.known_count || 0),
|
known: Number(counts.known_count || 0),
|
||||||
supported: Number(counts.supported_count || 0),
|
supported: Number(counts.supported_count || 0),
|
||||||
retired: Number(counts.retired_count || 0),
|
retired: Number(counts.retired_count || 0),
|
||||||
inventory_enabled: Number(counts.inventory_enabled_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) {
|
function normalizeTradingPairRow(row) {
|
||||||
return {
|
return {
|
||||||
pairId: row.pair_id,
|
pairId: row.pair_id,
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ 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;
|
const BOOTSTRAP_PAGE_SIZE = 20;
|
||||||
|
const BOOTSTRAP_TIMEOUT_MS = 45_000;
|
||||||
|
|
||||||
function LoadingPanel({ error, onRetry }) {
|
function LoadingPanel({ error, onRetry }) {
|
||||||
return (
|
return (
|
||||||
|
|
@ -34,7 +35,9 @@ export default function App() {
|
||||||
const criticalBanner = null;
|
const criticalBanner = null;
|
||||||
|
|
||||||
async function loadBootstrap(page = 1) {
|
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 });
|
dispatch({ type: 'bootstrap.loaded', dashboard });
|
||||||
return dashboard;
|
return dashboard;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -86,3 +86,8 @@ test('dashboard loading state exposes API failures and retry action', () => {
|
||||||
assert.match(appSource, /Retry/);
|
assert.match(appSource, /Retry/);
|
||||||
assert.match(appSource, /bootDashboard\(\)\.catch/);
|
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,
|
createPairStrategyConfigVersion,
|
||||||
enableObserveOnlyPair,
|
enableObserveOnlyPair,
|
||||||
importSupportedAssets,
|
importSupportedAssets,
|
||||||
|
loadAssetCatalogSummary,
|
||||||
loadTradingConfig,
|
loadTradingConfig,
|
||||||
pauseTradingPair,
|
pauseTradingPair,
|
||||||
seedTradingConfig,
|
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');
|
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 () => {
|
test('seeded DB config preserves current nBTC/EURe pair, 49 bps edge, and legacy BTC tracking', async () => {
|
||||||
const pool = createMemoryPool();
|
const pool = createMemoryPool();
|
||||||
await seedTradingConfig(pool, { now: '2026-05-12T16:35:00.000Z' });
|
await seedTradingConfig(pool, { now: '2026-05-12T16:35:00.000Z' });
|
||||||
|
|
@ -327,6 +348,9 @@ function createMemoryPool() {
|
||||||
async query(sql, params = []) {
|
async query(sql, params = []) {
|
||||||
if (/CREATE TABLE|CREATE (UNIQUE )?INDEX/i.test(sql)) return { rows: [], rowCount: 0 };
|
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 \* 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 (/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 (/INSERT INTO trading_assets/i.test(sql)) return insertAsset(this, params);
|
||||||
if (/UPDATE trading_assets/i.test(sql)) return retireAssets(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 };
|
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) {
|
function insertAsset(pool, params) {
|
||||||
const seed = params.length === 16;
|
const seed = params.length === 16;
|
||||||
const [
|
const [
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue