unrip/src/apps/operator-dashboard.mjs
philipp 75d7f64627
All checks were successful
deploy / deploy (push) Successful in 1m10s
Add quote lifecycle statistics chart
Proof: Added an ECharts stacked persisted quote lifecycle statistics chart with range controls, raised dashboard statistics loads to the existing 1000-row clamp, and covered the UI wiring with static regression assertions. Ran targeted dashboard tests, full npm test, and npm run operator-dashboard:build.

Assumptions: Persisted quote lifecycle statistics remain the durable source of truth for the chart; live counters stay separate until the history rollup refresh records them.

Still fake: Fee-aware PnL, quote size distributions, oracle-deviation analytics, and any already-pruned lifecycle detail remain out of scope; the chart can only show retained persisted statistics.
2026-06-12 20:34:41 +02:00

1031 lines
32 KiB
JavaScript

import http from 'node:http';
import process from 'node:process';
import { readdir, readFile } from 'node:fs/promises';
import path from 'node:path';
import { WebSocketServer } from 'ws';
import { createConsumer } from '../bus/kafka/consumer.mjs';
import { parseEventMessage } from '../core/event-envelope.mjs';
import { buildMakerCompetitivenessSummary } from '../core/maker-competitiveness.mjs';
import {
DASHBOARD_WEBSOCKET_MAX_BUFFERED_BYTES,
applyDashboardLiveEvent,
buildDashboardBootstrap,
buildDashboardControlErrorResponse,
buildLiveQuoteLifecycleStatistics,
buildLiveQuoteLifecycleRows,
buildQuoteLifecycleLookupResponse,
buildLiveStatusBar,
createDashboardLiveState,
isDashboardWebSocketBackpressured,
listDashboardServices,
resolveDashboardControl,
resolveDashboardControlTimeoutMs,
} from '../core/operator-dashboard.mjs';
import {
buildDashboardAuthChallengeHeader,
buildDashboardSessionCookie,
resolveDashboardRequestAuth,
} from '../core/operator-dashboard-auth.mjs';
import { createLogger, serializeError } from '../core/log.mjs';
import { normalizeNearIntentsStatus } from '../core/near-intents-status.mjs';
import { readJsonBody, sendJson } from '../core/control-api.mjs';
import { loadConfig } from '../lib/config.mjs';
import { fetchJson } from '../lib/http.mjs';
import {
createPairStrategyConfigVersion,
createPostgresPool,
createTradingConfigStore,
enableObserveOnlyPair,
ensureHistorySchema,
importSupportedAssets,
loadAssetCatalogSummary,
loadCurrentFundingObservations,
loadLatestInventorySnapshot,
loadLatestMarketPrice,
loadLatestPortfolioMetric,
loadPairConfigSummary,
loadQuoteLifecycleLookupEvidence,
loadQuoteLifecycleRetentionSummary,
loadQuoteLifecycleStatistics,
loadRecentAlertTransitions,
loadRecentDepositStatuses,
loadRecentEnvironmentStatuses,
loadRecentExecuteTradeCommands,
loadRecentExecutionResults,
loadRecentIntentRequests,
loadRecentQuoteLifecycleRollups,
loadRecentQuoteOutcomes,
loadRecentTradeDecisions,
loadRecentQuotes,
loadSubmissionPage,
loadSubmissionSummary,
pauseTradingPair,
refreshQuoteLifecycleStatistics,
seedTradingConfig,
setTradingPairMode,
} from '../lib/postgres.mjs';
const config = loadConfig();
const logger = createLogger({
service: 'operator-dashboard',
component: 'dashboard',
namespace: config.projectNamespace,
});
const dashboardRuntimeState = {
last_bootstrap_at: null,
last_bootstrap_error: null,
source_errors: {},
last_source_error_at: null,
last_live_event_error: null,
latest_maker_competitiveness: null,
websocket_clients: 0,
};
if (
config.operatorDashboardAuthMode === 'basic'
&& (!config.operatorDashboardAuthUsername || !config.operatorDashboardAuthPassword)
) {
logger.error('dashboard_basic_auth_config_missing', {
details: {
auth_mode: config.operatorDashboardAuthMode,
},
});
process.exit(1);
}
const pool = createPostgresPool({
connectionString: config.postgresUrl,
});
await ensureHistorySchema(pool);
await seedTradingConfig(pool);
const tradingConfigStore = createTradingConfigStore({
pool,
logger: logger.child({ component: 'trading-config' }),
});
const initialTradingConfig = await tradingConfigStore.forceRefresh();
const initialRuntimeConfig = buildRuntimeConfig(initialTradingConfig);
const staticAssets = await loadStaticAssets();
const initialServiceSnapshots = await loadServiceSnapshots();
const initialRecentQuotes = await safeSourceLoad(
'recent_quotes',
() => loadRecentQuotes(pool, {
limit: config.operatorDashboardQuoteLimit,
}),
[],
);
const initialSubmissionSummary = await safeSourceLoad(
'submission_summary',
() => loadSubmissionSummary(pool),
{ total: 0, last_submission_at: null },
);
const initialMarketPrice = await safeSourceLoad(
'latest_market_price',
() => loadLatestMarketPrice(pool),
null,
);
const initialInventory = await safeSourceLoad(
'latest_inventory',
() => loadLatestInventorySnapshot(pool),
null,
);
const initialRecentTradeDecisions = await safeSourceLoad(
'recent_trade_decisions',
() => loadRecentTradeDecisions(pool, { limit: 20 }),
[],
);
const initialRecentExecuteTradeCommands = await safeSourceLoad(
'recent_execute_trade_commands',
() => loadRecentExecuteTradeCommands(pool, { limit: 40 }),
[],
);
const initialRecentExecutionResults = await safeSourceLoad(
'recent_execution_results',
() => loadRecentExecutionResults(pool, { limit: 40 }),
[],
);
const initialRecentQuoteOutcomes = await safeSourceLoad(
'recent_quote_outcomes',
() => loadRecentQuoteOutcomes(pool, { limit: 200 }),
[],
);
const initialNearIntentsStatus = await safeSourceLoad(
'near_intents_status',
() => loadNearIntentsStatus(initialRuntimeConfig),
null,
);
const liveState = createDashboardLiveState({
config: initialRuntimeConfig,
recentQuotes: initialRecentQuotes,
recentTradeDecisions: initialRecentTradeDecisions,
recentExecuteTradeCommands: initialRecentExecuteTradeCommands,
recentExecutionResults: initialRecentExecutionResults,
recentQuoteOutcomes: initialRecentQuoteOutcomes,
latestMarketPrice: initialMarketPrice,
latestInventory: initialInventory,
recentSubmissionCount: initialSubmissionSummary.total,
lastSubmissionAt: initialSubmissionSummary.last_submission_at,
nearIntentsStatus: initialNearIntentsStatus,
activeAlerts:
initialServiceSnapshots.find((snapshot) => snapshot.service === 'ops-sentinel')?.state?.active_alerts
|| [],
});
dashboardRuntimeState.latest_maker_competitiveness = buildMakerCompetitivenessSummary({
lifecycleRows: buildLiveQuoteLifecycleRows(liveState),
});
const liveConsumer = await createConsumer({
groupId: config.kafkaConsumerGroupOperatorDashboard,
brokers: config.kafkaBrokers,
clientId: config.kafkaClientId,
logger,
});
const liveTopics = [
config.kafkaTopicNormSwapDemand,
config.kafkaTopicDecisionTradeDecision,
config.kafkaTopicCmdExecuteTrade,
config.kafkaTopicRefMarketPrice,
config.kafkaTopicStateIntentInventory,
config.kafkaTopicOpsAlert,
config.kafkaTopicOpsEnvironmentStatus,
config.kafkaTopicExecTradeResult,
];
for (const topic of liveTopics) {
await liveConsumer.subscribe({ topic, fromBeginning: false });
}
await liveConsumer.run({
eachMessage: async ({ topic, message }) => {
if (!message.value) return;
try {
const event = parseEventMessage(message.value.toString());
const updates = applyDashboardLiveEvent(liveState, { topic, event });
for (const update of updates) {
if (update.maker_competitiveness) {
dashboardRuntimeState.latest_maker_competitiveness = update.maker_competitiveness;
}
broadcast(update);
}
} catch (error) {
dashboardRuntimeState.last_live_event_error = serializeError(error);
logger.error('dashboard_live_event_failed', {
topic,
details: {
error: serializeError(error),
},
});
}
},
});
const webSockets = new Set();
const webSocketServer = new WebSocketServer({
noServer: true,
});
webSocketServer.on('connection', (socket, _req, authContext) => {
webSockets.add(socket);
dashboardRuntimeState.websocket_clients = webSockets.size;
const recentLifecycleRows = buildLiveQuoteLifecycleRows(liveState);
const quoteLifecycleStatistics = buildLiveQuoteLifecycleStatistics(liveState);
dashboardRuntimeState.latest_maker_competitiveness = buildMakerCompetitivenessSummary({
lifecycleRows: recentLifecycleRows,
});
socket.send(JSON.stringify({
type: 'session.ready',
session: authContext,
live: {
recent_quotes: liveState.recent_quotes,
recent_lifecycle_rows: recentLifecycleRows,
quote_lifecycle_statistics: quoteLifecycleStatistics,
maker_competitiveness: dashboardRuntimeState.latest_maker_competitiveness,
status_bar: buildLiveStatusBar(liveState),
},
}));
socket.on('close', () => {
webSockets.delete(socket);
dashboardRuntimeState.websocket_clients = webSockets.size;
});
});
const server = http.createServer(async (req, res) => {
try {
const url = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`);
if (req.method === 'GET' && url.pathname === '/healthz') {
return sendJson(res, 200, {
ok: Object.keys(dashboardRuntimeState.source_errors).length === 0 && !dashboardRuntimeState.last_bootstrap_error,
service: 'operator-dashboard',
websocket_clients: webSockets.size,
source_error_count: Object.keys(dashboardRuntimeState.source_errors).length,
last_source_error_at: dashboardRuntimeState.last_source_error_at,
last_bootstrap_at: dashboardRuntimeState.last_bootstrap_at,
last_bootstrap_error: dashboardRuntimeState.last_bootstrap_error,
last_live_event_error: dashboardRuntimeState.last_live_event_error,
});
}
if (req.method === 'GET' && url.pathname === '/state') {
return sendJson(res, 200, {
service: 'operator-dashboard',
namespace: config.projectNamespace,
websocket_clients: webSockets.size,
last_bootstrap_at: dashboardRuntimeState.last_bootstrap_at,
last_bootstrap_error: dashboardRuntimeState.last_bootstrap_error,
source_errors: Object.values(dashboardRuntimeState.source_errors),
source_error_count: Object.keys(dashboardRuntimeState.source_errors).length,
last_source_error_at: dashboardRuntimeState.last_source_error_at,
last_live_event_error: dashboardRuntimeState.last_live_event_error,
latest_maker_competitiveness: dashboardRuntimeState.latest_maker_competitiveness,
});
}
const auth = authenticateHttpRequest(req, res);
if (!auth) return;
if (url.pathname.startsWith('/api/')) {
return await handleApiRequest({ req, res, url, auth });
}
if (req.method === 'GET' && staticAssets.has(url.pathname)) {
const asset = staticAssets.get(url.pathname);
res.statusCode = 200;
res.setHeader('content-type', asset.contentType);
res.end(asset.body);
return;
}
return sendJson(res, 404, { error: 'not_found' });
} catch (error) {
logger.error('dashboard_request_failed', {
details: {
path: req.url,
error: serializeError(error),
},
});
return sendJson(res, 500, {
error: error.message,
});
}
});
server.on('upgrade', (req, socket, head) => {
const url = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`);
if (url.pathname !== '/ws') {
socket.destroy();
return;
}
const auth = resolveDashboardRequestAuth({
mode: config.operatorDashboardAuthMode,
authorizationHeader: req.headers.authorization || '',
cookieHeader: req.headers.cookie || '',
username: config.operatorDashboardAuthUsername,
password: config.operatorDashboardAuthPassword,
});
if (!auth.authenticated) {
socket.write(
`HTTP/1.1 401 Unauthorized\r\nWWW-Authenticate: ${buildDashboardAuthChallengeHeader({
realm: config.operatorDashboardAuthRealm,
})}\r\n\r\n`,
);
socket.destroy();
return;
}
webSocketServer.handleUpgrade(req, socket, head, (ws) => {
webSocketServer.emit('connection', ws, req, auth);
});
});
server.listen(config.operatorDashboardControlPort, config.operatorDashboardControlHost, () => {
logger.info('operator_dashboard_started', {
details: {
host: config.operatorDashboardControlHost,
port: config.operatorDashboardControlPort,
},
});
});
async function handleApiRequest({ req, res, url, auth }) {
if (req.method === 'GET' && url.pathname === '/api/session') {
return sendJson(res, 200, auth);
}
if (req.method === 'GET' && url.pathname === '/api/bootstrap') {
const page = Number(url.searchParams.get('page') || 1);
const pageSize = Number(
url.searchParams.get('page_size') || config.operatorDashboardTradePageSize,
);
const payload = await loadBootstrapPayload({
auth,
page,
pageSize,
});
return sendJson(res, 200, payload);
}
if (req.method === 'GET' && (url.pathname === '/api/submissions' || url.pathname === '/api/trades')) {
const page = Number(url.searchParams.get('page') || 1);
const pageSize = Number(
url.searchParams.get('page_size') || config.operatorDashboardTradePageSize,
);
const submissionPage = await loadSubmissionPage(pool, {
page,
pageSize,
});
return sendJson(res, 200, submissionPage);
}
if (req.method === 'GET' && url.pathname === '/api/strategy/quote-lifecycle/statistics') {
const payload = await loadQuoteLifecycleStatisticsPayload({
grains: url.searchParams.get('grains') || undefined,
limit: Number(url.searchParams.get('limit') || 1000),
});
return sendJson(res, 200, payload);
}
const lifecycleLookupMatch = req.method === 'GET'
? url.pathname.match(/^\/api\/strategy\/quote-lifecycle\/(.+)$/)
: null;
if (lifecycleLookupMatch) {
const identifier = decodeURIComponent(lifecycleLookupMatch[1] || '').trim();
if (!identifier) {
return sendJson(res, 400, {
ok: false,
error: 'quote_lifecycle_identifier_required',
lifecycle_row: null,
lookup: {
requested_identifier: '',
matched_identifier_type: null,
found: false,
lookup_at: new Date().toISOString(),
latest_stage_at: null,
matched_identifiers: {
quote_ids: [],
decision_ids: [],
command_ids: [],
},
},
});
}
const payload = await loadQuoteLifecycleLookupPayload({ identifier });
return sendJson(res, payload.ok ? 200 : 404, {
...payload,
...(payload.ok ? {} : { error: 'quote_lifecycle_not_found' }),
});
}
const controlMatch = req.method === 'POST'
? url.pathname.match(/^\/api\/control\/([^/]+)\/([^/]+)$/)
: null;
if (controlMatch) {
const [, service, action] = controlMatch;
const body = await readJsonBody(req);
const control = resolveDashboardControl({ service, action });
if (!control) {
return sendJson(res, 404, {
error: 'unknown_control',
});
}
const serviceDefinition = listDashboardServices(config)
.find((definition) => definition.service === control.service);
try {
const result = await invokeControl(control, body || {});
const serviceSnapshot = serviceDefinition
? await loadServiceSnapshot(serviceDefinition)
: null;
return sendJson(res, 200, {
ok: true,
control,
result,
service_snapshot: serviceSnapshot,
});
} catch (error) {
logger.warn('dashboard_control_failed', {
details: {
control,
error: serializeError(error),
},
});
const serviceSnapshot = serviceDefinition
? await loadServiceSnapshot(serviceDefinition).catch((snapshotError) => ({
...serviceDefinition,
reachable: false,
state: null,
health: null,
error: serializeError(snapshotError),
}))
: null;
const failure = buildDashboardControlErrorResponse(error, { control });
return sendJson(res, failure.statusCode, {
...failure.payload,
service_snapshot: serviceSnapshot,
});
}
}
return sendJson(res, 404, { error: 'not_found' });
}
async function loadBootstrapPayload({ auth, page, pageSize }) {
const sourceErrors = [];
const tradingConfig = await tradingConfigStore.forceRefresh();
const runtimeConfig = buildRuntimeConfig(tradingConfig);
const [
portfolioMetric,
inventorySnapshot,
marketPrice,
recentQuotes,
submissionSummary,
submissionPage,
fundingObservations,
recentDepositStatuses,
recentTradeDecisions,
recentExecuteTradeCommands,
recentExecutionResults,
recentQuoteOutcomes,
quoteLifecycleRetention,
quoteLifecycleRollups,
quoteLifecycleStatistics,
recentIntentRequests,
recentAlertTransitions,
recentEnvironmentStatuses,
assetCatalog,
pairConfig,
serviceSnapshots,
nearIntentsStatus,
] = await Promise.all([
safeSourceLoad('portfolio_metric', () => loadLatestPortfolioMetric(pool), null, sourceErrors),
safeSourceLoad('latest_inventory', () => loadLatestInventorySnapshot(pool), null, sourceErrors),
safeSourceLoad('latest_market_price', () => loadLatestMarketPrice(pool), null, sourceErrors),
safeSourceLoad(
'recent_quotes',
() => loadRecentQuotes(pool, {
limit: runtimeConfig.operatorDashboardQuoteLimit,
}),
[],
sourceErrors,
),
safeSourceLoad(
'submission_summary',
() => loadSubmissionSummary(pool),
{ total: 0, last_submission_at: null },
sourceErrors,
),
safeSourceLoad(
'submission_page',
() => loadSubmissionPage(pool, {
page,
pageSize,
}),
{
page,
page_size: pageSize,
total: 0,
total_pages: 1,
items: [],
},
sourceErrors,
),
safeSourceLoad('funding_observations', () => loadCurrentFundingObservations(pool), [], sourceErrors),
safeSourceLoad(
'recent_deposit_statuses',
() => loadRecentDepositStatuses(pool, { limit: 20 }),
[],
sourceErrors,
),
safeSourceLoad(
'recent_trade_decisions',
() => loadRecentTradeDecisions(pool, { limit: 20 }),
[],
sourceErrors,
),
safeSourceLoad(
'recent_execute_trade_commands',
() => loadRecentExecuteTradeCommands(pool, { limit: 40 }),
[],
sourceErrors,
),
safeSourceLoad(
'recent_execution_results',
() => loadRecentExecutionResults(pool, { limit: 40 }),
[],
sourceErrors,
),
safeSourceLoad(
'recent_quote_outcomes',
() => loadRecentQuoteOutcomes(pool, { limit: 200 }),
[],
sourceErrors,
),
safeSourceLoad(
'quote_lifecycle_retention',
() => loadQuoteLifecycleRetentionSummary(pool),
null,
sourceErrors,
),
safeSourceLoad(
'quote_lifecycle_rollups',
() => loadRecentQuoteLifecycleRollups(pool, { limit: 40 }),
[],
sourceErrors,
),
safeSourceLoad(
'quote_lifecycle_statistics',
() => loadQuoteLifecycleStatistics(pool, { limit: 1000 }),
[],
sourceErrors,
),
safeSourceLoad(
'recent_intent_requests',
() => loadRecentIntentRequests(pool, {
limit: 20,
btcAsset: runtimeConfig.tradingBtc,
eureAsset: runtimeConfig.tradingEure,
refreshOutcomes: false,
}),
[],
sourceErrors,
),
safeSourceLoad(
'recent_alert_transitions',
() => loadRecentAlertTransitions(pool, { limit: 20 }),
[],
sourceErrors,
),
safeSourceLoad(
'recent_environment_statuses',
() => loadRecentEnvironmentStatuses(pool, { limit: 20 }),
[],
sourceErrors,
),
safeSourceLoad('asset_catalog', () => loadAssetCatalogSummary(pool, { limit: 250 }), null, sourceErrors),
safeSourceLoad('pair_config', () => loadPairConfigSummary(pool), null, sourceErrors),
loadServiceSnapshots(),
safeSourceLoad('near_intents_status', () => loadNearIntentsStatus(runtimeConfig), null, sourceErrors),
]);
const payload = buildDashboardBootstrap({
config: runtimeConfig,
auth,
portfolioMetric,
inventorySnapshot,
marketPrice,
recentQuotes,
submissionPage,
submissionSummary,
fundingObservations,
recentDepositStatuses,
recentTradeDecisions,
recentExecuteTradeCommands,
recentExecutionResults,
recentQuoteOutcomes,
quoteLifecycleRetention,
quoteLifecycleRollups,
quoteLifecycleStatistics: {
persisted: {
source: 'persisted',
generated_at: quoteLifecycleStatistics[0]?.computed_at || null,
rows: quoteLifecycleStatistics,
},
live: buildLiveQuoteLifecycleStatistics(liveState),
},
recentIntentRequests,
recentAlertTransitions,
recentEnvironmentStatuses,
assetCatalog,
pairConfig,
serviceSnapshots,
nearIntentsStatus,
sourceErrors,
});
dashboardRuntimeState.last_bootstrap_at = new Date().toISOString();
dashboardRuntimeState.last_bootstrap_error = null;
dashboardRuntimeState.latest_maker_competitiveness = (
payload.strategy?.strategy_state?.maker_competitiveness
|| dashboardRuntimeState.latest_maker_competitiveness
);
return payload;
}
async function loadQuoteLifecycleLookupPayload({ identifier }) {
const tradingConfig = await tradingConfigStore.forceRefresh();
const runtimeConfig = buildRuntimeConfig(tradingConfig);
const evidence = await loadQuoteLifecycleLookupEvidence(pool, {
identifier,
});
return buildQuoteLifecycleLookupResponse({
config: runtimeConfig,
identifier,
evidence,
});
}
async function loadQuoteLifecycleStatisticsPayload({ grains, limit } = {}) {
const refreshed = await refreshQuoteLifecycleStatistics(pool, {
now: new Date().toISOString(),
grains,
});
const rows = await loadQuoteLifecycleStatistics(pool, {
grains,
limit,
});
return {
ok: true,
refreshed_at: refreshed.computed_at,
persisted: {
source: 'persisted',
generated_at: rows[0]?.computed_at || refreshed.computed_at,
rows,
},
live: buildLiveQuoteLifecycleStatistics(liveState),
refresh: {
statistic_row_count: refreshed.statistic_row_count,
quote_count: refreshed.quote_count,
evidence_counts: refreshed.evidence_counts,
},
};
}
async function loadServiceSnapshots() {
const services = listDashboardServices(config);
return Promise.all(services.map((service) => loadServiceSnapshot(service)));
}
async function loadServiceSnapshot(service) {
const [stateResult, healthResult] = await Promise.allSettled([
fetchUpstreamJson(`${service.base_url}/state`),
fetchUpstreamJson(`${service.base_url}/healthz`),
]);
const state = stateResult.status === 'fulfilled' ? stateResult.value : null;
const health = healthResult.status === 'fulfilled' ? healthResult.value : null;
const error = stateResult.status === 'rejected'
? serializeError(stateResult.reason)
: healthResult.status === 'rejected'
? serializeError(healthResult.reason)
: null;
return {
...service,
reachable: Boolean(state || health),
state,
health,
error,
};
}
async function fetchUpstreamJson(url) {
return fetchJson(url, {
signal: AbortSignal.timeout(config.operatorDashboardUpstreamTimeoutMs),
});
}
async function loadNearIntentsStatus(runtimeConfig = config) {
const [servicesResponse, postsResponse, postEnumsResponse] = await Promise.all([
fetchNearIntentsStatusJson(config.nearIntentsStatusServicesUrl),
fetchNearIntentsStatusJson(config.nearIntentsStatusPostsUrl),
fetchNearIntentsStatusJson(config.nearIntentsStatusPostEnumsUrl),
]);
return normalizeNearIntentsStatus({
servicesResponse,
postsResponse,
postEnumsResponse,
observedAt: new Date().toISOString(),
trackedAssets: runtimeConfig.trackedAssets,
});
}
async function fetchNearIntentsStatusJson(url) {
return fetchJson(url, {
signal: AbortSignal.timeout(config.nearIntentsStatusTimeoutMs),
});
}
async function invokeControl(control, body) {
if (control.service === 'operator-dashboard' && control.action === 'import-supported-assets') {
const result = await importSupportedAssets(pool);
await tradingConfigStore.forceRefresh();
return result;
}
if (control.service === 'operator-dashboard' && control.action === 'update-pair-edge') {
const result = await createPairStrategyConfigVersion(pool, {
pairId: body.pair_id || body.pair,
edgeBps: Number(body.edge_bps),
maxNotional: body.max_notional,
minNotional: bodyField(body, 'min_notional', 'minNotional'),
requestDefaultNotional: bodyField(body, 'request_default_notional', 'requestDefaultNotional'),
requestMaxNotional: bodyField(body, 'request_max_notional', 'requestMaxNotional'),
requestMaxSlippageBps: bodyField(body, 'request_max_slippage_bps', 'requestMaxSlippageBps'),
makerMaxQuoteAgeEnabled: bodyField(body, 'maker_max_quote_age_enabled', 'makerMaxQuoteAgeEnabled'),
makerMaxQuoteAgeMs: bodyField(body, 'maker_max_quote_age_ms', 'makerMaxQuoteAgeMs'),
makerLatencyPolicyReason: bodyField(body, 'maker_latency_policy_reason', 'makerLatencyPolicyReason'),
changedBy: body.changed_by || 'operator',
reason: body.reason || 'dashboard pair strategy config update',
});
await tradingConfigStore.forceRefresh();
return result;
}
if (control.service === 'operator-dashboard' && control.action === 'enable-observe-only-pair') {
const result = await enableObserveOnlyPair(pool, {
assetIn: body.asset_in,
assetOut: body.asset_out,
changedBy: body.changed_by || 'operator',
reason: body.reason || 'dashboard observe-only enable',
});
await tradingConfigStore.forceRefresh();
return result;
}
if (control.service === 'operator-dashboard' && control.action === 'set-pair-mode') {
const result = await setTradingPairMode(pool, {
pairId: body.pair_id || body.pair,
assetIn: body.asset_in,
assetOut: body.asset_out,
mode: body.mode,
edgeBps: body.edge_bps,
maxNotional: body.max_notional,
minNotional: bodyField(body, 'min_notional', 'minNotional'),
requestDefaultNotional: bodyField(body, 'request_default_notional', 'requestDefaultNotional'),
requestMaxNotional: bodyField(body, 'request_max_notional', 'requestMaxNotional'),
requestMaxSlippageBps: bodyField(body, 'request_max_slippage_bps', 'requestMaxSlippageBps'),
makerMaxQuoteAgeEnabled: bodyField(body, 'maker_max_quote_age_enabled', 'makerMaxQuoteAgeEnabled'),
makerMaxQuoteAgeMs: bodyField(body, 'maker_max_quote_age_ms', 'makerMaxQuoteAgeMs'),
makerLatencyPolicyReason: bodyField(body, 'maker_latency_policy_reason', 'makerLatencyPolicyReason'),
changedBy: body.changed_by || 'operator',
reason: body.reason || 'dashboard pair mode update',
});
await tradingConfigStore.forceRefresh();
return result;
}
if (control.service === 'operator-dashboard' && control.action === 'pause-pair') {
const result = await pauseTradingPair(pool, {
pairId: body.pair_id || body.pair,
changedBy: body.changed_by || 'operator',
reason: body.reason || 'dashboard pair pause',
});
await tradingConfigStore.forceRefresh();
return result;
}
const response = await fetchJson(
`${lookupServiceBaseUrl(control.service)}${control.path}`,
{
method: control.method,
headers: {
'content-type': 'application/json',
},
body: JSON.stringify(body || {}),
signal: AbortSignal.timeout(resolveDashboardControlTimeoutMs({ control, config })),
},
);
return response;
}
function lookupServiceBaseUrl(serviceName) {
const service = listDashboardServices(config).find((entry) => entry.service === serviceName);
if (!service) {
throw new Error(`unknown service: ${serviceName}`);
}
return service.base_url;
}
function bodyField(body, snakeKey, camelKey) {
if (Object.hasOwn(body || {}, snakeKey)) return body[snakeKey];
if (Object.hasOwn(body || {}, camelKey)) return body[camelKey];
return undefined;
}
function buildRuntimeConfig(tradingConfig) {
return {
...config,
...tradingConfig,
assetRegistry: tradingConfig.assetRegistry || config.assetRegistry,
trackedAssets: tradingConfig.trackedAssets || config.trackedAssets,
trackedAssetIds: tradingConfig.trackedAssetIds || config.trackedAssetIds,
tradingBtc: tradingConfig.tradingBtc || config.tradingBtc,
tradingBtcAssets: tradingConfig.tradingBtcAssets?.length
? tradingConfig.tradingBtcAssets
: config.tradingBtcAssets,
tradingEure: tradingConfig.tradingEure || config.tradingEure,
activePair: tradingConfig.activePair || config.activePair,
};
}
function broadcast(payload) {
const encoded = JSON.stringify(payload);
for (const socket of webSockets) {
if (socket.readyState !== 1) continue;
if (isDashboardWebSocketBackpressured(socket)) {
logger.warn('dashboard_websocket_backpressure_disconnect', {
details: {
buffered_amount: Number(socket.bufferedAmount || 0),
max_buffered_bytes: DASHBOARD_WEBSOCKET_MAX_BUFFERED_BYTES,
},
});
webSockets.delete(socket);
dashboardRuntimeState.websocket_clients = webSockets.size;
socket.terminate?.();
continue;
}
socket.send(encoded, (error) => {
if (!error) return;
dashboardRuntimeState.last_live_event_error = serializeError(error);
webSockets.delete(socket);
dashboardRuntimeState.websocket_clients = webSockets.size;
socket.terminate?.();
});
}
}
function authenticateHttpRequest(req, res) {
const auth = resolveDashboardRequestAuth({
mode: config.operatorDashboardAuthMode,
authorizationHeader: req.headers.authorization || '',
cookieHeader: req.headers.cookie || '',
username: config.operatorDashboardAuthUsername,
password: config.operatorDashboardAuthPassword,
});
if (!auth.authenticated) {
res.statusCode = 401;
res.setHeader('WWW-Authenticate', buildDashboardAuthChallengeHeader({
realm: config.operatorDashboardAuthRealm,
}));
if ((req.url || '').startsWith('/api/')) {
return sendJson(res, 401, { error: 'authentication_required' });
}
res.end('authentication required\n');
return null;
}
if (auth.setSessionCookie) {
res.setHeader('Set-Cookie', buildDashboardSessionCookie({
sessionCookieName: auth.sessionCookieName,
sessionToken: auth.sessionToken,
}));
}
return auth;
}
async function loadStaticAssets() {
const distDirectory = new URL('../operator-dashboard/dist/', import.meta.url);
const assets = new Map();
await loadStaticAssetDirectory(distDirectory, '', assets);
const indexAsset = assets.get('/index.html');
if (!indexAsset) {
throw new Error('operator dashboard frontend is missing /index.html; run the dashboard build');
}
assets.set('/', indexAsset);
return assets;
}
async function loadStaticAssetDirectory(directoryUrl, relativeDirectory, assets) {
const entries = await readdir(directoryUrl, { withFileTypes: true });
for (const entry of entries) {
if (entry.isDirectory()) {
await loadStaticAssetDirectory(
new URL(`${entry.name}/`, directoryUrl),
path.posix.join(relativeDirectory, entry.name),
assets,
);
continue;
}
const relativePath = path.posix.join(relativeDirectory, entry.name);
const requestPath = `/${relativePath}`;
const body = await readFile(new URL(entry.name, directoryUrl));
assets.set(requestPath, {
contentType: resolveStaticContentType(entry.name),
body,
});
}
}
function resolveStaticContentType(filename) {
switch (path.extname(filename)) {
case '.html':
return 'text/html; charset=utf-8';
case '.js':
return 'text/javascript; charset=utf-8';
case '.css':
return 'text/css; charset=utf-8';
case '.json':
return 'application/json; charset=utf-8';
case '.svg':
return 'image/svg+xml';
case '.png':
return 'image/png';
case '.jpg':
case '.jpeg':
return 'image/jpeg';
case '.webp':
return 'image/webp';
case '.ico':
return 'image/x-icon';
default:
return 'application/octet-stream';
}
}
async function safeSourceLoad(name, loader, fallback, sourceErrors = null) {
try {
const result = await loader();
delete dashboardRuntimeState.source_errors[name];
return result;
} catch (error) {
const serialized = serializeError(error);
dashboardRuntimeState.source_errors[name] = {
source: name,
error: serialized,
};
dashboardRuntimeState.last_source_error_at = new Date().toISOString();
logger.error('dashboard_source_load_failed', {
details: {
source: name,
error: serialized,
},
});
sourceErrors?.push({
source: name,
error: serialized,
});
dashboardRuntimeState.last_bootstrap_error = serialized;
return fallback;
}
}
async function shutdown() {
server.close(() => {});
for (const socket of webSockets) {
socket.close();
}
await liveConsumer.stop().catch(() => {});
await liveConsumer.disconnect().catch(() => {});
await pool.end().catch(() => {});
process.exit(0);
}
process.on('SIGINT', shutdown);
process.on('SIGTERM', shutdown);