c5976882be
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>
6.9 KiB
6.9 KiB
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.
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 usedangerouslySetInnerHTMLwith user-supplied content. - Server-side
sanitize_strprovides 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:
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:
- Define a TypeScript interface for the response if it's new.
- Add a named export function (
getX,createX,updateX,deleteX). - Use
api.get<T>(...),api.post<T>(...), etc.; always.then((r) => r.data).
TanStack Query conventions
Query keys (flat arrays, lowercase):
["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:
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:
useQuery({ queryKey: ["services"], queryFn: getServices,
refetchInterval: 30_000, refetchIntervalInBackground: true });
Route guards
// 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
useStatefor UI-only state (edit mode, pending values, open/closed). - Server state via
useQuery/useMutation— no duplicated local copies. cn()fromlib/utils.tsfor conditional Tailwind classes.lucide-reactfor all icons.- Never use
dangerouslySetInnerHTMLwith user-supplied content.
Naming & Code Conventions
- TypeScript strict mode — no
any. - API response types inferred from interfaces in
client.tsonly. - 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 |