20 KiB
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/andplans/; this overview is the map.
Status
| # | Milestone | Status | Spec | Plan | Branch |
|---|---|---|---|---|---|
| 1 | Foundation | ✅ shipped | 2026-04-14-foundation-design.md | 2026-04-14-foundation.md | foundation |
| 1.5 | Test infrastructure | ✅ shipped | 2026-04-14-test-infrastructure-design.md | 2026-04-14-test-infrastructure.md | foundation |
| 2 | Profile | ✅ shipped | 2026-04-14-profile-design.md | 2026-04-14-profile.md | foundation |
| 3 | Events (user) | ✅ shipped | 2026-04-14-events-design.md | 2026-04-14-events.md | main |
| 4 | Admin events | ✅ shipped | 2026-04-15-admin-events-design.md | 2026-04-15-admin-events.md | main |
| 5 | Admin users & stats | ✅ shipped | 2026-04-15-admin-users-stats-design.md | 2026-04-15-admin-users-stats.md | main |
| 6 | Check-in / QR | ✅ shipped | 2026-04-16-checkin-qr-design.md | 2026-04-16-checkin-qr.md | main |
| 7 | User agenda submit | ✅ shipped | 2026-04-16-user-agenda-design.md | 2026-04-16-user-agenda.md | main |
| 8 | Workbench | ✅ shipped | 2026-04-16-workbench-design.md | 2026-04-16-workbench.md | main |
| 9 | Polish | ✅ shipped | 2026-04-18-polish-design.md | 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.tsloador formactions. No client-sidefetchto the API.+page.svelterenders server-provided data. createApiClient(event)from$lib/server/apiis the only way to talk to the backend. It injectsAuthorization: Bearer <event.locals.accessToken>, binds toevent.fetch, installs the 401 refresh interceptor, and uses${event.url.origin}/app/api/v1as baseUrl.- Reactive refresh, one code path. 401 interceptor in
createApiClientcallsrefreshSingleFlightfrom$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.tsbootstraps the session. Readsaccess_tokencookie → callsgetUserInfothrough the API client (transparently refreshes if stale) → setsevent.locals.user/event.locals.accessToken. Every request passes through it.(app)route group is the authed space. Its+layout.server.tsredirects anonymous users to/app/authorize?redirect_to=…. Every new feature route except/authorize,/token,/magic-link-sent,/logoutlives under(app)/.- Cookies are httpOnly,
path=/app,sameSite=lax,secure. Always usesetSessionCookies/clearSessionCookies/setOAuthStateCookiefrom$lib/server/session. - Unified SDK error handling via
option-t. Every hey-api call goes throughcallSdk(form actions, returnsResult<T, NormalizedError>) orloadSdk(loads, throws SvelteKiterror()on failure). Both from$lib/server/errors. Never hand-rolltry/catcharound an SDK call or read.data/.errordirectly. - Base path
/app/. Usebasefrom$app/pathsin templates; hardcoded/app/...is fine in server-sideredirect(). - Generated SDK is untouchable.
src/lib/api/**ispnpm gen's output; prettier + eslint ignore it. Never hand-edit. Regenerate withpnpm gen(requires VPN to10.0.0.10:8000). - Forms:
sveltekit-superforms+ Zod. Schemas insrc/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 whenNODE_ENV=testand strips the/app/api/v1prefix so test overrides target operation paths directly. Tests never hit the real backend — seeCLAUDE.md"Writing E2E tests" anddocs/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 usefont-displayitalic; identifiers (email, username, backend keys) usefont-mono; body isfont-sans. Introduced in M2;/authorizeand/magic-link-sentretrofitted to match.
Conventions
- Imports:
$lib/...forsrc/lib/,$lib/apifor the generated SDK,$lib/server/...for server-only modules. Nothing under$lib/server/may be imported from.svelteclient code. - Commits: conventional prefix (
feat:/fix:/chore:/docs:/refactor:/test:). IncludeCo-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>trailer when Claude authored the change. - Type safety: no
anywithout a cast-comment explaining why. Preferunknown+ narrowing.pnpm checkmust pass before every commit. - Svelte 5 runes mode is enforced project-wide (
svelte.config.js). Use$state,$derived,$props,$effect. No Svelte 4export let/$:. - DaisyUI 5 idioms used across milestones:
- Layouts:
hero bg-base-200 min-h-svh+hero-contentfor 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">); conditionalinput-errorclass;fieldset-label text-errorfor hints. - Alerts:
alert alert-{info,error,success} alert-soft. - Buttons:
btn btn-primary btn-blockfor submit;btn btn-ghost btn-circlefor icon buttons; inlinespan.loading.loading-spinner loading-smduring submission. - Drawer shell:
drawer lg:drawer-openwithis-drawer-close:/is-drawer-open:variants for icon-only collapse;is-drawer-close:tooltip is-drawer-close:tooltip-rightfor collapsed-state labels. - Navbar:
navbar-start/navbar-endregions;dropdown dropdown-end+avatar avatar-placeholderfor user menu.
- Layouts:
- Icons:
@lucide/svelte. Dynamic render:<item.icon class="size-4" />. - Interactive primitives:
bits-uifor dropdowns, dialogs, selects, tabs, tooltips, etc. — DaisyUI CSS classes layered on top. - Theme: supports both
darkandlightDaisyUI themes since M9.data-themeon<html>is SSR-set from thethemecookie (defaultdark). Toggle button in the navbar posts toPOST /app/themeaction; palette defined insrc/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'requiredas unknown as ZodObjectTypecast. 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, notpnpm build && preview.dev === trueactivates the Turnstile hidden-input bypass in/authorize. Production mode loads Cloudflare's script and breaks e2e. createClientis re-exported from$lib/api/client, NOT$lib/api/client.gen. The.gen.tsfile only exports the singleton.getUserInfo,postAuthMagic, etc. are standalone functions — hey-api does not attach them to the client instance. Call asgetUserInfo({ client: api }).- Node
fetchrejects relative URLs.event.fetchwraps that for same-origin relatives, but hey-api internals pass URLs to the underlying fetch in a way that loses origin-resolution.baseUrlmust be absolute:${event.url.origin}/app/api/v1. - Parallel SDK calls in one
loaddepend on single-flight refresh. If you fan outPromise.all([api.a(), api.b()])and the access token is stale, both 401 at once; single-flight collapses them into one/auth/refreshcall. 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 syncruns.pnpm checkis 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_tokenas a hidden input whenimport { dev } from '$app/environment'istrue. 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/tokenwithexpclaim (~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.dataautomatically. - Error envelope:
{ status: number, msg: string, data?: unknown }.normalizeSdkErrorextractsmsg.
Milestone workflow
For each new milestone:
- Brainstorm via
superpowers:brainstormingskill. Produce a design spec atdocs/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. - Get user approval on the spec before writing the plan.
- Plan via
superpowers:writing-plansskill. Produce an implementation plan atdocs/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. - Execute via
superpowers:subagent-driven-developmentskill (recommended) orsuperpowers:executing-plans. One implementer subagent per task; spec reviewer + code-quality reviewer between tasks. - Update this overview after each milestone ships: flip the status row, link the spec and plan, and add any new attention points that emerged.
- Branch strategy: feature milestones on a branch named after the milestone (e.g.,
profile,events,admin-events). Merge tomainafter 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.