Files
cms-client/docs/superpowers/overview.md

116 lines
20 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# cms-client-svelte — Rewrite Overview
> Synthesis of the rewrite's milestone plan, cross-cutting architecture, and conventions. Primary audience: future agentic sessions spawning specs / plans for milestones 27. Per-milestone specs and plans live alongside this file in `specs/` and `plans/`; this overview is the map.
## Status
| # | Milestone | Status | Spec | Plan | Branch |
| --- | ------------------- | ---------- | ------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------- | ------------ |
| 1 | Foundation | ✅ shipped | [2026-04-14-foundation-design.md](specs/2026-04-14-foundation-design.md) | [2026-04-14-foundation.md](plans/2026-04-14-foundation.md) | `foundation` |
| 1.5 | Test infrastructure | ✅ shipped | [2026-04-14-test-infrastructure-design.md](specs/2026-04-14-test-infrastructure-design.md) | [2026-04-14-test-infrastructure.md](plans/2026-04-14-test-infrastructure.md) | `foundation` |
| 2 | Profile | ✅ shipped | [2026-04-14-profile-design.md](specs/2026-04-14-profile-design.md) | [2026-04-14-profile.md](plans/2026-04-14-profile.md) | `foundation` |
| 3 | Events (user) | ✅ shipped | [2026-04-14-events-design.md](specs/2026-04-14-events-design.md) | [2026-04-14-events.md](plans/2026-04-14-events.md) | `main` |
| 4 | Admin events | ✅ shipped | [2026-04-15-admin-events-design.md](specs/2026-04-15-admin-events-design.md) | [2026-04-15-admin-events.md](plans/2026-04-15-admin-events.md) | `main` |
| 5 | Admin users & stats | ✅ shipped | [2026-04-15-admin-users-stats-design.md](specs/2026-04-15-admin-users-stats-design.md) | [2026-04-15-admin-users-stats.md](plans/2026-04-15-admin-users-stats.md) | `main` |
| 6 | Check-in / QR | ✅ shipped | [2026-04-16-checkin-qr-design.md](specs/2026-04-16-checkin-qr-design.md) | [2026-04-16-checkin-qr.md](plans/2026-04-16-checkin-qr.md) | `main` |
| 7 | User agenda submit | ✅ shipped | [2026-04-16-user-agenda-design.md](specs/2026-04-16-user-agenda-design.md) | [2026-04-16-user-agenda.md](plans/2026-04-16-user-agenda.md) | `main` |
| 8 | Workbench | ✅ shipped | [2026-04-16-workbench-design.md](specs/2026-04-16-workbench-design.md) | [2026-04-16-workbench.md](plans/2026-04-16-workbench.md) | `main` |
| 9 | Polish | ✅ shipped | [2026-04-18-polish-design.md](specs/2026-04-18-polish-design.md) | [2026-04-18-polish.md](plans/2026-04-18-polish.md) | `develop` |
Each milestone: **brainstorm → spec (`specs/YYYY-MM-DD-<topic>-design.md`) → plan (`plans/YYYY-MM-DD-<topic>.md`) → subagent-driven execution → merge**. See "Milestone workflow" at the bottom.
## Milestone roadmap
**M1 — Foundation (shipped).** SvelteKit 2 + Svelte 5 (runes) app skeleton under base path `/app/`. Magic-link OAuth auth with httpOnly cookies and reactive 401 refresh. DaisyUI 5 drawer shell (icon-only collapsible sidebar, avatar dropdown with logout). Generated fetch SDK (hey-api) at `src/lib/api/`. Unified SDK error handling via `option-t`. Vitest (session helpers) + Playwright (auth smoke) rails. Out of scope: feature routes, Storybook, container/Caddy updates.
**M1.5 — Test infrastructure (shipped).** Programmable mock backend (Hono on `:4010`) with override-only semantics — no defaults, no state, no backend semantics. Vite proxy switches to the mock when `NODE_ENV=test` and strips `/app/api/v1` so overrides target operation paths. Test-side typed helpers (`mock.override`, `mock.requests`, `mock.clear`); custom Playwright `test` fixture auto-clears mock state and exposes a cookie-injection `loggedInUser` fixture. M1's auth smoke migrated; full magic-link pipeline verified end-to-end against the mock. Out of scope: backend-like state in the mock, openapi-driven defaults (Prism evaluated and rejected), per-worker mock isolation.
**M2 — Profile (shipped).** `/profile/[userId]` is the canonical URL for every profile (own + other); `/profile` is a bare convenience redirect to `/profile/<own_user_id>`. View shows avatar + permission badge + meta rows (email/username/public-flag) + copy-profile-link button; edit (self only) mutates username/nickname/subtitle/avatar/allow_public via a superforms-bound form. Introduces the three-face type system (Fraunces display, IBM Plex Sans body, IBM Plex Mono identifiers) with Noto SC fallbacks; M1 auth pages + navbar brand retrofitted. Out of scope (still): bio editing (→ M8), admin cross-edit (never), avatar upload.
**M3 — Events (user-facing) (shipped).** `/events` list with tab filter (all / joined), `/events/[eventId]` detail with hero, markdown description, attendance guide, and sticky sidebar. Join flow branches on `enable_kyc`: simple confirm dialog (`JoinDialog`) or five-stage KYC dialog (`KycDialog`, `createKycState` runes factory with `$effect` polling loop). `EventCard` three-zone layout with per-card KYC isolation. `POST /kyc-status` server proxy for token-safe KYC polling. Server actions `?/join` and `?/kycSession` with `Buffer`-based base64 codec. 9 E2E tests. Out of scope: `/joined-events` route, agenda, check-in QR, pagination, admin CRUD.
**M4 — Admin events.** `/admin/events` list + CRUD, `/admin/events/new`, `/admin/events/[eventId]` edit + agenda + attendance + stats subpages. Permission gate: `PARTY_EVENT_HOLDER` (Lv30) and up; `OFFICIAL_ADMIN` (Lv40) sees all events, others see their own. Markdown editor for event body (replace `@uiw/react-md-editor`; candidates: `bytemd`, `marked` + preview pane). DnD for agenda reordering (replace `@dnd-kit/*`; candidates: `svelte-dnd-action`). Out of scope: user/permissions admin (M5).
**M5 — Admin users & stats (shipped).** `/admin/users` list with sort, filter, pagination, and inline permission-level edit (gated by editor level — Lv40 assigns ≤Lv30, Lv50 assigns all); server-side validation of assignable levels. `/admin/stats` global dashboard with total-user count and event table. Route group `(admin-lv40)` guards both pages at Lv40+. `getAssignableLevels` helper in `$lib/permissions.ts`. 9 E2E tests covering list, inline edit, and permission-gate matrix. Out of scope: event-specific stats (M4), charts/visualizations (deferred).
**M6 — Check-in / QR (shipped).** `/checkin` staff scanner page (Lv20+, `(staff-lv20)` route group) with `@zxing/browser` camera viewfinder and 6-box `OtpInput.svelte` manual fallback; `?/submit` form action calls `POST /event/checkin/submit`. Attendee QR dialog on event detail page: fetches code via server proxy routes, renders QR with `qrcode` package, polls every 3 s, auto-closes on check-in. "扫码签到" sidebar entry for Lv20+. 7 E2E tests. Out of scope: attendance list (M4), per-event scanner scoping, scan history.
**M7 — User agenda submission (shipped).** Two surfaces added to `/events/[eventId]` with no new routes. (a) **Submission workflow:** "提交议程" button (visible only when joined, agenda not published, event not started) opens a superforms dialog with `name` (max 255) + `description` (required); submitted items appear in a "我的议程" sidebar card with status badges (待审核 / 已通过 / 已拒绝) and edit button for pending items. (b) **Published schedule:** once `is_agenda_published = true`, an "活动议程" timeline card renders in the main content column, visible to any user; descriptions rendered from base64 markdown via `marked`. Blocking rules (published, started, 5-pending) enforced both client-side (`canSubmit` derived) and server-side in `?/submitAgenda`. `?/editAgenda` skips re-checks; backend enforces ownership. Load extended with parallel `getEventGuide` + `getAgendaMyList` fan-out (joined-only) and sequential `getAgendaSchedule` (published-only). 12 E2E tests. Out of scope: admin review / approval / scheduling (M4 shipped), per-event backend quota changes.
**M8 — Workbench (工作台) (shipped).** Replace the Foundation placeholder at `/` with the full four-card dashboard. Also adds bio editing to the profile form (missed in M2). Data comes from two server `load` calls: `GET /user/info` (already available via `event.locals.user`) and `GET /event/list` (paginated, offset 0, same SDK as M3). Four cards: (a) **Welcome** — personalised greeting (`nickname ?? username`), joined-event count, quick links to `/events` and `/profile`; (b) **Current event** — finds the ongoing or nearest upcoming joined event, shows name/times/check-in status, "立即签到" shortcut for staff-eligible users; (c) **Upcoming schedule** — next 3 joined future events sorted by `start_time`, status badges (待开始 / 进行中 / 已签到), each links to `/events/[eventId]`; (d) **Profile completeness** — progress bar + checklist of 4 fields (nickname, subtitle, avatar, bio), CTA to `/profile`. All event derivation (ongoing detection, upcoming slice, completion %) happens in `+page.server.ts` `load`; no client-side fetch. E2E tests cover empty states and data-populated paths. NixOS snowflake watermark (bottom-right, low opacity) matching reference design. Out of scope: infinite-scroll pagination on the events feed, real-time check-in status updates.
**M9 — Polish (shipped).** Light/dark theme toggle via SSR cookie (`theme` cookie, `POST /app/theme` action, DaisyUI `data-theme`). Production Containerfile (Node 22 slim, multi-stage build) + Caddyfile (reverse-proxy to `:3000`, forward `/app/api/*` to backend, serve static assets). `.containerignore` added. All 78 unit tests and 77 E2E tests pass. Two residual E2E failures fixed during verification: auth test missing `GET /event/list` override, profile test strict-mode username ambiguity. Out of scope: performance audits, `API_BASE_URL` env redesign.
## Cross-cutting architecture (invariants)
Every feature milestone inherits these from M1 — **do not redesign them per-feature**:
- **SSR-first.** All data fetching in `+page.server.ts` `load` or form `actions`. No client-side `fetch` to the API. `+page.svelte` renders server-provided data.
- **`createApiClient(event)` from `$lib/server/api`** is the only way to talk to the backend. It injects `Authorization: Bearer <event.locals.accessToken>`, binds to `event.fetch`, installs the 401 refresh interceptor, and uses `${event.url.origin}/app/api/v1` as baseUrl.
- **Reactive refresh, one code path.** 401 interceptor in `createApiClient` calls `refreshSingleFlight` from `$lib/server/session`. Single-flight collapses concurrent refreshes per-process, keyed on the refresh-token string. No proactive JWT decoding, no duplicate refresh logic anywhere.
- **`hooks.server.ts` bootstraps the session.** Reads `access_token` cookie → calls `getUserInfo` through the API client (transparently refreshes if stale) → sets `event.locals.user` / `event.locals.accessToken`. Every request passes through it.
- **`(app)` route group is the authed space.** Its `+layout.server.ts` redirects anonymous users to `/app/authorize?redirect_to=…`. Every new feature route except `/authorize`, `/token`, `/magic-link-sent`, `/logout` lives under `(app)/`.
- **Cookies are httpOnly, `path=/app`, `sameSite=lax`, `secure`.** Always use `setSessionCookies` / `clearSessionCookies` / `setOAuthStateCookie` from `$lib/server/session`.
- **Unified SDK error handling via `option-t`.** Every hey-api call goes through `callSdk` (form actions, returns `Result<T, NormalizedError>`) or `loadSdk` (loads, throws SvelteKit `error()` on failure). Both from `$lib/server/errors`. Never hand-roll `try/catch` around an SDK call or read `.data` / `.error` directly.
- **Base path `/app/`.** Use `base` from `$app/paths` in templates; hardcoded `/app/...` is fine in server-side `redirect()`.
- **Generated SDK is untouchable.** `src/lib/api/**` is `pnpm gen`'s output; prettier + eslint ignore it. Never hand-edit. Regenerate with `pnpm gen` (requires VPN to `10.0.0.10:8000`).
- **Forms: `sveltekit-superforms` + Zod.** Schemas in `src/lib/schemas/`. Server: `superValidate(request, zod(schema))`. Client: `superForm(data.form, { dataType: 'form' })`.
- **E2E tests run against a programmable mock backend** (`scripts/mock-server.ts`, `tests/e2e/helpers/`). Vite proxy auto-flips when `NODE_ENV=test` and strips the `/app/api/v1` prefix so test overrides target operation paths directly. Tests never hit the real backend — see `CLAUDE.md` "Writing E2E tests" and `docs/superpowers/specs/2026-04-14-test-infrastructure-design.md`.
- **Three-face typography (Fraunces / IBM Plex Sans / IBM Plex Mono)** with Noto SC fallbacks. Utilities: `font-display` / `font-sans` / `font-mono`. Page titles + meta labels use `font-display` italic; identifiers (email, username, backend keys) use `font-mono`; body is `font-sans`. Introduced in M2; `/authorize` and `/magic-link-sent` retrofitted to match.
## Conventions
- **Imports:** `$lib/...` for `src/lib/`, `$lib/api` for the generated SDK, `$lib/server/...` for server-only modules. Nothing under `$lib/server/` may be imported from `.svelte` client code.
- **Commits:** conventional prefix (`feat:` / `fix:` / `chore:` / `docs:` / `refactor:` / `test:`). Include `Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>` trailer when Claude authored the change.
- **Type safety:** no `any` without a cast-comment explaining why. Prefer `unknown` + narrowing. `pnpm check` must pass before every commit.
- **Svelte 5 runes mode** is enforced project-wide (`svelte.config.js`). Use `$state`, `$derived`, `$props`, `$effect`. No Svelte 4 `export let` / `$:`.
- **DaisyUI 5 idioms used across milestones:**
- Layouts: `hero bg-base-200 min-h-svh` + `hero-content` for full-screen centered (auth pages, error).
- Cards: `card card-border bg-base-100 shadow-xl` + `card-body` + `card-title` + `card-actions`.
- Forms: `fieldset fieldset` > `legend fieldset-legend` + composite `<label class="input">` (icon child + `<input class="grow">`); conditional `input-error` class; `fieldset-label text-error` for hints.
- Alerts: `alert alert-{info,error,success} alert-soft`.
- Buttons: `btn btn-primary btn-block` for submit; `btn btn-ghost btn-circle` for icon buttons; inline `span.loading.loading-spinner loading-sm` during submission.
- Drawer shell: `drawer lg:drawer-open` with `is-drawer-close:` / `is-drawer-open:` variants for icon-only collapse; `is-drawer-close:tooltip is-drawer-close:tooltip-right` for collapsed-state labels.
- Navbar: `navbar-start` / `navbar-end` regions; `dropdown dropdown-end` + `avatar avatar-placeholder` for user menu.
- **Icons:** `@lucide/svelte`. Dynamic render: `<item.icon class="size-4" />`.
- **Interactive primitives:** `bits-ui` for dropdowns, dialogs, selects, tabs, tooltips, etc. — DaisyUI CSS classes layered on top.
- **Theme:** supports both `dark` and `light` DaisyUI themes since M9. `data-theme` on `<html>` is SSR-set from the `theme` cookie (default `dark`). Toggle button in the navbar posts to `POST /app/theme` action; palette defined in `src/routes/layout.css`.
- **Internationalization:** UI copy is zh-CN (matching the React project). Keep error messages short and native-feeling; avoid mixing English unless it's a proper noun.
## Attention points (gotchas surfaced during M1)
- **Hyphenated Zod keys break superforms types.** `'cf-turnstile-response'` required `as unknown as ZodObjectType` cast. Prefer camelCase keys (`turnstileToken`) and rename at the FormData layer if necessary; rename the existing cast-case in a future pass.
- **Playwright webServer must be `pnpm dev`, not `pnpm build && preview`.** `dev === true` activates the Turnstile hidden-input bypass in `/authorize`. Production mode loads Cloudflare's script and breaks e2e.
- **`createClient` is re-exported from `$lib/api/client`, NOT `$lib/api/client.gen`.** The `.gen.ts` file only exports the singleton.
- **`getUserInfo`, `postAuthMagic`, etc. are standalone functions** — hey-api does not attach them to the client instance. Call as `getUserInfo({ client: api })`.
- **Node `fetch` rejects relative URLs.** `event.fetch` wraps that for same-origin relatives, but hey-api internals pass URLs to the underlying fetch in a way that loses origin-resolution. `baseUrl` must be absolute: `${event.url.origin}/app/api/v1`.
- **Parallel SDK calls in one `load` depend on single-flight refresh.** If you fan out `Promise.all([api.a(), api.b()])` and the access token is stale, both 401 at once; single-flight collapses them into one `/auth/refresh` call. Without it, the second call would race against a rotated refresh token. The backend's grace-use window is an integration assumption (below); single-flight is the defense-in-depth.
- **Svelte 5 diagnostic lag.** IDE often reports stale "Cannot find module './$types'" or "Binding element implicitly has any type" after `svelte-kit sync` runs. `pnpm check` is authoritative — if it passes, ignore the editor squiggles.
- **Short access token lifetime (~15s)** means most requests trigger a refresh. Don't build UI that expects <5ms server roundtrips; backend is on internal network but refresh is still an extra hop.
- **Turnstile dev bypass** uses the literal string `turnstile_token` as a hidden input when `import { dev } from '$app/environment'` is `true`. Backend must accept this in dev mode. Do not regress this for production.
## Backend integration assumptions
- Backend at `http://10.0.0.10:8000`, reachable only via internal network / VPN. Vite dev proxy mounts it at `/app/api/*`; Caddy does the same in prod.
- Access token is a JWT issued by `POST /auth/token` with `exp` claim (~15s).
- Refresh-token rotation **tolerates one grace use** of the previous refresh token to cover multi-tab / parallel-request races (single-flight within a process handles the common case).
- Response envelope: `{ status: UtilsRespStatus, data: T }` — hey-api unwraps `.data` automatically.
- Error envelope: `{ status: number, msg: string, data?: unknown }`. `normalizeSdkError` extracts `msg`.
## Milestone workflow
For each new milestone:
1. **Brainstorm** via `superpowers:brainstorming` skill. Produce a design spec at `docs/superpowers/specs/YYYY-MM-DD-<topic>-design.md`. Use the Foundation spec as the template for structure, not content — re-derive the feature details for the new milestone.
2. **Get user approval on the spec** before writing the plan.
3. **Plan** via `superpowers:writing-plans` skill. Produce an implementation plan at `docs/superpowers/plans/YYYY-MM-DD-<topic>.md`. Include: task list with bite-sized steps, TDD where applicable, exact code for each step, verification commands, commit messages. Context7 for any new library docs.
4. **Execute** via `superpowers:subagent-driven-development` skill (recommended) or `superpowers:executing-plans`. One implementer subagent per task; spec reviewer + code-quality reviewer between tasks.
5. **Update this overview** after each milestone ships: flip the status row, link the spec and plan, and add any new attention points that emerged.
6. **Branch strategy:** feature milestones on a branch named after the milestone (e.g., `profile`, `events`, `admin-events`). Merge to `main` after the milestone verifies.
Canonical references when in doubt:
- `../CLAUDE.md` — repo-level conventions (loaded into every Claude session).
- `~/.claude/CLAUDE.md` — user's milestone discipline.
- `specs/2026-04-14-foundation-design.md` + `plans/2026-04-14-foundation.md` — template shape for new milestones.