# 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/admin` | `DocumentAdminSettingsPage` | Admin only | | `/apps/ai/settings/admin` | `AIAdminSettingsPage` | Admin only | | `/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 - Admin settings link shown for admins regardless of health status ### 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=` - 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` **App cards — Extension button:** - `GET /api/plugins` is queried on the Apps page (already user-filtered by backend) - If an app's `id` matches a plugin `id`, an "Extension" button is shown on that card - Button links to `/settings/plugins/:id` alongside the existing admin "Settings" button - Only users with plugin access see the button (backend filters `GET /api/plugins`) ### 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 Admin Settings (`/apps/ai/settings/admin`) - Provider selector (lmstudio / ollama / anthropic) - Per-provider fields (base URL, model, API key) - Test Connection button (`POST /api/settings/ai/test`) - Save button ### Document Admin Settings (`/apps/documents/settings/admin`) - Upload Limits section only (max PDF size in MB) - Save button ### 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)