From b9c5eb6b94e5a1df356234171b9c76d122757750 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Mon, 1 Dec 2025 15:15:42 +0100 Subject: [PATCH 1/6] feat(ensadmin): include projection info on status page Also, make `` component to re-render every second to present relative time to the current time. --- .../src/app/mock/indexing-stats/page.tsx | 30 ++++++--- .../indexing-status/indexing-stats.tsx | 57 +++++++++-------- .../indexing-status/projection-info.tsx | 61 +++++++++++++++++++ .../use-indexing-status-with-swr.ts | 37 +++++++++-- .../use-stateful-fetch-registrar-actions.ts | 13 ++-- 5 files changed, 153 insertions(+), 45 deletions(-) create mode 100644 apps/ensadmin/src/components/indexing-status/projection-info.tsx diff --git a/apps/ensadmin/src/app/mock/indexing-stats/page.tsx b/apps/ensadmin/src/app/mock/indexing-stats/page.tsx index b169c2bd0..9daecb52f 100644 --- a/apps/ensadmin/src/app/mock/indexing-stats/page.tsx +++ b/apps/ensadmin/src/app/mock/indexing-stats/page.tsx @@ -1,10 +1,13 @@ "use client"; import { useQuery } from "@tanstack/react-query"; +import { getUnixTime } from "date-fns"; import { useEffect, useState } from "react"; import { - type IndexingStatusResponse, + CrossChainIndexingStatusSnapshot, + createRealtimeIndexingStatusProjection, + IndexingStatusResponseCodes, IndexingStatusResponseOk, OmnichainIndexingStatusIds, } from "@ensnode/ensnode-sdk"; @@ -13,10 +16,7 @@ import { IndexingStats } from "@/components/indexing-status/indexing-stats"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; -import { - indexingStatusResponseError, - indexingStatusResponseOkOmnichain, -} from "../indexing-status-api.mock"; +import { indexingStatusResponseOkOmnichain } from "../indexing-status-api.mock"; type LoadingVariant = "Loading" | "Loading Error"; type ResponseOkVariant = keyof typeof indexingStatusResponseOkOmnichain; @@ -37,7 +37,7 @@ let loadingTimeoutId: number; async function fetchMockedIndexingStatus( selectedVariant: Variant, -): Promise { +): Promise { // always try clearing loading timeout when performing a mocked fetch // this way we get a fresh and very long request to observe the loading state if (loadingTimeoutId) { @@ -48,14 +48,19 @@ async function fetchMockedIndexingStatus( case OmnichainIndexingStatusIds.Unstarted: case OmnichainIndexingStatusIds.Backfill: case OmnichainIndexingStatusIds.Following: - case OmnichainIndexingStatusIds.Completed: - return indexingStatusResponseOkOmnichain[selectedVariant] as IndexingStatusResponseOk; + case OmnichainIndexingStatusIds.Completed: { + const response = indexingStatusResponseOkOmnichain[ + selectedVariant + ] as IndexingStatusResponseOk; + + return response.realtimeProjection.snapshot; + } case "Error ResponseCode": throw new Error( "Received Indexing Status response with responseCode other than 'ok' which will not be cached.", ); case "Loading": - return new Promise((_resolve, reject) => { + return new Promise((_resolve, reject) => { loadingTimeoutId = +setTimeout(reject, 5 * 60 * 1_000); }); case "Loading Error": @@ -67,10 +72,17 @@ export default function MockIndexingStatusPage() { const [selectedVariant, setSelectedVariant] = useState( OmnichainIndexingStatusIds.Unstarted, ); + const now = getUnixTime(new Date()); const mockedIndexingStatus = useQuery({ queryKey: ["mock", "useIndexingStatus", selectedVariant], queryFn: () => fetchMockedIndexingStatus(selectedVariant), + select: (cachedSnapshot) => { + return { + responseCode: IndexingStatusResponseCodes.Ok, + realtimeProjection: createRealtimeIndexingStatusProjection(cachedSnapshot, now), + } satisfies IndexingStatusResponseOk; + }, retry: false, // allows loading error to be observed immediately }); diff --git a/apps/ensadmin/src/components/indexing-status/indexing-stats.tsx b/apps/ensadmin/src/components/indexing-status/indexing-stats.tsx index d89a0c3a6..2a47f955c 100644 --- a/apps/ensadmin/src/components/indexing-status/indexing-stats.tsx +++ b/apps/ensadmin/src/components/indexing-status/indexing-stats.tsx @@ -5,9 +5,8 @@ */ import { ChainIcon, ChainName } from "@namehash/namehash-ui"; -import type { PropsWithChildren, ReactElement } from "react"; +import type { PropsWithChildren, ReactElement, ReactNode } from "react"; -import type { useIndexingStatus } from "@ensnode/ensnode-react"; import { ChainIndexingConfigTypeIds, ChainIndexingStatusIds, @@ -24,7 +23,6 @@ import { sortChainStatusesByStartBlockAsc, } from "@ensnode/ensnode-sdk"; -import { useIndexingStatusWithSwr } from "@/components/indexing-status/use-indexing-status-with-swr"; import { Badge } from "@/components/ui/badge"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { formatChainStatus, formatOmnichainIndexingStatus } from "@/lib/indexing-status"; @@ -33,6 +31,8 @@ import { cn } from "@/lib/utils"; import { BackfillStatus } from "./backfill-status"; import { BlockStats } from "./block-refs"; import { IndexingStatusLoading } from "./indexing-status-loading"; +import { ProjectionInfo } from "./projection-info"; +import type { useIndexingStatusWithSwr } from "./use-indexing-status-with-swr"; interface IndexingStatsForOmnichainStatusSnapshotProps< OmnichainIndexingStatusSnapshotType extends @@ -316,30 +316,20 @@ export function IndexingStatsForSnapshotFollowing({ }); } +interface IndexingStatsShellProps extends PropsWithChildren { + title: ReactNode; +} + /** * Indexing Stats Shell * * UI component for presenting indexing stats UI for specific overall status. */ -export function IndexingStatsShell({ - omnichainStatus, - children, -}: PropsWithChildren<{ omnichainStatus?: OmnichainIndexingStatusId }>) { +export function IndexingStatsShell({ title, children }: IndexingStatsShellProps) { return ( - - Indexing Status - - {omnichainStatus && ( - - {formatOmnichainIndexingStatus(omnichainStatus)} - - )} - + {title} @@ -418,11 +408,32 @@ export function IndexingStatsForRealtimeStatusProjection({ break; } + const { snapshot, worstCaseDistance } = realtimeProjection; + const { omnichainSnapshot, snapshotTime } = snapshot; + const omnichainStatus = omnichainSnapshot.omnichainStatus; + return (
{maybeIndexingTimeline} - + + Indexing Status + + + + {omnichainStatus && ( + + {formatOmnichainIndexingStatus(omnichainStatus)} + + )} + + } + > {indexingStats}
@@ -439,7 +450,7 @@ export function IndexingStats(props: IndexingStatsProps) { if (indexingStatusQuery.isError) { return ( - + ); @@ -449,11 +460,9 @@ export function IndexingStats(props: IndexingStatsProps) { return ; } - const indexingStatus = indexingStatusQuery.data; - return ( ); } diff --git a/apps/ensadmin/src/components/indexing-status/projection-info.tsx b/apps/ensadmin/src/components/indexing-status/projection-info.tsx new file mode 100644 index 000000000..7093c718f --- /dev/null +++ b/apps/ensadmin/src/components/indexing-status/projection-info.tsx @@ -0,0 +1,61 @@ +"use client"; + +import { RelativeTime, useNow } from "@namehash/namehash-ui"; +import { InfoIcon } from "lucide-react"; + +import type { Duration, UnixTimestamp } from "@ensnode/ensnode-sdk"; + +import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; + +interface ProjectionInfoProps { + snapshotTime: UnixTimestamp; + worstCaseDistance: Duration; +} + +/** + * Displays metadata about the current indexing status projection in a tooltip. + * Shows when the projection was generated, when the snapshot was taken, and worst-case distance. + */ +export function ProjectionInfo({ snapshotTime, worstCaseDistance }: ProjectionInfoProps) { + const now = useNow(); + + return ( + + + + + +
+
+
+ Worst-Case Distance* +
+
+ {worstCaseDistance} second{worstCaseDistance !== 1 ? "s" : ""} +
+
+ +
+ * as of real-time projection generated just now from indexing status snapshot captured{" "} + + . +
+
+
+
+ ); +} diff --git a/apps/ensadmin/src/components/indexing-status/use-indexing-status-with-swr.ts b/apps/ensadmin/src/components/indexing-status/use-indexing-status-with-swr.ts index f1c1209b8..ae0791a25 100644 --- a/apps/ensadmin/src/components/indexing-status/use-indexing-status-with-swr.ts +++ b/apps/ensadmin/src/components/indexing-status/use-indexing-status-with-swr.ts @@ -1,5 +1,7 @@ "use client"; +import { useNow } from "@namehash/namehash-ui"; +import { secondsToMilliseconds } from "date-fns"; import { useCallback, useMemo } from "react"; import { @@ -11,16 +13,21 @@ import { WithSDKConfigParameter, } from "@ensnode/ensnode-react"; import { + CrossChainIndexingStatusSnapshotOmnichain, + createRealtimeIndexingStatusProjection, + Duration, type IndexingStatusRequest, IndexingStatusResponseCodes, IndexingStatusResponseOk, } from "@ensnode/ensnode-sdk"; -const DEFAULT_REFETCH_INTERVAL = 10 * 1000; +const DEFAULT_REFETCH_INTERVAL = secondsToMilliseconds(10); + +const REALTIME_PROJECTION_REFRESH_RATE: Duration = 1; interface UseIndexingStatusParameters extends IndexingStatusRequest, - QueryParameter {} + QueryParameter {} /** * A proxy hook for {@link useIndexingStatus} which applies @@ -31,6 +38,7 @@ export function useIndexingStatusWithSwr( ) { const { config, query = {} } = parameters; const _config = useENSNodeSDKConfig(config); + const now = useNow({ timeToRefresh: REALTIME_PROJECTION_REFRESH_RATE }); const queryOptions = useMemo(() => createIndexingStatusQueryOptions(_config), [_config]); const queryKey = useMemo(() => ["swr", ...queryOptions.queryKey], [queryOptions.queryKey]); @@ -46,12 +54,32 @@ export function useIndexingStatusWithSwr( ); } - // successful response to be cached - return response; + // The indexing status snapshot has been fetched and successfully validated for caching. + // Therefore, return it so that query cache for `queryOptions.queryKey` will: + // - Replace the currently cached value (if any) with this new value. + // - Return this non-null value. + return response.realtimeProjection.snapshot; }), [queryOptions.queryFn], ); + // Call select function to `createRealtimeIndexingStatusProjection` each time + // `now` is updated. + const select = useCallback( + (cachedSnapshot: CrossChainIndexingStatusSnapshotOmnichain): IndexingStatusResponseOk => { + const realtimeProjection = createRealtimeIndexingStatusProjection(cachedSnapshot, now); + + // Maintain the original response shape of `IndexingStatusResponse` + // for the consumers. Creating a new projection from the cached snapshot + // each time `now` is updated should be implementation detail. + return { + responseCode: IndexingStatusResponseCodes.Ok, + realtimeProjection, + } satisfies IndexingStatusResponseOk; + }, + [now], + ); + return useSwrQuery({ ...queryOptions, refetchInterval: query.refetchInterval ?? DEFAULT_REFETCH_INTERVAL, // Indexing status changes frequently @@ -59,5 +87,6 @@ export function useIndexingStatusWithSwr( enabled: query.enabled ?? queryOptions.enabled, queryKey, queryFn, + select, }); } diff --git a/apps/ensadmin/src/components/registrar-actions/use-stateful-fetch-registrar-actions.ts b/apps/ensadmin/src/components/registrar-actions/use-stateful-fetch-registrar-actions.ts index 7e9fd63af..8ed5a1eee 100644 --- a/apps/ensadmin/src/components/registrar-actions/use-stateful-fetch-registrar-actions.ts +++ b/apps/ensadmin/src/components/registrar-actions/use-stateful-fetch-registrar-actions.ts @@ -1,6 +1,5 @@ import { useENSNodeConfig, useRegistrarActions } from "@ensnode/ensnode-react"; import { - IndexingStatusResponseCodes, RegistrarActionsOrders, RegistrarActionsResponseCodes, registrarActionsPrerequisites, @@ -44,13 +43,10 @@ export function useStatefulRegistrarActions({ let isRegistrarActionsApiSupported = false; - if ( - ensNodeConfigQuery.isSuccess && - indexingStatusQuery.isSuccess && - indexingStatusQuery.data.responseCode === IndexingStatusResponseCodes.Ok - ) { + if (ensNodeConfigQuery.isSuccess && indexingStatusQuery.isSuccess) { const { ensIndexerPublicConfig } = ensNodeConfigQuery.data; - const { omnichainSnapshot } = indexingStatusQuery.data.realtimeProjection.snapshot; + const { realtimeProjection } = indexingStatusQuery.data; + const { omnichainSnapshot } = realtimeProjection.snapshot; isRegistrarActionsApiSupported = hasEnsIndexerConfigSupport(ensIndexerPublicConfig) && @@ -100,7 +96,8 @@ export function useStatefulRegistrarActions({ } satisfies StatefulFetchRegistrarActionsUnsupported; } - const { omnichainSnapshot } = indexingStatusQuery.data.realtimeProjection.snapshot; + const { realtimeProjection } = indexingStatusQuery.data; + const { omnichainSnapshot } = realtimeProjection.snapshot; // fetching is temporarily not possible due to indexing status being not advanced enough if (!hasIndexingStatusSupport(omnichainSnapshot.omnichainStatus)) { From ecab5efd3f14c33ae28e23aa872b0030e4233f17 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Tue, 10 Feb 2026 06:53:59 +0100 Subject: [PATCH 2/6] Apply AI agents feedback --- apps/ensadmin/src/app/mock/indexing-stats/page.tsx | 3 ++- .../components/indexing-status/indexing-stats.tsx | 14 ++++++-------- .../use-indexing-status-with-swr.ts | 2 +- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/apps/ensadmin/src/app/mock/indexing-stats/page.tsx b/apps/ensadmin/src/app/mock/indexing-stats/page.tsx index 9daecb52f..06c502b42 100644 --- a/apps/ensadmin/src/app/mock/indexing-stats/page.tsx +++ b/apps/ensadmin/src/app/mock/indexing-stats/page.tsx @@ -1,5 +1,6 @@ "use client"; +import { useNow } from "@namehash/namehash-ui"; import { useQuery } from "@tanstack/react-query"; import { getUnixTime } from "date-fns"; import { useEffect, useState } from "react"; @@ -72,7 +73,7 @@ export default function MockIndexingStatusPage() { const [selectedVariant, setSelectedVariant] = useState( OmnichainIndexingStatusIds.Unstarted, ); - const now = getUnixTime(new Date()); + const now = useNow(); const mockedIndexingStatus = useQuery({ queryKey: ["mock", "useIndexingStatus", selectedVariant], diff --git a/apps/ensadmin/src/components/indexing-status/indexing-stats.tsx b/apps/ensadmin/src/components/indexing-status/indexing-stats.tsx index 2a47f955c..e47df70d4 100644 --- a/apps/ensadmin/src/components/indexing-status/indexing-stats.tsx +++ b/apps/ensadmin/src/components/indexing-status/indexing-stats.tsx @@ -423,14 +423,12 @@ export function IndexingStatsForRealtimeStatusProjection({ - {omnichainStatus && ( - - {formatOmnichainIndexingStatus(omnichainStatus)} - - )} + + {formatOmnichainIndexingStatus(omnichainStatus)} + } > diff --git a/apps/ensadmin/src/components/indexing-status/use-indexing-status-with-swr.ts b/apps/ensadmin/src/components/indexing-status/use-indexing-status-with-swr.ts index ae0791a25..ba9d80cc2 100644 --- a/apps/ensadmin/src/components/indexing-status/use-indexing-status-with-swr.ts +++ b/apps/ensadmin/src/components/indexing-status/use-indexing-status-with-swr.ts @@ -82,8 +82,8 @@ export function useIndexingStatusWithSwr( return useSwrQuery({ ...queryOptions, - refetchInterval: query.refetchInterval ?? DEFAULT_REFETCH_INTERVAL, // Indexing status changes frequently ...query, + refetchInterval: query.refetchInterval ?? DEFAULT_REFETCH_INTERVAL, // Indexing status changes frequently enabled: query.enabled ?? queryOptions.enabled, queryKey, queryFn, From a12735cd52c34ff5734715bb36b84dbe5c99e3cb Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Tue, 10 Feb 2026 07:07:32 +0100 Subject: [PATCH 3/6] Apply AI agents feedback --- apps/ensadmin/src/app/mock/indexing-stats/page.tsx | 1 - .../src/components/indexing-status/indexing-stats.tsx | 8 +++----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/apps/ensadmin/src/app/mock/indexing-stats/page.tsx b/apps/ensadmin/src/app/mock/indexing-stats/page.tsx index 06c502b42..1ceffccf8 100644 --- a/apps/ensadmin/src/app/mock/indexing-stats/page.tsx +++ b/apps/ensadmin/src/app/mock/indexing-stats/page.tsx @@ -2,7 +2,6 @@ import { useNow } from "@namehash/namehash-ui"; import { useQuery } from "@tanstack/react-query"; -import { getUnixTime } from "date-fns"; import { useEffect, useState } from "react"; import { diff --git a/apps/ensadmin/src/components/indexing-status/indexing-stats.tsx b/apps/ensadmin/src/components/indexing-status/indexing-stats.tsx index e47df70d4..45afd25d7 100644 --- a/apps/ensadmin/src/components/indexing-status/indexing-stats.tsx +++ b/apps/ensadmin/src/components/indexing-status/indexing-stats.tsx @@ -458,9 +458,7 @@ export function IndexingStats(props: IndexingStatsProps) { return ; } - return ( - - ); + const { realtimeProjection } = indexingStatusQuery.data; + + return ; } From 45849d1e8cfd0fb1703f97ace0b32bc08fe9c56d Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Tue, 10 Feb 2026 07:38:11 +0100 Subject: [PATCH 4/6] docs(changeset): Includes `ProjectionInfo` component on Indexing Status page. --- .changeset/wild-results-wash.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/wild-results-wash.md diff --git a/.changeset/wild-results-wash.md b/.changeset/wild-results-wash.md new file mode 100644 index 000000000..fdcd0068b --- /dev/null +++ b/.changeset/wild-results-wash.md @@ -0,0 +1,5 @@ +--- +"ensadmin": minor +--- + +Includes `ProjectionInfo` component on Indexing Status page. From ec183a886317675352b0da53be1c919e4643cede Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Tue, 10 Feb 2026 07:39:58 +0100 Subject: [PATCH 5/6] docs(changeset): Updates `useIndexingStatusWithSwr` to always return current realtime indexing status projection. --- .changeset/sweet-cities-mate.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/sweet-cities-mate.md diff --git a/.changeset/sweet-cities-mate.md b/.changeset/sweet-cities-mate.md new file mode 100644 index 000000000..e40d9fd01 --- /dev/null +++ b/.changeset/sweet-cities-mate.md @@ -0,0 +1,5 @@ +--- +"ensadmin": minor +--- + +Updates `useIndexingStatusWithSwr` to always return current realtime indexing status projection. From a032afde55d8a9727fa69fb11bb0f7e388e1fc8e Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Tue, 10 Feb 2026 15:05:05 +0100 Subject: [PATCH 6/6] Address PR feedback Update worst case distance presentation --- .../indexing-status/indexing-stats.tsx | 6 ++- .../indexing-status/projection-info.tsx | 47 ++++++++++++++----- 2 files changed, 40 insertions(+), 13 deletions(-) diff --git a/apps/ensadmin/src/components/indexing-status/indexing-stats.tsx b/apps/ensadmin/src/components/indexing-status/indexing-stats.tsx index 45afd25d7..1af60ece8 100644 --- a/apps/ensadmin/src/components/indexing-status/indexing-stats.tsx +++ b/apps/ensadmin/src/components/indexing-status/indexing-stats.tsx @@ -421,7 +421,11 @@ export function IndexingStatsForRealtimeStatusProjection({ <> Indexing Status - + { + const presentWorstCaseDistanceAsRelativeTime = worstCaseDistance > 60; + + if (presentWorstCaseDistanceAsRelativeTime) { + return formatRelativeTime(omnichainIndexingCursor, true, true, true).replace(" ago", ""); + } + + return `${worstCaseDistance} seconds`; +}; + interface ProjectionInfoProps { + omnichainIndexingCursor: UnixTimestamp; snapshotTime: UnixTimestamp; worstCaseDistance: Duration; } @@ -16,19 +41,16 @@ interface ProjectionInfoProps { * Displays metadata about the current indexing status projection in a tooltip. * Shows when the projection was generated, when the snapshot was taken, and worst-case distance. */ -export function ProjectionInfo({ snapshotTime, worstCaseDistance }: ProjectionInfoProps) { +export function ProjectionInfo({ + omnichainIndexingCursor, + snapshotTime, + worstCaseDistance, +}: ProjectionInfoProps) { const now = useNow(); - return ( - +
- {worstCaseDistance} second{worstCaseDistance !== 1 ? "s" : ""} + {formatWorstCaseDistance(worstCaseDistance, omnichainIndexingCursor)}
- * as of real-time projection generated just now from indexing status snapshot captured{" "} + * as of the real-time projection generated just now from indexing status snapshot + captured{" "}