Skip to content

feat(dashboard): bundle the operator user guide and serve it at /#/docs - #434

Merged
njbrake merged 6 commits into
mainfrom
fix/dashboard-bundle-docs
Jul 27, 2026
Merged

feat(dashboard): bundle the operator user guide and serve it at /#/docs#434
njbrake merged 6 commits into
mainfrom
fix/dashboard-bundle-docs

Conversation

@njbrake

@njbrake njbrake commented Jul 26, 2026

Copy link
Copy Markdown
Member

Note: this PR description was drafted by Claude via back-and-forth with @njbrake. The reasoning and decisions are his; the prose is Claude's.

Description

Fixes #329.

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.

What this does:

  • New docs page at /#/docs (behind the existing master-key sign-in) that renders docs/dashboard.md inline, with a "User guide" link in the sidebar footer.
  • The guide is imported with Vite's ?raw, so it inlines into the lazy-loaded docs chunk at build time and stays version-matched to the bundle. The chunk loads only when the page is visited, so the shell stays small.
  • Rendering uses react-markdown + remark-gfm (the guide has GFM tables and fenced code). Prose is styled with the existing --otari-* tokens via a scoped .otari-markdown stylesheet, mirroring the .otari-table pattern (no typography plugin added).
  • Sibling doc links that are not bundled here (for example configuration.md) are rewritten to the rendered source on GitHub and open in a new tab; in-page anchors stay in the tab.
  • The guide's own leading title is dropped so the page shows a single top-level heading ("User guide"), with the guide's intro flowing under it.

Frontend only: /#/docs is a client-side HashRouter route served by the existing index.html, so no backend route was needed. Because the guide lives outside web/, 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 in CI.

PR Type

  • New Feature

Relevant issues

Fixes #329

Checklist

  • I understand the code I am submitting.
  • I have added or updated tests that cover my change (web/src/pages/DocsPage.test.tsx, web/src/components/AppShell.test.tsx; this is a web/ change, so the JS suite covers it rather than tests/unit).
  • I ran the Definition of Done checks locally (npm --prefix web run typecheck, npm --prefix web test, npm --prefix web run build; no Python changed).
  • Documentation was updated where necessary (AGENTS.md notes the new guide bundling and its rebuild coupling).
  • If the API contract changed, I regenerated the OpenAPI spec (no API contract change).

AI Usage

  • AI was used for drafting/refactoring.

AI Model/Tool used: Claude Code (Opus 4.8)

Any additional AI details you'd like to share: Implemented via back-and-forth with @njbrake; the reasoning and decisions are his.

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

Summary

  • Added an authenticated /docs dashboard page that embeds the version-matched user guide.
  • Added a sidebar link to the guide and improved accessibility for navigation elements.
  • Added Markdown/GFM rendering, responsive tables, accessible code blocks, scoped styling, and GitHub links for related documentation.
  • Updated build configuration and workflow checks so guide changes rebuild or flag stale dashboard assets.
  • Regenerated bundled dashboard assets and added tests covering rendering, links, tables, and accessibility.

Benefits

Users can access the deployed version’s guide directly from the dashboard without leaving the application, while related documentation remains easy to discover and the rendered content is more accessible.

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 #329

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@njbrake
njbrake temporarily deployed to integration-tests July 26, 2026 18:40 — 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: 29 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: 76cd70b4-171d-41bf-8084-275b7de3e58f

📥 Commits

Reviewing files that changed from the base of the PR and between 30340fa and c0cacb4.

📒 Files selected for processing (19)
  • src/gateway/static/dashboard/assets/ActivityPage-RaXV2Jg6.js
  • src/gateway/static/dashboard/assets/AliasesPage-CX3eAAXN.js
  • src/gateway/static/dashboard/assets/BudgetsPage-2_kce_tq.js
  • src/gateway/static/dashboard/assets/ConfirmDialog-nzYO6eyy.js
  • src/gateway/static/dashboard/assets/DocsPage-C8AK643M.js
  • src/gateway/static/dashboard/assets/KeysPage-CaQTAd2N.js
  • src/gateway/static/dashboard/assets/ModelScopeControl-L-_tKOtj.js
  • src/gateway/static/dashboard/assets/ModelsPage-Cw8Xv_w2.js
  • src/gateway/static/dashboard/assets/OverviewPage-Deh5RtuT.js
  • src/gateway/static/dashboard/assets/ProvidersPage-BRR1ZEsl.js
  • src/gateway/static/dashboard/assets/SettingsPage-CDoI9NPf.js
  • src/gateway/static/dashboard/assets/TablePagination-ByewydW5.js
  • src/gateway/static/dashboard/assets/ToolsGuardrailsPage-bas8EytO.js
  • src/gateway/static/dashboard/assets/UsagePage-BD35XJZn.js
  • src/gateway/static/dashboard/assets/UsersPage-CbRblpp_.js
  • src/gateway/static/dashboard/assets/index-CI2beHfq.js
  • src/gateway/static/dashboard/index.html
  • web/src/pages/DocsPage.test.tsx
  • web/src/pages/DocsPage.tsx

