Skip to content

fix(dashboard): accessibility and UX polish pass - #435

Merged
njbrake merged 5 commits into
mainfrom
fix/dashboard-ux-polish
Jul 27, 2026
Merged

fix(dashboard): accessibility and UX polish pass#435
njbrake merged 5 commits into
mainfrom
fix/dashboard-ux-polish

Conversation

@njbrake

@njbrake njbrake commented Jul 26, 2026

Copy link
Copy Markdown
Member

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

  • Skip-to-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.
  • Decorative sidebar and header SVG icons marked aria-hidden, matching the convention already used elsewhere, so they are not announced beside their visible text labels.

Loading states

  • Shared PageLoading indicator (role="status") 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

  • Shared EmptyState component 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-security in use), and routing stays HashRouter (rationale documented in AGENTS.md: avoids shadowing /v1 and the asset paths, needs no server catch-all).

PR Type

  • New Feature
  • Bug Fix
  • Refactor
  • Documentation
  • Infrastructure / CI

Relevant issues

Fixes #312

Checklist

  • I understand the code I am submitting.
  • I have added or updated tests that cover my change. (Colocated Vitest tests under web/src; this PR touches no Python, so no tests/unit or tests/integration changes.)
  • I ran the Definition of Done checks locally. (Dashboard checks: npm --prefix web run typecheck, npm --prefix web test, npm --prefix web run build; no Python changed, so the make targets are not applicable.)
  • Documentation was updated where necessary. (No user-facing docs needed changes.)
  • If the API contract changed, I regenerated the OpenAPI spec. (N/A: no API contract change; dashboard-only.)

AI Usage

  • No AI was used.
  • AI was used for drafting/refactoring.
  • This is fully AI-generated.

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.

  • I am an AI Agent filling out this form (check box if true)

Summary

  • Improved dashboard accessibility with a keyboard-accessible skip link and clearer treatment of decorative icons.
  • Added consistent loading indicators to Overview, Settings, and Tools & Guardrails.
  • Standardized empty states across Keys, Users, Budgets, and Usage, including cleaner first-time onboarding displays.
  • Added focused tests covering accessibility, loading, empty states, and onboarding behavior.
  • Refreshed generated dashboard assets to reflect the updated frontend build.

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.

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>
@njbrake
njbrake temporarily deployed to integration-tests July 26, 2026 18:45 — with GitHub Actions Inactive
@github-actions github-actions Bot added the missing-template PR is missing required template sections label Jul 26, 2026
@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@njbrake, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 45 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: f646bf6e-be9b-4a4f-9220-5057e3ac53fe

📥 Commits

Reviewing files that changed from the base of the PR and between 3c28f69 and 4f42147.

📒 Files selected for processing (24)
  • src/gateway/static/dashboard/assets/ActivityPage-Byt0czqE.js
  • src/gateway/static/dashboard/assets/AliasesPage-C8gXJ6jx.js
  • src/gateway/static/dashboard/assets/BudgetsPage-Dg79mJut.js
  • src/gateway/static/dashboard/assets/ConfirmDialog-Dnahrq6G.js
  • src/gateway/static/dashboard/assets/DocsPage-gss9QTdF.js
  • src/gateway/static/dashboard/assets/KeysPage-Bo1aN9_q.js
  • src/gateway/static/dashboard/assets/ModelScopeControl-DedlZSDh.js
  • src/gateway/static/dashboard/assets/ModelsPage-BynwkYFh.js
  • src/gateway/static/dashboard/assets/OverviewPage-Cq0SX39a.js
  • src/gateway/static/dashboard/assets/ProvidersPage-DaZtXRNl.js
  • src/gateway/static/dashboard/assets/SettingsPage-BGHLvVKm.js
  • src/gateway/static/dashboard/assets/TablePagination-Cz-ztZ68.js
  • src/gateway/static/dashboard/assets/ToolsGuardrailsPage-BhhjnFCR.js
  • src/gateway/static/dashboard/assets/UsagePage-BiVR6PKf.js
  • src/gateway/static/dashboard/assets/UsersPage-CAT4LFyA.js
  • src/gateway/static/dashboard/assets/index-5xwcNRYi.js
  • src/gateway/static/dashboard/index.html
  • web/src/components/AppShell.tsx
  • web/src/components/ui.test.tsx
  • web/src/components/ui.tsx
  • web/src/pages/BudgetsPage.test.tsx
  • web/src/pages/BudgetsPage.tsx
  • web/src/pages/ProvidersPage.test.tsx
  • web/src/pages/ProvidersPage.tsx

Walkthrough

The 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.

Changes

Dashboard polish

