c45236651b
- Auto-create {service-id}-admin groups at startup (group_bootstrap.py)
- get_service_admin() dep: grants access to superusers OR service group members
- /api/settings/ai and /api/settings/documents/limits now allow service admins
- AI service exposes /plugin/manifest (ai-service-admin access group)
- DocServiceSettingsPage: combined upload limits + watch directory on one page
- ServiceAdminRoute in frontend guards new /apps/documents/settings and /apps/ai/settings
- Single Settings button per app card (visible to admins and service group members)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
216 lines
10 KiB
Markdown
216 lines
10 KiB
Markdown
# Frontend — Status
|
||
|
||
## What it is
|
||
|
||
React 18 + TypeScript + Vite SPA styled with **shadcn/ui** (Radix primitives) and **Tailwind CSS v3**. Design tokens are CSS custom properties supporting light/dark themes. In dev it runs on port `5173` and proxies `/api/*` to `backend:8000`. In prod it is served by nginx on port `80`.
|
||
|
||
All API calls go through `src/api/client.ts` (single Axios instance, JWT injected via request interceptor from `localStorage`).
|
||
|
||
---
|
||
|
||
## Routes
|
||
|
||
| Path | Component | Auth |
|
||
|------|-----------|------|
|
||
| `/login` | `LoginPage` | Public |
|
||
| `/` | `DashboardPage` | Required |
|
||
| `/apps` | `AppsPage` | Required |
|
||
| `/apps/documents` | `DocumentsPage` | Required |
|
||
| `/apps/documents/settings` | `DocServiceSettingsPage` | ServiceAdminRoute (is_admin OR doc-service-admin) |
|
||
| `/apps/ai/settings` | `AIAdminSettingsPage` | ServiceAdminRoute (is_admin OR ai-service-admin) |
|
||
| `/admin` | `AdminPage` (redirects to `/admin/users`) | Admin only |
|
||
| `/admin/users` | `AdminUsersPage` | Admin only |
|
||
| `/admin/groups` | `AdminGroupsPage` | Admin only |
|
||
| `/profile` | `ProfilePage` | Required |
|
||
| `/settings` | `SettingsPage` (placeholder) | Required |
|
||
| `/settings/plugins/:id` | `PluginSettingsPage` | Required (per-plugin access control) |
|
||
|
||
`PrivateRoute` redirects to `/login` when no token. `AdminRoute` redirects to `/` when not admin.
|
||
|
||
---
|
||
|
||
## Current functionality
|
||
|
||
### Auth
|
||
|
||
- Login form (`POST /api/auth/login`) stores JWT in `localStorage`
|
||
- Logout clears token and redirects to `/login`
|
||
- `GET /api/users/me` verifies token on protected routes
|
||
|
||
### Home dashboard (`/`)
|
||
|
||
Personalised landing page per user:
|
||
- Time-aware greeting with the user's display name (`full_name` or email). React JSX text rendering HTML-escapes all values — no `dangerouslySetInnerHTML` is used anywhere on this page.
|
||
- Grid of **pinned app cards** drawn from `GET /api/services`, filtered to the user's saved list.
|
||
- **Customize mode** (pencil button): shows all services; `+` / `−` toggle buttons on each card; changes committed with **Save** via `PATCH /api/users/me/preferences`.
|
||
- Empty-state prompt when no apps are pinned.
|
||
|
||
### Apps page (`/apps`)
|
||
|
||
Cards are rendered dynamically from `GET /api/services` (polled every 30 s via TanStack Query):
|
||
- **healthy=true + app_path set** — clickable card with "Available" badge
|
||
- **healthy=true + no app_path** — non-clickable card (e.g. AI Service — no user UI)
|
||
- **healthy=false** — non-clickable, dimmed card with "Unavailable" badge and explanation text
|
||
- Single **Settings** button per card — visible to global admins OR members of the service's admin group (checked via `GET /api/plugins` which backend filters by access). Links to `svc.settings_path`.
|
||
|
||
### Sidebar navigation
|
||
|
||
`Apps` is an expandable accordion in the sidebar:
|
||
- **Documents** sub-item (expandable) — lists all user categories beneath it; clicking a category navigates to `/apps/documents?category_id=<id>`
|
||
- AI Service is not listed (no openable UI)
|
||
- Sections auto-open when navigating to their route
|
||
- In collapsed (icons-only) mode, clicking the Apps icon navigates to `/apps`
|
||
|
||
### Documents page (`/apps/documents`)
|
||
|
||
**Upload:** PDF file input, 202 response, error display.
|
||
|
||
**Filter bar:**
|
||
- Search input (400ms debounce) — matches title, filename, tags, document_type
|
||
- Status dropdown (all / pending / processing / done / failed)
|
||
- Type dropdown (all / invoice / bill / receipt / order / expense / revenue / unknown)
|
||
- Sort selector (upload date / processed date / title / filename / file size / type / status)
|
||
- Asc/Desc toggle
|
||
- "Clear filters" button (appears when any filter is active)
|
||
|
||
**Pagination:** Prev/Next with "X–Y of Z" count. Only shown when total > per_page.
|
||
|
||
**Document row (collapsed):**
|
||
- Inline title editor (pencil icon, Enter to save, Esc to cancel; shows filename in italic when no title)
|
||
- Status badge (colour-coded)
|
||
- Document type label
|
||
- File size
|
||
- View button (opens PDF in new tab via blob URL — auth-gated)
|
||
- Download button
|
||
- Delete button (confirm dialog)
|
||
|
||
**Document row (expanded):**
|
||
- **Extracted data table** — all AI-extracted JSON fields (excludes `tags`, `suggested_categories`)
|
||
- **Error message** — shown if status=failed
|
||
- **Categories** — assigned chips with remove; dropdown to assign existing; AI-suggested chips with Accept / Create & Assign / Dismiss
|
||
- **Status polling** — auto-refetches every 3s while status is pending/processing; invalidates document list on done/failed
|
||
|
||
### AI Service Settings (`/apps/ai/settings`)
|
||
|
||
Accessible to global admins and `ai-service-admin` group members (`ServiceAdminRoute`).
|
||
- Provider selector (lmstudio / ollama / anthropic)
|
||
- Per-provider fields (base URL, model, API key)
|
||
- Test Connection button (`POST /api/settings/ai/test`)
|
||
- Save button
|
||
|
||
### Document Service Settings (`/apps/documents/settings`)
|
||
|
||
Accessible to global admins and `doc-service-admin` group members (`ServiceAdminRoute`).
|
||
Combined settings on one page, accessed via the single "Settings" button on the app card:
|
||
- **Upload Limits** — max PDF size in MB (`GET/PATCH /api/settings/documents/limits`)
|
||
- **Watch Directory** — file watcher config rendered via `PluginSchemaForm` from manifest (`GET/PATCH /api/plugins/doc-service/settings`)
|
||
|
||
### Admin — Users page (`/admin/users`)
|
||
|
||
- User list with role and active status
|
||
- Inline active status toggle
|
||
- Create user form (email, name, password, admin flag)
|
||
- Delete user
|
||
|
||
### Admin — Groups page (`/admin/groups`)
|
||
|
||
- Group list with name, description, member count
|
||
- Create group (name, optional description)
|
||
- Edit group name / description inline panel
|
||
- Delete group (with confirmation)
|
||
- Expand group row to manage members: view members, remove members, add non-members from dropdown
|
||
|
||
### Profile page (`/profile`)
|
||
|
||
- Display and edit personal information
|
||
|
||
---
|
||
|
||
## API client (`src/api/client.ts`)
|
||
|
||
Key functions:
|
||
|
||
| Function | Description |
|
||
|----------|-------------|
|
||
| `listDocuments(params)` | `GET /documents` — returns `DocumentPage`; supports `category_id` filter |
|
||
| `uploadDocument(file)` | `POST /documents/upload` |
|
||
| `deleteDocument(id)` | `DELETE /documents/{id}` |
|
||
| `downloadDocument(id, filename)` | Blob URL download |
|
||
| `viewDocument(id)` | Blob URL → `window.open`, auto-revoke after 60s |
|
||
| `getDocumentStatus(id)` | Poll endpoint |
|
||
| `listCategories()` | All categories for user |
|
||
| `createCategory(name)` | Create category |
|
||
| `assignCategory(docId, catId)` | Assign |
|
||
| `removeCategory(docId, catId)` | Remove |
|
||
| `updateDocumentTags(id, tags)` | `PATCH /documents/{id}/tags` |
|
||
| `updateDocumentTitle(id, title)` | `PATCH /documents/{id}/title` |
|
||
| `confirmFolderSuggestion(docId)` | `POST /documents/{id}/suggestions/folder/confirm` |
|
||
| `rejectFolderSuggestion(docId)` | `POST /documents/{id}/suggestions/folder/reject` |
|
||
| `confirmFilenameSuggestion(docId)` | `POST /documents/{id}/suggestions/filename/confirm` |
|
||
| `rejectFilenameSuggestion(docId)` | `POST /documents/{id}/suggestions/filename/reject` |
|
||
| `getAISettings()` | `GET /settings/ai` (masked) |
|
||
| `updateAISettings(data)` | `PATCH /settings/ai` |
|
||
| `testAIConnection()` | `POST /settings/ai/test` |
|
||
| `getDocumentLimits()` | `GET /settings/documents/limits` |
|
||
| `adminListGroups()` | `GET /admin/groups` |
|
||
| `adminCreateGroup(data)` | `POST /admin/groups` |
|
||
| `adminGetGroup(id)` | `GET /admin/groups/{id}` with members |
|
||
| `adminUpdateGroup(id, data)` | `PATCH /admin/groups/{id}` |
|
||
| `adminDeleteGroup(id)` | `DELETE /admin/groups/{id}` |
|
||
| `adminAddGroupMember(gId, uId)` | `POST /admin/groups/{gId}/members/{uId}` |
|
||
| `adminRemoveGroupMember(gId, uId)` | `DELETE /admin/groups/{gId}/members/{uId}` |
|
||
| `updateDocumentLimits(data)` | `PATCH /settings/documents/limits` |
|
||
| `getPlugins()` | `GET /plugins` — list accessible plugins |
|
||
| `getPluginManifest(id)` | `GET /plugins/{id}/manifest` |
|
||
| `getPluginSettings(id)` | `GET /plugins/{id}/settings` |
|
||
| `updatePluginSettings(id, data)` | `PATCH /plugins/{id}/settings` |
|
||
|
||
---
|
||
|
||
## State management
|
||
|
||
- **TanStack Query** — all server state; `queryKey: ["documents", params]` for cache isolation per filter/page combination
|
||
- **No global store** — local `useState` for UI-only state (editing mode, filter params, etc.)
|
||
- **Token** — `localStorage`, read by `useAuth` hook, injected by Axios interceptor
|
||
|
||
---
|
||
|
||
## Component inventory
|
||
|
||
| Component | Path | Description |
|
||
|-----------|------|-------------|
|
||
| `AppShell` | `src/components/AppShell.tsx` | Layout wrapper: Sidebar + scrollable main content |
|
||
| `Sidebar` | `src/components/Sidebar.tsx` | Collapsible left nav; includes dynamic "Extensions" section |
|
||
| `ThemeToggle` | `src/components/ThemeToggle.tsx` | Sun/moon ghost icon button; persists to localStorage |
|
||
| `PluginSchemaForm` | `src/components/PluginSchemaForm.tsx` | JSON Schema → React form (boolean/string/number/readOnly fields) |
|
||
| `PluginSettingsPage` | `src/pages/PluginSettingsPage.tsx` | Generic plugin settings page (manifest-driven) |
|
||
| `Button` | `src/components/ui/button.tsx` | shadcn/ui Button (default, ghost, outline, destructive) |
|
||
| `Input` | `src/components/ui/input.tsx` | shadcn/ui Input |
|
||
|
||
## Known limitations / not implemented
|
||
|
||
- **JWT in `localStorage`** — XSS risk; migrate to `httpOnly` cookie when backend supports it
|
||
- **No toast / notification system** — errors shown inline; success is silent
|
||
- **No loading skeletons** — "Loading…" text only
|
||
- **No app permission UI** per group — groups exist but permission grants are not yet implemented
|
||
- **No app permission UI** — all apps visible to all authenticated users
|
||
|
||
---
|
||
|
||
## Future work
|
||
|
||
- [x] UI component library: shadcn/ui + Tailwind CSS — installed and wired up
|
||
- [x] AppShell + Sidebar replacing inline Nav component
|
||
- [x] Light/dark theme context with OS preference detection
|
||
- [x] Generic plugin infrastructure: Extensions sidebar section, PluginSchemaForm, PluginSettingsPage
|
||
- [ ] Suggestion badges in DocumentsPage for `suggested_folder` / `suggested_filename` (confirm/reject buttons)
|
||
- [ ] Toast notification system (upload success, save feedback, errors)
|
||
- [ ] Loading skeletons
|
||
- [ ] `POST /queue/jobs` integration — show AI processing queue status / progress per document
|
||
- [ ] Advanced filter: extracted data fields (vendor, due date, amount) — needs backend support
|
||
- [x] Groups admin UI — list, create, edit, delete, add/remove members
|
||
- [ ] App permissions UI per group (blocked on backend group_app_permissions)
|
||
- [ ] Document sharing UI (blocked on backend)
|
||
- [ ] `httpOnly` cookie auth (requires backend change)
|
||
- [ ] Bulk document operations (select multiple, bulk delete / bulk categorise)
|