Ship Tier A Week 1–2: posture glossary, delivery mode, freshness UI, canonical tokens.
Expose mission-control mode on home/bridge/analytics, quiet-chain freshness copy, and a canonical-first indexed token list with WETH9 metadata override and non-canonical warnings. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -75,6 +75,9 @@ export default function ActivityContextPanel({
|
||||
</Explain>
|
||||
</div>
|
||||
<EntityBadge label={resolveLabel(context.state)} tone={tone} />
|
||||
{context.head_is_idle && context.state === 'low' ? (
|
||||
<EntityBadge label="quiet chain" tone="info" />
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{compact ? (
|
||||
@@ -130,6 +133,11 @@ export default function ActivityContextPanel({
|
||||
Open last non-empty block →
|
||||
</Link>
|
||||
) : null}
|
||||
{context.block_gap_to_latest_transaction != null ? (
|
||||
<span>
|
||||
Block gap to latest visible transaction: {context.block_gap_to_latest_transaction.toLocaleString()}
|
||||
</span>
|
||||
) : null}
|
||||
{context.latest_transaction_timestamp ? (
|
||||
<span>Latest visible transaction time: {formatTimestamp(context.latest_transaction_timestamp)}</span>
|
||||
) : null}
|
||||
|
||||
@@ -3,10 +3,12 @@ import Navbar from './Navbar'
|
||||
import Footer from './Footer'
|
||||
import ExplorerAgentTool from './ExplorerAgentTool'
|
||||
import { UiModeProvider } from './UiModeContext'
|
||||
import { PostureGlossaryProvider } from './PostureGlossaryProvider'
|
||||
|
||||
export default function ExplorerChrome({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<UiModeProvider>
|
||||
<PostureGlossaryProvider>
|
||||
<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"
|
||||
@@ -21,6 +23,7 @@ export default function ExplorerChrome({ children }: { children: ReactNode }) {
|
||||
<ExplorerAgentTool />
|
||||
<Footer />
|
||||
</div>
|
||||
</PostureGlossaryProvider>
|
||||
</UiModeProvider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -8,11 +8,15 @@ import {
|
||||
import { formatRelativeAge } from '@/utils/format'
|
||||
import { useUiMode } from './UiModeContext'
|
||||
|
||||
function buildSummary(context: ChainActivityContext) {
|
||||
function buildSummary(context: ChainActivityContext, activityState?: string | null) {
|
||||
if (context.transaction_visibility_unavailable) {
|
||||
return 'Chain-head visibility is current, while transaction freshness is currently unavailable.'
|
||||
}
|
||||
|
||||
if (activityState === 'quiet_chain' || (context.head_is_idle && context.state === 'low')) {
|
||||
return 'The chain head is current, but recent head blocks are quiet — this is normal low-activity visibility, not a broken index.'
|
||||
}
|
||||
|
||||
if (context.state === 'active') {
|
||||
return 'Chain head and latest indexed transactions are closely aligned.'
|
||||
}
|
||||
@@ -28,11 +32,21 @@ function buildSummary(context: ChainActivityContext) {
|
||||
return 'Freshness context is based on the latest visible public explorer evidence.'
|
||||
}
|
||||
|
||||
function buildDetail(context: ChainActivityContext, diagnosticExplanation?: string | null) {
|
||||
function buildDetail(context: ChainActivityContext, diagnosticExplanation?: string | null, activityState?: string | null) {
|
||||
if (diagnosticExplanation) {
|
||||
return diagnosticExplanation
|
||||
}
|
||||
|
||||
if (activityState === 'quiet_chain') {
|
||||
const latestNonEmptyBlock =
|
||||
context.last_non_empty_block_number != null ? `#${context.last_non_empty_block_number.toLocaleString()}` : 'unknown'
|
||||
const blockGap =
|
||||
context.block_gap_to_latest_transaction != null
|
||||
? `${context.block_gap_to_latest_transaction.toLocaleString()} blocks`
|
||||
: 'unknown'
|
||||
return `Quiet-chain signal: head blocks may be empty while the chain remains current. Block gap to latest visible transaction: ${blockGap}. Last non-empty block: ${latestNonEmptyBlock}.`
|
||||
}
|
||||
|
||||
if (context.transaction_visibility_unavailable) {
|
||||
return 'Use chain-head visibility and the last non-empty block as the current trust anchors.'
|
||||
}
|
||||
@@ -40,9 +54,13 @@ function buildDetail(context: ChainActivityContext, diagnosticExplanation?: stri
|
||||
const latestTxAge = formatRelativeAge(context.latest_transaction_timestamp)
|
||||
const latestNonEmptyBlock =
|
||||
context.last_non_empty_block_number != null ? `#${context.last_non_empty_block_number.toLocaleString()}` : 'unknown'
|
||||
const blockGap =
|
||||
context.block_gap_to_latest_transaction != null
|
||||
? `${context.block_gap_to_latest_transaction.toLocaleString()} blocks`
|
||||
: null
|
||||
|
||||
if (context.head_is_idle) {
|
||||
return `Latest visible transaction: ${latestTxAge}. Last non-empty block: ${latestNonEmptyBlock}.`
|
||||
return `Latest visible transaction: ${latestTxAge}. Block gap: ${blockGap || 'unknown'}. Last non-empty block: ${latestNonEmptyBlock}.`
|
||||
}
|
||||
|
||||
if (context.state === 'active') {
|
||||
@@ -74,13 +92,14 @@ export default function FreshnessTrustNote({
|
||||
const sourceLabel = resolveFreshnessSourceLabel(stats, bridgeStatus)
|
||||
const confidenceBadges = summarizeFreshnessConfidence(stats, bridgeStatus)
|
||||
const diagnosticExplanation = stats?.diagnostics?.explanation || bridgeStatus?.data?.diagnostics?.explanation || null
|
||||
const activityState = stats?.diagnostics?.activity_state || bridgeStatus?.data?.diagnostics?.activity_state || null
|
||||
const normalizedClassName = className ? ` ${className}` : ''
|
||||
|
||||
if (mode === 'expert') {
|
||||
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="flex flex-col gap-2 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div className="font-medium text-gray-900 dark:text-white">{buildSummary(context)}</div>
|
||||
<div className="font-medium text-gray-900 dark:text-white">{buildSummary(context, activityState)}</div>
|
||||
<div className="text-xs text-gray-600 dark:text-gray-400">{sourceLabel}</div>
|
||||
</div>
|
||||
<div className="mt-2 flex flex-wrap gap-2 text-xs text-gray-500 dark:text-gray-400">
|
||||
@@ -99,9 +118,9 @@ export default function FreshnessTrustNote({
|
||||
|
||||
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="font-medium text-gray-900 dark:text-white">{buildSummary(context, activityState)}</div>
|
||||
<div className="mt-1 text-gray-600 dark:text-gray-400">
|
||||
{normalizeSentence(buildDetail(context, diagnosticExplanation))}.{' '}
|
||||
{normalizeSentence(buildDetail(context, diagnosticExplanation, activityState))}.{' '}
|
||||
{scopeLabel ? `${normalizeSentence(scopeLabel)}. ` : ''}
|
||||
{normalizeSentence(sourceLabel)}.
|
||||
</div>
|
||||
|
||||
76
frontend/src/components/common/MissionDeliveryModePanel.tsx
Normal file
76
frontend/src/components/common/MissionDeliveryModePanel.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
'use client'
|
||||
|
||||
import { Card } from '@/libs/frontend-ui-primitives'
|
||||
import EntityBadge from '@/components/common/EntityBadge'
|
||||
import type { MissionControlMode } from '@/services/api/missionControl'
|
||||
import { formatRelativeAge } from '@/utils/format'
|
||||
|
||||
const REASON_LABELS: Record<string, string> = {
|
||||
live_homepage_stream_not_attached: 'Live homepage stream is not attached; relay posture uses snapshot polling.',
|
||||
relay_snapshot_only_source: 'Relay monitoring uses snapshot sources while other explorer feeds remain live.',
|
||||
partial_observability_inputs: 'Some freshness inputs are partial, so posture is reported conservatively.',
|
||||
}
|
||||
|
||||
const SCOPE_LABELS: Record<string, string> = {
|
||||
relay_monitoring_homepage_card_only: 'Affects relay monitoring and the homepage mission card only.',
|
||||
bridge_monitoring_and_homepage: 'Affects bridge monitoring and homepage summary surfaces.',
|
||||
homepage_summary_only: 'Affects homepage summary messaging only.',
|
||||
}
|
||||
|
||||
function humanizeKey(value?: string | null): string {
|
||||
if (!value) return 'Not specified'
|
||||
return SCOPE_LABELS[value] || REASON_LABELS[value] || value.replaceAll('_', ' ')
|
||||
}
|
||||
|
||||
function modeTone(kind?: string | null): 'success' | 'warning' | 'info' | 'neutral' {
|
||||
switch (String(kind || '').toLowerCase()) {
|
||||
case 'live':
|
||||
return 'success'
|
||||
case 'mixed':
|
||||
return 'warning'
|
||||
case 'snapshot':
|
||||
return 'info'
|
||||
default:
|
||||
return 'neutral'
|
||||
}
|
||||
}
|
||||
|
||||
export default function MissionDeliveryModePanel({
|
||||
mode,
|
||||
title = 'Delivery mode',
|
||||
className = '',
|
||||
}: {
|
||||
mode?: MissionControlMode | null
|
||||
title?: string
|
||||
className?: string
|
||||
}) {
|
||||
if (!mode?.kind) return null
|
||||
|
||||
const normalizedClassName = className ? ` ${className}` : ''
|
||||
|
||||
return (
|
||||
<Card
|
||||
className={`border border-indigo-200 bg-indigo-50/60 dark:border-indigo-900/40 dark:bg-indigo-950/20${normalizedClassName}`}
|
||||
title={title}
|
||||
>
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<EntityBadge label={`mode ${mode.kind}`} tone={modeTone(mode.kind)} />
|
||||
{mode.updated_at ? (
|
||||
<span className="text-xs text-gray-600 dark:text-gray-400">Updated {formatRelativeAge(mode.updated_at)}</span>
|
||||
) : null}
|
||||
</div>
|
||||
{mode.reason ? (
|
||||
<p className="text-sm leading-6 text-gray-700 dark:text-gray-300">
|
||||
<span className="font-medium text-gray-900 dark:text-white">Reason:</span> {humanizeKey(String(mode.reason))}
|
||||
</p>
|
||||
) : null}
|
||||
{mode.scope ? (
|
||||
<p className="text-sm leading-6 text-gray-700 dark:text-gray-300">
|
||||
<span className="font-medium text-gray-900 dark:text-white">Scope:</span> {humanizeKey(String(mode.scope))}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
33
frontend/src/components/common/PostureBadge.tsx
Normal file
33
frontend/src/components/common/PostureBadge.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
'use client'
|
||||
|
||||
import EntityBadge from '@/components/common/EntityBadge'
|
||||
import { usePostureGlossary } from '@/components/common/PostureGlossaryProvider'
|
||||
import { resolvePostureTermId } from '@/data/postureGlossary'
|
||||
|
||||
export default function PostureBadge({
|
||||
label,
|
||||
tone,
|
||||
className,
|
||||
}: {
|
||||
label: string
|
||||
tone?: 'neutral' | 'success' | 'warning' | 'info'
|
||||
className?: string
|
||||
}) {
|
||||
const { openTerm } = usePostureGlossary()
|
||||
const termId = resolvePostureTermId(label)
|
||||
|
||||
if (!termId) {
|
||||
return <EntityBadge label={label} tone={tone} className={className} />
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => openTerm(termId)}
|
||||
title="Open posture glossary"
|
||||
className="rounded-full focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500"
|
||||
>
|
||||
<EntityBadge label={label} tone={tone} className={className} />
|
||||
</button>
|
||||
)
|
||||
}
|
||||
82
frontend/src/components/common/PostureGlossaryProvider.tsx
Normal file
82
frontend/src/components/common/PostureGlossaryProvider.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
'use client'
|
||||
|
||||
import { createContext, useCallback, useContext, useMemo, useState, type ReactNode } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { getPostureGlossaryTerm, type PostureGlossaryTermId } from '@/data/postureGlossary'
|
||||
|
||||
interface PostureGlossaryContextValue {
|
||||
openTerm: (termId: PostureGlossaryTermId) => void
|
||||
close: () => void
|
||||
}
|
||||
|
||||
const PostureGlossaryContext = createContext<PostureGlossaryContextValue | null>(null)
|
||||
|
||||
export function PostureGlossaryProvider({ children }: { children: ReactNode }) {
|
||||
const [activeTermId, setActiveTermId] = useState<PostureGlossaryTermId | null>(null)
|
||||
const activeTerm = useMemo(
|
||||
() => (activeTermId ? getPostureGlossaryTerm(activeTermId) ?? null : null),
|
||||
[activeTermId],
|
||||
)
|
||||
|
||||
const openTerm = useCallback((termId: PostureGlossaryTermId) => {
|
||||
setActiveTermId(termId)
|
||||
}, [])
|
||||
|
||||
const close = useCallback(() => {
|
||||
setActiveTermId(null)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<PostureGlossaryContext.Provider value={{ openTerm, close }}>
|
||||
{children}
|
||||
{activeTerm ? (
|
||||
<div className="fixed inset-0 z-[80] flex items-end justify-center bg-black/45 p-4 sm:items-center" onClick={close}>
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="posture-glossary-title"
|
||||
className="max-h-[85vh] w-full max-w-xl overflow-y-auto rounded-2xl border border-gray-200 bg-white p-6 shadow-2xl dark:border-gray-700 dark:bg-gray-950"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<p className="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Posture glossary</p>
|
||||
<h2 id="posture-glossary-title" className="mt-1 text-xl font-semibold text-gray-900 dark:text-white">
|
||||
{activeTerm.title}
|
||||
</h2>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={close}
|
||||
className="rounded-lg border border-gray-300 px-3 py-1.5 text-sm font-medium text-gray-700 hover:bg-gray-50 dark:border-gray-700 dark:text-gray-200 dark:hover:bg-gray-900"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
<p className="mt-4 text-sm leading-6 text-gray-700 dark:text-gray-300">{activeTerm.summary}</p>
|
||||
<div className="mt-4 rounded-xl border border-sky-200 bg-sky-50/70 p-4 text-sm leading-6 text-sky-950 dark:border-sky-900/50 dark:bg-sky-950/20 dark:text-sky-100">
|
||||
<p className="text-xs font-semibold uppercase tracking-wide text-sky-700 dark:text-sky-300">Methodology</p>
|
||||
<p className="mt-2">{activeTerm.methodology}</p>
|
||||
</div>
|
||||
<div className="mt-5 flex flex-wrap gap-3 text-sm">
|
||||
<Link href="/docs/posture-glossary" className="font-medium text-primary-600 hover:underline" onClick={close}>
|
||||
Full glossary
|
||||
</Link>
|
||||
<Link href="/docs/gru" className="font-medium text-primary-600 hover:underline" onClick={close}>
|
||||
GRU guide
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</PostureGlossaryContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function usePostureGlossary() {
|
||||
const context = useContext(PostureGlossaryContext)
|
||||
if (!context) {
|
||||
throw new Error('usePostureGlossary must be used within PostureGlossaryProvider')
|
||||
}
|
||||
return context
|
||||
}
|
||||
Reference in New Issue
Block a user