Walkthrough

Adds an authenticated /docs dashboard route that renders docs/dashboard.md as Markdown, links it from the sidebar, styles and tests the page, configures filesystem access, and regenerates committed dashboard assets with freshness workflow triggers.

Changes

Dashboard user guide

Layer / File(s) Summary
Markdown guide rendering
web/package.json, web/vite.config.ts, web/src/pages/DocsPage.tsx, web/src/styles/globals.css, web/src/pages/DocsPage.test.tsx
Adds raw guide rendering with GitHub-Flavored Markdown, rewritten external links, accessible tables and code blocks, scoped styling, filesystem configuration, and coverage for the rendered output.
Route and sidebar access
web/src/App.tsx, web/src/components/AppShell.tsx, web/src/components/AppShell.test.tsx
Adds the lazy-loaded /docs route and a responsive sidebar footer link for the User guide.
Committed dashboard bundle
src/gateway/static/dashboard/assets/*, src/gateway/static/dashboard/index.html
Regenerates the dashboard JavaScript and CSS assets, adds the bundled DocsPage chunk, updates hashed imports, and points the static entry page to the new outputs.
Bundle freshness documentation and triggers
AGENTS.md, .github/workflows/otari-dashboard.yml
Documents the bundled guide workflow and triggers dashboard checks when the guide changes.

Estimated code review effort: 3 (Moderate) | ~30 minutes

Possibly related PRs

Suggested reviewers: khaledosman

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title is a conventional-commit-style summary that accurately describes the docs bundle and /docs route change.
Description check ✅ Passed The description includes the required sections, issue reference, checklist, and AI usage details, and is mostly complete.
Linked Issues check ✅ Passed The PR adds an authenticated /docs page, bundles the user guide, and adds the sidebar link requested by #329.
Out of Scope Changes check ✅ Passed The remaining workflow, docs, and bundle asset changes support the /docs feature and staleness detection, so no clear out-of-scope work stands out.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/dashboard-bundle-docs
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch fix/dashboard-bundle-docs

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.

@github-actions github-actions Bot removed the missing-template PR is missing required template sections label Jul 26, 2026
@coderabbitai
coderabbitai Bot requested a review from khaledosman July 26, 2026 18:40

@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: 2

🤖 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 @.github/workflows/otari-dashboard.yml:
- Around line 15-20: Add the workflow file itself to the pull_request path
filters in the relevant trigger configuration, matching the existing push
coverage so changes to the workflow’s staleness logic run validation before
merge.

In `@web/src/styles/globals.css`:
- Around line 203-210: Replace the hard-coded foreground color in
.otari-markdown pre code with a named --otari-* token, and define that token in
the :root section of globals.css using the existing `#e8eef0` value.
🪄 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: 611c782f-f76e-4dfd-85c3-d1e416c87ecf

📥 Commits

Reviewing files that changed from the base of the PR and between 5fa80d0 and 46fb308.

⛔ Files ignored due to path filters (1)
  • web/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (29)
  • .github/workflows/otari-dashboard.yml
  • AGENTS.md
  • src/gateway/static/dashboard/assets/ActivityPage-B5EUMS4E.js
  • src/gateway/static/dashboard/assets/AliasesPage-DONisBbD.js
  • src/gateway/static/dashboard/assets/BudgetsPage-BFFSfxbi.js
  • src/gateway/static/dashboard/assets/ConfirmDialog-DJSi-9PG.js
  • src/gateway/static/dashboard/assets/DocsPage-I5B5QJVx.js
  • src/gateway/static/dashboard/assets/KeysPage-Cy-JTip2.js
  • src/gateway/static/dashboard/assets/ModelScopeControl-CKbp2KZ3.js
  • src/gateway/static/dashboard/assets/ModelsPage-CyF9pIyI.js
  • src/gateway/static/dashboard/assets/OverviewPage-D3crDRZk.js
  • src/gateway/static/dashboard/assets/ProvidersPage-B-f-Neug.js
  • src/gateway/static/dashboard/assets/SettingsPage-jZ4yfj2x.js
  • src/gateway/static/dashboard/assets/TablePagination-ku5ssWSA.js
  • src/gateway/static/dashboard/assets/ToolsGuardrailsPage-BGS4wNea.js
  • src/gateway/static/dashboard/assets/UsagePage-CqeKwk9A.js
  • src/gateway/static/dashboard/assets/UsersPage-Bsvcg8ok.js
  • src/gateway/static/dashboard/assets/index-BpNdDlRZ.js
  • src/gateway/static/dashboard/assets/index-BzgMrLHf.js
  • src/gateway/static/dashboard/assets/index-C6_9f8lh.css
  • src/gateway/static/dashboard/index.html
  • web/package.json
  • web/src/App.tsx
  • web/src/components/AppShell.test.tsx
  • web/src/components/AppShell.tsx
  • web/src/pages/DocsPage.test.tsx
  • web/src/pages/DocsPage.tsx
  • web/src/styles/globals.css
  • web/vite.config.ts
💤 Files with no reviewable changes (1)
  • src/gateway/static/dashboard/assets/index-BzgMrLHf.js

Comment on lines 15 to +20
paths:
- 'web/**'
- 'src/gateway/static/dashboard/**'
# The dashboard bundles this guide (web/src/pages/DocsPage.tsx), so a change
# to it makes the committed bundle stale until web is rebuilt; run the check.
- 'docs/dashboard.md'

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Trigger validation when this workflow changes in a pull request.

The workflow file is included for push but omitted from pull_request.paths, so PR-only edits to its staleness logic are not validated before merge.

Proposed fix
   pull_request:
     branches: [ main ]
     paths:
       - 'web/**'
       - 'src/gateway/static/dashboard/**'
+      - '.github/workflows/otari-dashboard.yml'
       - 'docs/dashboard.md'
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
paths:
- 'web/**'
- 'src/gateway/static/dashboard/**'
# The dashboard bundles this guide (web/src/pages/DocsPage.tsx), so a change
# to it makes the committed bundle stale until web is rebuilt; run the check.
- 'docs/dashboard.md'
paths:
- 'web/**'
- 'src/gateway/static/dashboard/**'
- '.github/workflows/otari-dashboard.yml'
# The dashboard bundles this guide (web/src/pages/DocsPage.tsx), so a change
# to it makes the committed bundle stale until web is rebuilt; run the check.
- 'docs/dashboard.md'
🤖 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 @.github/workflows/otari-dashboard.yml around lines 15 - 20, Add the workflow
file itself to the pull_request path filters in the relevant trigger
configuration, matching the existing push coverage so changes to the workflow’s
staleness logic run validation before merge.

Comment thread web/src/styles/globals.css
…eadings

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>
@njbrake
njbrake temporarily deployed to integration-tests July 26, 2026 18:45 — with GitHub Actions Inactive
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>

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 adds an authenticated “User guide” experience to the admin dashboard by bundling docs/dashboard.md into the dashboard build and exposing it at /#/docs, ensuring the running gateway ships documentation that matches the deployed version.

Changes:

  • Add a new docs route and sidebar footer link that renders bundled Markdown using react-markdown + remark-gfm.
  • Add scoped global CSS for Markdown typography and code blocks using existing --otari-* design tokens.
  • Update Vite/Vitest config and CI workflow triggers so edits to docs/dashboard.md are caught as “stale bundle” unless the dashboard build output is regenerated and committed.

Reviewed changes

Copilot reviewed 26 out of 30 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
web/vite.config.ts Allows dev server/Vitest to read the bundled guide from outside web/.
web/src/styles/globals.css Adds scoped .otari-markdown styles and a new --otari-code-ink token for guide code blocks.
web/src/pages/DocsPage.tsx New page that bundles and renders docs/dashboard.md, with link rewriting for sibling docs.
web/src/pages/DocsPage.test.tsx Tests rendering, heading de-duplication, GFM tables, and link rewriting behavior.
web/src/components/AppShell.tsx Adds a “User guide” footer NavLink in the sidebar rail.
web/src/components/AppShell.test.tsx Adds a test asserting the footer link is present.
web/src/App.tsx Registers a lazy-loaded docs route under the authenticated app shell.
web/package.json Adds react-markdown and remark-gfm dependencies.
src/gateway/static/dashboard/index.html Updates hashed asset references from the rebuilt dashboard bundle.
src/gateway/static/dashboard/assets/UsersPage-Cf7XFqqg.js Regenerated dashboard asset output (hash/cross-chunk references updated).
src/gateway/static/dashboard/assets/UsagePage-B-wsXA_7.js Regenerated dashboard asset output (hash/cross-chunk references updated).
src/gateway/static/dashboard/assets/ToolsGuardrailsPage-CqzvXJGv.js Regenerated dashboard asset output (hash/cross-chunk references updated).
src/gateway/static/dashboard/assets/TablePagination-B6n-P8hP.js Regenerated dashboard asset output (hash/cross-chunk references updated).
src/gateway/static/dashboard/assets/SettingsPage-DuaBIB9C.js Regenerated dashboard asset output (hash/cross-chunk references updated).
src/gateway/static/dashboard/assets/OverviewPage-CnyUyPnI.js Regenerated dashboard asset output (hash/cross-chunk references updated).
src/gateway/static/dashboard/assets/ModelScopeControl-C86gAanu.js Regenerated dashboard asset output (hash/cross-chunk references updated).
src/gateway/static/dashboard/assets/KeysPage-D6zoVTgo.js Regenerated dashboard asset output (hash/cross-chunk references updated).
src/gateway/static/dashboard/assets/ConfirmDialog-BED6F4he.js Regenerated dashboard asset output (hash/cross-chunk references updated).
src/gateway/static/dashboard/assets/BudgetsPage-DspuW7X2.js Regenerated dashboard asset output (hash/cross-chunk references updated).
src/gateway/static/dashboard/assets/AliasesPage-9azvXhVZ.js Regenerated dashboard asset output (hash/cross-chunk references updated).
src/gateway/static/dashboard/assets/ActivityPage-C0KlBw31.js Regenerated dashboard asset output (hash/cross-chunk references updated).
AGENTS.md Documents the new guide bundling behavior and rebuild coupling.
.github/workflows/otari-dashboard.yml Ensures CI “stale bundle” checks run when docs/dashboard.md changes.
Files not reviewed (2)
  • src/gateway/static/dashboard/assets/index-6c8CgnYl.js: Generated file
  • web/package-lock.json: Generated file

Comment on lines +39 to +52
const markdownComponents: Components = {
a: ({ href, children, ...props }: AnchorHTMLAttributes<HTMLAnchorElement>) => {
const resolved = resolveDocHref(href);
// In-page anchors stay in the tab; every other link (bundled-out doc pages
// rewritten to GitHub, or already-external URLs) opens in a new tab so the
// operator does not lose the dashboard.
const external = href?.startsWith("#") ? false : isExternal(resolved);
return (
<a href={resolved} {...(external ? { target: "_blank", rel: "noreferrer" } : {})} {...props}>
{children}
</a>
);
},
};
Comment thread web/vite.config.ts Outdated
Comment on lines 52 to 55
// Allow reading the bundled user guide (docs/dashboard.md) from the repo
// root; the default only permits web/ and its workspace.
fs: { allow: [repoRoot] },
proxy: {

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

Reviewed the source diff (bundle excluded). One blocker (dev-server secret exposure), one confirmed rendering bug, and three a11y issues on the new Markdown styles. I reproduced the first two by running the code; details inline.

Also: this conflicts with #435 in web/src/components/AppShell.tsx and AppShell.test.tsx (both rewrite the sidebar footer, both insert tests at the same anchor). Whoever merges second should take this PR's footer <div> structure, re-apply aria-hidden to both svgs inside it (this PR's new book icon never had one, and resolving in favour of this block drops #435's addition on the otari.ai icon), keep both test blocks, run npm ci before rebuilding, and regenerate the bundle from scratch.

Not worth blocking on: the DocsPage lazy chunk is 168 KB for react-markdown + remark-gfm + the inlined guide. It is genuinely lazy, so it only costs visitors of /#/docs.


🤖 Review generated with Claude Code

Comment thread web/vite.config.ts Outdated
server: {
// Allow reading the bundled user guide (docs/dashboard.md) from the repo
// root; the default only permits web/ and its workspace.
fs: { allow: [repoRoot] },

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.

Blocker: this exposes config.yml (and otari.db) over the dev server.

An explicit server.fs.allow replaces Vite's default rather than extending it, and the default fs.deny only covers .env*, *.{crt,pem}, **/.git/**. Widening the root to the whole repo makes every gitignored file readable at /@fs/<repoRoot>/....

Reproduced on this branch with a sentinel config.yml:

pr-434:  curl /@fs/<repo>/config.yml  ->  HTTP 200   master_key: sk-SENTINEL-SECRET-123
main:    curl /@fs/<repo>/config.yml  ->  HTTP 403

Anyone who can reach the port gets the master key and provider API keys: npm run dev -- --host, a devcontainer, or a forwarded port in Codespaces.

Only docs/ needs to be readable. web/ has to stay listed explicitly, since the default is replaced:

fs: {
  allow: [
    fileURLToPath(new URL("./", import.meta.url)),
    fileURLToPath(new URL("../docs", import.meta.url)),
  ],
},

Comment thread web/src/pages/DocsPage.tsx Outdated
Comment on lines +40 to +47
a: ({ href, children, ...props }: AnchorHTMLAttributes<HTMLAnchorElement>) => {
const resolved = resolveDocHref(href);
// In-page anchors stay in the tab; every other link (bundled-out doc pages
// rewritten to GitHub, or already-external URLs) opens in a new tab so the
// operator does not lose the dashboard.
const external = href?.startsWith("#") ? false : isExternal(resolved);
return (
<a href={resolved} {...(external ? { target: "_blank", rel: "noreferrer" } : {})} {...props}>

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.

Every link in the guide ships an invalid node="[object Object]" attribute.

react-markdown 9 passes the hast node to custom components. Destructuring only href and children leaves node in ...props, and it gets forwarded onto the DOM element. Verified by rendering the page:

ANCHORS: 8   WITH_NODE_ATTR: 8
<a href="https://github.com/mozilla-ai/otari/blob/main/docs/configuration.md"
   target="_blank" rel="noreferrer" node="[object Object]">Configuration</a>

Typecheck does not catch it because the parameter is annotated AnchorHTMLAttributes<HTMLAnchorElement> rather than Components["a"], so TS never sees the extra prop. That also stringifies the whole hast subtree once per link.

a: ({ node: _node, href, children, ...props }: ComponentPropsWithoutRef<"a"> & ExtraProps) => {

(ExtraProps is exported from react-markdown.) Worth an assertion in DocsPage.test.tsx that anchors carry no node attribute.

Comment thread web/src/pages/DocsPage.tsx Outdated
// In-page anchors stay in the tab; every other link (bundled-out doc pages
// rewritten to GitHub, or already-external URLs) opens in a new tab so the
// operator does not lose the dashboard.
const external = href?.startsWith("#") ? false : isExternal(resolved);

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.

In-page anchors are kept in-tab here, but HashRouter owns location.hash, so this is the one link shape that breaks.

A ](#the-two-key-model) link renders as <a href="#the-two-key-model">; clicking it rewrites #/docs to #the-two-key-model, react-router parses the pathname as /the-two-key-model, nothing matches, and the path="*" route in App.tsx:77 redirects the operator to Overview. Reproduced against the real route table under Vitest.

Compounding it, there is no rehype-slug, so headings carry no id for such a link to target even under BrowserRouter.

Latent today (the guide has no ](# links), but remark-gfm footnotes generate #user-content-fn-* anchors that hit the same path. Either rewrite #frag to the GitHub source like the sibling doc links, or add rehype-slug and preventDefault() + scrollIntoView on click.

Comment thread web/src/styles/globals.css Outdated
Comment on lines +225 to +229
.otari-markdown table {
display: block;
width: max-content;
max-width: 100%;
overflow-x: auto;

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.

display: block on a <table> strips its table semantics.

Overriding the display removes the table box from the layout tree, and engines that derive a11y exposure from layout (WebKit definitively) drop role=table along with the row/cell/columnheader relationships on descendants. The guide's two-key table (docs/dashboard.md:19-25) then reaches VoiceOver as an undifferentiated run of text with no header-to-cell association, on a table whose third column states that losing OTARI_SECRET_KEY makes every stored provider key undecryptable.

DocsPage.test.tsx:33 cannot catch this: jsdom applies no stylesheet, so getAllByRole("table") resolves the implicit role from the bare <table> tag and passes either way.

Keep the native display and move the scroll to a wrapper via a components.table override in DocsPage.tsx:

table: ({ node: _node, ...props }) => (
  <div className="otari-markdown-scroll" tabIndex={0} role="region" aria-label="Table">
    <table {...props} />
  </div>
),

with .otari-markdown-scroll { overflow-x: auto; max-width: 100%; }.

padding: 0.85rem 1rem;
background-color: var(--otari-ink);
border-radius: 0.6rem;
overflow-x: auto;

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.

Scrollable region with no keyboard access (axe scrollable-region-focusable, WCAG 2.1.1): this pre and the table rule below both set overflow-x: auto but get no tabindex and contain no focusable descendants, so a keyboard-only or switch user tabs straight past them and can never reach the clipped content.

The table is the unconditional case (width: max-content sizes the two-key table to ~1200px, then clips); the pre blocks only overflow on narrow viewports. Add tabIndex={0} plus role="region" and an aria-label on the scroll containers via markdownComponents overrides, alongside the table wrapper fix.

Comment on lines +170 to +172
.otari-markdown a:hover {
color: var(--otari-brand);
}

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.

Hover drops link contrast below AA: --otari-brand-dark #3c6678 is 6.2:1 on the white card, --otari-brand #4e8295 is 4.24:1. .otari-markdown sets font-size: 0.875rem at normal weight, so WCAG 1.4.3 requires 4.5:1. Contrast falls below threshold in exactly the state meant to emphasise the link.

Keep --otari-brand-dark and signal hover another way (text-decoration-thickness, or a --otari-brand-tint background), or darken rather than lighten.

Resolve khaledosman's review findings on PR #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>
@njbrake
njbrake temporarily deployed to integration-tests July 27, 2026 10:00 — with GitHub Actions Inactive
@coderabbitai
coderabbitai Bot requested a review from khaledosman July 27, 2026 10:01

@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: 1

🤖 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/DocsPage-CX3Pja6K.js`:
- Around line 29-236: Regenerate the inlined guide constant Uc used by DocsPage
from the current docs/dashboard.md source so the bundled content matches HEAD,
including the # Admin dashboard heading and all guide updates. Keep the existing
DocsPage rendering and link-handling logic unchanged.
🪄 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: 04add438-86ce-4474-b806-6c80aabf6624

📥 Commits

Reviewing files that changed from the base of the PR and between 46fb308 and c102839.

📒 Files selected for processing (23)
  • src/gateway/static/dashboard/assets/ActivityPage-yp-Y6d4o.js
  • src/gateway/static/dashboard/assets/AliasesPage-BT4dCoEs.js
  • src/gateway/static/dashboard/assets/BudgetsPage-BWlLKmfo.js
  • src/gateway/static/dashboard/assets/ConfirmDialog-CT328Brd.js
  • src/gateway/static/dashboard/assets/DocsPage-CX3Pja6K.js
  • src/gateway/static/dashboard/assets/KeysPage-ApLcycd8.js
  • src/gateway/static/dashboard/assets/ModelScopeControl-D-rIiK14.js
  • src/gateway/static/dashboard/assets/ModelsPage-DSBlJhqc.js
  • src/gateway/static/dashboard/assets/OverviewPage-C67k-EK5.js
  • src/gateway/static/dashboard/assets/ProvidersPage-BIsJAuF5.js
  • src/gateway/static/dashboard/assets/SettingsPage-RiKmM5Jg.js
  • src/gateway/static/dashboard/assets/TablePagination-BEQ2yTEV.js
  • src/gateway/static/dashboard/assets/ToolsGuardrailsPage-BX7O3avK.js
  • src/gateway/static/dashboard/assets/UsagePage-CaSTdNwd.js
  • src/gateway/static/dashboard/assets/UsersPage-ncvRRs6a.js
  • src/gateway/static/dashboard/assets/index-73ln6Ld4.css
  • src/gateway/static/dashboard/assets/index-DYaMwNVC.js
  • src/gateway/static/dashboard/index.html
  • web/src/components/AppShell.tsx
  • web/src/pages/DocsPage.test.tsx
  • web/src/pages/DocsPage.tsx
  • web/src/styles/globals.css
  • web/vite.config.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/gateway/static/dashboard/index.html
  • web/src/pages/DocsPage.test.tsx
  • web/src/styles/globals.css
  • web/src/components/AppShell.tsx

Comment on lines +29 to +236
plaintext key is write-only. Providers configured in \`config.yml\` also appear
here, marked \`config\` and read-only; keys you add in the UI are marked \`stored\`
and can be edited, tested, and deleted.

### 6. Test the connection

On the Providers page, use **Test the connection** for the provider you just
added. Otari makes a live call to confirm the credential works before you route
real traffic through it.

### 7. Send your first request

The Providers page includes a "Send your first request" snippet you can copy.
Point any OpenAI-compatible client at the gateway using an Otari API key or the
master key, and select a model in \`provider:model\` form (for example
\`openai:gpt-4o\`). See the [Quickstart](quickstart.md) for a full end-to-end
example.

### 8. (Optional) Set up keys, users, and budgets

For multi-user or multi-app deployments, use the **Access** section of the
sidebar to hand out scoped API keys, define users, and attach budgets so spend
is enforced before each call. These are optional: a single-operator setup can
run on the master key alone.

## Page-by-page reference

The sidebar groups pages by what they do. This section is filled in as pages
land; the groups below match the current dashboard.

### Overview

The landing page. An at-a-glance view of spend, traffic, and health across the
gateway.

### Observability

- **Activity**: the per-request log of what the gateway served, with filters.
Use it to inspect individual requests, their models, and their outcomes.
- **Usage**: aggregate usage and analytics, showing spend and volume over time,
broken down by model and by user.

### Catalog

- **Providers**: add, edit, test, and delete provider credentials at runtime
(standalone only). Stored keys are encrypted with \`OTARI_SECRET_KEY\`; config
providers appear read-only. See the first-run walkthrough above.
- **Models**: browse the model catalogue and set per-model pricing, with specs
and modality metadata where available (from models.dev).
- **Aliases**: friendly names that resolve to a real provider model. Callers
use the alias; the underlying model stays private to the gateway.

### Access

- **Users**: the principals that keys and budgets attach to, including the
default model access for a user's keys.
- **API keys**: issue and revoke gateway API keys, optionally restricting the
models a key may call and setting an expiry (leave blank for a key that never
expires).
- **Budgets**: spending limits callers are held to, with per-period resets.

For how users, keys, and budgets fit together and the management endpoints behind these pages, see [Access control](access-control.md).

### System

- **Tools & Guardrails**: configure the backends for built-in tools (for
example the \`otari_web_search\` search backend) and request-level guardrails.
- **Settings**: search and toggle runtime settings, review and apply default
pricing updates, and rotate the generated master key. Rotating the master key
issues a fresh \`otari-mk-…\` value and keeps your current session signed in.

## Security notes

- **The master key is an admin credential.** Anyone who has it can read and
change gateway configuration through the management API. Rotate it if you
suspect it leaked.
- **Use HTTPS for anything but local access.** The \`http://localhost:8000/\`
examples here assume you are on the same machine (loopback). The master key
authorizes every management request and must never travel over cleartext HTTP,
so put the gateway behind HTTPS or a trusted reverse proxy before signing in
from another host.
- **Session storage, not local storage.** The dashboard keeps the master key in
the browser tab's session storage, so it does not persist across tabs or
survive closing the tab, and signing out clears it along with any cached admin
data.
- **Provider keys are write-only over the API.** Once stored, the plaintext is
never returned; the UI shows only the last four characters. Losing
\`OTARI_SECRET_KEY\` makes stored keys undecryptable, so back it up separately
from the database and rotate it by prepending a new key (see
[Configuration](configuration.md#runtime-provider-management)).

## See also

- [Configuration](configuration.md): every environment variable and config
field, including \`OTARI_MASTER_KEY\` and \`OTARI_SECRET_KEY\`.
- [Quickstart](quickstart.md): get the gateway running and make your first
request.
- [Modes](modes.md): standalone versus hybrid, and why the dashboard is
standalone-only.
`,er="https://github.com/mozilla-ai/otari/blob/main/docs/",qc="dashboard.md";function Vc(e){return!e||/^[a-z][a-z0-9+.-]*:/i.test(e)||e.startsWith("//")?e:e.startsWith("#")?er+qc+e:er+e.replace(/^\.\//,"")}function $c(e){return!!e&&(/^https?:\/\//i.test(e)||e.startsWith("//"))}const Wc=Uc.replace(/^#{1,6}[ \t]+.*\r?\n+/,""),Yc={a:({node:e,href:t,children:n,...r})=>{const i=Vc(t),o=$c(i);return re.jsx("a",{href:i,...o?{target:"_blank",rel:"noreferrer"}:{},...r,children:n})},table:({node:e,...t})=>re.jsx("div",{className:"otari-markdown-scroll",tabIndex:0,role:"region","aria-label":"Table",children:re.jsx("table",{...t})}),pre:({node:e,...t})=>re.jsx("pre",{tabIndex:0,role:"region","aria-label":"Code",...t})};function Jc(){return re.jsxs("div",{className:"flex flex-col gap-6",children:[re.jsx(gi,{title:"User guide",description:"The operator guide for this dashboard, bundled with and version-matched to the running gateway."}),re.jsx(ct,{children:re.jsx(ct.Content,{className:"p-5 sm:p-6",children:re.jsx("div",{className:"otari-markdown",children:re.jsx(vu,{remarkPlugins:[Hc],components:Yc,children:Wc})})})})]})}export{Jc as DocsPage};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Compare the guide text embedded in the committed bundle against docs/dashboard.md.
fd -t f 'DocsPage-.*\.js' src/gateway/static/dashboard/assets
fd -t f '^dashboard\.md$' docs

python - <<'PY'
import re, pathlib, sys

bundles = sorted(pathlib.Path('src/gateway/static/dashboard/assets').glob('DocsPage-*.js'))
src = pathlib.Path('docs/dashboard.md')
if not bundles or not src.exists():
    print('bundle or docs/dashboard.md not found'); sys.exit(0)

bundle_text = bundles[0].read_text(encoding='utf-8')
# The guide is inlined as a template literal starting at the top-level heading.
m = re.search(r'`# Admin dashboard\n(.*?)`,', bundle_text, re.S)
if not m:
    print('could not locate inlined guide in', bundles[0]); sys.exit(0)

embedded = ('# Admin dashboard\n' + m.group(1))
embedded = embedded.replace('\\`', '`').replace('\\$', '$').replace('\\\\', '\\')
on_disk = src.read_text(encoding='utf-8')
print('bundle:', bundles[0].name)
print('embedded chars:', len(embedded), '| docs/dashboard.md chars:', len(on_disk))
print('MATCH' if embedded.strip() == on_disk.strip() else 'MISMATCH — rebuild the dashboard bundle')
PY

Repository: mozilla-ai/otari

Length of output: 351


Keep the inlined dashboard guide synced with docs/dashboard.md.

The bundle contains # Admin dashboard, but it does not match the Markdown source at HEAD, so any guide edits won’t be version-matched until the dashboard bundle is rebuilt.

🤖 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 `@src/gateway/static/dashboard/assets/DocsPage-CX3Pja6K.js` around lines 29 -
236, Regenerate the inlined guide constant Uc used by DocsPage from the current
docs/dashboard.md source so the bundled content matches HEAD, including the #
Admin dashboard heading and all guide updates. Keep the existing DocsPage
rendering and link-handling logic unchanged.

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 #435's a11y pass, so resolving the (latent) footer merge conflict
in favour of this block drops none of #435's aria-hidden coverage.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@njbrake
njbrake temporarily deployed to integration-tests July 27, 2026 17:11 — with GitHub Actions Inactive
…uide

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>
@njbrake
njbrake temporarily deployed to integration-tests July 27, 2026 17:36 — with GitHub Actions Inactive

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

Re-reviewed at 30340fa. All six findings from my last pass are fixed, and I re-ran each check rather than reading the diff:

Finding Verification
server.fs.allow served repo secrets Live dev server on this branch: config.yml403 (was 200, master key in the body), pyproject.toml → 403, docs/dashboard.md?raw → 200, app root → 200.
node="[object Object]" on every anchor Rendered page: 0 elements carry a node attribute (was 8/8 anchors).
display: block stripped table semantics <table> keeps its native display and renders inside .otari-markdown-scroll.
Scroll regions unreachable by keyboard Table wrapper and all 3 <pre> now tabindex="0". No global outline reset in the built CSS (the one outline:none is scoped to a toast button), so the UA focus ring shows.
#frag links broke HashRouter Rewritten to …/docs/dashboard.md#frag; all 8 links now resolve to absolute GitHub URLs with target="_blank".
Hover contrast 4.24:1 text-decoration-thickness instead, keeping --otari-brand-dark at 6.2:1.

tsc -b --noEmit clean, 291/291 tests pass, and a fresh npm run build leaves src/gateway/static/dashboard/ byte-identical, so the committed bundle is current.

The aria-hidden added on the book icon also clears the merge hazard I flagged against #435: both svgs now carry it on either side, so resolving in favour of this PR's footer block no longer silently reverts that a11y fix.

Two nits inline, neither blocking.


🤖 Review generated with Claude Code

Comment on lines +70 to +79
// in) is circular here. Drop it; it stays in docs/dashboard.md for readers
// on GitHub and the /welcome tutorial, and its post-sign-in substance is
// covered by the page-by-page reference below.
const guideBody = dropSection(
dashboardGuide.replace(/^#{1,6}[ \t]+.*\r?\n+/, ""),
"First-run walkthrough",
);

// react-markdown passes each node's hast `node` to custom components; it must be
// destructured out of the DOM spread or it renders as node="[object Object]" on

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.

Nit: role="region" with an accessible name is a landmark, so the guide now injects four of them into the landmark list, three identically named:

landmark regions: 4
pres: role=region,region,region   labels: Code,Code,Code

Navigating by landmark gives "Code, Code, Code, Table" with nothing to tell them apart.

scrollable-region-focusable only requires tabindex; the role is what makes them landmarks. role="group" keeps the name announced when focus lands without polluting landmark navigation:

-  table: ({ node: _node, ...props }: MdProps<"table">) => (
-    <div className="otari-markdown-scroll" tabIndex={0} role="region" aria-label="Table">
+  table: ({ node: _node, ...props }: MdProps<"table">) => (
+    <div className="otari-markdown-scroll" tabIndex={0} role="group" aria-label="Table">

Same for the pre override.

// The two-key model is a Markdown table; rendering it as a real <table>
// proves remark-gfm is wired up (plain Markdown would leave it as text).
const tables = screen.getAllByRole("table");
expect(tables.length).toBeGreaterThan(0);

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.

Nit: the new coverage for the table region and the node leak is good, but two behaviours added in the fix commit are untested:

  • the #fragdashboard.md#frag rewrite, which is the case that originally broke under HashRouter
  • pre focusability (the table wrapper is asserted, the code blocks are not)

Both extend this test naturally, e.g. asserting an #anchor href resolves to https://github.com/mozilla-ai/otari/blob/main/docs/dashboard.md#… and that every <pre> carries tabindex="0".

@njbrake
njbrake merged commit 8d1c1e9 into main Jul 27, 2026
7 checks passed
@njbrake
njbrake deleted the fix/dashboard-bundle-docs branch July 27, 2026 17:47
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: bundle docs with dashboard server

3 participants