unrip/src/apps/operator-dashboard.mjs
philipp 601450c664
Some checks failed
deploy / deploy (push) Failing after 29s
Persist NEAR status changes only
Proof: npm test; npm run operator-dashboard:build; node --test test/near-intents-status.test.mjs test/environment-status-history.test.mjs test/operator-dashboard.test.mjs test/operator-dashboard-ui-static.test.mjs test/ops-sentinel-static.test.mjs; PYTHONPATH=. python3 test/repo_deployments_test.py; kubectl kustomize deploy/k8s/base.

Assumptions: NEAR Intents public status page API remains the official upstream environmental-status source; status fingerprint changes are the durable boundary for saving environmental history.

Still fake: This stores and displays official upstream status changes, but it does not create an alternate quote source or make NEAR quoting operational during an upstream pause.
2026-04-17 14:34:10 +02:00

751 lines
22 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 {
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);