# 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 2–7. 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--design.md`) → plan (`plans/YYYY-MM-DD-.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/`. 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 `, 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`) 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) ` 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 `