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:
2026-09-06 21:16:29 +08:00
co-authored by Claude Opus 5
parent 6932906327
commit 5b3f5f7197
3 changed files with 174 additions and 13 deletions
+56
View File
@@ -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: