Freshness diagnostics API, UI trust notes, mission control/stats updates, and deploy scripts.

Made-with: Cursor
This commit is contained in:
defiQUG
2026-04-12 06:33:54 -07:00
parent f46bd213ba
commit ee71f098ab
63 changed files with 5163 additions and 826 deletions

View File

@@ -0,0 +1,137 @@
import Link from 'next/link'
import { Card } from '@/libs/frontend-ui-primitives'
import EntityBadge from '@/components/common/EntityBadge'
import type { ChainActivityContext } from '@/utils/activityContext'
import { formatRelativeAge, formatTimestamp } from '@/utils/format'
import { Explain, useUiMode } from './UiModeContext'
function resolveTone(state: ChainActivityContext['state']): 'success' | 'warning' | 'neutral' {
switch (state) {
case 'active':
return 'success'
case 'low':
case 'inactive':
return 'warning'
default:
return 'neutral'
}
}
function resolveLabel(state: ChainActivityContext['state']): string {
switch (state) {
case 'active':
return 'active'
case 'low':
return 'low activity'
case 'inactive':
return 'inactive'
default:
return 'unknown'
}
}
function renderHeadline(context: ChainActivityContext): string {
if (context.transaction_visibility_unavailable) {
return 'Transaction index freshness is currently unavailable, while chain-head visibility remains live.'
}
if (context.state === 'unknown') {
return 'Recent activity context is temporarily unavailable.'
}
if (context.state === 'active') {
return 'Recent transactions are close to the visible chain tip.'
}
if (context.head_is_idle) {
return 'The chain head is advancing, but the latest visible transaction is older than the current tip.'
}
return 'Recent transaction activity is sparse right now.'
}
export default function ActivityContextPanel({
context,
title = 'Chain Activity Context',
}: {
context: ChainActivityContext
title?: string
}) {
const { mode } = useUiMode()
const tone = resolveTone(context.state)
const dualTimelineLabel =
context.latest_block_timestamp && context.latest_transaction_timestamp
? `${formatRelativeAge(context.latest_block_timestamp)} head · ${formatRelativeAge(context.latest_transaction_timestamp)} latest tx`
: 'Dual timeline unavailable'
return (
<Card className="border border-sky-200 bg-sky-50/60 dark:border-sky-900/40 dark:bg-sky-950/20" title={title}>
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
<div>
<div className="text-base font-semibold text-gray-900 dark:text-white">{renderHeadline(context)}</div>
<Explain>
<p className="mt-2 text-sm leading-6 text-gray-600 dark:text-gray-400">
Use the transaction tip and last non-empty block below to distinguish a quiet chain from a broken explorer.
</p>
</Explain>
</div>
<EntityBadge label={resolveLabel(context.state)} tone={tone} />
</div>
<div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-4">
<div className="rounded-2xl border border-white/50 bg-white/70 p-4 dark:border-white/10 dark:bg-black/10">
<div className="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Latest Block</div>
<div className="mt-2 text-xl font-semibold text-gray-900 dark:text-white">
{context.latest_block_number != null ? `#${context.latest_block_number}` : 'Unknown'}
</div>
<div className="mt-1 text-sm text-gray-600 dark:text-gray-400">
{formatRelativeAge(context.latest_block_timestamp)}
</div>
</div>
<div className="rounded-2xl border border-white/50 bg-white/70 p-4 dark:border-white/10 dark:bg-black/10">
<div className="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Latest Transaction</div>
<div className="mt-2 text-xl font-semibold text-gray-900 dark:text-white">
{context.latest_transaction_block_number != null ? `#${context.latest_transaction_block_number}` : 'Unknown'}
</div>
<div className="mt-1 text-sm text-gray-600 dark:text-gray-400">
{formatRelativeAge(context.latest_transaction_timestamp)}
</div>
</div>
<div className="rounded-2xl border border-white/50 bg-white/70 p-4 dark:border-white/10 dark:bg-black/10">
<div className="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Last Non-Empty Block</div>
<div className="mt-2 text-xl font-semibold text-gray-900 dark:text-white">
{context.last_non_empty_block_number != null ? `#${context.last_non_empty_block_number}` : 'Unknown'}
</div>
<div className="mt-1 text-sm text-gray-600 dark:text-gray-400">
{formatRelativeAge(context.last_non_empty_block_timestamp)}
</div>
</div>
<div className="rounded-2xl border border-white/50 bg-white/70 p-4 dark:border-white/10 dark:bg-black/10">
<div className="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Block Gap</div>
<div className="mt-2 text-xl font-semibold text-gray-900 dark:text-white">
{context.block_gap_to_latest_transaction != null ? context.block_gap_to_latest_transaction.toLocaleString() : 'Unknown'}
</div>
<div className="mt-1 text-sm text-gray-600 dark:text-gray-400">
{mode === 'guided'
? 'Difference between the current tip and the latest visible transaction block.'
: dualTimelineLabel}
</div>
</div>
</div>
<div className="flex flex-wrap gap-4 text-sm text-gray-600 dark:text-gray-400">
{context.latest_transaction_block_number != null ? (
<Link href={`/blocks/${context.latest_transaction_block_number}`} className="text-primary-600 hover:underline">
Open latest transaction block
</Link>
) : null}
{context.last_non_empty_block_number != null ? (
<Link href={`/blocks/${context.last_non_empty_block_number}`} className="text-primary-600 hover:underline">
Open last non-empty block
</Link>
) : null}
{context.latest_transaction_timestamp ? (
<span>Latest visible transaction time: {formatTimestamp(context.latest_transaction_timestamp)}</span>
) : null}
</div>
</div>
</Card>
)
}

