Files
explorer-monorepo/frontend/src/components/explorer/BridgeMonitoringPage.tsx
defiQUG ab9c1f9f98
Some checks failed
Deploy Explorer Live / deploy (push) Failing after 15s
Validate Explorer / frontend (push) Failing after 20s
Validate Explorer / smoke-e2e (push) Has been skipped
Ship bridge lanes, public API access doc, and WalletConnect client stack.
Align CCIP catalog UX with 11-lane config-ready routes, document the no-key public API decision, and enable browser WalletConnect pairing with backend session registration and deploy-time project ID wiring.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 02:21:37 -07:00

512 lines
20 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 { statsApi, type ExplorerStats } from '@/services/api/stats'
import { explorerFeaturePages } from '@/data/explorerOperations'
import { summarizeChainActivity } from '@/utils/activityContext'
import ActivityContextPanel from '@/components/common/ActivityContextPanel'
import FreshnessTrustNote from '@/components/common/FreshnessTrustNote'
import SubsystemPosturePanel from '@/components/common/SubsystemPosturePanel'
import { resolveEffectiveFreshness } from '@/utils/explorerFreshness'
import { bridgeRoutesApi, normalizeBridgeRouteEntries, type BridgeRoutesResponse } from '@/services/api/bridgeRoutes'
import { createVisibilityAwarePoller } from '@/utils/visibilityRefresh'
import { HOME_DASHBOARD_REFRESH_MS } from '@/utils/featuredTokens'
import OperationsSurfaceNav from './OperationsSurfaceNav'
import OperationsActionGrid from './OperationsActionGrid'
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
issueScope: string | null
issueMessage: string | null
inventoryShortfallWei: string | null
inventoryRequiredWei: string | null
inventoryAvailableWei: string | null
}
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 relayPolicyCue(snapshot: MissionControlRelaySnapshot | null): string | null {
if (!snapshot) return null
if (snapshot.last_error?.scope === 'bridge_inventory') {
return 'Queued release waiting on bridge inventory'
}
if (snapshot.last_error?.scope === 'bridge_inventory_probe') {
return 'Bridge inventory check is temporarily unavailable'
}
if (String(snapshot.status || '').toLowerCase() === 'paused' && snapshot.monitoring?.delivery_enabled === false) {
return 'Delivery disabled by policy'
}
return 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 formatWeiToToken(value?: string | null): string {
if (!value) return 'Unknown'
try {
const raw = BigInt(value)
const whole = raw / 10n ** 18n
const fractional = (raw % 10n ** 18n).toString().padStart(18, '0').slice(0, 6).replace(/0+$/, '')
return fractional ? `${whole.toString()}.${fractional}` : whole.toString()
} catch {
return value
}
}
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({
initialBridgeStatus = null,
initialStats = null,
}: {
initialBridgeStatus?: MissionControlBridgeStatusResponse | null
initialStats?: ExplorerStats | null
}) {
const [bridgeStatus, setBridgeStatus] = useState<MissionControlBridgeStatusResponse | null>(initialBridgeStatus)
const [stats, setStats] = useState<ExplorerStats | null>(initialStats)
const [bridgeRoutes, setBridgeRoutes] = useState<BridgeRoutesResponse | null>(null)
const [feedState, setFeedState] = useState<FeedState>(initialBridgeStatus ? 'fallback' : 'connecting')
const page = explorerFeaturePages.bridge
useEffect(() => {
let cancelled = false
const loadSnapshot = async () => {
try {
const [snapshot, latestStats] = await Promise.all([
missionControlApi.getBridgeStatus(),
statsApi.get().catch(() => null),
])
if (!cancelled) {
setBridgeStatus(snapshot)
if (latestStats) {
setStats(latestStats)
}
}
} 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()
}
}, [])
useEffect(() => {
let cancelled = false
bridgeRoutesApi.getRoutesSafe().then(({ ok, data }) => {
if (!cancelled && ok) {
setBridgeRoutes(data)
}
})
return () => {
cancelled = true
}
}, [])
useEffect(() => {
if (feedState !== 'fallback') return
let cancelled = false
const refreshSnapshot = async () => {
try {
const snapshot = await missionControlApi.getBridgeStatus()
if (!cancelled) {
setBridgeStatus(snapshot)
}
} catch (error) {
if (!cancelled && process.env.NODE_ENV !== 'production') {
console.warn('Failed to refresh bridge monitoring snapshot:', error)
}
}
}
return createVisibilityAwarePoller({
intervalMs: HOME_DASHBOARD_REFRESH_MS,
task: refreshSnapshot,
})
}, [feedState])
const routeEntries = useMemo(
() => normalizeBridgeRouteEntries(bridgeRoutes?.routes),
[bridgeRoutes?.routes],
)
const activityContext = useMemo(
() =>
summarizeChainActivity({
blocks: [],
transactions: [],
latestBlockNumber: stats?.latest_block,
latestBlockTimestamp: null,
freshness: resolveEffectiveFreshness(stats, bridgeStatus),
diagnostics: stats?.diagnostics ?? bridgeStatus?.data?.diagnostics ?? null,
}),
[bridgeStatus, stats],
)
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:
snapshot?.last_error?.scope === 'bridge_inventory'
? 'underfunded'
: snapshot?.last_error?.scope === 'bridge_inventory_probe'
? 'warning'
: 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 ||
'',
issueScope: snapshot?.last_error?.scope || null,
issueMessage: snapshot?.last_error?.message || null,
inventoryShortfallWei: snapshot?.last_error?.shortfall || null,
inventoryRequiredWei: snapshot?.last_error?.required_amount || null,
inventoryAvailableWei: snapshot?.last_error?.available_amount || null,
}
})
.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}
<OperationsSurfaceNav />
<div className="mb-6">
<ActivityContextPanel context={activityContext} title="Bridge Freshness Context" />
<FreshnessTrustNote
className="mt-3"
context={activityContext}
stats={stats}
bridgeStatus={bridgeStatus}
scopeLabel="Bridge relay posture is shown alongside the same explorer freshness model used on the homepage and core explorer routes"
/>
<SubsystemPosturePanel
className="mt-3"
subsystems={bridgeStatus?.data?.subsystems}
title="Bridge Subsystem Posture"
preferredKeys={['rpc_head', 'tx_index', 'bridge_relay_monitoring', 'stats_summary', 'freshness_queries']}
scopeLabel="These bridge-facing subsystem signals show whether the limiting factor is public head visibility, transaction indexing, relay monitoring, or degraded freshness queries."
/>
</div>
<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>
{relayPolicyCue(resolveSnapshot((getMissionControlRelays(bridgeStatus) || {})[lane.key])) ? (
<div className="mt-3 text-xs font-medium uppercase tracking-wide text-amber-700 dark:text-amber-300">
{relayPolicyCue(resolveSnapshot((getMissionControlRelays(bridgeStatus) || {})[lane.key]))}
</div>
) : null}
{lane.issueScope === 'bridge_inventory' ? (
<div className="mt-3 rounded-2xl border border-amber-200 bg-amber-50/80 p-3 text-sm text-amber-950 dark:border-amber-900/60 dark:bg-amber-950/30 dark:text-amber-100">
<div className="font-semibold">Bridge inventory below required release amount</div>
<div className="mt-1">
Shortfall: {formatWeiToToken(lane.inventoryShortfallWei)} WETH
</div>
<div className="mt-1 text-xs opacity-80">
Required: {formatWeiToToken(lane.inventoryRequiredWei)} WETH · Available: {formatWeiToToken(lane.inventoryAvailableWei)} WETH
</div>
</div>
) : null}
</Card>
))}
</div>
{routeEntries.length > 0 ? (
<Card title="CCIP route catalog" className="mb-8">
<p className="mb-4 text-sm text-gray-600 dark:text-gray-400">
Destination bridge contracts from{' '}
<code className="rounded bg-gray-100 px-1.5 py-0.5 text-xs dark:bg-gray-900">
/token-aggregation/api/v1/bridge/routes
</code>
{bridgeRoutes?.source ? (
<>
{' '}
(source: {bridgeRoutes.source}
{bridgeRoutes.lastModified ? ` · updated ${relativeAge(bridgeRoutes.lastModified)}` : ''})
</>
) : null}
. Gnosis, Cronos, Celo, and Wemix lanes are aligned to deployed CCIP receivers fund LINK on each remote bridge before live traffic.
</p>
<p className="mb-4 text-sm text-gray-600 dark:text-gray-400">
Operator runbook:{' '}
<Link href="/docs/public-api-access" className="text-primary-600 hover:underline">
public API access
</Link>{' '}
· config-ready chain completion in repo{' '}
<code className="rounded bg-gray-100 px-1.5 py-0.5 text-xs dark:bg-gray-900">CONFIG_READY_CHAINS_COMPLETION_RUNBOOK.md</code>
</p>
<div className="overflow-x-auto">
<table className="min-w-full text-sm">
<thead>
<tr className="border-b border-gray-200 text-left text-xs uppercase tracking-wide text-gray-500 dark:border-gray-700 dark:text-gray-400">
<th className="py-2 pr-4">Bridge</th>
<th className="py-2 pr-4">Destination</th>
<th className="py-2">Contract</th>
</tr>
</thead>
<tbody>
{routeEntries.map((entry) => (
<tr key={`${entry.bridge}-${entry.destination}`} className="border-b border-gray-100 last:border-0 dark:border-gray-800">
<td className="py-2 pr-4 font-medium text-gray-900 dark:text-white">{entry.bridge}</td>
<td className="py-2 pr-4 text-gray-700 dark:text-gray-300">{entry.destination}</td>
<td className="py-2 font-mono text-xs text-gray-600 dark:text-gray-400">{entry.address}</td>
</tr>
))}
</tbody>
</table>
</div>
</Card>
) : null}
<OperationsActionGrid actions={page.actions} />
</div>
)
}