Files
Business-Management/frontend/CLAUDE.md
T
curo1305 479108779f Replace Axios with native fetch; add global 401 session-expiry redirect
All API calls now go through a thin request() wrapper around native fetch.
Removes the axios dependency entirely. The wrapper injects the JWT on every
request and — the key fix — clears localStorage and redirects to /login on
any 401 response, so expired sessions no longer leave users on broken pages.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 21:04:18 +02:00

7.8 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                  ← 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
│   ├── 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)

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
// 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):

["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 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