Layer / File(s) Summary
Shared loading and empty states
web/src/components/ui.tsx, web/src/components/ui.test.tsx, web/src/pages/*.tsx, web/src/pages/*Page.test.tsx
Adds tested EmptyState and PageLoading components, then applies them to onboarding, empty, and loading branches across dashboard pages.
App shell accessibility
web/src/components/AppShell.tsx, web/src/components/AppShell.test.tsx
Adds skip-to-main focus behavior, drawer-aware inert handling, and aria-hidden attributes for decorative SVGs, with accessibility tests.
Regenerated dashboard page bundles
src/gateway/static/dashboard/assets/*Page-*.js
Adds or regenerates bundled dashboard pages, including overview, providers, settings, tools, usage, users, budgets, and documentation rendering.
Static asset and runtime wiring
src/gateway/static/dashboard/assets/index-*.js, src/gateway/static/dashboard/assets/index-*.css, src/gateway/static/dashboard/index.html
Updates generated CSS, Vite dependency mappings, and hashed script and stylesheet references.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

Suggested reviewers: khaledosman

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR covers accessibility, empty states, and loading states, but leaves the issue's responsive/mobile and error-state work unaddressed. Add the responsive/mobile polish and explicit error-state updates, or split #312 into narrower follow-up issues before closing it.
Docstring Coverage ⚠️ Warning Docstring coverage is 4.30% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the dashboard accessibility and UX changes and uses a conventional-commit style prefix.
Description check ✅ Passed The description follows the required template and includes the requested sections, checklist, and issue reference.
Out of Scope Changes check ✅ Passed The changes stay focused on dashboard accessibility, loading, and empty-state polish; no unrelated code paths stand out.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/dashboard-ux-polish
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch fix/dashboard-ux-polish

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai
coderabbitai Bot requested a review from khaledosman July 26, 2026 18:45

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 win

Keep the skip link inside the modal’s inert background.

When the mobile drawer is open, backgroundInert covers 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. Apply inert={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 win

Query the main region by role.

container.querySelector("main") bypasses the user-facing Testing Library query standard. Use screen.getByRole("main") instead.

As per coding guidelines, dashboard tests should query UI with getByRole, getByLabelText, or getByText.

🤖 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 win

Use the HeroUI v3 contract for the new skip control.

This adds a raw <button> with onClick in web/src, while the dashboard standard requires HeroUI v3 controls and onPress. Reuse the existing Button import, or document an explicit native-control exception.

As per coding guidelines, web/src/**/*.{ts,tsx,css} must use HeroUI v3 controls and onPress.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 5fa80d0 and 6382f87.

📒 Files selected for processing (37)
  • src/gateway/static/dashboard/assets/ActivityPage-mLg6_55c.js
  • src/gateway/static/dashboard/assets/AliasesPage-BS6u8ZL8.js
  • src/gateway/static/dashboard/assets/BudgetsPage-BAJNRbKO.js
  • src/gateway/static/dashboard/assets/BudgetsPage-DJhQve5r.js
  • src/gateway/static/dashboard/assets/ConfirmDialog-C9cFjmHT.js
  • src/gateway/static/dashboard/assets/KeysPage-D4aNiIgQ.js
  • src/gateway/static/dashboard/assets/KeysPage-D5EjvMLy.js
  • src/gateway/static/dashboard/assets/ModelScopeControl-DTVZtyQx.js
  • src/gateway/static/dashboard/assets/ModelsPage-DbaA1Mfd.js
  • src/gateway/static/dashboard/assets/OverviewPage-HaJPJjdZ.js
  • src/gateway/static/dashboard/assets/OverviewPage-qo0xwTL4.js
  • src/gateway/static/dashboard/assets/ProvidersPage-BKCHydlE.js
  • src/gateway/static/dashboard/assets/ProvidersPage-CK0QZLgW.js
  • src/gateway/static/dashboard/assets/SettingsPage-C8cekmka.js
  • src/gateway/static/dashboard/assets/SettingsPage-D0bz-SUp.js
  • src/gateway/static/dashboard/assets/TablePagination-CA-1c169.js
  • src/gateway/static/dashboard/assets/ToolsGuardrailsPage-B7Bt8WzU.js
  • src/gateway/static/dashboard/assets/ToolsGuardrailsPage-BVn9h5wq.js
  • src/gateway/static/dashboard/assets/UsagePage-DOWOrs0n.js
  • src/gateway/static/dashboard/assets/UsagePage-cpJfmFkD.js
  • src/gateway/static/dashboard/assets/UsersPage-B7fcuwaB.js
  • src/gateway/static/dashboard/assets/UsersPage-CRrrsFxS.js
  • src/gateway/static/dashboard/assets/index-BzgMrLHf.js
  • src/gateway/static/dashboard/assets/index-DVf-iaQ1.css
  • src/gateway/static/dashboard/assets/index-DZsQkm8V.js
  • src/gateway/static/dashboard/index.html
  • web/src/components/AppShell.test.tsx
  • web/src/components/AppShell.tsx
  • web/src/components/ui.test.tsx
  • web/src/components/ui.tsx
  • web/src/pages/BudgetsPage.tsx
  • web/src/pages/KeysPage.tsx
  • web/src/pages/OverviewPage.tsx
  • web/src/pages/SettingsPage.tsx
  • web/src/pages/ToolsGuardrailsPage.tsx
  • web/src/pages/UsagePage.tsx
  • web/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

