Freshness diagnostics API, UI trust notes, mission control/stats updates, and deploy scripts.
Made-with: Cursor
This commit is contained in:
@@ -10,36 +10,106 @@ import {
|
||||
} from '@/services/api/stats'
|
||||
import {
|
||||
missionControlApi,
|
||||
summarizeMissionControlRelay,
|
||||
type MissionControlBridgeStatusResponse,
|
||||
type MissionControlRelaySummary,
|
||||
} from '@/services/api/missionControl'
|
||||
import { loadDashboardData } from '@/utils/dashboard'
|
||||
import EntityBadge from '@/components/common/EntityBadge'
|
||||
import { formatTimestamp, formatWeiAsEth } from '@/utils/format'
|
||||
import { formatRelativeAge, formatTimestamp, formatWeiAsEth } from '@/utils/format'
|
||||
import { transactionsApi, type Transaction } from '@/services/api/transactions'
|
||||
import { summarizeChainActivity } from '@/utils/activityContext'
|
||||
import ActivityContextPanel from '@/components/common/ActivityContextPanel'
|
||||
import FreshnessTrustNote from '@/components/common/FreshnessTrustNote'
|
||||
import { Explain, useUiMode } from '@/components/common/UiModeContext'
|
||||
import { resolveEffectiveFreshness, shouldExplainEmptyHeadBlocks } from '@/utils/explorerFreshness'
|
||||
|
||||
type HomeStats = ExplorerStats
|
||||
|
||||
interface HomePageProps {
|
||||
initialStats?: HomeStats | null
|
||||
initialRecentBlocks?: Block[]
|
||||
initialRecentTransactions?: Transaction[]
|
||||
initialTransactionTrend?: ExplorerTransactionTrendPoint[]
|
||||
initialActivitySnapshot?: ExplorerRecentActivitySnapshot | null
|
||||
initialBridgeStatus?: MissionControlBridgeStatusResponse | null
|
||||
initialRelaySummary?: MissionControlRelaySummary | null
|
||||
}
|
||||
|
||||
function resolveRelaySeverityLabel(status?: string, tone?: 'normal' | 'warning' | 'danger') {
|
||||
const normalized = String(status || '').toLowerCase()
|
||||
if (normalized === 'down') return 'down'
|
||||
if (normalized === 'degraded' || normalized === 'stale' || normalized === 'stopped') return 'degraded'
|
||||
if (normalized === 'paused') return 'paused'
|
||||
if (['starting', 'unknown', 'snapshot-error'].includes(normalized) || tone === 'warning') return 'warning'
|
||||
return 'operational'
|
||||
}
|
||||
|
||||
function resolveRelayBadgeTone(status?: string, tone?: 'normal' | 'warning' | 'danger'): 'success' | 'info' | 'warning' {
|
||||
const severity = resolveRelaySeverityLabel(status, tone)
|
||||
if (severity === 'operational') return 'success'
|
||||
if (severity === 'warning') return 'info'
|
||||
return 'warning'
|
||||
}
|
||||
|
||||
function getLaneImpactNote(key: string, severity: string) {
|
||||
if (key === 'mainnet_weth' && severity === 'paused') {
|
||||
return 'New Mainnet WETH bridge deliveries are currently queued while this lane is paused. Core Chain 138 browsing remains available.'
|
||||
}
|
||||
if (key === 'avax' || key === 'avalanche' || key === 'avax_cw' || key === 'avax_to_138') {
|
||||
return severity === 'operational'
|
||||
? 'Avalanche lane visibility is healthy.'
|
||||
: 'Affects Avalanche-connected bridge visibility and routing. Core Chain 138 browsing remains available.'
|
||||
}
|
||||
if (key.includes('mainnet')) {
|
||||
return severity === 'operational'
|
||||
? 'Ethereum Mainnet relay visibility is healthy.'
|
||||
: 'Affects Mainnet bridge posture and route visibility more than core Chain 138 browsing.'
|
||||
}
|
||||
if (key.includes('bsc')) {
|
||||
return severity === 'operational'
|
||||
? 'BSC relay visibility is healthy.'
|
||||
: 'Affects BSC-connected bridge posture and route visibility more than core Chain 138 browsing.'
|
||||
}
|
||||
return severity === 'operational'
|
||||
? 'Relay lane visibility is healthy.'
|
||||
: 'Affects this relay lane more than core Chain 138 chain browsing.'
|
||||
}
|
||||
|
||||
function formatObservabilityValue(value: number | null, formatter: (value: number) => string) {
|
||||
if (value == null) {
|
||||
return { value: 'Unknown', note: 'Not reported by the current public stats payload.' }
|
||||
}
|
||||
return { value: formatter(value), note: 'Current public stats payload.' }
|
||||
}
|
||||
|
||||
function formatGasPriceGwei(value: number) {
|
||||
if (!Number.isFinite(value)) return 'Unknown'
|
||||
return `${value.toFixed(3)} gwei`
|
||||
}
|
||||
|
||||
export default function Home({
|
||||
initialStats = null,
|
||||
initialRecentBlocks = [],
|
||||
initialRecentTransactions = [],
|
||||
initialTransactionTrend = [],
|
||||
initialActivitySnapshot = null,
|
||||
initialBridgeStatus = null,
|
||||
initialRelaySummary = null,
|
||||
}: HomePageProps) {
|
||||
const { mode } = useUiMode()
|
||||
const [stats, setStats] = useState<HomeStats | null>(initialStats)
|
||||
const [recentBlocks, setRecentBlocks] = useState<Block[]>(initialRecentBlocks)
|
||||
const [transactionTrend, setTransactionTrend] = useState<ExplorerTransactionTrendPoint[]>(initialTransactionTrend)
|
||||
const [recentTransactions, setRecentTransactions] = useState<Transaction[]>(initialRecentTransactions)
|
||||
const [activitySnapshot, setActivitySnapshot] = useState<ExplorerRecentActivitySnapshot | null>(initialActivitySnapshot)
|
||||
const [bridgeStatus, setBridgeStatus] = useState<MissionControlBridgeStatusResponse | null>(initialBridgeStatus)
|
||||
const [relaySummary, setRelaySummary] = useState<MissionControlRelaySummary | null>(initialRelaySummary)
|
||||
const [missionExpanded, setMissionExpanded] = useState(false)
|
||||
const [relayExpanded, setRelayExpanded] = useState(false)
|
||||
const [relayPage, setRelayPage] = useState(1)
|
||||
const [relayFeedState, setRelayFeedState] = useState<'connecting' | 'live' | 'fallback'>(
|
||||
initialRelaySummary ? 'fallback' : 'connecting'
|
||||
initialRelaySummary || initialBridgeStatus ? 'fallback' : 'connecting'
|
||||
)
|
||||
const chainId = parseInt(process.env.NEXT_PUBLIC_CHAIN_ID || '138')
|
||||
const latestBlock = stats?.latest_block ?? recentBlocks[0]?.number ?? null
|
||||
@@ -92,14 +162,41 @@ export default function Home({
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
|
||||
if (recentTransactions.length > 0) {
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}
|
||||
|
||||
transactionsApi.listSafe(chainId, 1, 5)
|
||||
.then(({ ok, data }) => {
|
||||
if (!cancelled && ok && data.length > 0) {
|
||||
setRecentTransactions(data)
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
if (!cancelled && process.env.NODE_ENV !== 'production') {
|
||||
console.warn('Failed to load recent transactions for activity context:', error)
|
||||
}
|
||||
})
|
||||
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [chainId, recentTransactions.length])
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
|
||||
const loadSnapshot = async () => {
|
||||
try {
|
||||
const summary = await missionControlApi.getRelaySummary()
|
||||
const status = await missionControlApi.getBridgeStatus()
|
||||
if (!cancelled) {
|
||||
setRelaySummary(summary)
|
||||
setBridgeStatus(status)
|
||||
setRelaySummary(summarizeMissionControlRelay(status))
|
||||
}
|
||||
} catch (error) {
|
||||
if (!cancelled && process.env.NODE_ENV !== 'production') {
|
||||
@@ -110,10 +207,11 @@ export default function Home({
|
||||
|
||||
loadSnapshot()
|
||||
|
||||
const unsubscribe = missionControlApi.subscribeRelaySummary(
|
||||
(summary) => {
|
||||
const unsubscribe = missionControlApi.subscribeBridgeStatus(
|
||||
(status) => {
|
||||
if (!cancelled) {
|
||||
setRelaySummary(summary)
|
||||
setBridgeStatus(status)
|
||||
setRelaySummary(summarizeMissionControlRelay(status))
|
||||
setRelayFeedState('live')
|
||||
}
|
||||
},
|
||||
@@ -144,103 +242,375 @@ export default function Home({
|
||||
(best, point) => (!best || point.transaction_count > best.transaction_count ? point : best),
|
||||
null,
|
||||
)
|
||||
const averageBlockTimeSeconds =
|
||||
stats?.average_block_time_ms != null ? Math.round(stats.average_block_time_ms / 1000) : null
|
||||
const averageGasPriceGwei = stats?.average_gas_price_gwei ?? null
|
||||
const transactionsToday = stats?.transactions_today ?? null
|
||||
const networkUtilization =
|
||||
stats?.network_utilization_percentage != null ? Math.round(stats.network_utilization_percentage) : null
|
||||
const relayAttentionCount = relaySummary?.items.filter((item) => item.tone !== 'normal').length || 0
|
||||
const relayOperationalCount = relaySummary?.items.filter((item) => item.tone === 'normal').length || 0
|
||||
const relayPrimaryItems = relaySummary?.items.slice(0, 6) || []
|
||||
const relayPageSize = 4
|
||||
const relayPageCount = relaySummary?.items.length ? Math.max(1, Math.ceil(relaySummary.items.length / relayPageSize)) : 1
|
||||
const relayVisibleItems = relaySummary?.items.slice((relayPage - 1) * relayPageSize, relayPage * relayPageSize) || []
|
||||
const chainStatus = bridgeStatus?.data?.chains?.['138'] || (bridgeStatus?.data?.chains ? Object.values(bridgeStatus.data.chains)[0] : null)
|
||||
const checkedAt = bridgeStatus?.data?.checked_at || null
|
||||
const missionHeadline = relaySummary
|
||||
? relaySummary.tone === 'danger'
|
||||
? 'Relay lanes need attention'
|
||||
: relaySummary.tone === 'warning'
|
||||
? 'Relay lanes are degraded'
|
||||
: 'Relay lanes are operational'
|
||||
: chainStatus?.status === 'operational'
|
||||
? 'Chain 138 public health is operational'
|
||||
: chainStatus?.status
|
||||
? `Chain 138 public health is ${chainStatus.status}`
|
||||
: 'Mission control snapshot available'
|
||||
const missionDescription = (() => {
|
||||
const parts: string[] = []
|
||||
if (checkedAt) parts.push(`Last checked ${formatTimestamp(checkedAt)}`)
|
||||
if (chainStatus?.head_age_sec != null) parts.push(`head age ${Math.round(chainStatus.head_age_sec)}s`)
|
||||
if (chainStatus?.latency_ms != null) parts.push(`RPC latency ${Math.round(chainStatus.latency_ms)}ms`)
|
||||
if (relaySummary?.items.length) {
|
||||
parts.push(`${relayOperationalCount} operational lanes`)
|
||||
if (relayAttentionCount > 0) parts.push(`${relayAttentionCount} flagged lanes`)
|
||||
} else {
|
||||
parts.push('relay inventory unavailable in the current snapshot')
|
||||
}
|
||||
return parts.join(' · ')
|
||||
})()
|
||||
const snapshotAgeLabel = checkedAt ? formatRelativeAge(checkedAt) : 'Unknown'
|
||||
const chainVisibilityState =
|
||||
chainStatus?.head_age_sec != null
|
||||
? chainStatus.head_age_sec <= 30
|
||||
? 'current'
|
||||
: chainStatus.head_age_sec <= 120
|
||||
? 'slightly delayed'
|
||||
: 'stale'
|
||||
: 'unknown'
|
||||
const snapshotReason =
|
||||
relayFeedState === 'fallback'
|
||||
? 'Live indexing or relay streaming is not currently attached to this homepage card.'
|
||||
: relayFeedState === 'live'
|
||||
? 'Receiving named live mission-control events.'
|
||||
: 'Negotiating the mission-control event stream.'
|
||||
const snapshotScope =
|
||||
bridgeStatus?.data?.mode?.scope
|
||||
? bridgeStatus.data.mode.scope.replaceAll('_', ' ')
|
||||
: relayFeedState === 'fallback'
|
||||
? 'This primarily affects relay-lane freshness on the homepage card. Core explorer pages and public RPC health can still be current.'
|
||||
: relayFeedState === 'live'
|
||||
? 'Relay and chain status are arriving through live mission-control events.'
|
||||
: 'Homepage status is waiting for the mission-control stream to settle.'
|
||||
const missionImpact = relayAttentionCount > 0
|
||||
? 'Some cross-chain relay lanes are degraded. Core Chain 138 operation remains visible through the public RPC and explorer surfaces.'
|
||||
: 'Core Chain 138 operation and the visible relay lanes are currently healthy.'
|
||||
const activityContext = summarizeChainActivity({
|
||||
blocks: recentBlocks,
|
||||
transactions: recentTransactions,
|
||||
latestBlockNumber: latestBlock,
|
||||
latestBlockTimestamp: recentBlocks[0]?.timestamp ?? null,
|
||||
freshness: resolveEffectiveFreshness(stats, bridgeStatus),
|
||||
})
|
||||
const txCompleteness = stats?.completeness?.transactions_feed || bridgeStatus?.data?.subsystems?.tx_index?.completeness || null
|
||||
const blockCompleteness = stats?.completeness?.blocks_feed || null
|
||||
const statsGeneratedAt = stats?.sampling?.stats_generated_at || null
|
||||
const missionMode = bridgeStatus?.data?.mode || null
|
||||
const freshnessIssues = Object.entries({
|
||||
...(bridgeStatus?.data?.sampling?.issues || {}),
|
||||
...(stats?.sampling?.issues || {}),
|
||||
})
|
||||
const latestTransactionAgeLabel = activityContext.latest_transaction_timestamp
|
||||
? formatRelativeAge(activityContext.latest_transaction_timestamp)
|
||||
: 'Unknown'
|
||||
const latestTransactionFreshness =
|
||||
activityContext.latest_transaction_age_seconds == null
|
||||
? 'Latest transaction freshness is unavailable.'
|
||||
: activityContext.latest_transaction_age_seconds <= 15 * 60
|
||||
? 'Recent visible transactions are close to the chain head.'
|
||||
: activityContext.latest_transaction_age_seconds <= 3 * 60 * 60
|
||||
? 'The chain head is current, but visible transactions are older than the current tip.'
|
||||
: 'The chain head is current, but visible transactions are substantially older than the current tip.'
|
||||
const severityBreakdown = {
|
||||
down: relaySummary?.items.filter((item) => item.status === 'down').length || 0,
|
||||
degraded: relaySummary?.items.filter((item) => item.status === 'degraded').length || 0,
|
||||
warning:
|
||||
relaySummary?.items.filter((item) => ['paused', 'starting', 'unknown', 'snapshot-error'].includes(item.status)).length || 0,
|
||||
}
|
||||
const avgBlockTimeSummary = formatObservabilityValue(
|
||||
averageBlockTimeSeconds,
|
||||
(value) => `${value}s`,
|
||||
)
|
||||
const avgGasPriceSummary = formatObservabilityValue(
|
||||
averageGasPriceGwei,
|
||||
formatGasPriceGwei,
|
||||
)
|
||||
const transactionsTodaySummary = formatObservabilityValue(
|
||||
transactionsToday,
|
||||
(value) => value.toLocaleString(),
|
||||
)
|
||||
const networkUtilizationSummary =
|
||||
networkUtilization == null
|
||||
? { value: 'Unknown', note: 'Utilization is not reported by the current public stats payload.' }
|
||||
: networkUtilization === 0
|
||||
? { value: '0%', note: 'No utilization was reported in the latest visible stats sample.' }
|
||||
: { value: `${networkUtilization}%`, note: 'Current public stats payload.' }
|
||||
const missionCollapsedSummary = relaySummary
|
||||
? `${missionHeadline} · ${relayOperationalCount} operational`
|
||||
: `${missionHeadline}${chainStatus?.status ? ` · chain 138 ${chainStatus.status}` : ''}`
|
||||
|
||||
useEffect(() => {
|
||||
setRelayPage(1)
|
||||
}, [relaySummary?.items.length])
|
||||
|
||||
useEffect(() => {
|
||||
if (relayPage > relayPageCount) {
|
||||
setRelayPage(relayPageCount)
|
||||
}
|
||||
}, [relayPage, relayPageCount])
|
||||
|
||||
return (
|
||||
<main className="container mx-auto px-4 py-6 sm:py-8">
|
||||
<div className="mb-6 sm:mb-8">
|
||||
<h1 className="mb-2 text-3xl font-bold sm:text-4xl">SolaceScan</h1>
|
||||
<p className="text-base text-gray-600 dark:text-gray-400 sm:text-lg">Chain 138 Explorer by DBIS</p>
|
||||
</div>
|
||||
|
||||
{relaySummary && (
|
||||
<Card className={`mb-6 border shadow-sm ${relayToneClasses}`}>
|
||||
<div className="flex flex-col gap-5">
|
||||
<div className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
|
||||
<div className="max-w-3xl">
|
||||
<div className="text-sm font-semibold uppercase tracking-[0.22em] opacity-75">Mission Control</div>
|
||||
<div className="mt-2 text-xl font-semibold sm:text-2xl">
|
||||
{relaySummary.tone === 'danger'
|
||||
? 'Relay lanes need attention'
|
||||
: relaySummary.tone === 'warning'
|
||||
? 'Relay lanes are degraded'
|
||||
: 'Relay lanes are operational'}
|
||||
</div>
|
||||
<p className="mt-2 text-sm leading-6 opacity-90 sm:text-base">
|
||||
{relaySummary.text}. This surface summarizes the public relay posture in a compact operator-friendly format.
|
||||
</p>
|
||||
<div className="mt-4 flex flex-wrap gap-2">
|
||||
<EntityBadge
|
||||
label={relayFeedState === 'live' ? 'live sse' : relayFeedState === 'fallback' ? 'snapshot fallback' : 'connecting'}
|
||||
tone={relayFeedState === 'fallback' ? 'warning' : relayFeedState === 'connecting' ? 'info' : 'success'}
|
||||
/>
|
||||
<EntityBadge
|
||||
label={relaySummary.tone === 'danger' ? 'attention needed' : relaySummary.tone === 'warning' ? 'degraded' : 'operational'}
|
||||
tone={relaySummary.tone === 'danger' ? 'warning' : relaySummary.tone === 'warning' ? 'info' : 'success'}
|
||||
/>
|
||||
<EntityBadge label={`${relayOperationalCount} operational`} tone="success" />
|
||||
<EntityBadge label={`${relayAttentionCount} flagged`} tone={relayAttentionCount > 0 ? 'warning' : 'info'} />
|
||||
</div>
|
||||
{(relaySummary || bridgeStatus) && (
|
||||
<Card
|
||||
className={`border shadow-sm ${relayToneClasses} ${missionExpanded ? 'mb-6' : 'mb-4 !p-2 sm:!p-2'}`}
|
||||
>
|
||||
<div className={missionExpanded ? 'flex flex-col gap-5' : 'flex'}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setMissionExpanded((current) => !current)}
|
||||
aria-expanded={missionExpanded}
|
||||
className={`flex w-full items-center justify-between text-left shadow-sm backdrop-blur transition hover:bg-white/70 dark:border-white/10 dark:bg-black/10 dark:hover:bg-black/20 ${
|
||||
missionExpanded
|
||||
? 'gap-3 rounded-xl border border-white/40 bg-white/55 px-4 py-2.5'
|
||||
: 'gap-2 rounded-lg border border-white/35 bg-white/50 px-3 py-2'
|
||||
}`}
|
||||
>
|
||||
<div className={`min-w-0 opacity-90 ${missionExpanded ? 'text-sm leading-6 sm:text-base' : 'text-sm leading-5'}`}>
|
||||
<span className="font-semibold uppercase tracking-[0.22em] opacity-75">Mission Control</span>
|
||||
<span className={missionExpanded ? 'mx-2 opacity-40' : 'mx-1.5 opacity-40'}>•</span>
|
||||
<span>{missionCollapsedSummary}</span>
|
||||
</div>
|
||||
<div
|
||||
className={`shrink-0 font-semibold opacity-80 ${mode === 'guided' ? 'text-sm' : 'text-lg leading-none'}`}
|
||||
aria-label={missionExpanded ? 'Hide details' : 'Show details'}
|
||||
title={missionExpanded ? 'Hide details' : 'Show details'}
|
||||
>
|
||||
{mode === 'guided' ? (missionExpanded ? 'Hide details' : 'Show details') : (missionExpanded ? '\u2303' : '\u2304')}
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<div className="grid min-w-[220px] gap-3 sm:grid-cols-2 lg:w-[290px] lg:grid-cols-1">
|
||||
<div className="rounded-2xl border border-white/40 bg-white/50 p-4 shadow-sm backdrop-blur dark:border-white/10 dark:bg-black/10">
|
||||
<div className="text-xs font-semibold uppercase tracking-wide opacity-70">Live Feed</div>
|
||||
{missionExpanded ? (
|
||||
<>
|
||||
<div className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
|
||||
<div className="max-w-3xl">
|
||||
<div className="mt-2 text-xl font-semibold sm:text-2xl">{missionHeadline}</div>
|
||||
<p className="mt-2 text-sm leading-6 opacity-90 sm:text-base">
|
||||
{missionDescription}.
|
||||
{mode === 'guided'
|
||||
? ' This surface summarizes the public chain and relay posture in a compact operator-friendly format.'
|
||||
: ' Public chain and relay posture.'}
|
||||
</p>
|
||||
<p className="mt-2 text-sm leading-6 opacity-90 sm:text-base">
|
||||
{missionImpact}
|
||||
</p>
|
||||
<Explain>
|
||||
<p className="mt-2 text-sm leading-6 opacity-90 sm:text-base">
|
||||
{latestTransactionFreshness}
|
||||
</p>
|
||||
</Explain>
|
||||
<div className="mt-4 flex flex-wrap gap-2">
|
||||
<EntityBadge
|
||||
label={relayFeedState === 'live' ? 'live sse' : relayFeedState === 'fallback' ? 'snapshot' : 'connecting'}
|
||||
tone={relayFeedState === 'fallback' ? 'warning' : relayFeedState === 'connecting' ? 'info' : 'success'}
|
||||
/>
|
||||
{relaySummary ? (
|
||||
<EntityBadge
|
||||
label={relaySummary.tone === 'danger' ? 'attention needed' : relaySummary.tone === 'warning' ? 'degraded' : 'operational'}
|
||||
tone={relaySummary.tone === 'danger' ? 'warning' : relaySummary.tone === 'warning' ? 'info' : 'success'}
|
||||
/>
|
||||
) : null}
|
||||
{chainStatus?.status ? (
|
||||
<EntityBadge
|
||||
label={`chain 138 ${chainStatus.status}`}
|
||||
tone={chainStatus.status === 'operational' ? 'success' : 'warning'}
|
||||
/>
|
||||
) : null}
|
||||
<EntityBadge label={`${relayOperationalCount} operational`} tone="success" />
|
||||
{severityBreakdown.down > 0 ? <EntityBadge label={`${severityBreakdown.down} down`} tone="warning" /> : null}
|
||||
{severityBreakdown.degraded > 0 ? <EntityBadge label={`${severityBreakdown.degraded} degraded`} tone="info" /> : null}
|
||||
{severityBreakdown.warning > 0 ? <EntityBadge label={`${severityBreakdown.warning} warning`} tone="warning" /> : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid min-w-[220px] gap-3 sm:grid-cols-2 lg:w-[290px] lg:grid-cols-1">
|
||||
<div className="rounded-2xl border border-white/40 bg-white/50 p-4 shadow-sm backdrop-blur dark:border-white/10 dark:bg-black/10">
|
||||
<div className="text-xs font-semibold uppercase tracking-wide opacity-70">Live Feed</div>
|
||||
<div className="mt-2 text-lg font-semibold">
|
||||
{missionMode?.kind === 'live'
|
||||
? 'Streaming'
|
||||
: missionMode?.kind === 'snapshot' || relayFeedState === 'fallback'
|
||||
? 'Snapshot mode'
|
||||
: 'Connecting'}
|
||||
</div>
|
||||
<div className="mt-1 text-sm opacity-80">
|
||||
{`${statsGeneratedAt ? `Snapshot updated ${formatRelativeAge(statsGeneratedAt)}.` : `Snapshot updated ${snapshotAgeLabel}.`} ${
|
||||
missionMode?.reason ? missionMode.reason.replaceAll('_', ' ') : snapshotReason
|
||||
}`}
|
||||
</div>
|
||||
<div className="mt-2 text-xs opacity-75">
|
||||
{snapshotScope}
|
||||
</div>
|
||||
{freshnessIssues.length > 0 ? (
|
||||
<div className="mt-2 text-xs opacity-75">
|
||||
Freshness diagnostics: {freshnessIssues.map(([key]) => key.replaceAll('_', ' ')).join(', ')}.
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Link
|
||||
href="/bridge"
|
||||
className="inline-flex items-center justify-center rounded-xl bg-gray-900 px-4 py-2.5 text-sm font-semibold text-white hover:bg-black dark:bg-white dark:text-gray-900 dark:hover:bg-gray-100"
|
||||
>
|
||||
Open bridge monitoring
|
||||
</Link>
|
||||
<Link
|
||||
href="/operations"
|
||||
className="inline-flex items-center justify-center rounded-xl border border-current/20 px-4 py-2.5 text-sm font-semibold hover:bg-white/40 dark:hover:bg-black/10"
|
||||
>
|
||||
Open operations hub
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{chainStatus ? (
|
||||
<div className="grid gap-3 md:grid-cols-3 xl:grid-cols-5">
|
||||
<div className="rounded-2xl border border-white/40 bg-white/55 p-4 shadow-sm backdrop-blur dark:border-white/10 dark:bg-black/10">
|
||||
<div className="text-xs font-semibold uppercase tracking-wide opacity-70">Chain 138 Status</div>
|
||||
<div className="mt-2 text-lg font-semibold">{chainStatus.status || 'unknown'}</div>
|
||||
<div className="mt-1 text-sm opacity-80">{chainStatus.name || 'Defi Oracle Meta Mainnet'}</div>
|
||||
</div>
|
||||
<div className="rounded-2xl border border-white/40 bg-white/55 p-4 shadow-sm backdrop-blur dark:border-white/10 dark:bg-black/10">
|
||||
<div className="text-xs font-semibold uppercase tracking-wide opacity-70">Head Age</div>
|
||||
<div className="mt-2 text-lg font-semibold">
|
||||
{relayFeedState === 'live' ? 'Streaming' : relayFeedState === 'fallback' ? 'Snapshot mode' : 'Connecting'}
|
||||
{chainStatus.head_age_sec != null ? `${Math.round(chainStatus.head_age_sec)}s` : 'Unknown'}
|
||||
</div>
|
||||
<div className="mt-1 text-sm opacity-80">Latest public RPC head freshness.</div>
|
||||
<div className="mt-2 text-xs opacity-75">Chain visibility is currently {chainVisibilityState}.</div>
|
||||
</div>
|
||||
<div className="rounded-2xl border border-white/40 bg-white/55 p-4 shadow-sm backdrop-blur dark:border-white/10 dark:bg-black/10">
|
||||
<div className="text-xs font-semibold uppercase tracking-wide opacity-70">RPC Latency</div>
|
||||
<div className="mt-2 text-lg font-semibold">
|
||||
{chainStatus.latency_ms != null ? `${Math.round(chainStatus.latency_ms)}ms` : 'Unknown'}
|
||||
</div>
|
||||
<div className="mt-1 text-sm opacity-80">Public Chain 138 RPC probe latency.</div>
|
||||
</div>
|
||||
<div className="rounded-2xl border border-white/40 bg-white/55 p-4 shadow-sm backdrop-blur dark:border-white/10 dark:bg-black/10">
|
||||
<div className="text-xs font-semibold uppercase tracking-wide opacity-70">Latest Transaction</div>
|
||||
<div className="mt-2 text-lg font-semibold">
|
||||
{activityContext.latest_transaction_block_number != null ? `#${activityContext.latest_transaction_block_number}` : 'Unknown'}
|
||||
</div>
|
||||
<div className="mt-1 text-sm opacity-80">{latestTransactionAgeLabel}</div>
|
||||
<div className="mt-2 text-xs opacity-75">
|
||||
Latest visible transaction freshness{txCompleteness ? ` · ${txCompleteness}` : ''}.
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-2xl border border-white/40 bg-white/55 p-4 shadow-sm backdrop-blur dark:border-white/10 dark:bg-black/10">
|
||||
<div className="text-xs font-semibold uppercase tracking-wide opacity-70">Last Non-Empty Block</div>
|
||||
<div className="mt-2 text-lg font-semibold">
|
||||
{activityContext.last_non_empty_block_number != null ? `#${activityContext.last_non_empty_block_number}` : 'Unknown'}
|
||||
</div>
|
||||
<div className="mt-1 text-sm opacity-80">
|
||||
{relayFeedState === 'live'
|
||||
? 'Receiving named mission-control events.'
|
||||
: relayFeedState === 'fallback'
|
||||
? 'Using the latest available snapshot.'
|
||||
: 'Negotiating the event stream.'}
|
||||
{activityContext.block_gap_to_latest_transaction != null
|
||||
? `${activityContext.block_gap_to_latest_transaction.toLocaleString()} blocks behind tip`
|
||||
: 'Gap unavailable'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Link
|
||||
href="/operations"
|
||||
className="inline-flex items-center justify-center rounded-xl bg-gray-900 px-4 py-2.5 text-sm font-semibold text-white hover:bg-black dark:bg-white dark:text-gray-900 dark:hover:bg-gray-100"
|
||||
>
|
||||
Open operations hub
|
||||
</Link>
|
||||
<Link
|
||||
href="/explorer-api/v1/mission-control/stream"
|
||||
className="inline-flex items-center justify-center rounded-xl border border-current/20 px-4 py-2.5 text-sm font-semibold hover:bg-white/40 dark:hover:bg-black/10"
|
||||
>
|
||||
Open live stream
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-3">
|
||||
{relayPrimaryItems.map((item) => (
|
||||
<div
|
||||
key={item.key}
|
||||
className="rounded-2xl border border-white/40 bg-white/55 p-4 shadow-sm backdrop-blur dark:border-white/10 dark:bg-black/10"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<div className="text-sm font-semibold">{item.label}</div>
|
||||
<div className="mt-1 text-xs uppercase tracking-wide opacity-70">{item.status}</div>
|
||||
</div>
|
||||
<EntityBadge
|
||||
label={item.tone === 'danger' ? 'flagged' : item.tone === 'warning' ? 'degraded' : 'live'}
|
||||
tone={item.tone === 'danger' ? 'warning' : item.tone === 'warning' ? 'info' : 'success'}
|
||||
/>
|
||||
</div>
|
||||
<p className="mt-3 text-sm leading-6 opacity-90">{item.text}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{relaySummary.items.length > relayPrimaryItems.length ? (
|
||||
<div className="text-sm opacity-80">
|
||||
Showing {relayPrimaryItems.length} of {relaySummary.items.length} relay lanes. The live stream and operations hub carry the fuller view.
|
||||
</div>
|
||||
{relaySummary?.items.length ? (
|
||||
<div className="space-y-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setRelayExpanded((current) => !current)}
|
||||
aria-expanded={relayExpanded}
|
||||
className="flex w-full items-center justify-between gap-4 rounded-2xl border border-white/40 bg-white/55 p-4 text-left shadow-sm backdrop-blur transition hover:bg-white/70 dark:border-white/10 dark:bg-black/10 dark:hover:bg-black/20"
|
||||
>
|
||||
<div>
|
||||
<div className="text-sm font-semibold">Relay lane status</div>
|
||||
<p className="mt-1 text-sm leading-6 opacity-90">
|
||||
{relaySummary.text}. {relaySummary.items.length} configured lanes.
|
||||
</p>
|
||||
</div>
|
||||
<div className="shrink-0 text-sm font-semibold opacity-80">
|
||||
{relayExpanded ? 'Hide lanes' : 'Show lanes'}
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{relayExpanded ? (
|
||||
<>
|
||||
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-2">
|
||||
{relayVisibleItems.map((item) => (
|
||||
<div
|
||||
key={item.key}
|
||||
className="rounded-2xl border border-white/40 bg-white/55 p-4 shadow-sm backdrop-blur dark:border-white/10 dark:bg-black/10"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<div className="text-sm font-semibold">{item.label}</div>
|
||||
<div className="mt-1 text-xs uppercase tracking-wide opacity-70">{item.status}</div>
|
||||
</div>
|
||||
<EntityBadge label={resolveRelaySeverityLabel(item.status, item.tone)} tone={resolveRelayBadgeTone(item.status, item.tone)} />
|
||||
</div>
|
||||
<p className="mt-3 text-sm leading-6 opacity-90">{item.text}</p>
|
||||
<p className="mt-2 text-xs opacity-75">
|
||||
{getLaneImpactNote(item.key, resolveRelaySeverityLabel(item.status, item.tone))}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{relayPageCount > 1 ? (
|
||||
<div className="flex items-center justify-between gap-3 rounded-2xl border border-white/40 bg-white/40 px-4 py-3 text-sm shadow-sm backdrop-blur dark:border-white/10 dark:bg-black/10">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setRelayPage((current) => Math.max(1, current - 1))}
|
||||
disabled={relayPage === 1}
|
||||
className="rounded-lg border border-current/20 px-3 py-2 font-semibold disabled:cursor-not-allowed disabled:opacity-40"
|
||||
>
|
||||
Previous
|
||||
</button>
|
||||
<div className="text-center opacity-80">
|
||||
Page {relayPage} of {relayPageCount}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setRelayPage((current) => Math.min(relayPageCount, current + 1))}
|
||||
disabled={relayPage === relayPageCount}
|
||||
className="rounded-lg border border-current/20 px-3 py-2 font-semibold disabled:cursor-not-allowed disabled:opacity-40"
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-2xl border border-white/40 bg-white/55 p-4 text-sm opacity-90 shadow-sm backdrop-blur dark:border-white/10 dark:bg-black/10">
|
||||
The current mission-control snapshot does not include per-lane relay inventory. Chain health is still shown above, and the bridge monitoring page remains the canonical operator view.
|
||||
</div>
|
||||
)}
|
||||
{relaySummary ? (
|
||||
<div className="flex flex-wrap gap-2 text-sm opacity-80">
|
||||
{severityBreakdown.down > 0 ? <EntityBadge label={`${severityBreakdown.down} down`} tone="warning" /> : null}
|
||||
{severityBreakdown.degraded > 0 ? <EntityBadge label={`${severityBreakdown.degraded} degraded`} tone="info" /> : null}
|
||||
{severityBreakdown.warning > 0 ? <EntityBadge label={`${severityBreakdown.warning} warning`} tone="warning" /> : null}
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
</Card>
|
||||
@@ -253,22 +623,61 @@ export default function Home({
|
||||
<div className="text-xl font-bold sm:text-2xl">
|
||||
{latestBlock != null ? latestBlock.toLocaleString() : 'Unavailable'}
|
||||
</div>
|
||||
<div className="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
||||
{activityContext.latest_block_timestamp
|
||||
? `Head freshness ${formatRelativeAge(activityContext.latest_block_timestamp)}${blockCompleteness ? ` · ${blockCompleteness}` : ''}`
|
||||
: 'Head freshness unavailable.'}
|
||||
</div>
|
||||
</Card>
|
||||
<Card>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">Total Blocks</div>
|
||||
<div className="text-xl font-bold sm:text-2xl">{stats.total_blocks.toLocaleString()}</div>
|
||||
<div className="mt-2 text-xs text-gray-500 dark:text-gray-400">Visible public explorer block count.</div>
|
||||
</Card>
|
||||
<Card>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">Total Transactions</div>
|
||||
<div className="text-xl font-bold sm:text-2xl">{stats.total_transactions.toLocaleString()}</div>
|
||||
<div className="mt-2 text-xs text-gray-500 dark:text-gray-400">Latest visible tx {latestTransactionAgeLabel}.</div>
|
||||
</Card>
|
||||
<Card>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">Total Addresses</div>
|
||||
<div className="text-xl font-bold sm:text-2xl">{stats.total_addresses.toLocaleString()}</div>
|
||||
<div className="mt-2 text-xs text-gray-500 dark:text-gray-400">Current public explorer address count.</div>
|
||||
</Card>
|
||||
<Card>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">Avg Block Time</div>
|
||||
<div className="text-xl font-bold sm:text-2xl">{avgBlockTimeSummary.value}</div>
|
||||
<div className="mt-2 text-xs text-gray-500 dark:text-gray-400">{avgBlockTimeSummary.note}</div>
|
||||
</Card>
|
||||
<Card>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">Avg Gas Price</div>
|
||||
<div className="text-xl font-bold sm:text-2xl">{avgGasPriceSummary.value}</div>
|
||||
<div className="mt-2 text-xs text-gray-500 dark:text-gray-400">{avgGasPriceSummary.note}</div>
|
||||
</Card>
|
||||
<Card>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">Transactions Today</div>
|
||||
<div className="text-xl font-bold sm:text-2xl">{transactionsTodaySummary.value}</div>
|
||||
<div className="mt-2 text-xs text-gray-500 dark:text-gray-400">{transactionsTodaySummary.note}</div>
|
||||
</Card>
|
||||
<Card>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">Network Utilization</div>
|
||||
<div className="text-xl font-bold sm:text-2xl">{networkUtilizationSummary.value}</div>
|
||||
<div className="mt-2 text-xs text-gray-500 dark:text-gray-400">{networkUtilizationSummary.note}</div>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mb-8">
|
||||
<ActivityContextPanel context={activityContext} />
|
||||
<FreshnessTrustNote
|
||||
className="mt-3"
|
||||
context={activityContext}
|
||||
stats={stats}
|
||||
bridgeStatus={bridgeStatus}
|
||||
scopeLabel="Homepage status combines chain freshness, transaction visibility, and mission-control posture."
|
||||
/>
|
||||
</div>
|
||||
|
||||
{!stats && (
|
||||
<Card className="mb-8">
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
@@ -284,6 +693,11 @@ export default function Home({
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{shouldExplainEmptyHeadBlocks(recentBlocks, activityContext) ? (
|
||||
<p className="rounded-xl border border-amber-200 bg-amber-50/70 px-3 py-2 text-sm text-amber-900 dark:border-amber-900/40 dark:bg-amber-950/20 dark:text-amber-100">
|
||||
Recent head blocks are currently empty; use the latest transaction block for recent visible activity.
|
||||
</p>
|
||||
) : null}
|
||||
{recentBlocks.map((block) => (
|
||||
<div key={block.number} className="flex flex-col gap-1.5 border-b border-gray-200 py-2 last:border-0 dark:border-gray-700 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
@@ -315,7 +729,9 @@ export default function Home({
|
||||
<div className="mt-8 grid grid-cols-1 gap-4 lg:grid-cols-2">
|
||||
<Card title="Activity Pulse">
|
||||
<p className="text-sm leading-6 text-gray-600 dark:text-gray-400">
|
||||
A concise public view of chain activity, index coverage, and recent execution patterns.
|
||||
{mode === 'guided'
|
||||
? 'A concise public view of chain activity, index coverage, and recent execution patterns.'
|
||||
: 'Public chain activity and index posture.'}
|
||||
</p>
|
||||
<div className="mt-4 grid gap-3 sm: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">
|
||||
|
||||
Reference in New Issue
Block a user