Split monolithic CLAUDE.md into per-service sub-files
Root CLAUDE.md now contains only project-wide concerns (stack, architecture, Docker, workflows, security hook). Service-specific details moved to: - backend/CLAUDE.md — DB models, API endpoints, JWT/bcrypt, naming conventions - frontend/CLAUDE.md — routes, TanStack Query patterns, XSS prevention - features/ai-service/CLAUDE.md — queue endpoints, provider notes - features/doc-service/CLAUDE.md — document models, PDF limits, proxy endpoints Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
# 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 ← Axios instance + ALL API functions (single source of truth)
|
||||
│ ├── 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
|
||||
│ ├── 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 |
|
||||
| `*` | 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`)
|
||||
|
||||
Single Axios instance — **all** API calls live here, nowhere else:
|
||||
|
||||
```typescript
|
||||
const api = axios.create({ baseURL: "/api" });
|
||||
api.interceptors.request.use((config) => {
|
||||
const token = localStorage.getItem("token");
|
||||
if (token) config.headers.Authorization = `Bearer ${token}`;
|
||||
return config;
|
||||
});
|
||||
```
|
||||
|
||||
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 `api.get<T>(...)`, `api.post<T>(...)`, etc.; always `.then((r) => r.data)`.
|
||||
|
||||
### 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` |
|
||||
Reference in New Issue
Block a user