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 { applyDashboardLiveEvent, buildDashboardBootstrap, buildDashboardControlErrorResponse, buildLiveQuoteLifecycleRows, buildLiveStatusBar, createDashboardLiveState, 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 { createPostgresPool, ensureHistorySchema, loadCurrentFundingObservations, loadLatestInventorySnapshot, loadLatestMarketPrice, loadLatestPortfolioMetric, loadRecentAlertTransitions, loadRecentDepositStatuses, loadRecentEnvironmentStatuses, loadRecentExecuteTradeCommands, loadRecentExecutionResults, loadRecentIntentRequests, loadRecentQuoteOutcomes, loadRecentTradeDecisions, loadRecentQuotes, loadSubmissionPage, loadSubmissionSummary, } 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, 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); 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(), null, ); const liveState = createDashboardLiveState({ config, 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 || [], }); 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) { 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; socket.send(JSON.stringify({ type: 'session.ready', session: authContext, live: { recent_quotes: liveState.recent_quotes, recent_lifecycle_rows: buildLiveQuoteLifecycleRows(liveState), 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, }); } 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); } 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 = await loadServiceSnapshot(serviceDefinition); 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 = await loadServiceSnapshot(serviceDefinition).catch((snapshotError) => ({ ...serviceDefinition, reachable: false, state: null, health: null, error: serializeError(snapshotError), })); 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 [ portfolioMetric, inventorySnapshot, marketPrice, recentQuotes, submissionSummary, submissionPage, fundingObservations, recentDepositStatuses, recentTradeDecisions, recentExecuteTradeCommands, recentExecutionResults, recentQuoteOutcomes, recentIntentRequests, recentAlertTransitions, recentEnvironmentStatuses, 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: config.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( 'recent_intent_requests', () => loadRecentIntentRequests(pool, { limit: 20, btcAsset: config.tradingBtc, eureAsset: config.tradingEure, }), [], sourceErrors, ), safeSourceLoad( 'recent_alert_transitions', () => loadRecentAlertTransitions(pool, { limit: 20 }), [], sourceErrors, ), safeSourceLoad( 'recent_environment_statuses', () => loadRecentEnvironmentStatuses(pool, { limit: 20 }), [], sourceErrors, ), loadServiceSnapshots(), safeSourceLoad('near_intents_status', () => loadNearIntentsStatus(), null, sourceErrors), ]); const payload = buildDashboardBootstrap({ config, auth, portfolioMetric, inventorySnapshot, marketPrice, recentQuotes, submissionPage, submissionSummary, fundingObservations, recentDepositStatuses, recentTradeDecisions, recentExecuteTradeCommands, recentExecutionResults, recentQuoteOutcomes, recentIntentRequests, recentAlertTransitions, recentEnvironmentStatuses, serviceSnapshots, nearIntentsStatus, sourceErrors, }); dashboardRuntimeState.last_bootstrap_at = new Date().toISOString(); dashboardRuntimeState.last_bootstrap_error = null; return payload; } 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() { 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(), }); } async function fetchNearIntentsStatusJson(url) { return fetchJson(url, { signal: AbortSignal.timeout(config.nearIntentsStatusTimeoutMs), }); } async function invokeControl(control, body) { 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 broadcast(payload) { const encoded = JSON.stringify(payload); for (const socket of webSockets) { if (socket.readyState !== 1) continue; socket.send(encoded); } } 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, })); 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);