Skip to content

feat(dashboard): code-split route bundles - #373

Merged
njbrake merged 6 commits into
mainfrom
fix/dashboard-code-splitting
Jul 22, 2026
Merged

feat(dashboard): code-split route bundles#373
njbrake merged 6 commits into
mainfrom
fix/dashboard-code-splitting

Conversation

@njbrake

@njbrake njbrake commented Jul 22, 2026

Copy link
Copy Markdown
Member

Description

Loads dashboard routes on demand so the first visit does not download every page. Splits React, TanStack Query, and HeroUI into stable vendor chunks for better caching.

PR Type

  • Refactor

Relevant issues

Fixes #311

Checklist

  • I understand the code I am submitting.
  • I have added or updated tests that cover my change (tests/unit, tests/integration).
  • I ran the Definition of Done checks locally (make lint, make typecheck, make test).
  • Documentation was updated where necessary.
  • If the API contract changed, I regenerated the OpenAPI spec (uv run python scripts/generate_openapi.py).

AI Usage

  • This is fully AI-generated.

AI Model/Tool used: Codex (GPT-5)

Any additional AI details you would like to share: Implemented, tested, and reviewed with Codex.

  • I am an AI Agent filling out this form

Summary

  • Dashboard pages now load on demand instead of shipping every route in the initial bundle.
  • Shared React, TanStack Query, and HeroUI dependencies are separated into cacheable vendor bundles.
  • Added loading-state coverage for lazy routes and regenerated dashboard assets.
  • Improved budget assignment retries and numeric validation.
  • Hardened model capability filtering, clipboard feedback, date formatting, and secret-key inputs.
  • Expanded tests for lazy loading and the updated page behaviors.

Benefits

  • Faster initial dashboard load.
  • Better browser caching across route visits.
  • More reliable error handling and validation.

Load dashboard pages only when their routes are opened and split core vendor dependencies into cacheable chunks.

Fixes #311
@njbrake
njbrake temporarily deployed to integration-tests July 22, 2026 16:42 — with GitHub Actions Inactive
@coderabbitai

coderabbitai Bot commented Jul 22, 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: 14 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

Run ID: ae2e9e00-bbd6-42ff-871e-222727e4dc7d

📥 Commits

Reviewing files that changed from the base of the PR and between 1b6dc17 and 82ebb23.

📒 Files selected for processing (23)
  • src/gateway/static/dashboard/assets/ActivityPage-Dv1t5m0G.js
  • src/gateway/static/dashboard/assets/AliasesPage-BF_aAhmS.js
  • src/gateway/static/dashboard/assets/BudgetsPage-BOXsUwOg.js
  • src/gateway/static/dashboard/assets/KeysPage-DJK7XqLD.js
  • src/gateway/static/dashboard/assets/ModelComboBox-DxZFCNam.js
  • src/gateway/static/dashboard/assets/ModelScopeControl-BQhlJLdc.js
  • src/gateway/static/dashboard/assets/ModelsPage-CCg9xDDB.js
  • src/gateway/static/dashboard/assets/OverviewPage-DbZcfLzl.js
  • src/gateway/static/dashboard/assets/ProvidersPage-DdqRDim1.js
  • src/gateway/static/dashboard/assets/SettingsPage-CbOQpLvM.js
  • src/gateway/static/dashboard/assets/ToolsGuardrailsPage-Dq8uwOq0.js
  • src/gateway/static/dashboard/assets/UsagePage-Cf1iUtft.js
  • src/gateway/static/dashboard/assets/UsersPage-B0KqVzbo.js
  • src/gateway/static/dashboard/assets/index-BcI4fr4n.js
  • src/gateway/static/dashboard/index.html
  • web/src/App.test.tsx
  • web/src/pages/BudgetsPage.test.tsx
  • web/src/pages/BudgetsPage.tsx
  • web/src/pages/OverviewPage.test.tsx
  • web/src/pages/OverviewPage.tsx
  • web/src/pages/ProvidersPage.test.tsx
  • web/src/pages/ProvidersPage.tsx
  • web/vite.config.ts

Walkthrough

The dashboard now lazy-loads routed pages, splits major vendor dependencies into dedicated bundles, adds generated page and shared-component assets, and includes tests and fixes for clipboard handling, budget assignment retries, model filtering, date keys, and secret inputs.

Changes

Dashboard code splitting

