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.
-
-
+
+
-
+
- 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);