View File

@@ -0,0 +1,27 @@
import BrandMark from './BrandMark'
export default function BrandLockup({ compact = false }: { compact?: boolean }) {
return (
<>
<BrandMark size={compact ? 'compact' : 'default'} />
<span className="min-w-0">
<span
className={[
'block truncate font-semibold tracking-[-0.02em] text-gray-950 dark:text-white',
compact ? 'text-[1.45rem]' : 'text-[1.65rem]',
].join(' ')}
>
SolaceScan
</span>
<span
className={[
'block truncate font-medium uppercase text-gray-500 dark:text-gray-400',
compact ? 'text-[0.72rem] tracking-[0.14em]' : 'text-[0.8rem] tracking-[0.12em]',
].join(' ')}
>
Chain 138 Explorer by DBIS
</span>
</span>
</>
)
}

View File

@@ -0,0 +1,45 @@
export default function BrandMark({ size = 'default' }: { size?: 'default' | 'compact' }) {
const containerClassName =
size === 'compact'
? 'h-10 w-10 rounded-xl'
: 'h-11 w-11 rounded-2xl'
const iconClassName = size === 'compact' ? 'h-6 w-6' : 'h-7 w-7'
return (
<span
className={[
'relative inline-flex shrink-0 items-center justify-center border border-primary-200/70 bg-white text-primary-600 shadow-[0_10px_30px_rgba(37,99,235,0.10)] transition-transform group-hover:-translate-y-0.5 dark:border-primary-500/20 dark:bg-gray-900 dark:text-primary-400',
containerClassName,
].join(' ')}
>
<svg className={iconClassName} viewBox="0 0 32 32" fill="none" aria-hidden>
<path
d="M16 4.75 7.5 9.2v9.55L16 23.2l8.5-4.45V9.2L16 4.75Z"
stroke="currentColor"
strokeWidth="1.8"
/>
<path
d="m7.75 9.45 8.25 4.3 8.25-4.3"
stroke="currentColor"
strokeWidth="1.6"
strokeLinecap="round"
/>
<path d="M16 13.9v9" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" />
<path
d="M22.75 6.8c2.35 1.55 3.9 4.2 3.9 7.2"
stroke="currentColor"
strokeWidth="1.6"
strokeLinecap="round"
opacity=".9"
/>
<path
d="M9.35 6.8c-2.3 1.55-3.85 4.2-3.85 7.2"
stroke="currentColor"
strokeWidth="1.6"
strokeLinecap="round"
opacity=".65"
/>
</svg>
</span>
)
}

