diff --git a/backend/api/track1/bridge_mode.go b/backend/api/track1/bridge_mode.go
new file mode 100644
index 0000000..61cf2ae
--- /dev/null
+++ b/backend/api/track1/bridge_mode.go
@@ -0,0 +1,78 @@
+package track1
+
+import (
+ "strings"
+
+ "github.com/explorer/backend/api/freshness"
+)
+
+type bridgeDeliveryMode struct {
+ Kind string
+ Reason any
+ Scope any
+}
+
+func resolveBridgeDeliveryMode(hasRelays bool, diagnostics *freshness.Diagnostics, txFeed freshness.Completeness) bridgeDeliveryMode {
+ if !hasRelays {
+ if diagnostics != nil && isStaleTransactionVisibility(diagnostics) {
+ return bridgeDeliveryMode{
+ Kind: "mixed",
+ Reason: "partial_observability_inputs",
+ Scope: "homepage_summary_only",
+ }
+ }
+ return bridgeDeliveryMode{
+ Kind: "live",
+ Reason: nil,
+ Scope: nil,
+ }
+ }
+
+ if diagnostics != nil && isStaleTransactionVisibility(diagnostics) {
+ return bridgeDeliveryMode{
+ Kind: "mixed",
+ Reason: "relay_snapshot_only_source",
+ Scope: "bridge_monitoring_and_homepage",
+ }
+ }
+
+ if txFeed == freshness.CompletenessPartial || txFeed == freshness.CompletenessStale {
+ return bridgeDeliveryMode{
+ Kind: "mixed",
+ Reason: "partial_observability_inputs",
+ Scope: "bridge_monitoring_and_homepage",
+ }
+ }
+
+ return bridgeDeliveryMode{
+ Kind: "snapshot",
+ Reason: "live_homepage_stream_not_attached",
+ Scope: "relay_monitoring_homepage_card_only",
+ }
+}
+
+func isStaleTransactionVisibility(diagnostics *freshness.Diagnostics) bool {
+ if diagnostics == nil {
+ return false
+ }
+ state := strings.ToLower(strings.TrimSpace(diagnostics.ActivityState))
+ switch state {
+ case "fresh_head_stale_transaction_visibility", "lagging", "stale_transaction_visibility":
+ return true
+ default:
+ return strings.Contains(state, "stale") && strings.Contains(state, "transaction")
+ }
+}
+
+func buildBridgeModePayload(now string, resolved bridgeDeliveryMode) map[string]interface{} {
+ return map[string]interface{}{
+ "kind": resolved.Kind,
+ "updated_at": now,
+ "age_seconds": int64(0),
+ "reason": resolved.Reason,
+ "scope": resolved.Scope,
+ "source": freshness.SourceReported,
+ "confidence": freshness.ConfidenceHigh,
+ "provenance": freshness.ProvenanceMissionFeed,
+ }
+}
diff --git a/backend/api/track1/bridge_mode_test.go b/backend/api/track1/bridge_mode_test.go
new file mode 100644
index 0000000..f6d7b3f
--- /dev/null
+++ b/backend/api/track1/bridge_mode_test.go
@@ -0,0 +1,36 @@
+package track1
+
+import (
+ "testing"
+
+ "github.com/explorer/backend/api/freshness"
+ "github.com/stretchr/testify/require"
+)
+
+func TestResolveBridgeDeliveryModeLiveWithoutRelays(t *testing.T) {
+ got := resolveBridgeDeliveryMode(false, nil, freshness.CompletenessComplete)
+ require.Equal(t, "live", got.Kind)
+ require.Nil(t, got.Reason)
+}
+
+func TestResolveBridgeDeliveryModeSnapshotWithRelays(t *testing.T) {
+ got := resolveBridgeDeliveryMode(true, nil, freshness.CompletenessComplete)
+ require.Equal(t, "snapshot", got.Kind)
+ require.Equal(t, "live_homepage_stream_not_attached", got.Reason)
+ require.Equal(t, "relay_monitoring_homepage_card_only", got.Scope)
+}
+
+func TestResolveBridgeDeliveryModeMixedWhenTransactionVisibilityStale(t *testing.T) {
+ diagnostics := &freshness.Diagnostics{
+ ActivityState: "fresh_head_stale_transaction_visibility",
+ }
+ got := resolveBridgeDeliveryMode(true, diagnostics, freshness.CompletenessPartial)
+ require.Equal(t, "mixed", got.Kind)
+ require.Equal(t, "relay_snapshot_only_source", got.Reason)
+ require.Equal(t, "bridge_monitoring_and_homepage", got.Scope)
+}
+
+func TestIsStaleTransactionVisibility(t *testing.T) {
+ require.True(t, isStaleTransactionVisibility(&freshness.Diagnostics{ActivityState: "fresh_head_stale_transaction_visibility"}))
+ require.False(t, isStaleTransactionVisibility(&freshness.Diagnostics{ActivityState: "healthy"}))
+}
diff --git a/backend/api/track1/bridge_status_data.go b/backend/api/track1/bridge_status_data.go
index 44273f6..d66f9e0 100644
--- a/backend/api/track1/bridge_status_data.go
+++ b/backend/api/track1/bridge_status_data.go
@@ -133,6 +133,8 @@ func (s *Server) BuildBridgeStatusData(ctx context.Context) map[string]interface
}
if s.freshnessLoader != nil {
if snapshot, completeness, sampling, diagnostics, err := s.freshnessLoader(ctx); err == nil && snapshot != nil {
+ txFeed := completeness.TransactionsFeed
+ resolvedMode := resolveBridgeDeliveryMode(false, diagnostics, txFeed)
subsystems := map[string]interface{}{
"rpc_head": map[string]interface{}{
"status": chainStatusFromProbe(p138),
@@ -174,39 +176,13 @@ func (s *Server) BuildBridgeStatusData(ctx context.Context) map[string]interface
"issues": sampling.Issues,
}
}
- modeKind := "live"
- modeReason := any(nil)
- modeScope := any(nil)
- if relays, ok := data["ccip_relays"].(map[string]interface{}); ok && len(relays) > 0 {
- modeKind = "snapshot"
- modeReason = "live_homepage_stream_not_attached"
- modeScope = "relay_monitoring_homepage_card_only"
- subsystems["bridge_relay_monitoring"] = map[string]interface{}{
- "status": overall,
- "updated_at": now,
- "age_seconds": int64(0),
- "source": freshness.SourceReported,
- "confidence": freshness.ConfidenceHigh,
- "provenance": freshness.ProvenanceMissionFeed,
- "completeness": freshness.CompletenessComplete,
- }
- }
data["freshness"] = snapshot
data["subsystems"] = subsystems
data["sampling"] = sampling
if diagnostics != nil {
data["diagnostics"] = diagnostics
}
- data["mode"] = map[string]interface{}{
- "kind": modeKind,
- "updated_at": now,
- "age_seconds": int64(0),
- "reason": modeReason,
- "scope": modeScope,
- "source": freshness.SourceReported,
- "confidence": freshness.ConfidenceHigh,
- "provenance": freshness.ProvenanceMissionFeed,
- }
+ data["mode"] = buildBridgeModePayload(now, resolvedMode)
}
}
if relays := FetchCCIPRelayHealths(ctx); relays != nil {
@@ -224,9 +200,22 @@ func (s *Server) BuildBridgeStatusData(ctx context.Context) map[string]interface
}
if mode, ok := data["mode"].(map[string]interface{}); ok {
if relays, ok := data["ccip_relays"].(map[string]interface{}); ok && len(relays) > 0 {
- mode["kind"] = "snapshot"
- mode["reason"] = "live_homepage_stream_not_attached"
- mode["scope"] = "relay_monitoring_homepage_card_only"
+ var diagnostics *freshness.Diagnostics
+ if diag, ok := data["diagnostics"].(*freshness.Diagnostics); ok {
+ diagnostics = diag
+ }
+ txFeed := freshness.CompletenessUnavailable
+ if subsystems, ok := data["subsystems"].(map[string]interface{}); ok {
+ if txIndex, ok := subsystems["tx_index"].(map[string]interface{}); ok {
+ if feed, ok := txIndex["completeness"].(freshness.Completeness); ok {
+ txFeed = feed
+ }
+ }
+ }
+ resolved := resolveBridgeDeliveryMode(true, diagnostics, txFeed)
+ mode["kind"] = resolved.Kind
+ mode["reason"] = resolved.Reason
+ mode["scope"] = resolved.Scope
if subsystems, ok := data["subsystems"].(map[string]interface{}); ok {
subsystems["bridge_relay_monitoring"] = map[string]interface{}{
"status": data["status"],
@@ -239,6 +228,9 @@ func (s *Server) BuildBridgeStatusData(ctx context.Context) map[string]interface
}
}
}
+ } else if relays, ok := data["ccip_relays"].(map[string]interface{}); ok && len(relays) > 0 {
+ resolved := resolveBridgeDeliveryMode(true, nil, freshness.CompletenessUnavailable)
+ data["mode"] = buildBridgeModePayload(now, resolved)
}
return data
}
diff --git a/backend/api/track1/ccip_health_test.go b/backend/api/track1/ccip_health_test.go
index 35b51de..ca249e9 100644
--- a/backend/api/track1/ccip_health_test.go
+++ b/backend/api/track1/ccip_health_test.go
@@ -212,6 +212,11 @@ func TestBuildBridgeStatusDataIncludesCCIPRelay(t *testing.T) {
require.Contains(t, got, "diagnostics")
require.Contains(t, got, "subsystems")
require.Contains(t, got, "mode")
+ mode, ok := got["mode"].(map[string]interface{})
+ require.True(t, ok)
+ require.Equal(t, "mixed", mode["kind"])
+ require.Equal(t, "relay_snapshot_only_source", mode["reason"])
+ require.Equal(t, "bridge_monitoring_and_homepage", mode["scope"])
}
func TestBuildBridgeStatusDataDegradesWhenNamedRelayFails(t *testing.T) {
diff --git a/frontend/src/components/common/ActivityContextPanel.tsx b/frontend/src/components/common/ActivityContextPanel.tsx
index 09f6e92..ff6c867 100644
--- a/frontend/src/components/common/ActivityContextPanel.tsx
+++ b/frontend/src/components/common/ActivityContextPanel.tsx
@@ -75,6 +75,9 @@ export default function ActivityContextPanel({
+ {context.head_is_idle && context.state === 'low' ? (
+
+ ) : null}
{compact ? (
@@ -130,6 +133,11 @@ export default function ActivityContextPanel({
Open last non-empty block →
) : null}
+ {context.block_gap_to_latest_transaction != null ? (
+
+ Block gap to latest visible transaction: {context.block_gap_to_latest_transaction.toLocaleString()}
+
+ ) : null}
{context.latest_transaction_timestamp ? (
Latest visible transaction time: {formatTimestamp(context.latest_transaction_timestamp)}
) : null}
diff --git a/frontend/src/components/common/ExplorerChrome.tsx b/frontend/src/components/common/ExplorerChrome.tsx
index ca92c6b..df1c9bc 100644
--- a/frontend/src/components/common/ExplorerChrome.tsx
+++ b/frontend/src/components/common/ExplorerChrome.tsx
@@ -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 (
+
+
)
}
diff --git a/frontend/src/components/common/FreshnessTrustNote.tsx b/frontend/src/components/common/FreshnessTrustNote.tsx
index 99c496e..596953f 100644
--- a/frontend/src/components/common/FreshnessTrustNote.tsx
+++ b/frontend/src/components/common/FreshnessTrustNote.tsx
@@ -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 (
-
{buildSummary(context)}
+
{buildSummary(context, activityState)}
{sourceLabel}
@@ -99,9 +118,9 @@ export default function FreshnessTrustNote({
return (
-
{buildSummary(context)}
+
{buildSummary(context, activityState)}
- {normalizeSentence(buildDetail(context, diagnosticExplanation))}.{' '}
+ {normalizeSentence(buildDetail(context, diagnosticExplanation, activityState))}.{' '}
{scopeLabel ? `${normalizeSentence(scopeLabel)}. ` : ''}
{normalizeSentence(sourceLabel)}.
diff --git a/frontend/src/components/common/MissionDeliveryModePanel.tsx b/frontend/src/components/common/MissionDeliveryModePanel.tsx
new file mode 100644
index 0000000..37d9c91
--- /dev/null
+++ b/frontend/src/components/common/MissionDeliveryModePanel.tsx
@@ -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
= {
+ 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 = {
+ 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 (
+
+
+
+
+ {mode.updated_at ? (
+ Updated {formatRelativeAge(mode.updated_at)}
+ ) : null}
+
+ {mode.reason ? (
+
+ Reason: {humanizeKey(String(mode.reason))}
+
+ ) : null}
+ {mode.scope ? (
+
+ Scope: {humanizeKey(String(mode.scope))}
+
+ ) : null}
+
+
+ )
+}
diff --git a/frontend/src/components/common/PostureBadge.tsx b/frontend/src/components/common/PostureBadge.tsx
new file mode 100644
index 0000000..909e646
--- /dev/null
+++ b/frontend/src/components/common/PostureBadge.tsx
@@ -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
+ }
+
+ return (
+ openTerm(termId)}
+ title="Open posture glossary"
+ className="rounded-full focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500"
+ >
+
+
+ )
+}
diff --git a/frontend/src/components/common/PostureGlossaryProvider.tsx b/frontend/src/components/common/PostureGlossaryProvider.tsx
new file mode 100644
index 0000000..e5cfa14
--- /dev/null
+++ b/frontend/src/components/common/PostureGlossaryProvider.tsx
@@ -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(null)
+
+export function PostureGlossaryProvider({ children }: { children: ReactNode }) {
+ const [activeTermId, setActiveTermId] = useState(null)
+ const activeTerm = useMemo(
+ () => (activeTermId ? getPostureGlossaryTerm(activeTermId) ?? null : null),
+ [activeTermId],
+ )
+
+ const openTerm = useCallback((termId: PostureGlossaryTermId) => {
+ setActiveTermId(termId)
+ }, [])
+
+ const close = useCallback(() => {
+ setActiveTermId(null)
+ }, [])
+
+ return (
+
+ {children}
+ {activeTerm ? (
+
+
event.stopPropagation()}
+ >
+
+
+
Posture glossary
+
+ {activeTerm.title}
+
+
+
+ Close
+
+
+
{activeTerm.summary}
+
+
Methodology
+
{activeTerm.methodology}
+
+
+
+ Full glossary
+
+
+ GRU guide
+
+
+
+
+ ) : null}
+
+ )
+}
+
+export function usePostureGlossary() {
+ const context = useContext(PostureGlossaryContext)
+ if (!context) {
+ throw new Error('usePostureGlossary must be used within PostureGlossaryProvider')
+ }
+ return context
+}
diff --git a/frontend/src/components/explorer/AnalyticsOperationsPage.tsx b/frontend/src/components/explorer/AnalyticsOperationsPage.tsx
index c8a7e04..512d448 100644
--- a/frontend/src/components/explorer/AnalyticsOperationsPage.tsx
+++ b/frontend/src/components/explorer/AnalyticsOperationsPage.tsx
@@ -19,6 +19,7 @@ import { formatWeiAsEth } from '@/utils/format'
import { summarizeChainActivity } from '@/utils/activityContext'
import ActivityContextPanel from '@/components/common/ActivityContextPanel'
import FreshnessTrustNote from '@/components/common/FreshnessTrustNote'
+import MissionDeliveryModePanel from '@/components/common/MissionDeliveryModePanel'
import SubsystemPosturePanel from '@/components/common/SubsystemPosturePanel'
import { resolveEffectiveFreshness, shouldExplainEmptyHeadBlocks } from '@/utils/explorerFreshness'
import OperationsPageShell, {
@@ -156,6 +157,7 @@ export default function AnalyticsOperationsPage({
bridgeStatus={bridgeStatus}
scopeLabel="This page combines public stats, recent block samples, and indexed transactions."
/>
+
+
+
+
+
+
setStatsDetailsExpanded((current) => !current)}
className="text-sm font-semibold text-primary-600 hover:underline"
>
- {statsDetailsExpanded ? 'Hide telemetry and freshness' : 'Show telemetry and freshness'}
+ {statsDetailsExpanded ? 'Hide extended telemetry' : 'Show extended telemetry'}
{statsDetailsExpanded ? (
@@ -849,22 +868,6 @@ export default function Home({
)}
-
-
>
) : null}
diff --git a/frontend/src/data/postureGlossary.ts b/frontend/src/data/postureGlossary.ts
new file mode 100644
index 0000000..55b6d55
--- /dev/null
+++ b/frontend/src/data/postureGlossary.ts
@@ -0,0 +1,101 @@
+export type PostureGlossaryTermId =
+ | 'x402'
+ | 'iso20022'
+ | 'forward-canonical'
+ | 'reference-asset'
+ | 'cw-public-network'
+ | 'transport-active'
+ | 'gru'
+
+export interface PostureGlossaryTerm {
+ id: PostureGlossaryTermId
+ title: string
+ shortLabel: string
+ summary: string
+ methodology: string
+}
+
+export const postureGlossaryTerms: PostureGlossaryTerm[] = [
+ {
+ id: 'gru',
+ title: 'GRU instrument',
+ shortLabel: 'GRU',
+ summary: 'A Global Reserve Unit instrument issued under DBIS taxonomy on Chain 138 or represented on public networks.',
+ methodology:
+ 'The explorer marks GRU when token metadata, registry tags, or curated catalog entries align with the GRU transport and compliance model documented in the GRU guide.',
+ },
+ {
+ id: 'x402',
+ title: 'x402 readiness',
+ shortLabel: 'x402 ready',
+ summary: 'The token surface exposes the typed-data and domain metadata commonly required for HTTP-native payment authorization flows.',
+ methodology:
+ 'Readiness is inferred from detected signing surfaces (EIP-712 domain, ERC-5267, and ERC-2612 or ERC-3009). It describes technical capability — not a guarantee that a live x402 merchant endpoint exists.',
+ },
+ {
+ id: 'iso20022',
+ title: 'ISO-20022 alignment',
+ shortLabel: 'ISO-20022',
+ summary: 'The asset is modeled as part of the ISO-20022-aligned settlement and reporting posture for institutional messaging.',
+ methodology:
+ 'This badge reflects GRU metadata and governance expectations around supervised disclosure — not a claim that every transfer is already formatted as an ISO-20022 message on-chain.',
+ },
+ {
+ id: 'forward-canonical',
+ title: 'Forward-canonical posture',
+ shortLabel: 'forward canonical',
+ summary: 'The asset version is the forward-looking canonical representation operators should wire for new integrations.',
+ methodology:
+ 'Used when a token family has legacy or parallel deployments. Prefer forward-canonical addresses for routers, wallets, and explorer deep links unless a migration note says otherwise.',
+ },
+ {
+ id: 'reference-asset',
+ title: 'Reference asset',
+ shortLabel: 'reference asset',
+ summary: 'A non-GRU mirrored or externally issued asset shown for routing, liquidity, or price context.',
+ methodology:
+ 'Reference assets help explain cross-chain routes and pool composition. They are not implied to be DBIS-issued compliant instruments unless separately tagged.',
+ },
+ {
+ id: 'cw-public-network',
+ title: 'cW public-network representation',
+ shortLabel: 'cW public-network',
+ summary: 'A wrapped GRU instrument activated on a public network (for example mainnet cWUSDC) while Chain 138 remains the program ledger.',
+ methodology:
+ 'Public-network overlays use cW* naming. Liquidity and bridge lanes may reference these addresses even when the canonical compliant token lives on Chain 138.',
+ },
+ {
+ id: 'transport-active',
+ title: 'transportActive (config compatibility)',
+ shortLabel: 'transportActive',
+ summary: 'Legacy JSON key indicating whether a public-network transport overlay is active in published `/config` manifests.',
+ methodology:
+ 'Machine consumers should treat this as a v1 compatibility field. A future v2 schema will expose `publicNetworkActive` aliases before old keys are removed.',
+ },
+]
+
+const labelToTermId: Record
= {
+ gru: 'gru',
+ 'x402 ready': 'x402',
+ 'x402 not ready': 'x402',
+ 'iso-20022': 'iso20022',
+ 'iso-20022 aligned': 'iso20022',
+ 'iso-20022 unclear': 'iso20022',
+ 'forward canonical': 'forward-canonical',
+ 'reference asset': 'reference-asset',
+ wrapped: 'cw-public-network',
+ transportactive: 'transport-active',
+}
+
+export function resolvePostureTermId(label: string): PostureGlossaryTermId | null {
+ const normalized = label.trim().toLowerCase()
+ if (labelToTermId[normalized]) return labelToTermId[normalized]
+ if (normalized.startsWith('cw public-network')) return 'cw-public-network'
+ if (normalized.startsWith('forward ')) return 'forward-canonical'
+ if (normalized.includes('transportactive') || normalized.includes('transport active')) return 'transport-active'
+ return null
+}
+
+export function getPostureGlossaryTerm(id: PostureGlossaryTermId): PostureGlossaryTerm | undefined {
+ return postureGlossaryTerms.find((term) => term.id === id)
+}
diff --git a/frontend/src/pages/addresses/[address].tsx b/frontend/src/pages/addresses/[address].tsx
index 9a2eebe..13b0b49 100644
--- a/frontend/src/pages/addresses/[address].tsx
+++ b/frontend/src/pages/addresses/[address].tsx
@@ -21,6 +21,7 @@ import {
import { formatTimestamp, formatTokenAmount, formatWeiAsEth } from '@/utils/format'
import { DetailRow } from '@/components/common/DetailRow'
import EntityBadge from '@/components/common/EntityBadge'
+import PostureBadge from '@/components/common/PostureBadge'
import {
isWatchlistEntry,
readWatchlistFromStorage,
@@ -377,8 +378,8 @@ export default function AddressDetailPage() {
{balance.token_symbol || balance.token_name || 'Token'}
)}
{gruMetadata ? : null}
- {gruMetadata?.x402Ready ? : null}
- {gruMetadata?.iso20022Ready ? : null}
+ {gruMetadata?.x402Ready ? : null}
+ {gruMetadata?.iso20022Ready ? : null}
{balance.token_name && balance.token_symbol && (
{balance.token_name}
@@ -430,9 +431,9 @@ export default function AddressDetailPage() {
{transfer.token_symbol || transfer.token_name || 'Token'}
{gruMetadata ?
: null}
- {gruMetadata?.x402Ready ?
: null}
- {gruMetadata?.iso20022Ready ?
: null}
- {gruMetadata?.transportActiveVersion ?
: null}
+ {gruMetadata?.x402Ready ?
: null}
+ {gruMetadata?.iso20022Ready ?
: null}
+ {gruMetadata?.transportActiveVersion ?
: null}
{transfer.token_address && (
diff --git a/frontend/src/pages/docs/index.tsx b/frontend/src/pages/docs/index.tsx
index 3a28d14..ab8ddc2 100644
--- a/frontend/src/pages/docs/index.tsx
+++ b/frontend/src/pages/docs/index.tsx
@@ -5,6 +5,11 @@ import { Card } from '@/libs/frontend-ui-primitives'
import PageIntro from '@/components/common/PageIntro'
const docsCards = [
+ {
+ title: 'Posture glossary',
+ href: '/docs/posture-glossary',
+ description: 'First-read definitions for x402, ISO-20022, forward-canonical, cW public-network, and related explorer badges.',
+ },
{
title: 'Public API access',
href: '/docs/public-api-access',
diff --git a/frontend/src/pages/docs/posture-glossary.tsx b/frontend/src/pages/docs/posture-glossary.tsx
new file mode 100644
index 0000000..0e54760
--- /dev/null
+++ b/frontend/src/pages/docs/posture-glossary.tsx
@@ -0,0 +1,80 @@
+'use client'
+
+import Link from 'next/link'
+import { Card } from '@/libs/frontend-ui-primitives'
+import PageIntro from '@/components/common/PageIntro'
+import PostureBadge from '@/components/common/PostureBadge'
+import { postureGlossaryTerms } from '@/data/postureGlossary'
+
+export default function PostureGlossaryDocsPage() {
+ return (
+
+
+
+
+
+
+ Click any posture badge on a live token, address, or transaction page to open the same definitions in a drawer.
+ This page is the canonical long-form reference for audit and policy reviewers.
+
+
+
+
+ {postureGlossaryTerms.map((term) => (
+
+
+
{term.summary}
+
+
Methodology
+
{term.methodology}
+
+ {term.id === 'transport-active' ? (
+
+ Planned v2 aliases: publicNetworkActive
+ {' '}and{' '}
+ livePublicNetworkAssets.
+
+ ) : null}
+
+
+ ))}
+
+
+
+
+
+
+ )
+}
diff --git a/frontend/src/pages/search/index.tsx b/frontend/src/pages/search/index.tsx
index 25a05ce..7852b90 100644
--- a/frontend/src/pages/search/index.tsx
+++ b/frontend/src/pages/search/index.tsx
@@ -7,6 +7,7 @@ import type { TokenListToken } from '@/services/api/config'
import { tokensApi } from '@/services/api/tokens'
import { tokenAggregationApi, type TokenAggregationTokenSnapshot } from '@/services/api/tokenAggregation'
import EntityBadge from '@/components/common/EntityBadge'
+import PostureBadge from '@/components/common/PostureBadge'
import {
inferDirectSearchTarget,
inferTokenSearchTarget,
@@ -394,7 +395,7 @@ export default function SearchPage({
{result.token_type &&
}
{result.is_curated_token &&
}
{result.is_gru_token &&
}
- {result.is_x402_ready &&
}
+ {result.is_x402_ready &&
}
{result.is_wrapped_transport &&
}
{result.currency_code ?
: null}
{result.match_reason ?
: null}
diff --git a/frontend/src/pages/tokens/[address].tsx b/frontend/src/pages/tokens/[address].tsx
index 47420f0..61e0329 100644
--- a/frontend/src/pages/tokens/[address].tsx
+++ b/frontend/src/pages/tokens/[address].tsx
@@ -10,6 +10,7 @@ import type { MissionControlLiquidityPool } from '@/services/api/routes'
import PageIntro from '@/components/common/PageIntro'
import { DetailRow } from '@/components/common/DetailRow'
import EntityBadge from '@/components/common/EntityBadge'
+import PostureBadge from '@/components/common/PostureBadge'
import GruStandardsCard from '@/components/common/GruStandardsCard'
import TokenSigningSurfaceCard from '@/components/common/TokenSigningSurfaceCard'
import MarketEvidenceNote from '@/components/common/MarketEvidenceNote'
@@ -260,8 +261,8 @@ export default function TokenDetailPage() {
return (
- {gruMetadata.x402Ready ?
: null}
- {gruMetadata.iso20022Ready ?
: null}
+ {gruMetadata.x402Ready ?
: null}
+ {gruMetadata.iso20022Ready ?
: null}
)
},
@@ -358,6 +359,16 @@ export default function TokenDetailPage() {
) : (
+ {!provenance?.listed ? (
+
+
+
+ This contract is indexed by Blockscout but is not in the curated Chain 138 token registry. Prefer canonical addresses from the token index for trading, liquidity, and bridge routing.
+
+
+ ) : null}
{token.name || provenance?.name || 'Unknown'}
@@ -451,9 +462,9 @@ export default function TokenDetailPage() {
x402 readiness
-
+
{gruExplorerMetadata.x402PreferredVersion ?
: null}
- {gruExplorerMetadata.canonicalForwardVersion ?
: null}
+ {gruExplorerMetadata.canonicalForwardVersion ?
: null}
{gruExplorerMetadata.x402Ready
@@ -464,7 +475,7 @@ export default function TokenDetailPage() {
ISO-20022 and governance
-
+
{gruExplorerMetadata.currencyCode ?
: null}
@@ -473,6 +484,9 @@ export default function TokenDetailPage() {
: 'The explorer does not currently have a strong ISO-20022 posture signal for this asset.'}
+
+ Posture glossary →
+
GRU guide →
diff --git a/frontend/src/pages/tokens/index.tsx b/frontend/src/pages/tokens/index.tsx
index f97fb9f..2c7715c 100644
--- a/frontend/src/pages/tokens/index.tsx
+++ b/frontend/src/pages/tokens/index.tsx
@@ -6,8 +6,13 @@ import { Card } from '@/libs/frontend-ui-primitives'
import PageIntro from '@/components/common/PageIntro'
import EntityBadge from '@/components/common/EntityBadge'
import MarketEvidenceNote from '@/components/common/MarketEvidenceNote'
-import { tokensApi } from '@/services/api/tokens'
+import { tokensApi, type IndexedTokenListItem } from '@/services/api/tokens'
import type { TokenListToken } from '@/services/api/config'
+import {
+ buildCanonicalAddressSet,
+ isCanonicalTokenAddress,
+ sortIndexedTokensCanonicalFirst,
+} from '@/utils/canonicalTokens'
import { tokenAggregationApi, type TokenAggregationTokenSnapshot } from '@/services/api/tokenAggregation'
import { fetchTokenListForSurface, TOKEN_LIST_SURFACE_LABELS } from '@/services/api/tokenListSurfaces'
import { selectCuratedFeaturedTokens } from '@/utils/featuredTokens'
@@ -56,6 +61,8 @@ export default function TokensPage({ initialCuratedTokens }: TokensPageProps) {
const router = useRouter()
const [query, setQuery] = useState('')
const [curatedTokens, setCuratedTokens] = useState(initialCuratedTokens)
+ const [indexedTokens, setIndexedTokens] = useState([])
+ const [indexedLoading, setIndexedLoading] = useState(true)
const [featuredMarkets, setFeaturedMarkets] = useState>({})
const handleSubmit = (event: React.FormEvent) => {
@@ -90,6 +97,33 @@ export default function TokensPage({ initialCuratedTokens }: TokensPageProps) {
[curatedTokens],
)
+ const canonicalAddressSet = useMemo(() => buildCanonicalAddressSet(curatedTokens), [curatedTokens])
+
+ const sortedIndexedTokens = useMemo(
+ () => sortIndexedTokensCanonicalFirst(indexedTokens, canonicalAddressSet),
+ [canonicalAddressSet, indexedTokens],
+ )
+
+ useEffect(() => {
+ let active = true
+ setIndexedLoading(true)
+
+ tokensApi.listIndexedSafe(1, 50).then(({ ok, data }) => {
+ if (!active) return
+ setIndexedTokens(ok ? data : [])
+ setIndexedLoading(false)
+ }).catch(() => {
+ if (active) {
+ setIndexedTokens([])
+ setIndexedLoading(false)
+ }
+ })
+
+ return () => {
+ active = false
+ }
+ }, [])
+
useEffect(() => {
let active = true
@@ -197,6 +231,49 @@ export default function TokensPage({ initialCuratedTokens }: TokensPageProps) {
+
+
+
+ Canonical registry tokens appear first. Non-canonical indexed duplicates are labeled so operators can distinguish curated assets from stray contract listings.
+
+ {indexedLoading ? (
+ Loading indexed token feed…
+ ) : sortedIndexedTokens.length === 0 ? (
+ Indexed token feed is temporarily unavailable.
+ ) : (
+
+ {sortedIndexedTokens.map((token) => {
+ const canonical = isCanonicalTokenAddress(token.address, canonicalAddressSet)
+ return (
+
+
+
+
+ {token.symbol || token.name || 'Token'}
+
+ {canonical ? (
+
+ ) : (
+
+ )}
+
+
{token.name || token.address}
+
+
+ {token.holders != null ? `${token.holders.toLocaleString()} holders` : 'Holders unavailable'}
+
+
+ )
+ })}
+
+ )}
+
+
+
diff --git a/frontend/src/pages/transactions/[hash].tsx b/frontend/src/pages/transactions/[hash].tsx
index bfbd0af..13fd8ca 100644
--- a/frontend/src/pages/transactions/[hash].tsx
+++ b/frontend/src/pages/transactions/[hash].tsx
@@ -14,6 +14,7 @@ import {
import { formatTimestamp, formatTokenAmount, formatWeiAsEth } from '@/utils/format'
import { DetailRow } from '@/components/common/DetailRow'
import EntityBadge from '@/components/common/EntityBadge'
+import PostureBadge from '@/components/common/PostureBadge'
import PageIntro from '@/components/common/PageIntro'
import PaginationControls from '@/components/common/PaginationControls'
import SectionTabs, { type SectionTab } from '@/components/common/SectionTabs'
@@ -228,8 +229,8 @@ export default function TransactionDetailPage() {
{transfer.token_symbol || transfer.token_name || 'Token'}
{gruPosture?.isGru ?
: null}
- {gruPosture?.isX402Ready ?
: null}
- {gruPosture?.isWrappedTransport ?
: null}
+ {gruPosture?.isX402Ready ?
: null}
+ {gruPosture?.isWrappedTransport ?
: null}
{transfer.token_address && (
diff --git a/frontend/src/services/api/tokens.ts b/frontend/src/services/api/tokens.ts
index 682092e..b998d00 100644
--- a/frontend/src/services/api/tokens.ts
+++ b/frontend/src/services/api/tokens.ts
@@ -7,8 +7,20 @@ import {
mergeTokenListLookups,
type TokenListSurface,
} from './tokenListSurfaces'
+import { applyTokenDisplayOverrides } from '@/utils/canonicalTokens'
import type { AddressTokenTransfer } from './addresses'
+export interface IndexedTokenListItem {
+ address: string
+ name?: string
+ symbol?: string
+ decimals: number
+ type?: string
+ holders?: number
+ exchange_rate?: string | number | null
+ is_canonical?: boolean
+}
+
export interface TokenProfile {
address: string
name?: string
@@ -76,6 +88,32 @@ function normalizeTokenProfile(raw: {
}
}
+function normalizeIndexedToken(raw: {
+ address?: string | null
+ address_hash?: string | null
+ name?: string | null
+ symbol?: string | null
+ decimals?: string | number | null
+ type?: string | null
+ holders?: string | number | null
+ exchange_rate?: string | number | null
+}): IndexedTokenListItem | null {
+ const address = raw.address || raw.address_hash || ''
+ if (!address) {
+ return null
+ }
+
+ return applyTokenDisplayOverrides({
+ address,
+ name: raw.name || undefined,
+ symbol: raw.symbol || undefined,
+ decimals: Number(raw.decimals || 0),
+ type: raw.type || undefined,
+ holders: raw.holders != null ? Number(raw.holders) : undefined,
+ exchange_rate: raw.exchange_rate ?? null,
+ })
+}
+
function computeMarketCap(totalSupply: string | undefined, decimals: number, priceUsd: number | undefined): number | null {
if (!totalSupply || priceUsd == null || !Number.isFinite(priceUsd)) {
return null
@@ -187,8 +225,9 @@ export const tokensApi = {
const aggregationToken =
aggregationResult.status === 'fulfilled' && aggregationResult.value.ok ? aggregationResult.value.data : null
const merged = mergeTokenProfileWithAggregation(blockscoutToken, aggregationToken)
+ const displayReady = merged ? applyTokenDisplayOverrides(merged) : null
- return { ok: merged != null, data: merged }
+ return { ok: displayReady != null, data: displayReady }
} catch {
return { ok: false, data: null }
}
@@ -318,4 +357,36 @@ export const tokensApi = {
return { ok: false, data: [] }
}
},
+
+ listIndexedSafe: async (
+ page = 1,
+ pageSize = 50,
+ ): Promise<{ ok: boolean; data: IndexedTokenListItem[] }> => {
+ try {
+ const params = new URLSearchParams({
+ page: page.toString(),
+ items_count: pageSize.toString(),
+ })
+ const raw = await fetchBlockscoutJson<{ items?: Array<{
+ address?: string | null
+ address_hash?: string | null
+ name?: string | null
+ symbol?: string | null
+ decimals?: string | number | null
+ type?: string | null
+ holders?: string | number | null
+ exchange_rate?: string | number | null
+ }> }>(`/api/v2/tokens?${params.toString()}`)
+
+ const data = Array.isArray(raw.items)
+ ? raw.items
+ .map((item) => normalizeIndexedToken(item))
+ .filter((item): item is IndexedTokenListItem => item != null)
+ : []
+
+ return { ok: true, data }
+ } catch {
+ return { ok: false, data: [] }
+ }
+ },
}
diff --git a/frontend/src/utils/canonicalTokens.test.ts b/frontend/src/utils/canonicalTokens.test.ts
new file mode 100644
index 0000000..ad6ce07
--- /dev/null
+++ b/frontend/src/utils/canonicalTokens.test.ts
@@ -0,0 +1,47 @@
+import { describe, expect, it } from 'vitest'
+import {
+ applyTokenDisplayOverrides,
+ buildCanonicalAddressSet,
+ isCanonicalTokenAddress,
+ sortIndexedTokensCanonicalFirst,
+ WETH9_CANONICAL_ADDRESS,
+} from './canonicalTokens'
+
+describe('canonicalTokens', () => {
+ it('builds a lowercase canonical address set', () => {
+ const set = buildCanonicalAddressSet([
+ { chainId: 138, address: '0x93E66202A11B1772E55407B32B44e5Cd8eda7f22', symbol: 'cUSDT' },
+ ])
+
+ expect(isCanonicalTokenAddress('0x93e66202a11b1772e55407b32b44e5cd8eda7f22', set)).toBe(true)
+ expect(isCanonicalTokenAddress('0x0000000000000000000000000000000000000001', set)).toBe(false)
+ })
+
+ it('sorts canonical tokens before non-canonical entries', () => {
+ const canonicalSet = buildCanonicalAddressSet([
+ { chainId: 138, address: WETH9_CANONICAL_ADDRESS, symbol: 'WETH9' },
+ ])
+
+ const sorted = sortIndexedTokensCanonicalFirst(
+ [
+ { address: '0x0000000000000000000000000000000000000001', symbol: 'AAA' },
+ { address: WETH9_CANONICAL_ADDRESS, symbol: 'WETH9' },
+ ],
+ canonicalSet,
+ )
+
+ expect(sorted[0]?.address).toBe(WETH9_CANONICAL_ADDRESS)
+ })
+
+ it('applies WETH9 metadata override', () => {
+ const token = applyTokenDisplayOverrides({
+ address: WETH9_CANONICAL_ADDRESS,
+ name: null as unknown as string,
+ symbol: 'WETH',
+ decimals: 18,
+ })
+
+ expect(token.symbol).toBe('WETH9')
+ expect(token.name).toBe('Wrapped Ether (WETH9)')
+ })
+})
diff --git a/frontend/src/utils/canonicalTokens.ts b/frontend/src/utils/canonicalTokens.ts
new file mode 100644
index 0000000..6997122
--- /dev/null
+++ b/frontend/src/utils/canonicalTokens.ts
@@ -0,0 +1,58 @@
+import type { TokenListToken } from '@/services/api/config'
+
+export const WETH9_CANONICAL_ADDRESS = '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'
+
+export const TOKEN_DISPLAY_OVERRIDES: Record
= {
+ [WETH9_CANONICAL_ADDRESS.toLowerCase()]: {
+ name: 'Wrapped Ether (WETH9)',
+ symbol: 'WETH9',
+ decimals: 18,
+ },
+}
+
+export function buildCanonicalAddressSet(tokens: TokenListToken[]): Set {
+ const addresses = new Set()
+ for (const token of tokens) {
+ if (typeof token.address === 'string' && token.address.trim().length > 0) {
+ addresses.add(token.address.toLowerCase())
+ }
+ }
+ return addresses
+}
+
+export function isCanonicalTokenAddress(address: string, canonicalSet: Set): boolean {
+ return canonicalSet.has(address.toLowerCase())
+}
+
+export function applyTokenDisplayOverrides(
+ token: T,
+): T {
+ const override = TOKEN_DISPLAY_OVERRIDES[token.address.toLowerCase()]
+ if (!override) {
+ return token
+ }
+
+ return {
+ ...token,
+ name: override.name || token.name,
+ symbol: override.symbol || token.symbol,
+ decimals: override.decimals ?? token.decimals,
+ }
+}
+
+export function sortIndexedTokensCanonicalFirst(
+ tokens: T[],
+ canonicalSet: Set,
+): T[] {
+ return [...tokens].sort((left, right) => {
+ const leftCanonical = isCanonicalTokenAddress(left.address, canonicalSet)
+ const rightCanonical = isCanonicalTokenAddress(right.address, canonicalSet)
+ if (leftCanonical !== rightCanonical) {
+ return leftCanonical ? -1 : 1
+ }
+
+ const leftLabel = left.symbol || left.name || left.address
+ const rightLabel = right.symbol || right.name || right.address
+ return leftLabel.localeCompare(rightLabel, undefined, { sensitivity: 'base' })
+ })
+}
diff --git a/scripts/e2e-sprint-smoke.spec.ts b/scripts/e2e-sprint-smoke.spec.ts
index 9647c7b..cfd08ba 100644
--- a/scripts/e2e-sprint-smoke.spec.ts
+++ b/scripts/e2e-sprint-smoke.spec.ts
@@ -8,6 +8,7 @@ test.describe('Explorer sprint smoke', () => {
await page.goto(`${EXPLORER_URL}/`, { waitUntil: 'domcontentloaded', timeout: 20000 })
await expect(page.getByText(/Network overview/i)).toBeVisible({ timeout: 10000 })
await expect(page.getByRole('heading', { name: /Recent Transactions/i })).toBeVisible({ timeout: 10000 })
+ await expect(page.getByText(/Freshness Interpretation/i).first()).toBeVisible({ timeout: 10000 })
})
test('wallet page loads', async ({ page }) => {
@@ -24,6 +25,7 @@ test.describe('Explorer sprint smoke', () => {
await page.goto(`${EXPLORER_URL}/tokens`, { waitUntil: 'domcontentloaded', timeout: 20000 })
await expect(page.getByRole('heading', { name: /^Tokens$/i })).toBeVisible({ timeout: 10000 })
await expect(page.getByText(/Canonical Chain 138 trading set/i).first()).toBeVisible({ timeout: 10000 })
+ await expect(page.getByText(/Indexed tokens \(Blockscout\)/i).first()).toBeVisible({ timeout: 10000 })
})
test('canonical cUSDT token detail loads', async ({ page }) => {
@@ -42,6 +44,13 @@ test.describe('Explorer sprint smoke', () => {
await expect(page.getByRole('heading', { name: /Bridge & Relay Monitoring/i })).toBeVisible({ timeout: 15000 })
await expect(page.getByText(/CCIP route catalog/i).first()).toBeVisible({ timeout: 15000 })
await expect(page.getByText(/Wemix/i).first()).toBeVisible({ timeout: 15000 })
+ await expect(page.getByText(/Bridge Freshness Context/i).first()).toBeVisible({ timeout: 10000 })
+ })
+
+ test('posture glossary doc page loads', async ({ page }) => {
+ await page.goto(`${EXPLORER_URL}/docs/posture-glossary`, { waitUntil: 'domcontentloaded', timeout: 30000 })
+ await expect(page.getByRole('heading', { name: /Posture glossary/i })).toBeVisible({ timeout: 15000 })
+ await expect(page.getByText(/x402 readiness/i).first()).toBeVisible({ timeout: 10000 })
})
test('public API access doc page loads', async ({ page }) => {
@@ -87,6 +96,7 @@ test.describe('Explorer sprint smoke', () => {
await page.goto(`${EXPLORER_URL}/analytics`, { waitUntil: 'domcontentloaded', timeout: 30000 })
await expect(page.getByRole('heading', { name: /Analytics & Network Activity/i })).toBeVisible({ timeout: 15000 })
await expect(page.getByText(/Track 3 public surface/i).first()).toBeVisible({ timeout: 10000 })
+ await expect(page.getByText(/Analytics Freshness Context/i).first()).toBeVisible({ timeout: 10000 })
})
test('operator page shows track 4 surface note', async ({ page }) => {