Skip to content

feat(admin-ui): vendored admin UI (partitions, documents, jobs, presets, models, users) on the OpenRag API - #586

Merged
andyne13 merged 99 commits into
refactor/hexagonalfrom
feat/admin-ui
Jun 29, 2026
Merged

feat(admin-ui): vendored admin UI (partitions, documents, jobs, presets, models, users) on the OpenRag API#586
andyne13 merged 99 commits into
refactor/hexagonalfrom
feat/admin-ui

Conversation

@andyne13

@andyne13 andyne13 commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

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

  • Partitions / documents / jobs / presets / models / users / system pages on the real API (poll-based jobs + document list).
  • Indexation & retrieval preset editors driven by /presets/options.
  • Recent fixes: keep paginated lists on the current page after a delete; sortable columns + bulk delete in the documents list; image-captioning toggle defaults to the backend's value (Image captioning can still run when disabled during indexing #453); poll the documents list so newly-indexed files appear; persist the selected partition across navigation; topic-tagging toggle grayed as 'coming soon'.

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

  • New Features
    • Added a revamped Admin UI with partitions, documents, jobs, users, presets, model endpoints, system status, and an overview.
    • Added admin-only “model endpoint validate (draft)” to probe endpoint settings.
  • Bug Fixes
    • Improved OIDC redirect URI guidance to reduce blank/404/JSON landings and infinite login loops.
    • Enhanced partition listing to return consistent partition summaries with document counts and safer missing-partition behavior.
  • Documentation
    • Updated OIDC and SSO quickstart guides with “front door” vs backend/proxy redirect URI examples and troubleshooting.

andyne13 added 30 commits June 22, 2026 18:32
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.
andyne13 added 8 commits June 25, 2026 09:44
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).
@coderabbitai

coderabbitai Bot commented Jun 26, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 958f49c7-5998-4608-b67f-586735325983

📥 Commits

Reviewing files that changed from the base of the PR and between 59c0f82 and 4371ef1.

📒 Files selected for processing (1)
  • ui/src/mocks/handlers.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • ui/src/mocks/handlers.ts

📝 Walkthrough

Walkthrough

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

Changes

Admin UI and API rollout

