fix(dashboard): accessibility and UX polish pass - #435
Conversation
Cross-cutting accessibility, loading, and empty-state polish for the admin
dashboard.
Accessibility:
- Add a "Skip to main content" link as the first tab stop in AppShell, so
keyboard users can jump past the nav to the page body. It moves focus to the
main region programmatically (the region carries tabIndex={-1}) rather than
via an anchor, because HashRouter owns the URL hash and an href="#main-content"
would register as a route change.
- Mark the decorative sidebar and header SVG icons aria-hidden, matching the
convention already used elsewhere, so they are not announced beside their
visible text labels.
Loading states:
- Add a shared PageLoading indicator (role="status") and use it on the pages
that previously rendered a bare header over blank space while their first
fetch resolved: the Overview index (was returning null), Settings, and
Tools & Guardrails.
Empty states:
- Extract a shared EmptyState component (heading, description, optional call to
action, optional extra content) and adopt it on Keys, Users, Budgets, and
Usage, replacing four near-identical hand-rolled panels so empty pages read
the same way.
Tests cover the new EmptyState and PageLoading primitives, the skip link's
focus move without a route change, and the decorative-icon aria-hidden pass.
The remaining issue items need no code change: the secret fields already mask
via native type="password" (no -webkit-text-security in use), and the routing
choice stays HashRouter (rationale documented in AGENTS.md: avoids shadowing
/v1 and the asset paths, needs no server catch-all).
Fixes #312
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Warning Review limit reached
Next review available in: 45 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (24)
WalkthroughThe dashboard adds shared loading and empty-state components, integrates them across pages, improves AppShell keyboard and assistive-technology behavior, adds focused tests, and regenerates the dashboard’s bundled assets and HTML references. ChangesDashboard polish
Estimated code review effort: 4 (Complex) | ~60 minutes Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
✨ Simplify code
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
web/src/components/AppShell.tsx (1)
383-396: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winKeep the skip link inside the modal’s inert background.
When the mobile drawer is open,
backgroundInertcovers the header and main region but not this top-level skip button. It remains an active control outside the dialog, so assistive-technology users can reach it while the modal navigation is open. Applyinert={backgroundInert}to the button or wrap it with the inert background.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web/src/components/AppShell.tsx` around lines 383 - 396, Apply inert={backgroundInert} to the top-level “Skip to main content” button so it is unavailable while the mobile navigation drawer is open, while preserving its existing focus and click behavior when the background is active.
🧹 Nitpick comments (2)
web/src/components/AppShell.test.tsx (1)
171-189: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winQuery the main region by role.
container.querySelector("main")bypasses the user-facing Testing Library query standard. Usescreen.getByRole("main")instead.As per coding guidelines, dashboard tests should query UI with
getByRole,getByLabelText, orgetByText.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web/src/components/AppShell.test.tsx` around lines 171 - 189, Update the skip-link focus test around “moves focus to the main region via the skip link without changing the route” to retrieve the main element with screen.getByRole("main") instead of container.querySelector("main"), while preserving the existing focus and route assertions.Source: Coding guidelines
web/src/components/AppShell.tsx (1)
354-364: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winUse the HeroUI v3 contract for the new skip control.
This adds a raw
<button>withonClickinweb/src, while the dashboard standard requires HeroUI v3 controls andonPress. Reuse the existingButtonimport, or document an explicit native-control exception.As per coding guidelines,
web/src/**/*.{ts,tsx,css}must use HeroUI v3 controls andonPress.Also applies to: 390-396
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web/src/components/AppShell.tsx` around lines 354 - 364, The new skip control in the `skipToMain` flow must follow the HeroUI v3 contract instead of using a raw button with `onClick`. Reuse the existing HeroUI `Button` import and migrate the control to `onPress`, preserving the current focus behavior and event prevention.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/gateway/static/dashboard/assets/index-DZsQkm8V.js`:
- Line 2: Replace the em-dash separator in the sidebar tooltip prose with a
colon or parentheses in the source component containing “otari.ai — the hosted
Otari gateway,” then regenerate the hashed dashboard bundle through the normal
build process; do not edit the generated bundle directly.
In `@src/gateway/static/dashboard/assets/KeysPage-D4aNiIgQ.js`:
- Line 1: Prevent duplicate empty-state rendering by suppressing the DataTable
emptyContent fallback in the page-level empty-state branches: update the
KeysPage implementation in
src/gateway/static/dashboard/assets/KeysPage-D4aNiIgQ.js (line 1) and the
ProvidersPage implementation in
src/gateway/static/dashboard/assets/ProvidersPage-CK0QZLgW.js (line 1) while
their API-key or provider onboarding panels are shown.
In `@src/gateway/static/dashboard/assets/ProvidersPage-CK0QZLgW.js`:
- Line 1: Update the ProvidersPage component’s DataTable rendering so the
initial empty state represented by A does not also show DataTable’s emptyContent
fallback. Keep the ke onboarding panel visible for A, while suppressing or
conditionally changing oe’s emptyContent only during that state; preserve the
normal empty message when A is false.
In `@web/src/components/AppShell.test.tsx`:
- Around line 190-201: Update the icon assertions in the “hides the decorative
nav icons from assistive tech” test to verify each SVG has aria-hidden set to
the expected true value, rather than only checking that the attribute exists.
Keep the existing icon-count assertion and render flow unchanged.
In `@web/src/components/ui.test.tsx`:
- Around line 27-46: Add a colocated Vitest case in the EmptyState suite that
renders the component with an action label, onAction handler, and
isActionDisabled enabled, then assert the CTA button is disabled. Keep the
existing enabled-action and omitted-action coverage unchanged.
---
Outside diff comments:
In `@web/src/components/AppShell.tsx`:
- Around line 383-396: Apply inert={backgroundInert} to the top-level “Skip to
main content” button so it is unavailable while the mobile navigation drawer is
open, while preserving its existing focus and click behavior when the background
is active.
---
Nitpick comments:
In `@web/src/components/AppShell.test.tsx`:
- Around line 171-189: Update the skip-link focus test around “moves focus to
the main region via the skip link without changing the route” to retrieve the
main element with screen.getByRole("main") instead of
container.querySelector("main"), while preserving the existing focus and route
assertions.
In `@web/src/components/AppShell.tsx`:
- Around line 354-364: The new skip control in the `skipToMain` flow must follow
the HeroUI v3 contract instead of using a raw button with `onClick`. Reuse the
existing HeroUI `Button` import and migrate the control to `onPress`, preserving
the current focus behavior and event prevention.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 121e9d1b-c085-4481-824f-72bf4ddfb4a9
📒 Files selected for processing (37)
src/gateway/static/dashboard/assets/ActivityPage-mLg6_55c.jssrc/gateway/static/dashboard/assets/AliasesPage-BS6u8ZL8.jssrc/gateway/static/dashboard/assets/BudgetsPage-BAJNRbKO.jssrc/gateway/static/dashboard/assets/BudgetsPage-DJhQve5r.jssrc/gateway/static/dashboard/assets/ConfirmDialog-C9cFjmHT.jssrc/gateway/static/dashboard/assets/KeysPage-D4aNiIgQ.jssrc/gateway/static/dashboard/assets/KeysPage-D5EjvMLy.jssrc/gateway/static/dashboard/assets/ModelScopeControl-DTVZtyQx.jssrc/gateway/static/dashboard/assets/ModelsPage-DbaA1Mfd.jssrc/gateway/static/dashboard/assets/OverviewPage-HaJPJjdZ.jssrc/gateway/static/dashboard/assets/OverviewPage-qo0xwTL4.jssrc/gateway/static/dashboard/assets/ProvidersPage-BKCHydlE.jssrc/gateway/static/dashboard/assets/ProvidersPage-CK0QZLgW.jssrc/gateway/static/dashboard/assets/SettingsPage-C8cekmka.jssrc/gateway/static/dashboard/assets/SettingsPage-D0bz-SUp.jssrc/gateway/static/dashboard/assets/TablePagination-CA-1c169.jssrc/gateway/static/dashboard/assets/ToolsGuardrailsPage-B7Bt8WzU.jssrc/gateway/static/dashboard/assets/ToolsGuardrailsPage-BVn9h5wq.jssrc/gateway/static/dashboard/assets/UsagePage-DOWOrs0n.jssrc/gateway/static/dashboard/assets/UsagePage-cpJfmFkD.jssrc/gateway/static/dashboard/assets/UsersPage-B7fcuwaB.jssrc/gateway/static/dashboard/assets/UsersPage-CRrrsFxS.jssrc/gateway/static/dashboard/assets/index-BzgMrLHf.jssrc/gateway/static/dashboard/assets/index-DVf-iaQ1.csssrc/gateway/static/dashboard/assets/index-DZsQkm8V.jssrc/gateway/static/dashboard/index.htmlweb/src/components/AppShell.test.tsxweb/src/components/AppShell.tsxweb/src/components/ui.test.tsxweb/src/components/ui.tsxweb/src/pages/BudgetsPage.tsxweb/src/pages/KeysPage.tsxweb/src/pages/OverviewPage.tsxweb/src/pages/SettingsPage.tsxweb/src/pages/ToolsGuardrailsPage.tsxweb/src/pages/UsagePage.tsxweb/src/pages/UsersPage.tsx
💤 Files with no reviewable changes (9)
- src/gateway/static/dashboard/assets/ToolsGuardrailsPage-BVn9h5wq.js
- src/gateway/static/dashboard/assets/UsagePage-DOWOrs0n.js
- src/gateway/static/dashboard/assets/KeysPage-D5EjvMLy.js
- src/gateway/static/dashboard/assets/BudgetsPage-BAJNRbKO.js
- src/gateway/static/dashboard/assets/SettingsPage-D0bz-SUp.js
- src/gateway/static/dashboard/assets/ProvidersPage-BKCHydlE.js
- src/gateway/static/dashboard/assets/UsersPage-B7fcuwaB.js
- src/gateway/static/dashboard/assets/OverviewPage-qo0xwTL4.js
- src/gateway/static/dashboard/assets/index-BzgMrLHf.js
There was a problem hiding this comment.
Pull request overview
This PR performs a cross-page accessibility and UX polish pass for the admin dashboard, introducing shared primitives for empty/loading states and improving keyboard/screen-reader behavior in the app shell.
Changes:
- Added shared
EmptyStateandPageLoadingUI components and adopted them across multiple pages for consistent empty/loading behavior. - Improved AppShell accessibility with a skip-to-content control and marked decorative navigation SVG icons as
aria-hidden. - Added/expanded dashboard unit tests for the new UI primitives and skip-link behavior; rebuilt and committed the dashboard static bundle.
Reviewed changes
Copilot reviewed 27 out of 37 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| web/src/pages/UsersPage.tsx | Replaces the bespoke onboarding panel with shared EmptyState. |
| web/src/pages/UsagePage.tsx | Uses shared EmptyState for the “no usage yet” screen. |
| web/src/pages/ToolsGuardrailsPage.tsx | Shows shared PageLoading during initial fetch. |
| web/src/pages/SettingsPage.tsx | Shows shared PageLoading during initial fetch. |
| web/src/pages/OverviewPage.tsx | Replaces blank initial render with PageLoading. |
| web/src/pages/KeysPage.tsx | Replaces the bespoke onboarding panel with shared EmptyState. |
| web/src/pages/BudgetsPage.tsx | Replaces the bespoke onboarding panel with shared EmptyState. |
| web/src/components/ui.tsx | Adds EmptyState and PageLoading primitives. |
| web/src/components/ui.test.tsx | Adds tests for EmptyState and PageLoading. |
| web/src/components/AppShell.tsx | Adds skip-to-content behavior and decorates SVG icons with aria-hidden. |
| web/src/components/AppShell.test.tsx | Adds tests for skip-link focus behavior and aria-hidden SVGs. |
| src/gateway/static/dashboard/index.html | Updates hashed asset references from dashboard rebuild. |
| src/gateway/static/dashboard/assets/UsersPage-CRrrsFxS.js | Updated built dashboard asset (Users page). |
| src/gateway/static/dashboard/assets/UsersPage-B7fcuwaB.js | Removed old built dashboard asset (Users page). |
| src/gateway/static/dashboard/assets/UsagePage-DOWOrs0n.js | Removed old built dashboard asset (Usage page). |
| src/gateway/static/dashboard/assets/UsagePage-cpJfmFkD.js | Updated built dashboard asset (Usage page). |
| src/gateway/static/dashboard/assets/ToolsGuardrailsPage-BVn9h5wq.js | Removed old built dashboard asset (Tools & Guardrails page). |
| src/gateway/static/dashboard/assets/ToolsGuardrailsPage-B7Bt8WzU.js | Updated built dashboard asset (Tools & Guardrails page). |
| src/gateway/static/dashboard/assets/TablePagination-CA-1c169.js | Updated built dashboard asset (pagination chunk rehash). |
| src/gateway/static/dashboard/assets/SettingsPage-D0bz-SUp.js | Removed old built dashboard asset (Settings page). |
| src/gateway/static/dashboard/assets/SettingsPage-C8cekmka.js | Updated built dashboard asset (Settings page). |
| src/gateway/static/dashboard/assets/ProvidersPage-CK0QZLgW.js | Updated built dashboard asset (Providers page). |
| src/gateway/static/dashboard/assets/ProvidersPage-BKCHydlE.js | Removed old built dashboard asset (Providers page). |
| src/gateway/static/dashboard/assets/OverviewPage-qo0xwTL4.js | Removed old built dashboard asset (Overview page). |
| src/gateway/static/dashboard/assets/OverviewPage-HaJPJjdZ.js | Updated built dashboard asset (Overview page). |
| src/gateway/static/dashboard/assets/ModelScopeControl-DTVZtyQx.js | Updated built dashboard asset (model scope control chunk rehash). |
| src/gateway/static/dashboard/assets/KeysPage-D4aNiIgQ.js | Updated built dashboard asset (Keys page). |
| src/gateway/static/dashboard/assets/ConfirmDialog-C9cFjmHT.js | Updated built dashboard asset (confirm dialog chunk rehash). |
| src/gateway/static/dashboard/assets/BudgetsPage-DJhQve5r.js | Updated built dashboard asset (Budgets page). |
| src/gateway/static/dashboard/assets/BudgetsPage-BAJNRbKO.js | Removed old built dashboard asset (Budgets page). |
| src/gateway/static/dashboard/assets/AliasesPage-BS6u8ZL8.js | Updated built dashboard asset (Aliases page chunk rehash). |
Files not reviewed (9)
- src/gateway/static/dashboard/assets/BudgetsPage-DJhQve5r.js: Generated file
- src/gateway/static/dashboard/assets/KeysPage-D4aNiIgQ.js: Generated file
- src/gateway/static/dashboard/assets/OverviewPage-HaJPJjdZ.js: Generated file
- src/gateway/static/dashboard/assets/ProvidersPage-CK0QZLgW.js: Generated file
- src/gateway/static/dashboard/assets/SettingsPage-C8cekmka.js: Generated file
- src/gateway/static/dashboard/assets/ToolsGuardrailsPage-B7Bt8WzU.js: Generated file
- src/gateway/static/dashboard/assets/UsagePage-cpJfmFkD.js: Generated file
- src/gateway/static/dashboard/assets/UsersPage-CRrrsFxS.js: Generated file
- src/gateway/static/dashboard/assets/index-DZsQkm8V.js: Generated file
khaledosman
left a comment
There was a problem hiding this comment.
Good consistency pass. One a11y gap in the skip link (inline) that is a one-line fix, nothing blocking.
I chased and could not substantiate three things worth naming so they do not get re-raised: EmptyState's children/isActionDisabled are legitimate optional API rather than dead surface; the orphaned // ---------- onboarding ---------- banners are cosmetic; and the new PageLoading on Settings does not double up with the empty-state copy, since "No settings match your search" is guarded by data &&.
One coordination note: this conflicts with #434 in web/src/components/AppShell.tsx and AppShell.test.tsx (both rewrite the sidebar footer, both insert tests at the same anchor). #434's footer is structurally a superset, so resolving in its favour silently reverts the aria-hidden added here on the otari.ai icon, and #434's new book icon never had one. The blanket svg assertion added in this PR would catch that, but only if it survives its own conflict in the same file. Whoever merges second should take #434's footer <div> and re-apply aria-hidden to both svgs inside it.
🤖 Review generated with Claude Code
Resolve conflicts with #434 (bundled user guide): - AppShell.tsx: take #434's sidebar footer (Docs link + otari.ai); both its SVGs already carry aria-hidden, so this PR's otari.ai aria-hidden change is subsumed. This PR's skip link, main tabIndex, and the nav/hamburger/chevron aria-hidden changes are in untouched regions and merge cleanly. - AppShell.test.tsx: keep all tests from both sides (this PR's skip-link and decorative-icon tests plus #434's user-guide footer test). - Dashboard bundle + index.html: regenerated from the merged source with a fresh Node 22 build rather than hand-merged; verified deterministic. Also fold in the review fix from @khaledosman: the skip link now goes inert with the drawer (inert={backgroundInert}) so it is not a live no-op control in front of the modal, with a drawer-open test asserting it. The blanket "every svg is aria-hidden" test now also guards #434's two footer icons.
When a fresh gateway had no keys (or no users), both the onboarding EmptyState panel and the DataTable's own "no rows" fallback rendered, stacking two empty messages. Gate the table on `!showOnboarding` so the onboarding panel owns the empty state and the redundant empty table is suppressed. Regression tests assert only the panel shows (the table's unique fallback text and the grid are absent). Addresses a CodeRabbit finding on #435. The redundancy predated this PR; fixing it here keeps with the PR's empty-state-consistency theme.
There was a problem hiding this comment.
🧹 Nitpick comments (1)
web/src/components/AppShell.test.tsx (1)
219-227: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winExercise the configured
HashRouterin this test.Because
renderShell()usesMemoryRouter, this only verifies the memory-history URL. If the footer uses a router link, production hash navigation could differ while this test remains green. Add a HashRouter-specific render and assert the actual navigation behavior, verifying the exact href for the installed router version.As per coding guidelines, keep client-side routing in the configured
HashRouterstructure.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web/src/components/AppShell.test.tsx` around lines 219 - 227, Update the sidebar footer link test around renderShell to render the shell with the configured HashRouter instead of only MemoryRouter. Exercise the footer’s router link and assert the exact generated href/navigation value for the installed router version, while preserving the existing “User guide” link lookup and routing structure.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@web/src/components/AppShell.test.tsx`:
- Around line 219-227: Update the sidebar footer link test around renderShell to
render the shell with the configured HashRouter instead of only MemoryRouter.
Exercise the footer’s router link and assert the exact generated href/navigation
value for the installed router version, while preserving the existing “User
guide” link lookup and routing structure.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: f3fc39fe-034a-4c04-8b4c-230d99f7c228
📒 Files selected for processing (24)
src/gateway/static/dashboard/assets/ActivityPage-l6qAuM38.jssrc/gateway/static/dashboard/assets/AliasesPage-CkBUnFDs.jssrc/gateway/static/dashboard/assets/BudgetsPage-DEnVlBgO.jssrc/gateway/static/dashboard/assets/ConfirmDialog-D6sbSZW_.jssrc/gateway/static/dashboard/assets/DocsPage-DJlCEoVk.jssrc/gateway/static/dashboard/assets/KeysPage-BLxP70jq.jssrc/gateway/static/dashboard/assets/ModelScopeControl-HCOWUmyk.jssrc/gateway/static/dashboard/assets/ModelsPage-pjOjR_7H.jssrc/gateway/static/dashboard/assets/OverviewPage-DF4vNei6.jssrc/gateway/static/dashboard/assets/ProvidersPage-C5UhEWyN.jssrc/gateway/static/dashboard/assets/SettingsPage-BWWxqeMF.jssrc/gateway/static/dashboard/assets/TablePagination-YCOYLJAM.jssrc/gateway/static/dashboard/assets/ToolsGuardrailsPage-C2buPoJ5.jssrc/gateway/static/dashboard/assets/UsagePage-fRandTvt.jssrc/gateway/static/dashboard/assets/UsersPage-D6NNTp6g.jssrc/gateway/static/dashboard/assets/index-C6t-OIZc.csssrc/gateway/static/dashboard/assets/index-WTCTqLVV.jssrc/gateway/static/dashboard/index.htmlweb/src/components/AppShell.test.tsxweb/src/components/AppShell.tsxweb/src/pages/KeysPage.test.tsxweb/src/pages/KeysPage.tsxweb/src/pages/UsersPage.test.tsxweb/src/pages/UsersPage.tsx
🚧 Files skipped from review as they are similar to previous changes (4)
- web/src/pages/KeysPage.tsx
- src/gateway/static/dashboard/index.html
- web/src/pages/UsersPage.tsx
- web/src/components/AppShell.tsx
Extend the previous Keys/Users fix to the other two list pages: gate the DataTable on `!showOnboarding` so the onboarding panel owns the empty state instead of stacking over the table's own "no rows" fallback. Providers' panel is titled "Welcome to Otari", so its regression test keys off "No providers yet" (unique to the table fallback); the same test's wait now targets the onboarding button rather than that now-suppressed table text.
…CTA test) - AppShell: replace the em dash in the otari.ai sidebar tooltip with a colon, per the repo prose-style rule (flagged by CodeRabbit). - EmptyState: narrow `description` from ReactNode to string (like PageHeader) so a block element can't be nested inside its <p>; rich content still goes through `children` (flagged by Copilot). - ui.test.tsx: cover the disabled-CTA branch (isActionDisabled), asserting the button is disabled and onAction does not fire (flagged by CodeRabbit).
…cs (mozilla-ai#434) * feat(dashboard): bundle the operator user guide and serve it at /#/docs A self-hosted gateway can run a version of Otari that differs from otari.ai, so the dashboard should carry the guide that matches the running build rather than sending operators to a separate docs site that may describe a different version. The dashboard now renders docs/dashboard.md at /#/docs, behind the existing master-key sign-in, and links to it from the sidebar footer. The guide is imported with Vite's ?raw so it inlines into the (lazy-loaded) docs chunk at build time, keeping it version-matched to the bundle. Sibling doc links that are not bundled here are rewritten to the rendered source on GitHub and open in a new tab. Because the guide lives outside web/, vite.config.ts grants server.fs access to the repo root, and the dashboard staleness workflow now also triggers on docs/dashboard.md so an edit that leaves the committed bundle stale is caught. Fixes mozilla-ai#329 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(dashboard): tighten the guide's leading-title strip to ATX headings Only strip the guide's opening line when it is a real ATX heading (`#`..`######` followed by whitespace), so a first line that merely starts with `#` is left intact. No behavior change for the current guide. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style(dashboard): tokenize the guide code-block foreground color Replace the raw #e8eef0 hex in .otari-markdown pre code with a named --otari-code-ink token defined in :root, matching the dashboard's token-only color convention (addresses a CodeRabbit review note). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(dashboard): address review on the bundled user guide Resolve khaledosman's review findings on PR mozilla-ai#434: - Security: narrow the dev server's server.fs.allow to web/ and docs/ instead of the repo root. An explicit allow list replaces Vite's default, so the repo root exposed gitignored secrets (config.yml, otari.db) at /@fs/... over `npm run dev`. Verified config.yml now returns 403 while docs/dashboard.md still loads. - Bug: destructure react-markdown's `node` out of the `a`/`table`/`pre` DOM spread so links no longer ship node="[object Object]"; type the components with ComponentPropsWithoutRef & ExtraProps so TS catches the extra prop. - Rewrite in-page "#frag" links (and remark-gfm footnote anchors) to the GitHub source, since HashRouter owns location.hash and would bounce the operator to Overview. - A11y: keep the native <table> display (a display:block table drops role=table in WebKit) and move the horizontal scroll to a focusable region wrapper; make scrollable <pre> blocks focusable too (axe scrollable-region-focusable); signal link hover by thickening the underline instead of lightening the text below the 4.5:1 AA threshold; mark the sidebar guide icon aria-hidden. Adds tests that anchors carry no `node` attribute and that the table renders inside a focusable scroll region with its semantics intact. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * a11y(dashboard): mark the sidebar otari.ai icon aria-hidden Decorative icon on a text-labelled link; hide it from assistive tech, matching the new user-guide icon beside it. This also makes the footer block a complete superset of PR mozilla-ai#435's a11y pass, so resolving the (latent) footer merge conflict in favour of this block drops none of mozilla-ai#435's aria-hidden coverage. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(dashboard): trim the first-run walkthrough from the in-app guide The guide is reachable only through a running, signed-in dashboard, so the getting-started walkthrough (start the gateway, find the master key, sign in) is circular there. Drop that section from the rendered view so /#/docs reads as an operator reference (two-key model, page-by-page reference, security notes), and point newcomers at /welcome for the get-started flow. The trim is render-side only: docs/dashboard.md stays whole for readers on GitHub and the /welcome tutorial. Its post-sign-in substance (adding a provider, keys, budgets) is already covered by the retained page-by-page reference. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Description
Cross-cutting accessibility, loading, and empty-state polish for the admin dashboard. Started from a survey of all 11 pages so the changes are consistent rather than one-off.
Accessibility
AppShell, so keyboard users can jump past the nav to the page body. It moves focus to the main region programmatically (the region carriestabIndex={-1}) rather than via an anchor, becauseHashRouterowns the URL hash and anhref="#main-content"would register as a route change.aria-hidden, matching the convention already used elsewhere, so they are not announced beside their visible text labels.Loading states
PageLoadingindicator (role="status") on the pages that previously rendered a bare header over blank space while their first fetch resolved: the Overview index (was returningnull), Settings, and Tools & Guardrails.Empty states
EmptyStatecomponent replacing four near-identical hand-rolled onboarding panels (Keys, Users, Budgets, Usage), so empty pages read the same way.Items that needed no code change: secret fields already mask via native
type="password"(no-webkit-text-securityin use), and routing staysHashRouter(rationale documented in AGENTS.md: avoids shadowing/v1and the asset paths, needs no server catch-all).PR Type
Relevant issues
Fixes #312
Checklist
web/src; this PR touches no Python, so notests/unitortests/integrationchanges.)npm --prefix web run typecheck,npm --prefix web test,npm --prefix web run build; no Python changed, so themaketargets are not applicable.)AI Usage
AI Model/Tool used:
Claude Code (Opus 4.8)
Any additional AI details you'd like to share:
Implemented and reviewed by Claude Code via back-and-forth with @njbrake; the reasoning and decisions are his, the code and prose are Claude's.
Summary
Benefits
These changes provide clearer feedback during loading, more consistent guidance for empty pages, and improved keyboard and screen-reader usability across the admin dashboard.
Technical notes
Routing remains based on
HashRouter; native password masking, responsive layouts, API contracts, and Python code are unchanged.