docs: add CLAUDE.md, expand AGENTS.md, correct stale README
The repo had no agent-facing project guidance: AGENTS.md contained only the generated TanStack Intent skill pointers, and README.md described a stack this app does not use. - CLAUDE.md (new): API client contract (envelope, ApiError, single-flight 401 refresh, the FormData Content-Type trap), the PERM catalog mirroring the backend permission classes, the route + NAV_GROUPS + PERM triad that must be edited together to add a page, and the useListPage/DataTable pattern. - AGENTS.md: gotchas above the generated block, which is left untouched — global query retry policy, can() vs useCan() reactivity, the gig-admin-auth persist key, and that jsdom is opted into per file via a `// @vitest-environment jsdom` pragma since there is no vitest config. - README.md: @tanstack/react-start is not a dependency (this is a plain Vite SPA, no SSR); cf:deploy no longer copies a shell to index.html; the feature list was a milestone-1 snapshot that omitted five shipped nav groups and still called moderation reports a future milestone. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_013MADG54Y51wZkU3ViRjeSi
This commit is contained in:
@@ -1,3 +1,59 @@
|
||||
# AGENTS.md
|
||||
|
||||
See `CLAUDE.md` for the architecture and command reference. This file adds gotchas and non-obvious conventions not worth repeating there.
|
||||
|
||||
The block at the bottom of this file, between the `intent-skills` markers, is generated by TanStack Intent — leave it alone and edit only above it.
|
||||
|
||||
## It is not TanStack Start
|
||||
|
||||
`@tanstack/react-start` is **not** a dependency. This is a plain Vite SPA: `index.html` → `src/main.tsx` → `createRoot` → `RouterProvider`, with TanStack **Router** (+ `@tanstack/router-plugin`) for routing only. There is no SSR, no server functions, no loaders running server-side — don't reach for `createServerFn`, `createMiddleware`, or `.server.ts` files. `getRouter()` in `src/router.tsx` is a plain `createTanStackRouter`.
|
||||
|
||||
## Query behaviour is configured once, in `src/router.tsx`
|
||||
|
||||
- `staleTime: 30_000` globally — don't re-declare it per query.
|
||||
- The retry predicate **never retries 401 / 403 / 404 / 422**; everything else retries twice. If you add a query that must not retry, express it through the status code rather than a local `retry: false`.
|
||||
- `defaultPreload: 'intent'` with `defaultPreloadStaleTime: 0`, so hovering a nav link refetches. Route `beforeLoad` guards run on preload too — keep them cheap and side-effect free (`requirePermission` only reads the store).
|
||||
|
||||
## Mutations go through `handleApiError`
|
||||
|
||||
`src/lib/errors.ts` is the single error path for mutations: a 422 with `errors` populates react-hook-form fields (when you pass `setError`), 403 becomes a fixed `无权限执行此操作` toast, anything else toasts the backend's localized `message`. Don't hand-roll `try/catch` + `toast` in components.
|
||||
|
||||
## Auth-store subtleties
|
||||
|
||||
- Persisted to `localStorage` under **`gig-admin-auth`** (zustand `persist`). Clearing that key is the way to force a logged-out state in the browser.
|
||||
- `isAuthenticated()` deliberately returns **false** for a session that has a `user` but zero roles *and* zero permissions — a backend account with no admin grants is treated as not logged in, and `_authed.tsx` logs it out with a toast.
|
||||
- `can()` (imperative, for guards/nav) and `useCan()` (reactive, for components) are separate on purpose. Using `can()` inside a component won't re-render when `/auth/me` refreshes permissions — `_authed.tsx` subscribes to `s.permissions` explicitly for exactly that reason.
|
||||
|
||||
## The 401 refresh hits the *public* API prefix
|
||||
|
||||
`tryRefresh()` in `src/api/client.ts` posts to `/api/v1/auth/refresh-token`, not `/api/admin/v1/...`, with `client_machine_name: 'admin-api-client'`. It is single-flight — concurrent 401s share one attempt — and each request retries at most once before the session is cleared. Login and refresh must pass `anonymous: true` or they would recurse.
|
||||
|
||||
## Uploads
|
||||
|
||||
Pass a `FormData` to `api.upload()` and do not set `Content-Type` anywhere. The client detects `FormData` and skips the header so the browser can supply the multipart boundary; setting it by hand makes the server see an empty upload — a 422 where you expected a 202. `fetch` exposes no upload progress, so upload UIs must be indeterminate.
|
||||
|
||||
## Tests
|
||||
|
||||
- **There is no vitest config file.** jsdom is opted into per file with a `// @vitest-environment jsdom` pragma on line 1 — omit it and the test runs in node and fails on DOM globals.
|
||||
- The API tests stub `fetch` with `vi.stubGlobal` and reset `useAuthStore` in `beforeEach`; follow that rather than mocking the `api` module, since the envelope/refresh logic is the thing under test.
|
||||
|
||||
## Generated files
|
||||
|
||||
- `src/routeTree.gen.ts` — written by the router plugin on dev/build (or `pnpm generate-routes`). Never hand-edit; never resolve merge conflicts in it by hand, regenerate instead.
|
||||
- `src/api/types.gen.ts` — `@ts-nocheck`, reference only. Scribe emits duplicate operationIds so it does not typecheck; authoritative shapes are hand-written in `src/api/types.ts`. Regenerating needs the sibling `../gig-platform` checkout to have run `ddev artisan api-docs:generate` first.
|
||||
|
||||
## Theme
|
||||
|
||||
Dark mode is a `dark` class on `<html>`, set by an inline script in `index.html` before first paint (reads `localStorage.theme`, falls back to `prefers-color-scheme`) to avoid a flash. The toggle in `_authed.tsx` writes that same key. Any new theming must go through the class, not a React-held theme value — `next-themes` is installed but unused.
|
||||
|
||||
## Chinese UI copy
|
||||
|
||||
All user-facing strings are hardcoded 简体中文, matching the backend's localized `message` values. `i18next` / `react-i18next` are in `package.json` but nothing imports them — don't introduce `useTranslation` for one screen.
|
||||
|
||||
## No linter
|
||||
|
||||
There is no ESLint config and no lint script. `pnpm typecheck && pnpm test` is the whole gate, locally and in CI (`.github/workflows/ci.yml`, mirrored in `.gitea/workflows/ci.yml`). `tsconfig.json` runs `strict` plus `noUnusedLocals` / `noUnusedParameters`, so unused imports are type errors, not warnings.
|
||||
|
||||
<!-- intent-skills:start -->
|
||||
# TanStack Intent - before editing files, run the matching guidance command.
|
||||
tanstackIntent:
|
||||
|
||||
Reference in New Issue
Block a user