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

20 KiB
Raw Permalink Blame History

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 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.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.