Files
explorer-monorepo/frontend/src/components/explorer/BridgeMonitoringPage.tsx
defiQUG 6eef6b07f6 feat: explorer API, wallet, CCIP scripts, and config refresh
- Backend REST/gateway/track routes, analytics, Blockscout proxy paths.
- Frontend wallet and liquidity surfaces; MetaMask token list alignment.
- Deployment docs, verification scripts, address inventory updates.

Check: go build ./... under backend/ (pass).
Made-with: Cursor
2026-04-07 23:22:12 -07:00

327 lines
12 KiB
TypeScript

import { useEffect, useMemo, useState } from 'react'
import Link from 'next/link'
import { Card } from '@/libs/frontend-ui-primitives'
import {
getMissionControlRelayLabel,
getMissionControlRelays,
missionControlApi,
type MissionControlBridgeStatusResponse,
type MissionControlRelayPayload,
type MissionControlRelaySnapshot,
} from '@/services/api/missionControl'
import { explorerFeaturePages } from '@/data/explorerOperations'
type FeedState = 'connecting' | 'live' | 'fallback'
interface RelayLaneCard {
key: string
label: string
status: string
profile: string
sourceChain: string
destinationChain: string
queueSize: number
processed: number
failed: number
lastPolled: string
bridgeAddress: string
}
const relayOrder = ['mainnet_cw', 'mainnet_weth', 'bsc', 'avax', 'avax_cw', 'avax_to_138']
function relativeAge(isoString?: string): string {
if (!isoString) return 'Unknown'
const parsed = Date.parse(isoString)
if (!Number.isFinite(parsed)) return 'Unknown'
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`
}
function shortAddress(value?: string): string {
if (!value) return 'Unspecified'
if (value.length <= 14) return value
return `${value.slice(0, 6)}...${value.slice(-4)}`
}
function resolveSnapshot(relay?: MissionControlRelayPayload): MissionControlRelaySnapshot | null {
return relay?.url_probe?.body || relay?.file_snapshot || null
}
function laneToneClasses(status: string): string {
const normalized = status.toLowerCase()
if (['degraded', 'stale', 'stopped', 'down', 'snapshot-error'].includes(normalized)) {
return 'border-red-200 bg-red-50/80 dark:border-red-900/60 dark:bg-red-950/20'
}
if (['paused', 'starting'].includes(normalized)) {
return 'border-amber-200 bg-amber-50/80 dark:border-amber-900/60 dark:bg-amber-950/20'
}
return 'border-emerald-200 bg-emerald-50/80 dark:border-emerald-900/60 dark:bg-emerald-950/20'
}
function statusPillClasses(status: string): string {
const normalized = status.toLowerCase()
if (['degraded', 'stale', 'stopped', 'down', 'snapshot-error'].includes(normalized)) {
return 'bg-red-100 text-red-700 dark:bg-red-900/50 dark:text-red-100'
}
if (['paused', 'starting'].includes(normalized)) {
return 'bg-amber-100 text-amber-800 dark:bg-amber-900/50 dark:text-amber-100'
}
return 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/50 dark:text-emerald-100'
}
function ActionLink({
href,
label,
external,
}: {
href: string
label: string
external?: boolean
}) {
const className = 'inline-flex items-center text-sm font-semibold text-primary-600 hover:underline'
const text = `${label} ->`
if (external) {
return (
<a href={href} className={className} target="_blank" rel="noopener noreferrer">
{text}
</a>
)
}
return (
<Link href={href} className={className}>
{text}
</Link>
)
}
export default function BridgeMonitoringPage() {
const [bridgeStatus, setBridgeStatus] = useState<MissionControlBridgeStatusResponse | null>(null)
const [feedState, setFeedState] = useState<FeedState>('connecting')
const page = explorerFeaturePages.bridge
useEffect(() => {
let cancelled = false
const loadSnapshot = async () => {
try {
const snapshot = await missionControlApi.getBridgeStatus()
if (!cancelled) {
setBridgeStatus(snapshot)
}
} catch (error) {
if (!cancelled && process.env.NODE_ENV !== 'production') {
console.warn('Failed to load bridge monitoring snapshot:', error)
}
}
}
loadSnapshot()
const unsubscribe = missionControlApi.subscribeBridgeStatus(
(status) => {
if (!cancelled) {
setBridgeStatus(status)
setFeedState('live')
}
},
(error) => {
if (!cancelled) {
setFeedState('fallback')
}
if (process.env.NODE_ENV !== 'production') {
console.warn('Bridge monitoring live stream issue:', error)
}
}
)
return () => {
cancelled = true
unsubscribe()
}
}, [])
const relayLanes = useMemo((): RelayLaneCard[] => {
const relays = getMissionControlRelays(bridgeStatus)
if (!relays) return []
const orderIndex = new Map(relayOrder.map((key, index) => [key, index]))
return Object.entries(relays)
.map(([key, relay]) => {
const snapshot = resolveSnapshot(relay)
const status = String(snapshot?.status || (relay.file_snapshot_error ? 'snapshot-error' : 'configured')).toLowerCase()
return {
key,
label: getMissionControlRelayLabel(key),
status,
profile: snapshot?.service?.profile || key,
sourceChain: snapshot?.source?.chain_name || 'Unknown',
destinationChain: snapshot?.destination?.chain_name || 'Unknown',
queueSize: snapshot?.queue?.size ?? 0,
processed: snapshot?.queue?.processed ?? 0,
failed: snapshot?.queue?.failed ?? 0,
lastPolled: relativeAge(snapshot?.last_source_poll?.at),
bridgeAddress:
snapshot?.destination?.relay_bridge_default ||
snapshot?.destination?.relay_bridge ||
snapshot?.source?.bridge_filter ||
'',
}
})
.sort((left, right) => {
const leftIndex = orderIndex.get(left.key) ?? Number.MAX_SAFE_INTEGER
const rightIndex = orderIndex.get(right.key) ?? Number.MAX_SAFE_INTEGER
return leftIndex - rightIndex || left.label.localeCompare(right.label)
})
}, [bridgeStatus])
const chainStatus = bridgeStatus?.data?.chains?.['138']
const overallStatus = bridgeStatus?.data?.status || 'unknown'
const checkedAt = relativeAge(bridgeStatus?.data?.checked_at)
return (
<div className="container mx-auto px-4 py-6 sm:py-8">
<div className="mb-6 max-w-4xl sm:mb-8">
<div className="mb-3 inline-flex rounded-full border border-sky-200 bg-sky-50 px-3 py-1 text-xs font-semibold uppercase tracking-[0.2em] text-sky-700">
{page.eyebrow}
</div>
<h1 className="mb-3 text-3xl font-bold text-gray-900 dark:text-white sm:text-4xl">
{page.title}
</h1>
<p className="text-base leading-7 text-gray-600 dark:text-gray-400 sm:text-lg sm:leading-8">
{page.description}
</p>
</div>
{page.note ? (
<Card className="mb-6 border border-amber-200 bg-amber-50/70 dark:border-amber-900/50 dark:bg-amber-950/20">
<p className="text-sm leading-6 text-amber-950 dark:text-amber-100">
{page.note}
</p>
</Card>
) : null}
<div className="mb-6 grid gap-4 lg:grid-cols-3">
<Card className="border border-sky-200 bg-sky-50/70 dark:border-sky-900/50 dark:bg-sky-950/20">
<div className="text-sm font-semibold uppercase tracking-wide text-sky-800 dark:text-sky-100">
Relay Fleet
</div>
<div className="mt-2 text-2xl font-bold text-gray-900 dark:text-white">
{overallStatus}
</div>
<div className="mt-2 text-sm text-gray-700 dark:text-gray-300">
{relayLanes.length} managed lanes visible
</div>
<div className="mt-2 text-xs font-medium uppercase tracking-wide text-sky-800/80 dark:text-sky-100/80">
Feed: {feedState === 'live' ? 'Live SSE' : feedState === 'fallback' ? 'Snapshot fallback' : 'Connecting'}
</div>
</Card>
<Card className="border border-gray-200 dark:border-gray-700">
<div className="text-sm font-semibold uppercase tracking-wide text-gray-700 dark:text-gray-300">
Chain 138 RPC
</div>
<div className="mt-2 text-2xl font-bold text-gray-900 dark:text-white">
{chainStatus?.status || 'unknown'}
</div>
<div className="mt-2 text-sm text-gray-600 dark:text-gray-400">
Head age: {chainStatus?.head_age_sec != null ? `${chainStatus.head_age_sec.toFixed(1)}s` : 'Unknown'}
</div>
<div className="mt-1 text-sm text-gray-600 dark:text-gray-400">
Latency: {chainStatus?.latency_ms != null ? `${chainStatus.latency_ms}ms` : 'Unknown'}
</div>
</Card>
<Card className="border border-gray-200 dark:border-gray-700">
<div className="text-sm font-semibold uppercase tracking-wide text-gray-700 dark:text-gray-300">
Last Check
</div>
<div className="mt-2 text-2xl font-bold text-gray-900 dark:text-white">
{checkedAt}
</div>
<div className="mt-2 text-sm text-gray-600 dark:text-gray-400">
Public status JSON and live stream are both active.
</div>
<div className="mt-4">
<ActionLink href="/explorer-api/v1/track1/bridge/status" label="Open status JSON" external />
</div>
</Card>
</div>
<div className="mb-8 grid gap-4 lg:grid-cols-2 xl:grid-cols-3">
{relayLanes.map((lane) => (
<Card key={lane.key} className={`border ${laneToneClasses(lane.status)}`}>
<div className="flex items-start justify-between gap-3">
<div>
<div className="text-lg font-semibold text-gray-900 dark:text-white">
{lane.label}
</div>
<div className="mt-1 text-sm text-gray-600 dark:text-gray-400">
{`${lane.sourceChain} -> ${lane.destinationChain}`}
</div>
</div>
<div className={`rounded-full px-3 py-1 text-xs font-semibold uppercase tracking-wide ${statusPillClasses(lane.status)}`}>
{lane.status}
</div>
</div>
<div className="mt-4 grid grid-cols-2 gap-3 text-sm">
<div>
<div className="text-gray-500 dark:text-gray-400">Profile</div>
<div className="font-medium text-gray-900 dark:text-white">{lane.profile}</div>
</div>
<div>
<div className="text-gray-500 dark:text-gray-400">Queue</div>
<div className="font-medium text-gray-900 dark:text-white">{lane.queueSize}</div>
</div>
<div>
<div className="text-gray-500 dark:text-gray-400">Processed</div>
<div className="font-medium text-gray-900 dark:text-white">{lane.processed}</div>
</div>
<div>
<div className="text-gray-500 dark:text-gray-400">Failed</div>
<div className="font-medium text-gray-900 dark:text-white">{lane.failed}</div>
</div>
</div>
<div className="mt-4 text-sm text-gray-600 dark:text-gray-400">
Last polled: {lane.lastPolled}
</div>
<div className="mt-1 text-sm text-gray-600 dark:text-gray-400">
Bridge: {shortAddress(lane.bridgeAddress)}
</div>
</Card>
))}
</div>
<div className="grid gap-4 lg:grid-cols-2">
{page.actions.map((action) => (
<Card key={`${action.title}-${action.href}`} className="border border-gray-200 dark:border-gray-700">
<div className="flex h-full flex-col">
<div className="text-base font-semibold text-gray-900 dark:text-white">
{action.title}
</div>
<p className="mt-2 flex-1 text-sm leading-6 text-gray-600 dark:text-gray-400">
{action.description}
</p>
<div className="mt-4">
<ActionLink
href={action.href}
label={action.label}
external={'external' in action ? action.external : undefined}
/>
</div>
</div>
</Card>
))}
</div>
</div>
)
}