cfec3bb906
- backend/app/routers/storage_config.py: 5 admin-only endpoints proxying storage-service config + migration API (GET/PATCH/POST/DELETE) - backend/app/main.py: register storage_config router - frontend/src/api/client.ts: StorageStatus, MigrationStatus, StorageBackendConfig interfaces + 5 API functions - frontend/src/pages/StorageAdminPage.tsx: full admin UI — backend health dot, driver selector (local/S3/WebDAV), conditional credential fields, Test & Migrate button, live 2s-poll migration progress bar, Cancel - frontend/src/App.tsx: /admin/storage route (AdminRoute guard) - CLAUDE.md: storage enforcement rule, updated Docker tables (6 services, 3 volumes), §20 in merge checklist - backend/CLAUDE.md, frontend/CLAUDE.md, doc-service/CLAUDE.md, ai-service/CLAUDE.md: updated to reflect storage-service integration - tests/ALL_TESTS.md + tests/storage-service_tests.md: §20 (20 tests) - backend/STATUS.md, frontend/STATUS.md: updated with new endpoints/routes - changelog/2026-04-20_storage-service.md: full change log Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
191 lines
8.0 KiB
Markdown
191 lines
8.0 KiB
Markdown
# frontend — Claude context
|
|
|
|
React 18 SPA built with Vite, port 5173 dev / 80 prod, served by nginx-unprivileged in production. All `/api/*` requests are proxied to `backend:8000`. See root `CLAUDE.md` for architecture, Docker, and project-wide workflows.
|
|
|
|
---
|
|
|
|
## Commands
|
|
|
|
All commands run inside Docker — never on the host.
|
|
|
|
```bash
|
|
docker compose exec frontend npm run typecheck
|
|
docker compose exec frontend npm run lint
|
|
```
|
|
|
|
---
|
|
|
|
## File & Folder Tree
|
|
|
|
```
|
|
frontend/
|
|
├── src/
|
|
│ ├── main.tsx ← React root, QueryClientProvider, BrowserRouter
|
|
│ ├── App.tsx ← Route tree, PrivateRoute, AdminRoute
|
|
│ ├── api/client.ts ← Native fetch wrapper (`request()`) + ALL API functions (single source of truth); no Axios
|
|
│ ├── hooks/
|
|
│ │ ├── useAuth.ts ← Token state (localStorage), login/logout
|
|
│ │ └── useTheme.ts ← Theme toggle
|
|
│ ├── components/
|
|
│ │ ├── AppShell.tsx ← Layout: Sidebar + SourcePanel (on /apps/documents) + main
|
|
│ │ ├── Sidebar.tsx ← Collapsible nav (icons ↔ icons+labels)
|
|
│ │ ├── SourcePanel.tsx ← Views + searchable category tree (docs route only)
|
|
│ │ ├── ManageCategoriesDialog.tsx ← Category CRUD modal (rename, delete)
|
|
│ │ ├── DocumentSlideOver.tsx ← Right slide-over: detail, edit, share, AI suggestions
|
|
│ │ ├── ThemeToggle.tsx ← Light/dark mode toggle
|
|
│ │ ├── PluginSchemaForm.tsx ← JSON Schema → React form (boolean/string/number/readOnly)
|
|
│ │ └── ui/ ← shadcn/ui components (Button, Input, …)
|
|
│ ├── pages/ ← One file per route
|
|
│ │ ├── DocServiceSettingsPage.tsx ← Combined doc-service settings: upload limits + watch directory
|
|
│ │ ├── PluginSettingsPage.tsx ← Generic plugin settings page driven by manifest
|
|
│ │ └── StorageAdminPage.tsx ← Admin storage backend config + live migration progress
|
|
│ ├── lib/utils.ts ← cn() = clsx + tailwind-merge
|
|
│ └── styles/theme.css ← CSS custom properties, Tailwind setup
|
|
├── vite.config.ts ← /api/* proxied to backend:8000
|
|
├── tailwind.config.ts
|
|
├── components.json ← shadcn/ui config
|
|
├── Dockerfile ← Multi-stage: Node build → nginx-unprivileged
|
|
└── STATUS.md
|
|
```
|
|
|
|
---
|
|
|
|
## Frontend Routes
|
|
|
|
| Path | Component | Guard |
|
|
|------|-----------|-------|
|
|
| `/login` | `LoginPage` | Public |
|
|
| `/` | `DashboardPage` | PrivateRoute |
|
|
| `/apps` | `AppsPage` | PrivateRoute |
|
|
| `/apps/documents` | `DocumentsPage` | PrivateRoute |
|
|
| `/apps/documents/settings` | `DocServiceSettingsPage` | ServiceAdminRoute (is_admin OR doc-service-admin member) |
|
|
| `/apps/ai/settings` | `AIAdminSettingsPage` | ServiceAdminRoute (is_admin OR ai-service-admin member) |
|
|
| `/profile` | `ProfilePage` | PrivateRoute |
|
|
| `/settings` | `SettingsPage` | PrivateRoute |
|
|
| `/settings/plugins/:id` | `PluginSettingsPage` | PrivateRoute (auth enforced per-plugin by backend) |
|
|
| `/admin` | `AdminPage` (→ `/admin/users`) | AdminRoute |
|
|
| `/admin/users` | `AdminUsersPage` | AdminRoute |
|
|
| `/admin/groups` | `AdminGroupsPage` | AdminRoute |
|
|
| `/admin/appearance` | `AdminAppearancePage` | AdminRoute |
|
|
| `/admin/storage` | `StorageAdminPage` | AdminRoute |
|
|
| `*` | redirect to `/` | — |
|
|
|
|
`PrivateRoute` — checks `token` from `useAuth`, redirects to `/login` if absent.
|
|
`AdminRoute` — checks token AND queries `GET /api/users/me` for `is_admin`; waits for query to avoid flash; redirects to `/login` (not `/`) if not admin.
|
|
|
|
---
|
|
|
|
## Security Standards
|
|
|
|
### XSS prevention
|
|
|
|
- React JSX text interpolation (`{value}`) is HTML-escaped by the DOM renderer — **never** use `dangerouslySetInnerHTML` with user-supplied content.
|
|
- Server-side `sanitize_str` provides defense-in-depth (control char stripping, max length).
|
|
|
|
---
|
|
|
|
## Frontend Patterns & Conventions
|
|
|
|
### API client (`src/api/client.ts`)
|
|
|
|
**No Axios** — uses a thin native `fetch` wrapper. All API calls live here, nowhere else.
|
|
|
|
The core `request()` function handles:
|
|
- Prepends `/api` base URL
|
|
- Injects `Authorization: Bearer {token}` from `localStorage` on every request
|
|
- **Global 401 handler**: clears `localStorage` token and redirects to `/login` via `window.location.href` — this is the expired-session redirect
|
|
- Throws `ApiError(status, detail)` on non-2xx responses (detail parsed from JSON body)
|
|
- Returns `undefined` on 204 No Content
|
|
- Supports `blob: true` for file download/preview responses
|
|
|
|
```typescript
|
|
// The internal api object — use these methods in exported functions:
|
|
api.get<T>(path, params?) // GET with optional query params object
|
|
api.post<T>(path, json?) // POST with JSON body
|
|
api.postForm<T>(path, URLSearchParams) // POST with form-encoded body (login)
|
|
api.postFile<T>(path, FormData) // POST with multipart body (file upload)
|
|
api.patch<T>(path, json?) // PATCH with JSON body
|
|
api.delete<T>(path) // DELETE
|
|
api.getBlob(path) // GET → Blob (download / view)
|
|
```
|
|
|
|
Adding a new API call:
|
|
1. Define a TypeScript interface for the response if it's new.
|
|
2. Add a named export function (`getX`, `createX`, `updateX`, `deleteX`).
|
|
3. Use the appropriate `api.*` method — return the promise directly (no `.then((r) => r.data)`).
|
|
|
|
Error handling in components: catch blocks receive an `ApiError` instance with `.status` and `.message` (the detail string).
|
|
|
|
### TanStack Query conventions
|
|
|
|
**Query keys** (flat arrays, lowercase):
|
|
```typescript
|
|
["me"] // current user
|
|
["services"] // service health list
|
|
["dashboard-prefs"] // user dashboard preferences
|
|
["categories"] // document categories
|
|
["documents", params] // document list (params object for cache isolation)
|
|
["documents-shared", params] // shared-with-me list
|
|
["document", id] // single document
|
|
["document-shares", id] // share list for a specific document
|
|
["my-groups"] // current user's group memberships (for share picker)
|
|
["plugins"] // accessible plugin list (filtered by user access)
|
|
["plugin-manifest", id] // plugin manifest (cached)
|
|
["plugin-settings", id] // plugin current settings
|
|
```
|
|
|
|
**Mutation pattern**:
|
|
```typescript
|
|
const mutation = useMutation({
|
|
mutationFn: apiFunction,
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ["affected-key"] });
|
|
// additional side effects (close dialog, reset form, etc.)
|
|
},
|
|
});
|
|
// Usage:
|
|
mutation.mutate(data);
|
|
mutation.isPending // show spinner / disable button
|
|
mutation.isError // show error message
|
|
```
|
|
|
|
**Polling**:
|
|
```typescript
|
|
useQuery({ queryKey: ["services"], queryFn: getServices,
|
|
refetchInterval: 30_000, refetchIntervalInBackground: true });
|
|
```
|
|
|
|
### Route guards
|
|
|
|
```typescript
|
|
// PrivateRoute — redirect to /login if no token
|
|
// AdminRoute — redirect to /login if no token OR not admin
|
|
// (waits for getMe() query to avoid flash; uses 404 semantics)
|
|
```
|
|
|
|
### Component patterns
|
|
|
|
- Functional components only.
|
|
- Local `useState` for UI-only state (edit mode, pending values, open/closed).
|
|
- Server state via `useQuery` / `useMutation` — no duplicated local copies.
|
|
- `cn()` from `lib/utils.ts` for conditional Tailwind classes.
|
|
- `lucide-react` for all icons.
|
|
- Never use `dangerouslySetInnerHTML` with user-supplied content.
|
|
|
|
---
|
|
|
|
## Naming & Code Conventions
|
|
|
|
- TypeScript strict mode — no `any`.
|
|
- API response types inferred from interfaces in `client.ts` only.
|
|
- Error messages displayed inline (no alert); loading shown as disabled state or "…" text.
|
|
- All user-facing text: safe via React JSX rendering (not innerHTML).
|
|
|
|
---
|
|
|
|
## Default Values & Limits
|
|
|
|
| Parameter | Value | Location |
|
|
|-----------|-------|----------|
|
|
| Token localStorage key | `"token"` | `useAuth.ts` |
|