feat(admin-ui): vendored admin UI (partitions, documents, jobs, presets, models, users) on the OpenRag API - #586
Conversation
Vite + React 19 + Tailwind 4 + TS config, app entry and public assets. Replaces the indexer-ui submodule symlink with a vendored ui/ directory. MSW service-worker stub is git-ignored (regenerate via npx msw init public/).
Radix-based UI components and the cn() class helper.
DataTable, dialogs, page header, status badge; app layout and route guards.
Typed fetch client, per-resource API modules and the auth context.
Backend-less dev + vitest fixtures; never bundled in production. Mandragora-shaped for now, rewritten to OpenRag shapes in the API-wiring phase.
Linagora crimson (#c71f45) palette, OpenRAG wordmark logo, light sidebar with crimson accents, bundled Inter font. Sidebar trimmed to the admin-only nav (no chat/workspace sections).
Drop user creation/bulk-import and passwords (SSO-provisioned). Settings + user detail use a single regenerable API token. Users managed via read-only IdP identity + admin/quota. Partition roles use OpenRag viewer/editor/owner with descriptions. Routes/nav for dropped features removed.
Rewire lib/api/models.ts and presets.ts to the real backend shapes
(verified vs api/schemas/admin + routers/admin):
- list endpoints return bare arrays (not {endpoints}/{presets})
- /model-endpoints and /presets prefixes (drop /api/v1/admin)
- 204 deletes; set-default and validate by (type, name)
- preset options retrieval_types / reranker_providers
Update consumer pages and rewrite MSW handlers to match.
Add per-partition file counts to the partition list and detail responses, reusing the files table: - core port: add count_files_by_partition() + get_partition_file_count() - pg repo: one GROUP BY for the map; per-partition COUNT for detail - partition service: file_counts_by_partition(); detail carries document_count - list router enriches each row; PartitionDetailResponse gains document_count Unit tests cover the new field.
Wire lib/api/partitions.ts to the real backend (verified vs
partition_schemas.py + routers/admin/partitions.py):
- list -> GET /partition/ ({partitions:[{partition,created_at,document_count}]})
- detail -> GET /partition/{p}/config (PartitionDetailResponse, incl. document_count)
- create -> POST /partition/{p} (name in path, no body) + follow-up PATCH for config
- update -> PATCH; delete -> 204
- members (partition-centric): list/add/updateRole/remove via /partition/{p}/users
- add getPartitionConfig + listPartitionFiles for the documents slice
Transitional compat (name aliases partition; legacy member/user-list shapes)
keeps the not-yet-migrated pages (partitions/list+detail, documents/*, overview)
compiling; removed as each is wired. MSW partition handlers updated to match.
… list GET /partition/ now returns each partition's stored config columns (description, embedder, preset references, dimension, chat config) plus document_count, via a new list_partition_summaries(). It reads stored columns only and does NOT resolve indexation/retrieval pipelines, so the list stays two queries regardless of partition count; pipeline resolution remains reserved for the single-partition detail (get_partition_config). Unit test covers the summary shape and counts.
- single membership-scoped query (GET /partition/ serves admin + user; isAdmin only gates admin-only columns/fields) - create via createPartition (POST name + follow-up PATCH); drop the server-managed 'Collection Name' field - Description/Embedder/Preset columns now render real data from the enriched list response; document_count shown directly - drop the any cast and transitional compat casts (rows are PartitionResponse)
… detail
PartitionDetailResponse (GET /partition/{p}/config) now includes the stored
chat_history_depth and chat_llm columns, so the admin detail view can
pre-populate and edit them. Unit test asserts the fields are present.
- config view via getPartitionConfig (/config) typed as PartitionConfig - members tab uses partition-centric ops: listPartitionMembers + add/remove (numeric user_id, multipart); drops the users.ts assignPartition/removePartition calls - drop the server-managed Collection Name display - PartitionConfig gains chat_history_depth/chat_llm; remove the now-unused listPartitionUsers / member-list compat from partitions.ts
…ion files) - documents.ts: real read side (getFileDetail, listPartitionChunks, listFileChunks, getExtract); indexing.ts: real write side (uploadFile, replaceFile, updateFileMetadata, deleteFile, copyFile, newFileId) - documents/list.tsx: partition-scoped (pick a partition -> its files); multi-file upload via client-side loop (one indexing task per file); drop status filter/StatusBadge, batch endpoint, file-size column - detail route is now /documents/:partition/:fileId - MSW: /indexer/* handlers + enriched file-list rows Transitional: documents/detail + overview still use the retained legacy flat-model functions (listDocuments/getDocument/...); removed as those pages are wired (detail = next commit).
…file) - route /documents/:partition/:fileId; single listFileChunks query feeds both the metadata (Details) and the chunk viewer - replace/copy/delete via the indexer file endpoints (replaceFile, copyFile to a chosen partition, deleteFile) - drop status badge, file size, entities/topics, and the indexation-pipeline snapshot (not in OpenRag's model) - remove the detail-only flat-model compat from documents.ts (keep only listDocuments for the overview slice)
Add /partition/{p}/file/{id}, /partition/{p}/chunks, and /extract/{id} mock
handlers (file detail + chunk viewer); remove the legacy flat-model document
+ batch-indexing handlers. Keep GET /api/v1/admin/documents for the overview
slice (still on listDocuments) until it is wired.
Add the real /queue + /indexer/task surface (getQueueInfo, listTasks, getTaskStatus, getTaskError, getTaskLogs, cancelTask + TaskState helpers). OpenRag has no task SSE — and the post-phase-15 roadmap (PHASE_17 'poll the task', scaled multi-replica topology in 17/19/20) makes polling the intended, scale-correct pattern, not a stopgap. Legacy SSE/flat functions are retained so the jobs pages keep compiling; they migrate to polling next.
jobs/list.tsx -> listTasks (/queue/tasks); tabs ALL/ACTIVE/COMPLETED/FAILED/ CANCELLED; rows show task state + file + partition; 5s poll (no SSE). MSW: add /queue/info + /queue/tasks. Detail page still on legacy getJob/SSE until its rewrite (next).
Migrate the last two pages off the legacy Mandragora batch-jobs API (/api/v1/admin/jobs + SSE) onto OpenRag's per-file task model: - jobs/detail: poll getTaskStatus until terminal; show task details, failure traceback (getTaskError), tailing logs (getTaskLogs), and a Cancel action (cancelTask). Replaces the SSE/stage-timing view. - overview: Recent Jobs + Active Jobs now read listTasks; shared RecentTasks table (tasks carry no doc-count/timestamp). - status-badge: color OpenRag task states (SERIALIZING/CHUNKING/ INSERTING/CANCELLED). - remove now-dead listJobs/getJob/streamJobEvents + legacy job types and their MSW handlers; add task detail/error/logs/cancel mocks.
…-import, auth refresh, prompt CRUD) These map to features the admin UI drops or that OpenRag handles differently; the corresponding pages were already removed: - auth.refresh — OIDC refreshes server-side - users.createApiKey/revokeApiKey — OpenRag has one rotating token per user - users.importUsers + bulk-import types — bulk import dropped - prompts CRUD + partition-prompt assignment — prompts is a dropped feature; presets only reads listPrompts Also removes the now-orphaned MSW handlers. Recoverable from git history if any feature is reinstated.
pymupdf is the text-only PDF backend (no image extraction), so image captioning is a no-op when parsing_strategy is pymupdf. Disable the captioning toggle in the preset editor with an explanatory hint, and clear enable_image_captioning when switching the parser to pymupdf so the saved config stays consistent. marker/docling remain the image-aware backends. Refs #572
…elete Deleting a row refetches the list, and TanStack Table's default autoResetPageIndex snapped the table back to page 1 — so deleting a document on page 2 bounced the user to page 1. Make pagination controlled with autoResetPageIndex=false, and clamp to the last valid page when the current page disappears (deleting the last row on the last page). Applies to the documents, jobs and users lists (shared DataTable).
- Make Filename and Indexed columns sortable (default sort: newest first). - Add optional row selection to the shared DataTable: a checkbox column plus a contextual bulk-action bar (keyed by a stable getRowId so the selection survives refetches). - Documents list: select rows and delete them at once (concurrent per-file deletes via Promise.allSettled, with a success/failure toast), 'Select all N' across pages, gated on write permission. - SortableHeader now shows the active sort direction.
#453) The toggle defaulted to OFF when a preset omitted enable_image_captioning, but the backend defaults that field to True and presets are stored sparse (only non-defaults). So a preset that never set the flag showed 'off' in the UI while indexing still captioned. Default the toggle to the backend's true so the displayed state matches what runs; toggling off persists an explicit false as before.
A file only shows in the partition file list once its indexing job finishes (the catalog row is written post-indexing), but the list only refetched on partition switch/mount — so a freshly-indexed doc never appeared until the user switched partitions and back. Poll every 5s (mirrors the Jobs page; only when focused) to pick them up.
…tion The documents view held the selected partition in component-local state, so leaving (open a doc's detail, or switch to Jobs) and coming back reset it to the first/default partition. Persist it: the detail 'Back to Documents' (and post-delete nav) carry ?partition=, and the list resolves its partition from the URL, then the last choice in sessionStorage, then the first partition — so you land where you were.
Topic tags are generated and persisted but not yet surfaced or used in retrieval, so offering the toggle is misleading. Disable it with a "coming soon" hint until the feature is fully wired (next release).
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughThe PR updates OIDC deployment guidance, adds backend partition summary and model validation APIs, introduces an admin UI image/runtime and shared React shell, expands typed UI clients and MSW handlers, and adds login, settings, dashboard, and admin management pages. ChangesAdmin UI and API rollout
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…ct-router bump - Remount the user detail view via a route key so the profile form, quota input, and active tab re-seed when navigating between user ids instead of leaking the previous user's values (the formLoaded one-shot never reset). - Send explicit null (not undefined) for cleared profile fields so a PATCH can clear email / external_user_id back to the token-only state; widen UserUpdateRequest to allow null on those identity columns. - Bump react-router-dom ^7.13.0 -> ^7.18.0 to clear the react-router audit advisories (open redirect / CSRF / SSR-mode issues).
There was a problem hiding this comment.
Actionable comments posted: 1
Note
Due to the large number of review comments, Critical severity comments were prioritized as inline comments.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
ui/README.md (1)
33-39: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winDocument
VITE_GRAFANA_URLhere too.
ui/.env.example:1-2andui/src/pages/admin/system.tsx:20-45already support this variable, but the environment table never mentions it. That makes the System page's Grafana link look undocumented.🤖 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 `@ui/README.md` around lines 33 - 39, Add documentation for VITE_GRAFANA_URL in the environment variables table in README so the System page Grafana link is covered. Update the section alongside the existing VITE_API_BASE_URL, VITE_AUTH_MODE, VITE_MOCK_API, and VITE_BASE_PATH entries, and make sure the description matches how ui/src/pages/admin/system.tsx uses the variable.
🟠 Major comments (26)
openrag/api/routers/admin/model_endpoints.py-100-111 (1)
100-111: 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy liftBlock SSRF on the draft validation route.
body.endpointis forwarded straight intoservice.validate_endpoint(), and that service performs a server-sideGET {url}/modelswith an optional bearer token. This gives any admin caller a direct probe into arbitrary internal/private hosts without even persisting an endpoint first. Please reject loopback/link-local/private targets (or enforce an allowlist) before calling the service.🤖 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 `@openrag/api/routers/admin/model_endpoints.py` around lines 100 - 111, The draft validation route is forwarding untrusted endpoint URLs directly into service.validate_endpoint(), which can be used to probe internal/private hosts. Add SSRF protection in validate_endpoint_draft by rejecting loopback, link-local, private, and otherwise disallowed targets (or enforcing an allowlist) before invoking the model endpoint service. Use the existing validate_endpoint_draft handler and the service.validate_endpoint call as the place to apply this guard so unsafe URLs never reach the GET {url}/models request.infra/compose/nginx/openrag-admin.conf-53-55 (1)
53-55: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winDon’t hardcode the backend’s container port here.
proxy_pass http://$openrag_upstream:8080;ignores this repo’s${APP_PORT}:${APP_iPORT}contract. If a deployment changes onlyAPP_iPORT, the SPA still comes up but every proxied API request turns into a 502 until this file is rebuilt with the new port. Template the upstream port, or explicitly lock the front-door contract toAPP_iPORT=8080. Based on learnings,APP_iPORTis the container-internal bind port in this repo.🤖 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 `@infra/compose/nginx/openrag-admin.conf` around lines 53 - 55, The Nginx proxy configuration is hardcoding the backend container port in the `proxy_pass` directive, which can break API routing when `APP_iPORT` changes. Update the `openrag-admin.conf` proxy setup to use the repository’s `APP_iPORT` contract instead of a fixed port, or explicitly document and enforce that the backend always binds to 8080. Use the existing `proxy_pass`, `set $openrag_upstream`, and related upstream configuration in this file as the place to make the port configurable.Source: Learnings
ui/.env.example-1-1 (1)
1-1: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winDon't default the example to a cross-origin API base.
ui/src/lib/api/client.ts:1-90uses a plainfetch()with nocredentials: "include", so copying this file makes the app talk tohttp://localhost:8000cross-origin and OIDC/session-cookie auth stops working. LeaveVITE_API_BASE_URLempty by default, or label this value as token-mode/dev-only.🤖 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 `@ui/.env.example` at line 1, The example API base is currently set to a cross-origin default, which breaks the session-cookie flow used by the API client. Update the .env example so VITE_API_BASE_URL is empty by default or clearly marked as token-mode/dev-only, and keep it aligned with ui/src/lib/api/client.ts so the default setup uses same-origin behavior instead of http://localhost:8000.ui/src/mocks/handlers.ts-226-231 (1)
226-231: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winMock partition creation never survives the follow-up config fetch.
createPartition()falls back toGET /partition/:name/configwhen only a name is provided, but this POST handler never inserts the new partition and the config handler 404s unknown names. In mock mode, name-only partition creation will always fail after the initial201.Also applies to: 251-251
🤖 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 `@ui/src/mocks/handlers.ts` around lines 226 - 231, The mock partition creation flow is incomplete because the POST handler for createPartition only returns a 201 and never stores the new partition, so the follow-up GET in createPartition’s name-only path hits the config handler and 404s. Update the handlers in handlers.ts so the POST /partition behavior inserts the created partition into the shared partitions list (or whatever backing mock state partitionConfig(name) reads from), and ensure the GET /partition/:name/config handler can resolve newly created names. Use createPartition, partitionConfig, and the existing partitions mock state as the key symbols to wire this through.ui/src/mocks/handlers.ts-235-249 (1)
235-249: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winDocument deletes don't update the backing fixture state.
The list endpoint reads from
documents, but the delete endpoint only returns204. After a successful delete, the next refetch serves the same file again, which breaks the bulk-delete/current-page flows this PR is trying to exercise in mock mode.Suggested fix
-http.delete(`${API}/indexer/partition/:name/file/:fileId`, () => new HttpResponse(null, { status: 204 })), +http.delete(`${API}/indexer/partition/:name/file/:fileId`, ({ params }) => { + const fileId = String(params.fileId); + const partition = String(params.name); + const index = documents.findIndex((d) => d.id === fileId && d.partition === partition); + if (index !== -1) documents.splice(index, 1); + delete documentChunks[fileId]; + return new HttpResponse(null, { status: 204 }); +}),Also applies to: 295-295
🤖 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 `@ui/src/mocks/handlers.ts` around lines 235 - 249, The mock delete flow does not mutate the backing documents fixture, so refetches from the partition list still return deleted files. Update the delete handler in handlers.ts to remove the matching entry from documents (and any related file state used by the list endpoints) before returning 204, so http.get(`${API}/partition/:name`) reflects the deletion across bulk-delete and current-page flows.ui/src/lib/api/account.ts-1-1 (1)
1-1: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winRotate the stored bearer token when self-regenerating.
This helper invalidates the caller's current token, but the current consumer in
ui/src/pages/app/settings.tsxonly keepsdata.tokenin component state. In token mode, the next API call still uses the revoked value fromTOKEN_KEY, so the user gets logged out immediately after rotating their own key.Suggested fix
-import { request } from "./client"; +import { request, TOKEN_KEY } from "./client"; @@ export async function regenerateMyToken(): Promise<{ token: string }> { const me = await getMyInfo(); - return request<{ token: string }>(`/users/${me.id}/regenerate_token`, { method: "POST" }); + const rotated = await request<{ token: string }>(`/users/${me.id}/regenerate_token`, { + method: "POST", + }); + if (localStorage.getItem(TOKEN_KEY)) { + localStorage.setItem(TOKEN_KEY, rotated.token); + } + return rotated; }Also applies to: 29-31
🤖 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 `@ui/src/lib/api/account.ts` at line 1, The self-rotation flow only updates the in-memory token from `data.token`, so the revoked bearer token remains in `TOKEN_KEY` and subsequent requests keep using the invalid value. Update the token refresh path in `ui/src/pages/app/settings.tsx` to persist the new token wherever the API client reads auth state, and ensure the account helper in `ui/src/lib/api/account.ts` returns/propagates the regenerated token so the caller can replace the stored one immediately. Verify the rotation logic replaces the old token rather than only updating component state.ui/src/index.css-88-89 (1)
88-89: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
--destructive-foregroundmatches the destructive fill.Any control using the normal
bg-destructive text-destructive-foregroundpairing will render red-on-red in light mode, so destructive buttons/alerts become unreadable.Suggested fix
--destructive: oklch(0.577 0.245 27.325); - --destructive-foreground: oklch(0.577 0.245 27.325); + --destructive-foreground: `#ffffff`;🤖 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 `@ui/src/index.css` around lines 88 - 89, The destructive color tokens are mismatched because `--destructive-foreground` currently uses the same value as `--destructive`, causing red-on-red text in components like `bg-destructive text-destructive-foreground`. Update the `:root` theme variables in `index.css` so `--destructive-foreground` is a contrasting foreground color, and keep the `destructive`/`destructive-foreground` pairing consistent with the rest of the theme tokens.ui/src/lib/auth.tsx-37-45 (1)
37-45: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winDon't treat every
/users/infofailure as a logout.
request()already distinguishes401from other failures, butload()nullsuseron any thrown error. A transient backend/network issue will therefore de-authenticate an existing session and strip all admin capabilities even though the credentials are still valid.Suggested fix
const load = useCallback(async () => { setIsLoading(true); try { setUser(await getMyInfo()); - } catch { - setUser(null); + } catch (e) { + if ((e as { status?: number }).status === 401) { + setUser(null); + } } finally { setIsLoading(false); } }, []);🤖 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 `@ui/src/lib/auth.tsx` around lines 37 - 45, The `load` callback in `AuthProvider` is clearing `user` on every `getMyInfo()` error, which incorrectly logs out valid sessions on transient failures. Update `load()` to only call `setUser(null)` when the failure represents an unauthorized response from `request()`/`getMyInfo()`; for non-401 errors, preserve the existing user state and keep the session intact. Use the `load`, `getMyInfo`, and `request` flow in `auth.tsx` to keep the 401/logout behavior isolated from backend or network errors.ui/src/pages/admin/users/list.tsx-122-142 (1)
122-142: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winIcon-only actions need accessible labels.
The row actions and token-copy button render only icons, so assistive tech does not get a stable action name. Add
aria-label(or visible text) to these controls instead of relying on the SVG ortitle.Also applies to: 244-246
🤖 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 `@ui/src/pages/admin/users/list.tsx` around lines 122 - 142, The icon-only row actions in the users list do not expose accessible names, so add stable labels to the controls in the table cell renderer and the token-copy button instead of relying on the SVG or title. Update the relevant Button/Copy control usage in the users list page (including the row action buttons and the token copy action) to provide aria-label or visible text so assistive tech can identify each action consistently.ui/src/lib/api/users.ts-120-133 (1)
120-133: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winDon't treat membership fetch failures as “not a member”.
The inner
catchconverts every network/4xx/5xx failure intonull, so the UI gets an incomplete membership list and can offer already-assigned partitions as “available”. Bubble the error, or return/report partial failures instead of silently dropping rows.🤖 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 `@ui/src/lib/api/users.ts` around lines 120 - 133, The listUserMemberships function is swallowing errors from listPartitionMembers and treating failed membership fetches as missing memberships. Update the Promise.all mapping in listUserMemberships so network/4xx/5xx failures are not converted to null; instead let the error bubble up or explicitly report partial failures. Keep the successful member lookup and filtering logic, but remove the silent catch path so the UI does not treat failed partition lookups as “not a member.”ui/src/lib/api/partitions.ts-147-159 (1)
147-159: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy liftRollback here can delete a successfully created partition.
If the
POSTsucceeds and thePATCHtimes out or the response is lost after the server commits, thiscatchstill callsdeletePartition(). That turns a retryable/ambiguous failure into data loss. Please only roll back on definitive validation failures, or surface a partial-success state and let the user reconcile it.🤖 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 `@ui/src/lib/api/partitions.ts` around lines 147 - 159, The rollback in createPartition is too aggressive because the catch after updatePartition(name, data) deletes the partition even when the POST may have succeeded and the PATCH result is only ambiguous. Update this flow so deletePartition(name) is only called for definitive validation/configuration failures, not transport/timeouts or lost responses, and otherwise surface a partial-success state or rethrow without cleanup. Use the createPartition, updatePartition, and deletePartition paths to keep retries safe and avoid deleting a successfully created partition.ui/src/pages/admin/users/list.tsx-48-51 (1)
48-51: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winRender a failure state when the users query errors.
If
listUsers()rejects,isLoadingbecomes false and this falls through toDataTablewith[], which looks identical to “there are no users”. HandleisError/errorseparately so auth and backend failures are not masked.Also applies to: 161-165
🤖 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 `@ui/src/pages/admin/users/list.tsx` around lines 48 - 51, The users list query currently only checks isLoading, so listUsers() failures fall through to the empty DataTable state and hide auth/backend errors. Update the users page component that uses useQuery and DataTable to also read isError/error, and render a distinct failure state before the table when the query rejects. Apply the same handling in the other affected users-list section referenced by the duplicate useQuery/DataTable path so both places surface errors instead of showing an empty list.ui/src/pages/admin/users/detail.tsx-337-344 (1)
337-344: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winLabel the icon-only controls in this page.
The membership revoke and token-copy buttons are icon-only, so assistive tech will not announce what they do. Give them an
aria-labelor hidden text.Also applies to: 546-548
🤖 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 `@ui/src/pages/admin/users/detail.tsx` around lines 337 - 344, The icon-only controls in the user detail page are not accessible because they lack an accessible name. Update the revoke action inside the membership row and the token-copy button in the same component (the controls wrapped by ConfirmDialog and the copy action near the token section) to include an aria-label or equivalent hidden text so assistive tech can announce their purpose.ui/src/pages/admin/users/list.tsx-201-209 (1)
201-209: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winValidate quota before serializing the create payload.
type="number"still allows transient values like-,e, or1e309.Number(quota)turns those intoNaN/Infinity, andJSON.stringifyserializes both asnull, so this can silently send “use the global default” instead of rejecting the bad value.🤖 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 `@ui/src/pages/admin/users/list.tsx` around lines 201 - 209, The create user mutation is serializing an unvalidated quota value, which can turn transient number inputs into NaN/Infinity and be sent as null. Update createMut in the admin users list flow to validate quota before building the createUser payload, using the existing quota input handling around file_quota. Reject or ignore invalid numeric states like -, e, and 1e309, and only pass a real finite number or null when the field is intentionally empty.ui/src/pages/admin/users/detail.tsx-415-417 (1)
415-417: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winGuard invalid quota strings here too.
This path has the same
Number(quota)issue as the create dialog:NaN/Infinityare serialized asnull, so an invalid entry can unexpectedly clear the per-user quota back to the global default.Also applies to: 431-435
🤖 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 `@ui/src/pages/admin/users/detail.tsx` around lines 415 - 417, The user quota update path in the admin user detail view still converts arbitrary strings with Number(quota), which lets invalid inputs become null and clear the quota unexpectedly. Update the mutation logic in the user detail save flow, including the create/update paths referenced by saveMut, to validate the quota string before building the payload. Only send a numeric file_quota when the input is a finite valid number; otherwise reject or preserve the existing value instead of serializing an invalid value as null.ui/src/pages/admin/users/detail.tsx-70-74 (1)
70-74: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winDon't map every load failure to
User not found.Any 401/500/network error leaves
userundefined here, so the page renders the same message as a real missing record. Branch onisError/errorseparately; otherwise admin/API outages look like a deleted user.Also applies to: 119-120
🤖 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 `@ui/src/pages/admin/users/detail.tsx` around lines 70 - 74, The user detail page is treating every failed `useQuery`/`getUser` request as a missing record, so `user` being undefined renders the same “User not found” state for 401/500/network failures. Update the `useQuery` flow in `detail.tsx` and the related render branch around the other `User not found` handling to check `isError` and `error` separately from the true missing-user case. Keep `user`-absent logic only for an actual empty result, and show a distinct error state/message for request failures.ui/src/lib/api/client.ts-47-55 (1)
47-55: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winNormalize
fetchOptions.headersbefore merging.RequestInit.headerscan be aHeadersobject or tuple array, and casting it toRecord<string, string>drops those values entirely; theContent-Typecheck is also case-sensitive, so a lowercase header can still get duplicated. Usenew Headers(fetchOptions.headers)andhas/setinstead.🤖 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 `@ui/src/lib/api/client.ts` around lines 47 - 55, Normalize fetchOptions.headers before merging so non-object header types are preserved: the current header merge in the client request builder treats RequestInit.headers as a Record and can drop Headers/tuple values. Update the header handling in the client API code to use Headers-based merging, and make the Content-Type presence check case-insensitive by relying on has/set instead of direct object key access, so lowercase Content-Type values are not duplicated.ui/src/pages/admin/models.tsx-46-49 (1)
46-49: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winHandle the endpoints query error separately from the empty state.
When
listModelEndpoints()fails,datastaysundefined,isLoadingbecomes false, and the page renders “No {type} endpoints configured.” That hides 401/403/500s as a successful empty load.Suggested fix
- const { data, isLoading } = useQuery({ + const { data, isLoading, isError, error } = useQuery({ queryKey: ["model-endpoints"], queryFn: () => listModelEndpoints(), }); + + if (isError) { + return ( + <div> + <PageHeader + title="Model Endpoints" + description="Manage embedder, reranker, LLM, and VLM endpoints" + /> + <p className="text-sm text-destructive">{error.message}</p> + </div> + ); + } const endpoints = data ?? [];Also applies to: 93-94, 128-209
🤖 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 `@ui/src/pages/admin/models.tsx` around lines 46 - 49, The models page query in useQuery for listModelEndpoints() is treating fetch failures like an empty result, so separate error handling from the empty state. Update the models page flow in the main query and the related rendering branches in the models page components so query errors are surfaced distinctly (for example via an error state or error message) instead of falling through to “No {type} endpoints configured.”, while keeping the existing empty-state behavior only for a successful request with no data.ui/src/components/layout/header.tsx-36-38 (1)
36-38: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winAdd an accessible name to the logout button.
This is an icon-only control, so screen readers get an unnamed button here. Add
aria-label="Log out"(and ideallytitle) so logout stays discoverable.Suggested fix
- <Button variant="ghost" size="icon" onClick={handleLogout} className="text-muted-foreground hover:text-destructive"> + <Button + variant="ghost" + size="icon" + onClick={handleLogout} + aria-label="Log out" + title="Log out" + className="text-muted-foreground hover:text-destructive" + >🤖 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 `@ui/src/components/layout/header.tsx` around lines 36 - 38, The logout control in the header is icon-only, so it needs an accessible name for screen readers. Update the Button in header.tsx that renders the LogOut icon by adding an aria-label like “Log out” and, if appropriate, a matching title so the handleLogout action remains discoverable and accessible.ui/src/pages/admin/partitions/detail.tsx-59-67 (1)
59-67: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winResync the form state when
partitionchanges.These
useState(partition...)initializers only run on the first mount. After the detail query refetches normalized data, or if this route is reused for another partition, the form keeps the previous values and can submit stale settings.Suggested fix
+ useEffect(() => { + setDescription(partition.description ?? ""); + setChatHistoryDepth(String(partition.chat_history_depth)); + setIndexationPreset(partition.indexation_preset); + setRetrievalPreset(partition.retrieval_preset); + setChatLlm(partition.chat_llm ?? "__default__"); + setLlmValidated(partition.chat_llm ? null : true); + setLlmValidating(false); + }, [ + partition.name, + partition.description, + partition.chat_history_depth, + partition.indexation_preset, + partition.retrieval_preset, + partition.chat_llm, + ]);🤖 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 `@ui/src/pages/admin/partitions/detail.tsx` around lines 59 - 67, The partition detail form state is only initialized once from partition props, so it can keep stale values after refetches or route reuse. Update the state in the detail component when the partition data changes by syncing the relevant useState fields inside the partition-detail page logic. Use the existing state setters in detail.tsx for description, chatHistoryDepth, indexationPreset, retrievalPreset, chatLlm, and llmValidated/llmValidating, and key the sync off the partition object so the form always reflects the latest normalized data.ui/src/pages/admin/documents/list.tsx-101-105 (1)
101-105: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winCap bulk-delete concurrency.
This fires one
DELETEper selected row at once. On a large “Select all”, that can burst hundreds of requests and turn bulk delete into a 429/5xx storm instead of a predictable operation.♻️ Suggested change
const bulkDeleteMutation = useMutation({ mutationFn: async (fileIds: string[]) => { setBulkDeleting(true); - const results = await Promise.allSettled(fileIds.map((id) => deleteFile(selected, id))); + const results: PromiseSettledResult<void>[] = []; + const batchSize = 10; + for (let i = 0; i < fileIds.length; i += batchSize) { + results.push( + ...(await Promise.allSettled( + fileIds.slice(i, i + batchSize).map((id) => deleteFile(selected, id)), + )), + ); + } const ok = results.filter((r) => r.status === "fulfilled").length; return { ok, failed: results.length - ok }; },🤖 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 `@ui/src/pages/admin/documents/list.tsx` around lines 101 - 105, The bulk delete flow in useMutation for the selected fileIds currently fires all deleteFile requests at once via Promise.allSettled, which can overwhelm the backend on large selections. Update the mutationFn in list.tsx to process deletions with a bounded concurrency limit instead of launching every request simultaneously, keeping the existing success/failure aggregation logic intact and tied to deleteFile, selected, and bulkDeleteMutation.ui/src/pages/admin/partitions/list.tsx-175-204 (1)
175-204: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winIgnore stale LLM validation responses.
Lines 175-204 let whichever
validateStoredModelEndpoint()call finishes last overwritellmValidated, even if the user has already picked anotherchatLlm. Switching A → B quickly can therefore enable submit for an unvalidated value or keep a valid selection marked failed. Track the requested value/version and discard completions for superseded requests.🤖 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 `@ui/src/pages/admin/partitions/list.tsx` around lines 175 - 204, The LLM validation flow in validateLlm and handleChatLlmChange can let an older validateStoredModelEndpoint("llm", name) response overwrite the current chatLlm state. Track the requested endpoint value or a request/version token when starting validation, and before calling setLlmValidated or showing toasts, confirm the response still matches the latest selected chatLlm; otherwise discard it. Keep the logic localized to validateLlm, handleChatLlmChange, and the llmValidated state updates.ui/src/pages/admin/partitions/list.tsx-420-519 (1)
420-519: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winWire the dialog labels to their controls.
These
Labelcomponents are rendered next to inputs/selects, but none of the fields have matchingid/htmlFor, so assistive tech will not reliably announce the field names. That makes the create-partition form hard to use with a screen reader.♿ Suggested pattern
- <Label>Name *</Label> + <Label htmlFor="partition-name">Name *</Label> <Input + id="partition-name" placeholder="my-partition" value={name} onChange={(e) => setName(e.target.value)} required />Apply the same
htmlFor/idpairing to the description, embedder, preset, chat LLM, and chat history depth controls.🤖 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 `@ui/src/pages/admin/partitions/list.tsx` around lines 420 - 519, The create-partition dialog has unassociated labels, so the form controls are not properly named for assistive tech. Update the Label/Input/Select pairs in the partition form within the create dialog to use matching htmlFor and id attributes for description, embedder, indexation preset, retrieval preset, chat LLM, and chat history depth, following the existing form structure in list.tsx so each Label clearly targets its control.ui/src/pages/admin/presets.tsx-523-528 (1)
523-528: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winAssociate the visible text with these switches.
These toggles are rendered next to plain text
Labels, but there is noid/htmlFororaria-labellinking the control to its name. Screen readers will announce unlabeled switches, and clicking the label text will not toggle them.Also applies to: 679-683, 722-737
🤖 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 `@ui/src/pages/admin/presets.tsx` around lines 523 - 528, The switch controls rendered in the preset rows are not programmatically associated with their visible text labels, so update the toggle components to link each Label with its corresponding Switch using a stable id/htmlFor pair or an equivalent accessible name. Apply this in the shared toggle rendering code around the Label and Switch usage, and propagate the same fix to the other affected toggle blocks so clicking the text toggles the control and screen readers announce the correct name.ui/src/pages/admin/presets.tsx-51-55 (1)
51-55: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winHandle preset query errors separately from the empty state.
listPresets()failures still fall through viadata ?? [], so API/auth errors render “No {type} presets configured” instead of an error.ui/src/pages/admin/presets.tsx:51-55, 87-88, 138-197🤖 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 `@ui/src/pages/admin/presets.tsx` around lines 51 - 55, The preset list query in useQuery/listPresets is treating failures the same as an empty result, so API/auth errors still reach the data ?? [] empty-state path. Update the presets page logic in presets.tsx to track and render query errors separately from the “no presets configured” state, using the existing useQuery result alongside the empty-state rendering block. Ensure the section that renders the presets list and the empty-state message only runs when the query succeeded, and show a dedicated error state when listPresets fails.ui/src/components/shared/data-table.tsx-194-209 (1)
194-209: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winAdd accessible names to the pager buttons.
Lines 194-209 render icon-only controls with no
aria-label. Screen readers get two unnamed buttons, andui/src/components/shared/data-table.test.tsx:15-20already has to fall back to button order because there is no stable name to target.Proposed fix
<Button variant="outline" size="sm" + aria-label="Previous page" onClick={() => table.previousPage()} disabled={!table.getCanPreviousPage()} > <ChevronLeft className="h-4 w-4" /> </Button> <Button variant="outline" size="sm" + aria-label="Next page" onClick={() => table.nextPage()} disabled={!table.getCanNextPage()} > <ChevronRight className="h-4 w-4" /> </Button>🤖 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 `@ui/src/components/shared/data-table.tsx` around lines 194 - 209, Add accessible names to the icon-only pager buttons in data-table so screen readers can distinguish them. Update the previous/next controls in the data table pagination block to include clear aria-label values while keeping the existing onClick handlers and disabled states. Use the Button elements with ChevronLeft and ChevronRight as the lookup points, and make the labels stable enough for ui/src/components/shared/data-table.test.tsx to query by name instead of relying on button order.
🧹 Nitpick comments (2)
tests/unit/api/routers/admin/test_partition_list_route.py (1)
90-115: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick winAdd the non-admin literal
"all"regression case.The route now has a dedicated guard to stop a regular user who owns a real partition named
allfrom expanding into every partition, but this file never exercises that branch. A small regression test here would keep that cross-partition leak from coming back.Suggested test shape
+@pytest.mark.asyncio +async def test_regular_user_literal_all_partition_does_not_expand(async_client_factory): + summaries = { + "all": _summary("all", document_count=1), + "secret": _summary("secret", document_count=9), + } + app = _build_app( + _FakeService(summaries), + [{"partition": "all", "role": "owner"}], + is_admin=False, + ) + async with async_client_factory(app) as client: + resp = await client.get("/partition/") + + assert resp.status_code == 200 + assert {r["partition"] for r in resp.json()["partitions"]} == {"all"} +``` </details> <details> <summary>🤖 Prompt for AI Agents</summary>Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.In
@tests/unit/api/routers/admin/test_partition_list_route.pyaround lines 90 -
115, Add a regression test in test_partition_list_route for the non-admin
literal "all" case so the new guard is exercised. Reuse the existing _build_app
and _FakeService helpers, but create a regular caller (is_admin=False) with a
principal containing {"partition": "all", "role": ...} and summaries for
multiple partitions; assert that the PartitionList route returns only the
literal all partition instead of expanding to every summary. Name the test
alongside test_super_admin_all_sentinel_lists_every_partition and target the
/partition/ endpoint so the guard in the partition listing flow is covered.</details> <!-- cr-comment:v1:dbcc0f4e686f0825be1b6621 --> </blockquote></details> <details> <summary>infra/docker/ui.Dockerfile (1)</summary><blockquote> `32-37`: _🔒 Security & Privacy_ | _🔵 Trivial_ | _⚡ Quick win_ **Consider serving this image as non-root.** This front-door container still inherits nginx’s root default, and Trivy is already flagging it. Switching to an unprivileged nginx image (or `USER nginx` plus a non-privileged listen port and matching compose mapping) removes that extra privilege with no product change. <details> <summary>🤖 Prompt for AI Agents</summary>Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.In
@infra/docker/ui.Dockerfilearound lines 32 - 37, The UI container still runs
as root because it uses the default nginx image, so update the ui.Dockerfile to
run nginx unprivileged. Switch the base image in the Dockerfile to an
unprivileged nginx variant or set a non-root USER, then adjust the nginx config
and any compose port mapping so the server listens on a non-privileged port
while keeping the same behavior. Use the nginx-related setup in ui.Dockerfile
and the copied openrag-admin.conf as the places to make the change.</details> <!-- cr-comment:v1:1a6cb04ca745a4b116cc9b88 --> _Source: Linters/SAST tools_ </blockquote></details> </blockquote></details> --- <details> <summary>ℹ️ Review info</summary> <details> <summary>⚙️ Run configuration</summary> **Configuration used**: Path: .coderabbit.yaml **Review profile**: CHILL **Plan**: Pro **Run ID**: `a6f54ae0-c485-4e16-a7c1-149e4176456c` </details> <details> <summary>📥 Commits</summary> Reviewing files that changed from the base of the PR and between 3cd9957d03cddea163d78ffe6159c9f521572a17 and 4f7c75da8135fa8a992ed3223733a1438bff4406. </details> <details> <summary>⛔ Files ignored due to path filters (12)</summary> * `ui/package-lock.json` is excluded by `!**/package-lock.json` * `ui/public/favicon.png` is excluded by `!**/*.png` * `ui/public/fonts/Inter.ttf` is excluded by `!**/*.ttf` * `ui/public/logo-openrag-blue500.svg` is excluded by `!**/*.svg` * `ui/public/logo-openrag-indigo500.svg` is excluded by `!**/*.svg` * `ui/public/logo-openrag-indigo600.svg` is excluded by `!**/*.svg` * `ui/public/logo-openrag-violet400.svg` is excluded by `!**/*.svg` * `ui/public/logo-openrag-violet600.svg` is excluded by `!**/*.svg` * `ui/public/logo-openrag.svg` is excluded by `!**/*.svg` * `ui/public/logo.png` is excluded by `!**/*.png` * `ui/public/openrag-title-white.svg` is excluded by `!**/*.svg` * `ui/public/openrag-title.svg` is excluded by `!**/*.svg` </details> <details> <summary>📒 Files selected for processing (115)</summary> * `CLAUDE.md` * `docs/content/docs/documentation/oidc.md` * `docs/content/docs/documentation/sso-quickstart.md` * `infra/compose/.env.example` * `infra/compose/docker-compose.yaml` * `infra/compose/nginx/openrag-admin.conf` * `infra/docker/ui.Dockerfile` * `infra/docker/ui.Dockerfile.dockerignore` * `openrag/api/routers/admin/model_endpoints.py` * `openrag/api/routers/admin/partitions.py` * `openrag/api/schemas/admin/model_endpoint_schemas.py` * `openrag/api/schemas/admin/partition_schemas.py` * `openrag/core/config/auth.py` * `openrag/core/ports/partition_repo.py` * `openrag/services/orchestrators/partition_service.py` * `openrag/services/persistence/partition_repo.py` * `tests/unit/api/routers/admin/test_partition_list_route.py` * `tests/unit/api/routers/admin/test_phase14_admin_routers.py` * `tests/unit/api/routers/admin/test_phase14_partition_routes.py` * `tests/unit/infra/test_admin_ui_compose.py` * `tests/unit/services/orchestrators/test_partition_preset_resolution.py` * `ui` * `ui/.env.example` * `ui/.gitignore` * `ui/README.md` * `ui/components.json` * `ui/eslint.config.js` * `ui/index.html` * `ui/package.json` * `ui/src/App.tsx` * `ui/src/components/layout/admin-layout.tsx` * `ui/src/components/layout/admin-route.test.tsx` * `ui/src/components/layout/admin-route.tsx` * `ui/src/components/layout/header.tsx` * `ui/src/components/layout/protected-route.tsx` * `ui/src/components/layout/sidebar.tsx` * `ui/src/components/shared/confirm-dialog.tsx` * `ui/src/components/shared/data-table.test.tsx` * `ui/src/components/shared/data-table.tsx` * `ui/src/components/shared/page-header.tsx` * `ui/src/components/shared/quota-usage-meter.tsx` * `ui/src/components/shared/status-badge.tsx` * `ui/src/components/ui/alert-dialog.tsx` * `ui/src/components/ui/alert.tsx` * `ui/src/components/ui/avatar.tsx` * `ui/src/components/ui/badge.tsx` * `ui/src/components/ui/button.tsx` * `ui/src/components/ui/card.tsx` * `ui/src/components/ui/checkbox.tsx` * `ui/src/components/ui/command.tsx` * `ui/src/components/ui/dialog.tsx` * `ui/src/components/ui/dropdown-menu.tsx` * `ui/src/components/ui/form.tsx` * `ui/src/components/ui/input.tsx` * `ui/src/components/ui/label.tsx` * `ui/src/components/ui/popover.tsx` * `ui/src/components/ui/scroll-area.tsx` * `ui/src/components/ui/select.tsx` * `ui/src/components/ui/separator.tsx` * `ui/src/components/ui/sheet.tsx` * `ui/src/components/ui/sidebar.tsx` * `ui/src/components/ui/skeleton.tsx` * `ui/src/components/ui/sonner.tsx` * `ui/src/components/ui/switch.tsx` * `ui/src/components/ui/table.tsx` * `ui/src/components/ui/tabs.tsx` * `ui/src/components/ui/textarea.tsx` * `ui/src/components/ui/tooltip.tsx` * `ui/src/hooks/use-mobile.ts` * `ui/src/index.css` * `ui/src/lib/api/account.ts` * `ui/src/lib/api/client.test.ts` * `ui/src/lib/api/client.ts` * `ui/src/lib/api/documents.ts` * `ui/src/lib/api/indexing.ts` * `ui/src/lib/api/jobs.ts` * `ui/src/lib/api/models.test.ts` * `ui/src/lib/api/models.ts` * `ui/src/lib/api/partitions.test.ts` * `ui/src/lib/api/partitions.ts` * `ui/src/lib/api/presets.ts` * `ui/src/lib/api/prompts.ts` * `ui/src/lib/api/system.ts` * `ui/src/lib/api/users.ts` * `ui/src/lib/auth.test.tsx` * `ui/src/lib/auth.tsx` * `ui/src/lib/brand.ts` * `ui/src/lib/permissions.test.tsx` * `ui/src/lib/permissions.ts` * `ui/src/lib/routes.test.ts` * `ui/src/lib/routes.ts` * `ui/src/lib/utils.test.ts` * `ui/src/lib/utils.ts` * `ui/src/main.tsx` * `ui/src/mocks/browser.ts` * `ui/src/mocks/handlers.ts` * `ui/src/pages/admin/documents/detail.tsx` * `ui/src/pages/admin/documents/list.tsx` * `ui/src/pages/admin/jobs/detail.tsx` * `ui/src/pages/admin/jobs/list.tsx` * `ui/src/pages/admin/models.tsx` * `ui/src/pages/admin/overview.tsx` * `ui/src/pages/admin/partitions/detail.tsx` * `ui/src/pages/admin/partitions/list.tsx` * `ui/src/pages/admin/presets.tsx` * `ui/src/pages/admin/system.tsx` * `ui/src/pages/admin/users/detail.tsx` * `ui/src/pages/admin/users/list.tsx` * `ui/src/pages/app/settings.tsx` * `ui/src/pages/login.tsx` * `ui/src/router.tsx` * `ui/tsconfig.app.json` * `ui/tsconfig.json` * `ui/tsconfig.node.json` * `ui/vite.config.ts` </details> <details> <summary>💤 Files with no reviewable changes (1)</summary> * ui </details> </details> <!-- This is an auto-generated comment by CodeRabbit for review status -->
#10) The capability layer offered actions the API rejects. Align the UI with the backend so it never shows a control guaranteed to 403: - #10 usePermissions honors SUPER_ADMIN_MODE: the admin→all-partitions bypass (canRead/canWrite/canManageMembers, new canConfigurePartition) only applies when the backend flag is on, read from /config (admin-gated query). A plain admin without it is treated by their actual partition role, like the API does. - #2 documents/detail gates delete/replace on the caller's partition role and filters copy targets to partitions the user can write — parity with the list. - #3 partitions GeneralTab is role-aware: read-only for non-owners (banner, no Save, disabled inputs); preset/model registries (admin-only) are only fetched for admins so a member's view doesn't fire guaranteed-403 requests. Also fixes the partition-list role lookup (key is `partition`, not `name`) in GeneralTab and UsersTab, which #10 would otherwise leave undefined once the blanket is_admin bypass is gone. Mock /config exposes super_admin_mode top-level to match api/main.py.
There was a problem hiding this comment.
LGTM, approved!
I rechecked the Admin UI changes and the issues I previously reported all look fixed now.
The remaining backend improvements are already being handled in PR #588, so I don't see them as blockers for this Admin UI PR. Nice work!
Brings the new admin UI into the hexagonal refactor: a static SPA (served via nginx, same-origin API proxy) wired to the OpenRag API for partitions, documents, jobs, presets, models, users and system ops, with token + OIDC auth and a role-aware capability layer.
Highlights
/presets/options.Merge status: no conflicts with
refactor/hexagonal(disjoint divergence from #561). Recommend a merge commit to preserve history.Note: a few additive backend endpoints support the UI (partition document_count / chat fields, model-endpoint validation, etc.).
Summary by CodeRabbit