View File

@@ -2,22 +2,25 @@ import type { ReactNode } from 'react'
import Navbar from './Navbar'
import Footer from './Footer'
import ExplorerAgentTool from './ExplorerAgentTool'
import { UiModeProvider } from './UiModeContext'
export default function ExplorerChrome({ children }: { children: ReactNode }) {
return (
<div className="flex min-h-screen flex-col bg-gray-50 text-gray-900 dark:bg-gray-900 dark:text-gray-100">
<a
href="#main-content"
className="sr-only focus:not-sr-only focus:absolute focus:left-4 focus:top-4 focus:z-[100] focus:rounded-md focus:bg-primary-600 focus:px-4 focus:py-2 focus:text-sm focus:font-medium focus:text-white"
>
Skip to content
</a>
<Navbar />
<div id="main-content" className="flex-1">
{children}
<UiModeProvider>
<div className="flex min-h-screen flex-col bg-gray-50 text-gray-900 dark:bg-gray-900 dark:text-gray-100">
<a
href="#main-content"
className="sr-only focus:not-sr-only focus:absolute focus:left-4 focus:top-4 focus:z-[100] focus:rounded-md focus:bg-primary-600 focus:px-4 focus:py-2 focus:text-sm focus:font-medium focus:text-white"
>
Skip to content
</a>
<Navbar />
<div id="main-content" className="flex-1">
{children}
</div>
<ExplorerAgentTool />
<Footer />
</div>
<ExplorerAgentTool />
<Footer />
</div>
</UiModeProvider>
)
}

View File

@@ -0,0 +1,85 @@
import type { MissionControlBridgeStatusResponse } from '@/services/api/missionControl'
import type { ExplorerStats } from '@/services/api/stats'
import type { ChainActivityContext } from '@/utils/activityContext'
import {
resolveFreshnessSourceLabel,
summarizeFreshnessConfidence,
} from '@/utils/explorerFreshness'
import { formatRelativeAge } from '@/utils/format'
function buildSummary(context: ChainActivityContext) {
if (context.transaction_visibility_unavailable) {
return 'Chain-head visibility is current, while transaction freshness is currently unavailable.'
}
if (context.state === 'active') {
return 'Chain head and latest indexed transactions are closely aligned.'
}
if (context.head_is_idle) {
return 'Chain head is current, while latest visible transactions trail the tip.'
}
if (context.state === 'low' || context.state === 'inactive') {
return 'Chain head is current, and recent visible transaction activity is sparse.'
}
return 'Freshness context is based on the latest visible public explorer evidence.'
}
function buildDetail(context: ChainActivityContext) {
if (context.transaction_visibility_unavailable) {
return 'Use chain-head visibility and the last non-empty block as the current trust anchors.'
}
const latestTxAge = formatRelativeAge(context.latest_transaction_timestamp)
const latestNonEmptyBlock =
context.last_non_empty_block_number != null ? `#${context.last_non_empty_block_number.toLocaleString()}` : 'unknown'
if (context.head_is_idle) {
return `Latest visible transaction: ${latestTxAge}. Last non-empty block: ${latestNonEmptyBlock}.`
}
if (context.state === 'active') {
return `Latest visible transaction: ${latestTxAge}. Recent indexed activity remains close to the tip.`
}
return `Latest visible transaction: ${latestTxAge}. Recent head blocks may be quiet even while the chain remains current.`
}
export default function FreshnessTrustNote({
context,
stats,
bridgeStatus,
scopeLabel,
className = '',
}: {
context: ChainActivityContext
stats?: ExplorerStats | null
bridgeStatus?: MissionControlBridgeStatusResponse | null
scopeLabel?: string
className?: string
}) {
const sourceLabel = resolveFreshnessSourceLabel(stats, bridgeStatus)
const confidenceBadges = summarizeFreshnessConfidence(stats, bridgeStatus)
const normalizedClassName = className ? ` ${className}` : ''
return (
<div className={`rounded-2xl border border-gray-200 bg-white/80 px-4 py-3 text-sm dark:border-gray-800 dark:bg-gray-950/40${normalizedClassName}`}>
<div className="font-medium text-gray-900 dark:text-white">{buildSummary(context)}</div>
<div className="mt-1 text-gray-600 dark:text-gray-400">
{buildDetail(context)} {scopeLabel ? `${scopeLabel}. ` : ''}{sourceLabel}
</div>
<div className="mt-2 flex flex-wrap gap-2 text-xs text-gray-500 dark:text-gray-400">
{confidenceBadges.map((badge) => (
<span
key={badge}
className="rounded-full border border-gray-200 bg-gray-50 px-2.5 py-1 dark:border-gray-700 dark:bg-gray-900/70"
>
{badge}
</span>
))}
</div>
</div>
)
}

