# gig-admin Standalone admin dashboard for the gig-platform backend (`/api/admin/v1/*`). React 19 · TanStack Router · TanStack Query/Table · shadcn/ui · Tailwind v4 · TypeScript · Vite. It is a plain client-rendered SPA (`index.html` → `src/main.tsx` → `RouterProvider`). There is no SSR and no server functions — `@tanstack/react-start` is not used. > Working on this repo with an AI agent? `CLAUDE.md` has the architecture and conventions, > `AGENTS.md` the gotchas. ## Quick start ```bash cp .env.example .env # VITE_API_BASE_URL → backend origin pnpm install pnpm dev # http://localhost:3000 ``` Log in with an admin-role account (Passport password grant against `POST /api/admin/v1/auth/login/password`). To seed a local admin, run in the backend repo: ```bash ddev exec php artisan tinker storage/app/e2e-admin-seed.php # → admin@gig.local / Admin@12345 ``` An account that authenticates but holds no admin roles or permissions is treated as logged out — the app clears the session and returns you to the login screen. ## Scripts | Command | Purpose | | --- | --- | | `pnpm dev` | dev server on :3000 | | `pnpm build` / `pnpm preview` | production build / preview | | `pnpm typecheck` | `tsc --noEmit` | | `pnpm test` | vitest — API client (envelope / refresh / errors) and auth store | | `pnpm generate-routes` | `tsr generate` — rewrite `src/routeTree.gen.ts` (dev/build do this automatically) | | `pnpm gen:api` | regenerate `src/api/types.gen.ts` from the backend's private admin OpenAPI spec (`../gig-platform/storage/app/api-docs/admin/openapi.yaml`; run `ddev artisan api-docs:generate` there first) | | `pnpm cf:build` / `pnpm cf:deploy` | Cloudflare build / build + `wrangler deploy` | There is no ESLint config and no lint script: `pnpm typecheck && pnpm test` is the gate, and it is what CI runs. `tsconfig.json` is `strict` with `noUnusedLocals`/`noUnusedParameters`, so an unused import fails the typecheck. ## Uploads `api.upload(path, formData)` is the multipart path. `rawRequest` deliberately does **not** set `Content-Type` for a `FormData` body — the browser must set it itself so the multipart boundary is included; setting it by hand makes the server see an empty upload (a 422 rather than a 202). `fetch` reports no upload progress, so upload UIs stay indeterminate. ## Deploy The app is a client-rendered Vite SPA served as static assets by a Cloudflare Worker (`wrangler.jsonc`, `not_found_handling: single-page-application`). Cloudflare returns `dist/index.html` for every client route. **Manual deploy** (needs `wrangler login` or `CLOUDFLARE_API_TOKEN` / `CLOUDFLARE_ACCOUNT_ID` in your env): ```bash pnpm cf:deploy # vite build → wrangler deploy ``` **CI/CD** — every push & PR runs typecheck + test + build; pushes to `main` build and deploy to Cloudflare. Two mirrored pipelines are provided: | Platform | Workflow | | --- | --- | | GitHub Actions | `.github/workflows/ci.yml` | | Gitea Actions | `.gitea/workflows/ci.yml` | Configure these on the repo before the first deploy: | Type | Name | Value | | --- | --- | --- | | Variable | `VITE_API_BASE_URL` | backend API origin baked into the build | | Secret | `CLOUDFLARE_API_TOKEN` | token with "Edit Cloudflare Workers" scope | | Secret | `CLOUDFLARE_ACCOUNT_ID` | Cloudflare account id | - GitHub: *Settings → Secrets and variables → Actions*. - Gitea: *Repo → Settings → Actions → Secrets / Variables*. Actions must be enabled and an `act_runner` registered with the `ubuntu-latest` label (use an image such as `catthehacker/ubuntu:act-22.04`). `VITE_API_BASE_URL` is read at build time, so pointing a deployment at a different backend means rebuilding, not changing a runtime setting. ## Architecture notes - **Auth**: Bearer tokens in localStorage under `gig-admin-auth` (zustand persist). `src/api/client.ts` unwraps the `{success, data, message}` envelope, sends `X-Language-Locale`, and does a single-flight refresh-then-retry on 401 (`POST /api/v1/auth/refresh-token`, `client_machine_name=admin-api-client`). - **Permissions**: `GET /auth/me` returns `meta.roles` / `meta.permissions`; nav items and routes gate on them (`super-admin` passes everything). The key catalog lives in `src/auth/permissions.ts` and mirrors the backend's `Modules/{Module}/app/Permissions/`. - **Lists**: `useListPage` + `` bind to the backend's `pagination {current_page, page_size, …}` shape (`page`/`page_size` params). - **Errors**: `handleApiError` (`src/lib/errors.ts`) is the single mutation error path — 422 field errors land on the form, everything else becomes a toast. - **Generated types** (`types.gen.ts`) are reference-only (`@ts-nocheck` — scribe emits duplicate operationIds); hand-written shapes live in `src/api/types.ts`. - **UI copy** is hardcoded 简体中文. `i18next` and `next-themes` are installed but unused; theming is a `dark` class set by an inline script in `index.html`. ## Features Sidebar groups (each entry hidden unless the account holds its permission): - **分类管理** — 分类树 CRUD、待审核队列(通过/驳回/合并到已有分类) - **内容工作室** — 机器人作者、文章系列、文章、子话题、分类简报、风格预设、 Agent 接入令牌(签发一次性展示 / 吊销) - **专长交易** — 需求、派单、订单、服务商监控 - **Dimzou 内容** — 文档监控、发布监控、翻译 - **简历导入** — 批量导入任务 - **内容审核** — 被举报评论、被举报事件 - **用户与系统** — 用户列表、角色管理、用户角色、SID 管理(保留规则、幸运号码、 查询/分配)、语言设置、推送设备、机器人会话令牌 - **开发工具** — API 文档、数据库管理台 `/` has no dashboard: it redirects to the first nav route the account can see, and renders a "未分配权限" card only when there is none. Dashboard stats are still pending backend endpoints.