Surface NEAR Intents upstream status truth
All checks were successful
deploy / deploy (push) Successful in 33s
All checks were successful
deploy / deploy (push) Successful in 33s
Proof: npm test; npm run operator-dashboard:build; node --test test/near-intents-status.test.mjs test/operator-dashboard.test.mjs test/operator-dashboard-ui-static.test.mjs; PYTHONPATH=. python3 test/repo_deployments_test.py; kubectl kustomize deploy/k8s/base; live normalization against https://status.near-intents.org returned disrupted/upstream paused for the current 1Click quoting pause. Assumptions: NEAR Intents public status page API is the official upstream disruption source for operator display; relay websocket reachability remains separately observed by ingest and executor state. Still fake: This does not add an alternate quote source or recover trading while NEAR Intents quoting is paused; it only makes the upstream disruption explicit and separates it from local service freshness.
This commit is contained in:
parent
8641c60ab7
commit
99ca09b69e
9 changed files with 399 additions and 6 deletions
|
|
@ -24,6 +24,7 @@ import {
|
||||||
resolveDashboardRequestAuth,
|
resolveDashboardRequestAuth,
|
||||||
} from '../core/operator-dashboard-auth.mjs';
|
} from '../core/operator-dashboard-auth.mjs';
|
||||||
import { createLogger, serializeError } from '../core/log.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 { readJsonBody, sendJson } from '../core/control-api.mjs';
|
||||||
import { loadConfig } from '../lib/config.mjs';
|
import { loadConfig } from '../lib/config.mjs';
|
||||||
import { fetchJson } from '../lib/http.mjs';
|
import { fetchJson } from '../lib/http.mjs';
|
||||||
|
|
@ -402,6 +403,7 @@ async function loadBootstrapPayload({ auth, page, pageSize }) {
|
||||||
recentIntentRequests,
|
recentIntentRequests,
|
||||||
recentAlertTransitions,
|
recentAlertTransitions,
|
||||||
serviceSnapshots,
|
serviceSnapshots,
|
||||||
|
nearIntentsStatus,
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
safeSourceLoad('portfolio_metric', () => loadLatestPortfolioMetric(pool), null, sourceErrors),
|
safeSourceLoad('portfolio_metric', () => loadLatestPortfolioMetric(pool), null, sourceErrors),
|
||||||
safeSourceLoad('latest_inventory', () => loadLatestInventorySnapshot(pool), null, sourceErrors),
|
safeSourceLoad('latest_inventory', () => loadLatestInventorySnapshot(pool), null, sourceErrors),
|
||||||
|
|
@ -483,6 +485,7 @@ async function loadBootstrapPayload({ auth, page, pageSize }) {
|
||||||
sourceErrors,
|
sourceErrors,
|
||||||
),
|
),
|
||||||
loadServiceSnapshots(),
|
loadServiceSnapshots(),
|
||||||
|
safeSourceLoad('near_intents_status', () => loadNearIntentsStatus(), null, sourceErrors),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const payload = buildDashboardBootstrap({
|
const payload = buildDashboardBootstrap({
|
||||||
|
|
@ -503,6 +506,7 @@ async function loadBootstrapPayload({ auth, page, pageSize }) {
|
||||||
recentIntentRequests,
|
recentIntentRequests,
|
||||||
recentAlertTransitions,
|
recentAlertTransitions,
|
||||||
serviceSnapshots,
|
serviceSnapshots,
|
||||||
|
nearIntentsStatus,
|
||||||
sourceErrors,
|
sourceErrors,
|
||||||
});
|
});
|
||||||
dashboardRuntimeState.last_bootstrap_at = new Date().toISOString();
|
dashboardRuntimeState.last_bootstrap_at = new Date().toISOString();
|
||||||
|
|
@ -544,6 +548,27 @@ async function fetchUpstreamJson(url) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
async function invokeControl(control, body) {
|
||||||
const response = await fetchJson(
|
const response = await fetchJson(
|
||||||
`${lookupServiceBaseUrl(control.service)}${control.path}`,
|
`${lookupServiceBaseUrl(control.service)}${control.path}`,
|
||||||
|
|
|
||||||
116
src/core/near-intents-status.mjs
Normal file
116
src/core/near-intents-status.mjs
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
const TERMINAL_STATUS_NAMES = new Set([
|
||||||
|
'resolved',
|
||||||
|
'completed',
|
||||||
|
'complete',
|
||||||
|
'done',
|
||||||
|
]);
|
||||||
|
|
||||||
|
export function normalizeNearIntentsStatus({
|
||||||
|
postsResponse = null,
|
||||||
|
servicesResponse = null,
|
||||||
|
postEnumsResponse = null,
|
||||||
|
observedAt = new Date().toISOString(),
|
||||||
|
} = {}) {
|
||||||
|
const posts = Array.isArray(postsResponse?.posts)
|
||||||
|
? postsResponse.posts
|
||||||
|
: Array.isArray(postsResponse)
|
||||||
|
? postsResponse
|
||||||
|
: [];
|
||||||
|
const services = Array.isArray(servicesResponse?.services)
|
||||||
|
? servicesResponse.services
|
||||||
|
: Array.isArray(servicesResponse)
|
||||||
|
? servicesResponse
|
||||||
|
: [];
|
||||||
|
const postEnums = Array.isArray(postEnumsResponse?.post_enums)
|
||||||
|
? postEnumsResponse.post_enums
|
||||||
|
: Array.isArray(postEnumsResponse)
|
||||||
|
? postEnumsResponse
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const statusById = new Map(
|
||||||
|
postEnums
|
||||||
|
.filter((entry) => entry?.post_enum_type === 'status')
|
||||||
|
.map((entry) => [entry.id, entry]),
|
||||||
|
);
|
||||||
|
const severityById = new Map(
|
||||||
|
postEnums
|
||||||
|
.filter((entry) => entry?.post_enum_type === 'severity')
|
||||||
|
.map((entry) => [entry.id, entry]),
|
||||||
|
);
|
||||||
|
const serviceById = new Map(services.map((entry) => [entry.id, entry]));
|
||||||
|
|
||||||
|
const incidents = posts
|
||||||
|
.map((post) => normalizePost(post, { statusById, severityById, serviceById }))
|
||||||
|
.filter(Boolean)
|
||||||
|
.filter((post) => post.active);
|
||||||
|
|
||||||
|
const affectedServices = [...new Set(
|
||||||
|
incidents.flatMap((incident) => incident.impacts.map((impact) => impact.service_name || impact.service_id)),
|
||||||
|
)].filter(Boolean);
|
||||||
|
const primaryIncident = incidents[0] || null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
observed_at: observedAt,
|
||||||
|
source: 'near_intents_status_page',
|
||||||
|
status: incidents.length > 0 ? 'disrupted' : 'operational',
|
||||||
|
label: incidents.length > 0 ? 'upstream paused' : 'operational',
|
||||||
|
current_incident_count: incidents.length,
|
||||||
|
current_incidents: incidents,
|
||||||
|
affected_services: affectedServices,
|
||||||
|
quoting_stopped: incidents.some((incident) => /1click|quoting|solver|swap/i.test(
|
||||||
|
`${incident.title || ''} ${incident.message_text || ''}`,
|
||||||
|
)),
|
||||||
|
decisive_reason: primaryIncident
|
||||||
|
? [primaryIncident.title, primaryIncident.message_text].filter(Boolean).join(': ')
|
||||||
|
: 'NEAR Intents status page reports no active incident.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizePost(post, { statusById, severityById, serviceById }) {
|
||||||
|
if (!post || typeof post !== 'object') return null;
|
||||||
|
const latestUpdate = post.latest_update || [...(post.updates || [])].pop() || {};
|
||||||
|
const status = statusById.get(latestUpdate.status_id) || {};
|
||||||
|
const severity = severityById.get(latestUpdate.severity_id) || {};
|
||||||
|
const statusName = normalizeName(status.name || status.description || latestUpdate.status || 'unknown');
|
||||||
|
const ended = post.ends_at && Number(post.ends_at) <= Date.now();
|
||||||
|
const active = !ended && !TERMINAL_STATUS_NAMES.has(statusName);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: post.id || null,
|
||||||
|
title: post.title || null,
|
||||||
|
post_type: post.post_type || null,
|
||||||
|
status: statusName,
|
||||||
|
severity: normalizeName(severity.name || severity.description || 'unknown'),
|
||||||
|
active,
|
||||||
|
first_update_at: toIso(post.first_update_at || post.starts_at),
|
||||||
|
last_update_at: toIso(post.last_update_at || latestUpdate.reported_at),
|
||||||
|
message_text: stripHtml(latestUpdate.message || post.message || ''),
|
||||||
|
impacts: (latestUpdate.impacts || []).map((impact) => {
|
||||||
|
const service = serviceById.get(impact.service_id) || {};
|
||||||
|
const impactSeverity = severityById.get(impact.severity_id) || {};
|
||||||
|
return {
|
||||||
|
service_id: impact.service_id || null,
|
||||||
|
service_name: service.display_name || service.name || impact.service_id || null,
|
||||||
|
severity: normalizeName(impactSeverity.name || impactSeverity.description || 'unknown'),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeName(value) {
|
||||||
|
return String(value || '').trim().toLowerCase().replaceAll(' ', '_') || 'unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
function stripHtml(value) {
|
||||||
|
return String(value || '')
|
||||||
|
.replace(/<[^>]*>/g, ' ')
|
||||||
|
.replace(/\s+/g, ' ')
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function toIso(value) {
|
||||||
|
if (value == null || value === '') return null;
|
||||||
|
const numeric = Number(value);
|
||||||
|
const date = Number.isFinite(numeric) ? new Date(numeric) : new Date(value);
|
||||||
|
return Number.isNaN(date.getTime()) ? null : date.toISOString();
|
||||||
|
}
|
||||||
|
|
@ -454,6 +454,7 @@ export function buildDashboardBootstrap({
|
||||||
recentIntentRequests = [],
|
recentIntentRequests = [],
|
||||||
recentAlertTransitions,
|
recentAlertTransitions,
|
||||||
serviceSnapshots,
|
serviceSnapshots,
|
||||||
|
nearIntentsStatus = null,
|
||||||
sourceErrors = [],
|
sourceErrors = [],
|
||||||
} = {}) {
|
} = {}) {
|
||||||
const servicesByName = Object.fromEntries(
|
const servicesByName = Object.fromEntries(
|
||||||
|
|
@ -498,6 +499,7 @@ export function buildDashboardBootstrap({
|
||||||
marketPrice,
|
marketPrice,
|
||||||
activeAlerts,
|
activeAlerts,
|
||||||
servicesByName,
|
servicesByName,
|
||||||
|
nearIntentsStatus,
|
||||||
}),
|
}),
|
||||||
funds: {
|
funds: {
|
||||||
profitability,
|
profitability,
|
||||||
|
|
@ -536,6 +538,7 @@ export function buildDashboardBootstrap({
|
||||||
servicesByName,
|
servicesByName,
|
||||||
activeAlerts,
|
activeAlerts,
|
||||||
recentAlerts,
|
recentAlerts,
|
||||||
|
nearIntentsStatus,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -653,9 +656,14 @@ function buildStatusBar({
|
||||||
marketPrice,
|
marketPrice,
|
||||||
activeAlerts,
|
activeAlerts,
|
||||||
servicesByName,
|
servicesByName,
|
||||||
|
nearIntentsStatus = null,
|
||||||
}) {
|
}) {
|
||||||
return {
|
return {
|
||||||
active_pair: config.activePair,
|
active_pair: config.activePair,
|
||||||
|
near_intents_upstream_status: nearIntentsStatus?.status || null,
|
||||||
|
near_intents_upstream_label: nearIntentsStatus?.label || null,
|
||||||
|
near_intents_upstream_reason: nearIntentsStatus?.decisive_reason || null,
|
||||||
|
near_intents_upstream_observed_at: nearIntentsStatus?.observed_at || null,
|
||||||
latest_reference_price_eure_per_btc: marketPrice?.payload?.eure_per_btc || null,
|
latest_reference_price_eure_per_btc: marketPrice?.payload?.eure_per_btc || null,
|
||||||
market_observed_at: marketPrice?.payload?.observed_at || marketPrice?.ingested_at || null,
|
market_observed_at: marketPrice?.payload?.observed_at || marketPrice?.ingested_at || null,
|
||||||
market_freshness_ms: ageMs(marketPrice?.payload?.observed_at || marketPrice?.ingested_at),
|
market_freshness_ms: ageMs(marketPrice?.payload?.observed_at || marketPrice?.ingested_at),
|
||||||
|
|
@ -1445,7 +1453,7 @@ function summarizeGrossEdgeEstimate(rows = []) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildSystemSummary({ servicesByName, activeAlerts, recentAlerts }) {
|
function buildSystemSummary({ servicesByName, activeAlerts, recentAlerts, nearIntentsStatus = null }) {
|
||||||
const historyWriterState = servicesByName['history-writer']?.state || {};
|
const historyWriterState = servicesByName['history-writer']?.state || {};
|
||||||
void activeAlerts;
|
void activeAlerts;
|
||||||
void recentAlerts;
|
void recentAlerts;
|
||||||
|
|
@ -1455,6 +1463,7 @@ function buildSystemSummary({ servicesByName, activeAlerts, recentAlerts }) {
|
||||||
summarizeServiceSnapshot(snapshot, {
|
summarizeServiceSnapshot(snapshot, {
|
||||||
authoritativeHealth: null,
|
authoritativeHealth: null,
|
||||||
activeAlerts: [],
|
activeAlerts: [],
|
||||||
|
nearIntentsStatus,
|
||||||
})
|
})
|
||||||
)),
|
)),
|
||||||
alerts: {
|
alerts: {
|
||||||
|
|
@ -1478,7 +1487,7 @@ function buildSystemSummary({ servicesByName, activeAlerts, recentAlerts }) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function summarizeServiceSnapshot(snapshot, { authoritativeHealth = null, activeAlerts = [] } = {}) {
|
function summarizeServiceSnapshot(snapshot, { authoritativeHealth = null, activeAlerts = [], nearIntentsStatus = null } = {}) {
|
||||||
const state = snapshot.state || {};
|
const state = snapshot.state || {};
|
||||||
const health = snapshot.health || {};
|
const health = snapshot.health || {};
|
||||||
void authoritativeHealth;
|
void authoritativeHealth;
|
||||||
|
|
@ -1486,17 +1495,30 @@ function summarizeServiceSnapshot(snapshot, { authoritativeHealth = null, active
|
||||||
const freshnessAt = inferServiceFreshnessTimestamp(snapshot.service, state, health);
|
const freshnessAt = inferServiceFreshnessTimestamp(snapshot.service, state, health);
|
||||||
const reachable = snapshot.reachable !== false;
|
const reachable = snapshot.reachable !== false;
|
||||||
const online = reachable && health.ok !== false;
|
const online = reachable && health.ok !== false;
|
||||||
const healthStatus = online ? 'online' : reachable ? 'reachable' : 'offline';
|
const upstreamStatus = resolveServiceUpstreamStatus(snapshot.service, nearIntentsStatus);
|
||||||
|
const upstreamDisrupted = upstreamStatus?.status === 'disrupted';
|
||||||
|
const healthStatus = upstreamDisrupted
|
||||||
|
? 'upstream_paused'
|
||||||
|
: online
|
||||||
|
? 'online'
|
||||||
|
: reachable
|
||||||
|
? 'reachable'
|
||||||
|
: 'offline';
|
||||||
|
const healthLabel = upstreamDisrupted ? 'upstream paused' : healthStatus;
|
||||||
|
const healthReasons = upstreamDisrupted && upstreamStatus.decisive_reason
|
||||||
|
? [upstreamStatus.decisive_reason]
|
||||||
|
: [];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
service: snapshot.service,
|
service: snapshot.service,
|
||||||
label: snapshot.label,
|
label: snapshot.label,
|
||||||
base_url: snapshot.base_url,
|
base_url: snapshot.base_url,
|
||||||
reachable,
|
reachable,
|
||||||
health_ok: online,
|
health_ok: healthStatus === 'online',
|
||||||
health_status: healthStatus,
|
health_status: healthStatus,
|
||||||
health_label: healthStatus,
|
health_label: healthLabel,
|
||||||
health_reasons: [],
|
health_reasons: healthReasons,
|
||||||
|
upstream_status: upstreamStatus,
|
||||||
highest_alert_severity: null,
|
highest_alert_severity: null,
|
||||||
paused: state.paused ?? health.paused ?? null,
|
paused: state.paused ?? health.paused ?? null,
|
||||||
armed: state.armed ?? null,
|
armed: state.armed ?? null,
|
||||||
|
|
@ -1508,6 +1530,22 @@ function summarizeServiceSnapshot(snapshot, { authoritativeHealth = null, active
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const NEAR_INTENTS_RELAY_SERVICES = new Set(['near-intents-ingest', 'trade-executor']);
|
||||||
|
|
||||||
|
function resolveServiceUpstreamStatus(service, nearIntentsStatus) {
|
||||||
|
if (!NEAR_INTENTS_RELAY_SERVICES.has(service) || !nearIntentsStatus) return null;
|
||||||
|
return {
|
||||||
|
source: nearIntentsStatus.source || 'near_intents_status_page',
|
||||||
|
status: nearIntentsStatus.status || 'unknown',
|
||||||
|
label: nearIntentsStatus.label || nearIntentsStatus.status || 'unknown',
|
||||||
|
observed_at: nearIntentsStatus.observed_at || null,
|
||||||
|
decisive_reason: nearIntentsStatus.decisive_reason || null,
|
||||||
|
current_incident_count: nearIntentsStatus.current_incident_count || 0,
|
||||||
|
affected_services: nearIntentsStatus.affected_services || [],
|
||||||
|
quoting_stopped: nearIntentsStatus.quoting_stopped ?? null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function buildServiceSummary(service, state) {
|
function buildServiceSummary(service, state) {
|
||||||
switch (service) {
|
switch (service) {
|
||||||
case 'near-intents-ingest':
|
case 'near-intents-ingest':
|
||||||
|
|
|
||||||
|
|
@ -105,6 +105,10 @@ const DEFAULTS = {
|
||||||
operatorDashboardQuoteLimit: 10,
|
operatorDashboardQuoteLimit: 10,
|
||||||
operatorDashboardTradePageSize: 20,
|
operatorDashboardTradePageSize: 20,
|
||||||
operatorDashboardUpstreamTimeoutMs: 3_000,
|
operatorDashboardUpstreamTimeoutMs: 3_000,
|
||||||
|
nearIntentsStatusServicesUrl: 'https://status.near-intents.org/api/services',
|
||||||
|
nearIntentsStatusPostsUrl: 'https://status.near-intents.org/api/posts?is_featured=true&limit=500',
|
||||||
|
nearIntentsStatusPostEnumsUrl: 'https://status.near-intents.org/api/post_enums',
|
||||||
|
nearIntentsStatusTimeoutMs: 3_000,
|
||||||
notificationNtfyBaseUrl: '',
|
notificationNtfyBaseUrl: '',
|
||||||
notificationNtfyTopic: 'unrip',
|
notificationNtfyTopic: 'unrip',
|
||||||
notificationNtfyToken: '',
|
notificationNtfyToken: '',
|
||||||
|
|
@ -575,6 +579,16 @@ export function loadConfig({ envPath = '.env' } = {}) {
|
||||||
process.env.OPERATOR_DASHBOARD_UPSTREAM_TIMEOUT_MS,
|
process.env.OPERATOR_DASHBOARD_UPSTREAM_TIMEOUT_MS,
|
||||||
DEFAULTS.operatorDashboardUpstreamTimeoutMs,
|
DEFAULTS.operatorDashboardUpstreamTimeoutMs,
|
||||||
),
|
),
|
||||||
|
nearIntentsStatusServicesUrl:
|
||||||
|
process.env.NEAR_INTENTS_STATUS_SERVICES_URL || DEFAULTS.nearIntentsStatusServicesUrl,
|
||||||
|
nearIntentsStatusPostsUrl:
|
||||||
|
process.env.NEAR_INTENTS_STATUS_POSTS_URL || DEFAULTS.nearIntentsStatusPostsUrl,
|
||||||
|
nearIntentsStatusPostEnumsUrl:
|
||||||
|
process.env.NEAR_INTENTS_STATUS_POST_ENUMS_URL || DEFAULTS.nearIntentsStatusPostEnumsUrl,
|
||||||
|
nearIntentsStatusTimeoutMs: parseNumber(
|
||||||
|
process.env.NEAR_INTENTS_STATUS_TIMEOUT_MS,
|
||||||
|
DEFAULTS.nearIntentsStatusTimeoutMs,
|
||||||
|
),
|
||||||
notificationNtfyBaseUrl:
|
notificationNtfyBaseUrl:
|
||||||
process.env.NOTIFICATION_NTFY_BASE_URL || DEFAULTS.notificationNtfyBaseUrl,
|
process.env.NOTIFICATION_NTFY_BASE_URL || DEFAULTS.notificationNtfyBaseUrl,
|
||||||
notificationNtfyTopic:
|
notificationNtfyTopic:
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,15 @@ export default function ServiceCard({ service }) {
|
||||||
<div>{`Armed ${formatBoolean(service.armed)}`}</div>
|
<div>{`Armed ${formatBoolean(service.armed)}`}</div>
|
||||||
<div>{`Freshness ${freshnessAge}${freshnessAge === 'Unavailable' ? '' : ' ago'}`}</div>
|
<div>{`Freshness ${freshnessAge}${freshnessAge === 'Unavailable' ? '' : ' ago'}`}</div>
|
||||||
<div>{`Freshness at ${formatTimestamp(service.freshness_at)}`}</div>
|
<div>{`Freshness at ${formatTimestamp(service.freshness_at)}`}</div>
|
||||||
|
{service.upstream_status ? (
|
||||||
|
<>
|
||||||
|
<div>{`Upstream ${service.upstream_status.label || service.upstream_status.status || 'unknown'}`}</div>
|
||||||
|
<div>{`Upstream at ${formatTimestamp(service.upstream_status.observed_at)}`}</div>
|
||||||
|
{service.upstream_status.decisive_reason ? (
|
||||||
|
<div>{service.upstream_status.decisive_reason}</div>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
<div className="mono">{service.base_url}</div>
|
<div className="mono">{service.base_url}</div>
|
||||||
{service.last_error ? <div>{JSON.stringify(service.last_error)}</div> : null}
|
{service.last_error ? <div>{JSON.stringify(service.last_error)}</div> : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,10 @@ function statusSubtitle(label, status, websocketState) {
|
||||||
return formatTimestamp(status.market_observed_at);
|
return formatTimestamp(status.market_observed_at);
|
||||||
case 'Inventory Freshness':
|
case 'Inventory Freshness':
|
||||||
return formatTimestamp(status.inventory_observed_at);
|
return formatTimestamp(status.inventory_observed_at);
|
||||||
|
case 'NEAR Intents':
|
||||||
|
return status.near_intents_upstream_observed_at
|
||||||
|
? `Official status at ${formatTimestamp(status.near_intents_upstream_observed_at)}`
|
||||||
|
: 'Official status page';
|
||||||
case SUBMISSION_COPY.statusTileLabel:
|
case SUBMISSION_COPY.statusTileLabel:
|
||||||
return SUBMISSION_COPY.statusTileSubtitle;
|
return SUBMISSION_COPY.statusTileSubtitle;
|
||||||
default:
|
default:
|
||||||
|
|
@ -17,8 +21,16 @@ function statusSubtitle(label, status, websocketState) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function StatusBar({ status, websocketState }) {
|
export default function StatusBar({ status, websocketState }) {
|
||||||
|
const nearIntentsTile = status.near_intents_upstream_label
|
||||||
|
? [[
|
||||||
|
'NEAR Intents',
|
||||||
|
status.near_intents_upstream_label,
|
||||||
|
status.near_intents_upstream_reason || status.near_intents_upstream_label,
|
||||||
|
]]
|
||||||
|
: [];
|
||||||
const tiles = [
|
const tiles = [
|
||||||
['Pair', truncateMiddle(status.active_pair, 40), status.active_pair],
|
['Pair', truncateMiddle(status.active_pair, 40), status.active_pair],
|
||||||
|
...nearIntentsTile,
|
||||||
['Portfolio', formatEur(status.current_total_portfolio_value_eure)],
|
['Portfolio', formatEur(status.current_total_portfolio_value_eure)],
|
||||||
['Reference BTC/EUR', formatEur(status.latest_reference_price_eure_per_btc)],
|
['Reference BTC/EUR', formatEur(status.latest_reference_price_eure_per_btc)],
|
||||||
['Market Freshness', formatAge(status.market_freshness_ms)],
|
['Market Freshness', formatAge(status.market_freshness_ms)],
|
||||||
|
|
|
||||||
83
test/near-intents-status.test.mjs
Normal file
83
test/near-intents-status.test.mjs
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
|
||||||
|
import { normalizeNearIntentsStatus } from '../src/core/near-intents-status.mjs';
|
||||||
|
|
||||||
|
const postEnumsResponse = {
|
||||||
|
post_enums: [
|
||||||
|
{ id: 'PSCS3IV', post_enum_type: 'status', name: 'investigating' },
|
||||||
|
{ id: 'PP34365', post_enum_type: 'status', name: 'detected' },
|
||||||
|
{ id: 'P8TG2TF', post_enum_type: 'status', name: 'resolved' },
|
||||||
|
{ id: 'P187122', post_enum_type: 'severity', name: 'minor' },
|
||||||
|
{ id: 'PCIGMKW', post_enum_type: 'severity', name: 'degraded' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const servicesResponse = {
|
||||||
|
services: [
|
||||||
|
{ id: 'PXQFSY1', name: 'Cross-Chain Bridging', display_name: 'Cross-Chain Bridging' },
|
||||||
|
{ id: 'PLT88AT', name: 'Solvers Network', display_name: 'Solvers Network' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
test('NEAR Intents status normalizer exposes current quoting disruption as upstream paused evidence', () => {
|
||||||
|
const normalized = normalizeNearIntentsStatus({
|
||||||
|
observedAt: '2026-04-16T12:40:00.000Z',
|
||||||
|
servicesResponse,
|
||||||
|
postEnumsResponse,
|
||||||
|
postsResponse: {
|
||||||
|
posts: [{
|
||||||
|
id: 'PM7LK6N',
|
||||||
|
title: '1Click Quoting is temporarily stopped',
|
||||||
|
post_type: 'incident',
|
||||||
|
latest_update: {
|
||||||
|
status_id: 'PSCS3IV',
|
||||||
|
severity_id: 'P187122',
|
||||||
|
reported_at: 1776342420000,
|
||||||
|
impacts: [{ service_id: 'PXQFSY1', severity_id: 'PCIGMKW' }],
|
||||||
|
message: '<p>The protocol is paused due to a security incident. Swaps are paused.</p>',
|
||||||
|
},
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(normalized.source, 'near_intents_status_page');
|
||||||
|
assert.equal(normalized.status, 'disrupted');
|
||||||
|
assert.equal(normalized.label, 'upstream paused');
|
||||||
|
assert.equal(normalized.quoting_stopped, true);
|
||||||
|
assert.deepEqual(normalized.affected_services, ['Cross-Chain Bridging']);
|
||||||
|
assert.equal(normalized.current_incident_count, 1);
|
||||||
|
assert.equal(normalized.current_incidents[0].status, 'investigating');
|
||||||
|
assert.equal(normalized.current_incidents[0].severity, 'minor');
|
||||||
|
assert.equal(normalized.current_incidents[0].impacts[0].severity, 'degraded');
|
||||||
|
assert.match(normalized.decisive_reason, /1Click Quoting is temporarily stopped/);
|
||||||
|
assert.match(normalized.decisive_reason, /Swaps are paused/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('resolved NEAR Intents status posts do not make the relay look disrupted', () => {
|
||||||
|
const normalized = normalizeNearIntentsStatus({
|
||||||
|
observedAt: '2026-04-16T13:00:00.000Z',
|
||||||
|
servicesResponse,
|
||||||
|
postEnumsResponse,
|
||||||
|
postsResponse: {
|
||||||
|
posts: [{
|
||||||
|
id: 'PM7LK6N',
|
||||||
|
title: '1Click Quoting is temporarily stopped',
|
||||||
|
post_type: 'incident',
|
||||||
|
latest_update: {
|
||||||
|
status_id: 'P8TG2TF',
|
||||||
|
severity_id: 'P187122',
|
||||||
|
reported_at: 1776346020000,
|
||||||
|
impacts: [{ service_id: 'PXQFSY1', severity_id: 'PCIGMKW' }],
|
||||||
|
message: '<p>Resolved.</p>',
|
||||||
|
},
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(normalized.status, 'operational');
|
||||||
|
assert.equal(normalized.label, 'operational');
|
||||||
|
assert.equal(normalized.quoting_stopped, false);
|
||||||
|
assert.equal(normalized.current_incident_count, 0);
|
||||||
|
assert.match(normalized.decisive_reason, /no active incident/i);
|
||||||
|
});
|
||||||
|
|
@ -6,6 +6,7 @@ const strategySource = readFileSync(new URL('../src/operator-dashboard/static/pa
|
||||||
const fundsSource = readFileSync(new URL('../src/operator-dashboard/static/pages/FundsPage.jsx', import.meta.url), 'utf8');
|
const fundsSource = readFileSync(new URL('../src/operator-dashboard/static/pages/FundsPage.jsx', import.meta.url), 'utf8');
|
||||||
const stylesSource = readFileSync(new URL('../src/operator-dashboard/static/styles.css', import.meta.url), 'utf8');
|
const stylesSource = readFileSync(new URL('../src/operator-dashboard/static/styles.css', import.meta.url), 'utf8');
|
||||||
const serviceCardSource = readFileSync(new URL('../src/operator-dashboard/static/components/ServiceCard.jsx', import.meta.url), 'utf8');
|
const serviceCardSource = readFileSync(new URL('../src/operator-dashboard/static/components/ServiceCard.jsx', import.meta.url), 'utf8');
|
||||||
|
const statusBarSource = readFileSync(new URL('../src/operator-dashboard/static/components/StatusBar.jsx', import.meta.url), 'utf8');
|
||||||
|
|
||||||
test('strategy page owns consolidated quote lifecycle and successful trade tables', () => {
|
test('strategy page owns consolidated quote lifecycle and successful trade tables', () => {
|
||||||
assert.match(strategySource, /Quote lifecycle/);
|
assert.match(strategySource, /Quote lifecycle/);
|
||||||
|
|
@ -43,3 +44,14 @@ test('mobile status bar uses normal document flow instead of sticky viewport pos
|
||||||
/@media \(max-width: 720px\)[\s\S]*?\.status-bar \{[\s\S]*?position: static;[\s\S]*?top: auto;[\s\S]*?z-index: auto;[\s\S]*?\}/,
|
/@media \(max-width: 720px\)[\s\S]*?\.status-bar \{[\s\S]*?position: static;[\s\S]*?top: auto;[\s\S]*?z-index: auto;[\s\S]*?\}/,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
test('dashboard UI exposes official NEAR upstream status separately from local freshness', () => {
|
||||||
|
assert.match(statusBarSource, /NEAR Intents/);
|
||||||
|
assert.match(statusBarSource, /near_intents_upstream_label/);
|
||||||
|
assert.match(statusBarSource, /near_intents_upstream_reason/);
|
||||||
|
assert.match(statusBarSource, /Official status at/);
|
||||||
|
assert.match(serviceCardSource, /upstream_status/);
|
||||||
|
assert.match(serviceCardSource, /Upstream at/);
|
||||||
|
assert.match(serviceCardSource, /decisive_reason/);
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -1533,3 +1533,87 @@ test('own request dashboard rows do not label relay accepted evidence as complet
|
||||||
/successful trade|completed trade|asset delta/i,
|
/successful trade|completed trade|asset delta/i,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
test('dashboard surfaces NEAR upstream disruption without calling submitted work completed', () => {
|
||||||
|
const config = buildConfig();
|
||||||
|
const nearIntentsStatus = {
|
||||||
|
source: 'near_intents_status_page',
|
||||||
|
status: 'disrupted',
|
||||||
|
label: 'upstream paused',
|
||||||
|
observed_at: '2026-04-16T12:40:00.000Z',
|
||||||
|
decisive_reason: '1Click Quoting is temporarily stopped: The protocol is paused.',
|
||||||
|
current_incident_count: 1,
|
||||||
|
affected_services: ['Cross-Chain Bridging'],
|
||||||
|
quoting_stopped: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const dashboard = buildDashboardBootstrap({
|
||||||
|
config,
|
||||||
|
auth: { authenticated: true },
|
||||||
|
portfolioMetric: null,
|
||||||
|
inventorySnapshot: null,
|
||||||
|
marketPrice: null,
|
||||||
|
recentQuotes: [],
|
||||||
|
submissionPage: { page: 1, page_size: 20, total: 0, total_pages: 1, items: [] },
|
||||||
|
submissionSummary: { total: 0, last_submission_at: null },
|
||||||
|
fundingObservations: [],
|
||||||
|
recentDepositStatuses: [],
|
||||||
|
recentTradeDecisions: [],
|
||||||
|
recentExecuteTradeCommands: [],
|
||||||
|
recentExecutionResults: [{
|
||||||
|
command_id: 'cmd-submitted',
|
||||||
|
decision_id: 'decision-submitted',
|
||||||
|
quote_id: 'quote-submitted',
|
||||||
|
status: 'submitted',
|
||||||
|
result_code: 'quote_response_ok',
|
||||||
|
}],
|
||||||
|
recentQuoteOutcomes: [],
|
||||||
|
recentIntentRequests: [],
|
||||||
|
recentAlertTransitions: [],
|
||||||
|
nearIntentsStatus,
|
||||||
|
serviceSnapshots: [
|
||||||
|
{
|
||||||
|
service: 'near-intents-ingest',
|
||||||
|
label: 'NEAR Intents Ingest',
|
||||||
|
base_url: 'http://near-intents-ingest',
|
||||||
|
reachable: true,
|
||||||
|
health: { ok: true },
|
||||||
|
state: { ingest: { connected: false, last_message_at: null } },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
service: 'trade-executor',
|
||||||
|
label: 'Trade Executor',
|
||||||
|
base_url: 'http://trade-executor',
|
||||||
|
reachable: true,
|
||||||
|
health: { ok: true },
|
||||||
|
state: { relay: { connected: false } },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
service: 'history-writer',
|
||||||
|
label: 'History Writer',
|
||||||
|
base_url: 'http://history-writer',
|
||||||
|
reachable: true,
|
||||||
|
health: { ok: true },
|
||||||
|
state: {},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(dashboard.status_bar.near_intents_upstream_status, 'disrupted');
|
||||||
|
assert.equal(dashboard.status_bar.near_intents_upstream_label, 'upstream paused');
|
||||||
|
assert.match(dashboard.status_bar.near_intents_upstream_reason, /protocol is paused/);
|
||||||
|
|
||||||
|
const services = Object.fromEntries(
|
||||||
|
dashboard.system.service_health.map((service) => [service.service, service]),
|
||||||
|
);
|
||||||
|
assert.equal(services['near-intents-ingest'].health_status, 'upstream_paused');
|
||||||
|
assert.equal(services['near-intents-ingest'].health_label, 'upstream paused');
|
||||||
|
assert.equal(services['near-intents-ingest'].health_ok, false);
|
||||||
|
assert.match(services['near-intents-ingest'].health_reasons[0], /1Click Quoting/);
|
||||||
|
assert.equal(services['near-intents-ingest'].upstream_status.status, 'disrupted');
|
||||||
|
assert.equal(services['trade-executor'].health_status, 'upstream_paused');
|
||||||
|
assert.equal(services['trade-executor'].upstream_status.quoting_stopped, true);
|
||||||
|
assert.equal(services['history-writer'].health_status, 'online');
|
||||||
|
assert.equal(services['history-writer'].upstream_status, null);
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue