diff --git a/web/src/App.test.tsx b/web/src/App.test.tsx index 6e5af60..db10867 100644 --- a/web/src/App.test.tsx +++ b/web/src/App.test.tsx @@ -100,6 +100,19 @@ describe("read-only operations console", () => { expect(screen.getByText("500.00 mUSDC")).toBeTruthy(); }); + it("makes every learning abbreviation keyboard-focusable with its explanation intact", () => { + const { container } = render(); + const abbreviations = Array.from(container.querySelectorAll("abbr")); + expect(abbreviations).toHaveLength(5); + + for (const abbreviation of abbreviations) { + expect(abbreviation.getAttribute("title")?.trim().length).toBeGreaterThan(0); + expect(abbreviation.getAttribute("tabindex")).toBe("0"); + (abbreviation as HTMLElement).focus(); + expect(document.activeElement).toBe(abbreviation); + } + }); + it("renders valid V1 activity newest first beside an isolated decode warning", () => { render(); diff --git a/web/src/components/AccountingGrid.tsx b/web/src/components/AccountingGrid.tsx index 84b83ac..888fbb1 100644 --- a/web/src/components/AccountingGrid.tsx +++ b/web/src/components/AccountingGrid.tsx @@ -9,8 +9,8 @@ export function AccountingGrid({ snapshot }: { snapshot: DashboardSnapshot }) {

Solvent when reserves are greater than or equal to liabilities.

-
Reserves
{formatAmount(snapshot.reserves)}
-
Liabilities
{formatAmount(snapshot.liabilities)}
+
Reserves
{formatAmount(snapshot.reserves)}
+
Liabilities
{formatAmount(snapshot.liabilities)}
Surplus
{formatSurplus(snapshot.surplus)}
Reserve ratio
{formatReserveRatio(snapshot.reserves, snapshot.liabilities)}
diff --git a/web/src/components/ContractIdentity.tsx b/web/src/components/ContractIdentity.tsx index f5c8543..356be89 100644 --- a/web/src/components/ContractIdentity.tsx +++ b/web/src/components/ContractIdentity.tsx @@ -30,10 +30,10 @@ export function ContractIdentity({ manifest, snapshot }: { manifest: DeploymentM

Reads target the proxy. The implementation address identifies its current logic.

-
Proxy
-
Implementation
+
Proxy
+
Implementation
Token
-
Owner
+
Owner
Deployment block
{manifest.deploymentBlock.toString()}
diff --git a/web/src/hooks/useBankDashboard.test.tsx b/web/src/hooks/useBankDashboard.test.tsx index 624c1c0..f72a586 100644 --- a/web/src/hooks/useBankDashboard.test.tsx +++ b/web/src/hooks/useBankDashboard.test.tsx @@ -1,8 +1,8 @@ -import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { focusManager, onlineManager, QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { act, renderHook, waitFor } from "@testing-library/react"; import type { ReactNode } from "react"; import type { Address } from "viem"; -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { DashboardSnapshot, DeploymentManifest } from "../types/dashboard"; import { useBankDashboard } from "./useBankDashboard"; @@ -55,8 +55,7 @@ function deferred() { return { promise, resolve, reject }; } -function createWrapper() { - const client = new QueryClient({ defaultOptions: { queries: { retry: false, gcTime: 0 } } }); +function createWrapper(client = new QueryClient({ defaultOptions: { queries: { retry: false, gcTime: 0 } } })) { return function Wrapper({ children }: { children: ReactNode }) { return {children}; }; @@ -65,6 +64,13 @@ function createWrapper() { describe("useBankDashboard", () => { beforeEach(() => { notifyBlock = undefined; + focusManager.setFocused(true); + onlineManager.setOnline(true); + }); + + afterEach(() => { + focusManager.setFocused(undefined); + onlineManager.setOnline(true); }); it("starts in loading while the manifest and first reconciled snapshot are pending", () => { @@ -151,6 +157,52 @@ describe("useBankDashboard", () => { expect(loader).toHaveBeenCalledTimes(1); }); + it("does not retry a terminal manifest-fetch failure on focus, reconnect, or remount", async () => { + const loadManifest = vi.fn().mockRejectedValue(new Error("manifest request failed")); + const client = new QueryClient({ defaultOptions: { queries: { retry: false, gcTime: Number.POSITIVE_INFINITY } } }); + const wrapper = createWrapper(client); + const options = { loadManifest, loader: vi.fn() }; + const first = renderHook(() => useBankDashboard(options), { wrapper }); + await waitFor(() => expect(first.result.current.status).toBe("invalid-manifest")); + + await act(async () => { + focusManager.setFocused(false); + focusManager.setFocused(true); + onlineManager.setOnline(false); + onlineManager.setOnline(true); + await new Promise((resolve) => setTimeout(resolve, 0)); + }); + first.unmount(); + const second = renderHook(() => useBankDashboard(options), { wrapper }); + await waitFor(() => expect(second.result.current.status).toBe("invalid-manifest")); + + expect(loadManifest).toHaveBeenCalledTimes(1); + }); + + it("does not retry a terminal chain mismatch on focus, reconnect, or remount", async () => { + const loader = vi.fn().mockRejectedValue(new Error( + "endpoint chain ID 84532 does not match manifest chain ID 31337", + )); + const client = new QueryClient({ defaultOptions: { queries: { retry: false, gcTime: Number.POSITIVE_INFINITY } } }); + const wrapper = createWrapper(client); + const options = { loadManifest: async () => manifestJson, loader }; + const first = renderHook(() => useBankDashboard(options), { wrapper }); + await waitFor(() => expect(first.result.current.status).toBe("chain-mismatch")); + + await act(async () => { + focusManager.setFocused(false); + focusManager.setFocused(true); + onlineManager.setOnline(false); + onlineManager.setOnline(true); + await new Promise((resolve) => setTimeout(resolve, 0)); + }); + first.unmount(); + const second = renderHook(() => useBankDashboard(options), { wrapper }); + await waitFor(() => expect(second.result.current.status).toBe("chain-mismatch")); + + expect(loader).toHaveBeenCalledTimes(1); + }); + it("reconciles exactly once when a new watched block arrives", async () => { const nextSnapshot = { ...snapshot, blockNumber: 13n, synchronizedAt: new Date("2026-08-21T10:01:00.000Z") }; const loader = vi.fn().mockResolvedValueOnce(snapshot).mockResolvedValueOnce(nextSnapshot); diff --git a/web/src/hooks/useBankDashboard.ts b/web/src/hooks/useBankDashboard.ts index 9f79d2c..11ee88c 100644 --- a/web/src/hooks/useBankDashboard.ts +++ b/web/src/hooks/useBankDashboard.ts @@ -54,7 +54,11 @@ export function useBankDashboard(options: BankDashboardOptions = {}): DashboardS queryKey: manifestQueryKey, queryFn: loadManifest, retry: false, + retryOnMount: false, staleTime: Number.POSITIVE_INFINITY, + refetchOnMount: false, + refetchOnReconnect: false, + refetchOnWindowFocus: false, }); const parsedManifest = useMemo(() => { @@ -79,6 +83,10 @@ export function useBankDashboard(options: BankDashboardOptions = {}): DashboardS }, enabled: manifest !== undefined, retry: false, + retryOnMount: false, + refetchOnMount: false, + refetchOnReconnect: false, + refetchOnWindowFocus: false, }); const mismatch = snapshotQuery.error !== null && isChainMismatch(snapshotQuery.error); const latestSnapshot = useRef(undefined);