Layer / File(s) Summary
OIDC front-door guidance
CLAUDE.md, docs/content/docs/documentation/oidc.md, docs/content/docs/documentation/sso-quickstart.md, infra/compose/.env.example, openrag/core/config/auth.py
OIDC_REDIRECT_URI now describes the UI front door, callback reachability, and deployment-specific examples.
Partition summary plumbing
openrag/core/ports/partition_repo.py, openrag/services/persistence/partition_repo.py, openrag/services/orchestrators/partition_service.py, openrag/api/schemas/admin/partition_schemas.py, openrag/api/routers/admin/partitions.py, tests/unit/api/routers/admin/*partition*, tests/unit/services/orchestrators/test_partition_preset_resolution.py
Partition repositories, service, route, and tests now expose file counts and normalized summaries.
Draft model validation
openrag/api/schemas/admin/model_endpoint_schemas.py, openrag/api/routers/admin/model_endpoints.py, tests/unit/api/routers/admin/test_phase14_admin_routers.py
The model-endpoint validate route and request schema now accept draft endpoint data and forward it to validation.
UI build and deployment
infra/compose/*, infra/docker/*, tests/unit/infra/*, ui/.env.example, ui/.gitignore, ui/README.md, ui/components.json, ui/eslint.config.js, ui/index.html, ui/package.json, ui/tsconfig*.json, ui/vite.config.ts
The UI build, compose, and package configs now define the admin-ui image, front-door proxy, and local workspace setup.
UI shell, shared components, API clients, and pages
ui/src/*
The React bootstrap, auth/router helpers, shared primitives, typed API clients, MSW fixtures, and admin pages now render the login, settings, dashboard, system, partition, document, job, user, model, and preset screens.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Suggested labels

feat

Suggested reviewers

  • Ahmath-Gadji

Poem

🐰 Hop, hop—front doors glow,
callbacks land where rabbits go.
Tabs and toasts all dance in tune,
carrots tagged with config moon,
hop hooray—the UI blooms.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 18.84% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: adding a vendored admin UI for OpenRag across the listed admin areas.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/admin-ui

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.

Comment thread ui/src/pages/admin/jobs/list.tsx
Comment thread ui/src/pages/admin/documents/detail.tsx Outdated
Comment thread ui/src/pages/admin/partitions/detail.tsx
Comment thread ui/package.json Outdated
Comment thread ui/src/pages/admin/presets.tsx
Comment thread ui/src/pages/admin/models.tsx
Comment thread ui/src/pages/admin/models.tsx
Comment thread ui/src/pages/admin/users/detail.tsx
Comment thread ui/src/pages/admin/users/detail.tsx Outdated
Comment thread ui/src/lib/permissions.ts Outdated
…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).
@coderabbitai coderabbitai Bot added the feat Add a new feature label Jun 26, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

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 win

Document VITE_GRAFANA_URL here too.

ui/.env.example:1-2 and ui/src/pages/admin/system.tsx:20-45 already 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 lift

Block SSRF on the draft validation route.

body.endpoint is forwarded straight into service.validate_endpoint(), and that service performs a server-side GET {url}/models with 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 win

Don’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 only APP_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 to APP_iPORT=8080. Based on learnings, APP_iPORT is 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 win

Don't default the example to a cross-origin API base.

ui/src/lib/api/client.ts:1-90 uses a plain fetch() with no credentials: "include", so copying this file makes the app talk to http://localhost:8000 cross-origin and OIDC/session-cookie auth stops working. Leave VITE_API_BASE_URL empty 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 win

Mock partition creation never survives the follow-up config fetch.

createPartition() falls back to GET /partition/:name/config when 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 initial 201.

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 win

Document deletes don't update the backing fixture state.

The list endpoint reads from documents, but the delete endpoint only returns 204. 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 win

Rotate 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.tsx only keeps data.token in component state. In token mode, the next API call still uses the revoked value from TOKEN_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-foreground matches the destructive fill.

Any control using the normal bg-destructive text-destructive-foreground pairing 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 win

Don't treat every /users/info failure as a logout.

request() already distinguishes 401 from other failures, but load() nulls user on 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 win

Icon-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 or title.

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 win

Don't treat membership fetch failures as “not a member”.

The inner catch converts every network/4xx/5xx failure into null, 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 lift

Rollback here can delete a successfully created partition.

If the POST succeeds and the PATCH times out or the response is lost after the server commits, this catch still calls deletePartition(). 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 win

Render a failure state when the users query errors.

If listUsers() rejects, isLoading becomes false and this falls through to DataTable with [], which looks identical to “there are no users”. Handle isError/error separately 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 win

Label 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-label or 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 win

Validate quota before serializing the create payload.

type="number" still allows transient values like -, e, or 1e309. Number(quota) turns those into NaN/Infinity, and JSON.stringify serializes both as null, 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 win

Guard invalid quota strings here too.

This path has the same Number(quota) issue as the create dialog: NaN/Infinity are serialized as null, 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 win

Don't map every load failure to User not found.

Any 401/500/network error leaves user undefined here, so the page renders the same message as a real missing record. Branch on isError/error separately; 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 win

Normalize fetchOptions.headers before merging. RequestInit.headers can be a Headers object or tuple array, and casting it to Record<string, string> drops those values entirely; the Content-Type check is also case-sensitive, so a lowercase header can still get duplicated. Use new Headers(fetchOptions.headers) and has/set instead.

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

Handle the endpoints query error separately from the empty state.

When listModelEndpoints() fails, data stays undefined, isLoading becomes 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 win

Add 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 ideally title) 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 win

Resync the form state when partition changes.

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 win

Cap bulk-delete concurrency.

This fires one DELETE per 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 win

Ignore stale LLM validation responses.

Lines 175-204 let whichever validateStoredModelEndpoint() call finishes last overwrite llmValidated, even if the user has already picked another chatLlm. 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 win

Wire the dialog labels to their controls.

These Label components are rendered next to inputs/selects, but none of the fields have matching id/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/id pairing 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 win

Associate the visible text with these switches.

These toggles are rendered next to plain text Labels, but there is no id/htmlFor or aria-label linking 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 win

Handle preset query errors separately from the empty state.
listPresets() failures still fall through via data ?? [], 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 win

Add accessible names to the pager buttons.

Lines 194-209 render icon-only controls with no aria-label. Screen readers get two unnamed buttons, and ui/src/components/shared/data-table.test.tsx:15-20 already 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 win

Add 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 all from 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.py around 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.Dockerfile around 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 -->

Comment thread ui/src/components/ui/form.tsx
andyne13 added 2 commits June 29, 2026 11:55
#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.

@hedhoud hedhoud left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feat Add a new feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants