- Admin agenda page: approved items now sorted by start_time (nulls/zero-dates last)
- AgendaSchedule: group items by day with M月D日 header above each group
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Extends agendaItemSchema with a required coerced integer estimatedTime
(1–999 minutes) for the agenda submit flow.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaced raw ISO string slicing with dayjs.utc() formatting, consistent
with the rest of the codebase, so agenda item times are not shifted by
the browser's local timezone offset.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The backend returns naive datetime strings already in UTC+8. dayjs was
re-applying the browser's local +8 offset, shifting date labels by one
day. Centralise dayjs configuration in src/lib/dayjs.ts with the UTC
plugin and switch all event start/end date formatting to dayjs.utc() so
the string is read as-is without timezone conversion.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>