View File

@@ -0,0 +1,202 @@
'use client'
import { useRouter } from 'next/navigation'
import { useEffect, useMemo, useRef, useState } from 'react'
import { Explain, useUiMode } from './UiModeContext'
export type HeaderCommandItem = {
href?: string
label: string
description?: string
section: string
keywords?: string[]
onSelect?: () => void | Promise<void>
}
function SearchIcon({ className = 'h-4 w-4' }: { className?: string }) {
return (
<svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.9} d="m21 21-4.35-4.35" />
<circle cx="11" cy="11" r="6.5" strokeWidth={1.9} />
</svg>
)
}
function matchItem(item: HeaderCommandItem, query: string) {
const haystack = `${item.label} ${item.description || ''} ${item.section} ${(item.keywords || []).join(' ')}`.toLowerCase()
return haystack.includes(query.toLowerCase())
}
export default function HeaderCommandPalette({
open,
onClose,
items,
}: {
open: boolean
onClose: () => void
items: HeaderCommandItem[]
}) {
const router = useRouter()
const { mode } = useUiMode()
const inputRef = useRef<HTMLInputElement | null>(null)
const itemRefs = useRef<Array<HTMLButtonElement | null>>([])
const [query, setQuery] = useState('')
const [activeIndex, setActiveIndex] = useState(0)
const filteredItems = useMemo(() => {
const matches = query.trim()
? items.filter((item) => matchItem(item, query))
: items
return [
{
href: `/search${query.trim() ? `?q=${encodeURIComponent(query.trim())}` : ''}`,
label: query.trim() ? `Search for “${query.trim()}` : 'Open full explorer search',
description: query.trim()
? 'Jump to the full search surface with the current query.'
: 'Open the full search page and browse the explorer index.',
section: 'Search',
keywords: ['query', 'find', 'lookup'],
},
...matches,
]
}, [items, query])
useEffect(() => {
if (!open) {
setQuery('')
setActiveIndex(0)
return
}
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
event.preventDefault()
onClose()
}
}
document.addEventListener('keydown', handleKeyDown)
requestAnimationFrame(() => inputRef.current?.focus())
return () => document.removeEventListener('keydown', handleKeyDown)
}, [onClose, open])
useEffect(() => {
setActiveIndex(0)
}, [query])
useEffect(() => {
if (!open) return
itemRefs.current[activeIndex]?.scrollIntoView({ block: 'nearest' })
}, [activeIndex, open])
if (!open) return null
const handleSelect = async (item: HeaderCommandItem) => {
onClose()
if (item.onSelect) {
await item.onSelect()
return
}
if (item.href) {
router.push(item.href)
}
}
return (
<div className="fixed inset-0 z-[80] flex items-start justify-center bg-gray-950/45 px-4 py-20 backdrop-blur-sm">
<div
role="dialog"
aria-modal="true"
aria-label="Explorer command palette"
className="w-full max-w-2xl overflow-hidden rounded-3xl border border-gray-200 bg-white shadow-[0_30px_100px_rgba(15,23,42,0.32)] dark:border-gray-700 dark:bg-gray-950"
>
<div className="border-b border-gray-200 px-5 py-4 dark:border-gray-800">
<label htmlFor="header-command-search" className="sr-only">
Search explorer destinations
</label>
<div className="flex items-center gap-3 rounded-2xl border border-gray-200 bg-gray-50 px-4 py-3 dark:border-gray-700 dark:bg-gray-900">
<SearchIcon className="h-5 w-5 text-gray-500 dark:text-gray-400" />
<input
id="header-command-search"
ref={inputRef}
value={query}
onChange={(event) => setQuery(event.target.value)}
onKeyDown={(event) => {
if (event.key === 'ArrowDown') {
event.preventDefault()
setActiveIndex((index) => Math.min(index + 1, filteredItems.length - 1))
}
if (event.key === 'ArrowUp') {
event.preventDefault()
setActiveIndex((index) => Math.max(index - 1, 0))
}
if (event.key === 'Enter') {
event.preventDefault()
const activeItem = filteredItems[activeIndex]
if (activeItem) void handleSelect(activeItem)
}
}}
placeholder={mode === 'expert' ? 'Search tx / addr / block / tool' : 'Search pages, tools, tokens, and routes'}
className="w-full border-0 bg-transparent text-sm text-gray-900 placeholder:text-gray-500 focus:outline-none dark:text-white dark:placeholder:text-gray-400"
/>
<kbd className="rounded-lg border border-gray-200 px-2 py-1 text-[11px] font-medium uppercase tracking-wide text-gray-500 dark:border-gray-700 dark:text-gray-400">
Esc
</kbd>
</div>
<Explain>
<p className="mt-3 text-xs leading-5 text-gray-500 dark:text-gray-400">
Search destinations and run high-frequency header actions from one keyboard-first surface.
</p>
</Explain>
</div>
<div className="max-h-[60vh] overflow-y-auto p-3">
<div className="grid gap-1.5">
{filteredItems.map((item, index) => (
<button
key={`${item.section}-${item.label}-${item.href || item.label}`}
ref={(node) => {
itemRefs.current[index] = node
}}
type="button"
onMouseEnter={() => setActiveIndex(index)}
onClick={() => void handleSelect(item)}
className={[
'flex w-full items-start gap-3 rounded-2xl px-4 py-3 text-left transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500',
activeIndex === index
? 'bg-primary-50 text-primary-900 dark:bg-primary-500/10 dark:text-primary-100'
: 'bg-white text-gray-800 hover:bg-gray-100 dark:bg-gray-950 dark:text-gray-100 dark:hover:bg-gray-900',
].join(' ')}
>
<span className="mt-0.5 inline-flex rounded-lg border border-gray-200 px-2 py-1 text-[11px] font-semibold uppercase tracking-wide text-gray-500 dark:border-gray-700 dark:text-gray-400">
{item.section}
</span>
<span className="min-w-0">
<span className="block font-semibold">{item.label}</span>
{mode === 'guided' && item.description ? (
<span className="mt-0.5 block text-xs leading-5 text-gray-500 dark:text-gray-400">
{item.description}
</span>
) : null}
</span>
</button>
))}
</div>
</div>
<div className="border-t border-gray-200 px-5 py-3 text-[11px] uppercase tracking-[0.16em] text-gray-500 dark:border-gray-800 dark:text-gray-400">
{mode === 'expert' ? 'Keyboard-first ' : 'Use '}
<kbd className="rounded border border-gray-200 px-1.5 py-0.5 font-medium dark:border-gray-700">/</kbd> or{' '}
<kbd className="rounded border border-gray-200 px-1.5 py-0.5 font-medium dark:border-gray-700">Ctrl/Cmd + K</kbd>{' '}
{mode === 'expert' ? 'to reopen.' : 'to reopen this palette.'}
</div>
</div>
<button
type="button"
onClick={onClose}
aria-label="Close command palette"
className="fixed inset-0 -z-10 cursor-default"
/>
</div>
)
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,57 @@
'use client'
import { createContext, type ReactNode, useContext, useEffect, useMemo, useState } from 'react'
export type UiMode = 'guided' | 'expert'
const UI_MODE_STORAGE_KEY = 'explorer_ui_mode'
const UiModeContext = createContext<{
mode: UiMode
setMode: (mode: UiMode) => void
toggleMode: () => void
} | null>(null)
export function UiModeProvider({ children }: { children: ReactNode }) {
const [mode, setModeState] = useState<UiMode>('guided')
useEffect(() => {
if (typeof window === 'undefined') return
const stored = window.localStorage.getItem(UI_MODE_STORAGE_KEY)
if (stored === 'guided' || stored === 'expert') {
setModeState(stored)
}
}, [])
const setMode = (nextMode: UiMode) => {
setModeState(nextMode)
if (typeof window !== 'undefined') {
window.localStorage.setItem(UI_MODE_STORAGE_KEY, nextMode)
}
}
const value = useMemo(
() => ({
mode,
setMode,
toggleMode: () => setMode(mode === 'guided' ? 'expert' : 'guided'),
}),
[mode],
)
return <UiModeContext.Provider value={value}>{children}</UiModeContext.Provider>
}
export function useUiMode() {
const context = useContext(UiModeContext)
if (!context) {
throw new Error('useUiMode must be used within a UiModeProvider')
}
return context
}
export function Explain({ children }: { children: ReactNode }) {
const { mode } = useUiMode()
if (mode === 'expert') return null
return <>{children}</>
}

