Remove the "Extensions" section from the sidebar nav. Instead, each app card on the Apps page shows an "Extension" button when the current user has access to that app's plugin (matched by service ID). The button links to /settings/plugins/:id alongside the existing admin Settings button. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
10 KiB
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 inlocalStorage - Logout clears token and redirects to
/login GET /api/users/meverifies token on protected routes
Home dashboard (/)
Personalised landing page per user:
- Time-aware greeting with the user's display name (
full_nameor email). React JSX text rendering HTML-escapes all values — nodangerouslySetInnerHTMLis 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 viaPATCH /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=<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/pluginsis queried on the Apps page (already user-filtered by backend)- If an app's
idmatches a pluginid, an "Extension" button is shown on that card - Button links to
/settings/plugins/:idalongside 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
useStatefor UI-only state (editing mode, filter params, etc.) - Token —
localStorage, read byuseAuthhook, 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 tohttpOnlycookie 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
- UI component library: shadcn/ui + Tailwind CSS — installed and wired up
- AppShell + Sidebar replacing inline Nav component
- Light/dark theme context with OS preference detection
- 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/jobsintegration — show AI processing queue status / progress per document- Advanced filter: extracted data fields (vendor, due date, amount) — needs backend support
- 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)
httpOnlycookie auth (requires backend change)- Bulk document operations (select multiple, bulk delete / bulk categorise)