Files
gig-admin/AGENTS.md
T
kongkxandClaude Opus 5 5b3f5f7197 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
2026-09-06 21:16:29 +08:00

154 lines
19 KiB
Markdown

# 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:
- id: "@tanstack/devtools#devtools-app-setup"
run: "pnpm dlx @tanstack/intent@latest load @tanstack/devtools#devtools-app-setup"
for: "Install TanStack Devtools, pick framework adapter (React/Vue/Solid/Preact), register plugins via plugins prop, configure shell (position, hotkeys, theme, hideUntilHover, requireUrlFlag, eventBusConfig). TanStackDevtools component, defaultOpen, localStorage persistence."
- id: "@tanstack/devtools#devtools-marketplace"
run: "pnpm dlx @tanstack/intent@latest load @tanstack/devtools#devtools-marketplace"
for: "Publish plugin to npm and submit to TanStack Devtools Marketplace. PluginMetadata registry format, plugin-registry.ts, pluginImport (importName, type), requires (packageName, minVersion), framework tagging, multi-framework submissions, featured plugins."
- id: "@tanstack/devtools#devtools-plugin-panel"
run: "pnpm dlx @tanstack/intent@latest load @tanstack/devtools#devtools-plugin-panel"
for: "Build devtools panel components that display emitted event data. Listen via EventClient.on(), handle theme (light/dark), use @tanstack/devtools-ui components. Plugin registration (name, render, id, defaultOpen), lifecycle (mount, activate, destroy), max 3 active plugins. Two paths: Solid.js core with devtools-ui for multi-framework support, or framework-specific panels."
- id: "@tanstack/devtools#devtools-production"
run: "pnpm dlx @tanstack/intent@latest load @tanstack/devtools#devtools-production"
for: "Handle devtools in production vs development. removeDevtoolsOnBuild, devDependency vs regular dependency, conditional imports, NoOp plugin variants for tree-shaking, non-Vite production exclusion patterns."
- id: "@tanstack/devtools-event-client#devtools-bidirectional"
run: "pnpm dlx @tanstack/intent@latest load @tanstack/devtools-event-client#devtools-bidirectional"
for: "Two-way event patterns between devtools panel and application. App-to-devtools observation, devtools-to-app commands, time-travel debugging with snapshots and revert. structuredClone for snapshot safety, distinct event suffixes for observation vs commands, serializable payloads only."
- id: "@tanstack/devtools-event-client#devtools-event-client"
run: "pnpm dlx @tanstack/intent@latest load @tanstack/devtools-event-client#devtools-event-client"
for: "Create typed EventClient for a library. Define event maps with typed payloads, pluginId auto-prepend namespacing, emit()/on()/onAll()/onAllPluginEvents() API. Connection lifecycle (5 retries, 300ms), event queuing, enabled/disabled state, SSR fallbacks, singleton pattern. Unique pluginId requirement to avoid event collisions."
- id: "@tanstack/devtools-event-client#devtools-instrumentation"
run: "pnpm dlx @tanstack/intent@latest load @tanstack/devtools-event-client#devtools-instrumentation"
for: "Analyze library codebase for critical architecture and debugging points, add strategic event emissions. Identify middleware boundaries, state transitions, lifecycle hooks. Consolidate events (1 not 15), debounce high-frequency updates, DRY shared payload fields, guard emit() for production. Transparent server/client event bridging."
- id: "@tanstack/devtools-vite#devtools-vite-plugin"
run: "pnpm dlx @tanstack/intent@latest load @tanstack/devtools-vite#devtools-vite-plugin"
for: "Configure @tanstack/devtools-vite for source inspection (data-tsd-source, inspectHotkey, ignore patterns), console piping (client-to-server, server-to-client, levels), enhanced logging, server event bus (port, host, HTTPS), production stripping (removeDevtoolsOnBuild), editor integration (launch-editor, custom editor.open). Must be FIRST plugin in Vite config. Vite ^6 || ^7 only."
- id: "@tanstack/react-start#lifecycle/migrate-from-nextjs"
run: "pnpm dlx @tanstack/intent@latest load @tanstack/react-start#lifecycle/migrate-from-nextjs"
for: "Step-by-step migration from Next.js App Router to TanStack Start: route definition conversion, API mapping, server function conversion from Server Actions, middleware conversion, data fetching pattern changes."
- id: "@tanstack/react-start#react-start"
run: "pnpm dlx @tanstack/intent@latest load @tanstack/react-start#react-start"
for: "React bindings for TanStack Start: createStart, StartClient, StartServer, React-specific imports, re-exports from @tanstack/react-router, full project setup with React, useServerFn hook."
- id: "@tanstack/react-start#react-start/server-components"
run: "pnpm dlx @tanstack/intent@latest load @tanstack/react-start#react-start/server-components"
for: "Implement, review, debug, and refactor TanStack Start React Server Components in React 19 apps. Use when tasks mention @tanstack/react-start/rsc, renderServerComponent, createCompositeComponent, CompositeComponent, renderToReadableStream, createFromReadableStream, createFromFetch, Composite Components, React Flight streams, loader or query owned RSC caching, router.invalidate, structuralSharing: false, selective SSR, stale names like renderRsc or .validator, or migration from Next App Router RSC patterns. Do not use for generic SSR or non-TanStack RSC frameworks except brief comparison."
- id: "@tanstack/router-core#router-core"
run: "pnpm dlx @tanstack/intent@latest load @tanstack/router-core#router-core"
for: "Framework-agnostic core concepts for TanStack Router: route trees, createRouter, createRoute, createRootRoute, createRootRouteWithContext, addChildren, Register type declaration, route matching, route sorting, file naming conventions. Entry point for all router skills."
- id: "@tanstack/router-core#router-core/auth-and-guards"
run: "pnpm dlx @tanstack/intent@latest load @tanstack/router-core#router-core/auth-and-guards"
for: "Route protection with beforeLoad, redirect()/throw redirect(), isRedirect helper, authenticated layout routes (_authenticated), non-redirect auth (inline login), RBAC with roles and permissions, auth provider integration (Auth0, Clerk, Supabase), router context for auth state."
- id: "@tanstack/router-core#router-core/code-splitting"
run: "pnpm dlx @tanstack/intent@latest load @tanstack/router-core#router-core/code-splitting"
for: "Automatic code splitting (autoCodeSplitting), .lazy.tsx convention, createLazyFileRoute, createLazyRoute, lazyRouteComponent, getRouteApi for typed hooks in split files, codeSplitGroupings per-route override, splitBehavior programmatic config, critical vs non-critical properties."
- id: "@tanstack/router-core#router-core/data-loading"
run: "pnpm dlx @tanstack/intent@latest load @tanstack/router-core#router-core/data-loading"
for: "Route loader option, loaderDeps for cache keys, staleTime/gcTime/ defaultPreloadStaleTime SWR caching, pendingComponent/pendingMs/ pendingMinMs, errorComponent/onError/onCatch, beforeLoad, router context and createRootRouteWithContext DI pattern, router.invalidate, Await component, deferred data loading with unawaited promises."
- id: "@tanstack/router-core#router-core/navigation"
run: "pnpm dlx @tanstack/intent@latest load @tanstack/router-core#router-core/navigation"
for: "Link component, useNavigate, Navigate component, router.navigate, ToOptions/NavigateOptions/LinkOptions, from/to relative navigation, activeOptions/activeProps, preloading (intent/viewport/render), preloadDelay, navigation blocking (useBlocker, Block), createLink, linkOptions helper, scroll restoration, MatchRoute."
- id: "@tanstack/router-core#router-core/not-found-and-errors"
run: "pnpm dlx @tanstack/intent@latest load @tanstack/router-core#router-core/not-found-and-errors"
for: "notFound() function, notFoundComponent, defaultNotFoundComponent, notFoundMode (fuzzy/root), errorComponent, CatchBoundary, CatchNotFound, isNotFound, NotFoundRoute (deprecated), route masking (mask option, createRouteMask, unmaskOnReload)."
- id: "@tanstack/router-core#router-core/path-params"
run: "pnpm dlx @tanstack/intent@latest load @tanstack/router-core#router-core/path-params"
for: "Dynamic path segments ($paramName), splat routes ($ / _splat), optional params ({-$paramName}), prefix/suffix patterns ({$param}.ext), useParams, params.parse/stringify, pathParamsAllowedCharacters, i18n locale patterns."
- id: "@tanstack/router-core#router-core/search-params"
run: "pnpm dlx @tanstack/intent@latest load @tanstack/router-core#router-core/search-params"
for: "validateSearch, search param validation with Zod/Valibot/ArkType adapters, fallback(), search middlewares (retainSearchParams, stripSearchParams), custom serialization (parseSearch, stringifySearch), search param inheritance, loaderDeps for cache keys, reading and writing search params."
- id: "@tanstack/router-core#router-core/ssr"
run: "pnpm dlx @tanstack/intent@latest load @tanstack/router-core#router-core/ssr"
for: "Non-streaming and streaming SSR, RouterClient/RouterServer, renderRouterToString/renderRouterToStream, createRequestHandler, defaultRenderHandler/defaultStreamHandler, HeadContent/Scripts components, head route option (meta/links/styles/scripts), ScriptOnce, automatic loader dehydration/hydration, memory history on server, data serialization, document head management."
- id: "@tanstack/router-core#router-core/type-safety"
run: "pnpm dlx @tanstack/intent@latest load @tanstack/router-core#router-core/type-safety"
for: "Full type inference philosophy (never cast, never annotate inferred values), Register module declaration, from narrowing on hooks and Link, strict:false for shared components, getRouteApi for code-split typed access, addChildren with object syntax for TS perf, LinkProps and ValidateLinkOptions type utilities, as const satisfies pattern."
- id: "@tanstack/router-plugin#router-plugin"
run: "pnpm dlx @tanstack/intent@latest load @tanstack/router-plugin#router-plugin"
for: "TanStack Router bundler plugin for route generation and automatic code splitting. Supports Vite, Webpack, Rspack, and esbuild. Configures autoCodeSplitting, routesDirectory, target framework, and code split groupings."
- id: "@tanstack/start-client-core#start-core"
run: "pnpm dlx @tanstack/intent@latest load @tanstack/start-client-core#start-core"
for: "Core overview for TanStack Start: tanstackStart() Vite plugin, getRouter() factory, root route document shell (HeadContent, Scripts, Outlet), client/server entry points, routeTree.gen.ts, tsconfig configuration. Entry point for all Start skills."
- id: "@tanstack/start-client-core#start-core/auth-server-primitives"
run: "pnpm dlx @tanstack/intent@latest load @tanstack/start-client-core#start-core/auth-server-primitives"
for: "Server-side authentication primitives for TanStack Start: session cookies (HttpOnly, Secure, SameSite, __Host- prefix), session read/issue/destroy via createServerFn and middleware, OAuth authorization-code flow with state and PKCE, password-reset enumeration defense, CSRF for non-GET RPCs, rate limiting auth endpoints, session rotation on privilege change. Pairs with router-core/auth-and-guards for the routing side."
- id: "@tanstack/start-client-core#start-core/deployment"
run: "pnpm dlx @tanstack/intent@latest load @tanstack/start-client-core#start-core/deployment"
for: "Deploy to Cloudflare Workers, Netlify, Vercel, Node.js/Docker, Bun, Railway. Selective SSR (ssr option per route), SPA mode, static prerendering, ISR with Cache-Control headers, SEO and head management."
- id: "@tanstack/start-client-core#start-core/execution-model"
run: "pnpm dlx @tanstack/intent@latest load @tanstack/start-client-core#start-core/execution-model"
for: "Isomorphic-by-default principle, environment boundary functions (createServerFn, createServerOnlyFn, createClientOnlyFn, createIsomorphicFn), ClientOnly component, useHydrated hook, import protection, dead code elimination, environment variable safety (VITE_ prefix, process.env)."
- id: "@tanstack/start-client-core#start-core/middleware"
run: "pnpm dlx @tanstack/intent@latest load @tanstack/start-client-core#start-core/middleware"
for: "createMiddleware, request middleware (.server only), server function middleware (.client + .server), context passing via next({ context }), sendContext for client-server transfer, global middleware via createStart in src/start.ts, middleware factories, method order enforcement, fetch override precedence."
- id: "@tanstack/start-client-core#start-core/server-functions"
run: "pnpm dlx @tanstack/intent@latest load @tanstack/start-client-core#start-core/server-functions"
for: "createServerFn (GET/POST), validator (Zod or function), useServerFn hook, server context utilities (getRequest, getRequestHeader, setResponseHeader, setResponseStatus), error handling (throw errors, redirect, notFound), streaming, FormData handling, file organization (.functions.ts, .server.ts)."
- id: "@tanstack/start-client-core#start-core/server-routes"
run: "pnpm dlx @tanstack/intent@latest load @tanstack/start-client-core#start-core/server-routes"
for: "Server-side API endpoints using the server property on createFileRoute, HTTP method handlers (GET, POST, PUT, DELETE), createHandlers for per-handler middleware, handler context (request, params, context), request body parsing, response helpers, file naming for API routes."
- id: "@tanstack/start-server-core#start-server-core"
run: "pnpm dlx @tanstack/intent@latest load @tanstack/start-server-core#start-server-core"
for: "Server-side runtime for TanStack Start: createStartHandler, request/response utilities (getRequest, setResponseHeader, setCookie, getCookie, useSession), three-phase request handling, AsyncLocalStorage context."
- id: "@tanstack/virtual-file-routes#virtual-file-routes"
run: "pnpm dlx @tanstack/intent@latest load @tanstack/virtual-file-routes#virtual-file-routes"
for: "Programmatic route tree building as an alternative to filesystem conventions: rootRoute, index, route, layout, physical, defineVirtualSubtreeConfig. Use with TanStack Router plugin's virtualRouteConfig option."
<!-- intent-skills:end -->