View File

@@ -16,6 +16,10 @@ import {
} from '@/services/api/stats'
import { transactionsApi, type Transaction } from '@/services/api/transactions'
import { formatWeiAsEth } from '@/utils/format'
import { summarizeChainActivity } from '@/utils/activityContext'
import ActivityContextPanel from '@/components/common/ActivityContextPanel'
import FreshnessTrustNote from '@/components/common/FreshnessTrustNote'
import { resolveEffectiveFreshness, shouldExplainEmptyHeadBlocks } from '@/utils/explorerFreshness'
import OperationsPageShell, {
MetricCard,
StatusBadge,
@@ -121,6 +125,17 @@ export default function AnalyticsOperationsPage({
() => trailingWindow.reduce((max, point) => Math.max(max, point.transaction_count), 0),
[trailingWindow],
)
const activityContext = useMemo(
() =>
summarizeChainActivity({
blocks,
transactions,
latestBlockNumber: stats?.latest_block ?? blocks[0]?.number ?? null,
latestBlockTimestamp: blocks[0]?.timestamp ?? null,
freshness: resolveEffectiveFreshness(stats, bridgeStatus),
}),
[blocks, bridgeStatus, stats, transactions],
)
return (
<OperationsPageShell page={page}>
@@ -130,6 +145,17 @@ export default function AnalyticsOperationsPage({
</Card>
) : null}
<div className="mb-6">
<ActivityContextPanel context={activityContext} title="Analytics Freshness Context" />
<FreshnessTrustNote
className="mt-3"
context={activityContext}
stats={stats}
bridgeStatus={bridgeStatus}
scopeLabel="This page combines public stats, recent block samples, and indexed transactions."
/>
</div>
<div className="mb-6 grid gap-4 md:grid-cols-2 xl:grid-cols-4">
<MetricCard
title="Total Blocks"
@@ -264,6 +290,11 @@ export default function AnalyticsOperationsPage({
<Card title="Recent Blocks">
<div className="space-y-4">
{shouldExplainEmptyHeadBlocks(blocks, 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}
{blocks.map((block) => (
<div
key={block.hash}

View File

@@ -51,6 +51,14 @@ function resolveSnapshot(relay?: MissionControlRelayPayload): MissionControlRela
return relay?.url_probe?.body || relay?.file_snapshot || null
}
function relayPolicyCue(snapshot: MissionControlRelaySnapshot | null): string | null {
if (!snapshot) return null
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)) {
@@ -300,6 +308,11 @@ export default function BridgeMonitoringPage({
<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}
</Card>
))}
</div>

View File

@@ -5,6 +5,11 @@ import { explorerFeaturePages } from '@/data/explorerOperations'
import { configApi, type CapabilitiesResponse, type NetworksConfigResponse, type TokenListResponse } from '@/services/api/config'
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 ActivityContextPanel from '@/components/common/ActivityContextPanel'
import FreshnessTrustNote from '@/components/common/FreshnessTrustNote'
import { resolveEffectiveFreshness } from '@/utils/explorerFreshness'
function relativeAge(isoString?: string): string {
if (!isoString) return 'Unknown'
@@ -60,6 +65,7 @@ export default function OperationsHubPage({
initialTokenList = null,
initialCapabilities = 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)
@@ -138,6 +144,19 @@ export default function OperationsHubPage({
new Set((tokenList?.tokens || []).map((token) => token.symbol).filter(Boolean) as string[])
).slice(0, 8)
}, [tokenList])
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(null, bridgeStatus),
}),
[bridgeStatus],
)
return (
<div className="container mx-auto px-4 py-6 sm:py-8">
@@ -167,6 +186,16 @@ export default function OperationsHubPage({
</Card>
) : null}
<div className="mb-6">
<ActivityContextPanel context={activityContext} title="Operations Freshness Context" />
<FreshnessTrustNote
className="mt-3"
context={activityContext}
bridgeStatus={bridgeStatus}
scopeLabel="This page reflects mission-control freshness, public bridge status, and explorer-served config surfaces."
/>
</div>
<div className="mb-6 grid gap-4 md:grid-cols-2 xl:grid-cols-4">
<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">
@@ -226,7 +255,7 @@ export default function OperationsHubPage({
{relativeAge(bridgeStatus?.data?.checked_at)}
</div>
<div className="mt-2 text-sm text-gray-600 dark:text-gray-400">
Public mission-control snapshot freshness.
{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">
@@ -235,7 +264,7 @@ export default function OperationsHubPage({
{relativeAge(routeMatrix?.updated)}
</div>
<div className="mt-2 text-sm text-gray-600 dark:text-gray-400">
Token-aggregation route inventory timestamp.
{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">
@@ -244,7 +273,7 @@ export default function OperationsHubPage({
{networksConfig?.defaultChainId ?? 'Unknown'}
</div>
<div className="mt-2 text-sm text-gray-600 dark:text-gray-400">
Wallet onboarding points at Chain 138 by default.
{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">
@@ -255,7 +284,7 @@ export default function OperationsHubPage({
: 'Partial'}
</div>
<div className="mt-2 text-sm text-gray-600 dark:text-gray-400">
`wallet_addEthereumChain` and `wallet_watchAsset` compatibility.
{mode === 'guided' ? '`wallet_addEthereumChain` and `wallet_watchAsset` compatibility.' : 'Wallet RPC support.'}
</div>
</div>
</div>

View File

@@ -33,6 +33,14 @@ function relaySnapshot(relay: MissionControlRelayPayload | undefined) {
return relay?.url_probe?.body || relay?.file_snapshot
}
function relaySummary(snapshot: ReturnType<typeof relaySnapshot>) {
if (!snapshot) return 'destination unknown'
if (snapshot.status === 'paused' && snapshot.monitoring?.delivery_enabled === false) {
return `Delivery paused · queue ${formatNumber(snapshot.queue?.size ?? 0)}`
}
return `Queue ${formatNumber(snapshot.queue?.size ?? 0)} · ${snapshot.destination?.chain_name || 'destination unknown'}`
}
export default function WethOperationsPage({
initialBridgeStatus = null,
initialPlannerCapabilities = null,
@@ -112,13 +120,13 @@ export default function WethOperationsPage({
<MetricCard
title="Mainnet WETH Lane"
value={mainnetWeth?.status || 'unknown'}
description={`Queue ${formatNumber(mainnetWeth?.queue?.size ?? 0)} · ${mainnetWeth?.destination?.chain_name || 'destination unknown'}`}
description={relaySummary(mainnetWeth)}
className="border border-sky-200 bg-sky-50/70 dark:border-sky-900/50 dark:bg-sky-950/20"
/>
<MetricCard
title="Mainnet cW Lane"
value={mainnetCw?.status || 'unknown'}
description={`Queue ${formatNumber(mainnetCw?.queue?.size ?? 0)} · ${mainnetCw?.destination?.chain_name || 'destination unknown'}`}
description={relaySummary(mainnetCw)}
className="border border-emerald-200 bg-emerald-50/70 dark:border-emerald-900/50 dark:bg-emerald-950/20"
/>
<MetricCard

View File

@@ -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">

View File

@@ -6,6 +6,7 @@ import type {
} from '@/components/wallet/AddToMetaMask'
import { AddToMetaMask } from '@/components/wallet/AddToMetaMask'
import Link from 'next/link'
import { Explain, useUiMode } from '@/components/common/UiModeContext'
interface WalletPageProps {
initialNetworks?: NetworksCatalog | null
@@ -17,19 +18,35 @@ interface WalletPageProps {
}
export default function WalletPage(props: WalletPageProps) {
const { mode } = useUiMode()
return (
<main className="container mx-auto px-4 py-6 sm:py-8">
<h1 className="mb-4 text-2xl font-bold sm:text-3xl">Wallet & MetaMask</h1>
<h1 className="mb-4 text-2xl font-bold sm:text-3xl">Wallet Tools</h1>
<p className="mb-6 text-sm leading-7 text-gray-600 dark:text-gray-400 sm:text-base">
Connect Chain 138 (DeFi Oracle Meta Mainnet) and Ethereum Mainnet to MetaMask and other Web3 wallets. Use the token list URL so tokens and oracles are discoverable.
{mode === 'guided'
? 'Use the explorer-served network catalog, token list, and capability metadata to connect Chain 138 (DeFi Oracle Meta Mainnet) and Ethereum Mainnet to MetaMask and other Web3 wallets.'
: 'Use explorer-served network and token metadata to connect Chain 138 and Ethereum Mainnet wallets.'}
</p>
<AddToMetaMask {...props} />
<div className="mt-6 rounded-lg border border-gray-200 bg-white p-4 text-sm text-gray-600 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400">
Need swap and liquidity discovery too? Visit the{' '}
<Link href="/liquidity" className="font-medium text-primary-600 hover:underline dark:text-primary-400">
Liquidity Access
</Link>{' '}
page for live Chain 138 pools, route matrix links, partner payload templates, and the internal fallback execution plan endpoints.
<Explain>
<>
Need swap and liquidity discovery too? Visit the{' '}
<Link href="/liquidity" className="font-medium text-primary-600 hover:underline dark:text-primary-400">
Liquidity Access
</Link>{' '}
page for live Chain 138 pools, route matrix links, partner payload templates, and the internal fallback execution plan endpoints.
</>
</Explain>
{mode === 'expert' ? (
<>
Liquidity and planner posture lives on the{' '}
<Link href="/liquidity" className="font-medium text-primary-600 hover:underline dark:text-primary-400">
Liquidity Access
</Link>{' '}
surface.
</>
) : null}
</div>
</main>
)