feat(explorer): mission-control resilience, ops token labels, and CI validate
Add SSE reconnect with backoff, fallback REST polling, visibility-aware refresh, extended token-list labels on operations pages, validate-on-pr workflow, and smoke coverage. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
13
frontend/src/components/common/TokenListSurfaceNote.tsx
Normal file
13
frontend/src/components/common/TokenListSurfaceNote.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { TOKEN_LIST_SURFACE_LABELS, type TokenListSurface } from '@/services/api/tokenListSurfaces'
|
||||
|
||||
interface TokenListSurfaceNoteProps {
|
||||
surface?: TokenListSurface
|
||||
className?: string
|
||||
}
|
||||
|
||||
export default function TokenListSurfaceNote({
|
||||
surface = 'extended',
|
||||
className = 'text-sm text-gray-600 dark:text-gray-400',
|
||||
}: TokenListSurfaceNoteProps) {
|
||||
return <p className={className}>{TOKEN_LIST_SURFACE_LABELS[surface]}</p>
|
||||
}
|
||||
@@ -3,7 +3,8 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { Card } from '@/libs/frontend-ui-primitives'
|
||||
import { configApi, type TokenListResponse } from '@/services/api/config'
|
||||
import { type TokenListResponse } from '@/services/api/config'
|
||||
import { tokensApi } from '@/services/api/tokens'
|
||||
import {
|
||||
aggregateLiquidityPools,
|
||||
featuredLiquiditySymbols,
|
||||
@@ -22,6 +23,7 @@ import ActivityContextPanel from '@/components/common/ActivityContextPanel'
|
||||
import FreshnessTrustNote from '@/components/common/FreshnessTrustNote'
|
||||
import MarketEvidenceNote from '@/components/common/MarketEvidenceNote'
|
||||
import SubsystemPosturePanel from '@/components/common/SubsystemPosturePanel'
|
||||
import TokenListSurfaceNote from '@/components/common/TokenListSurfaceNote'
|
||||
import { resolveEffectiveFreshness } from '@/utils/explorerFreshness'
|
||||
import {
|
||||
formatCurrency,
|
||||
@@ -98,7 +100,7 @@ export default function LiquidityOperationsPage({
|
||||
const load = async () => {
|
||||
const [tokenListResult, routeMatrixResult, plannerCapabilitiesResult, planResult, statsResult, bridgeResult] =
|
||||
await Promise.allSettled([
|
||||
configApi.getTokenList(),
|
||||
tokensApi.listForSurface('extended', 138).then(({ ok, data }) => ({ tokens: ok ? data : [] })),
|
||||
routesApi.getRouteMatrix(),
|
||||
plannerApi.getCapabilities(),
|
||||
plannerApi.getInternalExecutionPlan(),
|
||||
@@ -273,6 +275,7 @@ export default function LiquidityOperationsPage({
|
||||
public route matrix, planner capabilities, and mission-control token pool inventory together
|
||||
so integrators can inspect what Chain 138 is actually serving right now.
|
||||
</p>
|
||||
<TokenListSurfaceNote className="mt-3 text-sm text-gray-600 dark:text-gray-400" />
|
||||
</div>
|
||||
|
||||
{loadingError ? (
|
||||
|
||||
@@ -3,6 +3,7 @@ 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'
|
||||
@@ -10,6 +11,7 @@ import { summarizeChainActivity } from '@/utils/activityContext'
|
||||
import ActivityContextPanel from '@/components/common/ActivityContextPanel'
|
||||
import FreshnessTrustNote from '@/components/common/FreshnessTrustNote'
|
||||
import SubsystemPosturePanel from '@/components/common/SubsystemPosturePanel'
|
||||
import TokenListSurfaceNote from '@/components/common/TokenListSurfaceNote'
|
||||
import { resolveEffectiveFreshness } from '@/utils/explorerFreshness'
|
||||
import { statsApi, type ExplorerStats } from '@/services/api/stats'
|
||||
|
||||
@@ -88,7 +90,7 @@ export default function OperationsHubPage({
|
||||
missionControlApi.getBridgeStatus(),
|
||||
routesApi.getRouteMatrix(),
|
||||
configApi.getNetworks(),
|
||||
configApi.getTokenList(),
|
||||
tokensApi.listForSurface('extended', 138).then(({ ok, data }) => ({ tokens: ok ? data : [] })),
|
||||
configApi.getCapabilities(),
|
||||
statsApi.get(),
|
||||
])
|
||||
@@ -248,6 +250,7 @@ export default function OperationsHubPage({
|
||||
<div className="mt-2 text-sm text-gray-600 dark:text-gray-400">
|
||||
Chains · {(tokenList?.tokens || []).length} tokens across {tokenChainCoverage} networks
|
||||
</div>
|
||||
<TokenListSurfaceNote className="mt-2 text-xs text-gray-500 dark:text-gray-400" />
|
||||
</Card>
|
||||
|
||||
<Card className="border border-gray-200 dark:border-gray-700">
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { Card } from '@/libs/frontend-ui-primitives'
|
||||
import { configApi, type TokenListResponse } from '@/services/api/config'
|
||||
import { type TokenListResponse } from '@/services/api/config'
|
||||
import { tokensApi } from '@/services/api/tokens'
|
||||
import TokenListSurfaceNote from '@/components/common/TokenListSurfaceNote'
|
||||
import {
|
||||
aggregateLiquidityPools,
|
||||
getRouteBackedPoolAddresses,
|
||||
@@ -28,7 +30,7 @@ export default function PoolsOperationsPage() {
|
||||
|
||||
const load = async () => {
|
||||
const [tokenListResult, routeMatrixResult] = await Promise.allSettled([
|
||||
configApi.getTokenList(),
|
||||
tokensApi.listForSurface('extended', 138).then(({ ok, data }) => ({ tokens: ok ? data : [] })),
|
||||
routesApi.getRouteMatrix(),
|
||||
])
|
||||
|
||||
@@ -100,6 +102,7 @@ export default function PoolsOperationsPage() {
|
||||
This page now summarizes the live pool inventory discovered through mission-control token
|
||||
pool endpoints and cross-checks it against the current route matrix.
|
||||
</p>
|
||||
<TokenListSurfaceNote className="mt-3 text-sm text-gray-600 dark:text-gray-400" />
|
||||
</div>
|
||||
|
||||
{loadingError ? (
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useEffect, useMemo, useState } from 'react'
|
||||
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 { statsApi, type ExplorerStats } from '@/services/api/stats'
|
||||
@@ -11,6 +12,7 @@ import OperationsPageShell, {
|
||||
formatNumber,
|
||||
relativeAge,
|
||||
} from './OperationsPageShell'
|
||||
import TokenListSurfaceNote from '@/components/common/TokenListSurfaceNote'
|
||||
|
||||
interface SystemOperationsPageProps {
|
||||
initialBridgeStatus?: MissionControlBridgeStatusResponse | null
|
||||
@@ -46,7 +48,7 @@ export default function SystemOperationsPage({
|
||||
await Promise.allSettled([
|
||||
missionControlApi.getBridgeStatus(),
|
||||
configApi.getNetworks(),
|
||||
configApi.getTokenList(),
|
||||
tokensApi.listForSurface('extended', 138).then(({ ok, data }) => ({ tokens: ok ? data : [] })),
|
||||
configApi.getCapabilities(),
|
||||
routesApi.getRouteMatrix(),
|
||||
statsApi.get(),
|
||||
@@ -125,6 +127,7 @@ export default function SystemOperationsPage({
|
||||
description={`${formatNumber(capabilities?.tracing?.supportedMethods?.length)} tracing methods published.`}
|
||||
/>
|
||||
</div>
|
||||
<TokenListSurfaceNote className="mb-6 text-xs text-gray-500 dark:text-gray-400" />
|
||||
|
||||
<div className="mb-8 grid gap-6 lg:grid-cols-[1fr_1fr]">
|
||||
<Card title="Topology Snapshot">
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
HOME_PRICE_FEED_REFRESH_MS,
|
||||
resolveHomePriceFeedAddresses,
|
||||
} from '@/utils/featuredTokens'
|
||||
import { createVisibilityAwarePoller } from '@/utils/visibilityRefresh'
|
||||
|
||||
type HomeStats = ExplorerStats
|
||||
|
||||
@@ -180,14 +181,11 @@ export default function Home({
|
||||
}, [chainId])
|
||||
|
||||
useEffect(() => {
|
||||
loadDashboard()
|
||||
const timer = window.setInterval(() => {
|
||||
void loadDashboard()
|
||||
}, HOME_DASHBOARD_REFRESH_MS)
|
||||
|
||||
return () => {
|
||||
window.clearInterval(timer)
|
||||
}
|
||||
void loadDashboard()
|
||||
return createVisibilityAwarePoller({
|
||||
intervalMs: HOME_DASHBOARD_REFRESH_MS,
|
||||
task: loadDashboard,
|
||||
})
|
||||
}, [loadDashboard])
|
||||
|
||||
useEffect(() => {
|
||||
@@ -210,7 +208,6 @@ export default function Home({
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
let timer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
const refreshFeaturedPrices = async () => {
|
||||
try {
|
||||
@@ -221,20 +218,18 @@ export default function Home({
|
||||
if (!cancelled && process.env.NODE_ENV !== 'production') {
|
||||
console.warn('Failed to load featured token prices:', error)
|
||||
}
|
||||
} finally {
|
||||
if (!cancelled) {
|
||||
timer = setTimeout(() => {
|
||||
void refreshFeaturedPrices()
|
||||
}, HOME_PRICE_FEED_REFRESH_MS)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void refreshFeaturedPrices()
|
||||
const stop = createVisibilityAwarePoller({
|
||||
intervalMs: HOME_PRICE_FEED_REFRESH_MS,
|
||||
task: refreshFeaturedPrices,
|
||||
})
|
||||
|
||||
return () => {
|
||||
cancelled = true
|
||||
if (timer) clearTimeout(timer)
|
||||
stop()
|
||||
}
|
||||
}, [loadFeaturedPrices])
|
||||
|
||||
@@ -307,6 +302,31 @@ export default function Home({
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (relayFeedState !== 'fallback') return
|
||||
|
||||
let cancelled = false
|
||||
|
||||
const refreshSnapshot = async () => {
|
||||
try {
|
||||
const status = await missionControlApi.getBridgeStatus()
|
||||
if (!cancelled) {
|
||||
setBridgeStatus(status)
|
||||
setRelaySummary(summarizeMissionControlRelay(status))
|
||||
}
|
||||
} catch (error) {
|
||||
if (!cancelled && process.env.NODE_ENV !== 'production') {
|
||||
console.warn('Failed to refresh mission control snapshot:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return createVisibilityAwarePoller({
|
||||
intervalMs: HOME_DASHBOARD_REFRESH_MS,
|
||||
task: refreshSnapshot,
|
||||
})
|
||||
}, [relayFeedState])
|
||||
|
||||
const relayToneClasses =
|
||||
relaySummary?.tone === 'danger'
|
||||
? 'border-red-200 bg-red-50 text-red-900 dark:border-red-900/60 dark:bg-red-950/40 dark:text-red-100'
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
type TransactionSummary,
|
||||
} from '@/services/api/addresses'
|
||||
import { WALLET_SNAPSHOT_REFRESH_MS } from '@/utils/featuredTokens'
|
||||
import { createVisibilityAwarePoller } from '@/utils/visibilityRefresh'
|
||||
import { formatRelativeAge, formatTokenAmount } from '@/utils/format'
|
||||
import {
|
||||
isWatchlistEntry,
|
||||
@@ -146,8 +147,6 @@ export default function WalletPage(props: WalletPageProps) {
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
let timer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
if (!walletSession?.address) {
|
||||
setAddressInfo(null)
|
||||
setRecentAddressTransactions([])
|
||||
@@ -170,20 +169,18 @@ export default function WalletPage(props: WalletPageProps) {
|
||||
setTokenBalances([])
|
||||
setTokenTransfers([])
|
||||
}
|
||||
} finally {
|
||||
if (!cancelled) {
|
||||
timer = setTimeout(() => {
|
||||
void refreshSnapshot()
|
||||
}, WALLET_SNAPSHOT_REFRESH_MS)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void refreshSnapshot()
|
||||
const stop = createVisibilityAwarePoller({
|
||||
intervalMs: WALLET_SNAPSHOT_REFRESH_MS,
|
||||
task: refreshSnapshot,
|
||||
})
|
||||
|
||||
return () => {
|
||||
cancelled = true
|
||||
if (timer) clearTimeout(timer)
|
||||
stop()
|
||||
}
|
||||
}, [loadWalletSnapshot, walletSession?.address])
|
||||
|
||||
|
||||
Reference in New Issue
Block a user