Username: 3–32 → 5–255. Nickname: max 64 → max 24 (UTF-8 rune limit).
Applied to both onboarding and profile schemas.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Also fixes the `completeProfile` form action: SvelteKit only supports form
actions in `+page.server.ts`, not `+layout.server.ts`. Moved the action to a
dedicated `/(app)/onboarding/+page.server.ts` and updated the dialog form's
`action` attribute to the absolute path `/app/onboarding?/completeProfile`.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Routes Sentry envelopes through a first-party /app/vitals endpoint
instead of directly to *.ingest.sentry.io. DSN host and project ID
are validated server-side from the SENTRY_DSN env var before proxying.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Thin primary-color bar at top of viewport appears after 200 ms of
navigation latency and snaps away when the new page renders. Uses
nprogress wired to SvelteKit's navigating store via a Svelte 5
$effect. Fast navigations (< 200 ms) produce no visible flash.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
badge-neutral on the dark theme renders with insufficient contrast;
badge-warning (amber) is both readable and semantically correct for
an awaiting-review state.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
After creating an event, redirect to /admin/events instead of the new
event's edit page, which was confusing since the user hasn't configured
the event yet. Adds E2E test to assert the post-create redirect.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
window.onerror reports script URLs as absolute URLs, so f.filename is
"https://test.nix.org.cn/cdn-cgi/..." — startsWith('/cdn-cgi/') never
matched and Cloudflare email-decode noise kept reaching Sentry.
Fixes NIXCN-CMS-TEST-1
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Reject QR codes whose raw text is not exactly 6 digits (no strip-and-guess)
- Suppress repeated onScan calls for the same code within 10 s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Documents the production 401-on-rotation race, the four-layer client-side
fix (proactive refresh, in-process single-flight, rotation cache, 401
interceptor), the backend AT lifetime extension to 5min, and every
alternative considered with the reasoning behind each rejection.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Backend extended TTL_ACCESS to 300s. Matching the client-side constants
keeps proactive refresh in sync — fires 30s before expiry instead of
5s before a 15s token, reducing refresh frequency by ~20x.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The backend does not implement a refresh-token grace window. Without it,
two browser requests sent within the same ~15s window but in separate
HTTP round-trips both carry the same expired access token. The first
request successfully rotates the token; the backend immediately invalidates
the old token; the second request arrives after the first response's
Set-Cookie has been sent but before the browser has applied it, so it
still presents the old token — and the backend rejects the refresh.
The recentlyRotated cache (previous commit) covers same-process sequential
races. This commit adds the primary defence:
Proactive refresh in hooks.server.ts: decode the access token's iat claim
(payload only, no signature verification) and rotate when ≤5s remain on
the 15s lifetime, before any API call is made. All parallel load functions
in the same request then see a freshly-issued token. The single-flight in
refreshSingleFlight collapses simultaneous proactive refreshes from
concurrent browser requests on the same pod. The 401 interceptor in
api.ts remains as a safety net for unexpected cases (clock skew, etc.).
Adds getJwtIat and isTokenAboutToExpire helpers with full unit tests.
Updates CLAUDE.md to document the two-path refresh design.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Root cause (confirmed from backend logs): when two browser requests are
in-flight simultaneously with the same expired access token, the second
request arrives at the backend ~1s after the first refresh completed.
By then the backend's grace window has closed and it rejects the already-
rotated refresh token with 401.
The existing single-flight mechanism only collapses truly concurrent
refreshes (requests that arrive before the first promise resolves). It
cannot help a sequential caller that arrives after the first refresh
completes and its inFlight entry is deleted.
Fix: keep a process-level recentlyRotated cache keyed by the old refresh
token for 10 seconds after a successful rotation. A later caller with the
same old token gets the cached pair immediately, without a second backend
call that would be rejected.
Adds a regression test that reproduces the exact scenario.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
`currentColor` inherits black from the browser's favicon rendering context.
Hardcode #7ebae4 (NixOS light blue) so the icon is recognisable in the tab bar.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When refreshSingleFlight fails, we previously returned null with no
context. Now each branch emits a console.warn with the relevant detail
(HTTP status, missing token pair, or thrown error message). Sentry's
consoleLoggingIntegration picks these up automatically, so the next
refresh failure will show the backend's actual rejection status in Sentry.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fixes NIXCN-CMS-TEST-1: add beforeSend filter that drops events where
every stack frame originates from a Cloudflare /cdn-cgi/ script —
pure third-party noise from the email-decode injector.
Fixes NIXCN-CMS-TEST-2: when an API call inside a load function gets a
401 (session expired, refresh failed), loadSdk now throws redirect(303)
to /app/authorize instead of error(401). This gives the user a working
login redirect rather than an error page, and prevents SvelteKit from
passing the deserialized HttpError object to handleError where Sentry
was incorrectly capturing it as an unexpected exception.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces the Vite-processed $lib/assets/favicon.svg import with a direct
reference to static/nixos.svg via the base path, adding SVG MIME type.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Corrects misspelling "Nix CN CMS" → "NixCN CMS" across all source files,
docs, and tests; adds <title>NixCN CMS</title> to the root layout; documents
the correct spelling in CLAUDE.md.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Track verified state via on:turnstile-callback; button shows 等待 Turnstile...
spinner on load and remains disabled until CF widget succeeds or errors.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
PUBLIC_TURNSTILE_SITE_KEY uses $env/dynamic/public (runtime), not
static/build-time injection, so it doesn't need to be a build ARG.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Declare build-time ARG instructions for PUBLIC_TURNSTILE_SITE_KEY,
VITE_SENTRY_DSN, SENTRY_DSN, SENTRY_ORG, SENTRY_PROJECT, and
SENTRY_AUTH_TOKEN so CI can pass them via --build-arg. TURNSTILE_SECRET_KEY
and SENTRY_DSN are runtime-only and injected via -e at container run time.
Also removes TURNSTILE_SECRET_KEY from .env.example (runtime secret,
not a build-time value).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
superForm() and createKycState() called during component initialisation
were triggering reactive tracking warnings in Svelte 5 because they
access reactive state internally. Wrapping with untrack() prevents
spurious re-runs of the initialisation logic on subsequent reactive
updates.
Also converts the data.form cast in admin/events/new to $derived so it
stays in sync with SvelteKit page invalidations.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- overview.md: M9 row flipped to shipped, links to spec/plan added,
roadmap paragraph updated with actual deliverables, conventions
updated to reflect dual-theme support
- tests/e2e/auth.spec.ts: add GET /event/list override so the dashboard
renders after the magic-link → token flow (was 500ing with no override)
- tests/e2e/profile.spec.ts: use { exact: true } on username assertion to
avoid strict-mode violation (username 'alice' matched 3 elements)
- Formatting: prettier --write pass on polish spec/plan, layout.css,
layout.svelte
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Multi-stage Dockerfile for production builds with adapter-node, reverse proxy config with Caddy, and optimized Docker context.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Cookie propagation across the use:enhance redirect chain (authorize →
/token → /) may not settle before Playwright proceeds. Waiting for
networkidle after the submit click gives the chain time to complete
before asserting on the URL and user-menu elements.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ProfileCard never renders the permission level label, so the '普通用户'
assertion always fails. Replace with assertion on loggedInUser.username which
is guaranteed to be rendered.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fix 1: mock items in 'agenda tab lists items' now use status/description
fields instead of is_published, matching the page's filter logic.
Fix 2: 'agenda create submits form' replaced with 'approve button opens
approve dialog' since the admin agenda page has no 新增 button — it only
supports review (approve/reject) of user-submitted items.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The attendance tab test was providing a mock response with shape
{ status: 200, data: [ ... ] } (flat array). But +page.server.ts for
the admin events page casts the response to read inner?.items ?? [],
expecting shape { status: 200, data: { items: [ ... ] } }.
Updated the mock to wrap the attendance array in an { items: [...] }
object to match the server-side expectation.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The "立即签到" button is rendered by bits-ui Dialog.Trigger as a
<button>, not <a>. Change getByRole('link') to getByRole('button')
on lines 120, 133, and 149 for selector consistency.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
POST /app/theme sets the theme cookie and redirects back. Sun/Moon
button in the app navbar submits the form. E2E tests verify SSR
cookie-driven theme switching.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Reads theme cookie in hooks.server.ts and rewrites data-theme on <html>
before the page is sent. Root and (app) layout servers expose theme so
the navbar can show the correct toggle icon. Removes hardcoded
color-scheme meta (DaisyUI CSS handles it per-theme).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Covers three deliverables: light theme + SSR cookie toggle (form-action
approach, transformPageChunk in hooks.server.ts), multi-stage Dockerfile
+ Caddyfile for adapter-node production deploy, and fixes for 6 failing
E2E tests (option b: tests corrected to match existing code — wrong mock
shapes, wrong Playwright role selectors, and tests for features that were
never built).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace raw JSON pre-block with typed KycInfoDetail component for passport/cnrid info
- Upgrade KYC method selection from dropdown to card-based picker with icons
- Fix passport form loading state: replace '...' with Spinner component
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Prevents the unauthorized error page from briefly flashing before the
logout interceptor navigates away, by checking token presence synchronously
in beforeLoad and throwing a redirect immediately.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Uses getErrorMessage from the new exception.gen.ts dictionary to display
Chinese error descriptions instead of raw 13-char error codes in GlobalError.
Adds gen:errors script and supporting files for regenerating the dictionary.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- useUpdateUserById: new hook for PATCH /user/update/{user_id} (admin updating others)
- user-permission-edit: switch to useUpdateUserById, move user_id from body to path
- useAuthToken: extract bare useMutation in token route into a proper data hook
- edit-profile: rename shadowed callback param (no behavior change)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Update the admin agenda list to use the regenerated user_profile payload so submitter nicknames render correctly and link to the submitter's public profile.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Use the generated agenda list query key so approve/reject, schedule, and edit mutations invalidate the correct cache entry. Sync the generated client types with the updated agenda list response shape.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Regenerate client types making required fields non-optional
- Add checkin_count/join_count fields to EventListItems
- Pass event_id in agenda approve/reject mutations
- Use non-null assertion for eventId in event update form
- Replace kyc_status with kyc_info in attendance story examples
- Remove kycStatusFilter prop from attendance list stories
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Pending agendas should always be reviewable by admins. The previous
!isPublished guard hid approve/reject buttons even when pending items
existed after agenda publication.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
15 tasks covering Cypress setup, 92 test cases across 5 permission
levels, seed SQL, and Docker Compose CI configuration.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Switch from local-only to CI execution with Docker Compose isolation.
Replace manual test data setup with SQL dump (seed.sql) including
relative timestamps for ongoing event. Add docker-compose.e2e.yml note.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Covers all user permission levels (Lv10/20/30/40/50), 12 feature
modules, and 92 test cases (happy path + error states) using Cypress
against a real backend with fixed test accounts.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Sticky tabs now offset by --header-height to avoid hiding behind app header
- Replace type filter buttons with Tabs component for consistent styling
- Use isLoading + keepPreviousData in useAdminEvents to prevent list flash on page/filter change
- Expand trello skill label inference rules
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove KYC status filter dropdown and status column from attendance list
- Conditionally render KYC type and operation columns based on event's enable_kyc field
- Add keepPreviousData to useAdminAttendance to prevent input focus loss on filter change
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Regenerated API client: ServiceEventEventListResponse now wraps paginated
items (renamed to ServiceEventEventListItems), event info includes quota/limit,
attendance list includes checked_in_at, joined_at, kyc_status
- Fixed toEventInfo to use renamed ServiceEventEventListItems type
- Added required quota (外显上限) and limit (实际上限) fields to event
create/edit form with positive integer validation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Stop click propagation from Edit button to TableRow to prevent double-fire
- Store full editingUser state on open so dialog persists across page changes
- Remove unused targetUserId from dialog view props and currentUserId from container props
- Add useEffect to reset selectedLevel when targetCurrentLevel changes
- Replace double as-unknown-as cast with single typed assertion using ServiceUserUserInfoUpdateData
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements user admin list with sortable columns, permission level filter, and per-row edit button gated by canEditPermission. Adds permission edit dialog with assignable levels and optimistic query invalidation.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements AgendaListSkeleton, AgendaListView, AgendaListContainer and story
for reviewing, approving/rejecting, scheduling, and editing agenda submissions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>