- Updated branding from "SolaceScanScout" to "Solace" across various files including deployment scripts, API responses, and documentation. - Changed default base URL for Playwright tests and updated security headers to reflect the new branding. - Enhanced README and API documentation to include new authentication endpoints and product access details. This refactor aligns the project branding and improves clarity in the API documentation.
257 lines
7.0 KiB
TypeScript
257 lines
7.0 KiB
TypeScript
import { getExplorerApiBase } from './blockscout'
|
|
|
|
export interface MissionControlRelaySummary {
|
|
text: string
|
|
tone: 'normal' | 'warning' | 'danger'
|
|
items: MissionControlRelayItemSummary[]
|
|
}
|
|
|
|
export interface MissionControlRelayItemSummary {
|
|
key: string
|
|
label: string
|
|
status: string
|
|
text: string
|
|
tone: 'normal' | 'warning' | 'danger'
|
|
}
|
|
|
|
export interface MissionControlRelaySnapshot {
|
|
status?: string
|
|
service?: {
|
|
profile?: string
|
|
}
|
|
source?: {
|
|
chain_name?: string
|
|
bridge_filter?: string
|
|
}
|
|
destination?: {
|
|
chain_name?: string
|
|
relay_bridge?: string
|
|
relay_bridge_default?: string
|
|
}
|
|
queue?: {
|
|
size?: number
|
|
processed?: number
|
|
failed?: number
|
|
}
|
|
last_source_poll?: {
|
|
at?: string
|
|
ok?: boolean
|
|
logs_fetched?: number
|
|
}
|
|
}
|
|
|
|
export interface MissionControlRelayProbe {
|
|
ok?: boolean
|
|
body?: MissionControlRelaySnapshot
|
|
}
|
|
|
|
export interface MissionControlRelayPayload {
|
|
url_probe?: MissionControlRelayProbe
|
|
file_snapshot?: MissionControlRelaySnapshot
|
|
file_snapshot_error?: string
|
|
}
|
|
|
|
export interface MissionControlChainStatus {
|
|
status?: string
|
|
name?: string
|
|
head_age_sec?: number
|
|
latency_ms?: number
|
|
block_number?: string
|
|
}
|
|
|
|
export interface MissionControlBridgeStatusResponse {
|
|
data?: {
|
|
status?: string
|
|
checked_at?: string
|
|
chains?: Record<string, MissionControlChainStatus>
|
|
ccip_relay?: MissionControlRelayPayload
|
|
ccip_relays?: Record<string, MissionControlRelayPayload>
|
|
}
|
|
}
|
|
|
|
const missionControlRelayLabels: Record<string, string> = {
|
|
mainnet: 'Mainnet',
|
|
mainnet_weth: 'Mainnet WETH',
|
|
mainnet_cw: 'Mainnet cW',
|
|
bsc: 'BSC',
|
|
avax: 'Avalanche',
|
|
avalanche: 'Avalanche',
|
|
avax_cw: 'Avalanche cW',
|
|
avax_to_138: 'Avalanche -> 138',
|
|
}
|
|
|
|
function getMissionControlStreamUrl(): string {
|
|
return `${getExplorerApiBase()}/explorer-api/v1/mission-control/stream`
|
|
}
|
|
|
|
function getMissionControlBridgeStatusUrl(): string {
|
|
return `${getExplorerApiBase()}/explorer-api/v1/track1/bridge/status`
|
|
}
|
|
|
|
function relativeAge(isoString?: string): string {
|
|
if (!isoString) return ''
|
|
const parsed = Date.parse(isoString)
|
|
if (!Number.isFinite(parsed)) return ''
|
|
const seconds = Math.max(0, Math.round((Date.now() - parsed) / 1000))
|
|
if (seconds < 60) return `${seconds}s ago`
|
|
const minutes = Math.round(seconds / 60)
|
|
if (minutes < 60) return `${minutes}m ago`
|
|
const hours = Math.round(minutes / 60)
|
|
return `${hours}h ago`
|
|
}
|
|
|
|
export function summarizeMissionControlRelay(
|
|
response: MissionControlBridgeStatusResponse | null | undefined
|
|
): MissionControlRelaySummary | null {
|
|
const relays = getMissionControlRelays(response)
|
|
if (!relays) return null
|
|
|
|
const items = Object.entries(relays)
|
|
.map(([key, relay]): MissionControlRelayItemSummary | null => {
|
|
const label = getMissionControlRelayLabel(key)
|
|
|
|
if (relay.url_probe && relay.url_probe.ok === false) {
|
|
return {
|
|
key,
|
|
label,
|
|
status: 'down',
|
|
text: `${label}: probe failed`,
|
|
tone: 'danger',
|
|
}
|
|
}
|
|
|
|
if (relay.file_snapshot_error) {
|
|
return {
|
|
key,
|
|
label,
|
|
status: 'snapshot-error',
|
|
text: `${label}: snapshot error`,
|
|
tone: 'danger',
|
|
}
|
|
}
|
|
|
|
const snapshot = relay.url_probe?.body || relay.file_snapshot
|
|
if (!snapshot) {
|
|
return {
|
|
key,
|
|
label,
|
|
status: 'configured',
|
|
text: `${label}: probe configured`,
|
|
tone: 'normal',
|
|
}
|
|
}
|
|
|
|
const status = String(snapshot.status || 'unknown').toLowerCase()
|
|
const destination = snapshot.destination?.chain_name
|
|
const queueSize = snapshot.queue?.size
|
|
const pollAge = relativeAge(snapshot.last_source_poll?.at)
|
|
|
|
let text = `${label}: ${status}`
|
|
if (destination) text += ` -> ${destination}`
|
|
if (queueSize != null) text += ` · queue ${queueSize}`
|
|
if (pollAge) text += ` · polled ${pollAge}`
|
|
|
|
let tone: MissionControlRelaySummary['tone'] = 'normal'
|
|
if (['paused', 'starting'].includes(status)) {
|
|
tone = 'warning'
|
|
}
|
|
if (['degraded', 'stale', 'stopped', 'down'].includes(status)) {
|
|
tone = 'danger'
|
|
}
|
|
|
|
return { key, label, status, text, tone }
|
|
})
|
|
.filter((item): item is MissionControlRelayItemSummary => item !== null)
|
|
|
|
if (items.length === 0) return null
|
|
|
|
const tone: MissionControlRelaySummary['tone'] = items.some((item) => item.tone === 'danger')
|
|
? 'danger'
|
|
: items.some((item) => item.tone === 'warning')
|
|
? 'warning'
|
|
: 'normal'
|
|
|
|
const text =
|
|
items.length === 1
|
|
? items[0].text
|
|
: tone === 'normal'
|
|
? `${items.length} relay lanes operational`
|
|
: `Relay lanes need attention`
|
|
|
|
return { text, tone, items }
|
|
}
|
|
|
|
export function getMissionControlRelays(
|
|
response: MissionControlBridgeStatusResponse | null | undefined
|
|
): Record<string, MissionControlRelayPayload> | null {
|
|
return response?.data?.ccip_relays && Object.keys(response.data.ccip_relays).length > 0
|
|
? response.data.ccip_relays
|
|
: response?.data?.ccip_relay
|
|
? { mainnet: response.data.ccip_relay }
|
|
: null
|
|
}
|
|
|
|
export function getMissionControlRelayLabel(key: string): string {
|
|
return missionControlRelayLabels[key] || key.toUpperCase()
|
|
}
|
|
|
|
export const missionControlApi = {
|
|
getBridgeStatus: async (): Promise<MissionControlBridgeStatusResponse> => {
|
|
const response = await fetch(getMissionControlBridgeStatusUrl())
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP ${response.status}`)
|
|
}
|
|
return (await response.json()) as MissionControlBridgeStatusResponse
|
|
},
|
|
|
|
getRelaySummary: async (): Promise<MissionControlRelaySummary | null> => {
|
|
const json = await missionControlApi.getBridgeStatus()
|
|
return summarizeMissionControlRelay(json)
|
|
},
|
|
|
|
subscribeBridgeStatus: (
|
|
onStatus: (status: MissionControlBridgeStatusResponse) => void,
|
|
onError?: (error: unknown) => void
|
|
): (() => void) => {
|
|
if (typeof window === 'undefined' || typeof window.EventSource === 'undefined') {
|
|
onError?.(new Error('EventSource is not available in this environment'))
|
|
return () => {}
|
|
}
|
|
|
|
const eventSource = new window.EventSource(getMissionControlStreamUrl())
|
|
|
|
const handleMessage = (event: MessageEvent<string>) => {
|
|
try {
|
|
const payload = JSON.parse(event.data) as MissionControlBridgeStatusResponse
|
|
onStatus(payload)
|
|
} catch (error) {
|
|
onError?.(error)
|
|
}
|
|
}
|
|
|
|
eventSource.addEventListener('mission-control', handleMessage)
|
|
eventSource.onmessage = handleMessage
|
|
|
|
eventSource.onerror = () => {
|
|
onError?.(new Error('Mission-control live stream connection lost'))
|
|
}
|
|
|
|
return () => {
|
|
eventSource.removeEventListener('mission-control', handleMessage)
|
|
eventSource.close()
|
|
}
|
|
},
|
|
|
|
subscribeRelaySummary: (
|
|
onSummary: (summary: MissionControlRelaySummary | null) => void,
|
|
onError?: (error: unknown) => void
|
|
): (() => void) => {
|
|
return missionControlApi.subscribeBridgeStatus(
|
|
(payload) => {
|
|
onSummary(summarizeMissionControlRelay(payload))
|
|
},
|
|
onError
|
|
)
|
|
},
|
|
}
|