From 3b648460020d52821e6a23103085bf2a017ed55e Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 4 Aug 2026 07:26:54 +0000 Subject: [PATCH 1/2] fix(spa): announce the UsersPage load-failure to screen readers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit UsersPage rendered its load-failure (error && users === null) as a bare `

`, while every sibling screen — AuditPage, ReportsPage, ExportPage, StockPage, HistoryPage, SettingsPage — uses role="alert" for the same case. role="alert" is an implicit live region, so without it a screen-reader user who deep-links to /users and is refused hears the heading and then silence, while the visual user sees red text. Add role="alert" to match the siblings. Scope is the unambiguous load-failure only. The dialog-level and page-level error paragraphs are left as-is: per #389 those are a judgment call (a dialog error adjacent to its field may be better served by aria-describedby than by an alert), and belong in a separate pass. No unit test added: UsersPage.test.tsx documents (its "UsersPage load" block) that the mount-effect rejection branch cannot be asserted in this Vitest 3 + React 19 stack without a false-positive unhandled rejection. Per #389 the #385 Playwright ReadOnly deep-link spec is where this tightens to getByRole("alert"). typecheck clean; the 58 existing UsersPage tests pass. Closes #389. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01LSkqgsC9Sj4arUdevUSJfJ --- web/src/routes/UsersPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/routes/UsersPage.tsx b/web/src/routes/UsersPage.tsx index 61879e771..e8f97b5b2 100644 --- a/web/src/routes/UsersPage.tsx +++ b/web/src/routes/UsersPage.tsx @@ -344,7 +344,7 @@ export function UsersPage() { }); } - if (error && users === null) return

{t("heading")}

{error}

; + if (error && users === null) return

{t("heading")}

{error}

; if (users === null) return

{t("heading")}

{tc("loading")}

; return ( From f40269eef969e0d1bbdadc6a5e7f202a6482cc57 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 4 Aug 2026 07:32:52 +0000 Subject: [PATCH 2/2] fix(spa): also announce the UsersPage page-level error banner MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review-loop follow-up. The page-level error banner (shown only when no dialog is open: error && !creating && openUser === null && editUser === null && pwUser === null) was still a bare `

`, while the sibling page-level banners announce — ProductsPage (error && !dialogOpen) and ExpensesPage both mark theirs role="alert". It is the same unambiguous page-level class as the load-failure at line 347, not a dialog-adjacent judgment call, so it gets role="alert" too. Still deliberately left bare: the dialog-scoped validation errors (create / assign / edit / password dialogs), which #389 flags as a per-field judgment call (aria-describedby may beat an alert). typecheck clean; 58 UsersPage tests pass. Refs #389. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01LSkqgsC9Sj4arUdevUSJfJ --- web/src/routes/UsersPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/routes/UsersPage.tsx b/web/src/routes/UsersPage.tsx index e8f97b5b2..f74574b5b 100644 --- a/web/src/routes/UsersPage.tsx +++ b/web/src/routes/UsersPage.tsx @@ -406,7 +406,7 @@ export function UsersPage() { {/* Each dialog carries its own error copy while it is up. */} {error && !creating && openUser === null && editUser === null && pwUser === null - &&

{error}

} + &&

{error}

} {message &&

{message}

}