Defer heavy getServerSideProps on home, operator, addresses, and wallet to cut TTFB; centralize locale-safe formatters with client-only relative times; add compact ops UX, bridge/operator relay pagination, and Playwright route/scroll smoke in deploy. Co-authored-by: Cursor <cursoragent@cursor.com>
336 lines
14 KiB
TypeScript
336 lines
14 KiB
TypeScript
import { useEffect, useMemo, useState } from 'react'
|
|
import Link from 'next/link'
|
|
import { Card } from '@/libs/frontend-ui-primitives'
|
|
import { explorerFeaturePages } from '@/data/explorerOperations'
|
|
import { configApi, type CapabilitiesResponse, type NetworksConfigResponse, type TokenListResponse } from '@/services/api/config'
|
|
import { tokensApi } from '@/services/api/tokens'
|
|
import { getMissionControlRelays, missionControlApi, type MissionControlBridgeStatusResponse } from '@/services/api/missionControl'
|
|
import { routesApi, type RouteMatrixResponse } from '@/services/api/routes'
|
|
import { useUiMode } from '@/components/common/UiModeContext'
|
|
import { summarizeChainActivity } from '@/utils/activityContext'
|
|
import PageIntro from '@/components/common/PageIntro'
|
|
import ExplorerFreshnessDisclosure from '@/components/common/ExplorerFreshnessDisclosure'
|
|
import CompactMetricBar from '@/components/common/CompactMetricBar'
|
|
import DisclosureSection from '@/components/common/DisclosureSection'
|
|
import OperationsSurfaceNav from '@/components/explorer/OperationsSurfaceNav'
|
|
import OperationsActionGrid from '@/components/explorer/OperationsActionGrid'
|
|
import BridgeLaneHealthPanel from '@/components/explorer/BridgeLaneHealthPanel'
|
|
import { resolveEffectiveFreshness } from '@/utils/explorerFreshness'
|
|
import SubsystemPosturePanel from '@/components/common/SubsystemPosturePanel'
|
|
import ClientRelativeTime from '@/components/common/ClientRelativeTime'
|
|
import { statsApi, type ExplorerStats } from '@/services/api/stats'
|
|
|
|
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>
|
|
)
|
|
}
|
|
|
|
interface OperationsHubPageProps {
|
|
initialBridgeStatus?: MissionControlBridgeStatusResponse | null
|
|
initialRouteMatrix?: RouteMatrixResponse | null
|
|
initialNetworksConfig?: NetworksConfigResponse | null
|
|
initialTokenList?: TokenListResponse | null
|
|
initialCapabilities?: CapabilitiesResponse | null
|
|
initialStats?: ExplorerStats | null
|
|
}
|
|
|
|
export default function OperationsHubPage({
|
|
initialBridgeStatus = null,
|
|
initialRouteMatrix = null,
|
|
initialNetworksConfig = null,
|
|
initialTokenList = null,
|
|
initialCapabilities = null,
|
|
initialStats = null,
|
|
}: OperationsHubPageProps) {
|
|
const { mode } = useUiMode()
|
|
const [bridgeStatus, setBridgeStatus] = useState<MissionControlBridgeStatusResponse | null>(initialBridgeStatus)
|
|
const [routeMatrix, setRouteMatrix] = useState<RouteMatrixResponse | null>(initialRouteMatrix)
|
|
const [networksConfig, setNetworksConfig] = useState<NetworksConfigResponse | null>(initialNetworksConfig)
|
|
const [tokenList, setTokenList] = useState<TokenListResponse | null>(initialTokenList)
|
|
const [capabilities, setCapabilities] = useState<CapabilitiesResponse | null>(initialCapabilities)
|
|
const [stats, setStats] = useState<ExplorerStats | null>(initialStats)
|
|
const [loadingError, setLoadingError] = useState<string | null>(null)
|
|
const page = explorerFeaturePages.operations
|
|
|
|
useEffect(() => {
|
|
let cancelled = false
|
|
|
|
const load = async () => {
|
|
const [bridgeResult, routesResult, networksResult, tokenListResult, capabilitiesResult, statsResult] =
|
|
await Promise.allSettled([
|
|
missionControlApi.getBridgeStatus(),
|
|
routesApi.getRouteMatrix(),
|
|
configApi.getNetworks(),
|
|
tokensApi.listForSurface('extended', 138).then(({ ok, data }) => ({ tokens: ok ? data : [] })),
|
|
configApi.getCapabilities(),
|
|
statsApi.get(),
|
|
])
|
|
|
|
if (cancelled) return
|
|
|
|
if (bridgeResult.status === 'fulfilled') setBridgeStatus(bridgeResult.value)
|
|
if (routesResult.status === 'fulfilled') setRouteMatrix(routesResult.value)
|
|
if (networksResult.status === 'fulfilled') setNetworksConfig(networksResult.value)
|
|
if (tokenListResult.status === 'fulfilled') setTokenList(tokenListResult.value)
|
|
if (capabilitiesResult.status === 'fulfilled') setCapabilities(capabilitiesResult.value)
|
|
if (statsResult.status === 'fulfilled') setStats(statsResult.value)
|
|
|
|
const failedCount = [
|
|
bridgeResult,
|
|
routesResult,
|
|
networksResult,
|
|
tokenListResult,
|
|
capabilitiesResult,
|
|
statsResult,
|
|
].filter((result) => result.status === 'rejected').length
|
|
|
|
if (failedCount === 6) {
|
|
setLoadingError('Public explorer operations data is temporarily unavailable.')
|
|
}
|
|
}
|
|
|
|
load().catch((error) => {
|
|
if (!cancelled) {
|
|
setLoadingError(
|
|
error instanceof Error ? error.message : 'Public explorer operations data is temporarily unavailable.'
|
|
)
|
|
}
|
|
})
|
|
|
|
return () => {
|
|
cancelled = true
|
|
}
|
|
}, [])
|
|
|
|
const relayCount = useMemo(() => {
|
|
const relays = getMissionControlRelays(bridgeStatus)
|
|
return relays ? Object.keys(relays).length : 0
|
|
}, [bridgeStatus])
|
|
|
|
const totalQueue = useMemo(() => {
|
|
const relays = getMissionControlRelays(bridgeStatus)
|
|
if (!relays) return 0
|
|
return Object.values(relays).reduce((sum, relay) => {
|
|
const queueSize = relay.url_probe?.body?.queue?.size ?? relay.file_snapshot?.queue?.size ?? 0
|
|
return sum + queueSize
|
|
}, 0)
|
|
}, [bridgeStatus])
|
|
|
|
const tokenChainCoverage = useMemo(() => {
|
|
return new Set((tokenList?.tokens || []).map((token) => token.chainId).filter(Boolean)).size
|
|
}, [tokenList])
|
|
|
|
const topSymbols = useMemo(() => {
|
|
return Array.from(
|
|
new Set((tokenList?.tokens || []).map((token) => token.symbol).filter(Boolean) as string[])
|
|
).slice(0, 8)
|
|
}, [tokenList])
|
|
const laneSummary = useMemo(() => {
|
|
const lanes = bridgeStatus?.data?.bridge_lanes?.lanes || []
|
|
if (lanes.length === 0) return null
|
|
|
|
const funded = lanes.filter((lane) => String(lane.status || '').toLowerCase() === 'funded').length
|
|
const unfunded = lanes.filter((lane) => String(lane.status || '').toLowerCase() === 'unfunded').length
|
|
const proofRecorded = lanes.filter(
|
|
(lane) => String(lane.proof_status || '').toLowerCase() === 'proof-recorded'
|
|
).length
|
|
|
|
return {
|
|
total: lanes.length,
|
|
funded,
|
|
unfunded,
|
|
proofRecorded,
|
|
updatedAt: bridgeStatus?.data?.bridge_lanes?.updated_at,
|
|
}
|
|
}, [bridgeStatus])
|
|
|
|
const activityContext = useMemo(
|
|
() =>
|
|
summarizeChainActivity({
|
|
blocks: [],
|
|
transactions: [],
|
|
latestBlockNumber: bridgeStatus?.data?.chains?.['138']?.block_number
|
|
? Number(bridgeStatus.data.chains['138'].block_number)
|
|
: null,
|
|
latestBlockTimestamp: null,
|
|
freshness: resolveEffectiveFreshness(stats, bridgeStatus),
|
|
diagnostics: stats?.diagnostics ?? bridgeStatus?.data?.diagnostics ?? null,
|
|
}),
|
|
[bridgeStatus, stats],
|
|
)
|
|
|
|
return (
|
|
<div className="container mx-auto px-4 py-4 sm:py-6">
|
|
<PageIntro
|
|
compact
|
|
eyebrow={page.eyebrow}
|
|
title={page.title}
|
|
description={page.description}
|
|
actions={[
|
|
{ href: '/bridge', label: 'Bridge' },
|
|
{ href: '/routes', label: 'Routes' },
|
|
]}
|
|
/>
|
|
|
|
{page.note ? (
|
|
<DisclosureSection title="Scope note" defaultOpen={false} alwaysCollapsible className="mb-4">
|
|
<p className="rounded-lg border border-amber-200 bg-amber-50/70 px-3 py-2 text-sm text-amber-950 dark:border-amber-900/50 dark:bg-amber-950/20 dark:text-amber-100">
|
|
{page.note}
|
|
</p>
|
|
</DisclosureSection>
|
|
) : null}
|
|
|
|
<OperationsSurfaceNav />
|
|
|
|
{loadingError ? (
|
|
<Card className="mb-4 border border-red-200 bg-red-50/70 dark:border-red-900/50 dark:bg-red-950/20">
|
|
<p className="text-sm leading-6 text-red-900 dark:text-red-100">{loadingError}</p>
|
|
</Card>
|
|
) : null}
|
|
|
|
<ExplorerFreshnessDisclosure
|
|
context={activityContext}
|
|
stats={stats}
|
|
bridgeStatus={bridgeStatus}
|
|
title="Freshness & subsystems"
|
|
activityTitle="Operations recency"
|
|
scopeLabel="Mission-control freshness, bridge status, and explorer config surfaces."
|
|
>
|
|
<SubsystemPosturePanel
|
|
className="mt-3"
|
|
subsystems={bridgeStatus?.data?.subsystems}
|
|
title="Subsystem posture"
|
|
preferredKeys={['rpc_head', 'tx_index', 'bridge_relay_monitoring', 'stats_summary', 'freshness_queries']}
|
|
scopeLabel="Operations posture from backend subsystem truth."
|
|
/>
|
|
</ExplorerFreshnessDisclosure>
|
|
|
|
<CompactMetricBar
|
|
metrics={[
|
|
{ label: 'Bridge', value: bridgeStatus?.data?.status || 'unknown' },
|
|
{ label: 'Lanes', value: String(relayCount) },
|
|
{ label: 'Routes', value: String(routeMatrix?.counts?.filteredLiveRoutes ?? 0) },
|
|
{ label: 'Chains', value: String(networksConfig?.chains?.length ?? 0) },
|
|
]}
|
|
/>
|
|
|
|
<DisclosureSection title="Operations snapshot & config" defaultOpen={false} alwaysCollapsible className="mb-4">
|
|
<Card title="Operations Snapshot">
|
|
<div className="grid gap-4 md:grid-cols-2">
|
|
<div className="rounded-2xl border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-900/40">
|
|
<div className="text-sm text-gray-500 dark:text-gray-400">Bridge checked</div>
|
|
<div className="mt-1 text-lg font-semibold text-gray-900 dark:text-white">
|
|
<ClientRelativeTime value={bridgeStatus?.data?.checked_at} suffix=" ago" />
|
|
</div>
|
|
<div className="mt-2 text-sm text-gray-600 dark:text-gray-400">
|
|
{mode === 'guided' ? 'Public mission-control snapshot freshness.' : 'Mission-control freshness.'}
|
|
</div>
|
|
</div>
|
|
<div className="rounded-2xl border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-900/40">
|
|
<div className="text-sm text-gray-500 dark:text-gray-400">Route matrix updated</div>
|
|
<div className="mt-1 text-lg font-semibold text-gray-900 dark:text-white">
|
|
<ClientRelativeTime value={routeMatrix?.updated} suffix=" ago" />
|
|
</div>
|
|
<div className="mt-2 text-sm text-gray-600 dark:text-gray-400">
|
|
{mode === 'guided' ? 'Token-aggregation route inventory timestamp.' : 'Route inventory timestamp.'}
|
|
</div>
|
|
</div>
|
|
<div className="rounded-2xl border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-900/40">
|
|
<div className="text-sm text-gray-500 dark:text-gray-400">Default chain</div>
|
|
<div className="mt-1 text-lg font-semibold text-gray-900 dark:text-white">
|
|
{networksConfig?.defaultChainId ?? 'Unknown'}
|
|
</div>
|
|
<div className="mt-2 text-sm text-gray-600 dark:text-gray-400">
|
|
{mode === 'guided' ? 'Wallet onboarding points at Chain 138 by default.' : 'Default wallet chain.'}
|
|
</div>
|
|
</div>
|
|
<div className="rounded-2xl border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-900/40">
|
|
<div className="text-sm text-gray-500 dark:text-gray-400">Wallet support</div>
|
|
<div className="mt-1 text-lg font-semibold text-gray-900 dark:text-white">
|
|
{capabilities?.walletSupport?.walletAddEthereumChain && capabilities?.walletSupport?.walletWatchAsset
|
|
? 'Ready'
|
|
: 'Partial'}
|
|
</div>
|
|
<div className="mt-2 text-sm text-gray-600 dark:text-gray-400">
|
|
{mode === 'guided' ? '`wallet_addEthereumChain` and `wallet_watchAsset` compatibility.' : 'Wallet RPC support.'}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{laneSummary ? (
|
|
<div className="mt-4 rounded-2xl border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-900/40">
|
|
<div className="text-sm text-gray-500 dark:text-gray-400">Config-ready lanes</div>
|
|
<div className="mt-1 text-lg font-semibold text-gray-900 dark:text-white">
|
|
{laneSummary.funded}/{laneSummary.total} funded · {laneSummary.proofRecorded}/{laneSummary.total} proof-recorded
|
|
</div>
|
|
<div className="mt-2 text-sm text-gray-600 dark:text-gray-400">
|
|
{mode === 'guided'
|
|
? 'Remote CCIP bridge LINK balances and operator proof hashes. Full lane table below.'
|
|
: 'Lane funding and proof posture from bridge status API.'}
|
|
{' '}
|
|
<Link href="/bridge" className="font-semibold text-primary-600 hover:underline">
|
|
Open bridge monitoring
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
</Card>
|
|
|
|
<Card title="Public Config Highlights">
|
|
<div className="space-y-4">
|
|
<div className="rounded-2xl border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-900/40">
|
|
<div className="text-sm text-gray-500 dark:text-gray-400">Featured symbols</div>
|
|
<div className="mt-2 flex flex-wrap gap-2">
|
|
{topSymbols.map((symbol) => (
|
|
<span
|
|
key={symbol}
|
|
className="rounded-full bg-primary-50 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-primary-700 dark:bg-primary-900/30 dark:text-primary-300"
|
|
>
|
|
{symbol}
|
|
</span>
|
|
))}
|
|
</div>
|
|
</div>
|
|
<div className="rounded-2xl border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-900/40">
|
|
<div className="text-sm text-gray-500 dark:text-gray-400">Tracing posture</div>
|
|
<div className="mt-2 text-sm text-gray-700 dark:text-gray-300">
|
|
Supported: {(capabilities?.tracing?.supportedMethods || []).join(', ') || 'None'}
|
|
</div>
|
|
<div className="mt-2 text-sm text-gray-600 dark:text-gray-400">
|
|
Unsupported: {(capabilities?.tracing?.unsupportedMethods || []).join(', ') || 'None'}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
</DisclosureSection>
|
|
|
|
<DisclosureSection title="Bridge lane health" defaultOpen={false} alwaysCollapsible className="mb-4">
|
|
<BridgeLaneHealthPanel laneHealth={bridgeStatus?.data?.bridge_lanes} />
|
|
</DisclosureSection>
|
|
|
|
<OperationsActionGrid actions={page.actions} />
|
|
</div>
|
|
)
|
|
}
|