Layer / File(s) Summary
Shared dashboard controls
src/gateway/static/dashboard/assets/Field-*.js, ModelComboBox-*.js, ModelScopeControl-*.js, Table-*.js
Reusable field, model-selection, model-access, and resizable table components are added.
Administration pages and workflows
src/gateway/static/dashboard/assets/{Aliases,Budgets,Keys,Providers,Settings,ToolsGuardrails,Users}Page-*.js, web/src/pages/{Budgets,Providers}Page.*
Dashboard management pages and supporting validation, secret-field, and budget-assignment retry behavior are implemented and tested.
Analytics and model pages
src/gateway/static/dashboard/assets/{Activity,Models,Overview,Usage}Page-*.js, web/src/pages/{Activity,Models,Overview}Page.*
Analytics and model views are added, with clipboard handling, safe modality filtering, and normalized local date keys covered by tests.
Lazy route wiring and validation
web/src/App.tsx, web/src/App.test.tsx
All dashboard routes use lazy page imports with a Suspense loading fallback, verified by an application test.
Vendor chunks and generated assets
web/vite.config.ts, src/gateway/static/dashboard/assets/*, src/gateway/static/dashboard/index.html
Rollup creates named HeroUI, React, and TanStack Query chunks, and the committed entrypoint and preload references are refreshed.

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

Possibly related PRs

  • mozilla-ai/otari#297: Adds dashboard page modules and routing that correspond to several generated administration-page assets.
  • mozilla-ai/otari#318: Adds API-key routing and model-access dashboard modules represented in the split bundles.
  • mozilla-ai/otari#350: Adds the tools route and page functionality wired into the lazy-loaded dashboard.

Suggested reviewers: khaledosman

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Title check ⚠️ Warning The title is relevant, but it uses feat(dashboard): instead of the required feat: Conventional Commit prefix. Change it to feat: code-split dashboard route bundles or another feat: title that follows the repo convention.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.24% 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
Description check ✅ Passed The description matches the template sections and includes purpose, type, issues, checklist, and AI usage.
Linked Issues check ✅ Passed The PR implements route-level lazy loading and manual vendor chunking, aligning with #311's core requirements.
Out of Scope Changes check ✅ Passed The changes are focused on dashboard code-splitting and regenerated route assets, with no obvious unrelated additions.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/dashboard-code-splitting
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch fix/dashboard-code-splitting

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 22, 2026 16:43

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

🧹 Nitpick comments (2)
web/src/App.test.tsx (1)

21-21: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Mock apiFetch instead of the global fetch implementation.

Mocking globalThis.fetch can accidentally intercept unrelated requests and couples this test to internals below the application API boundary. Replace it with a mock of apiFetch and keep the real provider setup. As per coding guidelines, “mock only the apiFetch network boundary, not hooks.”

🤖 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/App.test.tsx` at line 21, Replace the globalThis.fetch spy in the App
test setup with a mock of the apiFetch network-boundary function, preserving the
existing real provider setup and response behavior. Do not mock hooks or other
unrelated request implementations.

Source: Coding guidelines

web/vite.config.ts (1)

59-62: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Add react-dom/client to the React vendor chunk. main.tsx imports createRoot from that subpath, and the current entry bundle still carries react-dom-client.production.js, so app changes keep invalidating the React DOM runtime.

🤖 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/vite.config.ts` around lines 59 - 62, Add "react-dom/client" to the React
vendor entry in the manualChunks configuration alongside "react", "react-dom",
and "react-router-dom", ensuring createRoot's subpath is included in the shared
React chunk.
🤖 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/ActivityPage-C_wXIKoV.js`:
- Line 1: Update the clipboard handler in the `we` component so the `Copied`
state is set only after `navigator.clipboard.writeText(t)` resolves
successfully. Handle unavailable or rejected clipboard writes without showing
success, while preserving the existing timeout that resets the state.

In `@src/gateway/static/dashboard/assets/BudgetsPage-Dzegp1K_.js`:
- Line 1: Update the budget-value validator ie and the custom reset-period
parser inside le to reject non-finite numbers, using Number.isFinite alongside
the existing validity checks. Ensure Infinity, -Infinity, and overflowed inputs
such as 1e309 are invalid and cannot reach either mutation path.
- Line 1: The budget creation handler in je, specifically the Promise.allSettled
assignment flow, must not silently ignore rejected user-assignment mutations or
close the dialog after partial failure. Prefer a transactional/bulk assignment
operation; otherwise collect failed user IDs, surface the failure through the
existing error UI, and keep the dialog/state retryable while preserving
successful assignments.

In `@src/gateway/static/dashboard/assets/ModelsPage-eIvU0Tj-.js`:
- Line 1: Update the vision, audio, and PDF predicates in the Ae
capability-filter definitions to guard optional input_modalities before calling
includes. Treat missing modality arrays as non-matches, preserving current
behavior when the array is present.

In `@src/gateway/static/dashboard/assets/OverviewPage-BRQfRYKi.js`:
- Line 1: Update the B date helper used by ht so it returns a valid ISO calendar
date, using a one-based, zero-padded month and zero-padded day while preserving
the existing local-date behavior. Ensure the resulting start_date passed to the
usage query is consistently formatted as YYYY-MM-DD.

In `@src/gateway/static/dashboard/assets/ProvidersPage-Y91J0S4G.js`:
- Line 1: Update the API-key field rendered by the D component to use the native
password input type instead of type="text" with WebkitTextSecurity. Preserve the
existing autocomplete, privacy, value, and change-handling behavior while
ensuring masking works across browsers.

---

Nitpick comments:
In `@web/src/App.test.tsx`:
- Line 21: Replace the globalThis.fetch spy in the App test setup with a mock of
the apiFetch network-boundary function, preserving the existing real provider
setup and response behavior. Do not mock hooks or other unrelated request
implementations.

In `@web/vite.config.ts`:
- Around line 59-62: Add "react-dom/client" to the React vendor entry in the
manualChunks configuration alongside "react", "react-dom", and
"react-router-dom", ensuring createRoot's subpath is included in the shared
React chunk.
🪄 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

Run ID: 4629f3b2-be11-407b-9063-327db3dcd0a2

📥 Commits

Reviewing files that changed from the base of the PR and between 3711f4c and 2783fab.

📒 Files selected for processing (24)
  • src/gateway/static/dashboard/assets/ActivityPage-C_wXIKoV.js
  • src/gateway/static/dashboard/assets/AliasesPage-BbofJEZD.js
  • src/gateway/static/dashboard/assets/BudgetsPage-Dzegp1K_.js
  • src/gateway/static/dashboard/assets/Field-DqDcnV5y.js
  • src/gateway/static/dashboard/assets/KeysPage-CuqHixxY.js
  • src/gateway/static/dashboard/assets/ModelComboBox-yD9VG5PR.js
  • src/gateway/static/dashboard/assets/ModelScopeControl-G6MLIL8B.js
  • src/gateway/static/dashboard/assets/ModelsPage-eIvU0Tj-.js
  • src/gateway/static/dashboard/assets/OverviewPage-BRQfRYKi.js
  • src/gateway/static/dashboard/assets/ProvidersPage-Y91J0S4G.js
  • src/gateway/static/dashboard/assets/SettingsPage-XDTK-jZi.js
  • src/gateway/static/dashboard/assets/Table-CugMZAuW.js
  • src/gateway/static/dashboard/assets/ToolsGuardrailsPage-YV0VY7KV.js
  • src/gateway/static/dashboard/assets/UsagePage-Dxv_hBLj.js
  • src/gateway/static/dashboard/assets/UsersPage-C4-VJxQJ.js
  • src/gateway/static/dashboard/assets/heroui-B1YjjANo.js
  • src/gateway/static/dashboard/assets/index-DISDvNSb.js
  • src/gateway/static/dashboard/assets/index-DnD03gwo.js
  • src/gateway/static/dashboard/assets/react-DY5rsDwN.js
  • src/gateway/static/dashboard/assets/tanstack-query-BPQ56vNC.js
  • src/gateway/static/dashboard/index.html
  • web/src/App.test.tsx
  • web/src/App.tsx
  • web/vite.config.ts

@@ -0,0 +1 @@
import{j as e}from"./tanstack-query-BPQ56vNC.js";import{u as re,r}from"./react-DY5rsDwN.js";import{u as le,a as ne,b as ie,c as oe,P as ce,E as de,F as H,d as G}from"./index-DISDvNSb.js";import{T as ue,a as me,b as c,L as xe,c as he,d as pe,e as d}from"./Table-CugMZAuW.js";import{B as g,S as ge}from"./heroui-B1YjjANo.js";const je=new Intl.NumberFormat(void 0,{style:"currency",currency:"USD",maximumFractionDigits:4});function Y(t){return t===null?"—":je.format(t)}function j(t){return t===null?"—":t.toLocaleString()}function Z(t){return t===null?"—":t<1e3?`${t} ms`:`${(t/1e3).toFixed(t<1e4?2:1)} s`}function ve(t){const a=new Date(t);return Number.isNaN(a.getTime())?t:a.toLocaleString()}function fe(t){const a=new Date(t).getTime();if(Number.isNaN(a))return t;const i=Math.max(0,Math.round((Date.now()-a)/1e3));if(i<60)return`${i}s ago`;const n=Math.round(i/60);if(n<60)return`${n}m ago`;const u=Math.round(n/60);return u<24?`${u}h ago`:`${Math.round(u/24)}d ago`}const be=3600,_=86400,Se=[{label:"Last hour",seconds:be},{label:"24h",seconds:_},{label:"7d",seconds:7*_},{label:"All",seconds:null}],Ne=[{label:"All",value:""},{label:"Success",value:"success"},{label:"Error",value:"error"}],_e=["/v1/chat/completions","/v1/messages","/v1/responses","/v1/embeddings","/v1/moderations","/v1/audio/transcriptions","/v1/audio/speech","/v1/images/generations","/v1/rerank","/v1/batches","/v1/batches/results"],S=50;function E(t){return new Date(Date.now()-t*1e3).toISOString()}function ke({status:t}){const a=t==="error"?"border-red-200 bg-red-50 text-red-700":"border-[var(--otari-line)] bg-[var(--otari-brand-tint)] text-[var(--otari-brand-dark)]";return e.jsx("span",{className:`inline-flex items-center rounded-full border px-2 py-0.5 text-xs font-medium ${a}`,children:t})}function we({text:t}){const[a,i]=r.useState(!1);return e.jsx(g,{size:"sm",variant:"ghost",onPress:()=>{var n;(n=navigator.clipboard)==null||n.writeText(t),i(!0),window.setTimeout(()=>i(!1),1500)},children:a?"Copied":"Copy"})}function l({label:t,children:a}){return e.jsxs("div",{className:"flex flex-col gap-0.5",children:[e.jsx("span",{className:"text-[11px] font-medium uppercase tracking-wide text-[var(--otari-muted)]",children:t}),e.jsx("span",{className:"text-sm text-[var(--otari-ink)] break-all",children:a})]})}function Te({entry:t}){return e.jsxs("div",{className:"flex flex-col gap-4 px-4 py-4",children:[t.error_message?e.jsxs("div",{className:"flex flex-col gap-1.5",children:[e.jsxs("div",{className:"flex items-center justify-between",children:[e.jsx("span",{className:"text-[11px] font-medium uppercase tracking-wide text-[var(--otari-muted)]",children:"Error (provider-reported)"}),e.jsx(we,{text:t.error_message})]}),e.jsx("pre",{className:"max-h-48 overflow-auto rounded-lg border border-red-200 bg-red-50 p-3 text-xs whitespace-pre-wrap break-all text-red-700",children:t.error_message})]}):null,e.jsxs("div",{className:"grid gap-4 sm:grid-cols-2 lg:grid-cols-4",children:[e.jsx(l,{label:"Provider",children:t.provider??"—"}),e.jsx(l,{label:"Endpoint",children:t.endpoint}),e.jsx(l,{label:"User",children:t.user_id??"—"}),e.jsx(l,{label:"API key",children:t.api_key_id??"—"}),e.jsx(l,{label:"Prompt tokens",children:j(t.prompt_tokens)}),e.jsx(l,{label:"Completion tokens",children:j(t.completion_tokens)}),e.jsx(l,{label:"Total tokens",children:j(t.total_tokens)}),e.jsx(l,{label:"Cost",children:Y(t.cost)}),e.jsx(l,{label:"Cache read tokens",children:j(t.cache_read_tokens)}),e.jsx(l,{label:"Cache write tokens",children:j(t.cache_write_tokens)}),e.jsx(l,{label:"Total time",children:Z(t.latency_ms)}),e.jsx(l,{label:"Request ID",children:t.id})]})]})}const C=7;function $e(){var q,z,B;const t=le(),[a]=re(),i=a.get("start_date")??void 0,[n,u]=r.useState(i?null:_),[N,k]=r.useState(()=>i??E(_)),[m,D]=r.useState(()=>a.get("status")??""),[o,A]=r.useState(()=>a.get("model")??""),[x,$]=r.useState(""),[h,M]=r.useState(()=>a.get("user_id")??""),[v,w]=r.useState(0),[J,K]=r.useState(null),T=r.useMemo(()=>({start_date:N,status:m||void 0,model:o.trim()||void 0,endpoint:x||void 0,user_id:h||void 0}),[N,m,o,x,h]);r.useEffect(()=>{w(0)},[T]);const p=ne(T,v,S),f=ie(T),Q=r.useMemo(()=>({start_date:N,status:m||void 0,endpoint:x||void 0,user_id:h||void 0}),[N,m,x,h]),P=((z=(q=oe(Q,"day").data)==null?void 0:q.by_model)==null?void 0:z.filter(s=>!s.is_other&&s.key!==null).map(s=>s.key))??[],b=p.data??[],F=((B=f.data)==null?void 0:B.total)??0,U=!!(m||o.trim()||x||h||n!==null),V=s=>{u(s),k(s===null?void 0:E(s))},W=()=>{u(null),k(void 0),D(""),A(""),$(""),M("")},X=()=>{n!==null&&k(E(n)),p.refetch(),f.refetch()},L=b.length>0,I=L?v*S+1:0,O=v*S+b.length,R=f.isSuccess&&!f.isPlaceholderData,ee=R?F===0?"0 of 0":`${I}–${O} of ${F.toLocaleString()}`:L?`${I}–${O}`:"0",se=R?(v+1)*S<F:b.length===S;return e.jsxs("div",{className:"flex flex-col gap-6",children:[e.jsx(ce,{title:"Activity",description:"A per-request log of what the gateway served: tokens, cost, latency, and failures. No request or response content is stored.",action:e.jsx(g,{variant:"outline",onPress:X,isDisabled:p.isFetching,children:"Refresh"})}),e.jsx(de,{error:p.error??f.error}),e.jsxs("div",{className:"flex flex-wrap items-end gap-3",children:[e.jsx("div",{className:"flex flex-wrap gap-2",children:Se.map(s=>e.jsx(g,{size:"sm",variant:n===s.seconds?"primary":"outline",onPress:()=>V(s.seconds),children:s.label},s.label))}),e.jsxs("div",{className:"flex flex-wrap items-end gap-3",children:[e.jsx(H,{id:"filter-status",label:"Status",value:m,onChange:D,children:Ne.map(s=>e.jsx("option",{value:s.value,children:s.label},s.value))}),e.jsxs(H,{id:"filter-endpoint",label:"Endpoint",value:x,onChange:$,children:[e.jsx("option",{value:"",children:"All endpoints"}),_e.map(s=>e.jsx("option",{value:s,children:s},s))]}),e.jsx(G,{label:"User",value:h,onChange:M,placeholder:"All users",options:(t.data??[]).map(s=>({value:s.user_id,label:s.alias?`${s.alias} (${s.user_id})`:s.user_id}))}),e.jsx(G,{label:"Model",value:o,onChange:A,allowsCustom:!0,placeholder:"Any model",options:(o&&!P.includes(o)?[o,...P]:P).map(s=>({value:s,label:s}))}),U?e.jsx(g,{size:"sm",variant:"ghost",onPress:W,children:"Clear filters"}):null]})]}),e.jsxs(ue,{children:[e.jsx(me,{children:e.jsxs("tr",{children:[e.jsx(c,{children:"Time"}),e.jsx(c,{children:"User"}),e.jsx(c,{children:"Model"}),e.jsx(c,{className:"text-right",children:"Tokens"}),e.jsx(c,{className:"text-right",children:"Cost"}),e.jsx(c,{className:"text-right",children:"Total time"}),e.jsx(c,{children:"Status"})]})}),e.jsx("tbody",{children:p.isLoading?e.jsx(xe,{colSpan:C}):b.length===0?e.jsx(he,{colSpan:C,children:U?"No requests match these filters.":"No requests recorded yet."}):b.map(s=>{const te=s.status==="error",y=J===s.id;return e.jsxs(r.Fragment,{children:[e.jsxs(pe,{selected:y,className:te?"bg-red-50":"",onClick:()=>K(ae=>ae===s.id?null:s.id),children:[e.jsx(d,{className:"text-[var(--otari-muted)]",children:e.jsx("span",{title:ve(s.timestamp),children:fe(s.timestamp)})}),e.jsx(d,{className:"text-[var(--otari-ink)]",children:s.user_id??"—"}),e.jsx(d,{className:"text-[var(--otari-ink)]",children:s.model}),e.jsx(d,{className:"text-right tabular-nums",children:j(s.total_tokens)}),e.jsx(d,{className:"text-right tabular-nums",children:Y(s.cost)}),e.jsx(d,{className:"text-right tabular-nums",children:Z(s.latency_ms)}),e.jsx(d,{children:e.jsx(ke,{status:s.status})})]}),y?e.jsx("tr",{className:"border-b border-[var(--otari-line)] bg-[var(--otari-bg)]",children:e.jsx("td",{colSpan:C,children:e.jsx(Te,{entry:s})})}):null]},s.id)})})]}),e.jsxs("div",{className:"flex items-center justify-between gap-3",children:[e.jsxs("span",{className:"inline-flex items-center gap-2 text-sm text-[var(--otari-muted)]",children:[ee,p.isFetching?e.jsx(ge,{size:"sm"}):null]}),e.jsxs("span",{className:"inline-flex gap-1.5",children:[e.jsx(g,{size:"sm",variant:"outline",isDisabled:v===0,onPress:()=>w(s=>Math.max(0,s-1)),children:"Previous"}),e.jsx(g,{size:"sm",variant:"outline",isDisabled:!se,onPress:()=>w(s=>s+1),children:"Next"})]})]})]})}export{$e as ActivityPage};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Handle clipboard failures before showing “Copied”.

Line 1 sets the success state immediately, even when navigator.clipboard.writeText() is unavailable or rejects. Users can be told the error was copied when it was not.

🤖 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/ActivityPage-C_wXIKoV.js` at line 1,
Update the clipboard handler in the `we` component so the `Copied` state is set
only after `navigator.clipboard.writeText(t)` resolves successfully. Handle
unavailable or rejected clipboard writes without showing success, while
preserving the existing timeout that resets the state.

@@ -0,0 +1 @@
import{j as e}from"./tanstack-query-BPQ56vNC.js";import{r as u}from"./react-DY5rsDwN.js";import{i as z,u as M,j as $,k as O,l as F,m as H,P as V,E as D,I as W,n as q}from"./index-DISDvNSb.js";import{F as B}from"./Field-DqDcnV5y.js";import{T as Y,a as G,b as N,L as K,c as Q,d as X,e as y}from"./Table-CugMZAuW.js";import{C,I as J,a as Z,b as ee,B as m,c as w,S as se}from"./heroui-B1YjjANo.js";const te=50;function re({value:s,onChange:r,users:a,label:i,description:l}){const[x,d]=u.useState(""),o=u.useMemo(()=>a.filter(t=>!t.user_id.startsWith("apikey-")).map(t=>({id:t.user_id,label:t.alias?`${t.user_id} (${t.alias})`:t.user_id})),[a]),f=u.useMemo(()=>{const t=x.trim().toLowerCase();return o.filter(c=>!s.includes(c.id)).filter(c=>!t||c.id.toLowerCase().includes(t)||c.label.toLowerCase().includes(t)).slice(0,te)},[o,s,x]),h=t=>{s.includes(t)||r([...s,t]),d("")},j=t=>r(s.filter(c=>c!==t));return e.jsxs("div",{className:"flex flex-col gap-2",children:[e.jsxs("div",{children:[e.jsx("span",{className:"text-sm font-medium text-[var(--otari-ink)]",children:i}),l?e.jsx("p",{className:"text-xs text-[var(--otari-muted)]",children:l}):null]}),s.length>0?e.jsx("div",{className:"flex flex-wrap gap-1.5",children:s.map(t=>e.jsxs("span",{className:"inline-flex items-center gap-1 rounded-full bg-[var(--otari-brand-tint)] px-2.5 py-1 font-mono text-xs text-[var(--otari-brand-dark)]",children:[t,e.jsx("button",{type:"button","aria-label":`Remove ${t}`,onClick:()=>j(t),className:"text-[var(--otari-brand-dark)] hover:text-red-700",children:"×"})]},t))}):null,o.length===0?e.jsx("span",{className:"text-xs text-[var(--otari-muted)]",children:"No users yet. Create users first, then assign them here or from the Users page."}):e.jsxs(C.Root,{allowsEmptyCollection:!0,menuTrigger:"input",inputValue:x,onInputChange:d,selectedKey:null,onSelectionChange:t=>{t!=null&&h(String(t))},className:"flex flex-col gap-1",children:[e.jsxs(C.InputGroup,{children:[e.jsx(J,{"aria-label":"Add a user",placeholder:"Search users…",autoComplete:"off"}),e.jsx(C.Trigger,{})]}),e.jsx(C.Popover,{children:e.jsx(Z,{items:f,className:"max-h-72 overflow-auto",children:t=>e.jsx(ee,{id:t.id,textValue:t.label,children:t.label})})})]})]})}const ae=new Intl.NumberFormat(void 0,{style:"currency",currency:"USD",maximumFractionDigits:2});function _(s){return ae.format(s)}const p=86400,L=3600,k=[{label:"No reset",seconds:null},{label:"Daily",seconds:p},{label:"Weekly",seconds:7*p},{label:"Monthly",seconds:30*p}];function ne(s){if(s===null)return"No reset";const r=k.find(a=>a.seconds===s);return r?r.label:s%p===0?`Every ${s/p} days`:s%L===0?`Every ${s/L} hours`:`Every ${s}s`}function U(s){if(!s)return"—";const r=new Date(s);return Number.isNaN(r.getTime())?"—":r.toLocaleString()}function ie(s){const r=s.trim();if(r==="")return{value:null,valid:!0};const a=Number(r);return Number.isNaN(a)||a<0?{value:null,valid:!1}:{value:a,valid:!0}}function le({value:s,onChange:r}){const a=k.some(d=>d.seconds===s),[i,l]=u.useState(!a),x=s!==null&&s%p===0?String(s/p):"";return e.jsxs("div",{className:"flex flex-col gap-2",children:[e.jsx("span",{className:"text-sm font-medium text-[var(--otari-ink)]",children:"Reset period"}),e.jsxs("div",{className:"flex flex-wrap gap-2",children:[k.map(d=>e.jsx(m,{size:"sm",variant:!i&&s===d.seconds?"primary":"outline",onPress:()=>{l(!1),r(d.seconds)},children:d.label},d.label)),e.jsx(m,{size:"sm",variant:i?"primary":"outline",onPress:()=>l(!0),children:"Custom"})]}),i?e.jsx("div",{className:"flex items-end gap-2",children:e.jsx(B,{label:"Every N days",value:x,onChange:d=>{const o=Number(d.trim());r(d.trim()===""||Number.isNaN(o)||o<=0?null:Math.round(o)*p)},placeholder:"14",description:"Whole days between resets."})}):null,e.jsx("span",{className:"text-xs text-[var(--otari-muted)]",children:"Spend returns to zero each period. A user’s clock starts when the budget is assigned to them."})]})}function E({title:s,submitLabel:r,initial:a,error:i,isPending:l,onSubmit:x,onClose:d,assignUsers:o}){const[f,h]=u.useState(a.name??""),[j,t]=u.useState(a.max_budget===null?"":String(a.max_budget)),[c,S]=u.useState(a.budget_duration_sec),[g,P]=u.useState([]),b=ie(j),n=()=>{l||!b.valid||x({name:f.trim()||null,max_budget:b.value,budget_duration_sec:c},g)};return e.jsx(w,{children:e.jsxs(w.Content,{className:"flex flex-col gap-4 p-5",children:[e.jsx("div",{className:"text-sm font-semibold text-[var(--otari-ink)]",children:s}),e.jsx(D,{error:i}),e.jsx(B,{label:"Name (optional)",value:f,onChange:h,autoFocus:!0,placeholder:"team-free-tier",description:"A label to recognize this budget later."}),e.jsx(B,{label:"Spending limit (USD)",value:j,onChange:t,placeholder:"100.00",description:b.valid?"The most a single user on this budget may spend per period. Leave blank for no limit.":e.jsx("span",{className:"text-red-700",children:"Enter a non-negative number, or leave blank for no limit."})}),e.jsx(le,{value:c,onChange:S}),o?e.jsx(re,{label:"Assign to users (optional)",description:"Attach this budget to existing users now. You can also manage assignments later on the Users page.",value:g,onChange:P,users:o}):null,e.jsxs("div",{className:"flex gap-2",children:[e.jsx(m,{variant:"primary",isDisabled:l||!b.valid,onPress:n,children:l?"Saving…":r}),e.jsx(m,{variant:"ghost",onPress:d,children:"Cancel"})]})]})})}function de({budget:s}){if(s.user_count===0)return e.jsx("span",{className:"text-xs text-[var(--otari-muted)]",children:"No users assigned"});const r=s.total_spend;if(s.max_budget===null)return e.jsxs("span",{className:"text-xs text-[var(--otari-ink)]",children:[_(r)," spent",e.jsx("span",{className:"text-[var(--otari-muted)]",children:" · no limit"})]});const a=s.max_budget*s.user_count,i=a>0?Math.min(100,r/a*100):0,l=r>a;return e.jsxs("div",{className:"flex min-w-[140px] flex-col gap-1",children:[e.jsxs("div",{className:"flex items-baseline justify-between gap-2 text-xs",children:[e.jsx("span",{className:"text-[var(--otari-ink)]",children:_(r)}),e.jsxs("span",{className:"text-[var(--otari-muted)]",children:["of ",_(a)]})]}),e.jsx("div",{className:"h-1.5 w-full overflow-hidden rounded-full bg-[var(--otari-line)]",role:"progressbar","aria-valuenow":Math.round(i),"aria-valuemin":0,"aria-valuemax":100,"aria-label":"Aggregate spend against total allocation",children:e.jsx("div",{className:`h-full rounded-full ${l?"bg-red-500":"bg-[var(--otari-brand)]"}`,style:{width:`${Math.max(i,l?100:2)}%`}})})]})}function oe({budgetId:s}){const r=q(s);if(r.isLoading)return e.jsxs("div",{className:"flex items-center gap-2 px-4 py-4 text-sm text-[var(--otari-muted)]",children:[e.jsx(se,{size:"sm"})," Loading reset history…"]});if(r.error)return e.jsx("div",{className:"px-4 py-4",children:e.jsx(D,{error:r.error})});const a=r.data??[];return a.length===0?e.jsx("div",{className:"px-4 py-4 text-sm text-[var(--otari-muted)]",children:"No resets recorded yet for this budget."}):e.jsx("div",{className:"overflow-x-auto px-4 py-3",children:e.jsxs("table",{className:"w-full border-collapse text-xs",children:[e.jsx("thead",{className:"text-left text-[var(--otari-muted)]",children:e.jsxs("tr",{children:[e.jsx("th",{className:"py-1.5 pr-4 font-medium",children:"User"}),e.jsx("th",{className:"py-1.5 pr-4 font-medium",children:"Spend cleared"}),e.jsx("th",{className:"py-1.5 pr-4 font-medium",children:"Reset at"}),e.jsx("th",{className:"py-1.5 font-medium",children:"Next reset"})]})}),e.jsx("tbody",{children:a.map(i=>e.jsxs("tr",{className:"border-t border-[var(--otari-line)]",children:[e.jsx("td",{className:"py-1.5 pr-4",children:e.jsx("code",{children:i.user_id??"—"})}),e.jsx("td",{className:"py-1.5 pr-4 text-[var(--otari-ink)]",children:_(i.previous_spend)}),e.jsx("td",{className:"py-1.5 pr-4 text-[var(--otari-muted)]",children:U(i.reset_at)}),e.jsx("td",{className:"py-1.5 text-[var(--otari-muted)]",children:U(i.next_reset_at)})]},i.id))})]})})}function ce({onCreate:s}){return e.jsx(w,{children:e.jsxs(w.Content,{className:"flex flex-col gap-4 p-6",children:[e.jsxs("div",{children:[e.jsx("h2",{className:"text-lg font-semibold text-[var(--otari-ink)]",children:"No budgets yet"}),e.jsx("p",{className:"mt-1 text-sm text-[var(--otari-muted)]",children:"A budget caps how much a user may spend and, optionally, resets that spend on a schedule. Create one, then assign it to users to enforce a limit."})]}),e.jsx("div",{children:e.jsx(m,{variant:"primary",onPress:s,children:"Create your first budget"})})]})})}function ue({label:s,isPending:r,onConfirm:a}){const[i,l]=u.useState(!1);return i?e.jsxs("div",{className:"flex flex-col items-end gap-1.5 rounded-lg border border-amber-200 bg-amber-50 p-2 text-right",children:[e.jsxs("span",{className:"max-w-xs text-xs text-amber-800",children:["Delete ",e.jsx("strong",{children:s}),"? Users keep their spend but lose this limit. Cannot be undone."]}),e.jsxs("span",{className:"inline-flex gap-1",children:[e.jsx(m,{size:"sm",variant:"danger",isDisabled:r,onPress:a,children:"Delete permanently"}),e.jsx(m,{size:"sm",variant:"ghost",isDisabled:r,onPress:()=>l(!1),children:"Cancel"})]})]}):e.jsx(m,{size:"sm",variant:"danger-soft",onPress:()=>l(!0),children:"Delete"})}function I(s){return s.split("-")[0]}function A(s){return s.name??I(s.budget_id)}function je(){const s=z(),r=M(),a=$(),i=O(),l=F(),x=H(),[d,o]=u.useState(!1),[f,h]=u.useState(null),[j,t]=u.useState(null),c=s.data??[],S=s.isLoading,g=c.find(n=>n.budget_id===f)??null,P=!S&&c.length===0&&!d,b=(n,v)=>{a.mutate(n,{onSuccess:async R=>{v.length>0&&await Promise.allSettled(v.map(T=>x.mutateAsync({id:T,body:{budget_id:R.budget_id}}))),o(!1)}})};return e.jsxs("div",{className:"flex flex-col gap-6",children:[e.jsx(V,{title:"Budgets",description:"Define spending limits and reset schedules. Assign a budget to users to enforce it.",action:d?null:e.jsx(m,{variant:"primary",onPress:()=>{h(null),o(!0)},children:"Create budget"})}),e.jsx(D,{error:s.error??a.error??i.error??l.error??x.error}),e.jsx(W,{children:"Assign a budget to users when you create it, or later from the Users page. Each row’s usage aggregates the spend of the users currently on that budget."}),P?e.jsx(ce,{onCreate:()=>{h(null),o(!0)}}):null,d?e.jsx(E,{title:"Create budget",submitLabel:"Create budget",initial:{name:null,max_budget:null,budget_duration_sec:null},error:a.error,isPending:a.isPending,assignUsers:r.data??[],onSubmit:b,onClose:()=>o(!1)}):null,g?e.jsx(E,{title:`Edit budget ${A(g)}`,submitLabel:"Save changes",initial:{name:g.name,max_budget:g.max_budget,budget_duration_sec:g.budget_duration_sec},error:i.error,isPending:i.isPending,onSubmit:n=>i.mutate({id:g.budget_id,body:n},{onSuccess:()=>h(null)}),onClose:()=>h(null)},g.budget_id):null,e.jsxs(Y,{children:[e.jsx(G,{children:e.jsxs("tr",{children:[e.jsx(N,{children:"Budget"}),e.jsx(N,{children:"Limit (per user)"}),e.jsx(N,{children:"Reset"}),e.jsx(N,{children:"Users"}),e.jsx(N,{children:"Usage"}),e.jsx(N,{className:"text-right",children:"Actions"})]})}),e.jsx("tbody",{children:S?e.jsx(K,{colSpan:6}):c.length===0?e.jsx(Q,{colSpan:6,children:"No budgets yet. Create one to cap spending."}):c.map(n=>e.jsxs(u.Fragment,{children:[e.jsxs(X,{selected:f===n.budget_id,onClick:()=>{o(!1),h(n.budget_id)},children:[e.jsx(y,{className:"font-medium text-[var(--otari-ink)]",children:e.jsxs("div",{className:"flex flex-col gap-0.5",children:[e.jsx("span",{children:n.name??e.jsx("span",{className:"text-[var(--otari-muted)]",children:"(unnamed)"})}),e.jsx("code",{className:"text-[11px] text-[var(--otari-muted)]",title:n.budget_id,children:I(n.budget_id)})]})}),e.jsx(y,{children:n.max_budget===null?e.jsx("span",{className:"text-[var(--otari-muted)]",children:"Unlimited"}):_(n.max_budget)}),e.jsx(y,{className:"text-[var(--otari-muted)]",children:ne(n.budget_duration_sec)}),e.jsx(y,{className:"text-[var(--otari-muted)]",children:n.user_count}),e.jsx(y,{children:e.jsx(de,{budget:n})}),e.jsx(y,{children:e.jsxs("div",{className:"flex items-center justify-end gap-1.5",onClick:v=>v.stopPropagation(),children:[e.jsx(m,{size:"sm",variant:"ghost",onPress:()=>t(v=>v===n.budget_id?null:n.budget_id),children:j===n.budget_id?"Hide history":"History"}),e.jsx(m,{size:"sm",variant:"ghost",onPress:()=>{o(!1),h(n.budget_id)},children:"Edit"}),e.jsx(ue,{label:A(n),isPending:l.isPending,onConfirm:()=>l.mutate(n.budget_id)})]})})]}),j===n.budget_id?e.jsx("tr",{className:"border-b border-[var(--otari-line)] bg-[var(--otari-bg)]",children:e.jsx("td",{colSpan:6,children:e.jsx(oe,{budgetId:n.budget_id})})}):null]},n.budget_id))})]})]})}export{je as BudgetsPage};

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

Reject non-finite budget values before mutation.

ie accepts Infinity and values such as 1e309 because it only checks Number.isNaN; the custom reset-period parser has the same gap. JSON serialization turns Infinity into null, so a supposed spending limit can become unlimited. Use Number.isFinite in both validation paths.

🤖 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/BudgetsPage-Dzegp1K_.js` at line 1,
Update the budget-value validator ie and the custom reset-period parser inside
le to reject non-finite numbers, using Number.isFinite alongside the existing
validity checks. Ensure Infinity, -Infinity, and overflowed inputs such as 1e309
are invalid and cannot reach either mutation path.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Do not hide partial user-assignment failures.

After creating a budget, Promise.allSettled ignores rejected assignment mutations and closes the dialog. The budget can therefore be reported as created while some users remain unassigned and unenforced. Prefer a transactional/bulk assignment endpoint, or surface failed user IDs and keep the operation retryable.

🤖 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/BudgetsPage-Dzegp1K_.js` at line 1, The
budget creation handler in je, specifically the Promise.allSettled assignment
flow, must not silently ignore rejected user-assignment mutations or close the
dialog after partial failure. Prefer a transactional/bulk assignment operation;
otherwise collect failed user IDs, surface the failure through the existing
error UI, and keep the dialog/state retryable while preserving successful
assignments.

@@ -0,0 +1 @@
import{j as e}from"./tanstack-query-BPQ56vNC.js";import{g as it,u as nt,r as l}from"./react-DY5rsDwN.js";import{t as at,v as lt,w as ct,x as ot,y as dt,P as ut,E as Fe,F as T,I as pt,z as me,A as Te,B as ve,D as xt,G as mt,H as We,C as Be,g as he,J as U}from"./index-DISDvNSb.js";import{M as ht}from"./ModelComboBox-yD9VG5PR.js";import{T as ft,a as vt,d as Le,b as fe,L as gt,e as q,c as bt}from"./Table-CugMZAuW.js";import{c as se,B as k,f as W}from"./heroui-B1YjjANo.js";function De(t){const r=t.indexOf(":");return r>0?t.slice(0,r):"—"}function jt(t,r=Date.now()){const s=new Map;for(const i of t){const u=s.get(i.model_key)??[];u.push(i),s.set(i.model_key,u)}const a=[];for(const i of s.values()){const u=[...i].sort((x,m)=>Date.parse(x.effective_at)-Date.parse(m.effective_at)),p=[...u].reverse().find(x=>Date.parse(x.effective_at)<=r);a.push(p??u[0])}return a.sort((i,u)=>i.model_key.localeCompare(u.model_key))}const yt="otari",Ee={vision:"Vision",reasoning:"Reasoning",embeddings:"Embeddings",streaming:"Streaming",audio:"Audio",image_generation:"Image gen",pdf:"PDF",rerank:"Rerank",responses_api:"Responses API",moderation:"Moderation",list_models:"Lists models"},Ae=[{value:"vision",label:"Vision",test:t=>t.input_modalities.includes("image")},{value:"tool_call",label:"Tool calling",test:t=>!!t.tool_call},{value:"reasoning",label:"Reasoning",test:t=>!!t.reasoning},{value:"structured_output",label:"Structured output",test:t=>!!t.structured_output},{value:"attachment",label:"Attachments",test:t=>!!t.attachment},{value:"audio",label:"Audio",test:t=>t.input_modalities.includes("audio")},{value:"pdf",label:"PDF",test:t=>t.input_modalities.includes("pdf")}],_t=[{key:"reasoning",label:"Reasoning"},{key:"tool_call",label:"Tool calling"},{key:"structured_output",label:"Structured output"},{key:"attachment",label:"Attachments"},{key:"temperature",label:"Temperature"}],Oe={text:"Text",image:"Image",audio:"Audio",video:"Video",pdf:"PDF"},Nt=[{value:"0",label:"Any context"},{value:"8000",label:"≥ 8K"},{value:"32000",label:"≥ 32K"},{value:"128000",label:"≥ 128K"},{value:"200000",label:"≥ 200K"},{value:"1000000",label:"≥ 1M"}],Pt=[{value:"",label:"Any price"},{value:"1",label:"≤ $1 / 1M in"},{value:"3",label:"≤ $3 / 1M in"},{value:"10",label:"≤ $10 / 1M in"},{value:"30",label:"≤ $30 / 1M in"}],St=[{value:"all",label:"Any release date"},{value:"365",label:"Past year"},{value:"730",label:"Past 2 years"},{value:"1095",label:"Past 3 years"}],kt=1440*60*1e3;function K(t){const r=Number(t);return t.trim()!==""&&Number.isFinite(r)&&r>=0}function R(t){if(t.trim()==="")return!0;const r=Number(t);return Number.isFinite(r)&&r>=0}function V(t){return t.trim()===""?null:Number(t)}function P({value:t,onChange:r,ariaLabel:s}){return e.jsx("input",{type:"number",step:"any",min:"0",inputMode:"decimal","aria-label":s,value:t,onChange:a=>r(a.target.value),className:"w-28 rounded-md border border-[var(--otari-line)] bg-white px-2 py-1 text-right text-sm tabular-nums focus:border-[var(--otari-brand)] focus:outline-none"})}function Ct({source:t}){return t==="configured"?e.jsx(W,{size:"sm",color:"default",children:"configured"}):t==="default"||t==="alias"?e.jsx(W,{size:"sm",color:"accent",children:t}):e.jsx("span",{className:"text-xs text-[var(--otari-muted)]",children:"not priced"})}function Mt({onClose:t}){const r=ve(),[s,a]=l.useState(""),[i,u]=l.useState(""),[p,x]=l.useState(""),[m,d]=l.useState(""),[f,h]=l.useState(""),_=s.trim()!==""&&K(i)&&K(p)&&R(m)&&R(f),A=()=>{_&&r.mutate({model_key:s.trim(),input_price_per_million:Number(i),output_price_per_million:Number(p),cache_read_price_per_million:V(m),cache_write_price_per_million:V(f)},{onSuccess:t})};return e.jsxs("div",{className:"flex flex-col gap-4",children:[e.jsx(Fe,{error:r.error}),e.jsxs("div",{className:"grid gap-4 sm:grid-cols-3",children:[e.jsx(ht,{label:"Model",value:s,onChange:a,isRequired:!0,autoFocus:!0,description:"Pick one your providers report, or type any provider:model key."}),e.jsxs("div",{className:"flex flex-col gap-1",children:[e.jsx("span",{className:"text-sm font-medium text-[var(--otari-ink)]",children:"Input $ / 1M"}),e.jsx(P,{value:i,onChange:u,ariaLabel:"Input price per million"})]}),e.jsxs("div",{className:"flex flex-col gap-1",children:[e.jsx("span",{className:"text-sm font-medium text-[var(--otari-ink)]",children:"Output $ / 1M"}),e.jsx(P,{value:p,onChange:x,ariaLabel:"Output price per million"})]}),e.jsxs("div",{className:"flex flex-col gap-1",children:[e.jsx("span",{className:"text-sm font-medium text-[var(--otari-ink)]",children:"Cache read $ / 1M"}),e.jsx(P,{value:m,onChange:d,ariaLabel:"Cache read price per million"})]}),e.jsxs("div",{className:"flex flex-col gap-1",children:[e.jsx("span",{className:"text-sm font-medium text-[var(--otari-ink)]",children:"Cache write $ / 1M"}),e.jsx(P,{value:f,onChange:h,ariaLabel:"Cache write price per million"})]})]}),e.jsx("p",{className:"text-xs text-[var(--otari-muted)]",children:"Cache rates are optional. Leave them blank to bill cached tokens at the input rate. Cache write applies to Anthropic cache creation."}),e.jsxs("div",{className:"flex gap-2",children:[e.jsx(k,{variant:"primary",isDisabled:!_||r.isPending,onPress:A,children:r.isPending?"Saving…":"Save price"}),e.jsx(k,{variant:"ghost",onPress:t,children:"Cancel"})]})]})}function ie({label:t,tone:r="info",children:s}){const a=l.useId();return e.jsxs("span",{className:"group relative inline-flex items-center font-normal normal-case",children:[e.jsx("button",{type:"button","aria-label":t,"aria-describedby":a,className:`inline-flex h-4 w-4 items-center justify-center rounded-full border text-[10px] leading-none ${r==="warning"?"border-[#c2843a] text-[#b45309]":"border-[var(--otari-line)] text-[var(--otari-muted)] hover:border-[var(--otari-brand)] hover:text-[var(--otari-brand)]"}`,children:"i"}),e.jsx("span",{id:a,role:"tooltip",className:"pointer-events-none absolute top-full right-0 z-20 mt-1.5 w-72 rounded-lg border border-[var(--otari-line)] bg-[var(--otari-surface)] px-3 py-2 text-left text-xs font-normal whitespace-normal break-words text-[var(--otari-ink)] opacity-0 shadow-lg transition-opacity group-hover:opacity-100 group-focus-within:opacity-100",children:s})]})}function It(){const t=mt();return t.data?t.data.default_pricing?e.jsx(ie,{label:"How unpriced models are metered",tone:"info",children:"Default pricing is on: models without a configured price are metered using community-maintained rates (the bundled genai-prices dataset). Set a price to override the fallback."}):e.jsxs(ie,{label:"How unpriced models are metered",tone:"warning",children:["Default pricing is off: only models with a configured price are metered.",t.data.require_pricing?" Requests for any other model are rejected (HTTP 402) because require_pricing is on.":" Other models are served without cost tracking."]}):null}function Lt({capabilities:t}){const r=Object.keys(Ee).filter(s=>t[s]);return r.length===0?e.jsx("span",{className:"text-sm text-[var(--otari-muted)]",children:"No capabilities reported."}):e.jsx("div",{className:"flex flex-wrap gap-1.5",children:r.map(s=>e.jsx(W,{size:"sm",color:"default",children:Ee[s]},s))})}function L({label:t,value:r}){return e.jsxs("div",{className:"flex items-baseline justify-between gap-3",children:[e.jsx("span",{className:"text-xs text-[var(--otari-muted)]",children:t}),e.jsx("span",{className:"text-right text-sm text-[var(--otari-ink)] tabular-nums",children:r})]})}function G({title:t,children:r}){return e.jsxs("div",{className:"flex flex-col gap-2",children:[e.jsx("span",{className:"text-xs font-semibold uppercase tracking-wide text-[var(--otari-muted)]",children:t}),r]})}function Dt({row:t}){const r=ve(),s=We(),[a,i]=l.useState(!1),[u,p]=l.useState(""),[x,m]=l.useState(""),[d,f]=l.useState(""),[h,_]=l.useState(""),A=()=>{p(t.inputPrice==null?"":String(t.inputPrice)),m(t.outputPrice==null?"":String(t.outputPrice)),f(t.cacheReadPrice==null?"":String(t.cacheReadPrice)),_(t.cacheWritePrice==null?"":String(t.cacheWritePrice)),i(!0)},J=K(u)&&K(x)&&R(d)&&R(h),B=()=>{J&&r.mutate({model_key:t.key,input_price_per_million:Number(u),output_price_per_million:Number(x),cache_read_price_per_million:V(d),cache_write_price_per_million:V(h)},{onSuccess:()=>i(!1)})};return a?e.jsxs("div",{className:"flex flex-col gap-2",children:[e.jsxs("div",{className:"flex items-center justify-between gap-2",children:[e.jsx("span",{className:"text-xs text-[var(--otari-muted)]",children:"Input $ / 1M"}),e.jsx(P,{value:u,onChange:p,ariaLabel:`Input price for ${t.key}`})]}),e.jsxs("div",{className:"flex items-center justify-between gap-2",children:[e.jsx("span",{className:"text-xs text-[var(--otari-muted)]",children:"Output $ / 1M"}),e.jsx(P,{value:x,onChange:m,ariaLabel:`Output price for ${t.key}`})]}),e.jsxs("div",{className:"flex items-center justify-between gap-2",children:[e.jsx("span",{className:"text-xs text-[var(--otari-muted)]",children:"Cache read $ / 1M"}),e.jsx(P,{value:d,onChange:f,ariaLabel:`Cache read price for ${t.key}`})]}),e.jsxs("div",{className:"flex items-center justify-between gap-2",children:[e.jsx("span",{className:"text-xs text-[var(--otari-muted)]",children:"Cache write $ / 1M"}),e.jsx(P,{value:h,onChange:_,ariaLabel:`Cache write price for ${t.key}`})]}),e.jsxs("div",{className:"flex items-center gap-2",children:[e.jsx(k,{size:"sm",variant:"primary",isDisabled:r.isPending||!J,onPress:B,children:"Save"}),e.jsx(k,{size:"sm",variant:"ghost",isDisabled:r.isPending,onPress:()=>i(!1),children:"Cancel"})]}),r.error?e.jsx("span",{className:"text-xs text-red-700",children:he(r.error)}):null]}):e.jsxs("div",{className:"flex flex-col gap-2",children:[e.jsx(L,{label:"Input",value:t.inputPrice==null?"—":`${U(t.inputPrice)} / 1M`}),e.jsx(L,{label:"Output",value:t.outputPrice==null?"—":`${U(t.outputPrice)} / 1M`}),e.jsx(L,{label:"Cache read",value:t.cacheReadPrice==null?"—":`${U(t.cacheReadPrice)} / 1M`}),e.jsx(L,{label:"Cache write",value:t.cacheWritePrice==null?"—":`${U(t.cacheWritePrice)} / 1M`}),e.jsxs("div",{className:"flex items-center gap-2 pt-1",children:[e.jsx(k,{size:"sm",variant:"outline",onPress:A,children:t.source==="configured"?"Edit price":"Set price"}),t.source==="configured"?e.jsxs(e.Fragment,{children:[e.jsx(Be,{confirmLabel:"Reset",isPending:s.isPending,onConfirm:()=>s.mutate(t.key),children:"Reset"}),e.jsx(ie,{label:"What reset does",children:"Removes the custom price. The model reverts to the default rate (genai-prices) when default pricing is on, otherwise it is metered at no cost."})]}):null,s.error?e.jsx("span",{className:"text-xs text-red-700",children:he(s.error)}):null]})]})}function Et({provider:t,info:r,modelCount:s,discoveryError:a}){return e.jsx(G,{title:"Provider",children:e.jsxs("div",{className:"flex flex-col gap-3 rounded-lg border border-[var(--otari-line)] p-3",children:[e.jsxs("div",{children:[e.jsx("div",{className:"text-sm font-medium text-[var(--otari-ink)]",children:(r==null?void 0:r.name)??t}),e.jsx("div",{className:"text-xs text-[var(--otari-muted)]",children:r&&r.provider_type!==t?`${t} · ${r.provider_type}`:t})]}),a?e.jsxs("span",{className:"text-xs text-red-700",children:["Could not list models: ",a]}):e.jsxs("span",{className:"text-xs text-[var(--otari-muted)]",children:[Te(s)," model",s===1?"":"s"," reported"]}),r?e.jsx(Lt,{capabilities:r.capabilities}):null,r!=null&&r.env_key?e.jsx("code",{className:"w-fit rounded bg-[var(--otari-bg)] px-2 py-0.5 text-xs text-[var(--otari-ink)]",children:r.env_key}):null,r&&(r.doc_url||r.pricing_urls.length>0)?e.jsxs("div",{className:"flex flex-col gap-1",children:[r.doc_url?e.jsx("a",{href:r.doc_url,target:"_blank",rel:"noreferrer",className:"text-xs text-[var(--otari-brand)] underline underline-offset-2",children:"API documentation"}):null,r.pricing_urls.slice(0,2).map(i=>e.jsx("a",{href:i,target:"_blank",rel:"noreferrer",className:"text-xs break-all text-[var(--otari-brand)] underline underline-offset-2",children:"Pricing page"},i))]}):null]})})}function At({row:t,metadata:r,metadataAvailable:s,providerInfo:a,providerModelCount:i,providerDiscoveryError:u,onMakeAlias:p,onClose:x}){const m=(r==null?void 0:r.input_modalities)??[],d=(r==null?void 0:r.output_modalities)??[],f=_t.filter(({key:h})=>r==null?void 0:r[h]);return e.jsx(se,{children:e.jsxs(se.Content,{className:"flex flex-col gap-5 p-5",children:[e.jsxs("div",{className:"flex items-start justify-between gap-3",children:[e.jsxs("div",{className:"min-w-0",children:[e.jsxs("div",{className:"flex items-center gap-2",children:[e.jsx("h2",{className:"text-base font-semibold break-all text-[var(--otari-ink)]",children:t.model}),r!=null&&r.deprecated?e.jsx(W,{size:"sm",color:"danger",children:"deprecated"}):null]}),r!=null&&r.family?e.jsx("p",{className:"text-xs text-[var(--otari-muted)]",children:r.family}):null]}),e.jsx("button",{type:"button","aria-label":"Close model details",onClick:x,className:"-mt-1 -mr-1 shrink-0 rounded-md px-1.5 py-0.5 text-lg leading-none text-[var(--otari-muted)] hover:bg-[var(--otari-bg)] hover:text-[var(--otari-ink)]",children:"✕"})]}),r!=null&&r.description?e.jsx("p",{className:"text-sm text-[var(--otari-ink)]",children:r.description}):null,e.jsxs(G,{title:"Pricing",children:[e.jsxs("div",{className:"flex items-center gap-2",children:[e.jsx(Ct,{source:t.source}),t.isDiscovered?null:e.jsx("span",{className:"text-xs text-[var(--otari-muted)]",children:"not discovered"})]}),e.jsx(Dt,{row:t},t.key),e.jsx(k,{size:"sm",variant:"outline",onPress:()=>p(t.key),children:"Make an alias"})]}),e.jsxs(G,{title:"Specs",children:[e.jsx(L,{label:"Context window",value:me(t.contextWindow)}),e.jsx(L,{label:"Max output",value:me((r==null?void 0:r.max_output_tokens)??null)}),e.jsx(L,{label:"Knowledge cutoff",value:(r==null?void 0:r.knowledge_cutoff)??"—"}),e.jsx(L,{label:"Released",value:xt(r==null?void 0:r.release_date)}),e.jsx(L,{label:"Open weights",value:r?r.open_weights?"Yes":"No":"—"})]}),e.jsx(G,{title:"Modalities",children:m.length===0&&d.length===0?e.jsx("span",{className:"text-sm text-[var(--otari-muted)]",children:"Unknown."}):e.jsxs("div",{className:"flex flex-col gap-1.5 text-xs text-[var(--otari-muted)]",children:[e.jsxs("div",{className:"flex flex-wrap items-center gap-1",children:[e.jsx("span",{children:"In:"}),m.map(h=>e.jsx(W,{size:"sm",color:"default",children:Oe[h]??h},h))]}),e.jsxs("div",{className:"flex flex-wrap items-center gap-1",children:[e.jsx("span",{children:"Out:"}),d.map(h=>e.jsx(W,{size:"sm",color:"default",children:Oe[h]??h},h))]})]})}),e.jsx(G,{title:"Capabilities",children:f.length>0?e.jsx("div",{className:"flex flex-wrap gap-1.5",children:f.map(({key:h,label:_})=>e.jsx(W,{size:"sm",color:"default",children:_},h))}):e.jsx("span",{className:"text-sm text-[var(--otari-muted)]",children:s?"None reported.":"Extended metadata unavailable (models.dev disabled or unreachable)."})}),a&&a.provider_type===t.provider?e.jsx(Et,{provider:t.provider,info:a,modelCount:i,discoveryError:u}):null]})})}const ue=25;function Ot({value:t,onChange:r,placeholder:s}){return e.jsx("input",{type:"search",value:t,onChange:a=>r(a.target.value),placeholder:s,"aria-label":s,className:"w-full max-w-xs rounded-md border border-[var(--otari-line)] bg-white px-3 py-1.5 text-sm focus:border-[var(--otari-brand)] focus:outline-none"})}function $t({page:t,pageCount:r,total:s,onPage:a}){return r<=1?null:e.jsxs("div",{className:"flex items-center justify-between px-1 pt-1 text-sm text-[var(--otari-muted)]",children:[e.jsxs("span",{children:["Page ",t+1," of ",r," · ",Te(s)," model",s===1?"":"s"]}),e.jsxs("span",{className:"inline-flex gap-2",children:[e.jsx(k,{size:"sm",variant:"outline",isDisabled:t===0,onPress:()=>a(t-1),children:"Prev"}),e.jsx(k,{size:"sm",variant:"outline",isDisabled:t>=r-1,onPress:()=>a(t+1),children:"Next"})]})]})}const ze="otari.dashboard.modelsSort",pe={col:"model",dir:"asc"},Ft=["model","context","released","input","output"];function Tt(){if(typeof window>"u")return pe;try{const t=window.localStorage.getItem(ze);if(!t)return pe;const r=JSON.parse(t);if(Ft.includes(r.col)&&(r.dir==="asc"||r.dir==="desc"))return{col:r.col,dir:r.dir}}catch{}return pe}function xe({label:t,col:r,sort:s,onSort:a,align:i="left",info:u}){const p=s.col===r;return e.jsx(fe,{className:i==="right"?"text-right":void 0,ariaSort:p?s.dir==="asc"?"ascending":"descending":"none",children:e.jsxs("span",{className:`inline-flex items-center gap-1.5 ${i==="right"?"flex-row-reverse":""}`,children:[e.jsxs("button",{type:"button",onClick:()=>a(r),className:`inline-flex items-center gap-1 ${i==="right"?"flex-row-reverse":""} hover:text-[var(--otari-ink)]`,children:[t,e.jsx("span",{className:"text-[10px] text-[var(--otari-muted)]",children:p?s.dir==="asc"?"▲":"▼":"↕"})]}),u]})})}function Wt({row:t,onClose:r}){const s=ve(),a=We(),[i,u]=l.useState(t.inputPrice==null?"":String(t.inputPrice)),[p,x]=l.useState(t.outputPrice==null?"":String(t.outputPrice)),[m,d]=l.useState(t.cacheReadPrice==null?"":String(t.cacheReadPrice)),[f,h]=l.useState(t.cacheWritePrice==null?"":String(t.cacheWritePrice)),_=K(i)&&K(p)&&R(m)&&R(f),A=()=>{_&&s.mutate({model_key:t.key,input_price_per_million:Number(i),output_price_per_million:Number(p),cache_read_price_per_million:V(m),cache_write_price_per_million:V(f)},{onSuccess:r})};return e.jsxs("div",{className:"flex flex-wrap items-center gap-3 px-4 py-3",children:[e.jsx("span",{className:"text-xs font-medium break-all text-[var(--otari-muted)]",children:t.key}),e.jsxs("label",{className:"flex items-center gap-1.5 text-xs text-[var(--otari-muted)]",children:["Input $ / 1M",e.jsx(P,{value:i,onChange:u,ariaLabel:`Input price for ${t.key}`})]}),e.jsxs("label",{className:"flex items-center gap-1.5 text-xs text-[var(--otari-muted)]",children:["Output $ / 1M",e.jsx(P,{value:p,onChange:x,ariaLabel:`Output price for ${t.key}`})]}),e.jsxs("label",{className:"flex items-center gap-1.5 text-xs text-[var(--otari-muted)]",children:["Cache read $ / 1M",e.jsx(P,{value:m,onChange:d,ariaLabel:`Cache read price for ${t.key}`})]}),e.jsxs("label",{className:"flex items-center gap-1.5 text-xs text-[var(--otari-muted)]",children:["Cache write $ / 1M",e.jsx(P,{value:f,onChange:h,ariaLabel:`Cache write price for ${t.key}`})]}),e.jsx(k,{size:"sm",variant:"primary",isDisabled:s.isPending||!_,onPress:A,children:s.isPending?"Saving…":"Save"}),e.jsx(k,{size:"sm",variant:"ghost",isDisabled:s.isPending,onPress:r,children:"Cancel"}),t.source==="configured"?e.jsxs("span",{className:"inline-flex items-center gap-1",children:[e.jsx(Be,{confirmLabel:"Reset",isPending:a.isPending,onConfirm:()=>a.mutate(t.key,{onSuccess:r}),children:"Reset"}),e.jsx(ie,{label:"What reset does",children:"Removes the custom price. The model reverts to the default rate (genai-prices) when default pricing is on, otherwise it is metered at no cost."})]}):null,s.error||a.error?e.jsx("span",{className:"text-xs text-red-700",children:he(s.error??a.error)}):null]})}function $e({primary:t,secondary:r,rowKey:s,primaryLabel:a,secondaryLabel:i,onEdit:u}){const p=(x,m)=>e.jsx("button",{type:"button","aria-label":`Edit ${m} price for ${s}`,className:"tabular-nums hover:text-[var(--otari-brand-dark)] hover:underline",onClick:d=>{d.stopPropagation(),u()},children:x==null?"—":U(x)});return e.jsxs("span",{className:"inline-flex items-center justify-end gap-1",children:[p(t,a),e.jsx("span",{className:"text-[var(--otari-muted)]",children:"/"}),p(r,i)]})}function Bt({rows:t,isLoading:r,empty:s,sort:a,onSort:i,selectedKey:u,onSelect:p,pricingKey:x,onSetPricingKey:m}){return e.jsxs(ft,{children:[e.jsx(vt,{children:e.jsxs(Le,{children:[e.jsx(xe,{label:"Model",col:"model",sort:a,onSort:i}),e.jsx(fe,{children:"Provider"}),e.jsx(xe,{label:"Context",col:"context",sort:a,onSort:i,align:"right"}),e.jsx(xe,{label:"In / Out $ / 1M",col:"input",sort:a,onSort:i,align:"right",info:e.jsx(It,{})}),e.jsx(fe,{className:"text-right",children:"Cache r / w $ / 1M"})]})}),e.jsx("tbody",{children:r?e.jsx(gt,{colSpan:5}):t.length>0?t.map(d=>{const f=()=>m(x===d.key?null:d.key);return e.jsxs(l.Fragment,{children:[e.jsxs(Le,{onClick:()=>p(d.key),selected:d.key===u,children:[e.jsx(q,{className:"font-medium break-all",children:d.model}),e.jsx(q,{className:"text-[var(--otari-muted)]",children:d.provider}),e.jsx(q,{className:"text-right tabular-nums text-[var(--otari-muted)]",children:me(d.contextWindow)}),e.jsx(q,{className:"text-right",children:e.jsx($e,{primary:d.inputPrice,secondary:d.outputPrice,rowKey:d.key,primaryLabel:"input",secondaryLabel:"output",onEdit:f})}),e.jsx(q,{className:"text-right",children:e.jsx($e,{primary:d.cacheReadPrice,secondary:d.cacheWritePrice,rowKey:d.key,primaryLabel:"cache read",secondaryLabel:"cache write",onEdit:f})})]}),x===d.key?e.jsx("tr",{className:"border-b border-[var(--otari-line)] bg-[var(--otari-bg)] last:border-b-0",children:e.jsx("td",{colSpan:5,children:e.jsx(Wt,{row:d,onClose:()=>m(null)})})}):null]},d.key)}):e.jsx(bt,{colSpan:5,children:s})})]})}function zt({providers:t}){return t.length===0?null:e.jsxs(pt,{tone:"warning",children:["Could not list ",t.map(r=>r.provider).join(", "),". Check that provider's credentials in config.yml; its models are missing from the list below."]})}function Ut(){var Se,ke,Ce,Me,Ie;const t=it(),[r]=nt(),s=at(),a=lt(),i=ct(),u=ot(),p=dt(),[x,m]=l.useState(""),[d,f]=l.useState(0),[h,_]=l.useState(!1),[A,J]=l.useState(null),[B,ge]=l.useState(null),[C,Ke]=l.useState(Tt);l.useEffect(()=>{try{window.localStorage.setItem(ze,JSON.stringify(C))}catch{}},[C]);const[D,be]=l.useState(r.get("provider")||"all"),[O,Re]=l.useState("all"),[w,Ve]=l.useState("all"),[Y,we]=l.useState("all"),[ne,Ye]=l.useState("0"),[X,He]=l.useState(""),[Z,qe]=l.useState("all"),E=((Se=p.data)==null?void 0:Se.models)??{},Ue=((ke=p.data)==null?void 0:ke.available)??!1,je=l.useMemo(()=>{var n;return new Set((((n=i.data)==null?void 0:n.providers)??[]).flatMap(v=>v.models.map(c=>c.key)))},[i.data]),Ge=n=>{m(n),f(0)},$=n=>v=>{n(v),f(0)},Je=n=>{Ke(v=>v.col===n?{col:n,dir:v.dir==="asc"?"desc":"asc"}:{col:n,dir:n==="released"?"desc":"asc"}),f(0)},Q=l.useMemo(()=>{var N,j,y,M,S;const n=new Map(jt(a.data??[]).map(o=>[o.model_key,o])),v=[],c=new Set,g=(o,de,st,b)=>{if(c.has(o))return;c.add(o);const I=n.get(o);v.push({key:o,model:de,provider:st,isDiscovered:je.has(o),contextWindow:(b==null?void 0:b.contextWindow)??null,inputPrice:I?I.input_price_per_million:(b==null?void 0:b.inputPrice)??null,outputPrice:I?I.output_price_per_million:(b==null?void 0:b.outputPrice)??null,cacheReadPrice:I?I.cache_read_price_per_million:(b==null?void 0:b.cacheReadPrice)??null,cacheWritePrice:I?I.cache_write_price_per_million:(b==null?void 0:b.cacheWritePrice)??null,source:I?"configured":(b==null?void 0:b.source)??"none"})};for(const o of((N=s.data)==null?void 0:N.data)??[]){if(o.owned_by===yt)continue;const de=o.pricing_source==="default"?"default":o.pricing?"configured":"none";g(o.id,o.id,o.owned_by||De(o.id),{key:o.id,model:o.id,provider:o.owned_by,contextWindow:o.context_window,inputPrice:((j=o.pricing)==null?void 0:j.input_price_per_million)??null,outputPrice:((y=o.pricing)==null?void 0:y.output_price_per_million)??null,cacheReadPrice:((M=o.pricing)==null?void 0:M.cache_read_price_per_million)??null,cacheWritePrice:((S=o.pricing)==null?void 0:S.cache_write_price_per_million)??null,source:de})}for(const o of n.keys())g(o,o,De(o));return v},[s.data,a.data,je]),Xe=l.useMemo(()=>new Map(Q.map(n=>[n.key,n])),[Q]),ee=l.useMemo(()=>{var c,g,N;const n=Q.map(j=>{var y,M;return{...j,contextWindow:j.contextWindow??((y=E[j.key])==null?void 0:y.context_window)??null,releaseDate:((M=E[j.key])==null?void 0:M.release_date)??null}}),v=new Set(n.map(j=>j.key));for(const j of((c=i.data)==null?void 0:c.providers)??[])for(const y of j.models)v.has(y.key)||(v.add(y.key),n.push({key:y.key,model:y.key,provider:j.provider,isDiscovered:!0,contextWindow:((g=E[y.key])==null?void 0:g.context_window)??null,releaseDate:((N=E[y.key])==null?void 0:N.release_date)??null,inputPrice:null,outputPrice:null,cacheReadPrice:null,cacheWritePrice:null,source:"none"}));return n},[Q,i.data,E]),Ze=(((Ce=i.data)==null?void 0:Ce.providers)??[]).filter(n=>!n.ok),te=l.useMemo(()=>{const n=Array.from(new Set(ee.map(v=>v.provider))).sort((v,c)=>v.localeCompare(c));return[{value:"all",label:"All providers"},...n.map(v=>({value:v,label:v}))]},[ee]);l.useEffect(()=>{D==="all"||te.length<=1||te.some(n=>n.value===D)||be("all")},[te,D]);const H=x.trim().toLowerCase(),ae=Number(ne)||0,le=X===""?Number.POSITIVE_INFINITY:Number(X),ce=Z==="all"?null:Date.now()-Number(Z)*kt,oe=l.useMemo(()=>{const n=c=>{if(H&&!c.key.toLowerCase().includes(H)&&!c.provider.toLowerCase().includes(H)||D!=="all"&&c.provider!==D||O==="configured"&&c.source!=="configured"||O==="default"&&c.source!=="default"||O==="priced"&&c.inputPrice==null||O==="unpriced"&&c.inputPrice!=null||w==="discovered"&&!c.isDiscovered||w==="custom"&&c.isDiscovered)return!1;if(Y!=="all"){const g=Ae.find(j=>j.value===Y),N=E[c.key];if(!g||!N||!g.test(N))return!1}if(ae>0&&(c.contextWindow==null||c.contextWindow<ae)||le!==Number.POSITIVE_INFINITY&&(c.inputPrice==null||c.inputPrice>le))return!1;if(ce!=null){const g=c.releaseDate?Date.parse(c.releaseDate):Number.NaN;if(Number.isNaN(g)||g<ce)return!1}return!0},v=(c,g)=>{const N=C.dir==="asc"?1:-1;if(C.col==="model")return c.model.localeCompare(g.model)*N;if(C.col==="released"){const S=c.releaseDate??null,o=g.releaseDate??null;return!S&&!o?c.model.localeCompare(g.model):S?o?(S<o?-1:S>o?1:0)*N||c.model.localeCompare(g.model):-1:1}const j=S=>C.col==="context"?S.contextWindow:C.col==="input"?S.inputPrice:S.outputPrice,y=j(c),M=j(g);return y==null&&M==null?c.model.localeCompare(g.model):y==null?1:M==null?-1:(y-M)*N||c.model.localeCompare(g.model)};return ee.filter(n).sort(v)},[ee,H,D,O,w,Y,ae,le,ce,E,C]),ye=oe.length,_e=Math.max(1,Math.ceil(ye/ue)),Ne=Math.min(d,_e-1),Pe=Ne*ue,Qe=oe.slice(Pe,Pe+ue),et=s.isLoading||a.isLoading||i.isLoading,tt=H!==""||D!=="all"||O!=="all"||w!=="all"||Y!=="all"||ne!=="0"||X!==""||Z!=="all"?"No models match your filters.":"No models yet. Add a provider on the Providers page, or price a model below.",F=B?oe.find(n=>n.key===B)??Xe.get(B)??null:null,re=F==null?void 0:F.provider,rt=re?(Me=u.data)==null?void 0:Me.providers.find(n=>n.instance===re):void 0,z=re?(((Ie=i.data)==null?void 0:Ie.providers)??[]).find(n=>n.provider===re):void 0;return e.jsxs("div",{className:"flex flex-col gap-6",children:[e.jsx(ut,{title:"Models",description:"Every model your providers can serve. Set a price on any model so budgets and usage tracking work."}),e.jsx(Fe,{error:s.error??a.error??i.error??u.error??p.error}),e.jsxs("div",{className:`grid gap-4 lg:items-start ${F?"lg:grid-cols-[minmax(0,1fr)_360px]":"grid-cols-1"}`,children:[e.jsxs("div",{className:"flex min-w-0 flex-col gap-3",children:[e.jsxs("div",{className:"flex flex-wrap items-center gap-2",children:[e.jsx(Ot,{value:x,onChange:Ge,placeholder:"Search models…"}),e.jsx(T,{ariaLabel:"Filter by provider",value:D,onChange:$(be),options:te}),e.jsx(T,{ariaLabel:"Filter by pricing",value:O,onChange:$(Re),options:[{value:"all",label:"Any pricing"},{value:"configured",label:"Custom price"},{value:"default",label:"Default price"},{value:"priced",label:"Priced"},{value:"unpriced",label:"Unpriced"}]}),e.jsx(T,{ariaLabel:"Filter by source",value:w,onChange:$(Ve),options:[{value:"all",label:"Any source"},{value:"discovered",label:"Discovered"},{value:"custom",label:"Custom (not discovered)"}]}),e.jsx(T,{ariaLabel:"Filter by capability",value:Y,onChange:$(we),options:[{value:"all",label:"Any capability"},...Ae.map(n=>({value:n.value,label:n.label}))]}),e.jsx(T,{ariaLabel:"Minimum context window",value:ne,onChange:$(Ye),options:Nt}),e.jsx(T,{ariaLabel:"Maximum input price",value:X,onChange:$(He),options:Pt}),e.jsx(T,{ariaLabel:"Filter by release date",value:Z,onChange:$(qe),options:St})]}),e.jsx(zt,{providers:Ze}),e.jsx(Bt,{rows:Qe,isLoading:et,empty:tt,sort:C,onSort:Je,selectedKey:B,onSelect:ge,pricingKey:A,onSetPricingKey:J}),e.jsxs("div",{className:"flex flex-wrap items-center justify-between gap-2",children:[e.jsx($t,{page:Ne,pageCount:_e,total:ye,onPage:f}),e.jsx("button",{type:"button",className:"text-xs font-medium text-[var(--otari-brand-dark)]",onClick:()=>_(n=>!n),children:h?"Hide":"+ Price a model not listed here"})]}),h?e.jsx(se,{children:e.jsxs(se.Content,{className:"flex flex-col gap-4 p-5",children:[e.jsx("div",{className:"text-sm font-semibold text-[var(--otari-ink)]",children:"Price a model not listed here"}),e.jsx(Mt,{onClose:()=>_(!1)})]})}):null]}),F?e.jsx("aside",{className:"lg:sticky lg:top-4",children:e.jsx(At,{row:F,metadata:E[F.key],metadataAvailable:Ue,providerInfo:rt,providerModelCount:(z==null?void 0:z.models.length)??0,providerDiscoveryError:z&&!z.ok?z.error:null,onMakeAlias:n=>t(`/aliases?target=${encodeURIComponent(n)}`),onClose:()=>ge(null)})}):null]})]})}export{Ut as ModelsPage};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Guard optional modality arrays before filtering.

Line 1 calls .includes() on input_modalities for vision, audio, and PDF filters. The same bundle treats that field as optional elsewhere, so metadata without the array can crash the Models page when a capability filter is evaluated.

Proposed fix
-test:t=>t.input_modalities.includes("image")
+test:t=>Array.isArray(t.input_modalities)&&t.input_modalities.includes("image")

Apply the same guard to the audio and PDF predicates.

🤖 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/ModelsPage-eIvU0Tj-.js` at line 1, Update
the vision, audio, and PDF predicates in the Ae capability-filter definitions to
guard optional input_modalities before calling includes. Treat missing modality
arrays as non-matches, preserving current behavior when the array is present.

@@ -0,0 +1 @@
import{j as t}from"./tanstack-query-BPQ56vNC.js";import{f as Q,r as f,N as W}from"./react-DY5rsDwN.js";import{x as z,c as N,K as J,i as X,o as Z,u as tt,a as et,L as b,P as at,E as F,S as v,M as D,N as j,A as k,O as rt,Q as E,R as I}from"./index-DISDvNSb.js";import{T as st,a as nt,b as y,L as ot,c as lt,d as it,e as w}from"./Table-CugMZAuW.js";import"./heroui-B1YjjANo.js";function R(e){return e==="neutral"?void 0:e}const dt=.02,ct=.1;function H(e){if(!e||e.request_count===0)return{rate:null,status:"neutral"};const l=e.error_count/e.request_count,r=l>=ct?"alert":l>=dt?"warn":"ok";return{rate:l,status:r}}function ut(e){return!e||e.total===0?"neutral":e.healthy>=e.total?"ok":e.healthy===0?"alert":"warn"}const xt=.8;function mt(e){if(e.length===0)return{status:"neutral",label:"No budgets configured",overCount:0,nearCount:0,cappedCount:0};const l=e.filter(s=>s.max_budget!==null&&s.user_count>0);if(l.length===0)return{status:"neutral",label:"No capped budgets",overCount:0,nearCount:0,cappedCount:0};let r=0,a=0,c,u=-1;for(const s of l){const o=s.max_budget*s.user_count,i=o>0?s.total_spend/o:0;i>=1?r+=1:i>=xt&&(a+=1),i>u&&(u=i,c={name:s.name??s.budget_id,spent:s.total_spend,allocated:o,pct:i})}const p=r>0?"alert":a>0?"warn":"ok",n=r>0?`${r} over limit`:a>0?`${a} near limit`:"All within budget";return{status:p,label:n,overCount:r,nearCount:a,cappedCount:l.length,worst:c}}const T=864e5,q=30;function B(){const e=new Date;return`${e.getFullYear()}-${e.getMonth()}-${e.getDate()}`}function ht(){const[e,l]=f.useState(B);return f.useEffect(()=>{const r=()=>{if(document.visibilityState==="visible"){const a=B();l(c=>c===a?c:a)}};return document.addEventListener("visibilitychange",r),window.addEventListener("focus",r),()=>{document.removeEventListener("visibilitychange",r),window.removeEventListener("focus",r)}},[]),f.useMemo(()=>{const r=Date.now(),a=new Date(r);return{today:new Date(a.getFullYear(),a.getMonth(),a.getDate()).toISOString(),periodStart:new Date(r-q*T).toISOString(),prevStart:new Date(r-2*q*T).toISOString()}},[e])}const vt={ok:"Healthy",warn:"Elevated",alert:"High"},ft={ok:"All up",warn:"Degraded",alert:"All down"},pt={ok:"On track",warn:"Near limit",alert:"Over budget"};function Rt(){const e=z();return e.isLoading?null:!e.isSuccess||e.data.providers.length===0?t.jsx(Q,{to:"/providers",replace:!0}):t.jsx(gt,{})}function gt(){var O,L,A,P,M;const e=ht(),l=f.useMemo(()=>({start_date:e.today}),[e]),r=f.useMemo(()=>({start_date:e.periodStart}),[e]),a=f.useMemo(()=>({start_date:e.prevStart,end_date:e.periodStart}),[e]),c=N(l,"hour"),u=N(r,"day"),p=N(a,"day"),n=J(),s=X(),o=Z(),i=tt(),S=et({},0,5),C=(O=c.data)==null?void 0:O.totals,d=(L=u.data)==null?void 0:L.totals,x=(A=p.data)==null?void 0:A.totals,m=H(d),$=H(x),K=m.rate!==null&&$.rate!==null?b(m.rate,$.rate):null,h=mt(s.data??[]),g=ut(n.data),Y=(o.data??[]).filter(_=>_.is_active).length,G=(i.data??[]).filter(_=>!_.blocked).length,V=c.error??u.error??n.error??s.error??o.error??i.error;return t.jsxs("div",{className:"flex flex-col gap-6",children:[t.jsx(at,{title:"Overview",description:"At-a-glance spend, traffic, and health across the gateway."}),t.jsx(F,{error:V}),t.jsx(bt,{providerHealth:g,healthy:((P=n.data)==null?void 0:P.healthy)??0,total:((M=n.data)==null?void 0:M.total)??0,budget:h,errStatus:m.status,errRate:m.rate,ready:n.isSuccess&&s.isSuccess&&u.isSuccess,failed:n.isError||s.isError||u.isError}),t.jsxs("div",{className:"grid grid-cols-2 gap-4 sm:grid-cols-3 xl:grid-cols-4",children:[t.jsx(v,{label:"Spend today",value:C?D(C.cost):"—"}),t.jsx(v,{label:"Spend, last 30 days",value:d?D(d.cost):"—",hint:d?t.jsx(j,{fraction:b(d.cost,x==null?void 0:x.cost)}):null}),t.jsx(v,{label:"Requests, last 30 days",value:d?k(d.request_count):"—",hint:d?t.jsx(j,{fraction:b(d.request_count,x==null?void 0:x.request_count)}):null}),t.jsx(v,{label:"Cache reads, last 30 days",value:d?rt(d.cache_read_tokens):"—",hint:d?t.jsx(j,{fraction:b(d.cache_read_tokens,x==null?void 0:x.cache_read_tokens)}):null,to:"/usage"}),t.jsx(v,{label:"Error rate, last 30 days",value:m.rate===null?"—":E(m.rate),status:R(m.status),statusLabel:m.status==="neutral"?void 0:vt[m.status],hint:m.rate!==null?t.jsx(j,{fraction:K}):null}),t.jsx(v,{label:"Budget health",value:s.data&&h.worst?E(h.worst.pct):"—",status:s.data?R(h.status):void 0,statusLabel:s.data&&h.status!=="neutral"?pt[h.status]:void 0,hint:s.data?h.worst?`${h.label} · worst: ${h.worst.name}`:h.label:void 0,to:"/budgets"}),t.jsx(v,{label:"Providers healthy",value:n.data?`${n.data.healthy}/${n.data.total}`:"—",status:n.data?R(g):void 0,statusLabel:n.data&&g!=="neutral"?ft[g]:void 0,hint:n.data?n.data.checked_at?`checked ${I(n.data.checked_at)}`:"not checked yet":void 0,to:"/providers"}),t.jsx(v,{label:"Active keys",value:o.data?k(Y):"—",to:"/keys"}),t.jsx(v,{label:"Active users",value:i.data?k(G):"—",to:"/users"})]}),t.jsx(yt,{entries:S.data??[],loading:S.isLoading,error:S.error})]})}function U({text:e}){return t.jsx("div",{role:"status",className:"flex items-center gap-2 rounded-xl border border-[var(--otari-line)] bg-[var(--otari-bg)] px-4 py-3 text-sm text-[var(--otari-muted)]",children:e})}function bt({providerHealth:e,healthy:l,total:r,budget:a,errStatus:c,errRate:u,ready:p,failed:n}){if(n)return t.jsx(U,{text:"Some status data could not be loaded."});if(!p)return t.jsx(U,{text:"Checking gateway status…"});const s=[];if((e==="warn"||e==="alert")&&r>0){const o=r-l;s.push({text:`${o} provider${o===1?"":"s"} unreachable`,to:"/providers"})}if(a.overCount>0?s.push({text:`${a.overCount} budget${a.overCount===1?"":"s"} over limit`,to:"/budgets"}):a.nearCount>0&&s.push({text:`${a.nearCount} budget${a.nearCount===1?"":"s"} near limit`,to:"/budgets"}),c==="alert"&&u!==null&&s.push({text:`error rate ${E(u)}`,to:"/activity?status=error"}),s.length===0){const o=r>0?`${l}/${r} providers healthy`:"no providers configured",i=a.cappedCount>0?"budgets within limit":a.label;return t.jsxs("div",{role:"status",className:"flex items-center gap-2 rounded-xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm text-emerald-800",children:[t.jsx("span",{"aria-hidden":!0,children:"✓"}),t.jsxs("span",{children:["All systems normal · ",o," · ",i]})]})}return t.jsxs("div",{role:"alert",className:"flex flex-col gap-2 rounded-xl border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-900 sm:flex-row sm:flex-wrap sm:items-center",children:[t.jsx("span",{className:"font-medium",children:"Needs attention:"}),s.map((o,i)=>t.jsxs("span",{className:"flex items-center gap-2",children:[i>0?t.jsx("span",{"aria-hidden":!0,className:"text-amber-400",children:"·"}):null,t.jsx(W,{to:o.to,className:"underline underline-offset-2 hover:text-amber-950",children:o.text})]},o.to+o.text))]})}function jt(e){return e==="error"?"error":"ok"}function yt({entries:e,loading:l,error:r}){return t.jsxs("div",{className:"flex flex-col gap-3",children:[t.jsxs("div",{className:"flex items-center justify-between",children:[t.jsx("h2",{className:"text-sm font-semibold text-[var(--otari-ink)]",children:"Recent activity"}),t.jsx(W,{to:"/activity",className:"text-sm text-[var(--otari-brand-dark)] hover:underline",children:"View all →"})]}),t.jsx(F,{error:r}),t.jsxs(st,{children:[t.jsx(nt,{children:t.jsxs("tr",{children:[t.jsx(y,{children:"Time"}),t.jsx(y,{children:"Model"}),t.jsx(y,{className:"text-right",children:"Cost"}),t.jsx(y,{children:"Status"})]})}),t.jsx("tbody",{children:l?t.jsx(ot,{colSpan:4}):e.length===0?t.jsx(lt,{colSpan:4,children:"No requests yet. Once the gateway serves traffic, it appears here."}):e.map(a=>t.jsxs(it,{children:[t.jsx(w,{className:"text-[var(--otari-muted)]",children:t.jsx("span",{title:new Date(a.timestamp).toLocaleString(),children:I(a.timestamp)})}),t.jsx(w,{className:"text-[var(--otari-ink)]",children:a.model}),t.jsx(w,{className:"text-right tabular-nums",children:a.cost===null?"—":D(a.cost)}),t.jsx(w,{children:t.jsx("span",{className:`inline-flex items-center rounded-full border px-2 py-0.5 text-xs font-medium ${a.status==="error"?"border-red-200 bg-red-50 text-red-700":"border-[var(--otari-line)] bg-[var(--otari-brand-tint)] text-[var(--otari-brand-dark)]"}`,children:jt(a.status)})})]},a.id))})]})]})}export{Rt as OverviewIndex,gt as OverviewPage};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Format the Overview date as a valid ISO date.

Line 1 uses zero-based getMonth() without padding, so the start_date sent to the usage query can be invalid or misparsed.

Proposed fix
-function B(){const e=new Date;return`${e.getFullYear()}-${e.getMonth()}-${e.getDate()}`}
+function B(){const e=new Date;return`${e.getFullYear()}-${String(e.getMonth()+1).padStart(2,"0")}-${String(e.getDate()).padStart(2,"0")}`}
📝 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
import{j as t}from"./tanstack-query-BPQ56vNC.js";import{f as Q,r as f,N as W}from"./react-DY5rsDwN.js";import{x as z,c as N,K as J,i as X,o as Z,u as tt,a as et,L as b,P as at,E as F,S as v,M as D,N as j,A as k,O as rt,Q as E,R as I}from"./index-DISDvNSb.js";import{T as st,a as nt,b as y,L as ot,c as lt,d as it,e as w}from"./Table-CugMZAuW.js";import"./heroui-B1YjjANo.js";function R(e){return e==="neutral"?void 0:e}const dt=.02,ct=.1;function H(e){if(!e||e.request_count===0)return{rate:null,status:"neutral"};const l=e.error_count/e.request_count,r=l>=ct?"alert":l>=dt?"warn":"ok";return{rate:l,status:r}}function ut(e){return!e||e.total===0?"neutral":e.healthy>=e.total?"ok":e.healthy===0?"alert":"warn"}const xt=.8;function mt(e){if(e.length===0)return{status:"neutral",label:"No budgets configured",overCount:0,nearCount:0,cappedCount:0};const l=e.filter(s=>s.max_budget!==null&&s.user_count>0);if(l.length===0)return{status:"neutral",label:"No capped budgets",overCount:0,nearCount:0,cappedCount:0};let r=0,a=0,c,u=-1;for(const s of l){const o=s.max_budget*s.user_count,i=o>0?s.total_spend/o:0;i>=1?r+=1:i>=xt&&(a+=1),i>u&&(u=i,c={name:s.name??s.budget_id,spent:s.total_spend,allocated:o,pct:i})}const p=r>0?"alert":a>0?"warn":"ok",n=r>0?`${r} over limit`:a>0?`${a} near limit`:"All within budget";return{status:p,label:n,overCount:r,nearCount:a,cappedCount:l.length,worst:c}}const T=864e5,q=30;function B(){const e=new Date;return`${e.getFullYear()}-${e.getMonth()}-${e.getDate()}`}function ht(){const[e,l]=f.useState(B);return f.useEffect(()=>{const r=()=>{if(document.visibilityState==="visible"){const a=B();l(c=>c===a?c:a)}};return document.addEventListener("visibilitychange",r),window.addEventListener("focus",r),()=>{document.removeEventListener("visibilitychange",r),window.removeEventListener("focus",r)}},[]),f.useMemo(()=>{const r=Date.now(),a=new Date(r);return{today:new Date(a.getFullYear(),a.getMonth(),a.getDate()).toISOString(),periodStart:new Date(r-q*T).toISOString(),prevStart:new Date(r-2*q*T).toISOString()}},[e])}const vt={ok:"Healthy",warn:"Elevated",alert:"High"},ft={ok:"All up",warn:"Degraded",alert:"All down"},pt={ok:"On track",warn:"Near limit",alert:"Over budget"};function Rt(){const e=z();return e.isLoading?null:!e.isSuccess||e.data.providers.length===0?t.jsx(Q,{to:"/providers",replace:!0}):t.jsx(gt,{})}function gt(){var O,L,A,P,M;const e=ht(),l=f.useMemo(()=>({start_date:e.today}),[e]),r=f.useMemo(()=>({start_date:e.periodStart}),[e]),a=f.useMemo(()=>({start_date:e.prevStart,end_date:e.periodStart}),[e]),c=N(l,"hour"),u=N(r,"day"),p=N(a,"day"),n=J(),s=X(),o=Z(),i=tt(),S=et({},0,5),C=(O=c.data)==null?void 0:O.totals,d=(L=u.data)==null?void 0:L.totals,x=(A=p.data)==null?void 0:A.totals,m=H(d),$=H(x),K=m.rate!==null&&$.rate!==null?b(m.rate,$.rate):null,h=mt(s.data??[]),g=ut(n.data),Y=(o.data??[]).filter(_=>_.is_active).length,G=(i.data??[]).filter(_=>!_.blocked).length,V=c.error??u.error??n.error??s.error??o.error??i.error;return t.jsxs("div",{className:"flex flex-col gap-6",children:[t.jsx(at,{title:"Overview",description:"At-a-glance spend, traffic, and health across the gateway."}),t.jsx(F,{error:V}),t.jsx(bt,{providerHealth:g,healthy:((P=n.data)==null?void 0:P.healthy)??0,total:((M=n.data)==null?void 0:M.total)??0,budget:h,errStatus:m.status,errRate:m.rate,ready:n.isSuccess&&s.isSuccess&&u.isSuccess,failed:n.isError||s.isError||u.isError}),t.jsxs("div",{className:"grid grid-cols-2 gap-4 sm:grid-cols-3 xl:grid-cols-4",children:[t.jsx(v,{label:"Spend today",value:C?D(C.cost):"—"}),t.jsx(v,{label:"Spend, last 30 days",value:d?D(d.cost):"—",hint:d?t.jsx(j,{fraction:b(d.cost,x==null?void 0:x.cost)}):null}),t.jsx(v,{label:"Requests, last 30 days",value:d?k(d.request_count):"—",hint:d?t.jsx(j,{fraction:b(d.request_count,x==null?void 0:x.request_count)}):null}),t.jsx(v,{label:"Cache reads, last 30 days",value:d?rt(d.cache_read_tokens):"—",hint:d?t.jsx(j,{fraction:b(d.cache_read_tokens,x==null?void 0:x.cache_read_tokens)}):null,to:"/usage"}),t.jsx(v,{label:"Error rate, last 30 days",value:m.rate===null?"—":E(m.rate),status:R(m.status),statusLabel:m.status==="neutral"?void 0:vt[m.status],hint:m.rate!==null?t.jsx(j,{fraction:K}):null}),t.jsx(v,{label:"Budget health",value:s.data&&h.worst?E(h.worst.pct):"—",status:s.data?R(h.status):void 0,statusLabel:s.data&&h.status!=="neutral"?pt[h.status]:void 0,hint:s.data?h.worst?`${h.label} · worst: ${h.worst.name}`:h.label:void 0,to:"/budgets"}),t.jsx(v,{label:"Providers healthy",value:n.data?`${n.data.healthy}/${n.data.total}`:"—",status:n.data?R(g):void 0,statusLabel:n.data&&g!=="neutral"?ft[g]:void 0,hint:n.data?n.data.checked_at?`checked ${I(n.data.checked_at)}`:"not checked yet":void 0,to:"/providers"}),t.jsx(v,{label:"Active keys",value:o.data?k(Y):"—",to:"/keys"}),t.jsx(v,{label:"Active users",value:i.data?k(G):"—",to:"/users"})]}),t.jsx(yt,{entries:S.data??[],loading:S.isLoading,error:S.error})]})}function U({text:e}){return t.jsx("div",{role:"status",className:"flex items-center gap-2 rounded-xl border border-[var(--otari-line)] bg-[var(--otari-bg)] px-4 py-3 text-sm text-[var(--otari-muted)]",children:e})}function bt({providerHealth:e,healthy:l,total:r,budget:a,errStatus:c,errRate:u,ready:p,failed:n}){if(n)return t.jsx(U,{text:"Some status data could not be loaded."});if(!p)return t.jsx(U,{text:"Checking gateway status…"});const s=[];if((e==="warn"||e==="alert")&&r>0){const o=r-l;s.push({text:`${o} provider${o===1?"":"s"} unreachable`,to:"/providers"})}if(a.overCount>0?s.push({text:`${a.overCount} budget${a.overCount===1?"":"s"} over limit`,to:"/budgets"}):a.nearCount>0&&s.push({text:`${a.nearCount} budget${a.nearCount===1?"":"s"} near limit`,to:"/budgets"}),c==="alert"&&u!==null&&s.push({text:`error rate ${E(u)}`,to:"/activity?status=error"}),s.length===0){const o=r>0?`${l}/${r} providers healthy`:"no providers configured",i=a.cappedCount>0?"budgets within limit":a.label;return t.jsxs("div",{role:"status",className:"flex items-center gap-2 rounded-xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm text-emerald-800",children:[t.jsx("span",{"aria-hidden":!0,children:"✓"}),t.jsxs("span",{children:["All systems normal · ",o," · ",i]})]})}return t.jsxs("div",{role:"alert",className:"flex flex-col gap-2 rounded-xl border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-900 sm:flex-row sm:flex-wrap sm:items-center",children:[t.jsx("span",{className:"font-medium",children:"Needs attention:"}),s.map((o,i)=>t.jsxs("span",{className:"flex items-center gap-2",children:[i>0?t.jsx("span",{"aria-hidden":!0,className:"text-amber-400",children:"·"}):null,t.jsx(W,{to:o.to,className:"underline underline-offset-2 hover:text-amber-950",children:o.text})]},o.to+o.text))]})}function jt(e){return e==="error"?"error":"ok"}function yt({entries:e,loading:l,error:r}){return t.jsxs("div",{className:"flex flex-col gap-3",children:[t.jsxs("div",{className:"flex items-center justify-between",children:[t.jsx("h2",{className:"text-sm font-semibold text-[var(--otari-ink)]",children:"Recent activity"}),t.jsx(W,{to:"/activity",className:"text-sm text-[var(--otari-brand-dark)] hover:underline",children:"View all →"})]}),t.jsx(F,{error:r}),t.jsxs(st,{children:[t.jsx(nt,{children:t.jsxs("tr",{children:[t.jsx(y,{children:"Time"}),t.jsx(y,{children:"Model"}),t.jsx(y,{className:"text-right",children:"Cost"}),t.jsx(y,{children:"Status"})]})}),t.jsx("tbody",{children:l?t.jsx(ot,{colSpan:4}):e.length===0?t.jsx(lt,{colSpan:4,children:"No requests yet. Once the gateway serves traffic, it appears here."}):e.map(a=>t.jsxs(it,{children:[t.jsx(w,{className:"text-[var(--otari-muted)]",children:t.jsx("span",{title:new Date(a.timestamp).toLocaleString(),children:I(a.timestamp)})}),t.jsx(w,{className:"text-[var(--otari-ink)]",children:a.model}),t.jsx(w,{className:"text-right tabular-nums",children:a.cost===null?"—":D(a.cost)}),t.jsx(w,{children:t.jsx("span",{className:`inline-flex items-center rounded-full border px-2 py-0.5 text-xs font-medium ${a.status==="error"?"border-red-200 bg-red-50 text-red-700":"border-[var(--otari-line)] bg-[var(--otari-brand-tint)] text-[var(--otari-brand-dark)]"}`,children:jt(a.status)})})]},a.id))})]})]})}export{Rt as OverviewIndex,gt as OverviewPage};
function B(){const e=new Date;return`${e.getFullYear()}-${String(e.getMonth()+1).padStart(2,"0")}-${String(e.getDate()).padStart(2,"0")}`}
🤖 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/OverviewPage-BRQfRYKi.js` at line 1,
Update the B date helper used by ht so it returns a valid ISO calendar date,
using a one-based, zero-padded month and zero-padded day while preserving the
existing local-date behavior. Ensure the resulting start_date passed to the
usage query is consistently formatted as YYYY-MM-DD.

@@ -0,0 +1 @@
import{j as e}from"./tanstack-query-BPQ56vNC.js";import{r as m,L as Z}from"./react-DY5rsDwN.js";import{x as J,T as Q,G as ee,K as te,U as se,V as ae,W as ne,P as re,E as K,C as ie,X as oe,Y as le,R as z,g as M,Z as U,_ as Y,$ as de}from"./index-DISDvNSb.js";import{F as _}from"./Field-DqDcnV5y.js";import{T as ce,a as me,b as S,L as xe,c as ue,d as pe,e as C}from"./Table-CugMZAuW.js";import{B as j,f as H,c as A,S as he,T as ve,L as V,I as W,D as ge,C as E,a as je,b as fe}from"./heroui-B1YjjANo.js";const be={WebkitTextSecurity:"disc"};function D({value:t,onChange:s,label:a,placeholder:n,description:l}){return e.jsxs(ve,{value:t,onChange:s,className:"flex max-w-md flex-col gap-1",children:[e.jsx(V,{className:"text-sm font-medium text-[var(--otari-ink)]",children:a}),e.jsx(W,{type:"text",placeholder:n??"sk-…",autoComplete:"off",autoCorrect:"off",autoCapitalize:"off",spellCheck:!1,"data-1p-ignore":!0,"data-lpignore":"true",style:be}),l?e.jsx(ge,{className:"text-xs text-[var(--otari-muted)]",children:l}):null]})}function G({label:t,value:s,onChange:a,description:n,placeholder:l,extra:o=[],includeCatalog:c=!0}){var k;const g=U(),x=m.useMemo(()=>c?[...o,...(g.data??[]).map(i=>({id:i.id,name:i.name}))]:o,[g.data,o,c]),[h,u]=m.useState(()=>{var i;return((i=x.find(f=>f.id===s))==null?void 0:i.name)??""}),p=((k=x.find(i=>i.id===s))==null?void 0:k.name)??"",v=h.trim()===p.trim()?"":h.trim().toLowerCase(),d=x.filter(i=>!v||i.name.toLowerCase().includes(v)||i.id.toLowerCase().includes(v)).slice(0,50);return e.jsxs(E.Root,{allowsEmptyCollection:!0,menuTrigger:"focus",inputValue:h,onInputChange:u,onSelectionChange:i=>{var f;i!=null?(a(String(i)),u(((f=x.find(P=>P.id===String(i)))==null?void 0:f.name)??"")):(a(""),u(""))},className:"flex max-w-md flex-col gap-1",children:[e.jsx(V,{className:"text-sm font-medium text-[var(--otari-ink)]",children:t}),e.jsxs(E.InputGroup,{children:[e.jsx(W,{placeholder:l??"Search providers…",autoComplete:"off","data-1p-ignore":!0,"data-lpignore":"true",onFocus:i=>i.currentTarget.select()}),e.jsx(E.Trigger,{})]}),e.jsx(E.Popover,{children:e.jsx(je,{items:d,className:"max-h-72 overflow-auto",children:i=>e.jsx(fe,{id:i.id,textValue:i.name,children:i.name})})}),n?e.jsx("span",{className:"text-xs text-[var(--otari-muted)]",children:n}):null]})}function X({getPayload:t}){const s=de(),a=t();return e.jsxs("div",{className:"flex flex-col gap-1.5",children:[e.jsx(j,{variant:"outline",isDisabled:a===null||s.isPending,onPress:()=>{a&&s.mutate(a)},children:s.isPending?"Testing…":"Test connection"}),e.jsx("span",{role:"status","aria-live":"polite",children:s.isPending?null:s.error?e.jsx("span",{className:"text-xs text-red-700",children:M(s.error)}):s.data?s.data.ok?e.jsxs("span",{className:"text-xs font-medium text-green-700",children:["Connected. ",s.data.model_count," model",s.data.model_count===1?"":"s"," available."]}):e.jsx("span",{className:"block max-w-md break-words text-xs text-red-700",children:s.data.error??"Connection failed."}):null})]})}function ye({onClose:t}){var T;const s=U(),a=Y(),[n,l]=m.useState(""),[o,c]=m.useState(""),[g,x]=m.useState(!1),[h,u]=m.useState(""),[p,v]=m.useState(""),d=(T=s.data)==null?void 0:T.find(b=>b.id===n),k=(d==null?void 0:d.env_key_present)??!1,i=((d==null?void 0:d.requires_api_key)??!0)&&!k,f=p.trim()!==""&&p.trim()!==n,P=/[:/]/.test(p),R=n!==""&&!P&&(!i||o.trim()!=="")&&!a.isPending,B=()=>{R&&a.mutate({instance:f?p.trim():n,provider_type:f?n:null,api_base:h.trim()||null,api_key:o.trim()||null},{onSuccess:t})};return e.jsxs("div",{className:"flex flex-col gap-4",children:[e.jsx(K,{error:a.error}),e.jsx(G,{label:"Provider",value:n,onChange:b=>{var w,I;l(b),v(""),u(((I=(w=s.data)==null?void 0:w.find(r=>r.id===b))==null?void 0:I.default_api_base)??"")},description:"Its endpoint is built in."}),e.jsx(D,{value:o,onChange:c,label:d&&!i?"API key (optional)":"API key",description:d?i?`${d.name}'s endpoint is built in — just add your key.`:k?`${d.env_key} is set on the server, so a key is optional here. Paste one to override it.`:`${d.name} needs no API key.`:"Stored encrypted. Requires OTARI_SECRET_KEY on the server."}),e.jsx("button",{type:"button",className:"self-start text-xs font-medium text-[var(--otari-brand-dark)]",onClick:()=>x(b=>!b),children:g?"Hide advanced":"Advanced (API base, rename)"}),g?e.jsxs("div",{className:"grid gap-4 sm:grid-cols-2",children:[e.jsx(_,{label:"API base",value:h,onChange:u,placeholder:(d==null?void 0:d.default_api_base)??"https://…/v1",description:"Only if you route through a proxy. Blank uses the built-in default."}),e.jsx(_,{label:"Name",value:p,onChange:v,placeholder:n||"instance name",description:P?e.jsx("span",{className:"text-red-700",children:"A name cannot contain “:” or “/”."}):"Rename to run two instances of the same provider."})]}):null,e.jsxs("div",{className:"flex flex-wrap items-start gap-2",children:[e.jsx(j,{variant:"primary",isDisabled:!R,onPress:B,children:a.isPending?"Adding…":"Add provider"}),e.jsx(j,{variant:"ghost",onPress:t,children:"Cancel"}),e.jsx(X,{getPayload:()=>n===""?null:{instance:f?p.trim():n,provider_type:f?n:null,api_base:h.trim()||null,api_key:o.trim()||null}})]})]})}function ke({onClose:t}){const s=Y(),[a,n]=m.useState(""),[l,o]=m.useState("openai-compatible"),[c,g]=m.useState(""),[x,h]=m.useState(""),u=/[:/]/.test(a),p=a.trim()!==""&&!u&&c.trim()!==""&&!s.isPending,v=()=>{p&&s.mutate({instance:a.trim(),provider_type:l||"openai-compatible",api_base:c.trim(),api_key:x.trim()||null},{onSuccess:t})};return e.jsxs("div",{className:"flex flex-col gap-4",children:[e.jsx(K,{error:s.error}),e.jsxs("div",{className:"grid gap-4 sm:grid-cols-2",children:[e.jsx(_,{label:"Name",value:a,onChange:n,placeholder:"my-local-llm",isRequired:!0,autoFocus:!0,description:u?e.jsx("span",{className:"text-red-700",children:"A name cannot contain “:” or “/”."}):"Call it whatever you want."}),e.jsx(G,{label:"Compatible with",value:l,onChange:o,includeCatalog:!1,description:"The API this endpoint speaks.",extra:[{id:"openai-compatible",name:"OpenAI"},{id:"anthropic-compatible",name:"Anthropic"}]})]}),e.jsx(_,{label:"API base",value:c,onChange:g,placeholder:"http://localhost:8000/v1",isRequired:!0,description:"The endpoint URL of your server."}),e.jsx(D,{value:x,onChange:h,label:"API key (optional)",description:"Many local backends need none. Stored encrypted."}),e.jsxs("div",{className:"flex flex-wrap items-start gap-2",children:[e.jsx(j,{variant:"primary",isDisabled:!p,onPress:v,children:s.isPending?"Adding…":"Add provider"}),e.jsx(j,{variant:"ghost",onPress:t,children:"Cancel"}),e.jsx(X,{getPayload:()=>a.trim()===""||c.trim()===""?null:{instance:a.trim(),provider_type:l||"openai-compatible",api_base:c.trim(),api_key:x.trim()||null}})]})]})}function Pe({onClose:t}){const[s,a]=m.useState("known");return e.jsx(A,{children:e.jsxs(A.Content,{className:"flex flex-col gap-4 p-5",children:[e.jsx("div",{className:"flex items-center justify-between",children:e.jsx("div",{className:"flex items-center gap-1 rounded-lg bg-[var(--otari-bg)] p-1",children:[["known","Known provider"],["custom","Custom endpoint"]].map(([n,l])=>e.jsx("button",{type:"button","aria-pressed":s===n,onClick:()=>a(n),className:s===n?"rounded-md bg-white px-3 py-1.5 text-sm font-medium text-[var(--otari-ink)] shadow-sm":"rounded-md px-3 py-1.5 text-sm text-[var(--otari-muted)] hover:text-[var(--otari-ink)]",children:l},n))})}),s==="known"?e.jsx(ye,{onClose:t}):e.jsx(ke,{onClose:t})]})})}function Ne({provider:t,onClose:s}){const a=oe(),[n,l]=m.useState(t.provider_type??""),[o,c]=m.useState(t.api_base??""),[g,x]=m.useState(!1),[h,u]=m.useState(""),p=()=>{if(a.isPending)return;const v={provider_type:n.trim()||null,api_base:o.trim()||null,expected_updated_at:t.updated_at};g&&h.trim()&&(v.api_key=h.trim()),a.mutate({instance:t.instance,body:v},{onSuccess:s})};return e.jsx(A,{children:e.jsxs(A.Content,{className:"flex flex-col gap-4 p-5",children:[e.jsxs("div",{className:"text-sm font-semibold text-[var(--otari-ink)]",children:["Edit ",e.jsx("code",{children:t.instance})]}),e.jsx(K,{error:a.error}),e.jsxs("div",{className:"grid gap-4 sm:grid-cols-2",children:[e.jsx(_,{label:"Provider type",value:n,onChange:l,placeholder:"openai"}),e.jsx(_,{label:"API base",value:o,onChange:c,placeholder:"https://api.openai.com/v1"})]}),e.jsx("div",{className:"flex flex-col gap-2",children:g?e.jsxs(e.Fragment,{children:[e.jsx(D,{value:h,onChange:u,label:"New API key",description:"Stored encrypted. The old key is replaced when you save."}),e.jsx("button",{type:"button",className:"self-start text-xs font-medium text-[var(--otari-brand-dark)]",onClick:()=>{x(!1),u("")},children:"Keep the current key"})]}):e.jsxs("div",{className:"flex items-center gap-3",children:[e.jsxs("span",{className:"text-sm text-[var(--otari-muted)]",children:["API key: ",e.jsx("code",{children:t.last4?`••••${t.last4}`:"none set"})]}),e.jsx(j,{size:"sm",variant:"outline",onPress:()=>x(!0),children:"Replace key"})]})}),e.jsxs("div",{className:"flex gap-2",children:[e.jsx(j,{variant:"primary",isDisabled:a.isPending,onPress:p,children:a.isPending?"Saving…":"Save changes"}),e.jsx(j,{variant:"ghost",onPress:s,children:"Cancel"})]})]})})}function Se(t,s){const a=new Map((s??[]).map(o=>[o.instance,o])),n=new Map((t??[]).map(o=>[o.instance,o]));return[...new Set([...a.keys(),...n.keys()])].sort().map(o=>{const c=a.get(o);return{instance:o,source:c?"stored":"config",stored:c,meta:n.get(o)}})}function Ce({state:t}){return t?t.status==="pending"?e.jsxs("span",{className:"inline-flex items-center gap-1.5 text-xs text-[var(--otari-muted)]",children:[e.jsx(he,{size:"sm"})," Testing…"]}):t.ok?e.jsxs("span",{className:"text-xs font-medium text-green-700",children:["Connected. ",t.model_count," model",t.model_count===1?"":"s"," available."]}):e.jsx("span",{className:"block max-w-xs break-words text-xs text-red-700",children:t.error??"Connection failed."}):null}function _e({health:t}){if(!t)return e.jsx("span",{className:"text-xs text-[var(--otari-muted)]",children:"—"});const s=t.ok?"border-green-200 bg-green-50 text-green-700":"border-red-200 bg-red-50 text-red-700",a=t.checked_at?`Last checked ${z(t.checked_at)}`:"Not checked yet",n=t.ok?a:`${t.error??"Unreachable"} · ${a}`;return e.jsxs("span",{title:n,className:`inline-flex items-center gap-1.5 rounded-full border px-2.5 py-0.5 text-xs font-medium ${s}`,children:[e.jsx("span",{"aria-hidden":!0,className:`h-1.5 w-1.5 rounded-full ${t.ok?"bg-green-500":"bg-red-500"}`}),t.ok?"Reachable":"Unreachable"]})}function Ae({healthy:t,total:s,checkedAt:a}){const n=t===s,l=le();return e.jsxs("div",{className:"flex flex-wrap items-center gap-3 rounded-xl border border-[var(--otari-line)] bg-[var(--otari-surface)] px-4 py-2.5 text-sm",children:[e.jsx("span",{"aria-hidden":!0,className:`h-2 w-2 rounded-full ${n?"bg-green-500":"bg-red-500"}`}),e.jsxs("span",{className:"font-medium text-[var(--otari-ink)]",children:[t," of ",s," provider",s===1?"":"s"," reachable"]}),a?e.jsxs("span",{className:"text-[var(--otari-muted)]",children:["Last checked ",z(a)]}):null,e.jsx(j,{size:"sm",variant:"ghost",className:"ml-auto",isDisabled:l.isPending,onPress:()=>l.mutate(),children:l.isPending?"Re-checking…":"Re-check all"})]})}function L({n:t,title:s,children:a}){return e.jsxs("li",{className:"flex gap-3",children:[e.jsx("span",{className:"flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-[var(--otari-brand-tint)] text-xs font-semibold text-[var(--otari-brand-dark)]",children:t}),e.jsxs("div",{className:"text-sm",children:[e.jsx("div",{className:"font-medium text-[var(--otari-ink)]",children:s}),e.jsx("div",{className:"text-[var(--otari-muted)]",children:a})]})]})}function Te({onAddProvider:t,needsPricing:s,onEnablePricing:a,enabling:n}){return e.jsx(A,{children:e.jsxs(A.Content,{className:"flex flex-col gap-4 p-6",children:[e.jsxs("div",{children:[e.jsx("h2",{className:"text-lg font-semibold text-[var(--otari-ink)]",children:"Welcome to Otari"}),e.jsx("p",{className:"mt-1 text-sm text-[var(--otari-muted)]",children:"You are signed in. Add a provider to start serving models: three quick steps."})]}),e.jsxs("ol",{className:"flex flex-col gap-3",children:[e.jsxs(L,{n:1,title:"Add a provider",children:["Enter a provider name (like ",e.jsx("code",{children:"openai"}),") and its API key. Keys are encrypted at rest."]}),e.jsxs(L,{n:2,title:"Test the connection",children:["Use ",e.jsx("strong",{children:"Test"})," on the provider row to confirm the key works and see how many models it serves."]}),e.jsxs(L,{n:3,title:"Send your first request",children:["Point your app at ",e.jsx("code",{children:"/v1"})," on this gateway with the API key printed in the server logs (",e.jsx("code",{children:"gw-…"}),"). See the"," ",e.jsx("a",{href:"/welcome",className:"font-medium text-[var(--otari-brand-dark)]",children:"quickstart"}),"."]})]}),s?e.jsxs("p",{className:"text-sm text-[var(--otari-muted)]",children:["Tip: ",e.jsx("code",{children:"require_pricing"})," is on, so requests are rejected until pricing is set."," ",e.jsx("button",{type:"button",className:"font-medium text-[var(--otari-brand-dark)] disabled:opacity-50",disabled:n,onClick:a,children:"Enable default pricing"})," ","to meter new models with public rates."]}):null,e.jsx("div",{children:e.jsx(j,{variant:"primary",onPress:t,children:"Add your first provider"})})]})})}function Le(){var T,b,w,I;const t=J(),s=Q(),a=ee(),n=te(),l=se(),o=ae(),c=ne(),[g,x]=m.useState(!1),[h,u]=m.useState(null),[p,v]=m.useState({}),d=Se((T=t.data)==null?void 0:T.providers,s.data),k=new Map((((b=n.data)==null?void 0:b.providers)??[]).map(r=>[r.instance,r])),i=t.isLoading||s.isLoading,f=((w=s.data)==null?void 0:w.find(r=>r.instance===h))??null,P=((I=a.data)==null?void 0:I.require_pricing)===!0&&a.data.default_pricing===!1,R=!i&&d.length===0,B=r=>{v(y=>({...y,[r]:{status:"pending"}})),o.mutate(r,{onSuccess:y=>v(N=>({...N,[r]:{status:"done",...y}})),onError:y=>v(N=>({...N,[r]:{status:"done",ok:!1,model_count:0,error:M(y)}}))})};return e.jsxs("div",{className:"flex flex-col gap-6",children:[e.jsx(re,{title:"Providers",description:"Add provider API keys here to serve models without editing config.yml. Keys are encrypted at rest.",action:g?null:e.jsx(j,{variant:"primary",onPress:()=>{u(null),x(!0)},children:"Add provider"})}),e.jsx(K,{error:t.error??s.error??a.error??n.error??c.error??l.error}),R?e.jsx(Te,{onAddProvider:()=>{u(null),x(!0)},needsPricing:P,onEnablePricing:()=>c.mutate({default_pricing:!0}),enabling:c.isPending}):null,g?e.jsx(Pe,{onClose:()=>x(!1)}):null,f?e.jsx(Ne,{provider:f,onClose:()=>u(null)}):null,!i&&d.length>0&&n.data&&n.data.total>0?e.jsx(Ae,{healthy:n.data.healthy,total:n.data.total,checkedAt:n.data.checked_at}):null,e.jsxs(ce,{children:[e.jsx(me,{children:e.jsxs("tr",{children:[e.jsx(S,{children:"Provider"}),e.jsx(S,{children:"Type"}),e.jsx(S,{children:"Source"}),e.jsx(S,{children:"API key"}),e.jsx(S,{children:"Status"}),e.jsx(S,{className:"text-right",children:"Actions"})]})}),e.jsx("tbody",{children:i?e.jsx(xe,{colSpan:6}):d.length===0?e.jsx(ue,{colSpan:6,children:"No providers yet. Add your first provider to start serving models."}):d.map(r=>{var y,N,$,q,F,O;return e.jsxs(pe,{children:[e.jsx(C,{className:"font-medium",children:e.jsx(Z,{to:`/models?provider=${encodeURIComponent(r.instance)}`,className:"text-[var(--otari-ink)] hover:text-[var(--otari-brand-dark)] hover:underline",children:r.instance})}),e.jsx(C,{className:"text-[var(--otari-muted)]",children:((y=r.meta)==null?void 0:y.provider_type)??((N=r.stored)==null?void 0:N.provider_type)??r.instance}),e.jsx(C,{children:r.source==="stored"?e.jsx(H,{size:"sm",color:"accent",children:"stored"}):e.jsx(H,{size:"sm",color:"default",children:"config"})}),e.jsx(C,{className:"text-[var(--otari-muted)]",children:r.source==="stored"?r.stored&&!r.stored.decryptable?e.jsx("span",{className:"text-amber-700",title:"This key can't be decrypted with the current OTARI_SECRET_KEY. Replace the key, or restore the original OTARI_SECRET_KEY.",children:"⚠ key unreadable"}):e.jsx("code",{children:($=r.stored)!=null&&$.last4?`••••${r.stored.last4}`:"none set"}):(q=r.meta)!=null&&q.env_key?e.jsxs("span",{children:["via ",e.jsx("code",{children:r.meta.env_key})]}):"config.yml"}),e.jsx(C,{children:e.jsx(_e,{health:k.get(r.instance)})}),e.jsx(C,{children:r.source==="stored"?e.jsxs("div",{className:"flex flex-col items-end gap-1.5",children:[e.jsxs("div",{className:"flex items-center gap-1.5",children:[e.jsx(j,{size:"sm",variant:"outline",isDisabled:((F=p[r.instance])==null?void 0:F.status)==="pending"||((O=r.stored)==null?void 0:O.decryptable)===!1,onPress:()=>B(r.instance),children:"Test"}),e.jsx(j,{size:"sm",variant:"ghost",onPress:()=>{x(!1),u(r.instance)},children:"Edit"}),e.jsx(ie,{confirmLabel:"Delete",isPending:l.isPending,onConfirm:()=>l.mutate(r.instance),children:"Delete"})]}),e.jsx(Ce,{state:p[r.instance]})]}):e.jsx("span",{className:"block text-right text-xs text-[var(--otari-muted)]",children:"managed in config.yml"})})]},r.instance)})})]})]})}export{Le as ProvidersPage};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Use a password input for API keys.

Line 1 renders the API-key field as type="text" and relies on WebkitTextSecurity, which is not cross-browser masking. In browsers that ignore it, users’ keys are visible while typing.

Proposed fix
- e.jsx(W,{type:"text", ..., style:be})
+ e.jsx(W,{type:"password", ...})
📝 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
import{j as e}from"./tanstack-query-BPQ56vNC.js";import{r as m,L as Z}from"./react-DY5rsDwN.js";import{x as J,T as Q,G as ee,K as te,U as se,V as ae,W as ne,P as re,E as K,C as ie,X as oe,Y as le,R as z,g as M,Z as U,_ as Y,$ as de}from"./index-DISDvNSb.js";import{F as _}from"./Field-DqDcnV5y.js";import{T as ce,a as me,b as S,L as xe,c as ue,d as pe,e as C}from"./Table-CugMZAuW.js";import{B as j,f as H,c as A,S as he,T as ve,L as V,I as W,D as ge,C as E,a as je,b as fe}from"./heroui-B1YjjANo.js";const be={WebkitTextSecurity:"disc"};function D({value:t,onChange:s,label:a,placeholder:n,description:l}){return e.jsxs(ve,{value:t,onChange:s,className:"flex max-w-md flex-col gap-1",children:[e.jsx(V,{className:"text-sm font-medium text-[var(--otari-ink)]",children:a}),e.jsx(W,{type:"text",placeholder:n??"sk-…",autoComplete:"off",autoCorrect:"off",autoCapitalize:"off",spellCheck:!1,"data-1p-ignore":!0,"data-lpignore":"true",style:be}),l?e.jsx(ge,{className:"text-xs text-[var(--otari-muted)]",children:l}):null]})}function G({label:t,value:s,onChange:a,description:n,placeholder:l,extra:o=[],includeCatalog:c=!0}){var k;const g=U(),x=m.useMemo(()=>c?[...o,...(g.data??[]).map(i=>({id:i.id,name:i.name}))]:o,[g.data,o,c]),[h,u]=m.useState(()=>{var i;return((i=x.find(f=>f.id===s))==null?void 0:i.name)??""}),p=((k=x.find(i=>i.id===s))==null?void 0:k.name)??"",v=h.trim()===p.trim()?"":h.trim().toLowerCase(),d=x.filter(i=>!v||i.name.toLowerCase().includes(v)||i.id.toLowerCase().includes(v)).slice(0,50);return e.jsxs(E.Root,{allowsEmptyCollection:!0,menuTrigger:"focus",inputValue:h,onInputChange:u,onSelectionChange:i=>{var f;i!=null?(a(String(i)),u(((f=x.find(P=>P.id===String(i)))==null?void 0:f.name)??"")):(a(""),u(""))},className:"flex max-w-md flex-col gap-1",children:[e.jsx(V,{className:"text-sm font-medium text-[var(--otari-ink)]",children:t}),e.jsxs(E.InputGroup,{children:[e.jsx(W,{placeholder:l??"Search providers…",autoComplete:"off","data-1p-ignore":!0,"data-lpignore":"true",onFocus:i=>i.currentTarget.select()}),e.jsx(E.Trigger,{})]}),e.jsx(E.Popover,{children:e.jsx(je,{items:d,className:"max-h-72 overflow-auto",children:i=>e.jsx(fe,{id:i.id,textValue:i.name,children:i.name})})}),n?e.jsx("span",{className:"text-xs text-[var(--otari-muted)]",children:n}):null]})}function X({getPayload:t}){const s=de(),a=t();return e.jsxs("div",{className:"flex flex-col gap-1.5",children:[e.jsx(j,{variant:"outline",isDisabled:a===null||s.isPending,onPress:()=>{a&&s.mutate(a)},children:s.isPending?"Testing…":"Test connection"}),e.jsx("span",{role:"status","aria-live":"polite",children:s.isPending?null:s.error?e.jsx("span",{className:"text-xs text-red-700",children:M(s.error)}):s.data?s.data.ok?e.jsxs("span",{className:"text-xs font-medium text-green-700",children:["Connected. ",s.data.model_count," model",s.data.model_count===1?"":"s"," available."]}):e.jsx("span",{className:"block max-w-md break-words text-xs text-red-700",children:s.data.error??"Connection failed."}):null})]})}function ye({onClose:t}){var T;const s=U(),a=Y(),[n,l]=m.useState(""),[o,c]=m.useState(""),[g,x]=m.useState(!1),[h,u]=m.useState(""),[p,v]=m.useState(""),d=(T=s.data)==null?void 0:T.find(b=>b.id===n),k=(d==null?void 0:d.env_key_present)??!1,i=((d==null?void 0:d.requires_api_key)??!0)&&!k,f=p.trim()!==""&&p.trim()!==n,P=/[:/]/.test(p),R=n!==""&&!P&&(!i||o.trim()!=="")&&!a.isPending,B=()=>{R&&a.mutate({instance:f?p.trim():n,provider_type:f?n:null,api_base:h.trim()||null,api_key:o.trim()||null},{onSuccess:t})};return e.jsxs("div",{className:"flex flex-col gap-4",children:[e.jsx(K,{error:a.error}),e.jsx(G,{label:"Provider",value:n,onChange:b=>{var w,I;l(b),v(""),u(((I=(w=s.data)==null?void 0:w.find(r=>r.id===b))==null?void 0:I.default_api_base)??"")},description:"Its endpoint is built in."}),e.jsx(D,{value:o,onChange:c,label:d&&!i?"API key (optional)":"API key",description:d?i?`${d.name}'s endpoint is built in — just add your key.`:k?`${d.env_key} is set on the server, so a key is optional here. Paste one to override it.`:`${d.name} needs no API key.`:"Stored encrypted. Requires OTARI_SECRET_KEY on the server."}),e.jsx("button",{type:"button",className:"self-start text-xs font-medium text-[var(--otari-brand-dark)]",onClick:()=>x(b=>!b),children:g?"Hide advanced":"Advanced (API base, rename)"}),g?e.jsxs("div",{className:"grid gap-4 sm:grid-cols-2",children:[e.jsx(_,{label:"API base",value:h,onChange:u,placeholder:(d==null?void 0:d.default_api_base)??"https://…/v1",description:"Only if you route through a proxy. Blank uses the built-in default."}),e.jsx(_,{label:"Name",value:p,onChange:v,placeholder:n||"instance name",description:P?e.jsx("span",{className:"text-red-700",children:"A name cannot contain “:” or “/”."}):"Rename to run two instances of the same provider."})]}):null,e.jsxs("div",{className:"flex flex-wrap items-start gap-2",children:[e.jsx(j,{variant:"primary",isDisabled:!R,onPress:B,children:a.isPending?"Adding…":"Add provider"}),e.jsx(j,{variant:"ghost",onPress:t,children:"Cancel"}),e.jsx(X,{getPayload:()=>n===""?null:{instance:f?p.trim():n,provider_type:f?n:null,api_base:h.trim()||null,api_key:o.trim()||null}})]})]})}function ke({onClose:t}){const s=Y(),[a,n]=m.useState(""),[l,o]=m.useState("openai-compatible"),[c,g]=m.useState(""),[x,h]=m.useState(""),u=/[:/]/.test(a),p=a.trim()!==""&&!u&&c.trim()!==""&&!s.isPending,v=()=>{p&&s.mutate({instance:a.trim(),provider_type:l||"openai-compatible",api_base:c.trim(),api_key:x.trim()||null},{onSuccess:t})};return e.jsxs("div",{className:"flex flex-col gap-4",children:[e.jsx(K,{error:s.error}),e.jsxs("div",{className:"grid gap-4 sm:grid-cols-2",children:[e.jsx(_,{label:"Name",value:a,onChange:n,placeholder:"my-local-llm",isRequired:!0,autoFocus:!0,description:u?e.jsx("span",{className:"text-red-700",children:"A name cannot contain “:” or “/”."}):"Call it whatever you want."}),e.jsx(G,{label:"Compatible with",value:l,onChange:o,includeCatalog:!1,description:"The API this endpoint speaks.",extra:[{id:"openai-compatible",name:"OpenAI"},{id:"anthropic-compatible",name:"Anthropic"}]})]}),e.jsx(_,{label:"API base",value:c,onChange:g,placeholder:"http://localhost:8000/v1",isRequired:!0,description:"The endpoint URL of your server."}),e.jsx(D,{value:x,onChange:h,label:"API key (optional)",description:"Many local backends need none. Stored encrypted."}),e.jsxs("div",{className:"flex flex-wrap items-start gap-2",children:[e.jsx(j,{variant:"primary",isDisabled:!p,onPress:v,children:s.isPending?"Adding…":"Add provider"}),e.jsx(j,{variant:"ghost",onPress:t,children:"Cancel"}),e.jsx(X,{getPayload:()=>a.trim()===""||c.trim()===""?null:{instance:a.trim(),provider_type:l||"openai-compatible",api_base:c.trim(),api_key:x.trim()||null}})]})]})}function Pe({onClose:t}){const[s,a]=m.useState("known");return e.jsx(A,{children:e.jsxs(A.Content,{className:"flex flex-col gap-4 p-5",children:[e.jsx("div",{className:"flex items-center justify-between",children:e.jsx("div",{className:"flex items-center gap-1 rounded-lg bg-[var(--otari-bg)] p-1",children:[["known","Known provider"],["custom","Custom endpoint"]].map(([n,l])=>e.jsx("button",{type:"button","aria-pressed":s===n,onClick:()=>a(n),className:s===n?"rounded-md bg-white px-3 py-1.5 text-sm font-medium text-[var(--otari-ink)] shadow-sm":"rounded-md px-3 py-1.5 text-sm text-[var(--otari-muted)] hover:text-[var(--otari-ink)]",children:l},n))})}),s==="known"?e.jsx(ye,{onClose:t}):e.jsx(ke,{onClose:t})]})})}function Ne({provider:t,onClose:s}){const a=oe(),[n,l]=m.useState(t.provider_type??""),[o,c]=m.useState(t.api_base??""),[g,x]=m.useState(!1),[h,u]=m.useState(""),p=()=>{if(a.isPending)return;const v={provider_type:n.trim()||null,api_base:o.trim()||null,expected_updated_at:t.updated_at};g&&h.trim()&&(v.api_key=h.trim()),a.mutate({instance:t.instance,body:v},{onSuccess:s})};return e.jsx(A,{children:e.jsxs(A.Content,{className:"flex flex-col gap-4 p-5",children:[e.jsxs("div",{className:"text-sm font-semibold text-[var(--otari-ink)]",children:["Edit ",e.jsx("code",{children:t.instance})]}),e.jsx(K,{error:a.error}),e.jsxs("div",{className:"grid gap-4 sm:grid-cols-2",children:[e.jsx(_,{label:"Provider type",value:n,onChange:l,placeholder:"openai"}),e.jsx(_,{label:"API base",value:o,onChange:c,placeholder:"https://api.openai.com/v1"})]}),e.jsx("div",{className:"flex flex-col gap-2",children:g?e.jsxs(e.Fragment,{children:[e.jsx(D,{value:h,onChange:u,label:"New API key",description:"Stored encrypted. The old key is replaced when you save."}),e.jsx("button",{type:"button",className:"self-start text-xs font-medium text-[var(--otari-brand-dark)]",onClick:()=>{x(!1),u("")},children:"Keep the current key"})]}):e.jsxs("div",{className:"flex items-center gap-3",children:[e.jsxs("span",{className:"text-sm text-[var(--otari-muted)]",children:["API key: ",e.jsx("code",{children:t.last4?`••••${t.last4}`:"none set"})]}),e.jsx(j,{size:"sm",variant:"outline",onPress:()=>x(!0),children:"Replace key"})]})}),e.jsxs("div",{className:"flex gap-2",children:[e.jsx(j,{variant:"primary",isDisabled:a.isPending,onPress:p,children:a.isPending?"Saving…":"Save changes"}),e.jsx(j,{variant:"ghost",onPress:s,children:"Cancel"})]})]})})}function Se(t,s){const a=new Map((s??[]).map(o=>[o.instance,o])),n=new Map((t??[]).map(o=>[o.instance,o]));return[...new Set([...a.keys(),...n.keys()])].sort().map(o=>{const c=a.get(o);return{instance:o,source:c?"stored":"config",stored:c,meta:n.get(o)}})}function Ce({state:t}){return t?t.status==="pending"?e.jsxs("span",{className:"inline-flex items-center gap-1.5 text-xs text-[var(--otari-muted)]",children:[e.jsx(he,{size:"sm"})," Testing…"]}):t.ok?e.jsxs("span",{className:"text-xs font-medium text-green-700",children:["Connected. ",t.model_count," model",t.model_count===1?"":"s"," available."]}):e.jsx("span",{className:"block max-w-xs break-words text-xs text-red-700",children:t.error??"Connection failed."}):null}function _e({health:t}){if(!t)return e.jsx("span",{className:"text-xs text-[var(--otari-muted)]",children:"—"});const s=t.ok?"border-green-200 bg-green-50 text-green-700":"border-red-200 bg-red-50 text-red-700",a=t.checked_at?`Last checked ${z(t.checked_at)}`:"Not checked yet",n=t.ok?a:`${t.error??"Unreachable"} · ${a}`;return e.jsxs("span",{title:n,className:`inline-flex items-center gap-1.5 rounded-full border px-2.5 py-0.5 text-xs font-medium ${s}`,children:[e.jsx("span",{"aria-hidden":!0,className:`h-1.5 w-1.5 rounded-full ${t.ok?"bg-green-500":"bg-red-500"}`}),t.ok?"Reachable":"Unreachable"]})}function Ae({healthy:t,total:s,checkedAt:a}){const n=t===s,l=le();return e.jsxs("div",{className:"flex flex-wrap items-center gap-3 rounded-xl border border-[var(--otari-line)] bg-[var(--otari-surface)] px-4 py-2.5 text-sm",children:[e.jsx("span",{"aria-hidden":!0,className:`h-2 w-2 rounded-full ${n?"bg-green-500":"bg-red-500"}`}),e.jsxs("span",{className:"font-medium text-[var(--otari-ink)]",children:[t," of ",s," provider",s===1?"":"s"," reachable"]}),a?e.jsxs("span",{className:"text-[var(--otari-muted)]",children:["Last checked ",z(a)]}):null,e.jsx(j,{size:"sm",variant:"ghost",className:"ml-auto",isDisabled:l.isPending,onPress:()=>l.mutate(),children:l.isPending?"Re-checking…":"Re-check all"})]})}function L({n:t,title:s,children:a}){return e.jsxs("li",{className:"flex gap-3",children:[e.jsx("span",{className:"flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-[var(--otari-brand-tint)] text-xs font-semibold text-[var(--otari-brand-dark)]",children:t}),e.jsxs("div",{className:"text-sm",children:[e.jsx("div",{className:"font-medium text-[var(--otari-ink)]",children:s}),e.jsx("div",{className:"text-[var(--otari-muted)]",children:a})]})]})}function Te({onAddProvider:t,needsPricing:s,onEnablePricing:a,enabling:n}){return e.jsx(A,{children:e.jsxs(A.Content,{className:"flex flex-col gap-4 p-6",children:[e.jsxs("div",{children:[e.jsx("h2",{className:"text-lg font-semibold text-[var(--otari-ink)]",children:"Welcome to Otari"}),e.jsx("p",{className:"mt-1 text-sm text-[var(--otari-muted)]",children:"You are signed in. Add a provider to start serving models: three quick steps."})]}),e.jsxs("ol",{className:"flex flex-col gap-3",children:[e.jsxs(L,{n:1,title:"Add a provider",children:["Enter a provider name (like ",e.jsx("code",{children:"openai"}),") and its API key. Keys are encrypted at rest."]}),e.jsxs(L,{n:2,title:"Test the connection",children:["Use ",e.jsx("strong",{children:"Test"})," on the provider row to confirm the key works and see how many models it serves."]}),e.jsxs(L,{n:3,title:"Send your first request",children:["Point your app at ",e.jsx("code",{children:"/v1"})," on this gateway with the API key printed in the server logs (",e.jsx("code",{children:"gw-…"}),"). See the"," ",e.jsx("a",{href:"/welcome",className:"font-medium text-[var(--otari-brand-dark)]",children:"quickstart"}),"."]})]}),s?e.jsxs("p",{className:"text-sm text-[var(--otari-muted)]",children:["Tip: ",e.jsx("code",{children:"require_pricing"})," is on, so requests are rejected until pricing is set."," ",e.jsx("button",{type:"button",className:"font-medium text-[var(--otari-brand-dark)] disabled:opacity-50",disabled:n,onClick:a,children:"Enable default pricing"})," ","to meter new models with public rates."]}):null,e.jsx("div",{children:e.jsx(j,{variant:"primary",onPress:t,children:"Add your first provider"})})]})})}function Le(){var T,b,w,I;const t=J(),s=Q(),a=ee(),n=te(),l=se(),o=ae(),c=ne(),[g,x]=m.useState(!1),[h,u]=m.useState(null),[p,v]=m.useState({}),d=Se((T=t.data)==null?void 0:T.providers,s.data),k=new Map((((b=n.data)==null?void 0:b.providers)??[]).map(r=>[r.instance,r])),i=t.isLoading||s.isLoading,f=((w=s.data)==null?void 0:w.find(r=>r.instance===h))??null,P=((I=a.data)==null?void 0:I.require_pricing)===!0&&a.data.default_pricing===!1,R=!i&&d.length===0,B=r=>{v(y=>({...y,[r]:{status:"pending"}})),o.mutate(r,{onSuccess:y=>v(N=>({...N,[r]:{status:"done",...y}})),onError:y=>v(N=>({...N,[r]:{status:"done",ok:!1,model_count:0,error:M(y)}}))})};return e.jsxs("div",{className:"flex flex-col gap-6",children:[e.jsx(re,{title:"Providers",description:"Add provider API keys here to serve models without editing config.yml. Keys are encrypted at rest.",action:g?null:e.jsx(j,{variant:"primary",onPress:()=>{u(null),x(!0)},children:"Add provider"})}),e.jsx(K,{error:t.error??s.error??a.error??n.error??c.error??l.error}),R?e.jsx(Te,{onAddProvider:()=>{u(null),x(!0)},needsPricing:P,onEnablePricing:()=>c.mutate({default_pricing:!0}),enabling:c.isPending}):null,g?e.jsx(Pe,{onClose:()=>x(!1)}):null,f?e.jsx(Ne,{provider:f,onClose:()=>u(null)}):null,!i&&d.length>0&&n.data&&n.data.total>0?e.jsx(Ae,{healthy:n.data.healthy,total:n.data.total,checkedAt:n.data.checked_at}):null,e.jsxs(ce,{children:[e.jsx(me,{children:e.jsxs("tr",{children:[e.jsx(S,{children:"Provider"}),e.jsx(S,{children:"Type"}),e.jsx(S,{children:"Source"}),e.jsx(S,{children:"API key"}),e.jsx(S,{children:"Status"}),e.jsx(S,{className:"text-right",children:"Actions"})]})}),e.jsx("tbody",{children:i?e.jsx(xe,{colSpan:6}):d.length===0?e.jsx(ue,{colSpan:6,children:"No providers yet. Add your first provider to start serving models."}):d.map(r=>{var y,N,$,q,F,O;return e.jsxs(pe,{children:[e.jsx(C,{className:"font-medium",children:e.jsx(Z,{to:`/models?provider=${encodeURIComponent(r.instance)}`,className:"text-[var(--otari-ink)] hover:text-[var(--otari-brand-dark)] hover:underline",children:r.instance})}),e.jsx(C,{className:"text-[var(--otari-muted)]",children:((y=r.meta)==null?void 0:y.provider_type)??((N=r.stored)==null?void 0:N.provider_type)??r.instance}),e.jsx(C,{children:r.source==="stored"?e.jsx(H,{size:"sm",color:"accent",children:"stored"}):e.jsx(H,{size:"sm",color:"default",children:"config"})}),e.jsx(C,{className:"text-[var(--otari-muted)]",children:r.source==="stored"?r.stored&&!r.stored.decryptable?e.jsx("span",{className:"text-amber-700",title:"This key can't be decrypted with the current OTARI_SECRET_KEY. Replace the key, or restore the original OTARI_SECRET_KEY.",children:"⚠ key unreadable"}):e.jsx("code",{children:($=r.stored)!=null&&$.last4?`••••${r.stored.last4}`:"none set"}):(q=r.meta)!=null&&q.env_key?e.jsxs("span",{children:["via ",e.jsx("code",{children:r.meta.env_key})]}):"config.yml"}),e.jsx(C,{children:e.jsx(_e,{health:k.get(r.instance)})}),e.jsx(C,{children:r.source==="stored"?e.jsxs("div",{className:"flex flex-col items-end gap-1.5",children:[e.jsxs("div",{className:"flex items-center gap-1.5",children:[e.jsx(j,{size:"sm",variant:"outline",isDisabled:((F=p[r.instance])==null?void 0:F.status)==="pending"||((O=r.stored)==null?void 0:O.decryptable)===!1,onPress:()=>B(r.instance),children:"Test"}),e.jsx(j,{size:"sm",variant:"ghost",onPress:()=>{x(!1),u(r.instance)},children:"Edit"}),e.jsx(ie,{confirmLabel:"Delete",isPending:l.isPending,onConfirm:()=>l.mutate(r.instance),children:"Delete"})]}),e.jsx(Ce,{state:p[r.instance]})]}):e.jsx("span",{className:"block text-right text-xs text-[var(--otari-muted)]",children:"managed in config.yml"})})]},r.instance)})})]})]})}export{Le as ProvidersPage};
const be={WebkitTextSecurity:"disc"};function D({value:t,onChange:s,label:a,placeholder:n,description:l}){return e.jsxs(ve,{value:t,onChange:s,className:"flex max-w-md flex-col gap-1",children:[e.jsx(V,{className:"text-sm font-medium text-[var(--otari-ink)]",children:a}),e.jsx(W,{type:"password",placeholder:n??"sk-…",autoComplete:"off",autoCorrect:"off",autoCapitalize:"off",spellCheck:!1,"data-1p-ignore":!0,"data-lpignore":"true"}),l?e.jsx(ge,{className:"text-xs text-[var(--otari-muted)]",children:l}):null]})}
🤖 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/ProvidersPage-Y91J0S4G.js` at line 1,
Update the API-key field rendered by the D component to use the native password
input type instead of type="text" with WebkitTextSecurity. Preserve the existing
autocomplete, privacy, value, and change-handling behavior while ensuring
masking works across browsers.

Handle the dashboard defects CodeRabbit found in regenerated route bundles and keep shared runtime code cacheable.

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

🧹 Nitpick comments (1)
src/gateway/static/dashboard/assets/Table-DEsIhjZo.js (1)

1-1: 🧹 Nitpick | 🔵 Trivial | 💤 Low value

Column resize is pointer-only — consider a keyboard fallback.

The resize handle (b) wires up onPointerDown/Move/Up/Cancel but there's no keyboard-driven way to adjust column widths (e.g., arrow keys while the handle is focused). Not blocking since it's a progressive enhancement over a working fixed layout, but worth a follow-up for full keyboard accessibility.
[optional_and_low_effort]

🤖 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/Table-DEsIhjZo.js` at line 1, Enhance the
resize handle component b with keyboard support so a focused handle can adjust
its column width using arrow keys, while preserving the existing pointer-resize
behavior. Make the handle focusable and expose an appropriate resize-control
role/label, then reuse setColumnWidth with a bounded width adjustment for the
relevant column.
🤖 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/heroui-CewI8xK4.js`:
- Around line 1-20: Update the Vite/Rollup manual chunk configuration to include
react/jsx-runtime in manualChunks.react, ensuring jsx/jsxs are emitted in the
React vendor chunk instead of tanstack-query. Apply this root-cause
configuration change for the affected generated assets:
src/gateway/static/dashboard/assets/heroui-CewI8xK4.js (lines 1-20) and
src/gateway/static/dashboard/assets/index-Dzny_nKM.js (line 2) require no direct
edits because they should be regenerated.

In `@web/src/pages/BudgetsPage.tsx`:
- Around line 101-102: Update the custom-period validation in the onChange
handler to reject values whose computed seconds equal zero by requiring seconds
> 0 alongside the existing checks, preventing positive fractions that round to
an invalid zero-day period.

In `@web/src/pages/OverviewPage.test.tsx`:
- Around line 100-107: Move timer restoration for the localDayKey test into test
cleanup by adding an afterEach hook or wrapping the fake-timer setup and
assertion in a finally block that calls vi.useRealTimers(). Remove the direct
cleanup call from the test body so fake timers are restored even when the
assertion fails.

---

Nitpick comments:
In `@src/gateway/static/dashboard/assets/Table-DEsIhjZo.js`:
- Line 1: Enhance the resize handle component b with keyboard support so a
focused handle can adjust its column width using arrow keys, while preserving
the existing pointer-resize behavior. Make the handle focusable and expose an
appropriate resize-control role/label, then reuse setColumnWidth with a bounded
width adjustment for the relevant column.
🪄 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

Run ID: 4ffe0f06-e19b-4886-88ea-16ec9c0a443f

📥 Commits

Reviewing files that changed from the base of the PR and between 2783fab and 1b6dc17.

📒 Files selected for processing (32)
  • src/gateway/static/dashboard/assets/ActivityPage-D8PiwZHY.js
  • src/gateway/static/dashboard/assets/AliasesPage-BtHa9giH.js
  • src/gateway/static/dashboard/assets/BudgetsPage-DpjWmQtE.js
  • src/gateway/static/dashboard/assets/Field-gj3-ox4q.js
  • src/gateway/static/dashboard/assets/KeysPage-BfYgsHv2.js
  • src/gateway/static/dashboard/assets/ModelComboBox-aBCUEkAW.js
  • src/gateway/static/dashboard/assets/ModelScopeControl-BZ1OtSOw.js
  • src/gateway/static/dashboard/assets/ModelsPage-RG6qo5OA.js
  • src/gateway/static/dashboard/assets/OverviewPage-BbbCKpIT.js
  • src/gateway/static/dashboard/assets/ProvidersPage-D5GA9y9_.js
  • src/gateway/static/dashboard/assets/SettingsPage-DotL__7x.js
  • src/gateway/static/dashboard/assets/Table-DEsIhjZo.js
  • src/gateway/static/dashboard/assets/ToolsGuardrailsPage-DsZLoYQB.js
  • src/gateway/static/dashboard/assets/UsagePage-DXzfJvRF.js
  • src/gateway/static/dashboard/assets/UsersPage-DHNIPdtI.js
  • src/gateway/static/dashboard/assets/heroui-CewI8xK4.js
  • src/gateway/static/dashboard/assets/index-Dzny_nKM.js
  • src/gateway/static/dashboard/assets/react-q-ooZ0ti.js
  • src/gateway/static/dashboard/assets/tanstack-query-W9y7rsMr.js
  • src/gateway/static/dashboard/index.html
  • web/src/App.test.tsx
  • web/src/pages/ActivityPage.test.tsx
  • web/src/pages/ActivityPage.tsx
  • web/src/pages/BudgetsPage.test.tsx
  • web/src/pages/BudgetsPage.tsx
  • web/src/pages/ModelsPage.test.tsx
  • web/src/pages/ModelsPage.tsx
  • web/src/pages/OverviewPage.test.tsx
  • web/src/pages/OverviewPage.tsx
  • web/src/pages/ProvidersPage.test.tsx
  • web/src/pages/ProvidersPage.tsx
  • web/vite.config.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/gateway/static/dashboard/index.html
  • web/vite.config.ts
  • web/src/App.test.tsx

Comment thread src/gateway/static/dashboard/assets/heroui-CewI8xK4.js
Comment thread web/src/pages/BudgetsPage.tsx Outdated
Comment thread web/src/pages/OverviewPage.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 improves the Otari admin dashboard’s load performance by code-splitting route bundles and creating stable vendor chunks, while also tightening a few UI behaviors (budget assignment retries, date key formatting, capability filtering, and clipboard feedback). The built dashboard assets are regenerated to reflect the new bundling and route loading behavior.

Changes:

  • Lazy-load dashboard routes via React.lazy + Suspense and add a loading-state test for lazy routes.
  • Configure Vite/Rollup vendor chunk splitting for improved caching (React, TanStack Query, HeroUI) and regenerate the committed dashboard bundle.
  • Harden several dashboard interactions (budget assignment retry flow, date refresh key formatting, capability filtering robustness, clipboard success reporting) with accompanying tests.

Reviewed changes

Copilot reviewed 14 out of 34 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
web/vite.config.ts Adds Rollup manualChunks configuration to split vendor dependencies into stable chunks.
web/src/App.tsx Switches route pages to lazy-loaded imports and wraps routes in a shared Suspense loading UI.
web/src/App.test.tsx Adds coverage for the lazy-route loading state.
web/src/pages/ActivityPage.tsx Adds clipboard helper that reports success only when copying succeeds.
web/src/pages/ActivityPage.test.tsx Adds tests for clipboard success/failure behavior.
web/src/pages/BudgetsPage.tsx Improves numeric validation and adds retryable user-assignment flow after budget creation.
web/src/pages/BudgetsPage.test.tsx Tests assignment retry behavior and non-finite numeric rejection.
web/src/pages/ModelsPage.tsx Makes capability filtering resilient to missing input_modalities.
web/src/pages/ModelsPage.test.tsx Adds regression test for missing input_modalities with capability filters.
web/src/pages/OverviewPage.tsx Fixes the local day refresh key (one-based month + zero padding) and exports helper for testing.
web/src/pages/OverviewPage.test.tsx Adds a test asserting the local day key format.
web/src/pages/ProvidersPage.tsx Changes secret input to a password field (still with autofill/spellcheck suppression).
web/src/pages/ProvidersPage.test.tsx Updates provider form test to assert password input type for API key field.
src/gateway/static/dashboard/index.html Updates entrypoint and preloads new vendor chunks produced by code-splitting.
src/gateway/static/dashboard/assets/index-Dzny_nKM.js Regenerated main bundle reflecting lazy route chunks and new chunk graph.
src/gateway/static/dashboard/assets/react-q-ooZ0ti.js Regenerated vendor chunk (React).
src/gateway/static/dashboard/assets/tanstack-query-W9y7rsMr.js Regenerated vendor chunk (TanStack Query).
src/gateway/static/dashboard/assets/heroui-CewI8xK4.js Regenerated vendor chunk (HeroUI).
src/gateway/static/dashboard/assets/ActivityPage-D8PiwZHY.js Regenerated route chunk reflecting clipboard behavior changes.
src/gateway/static/dashboard/assets/AliasesPage-BtHa9giH.js Regenerated route chunk for lazy-loaded Aliases page.
src/gateway/static/dashboard/assets/BudgetsPage-DpjWmQtE.js Regenerated route chunk reflecting budget assignment retry and validation changes.
src/gateway/static/dashboard/assets/Field-gj3-ox4q.js Regenerated shared component chunk.
src/gateway/static/dashboard/assets/KeysPage-BfYgsHv2.js Regenerated route chunk (also includes multiline snippet string changes from bundling).
src/gateway/static/dashboard/assets/ModelComboBox-aBCUEkAW.js Regenerated shared component chunk for model combo box.
src/gateway/static/dashboard/assets/ModelScopeControl-BZ1OtSOw.js Regenerated shared component chunk for model scoping control.
src/gateway/static/dashboard/assets/ModelsPage-RG6qo5OA.js Regenerated route chunk reflecting capability filtering hardening.
src/gateway/static/dashboard/assets/OverviewPage-BbbCKpIT.js Regenerated route chunk reflecting local day key formatting fix.
src/gateway/static/dashboard/assets/ProvidersPage-D5GA9y9_.js Regenerated route chunk reflecting secret input type change.
src/gateway/static/dashboard/assets/SettingsPage-DotL__7x.js Regenerated route chunk for lazy-loaded Settings page.
src/gateway/static/dashboard/assets/Table-DEsIhjZo.js Regenerated shared component chunk for table rendering.
src/gateway/static/dashboard/assets/ToolsGuardrailsPage-DsZLoYQB.js Regenerated route chunk for lazy-loaded Tools & Guardrails page.
src/gateway/static/dashboard/assets/UsagePage-DXzfJvRF.js Regenerated route chunk for lazy-loaded Usage page.
src/gateway/static/dashboard/assets/UsersPage-DHNIPdtI.js Regenerated route chunk for lazy-loaded Users page.
Files not reviewed (17)
  • src/gateway/static/dashboard/assets/ActivityPage-D8PiwZHY.js: Generated file
  • src/gateway/static/dashboard/assets/AliasesPage-BtHa9giH.js: Generated file
  • src/gateway/static/dashboard/assets/BudgetsPage-DpjWmQtE.js: Generated file
  • src/gateway/static/dashboard/assets/Field-gj3-ox4q.js: Generated file
  • src/gateway/static/dashboard/assets/KeysPage-BfYgsHv2.js: Generated file
  • src/gateway/static/dashboard/assets/ModelComboBox-aBCUEkAW.js: Generated file
  • src/gateway/static/dashboard/assets/ModelScopeControl-BZ1OtSOw.js: Generated file
  • src/gateway/static/dashboard/assets/ModelsPage-RG6qo5OA.js: Generated file
  • src/gateway/static/dashboard/assets/OverviewPage-BbbCKpIT.js: Generated file
  • src/gateway/static/dashboard/assets/ProvidersPage-D5GA9y9_.js: Generated file
  • src/gateway/static/dashboard/assets/SettingsPage-DotL__7x.js: Generated file
  • src/gateway/static/dashboard/assets/Table-DEsIhjZo.js: Generated file
  • src/gateway/static/dashboard/assets/ToolsGuardrailsPage-DsZLoYQB.js: Generated file
  • src/gateway/static/dashboard/assets/UsagePage-DXzfJvRF.js: Generated file
  • src/gateway/static/dashboard/assets/UsersPage-DHNIPdtI.js: Generated file
  • src/gateway/static/dashboard/assets/index-Dzny_nKM.js: Generated file
  • src/gateway/static/dashboard/assets/tanstack-query-W9y7rsMr.js: Generated file

Comment on lines +471 to +475
onClose={() => {
setAssignmentError(null);
setPendingAssignments(null);
setAddOpen(false);
}}
Comment thread web/src/App.test.tsx Outdated

it("shows a loading state while the current route loads", async () => {
window.sessionStorage.setItem("otari.dashboard.masterKey", "test-master-key");
vi.mocked(apiFetch).mockResolvedValue([]);
njbrake added 2 commits July 22, 2026 17:16
Keep vendor chunks independent and prevent invalid or stale budget form state during assignment.
@njbrake
njbrake temporarily deployed to integration-tests July 22, 2026 17:17 — with GitHub Actions Inactive
@njbrake
njbrake temporarily deployed to integration-tests July 22, 2026 17:22 — with GitHub Actions Inactive
@njbrake
njbrake enabled auto-merge (squash) July 22, 2026 17:22

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

Approving to unblock. Review of the code-splitting change. The lazy() + Suspense pattern and the manualChunks vendor split are clean, and the added tests are well-targeted. The bundled bugfixes (Infinity budget limit, clipboard success feedback, input_modalities guard, localDayKey padding) are all real improvements.

Two non-blocking notes beyond the inline comment:

  • Scope: the PR is scoped as code-splitting but also bundles unrelated hardening (clipboard, budget retry + finite validation, modality guard, date-key fix, secret-field type). AGENTS.md: "Keep diffs focused; avoid opportunistic refactors unless requested."
  • Overlaps with #367: both modify web/src/pages/ModelsPage.tsx, delete the same committed bundle (assets/index-DnD03gwo.js), and edit index.html. Whichever merges second needs a rebase and a dashboard rebuild.

Review created by Claude Code.

the key without any of that. */}
<Input
type="text"
type="password"

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.

This reverses a documented decision and deletes the comment that explained it. Tradeoff to surface: type="password" fixes masking in Firefox (which doesn't support -webkit-text-security, so the key currently renders in plaintext there), but reintroduces exactly what the removed comment warned about on non-HTTPS self-hosted deployments: Chrome's "form not secure" warning and password-manager autofill heuristics attaching to the sibling provider picker. Confirm this is intended and keep a one-line note on the tradeoff either way.

@njbrake
njbrake temporarily deployed to integration-tests July 22, 2026 17:28 — with GitHub Actions Inactive
@njbrake
njbrake disabled auto-merge July 22, 2026 17:28
@njbrake

njbrake commented Jul 22, 2026

Copy link
Copy Markdown
Member Author

@khaledosman Addressed in 82ebb23. Native password masking remains for cross-browser secret protection, and the source now records the HTTPS deployment tradeoff.

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: code-split the bundle

3 participants