Comment thread src/gateway/static/dashboard/assets/index-DZsQkm8V.js Outdated
Comment thread src/gateway/static/dashboard/assets/KeysPage-D4aNiIgQ.js Outdated
Comment thread src/gateway/static/dashboard/assets/ProvidersPage-CK0QZLgW.js Outdated
Comment thread web/src/components/AppShell.test.tsx
Comment thread web/src/components/ui.test.tsx

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 EmptyState and PageLoading UI 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

Comment thread web/src/components/AppShell.tsx
Comment thread web/src/components/ui.tsx

@khaledosman khaledosman left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread web/src/components/AppShell.tsx
@github-actions github-actions Bot removed the missing-template PR is missing required template sections label Jul 27, 2026
@njbrake
njbrake requested a review from khaledosman July 27, 2026 17:16
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.
@njbrake
njbrake temporarily deployed to integration-tests July 27, 2026 17:52 — with GitHub Actions Inactive
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.
@njbrake
njbrake temporarily deployed to integration-tests July 27, 2026 18:12 — with GitHub Actions Inactive

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
web/src/components/AppShell.test.tsx (1)

219-227: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Exercise the configured HashRouter in this test.

Because renderShell() uses MemoryRouter, 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 HashRouter structure.

🤖 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

📥 Commits

Reviewing files that changed from the base of the PR and between 6382f87 and 3c28f69.

📒 Files selected for processing (24)
  • src/gateway/static/dashboard/assets/ActivityPage-l6qAuM38.js
  • src/gateway/static/dashboard/assets/AliasesPage-CkBUnFDs.js
  • src/gateway/static/dashboard/assets/BudgetsPage-DEnVlBgO.js
  • src/gateway/static/dashboard/assets/ConfirmDialog-D6sbSZW_.js
  • src/gateway/static/dashboard/assets/DocsPage-DJlCEoVk.js
  • src/gateway/static/dashboard/assets/KeysPage-BLxP70jq.js
  • src/gateway/static/dashboard/assets/ModelScopeControl-HCOWUmyk.js
  • src/gateway/static/dashboard/assets/ModelsPage-pjOjR_7H.js
  • src/gateway/static/dashboard/assets/OverviewPage-DF4vNei6.js
  • src/gateway/static/dashboard/assets/ProvidersPage-C5UhEWyN.js
  • src/gateway/static/dashboard/assets/SettingsPage-BWWxqeMF.js
  • src/gateway/static/dashboard/assets/TablePagination-YCOYLJAM.js
  • src/gateway/static/dashboard/assets/ToolsGuardrailsPage-C2buPoJ5.js
  • src/gateway/static/dashboard/assets/UsagePage-fRandTvt.js
  • src/gateway/static/dashboard/assets/UsersPage-D6NNTp6g.js
  • src/gateway/static/dashboard/assets/index-C6t-OIZc.css
  • src/gateway/static/dashboard/assets/index-WTCTqLVV.js
  • src/gateway/static/dashboard/index.html
  • web/src/components/AppShell.test.tsx
  • web/src/components/AppShell.tsx
  • web/src/pages/KeysPage.test.tsx
  • web/src/pages/KeysPage.tsx
  • web/src/pages/UsersPage.test.tsx
  • web/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.
@njbrake
njbrake temporarily deployed to integration-tests July 27, 2026 18:18 — with GitHub Actions Inactive
…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).
@njbrake
njbrake temporarily deployed to integration-tests July 27, 2026 18:25 — with GitHub Actions Inactive
@njbrake
njbrake merged commit b153949 into main Jul 27, 2026
7 checks passed
@njbrake
njbrake deleted the fix/dashboard-ux-polish branch July 27, 2026 18:29
tbille pushed a commit to champ18ion/otari that referenced this pull request Jul 28, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Dashboard: accessibility, responsive, and UX polish

3 participants