Commit Graph

855 Commits

Author SHA1 Message Date
admin 6e50aa4c8c Merge branch 'dev' into test 2026-06-17 11:15:03 +08:00
admin 0d5c416cba fix(images): allowlist Facebook avatar hosts in next/image remotePatterns
Facebook OAuth login returns avatar URLs from
platform-lookaside.fbsbx.com (and occasionally graph.facebook.com).
next/image's optimizer rejects these with 400 Bad Request when they
are not in the allowlist, so chat/sidebar/subscription avatar
<Image> components all fail to render.

Add images.remotePatterns entries for both hosts. Three <Image>
consumers (message-avatar, user-header, subscription-user-row) need
no changes — they all pass the avatarUrl through verbatim and will
work once the optimizer accepts the host.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-17 11:14:24 +08:00
admin ee760abdf5 refactor(auth): clean up user logout handling in SidebarScreen 2026-06-17 11:11:25 +08:00
admin ae0f385bb0 Merge branch 'dev' into test 2026-06-17 11:10:24 +08:00
admin 67dcf97edc refactor(pwa): migrate from @ducanh2912/next-pwa to @serwist/turbopack
Next.js 16 ships Turbopack as the default bundler, but
@ducanh2912/next-pwa is webpack-only — the previous PWA setup
required `--webpack` on dev/build/"dev:proxy" scripts, accepting
a perf regression in exchange for working PWA install.

@serwist/turbopack (paired with the official 'serwist' package
and 'esbuild') is the Turbopack-native successor: it works with
the default bundler, no opt-out flag, and gives us a real TS
service worker source file we can read and edit.

What this commit does:

1. Swap deps in package.json:
   - Remove @ducanh2912/next-pwa
   - Add @serwist/turbopack, serwist, esbuild (all devDeps)
   - Drop --webpack flag from dev / dev:proxy / build scripts
     (Turbopack is restored as the bundler)

2. next.config.ts:
   - Replace withPWAInit wrapping with withSerwist named import
   - Update comment block to describe new architecture

3. New src/app/sw.ts: Serwist service worker source.
   skipWaiting + clientsClaim + navigationPreload + defaultCache.
   This file is compiled to a real SW at build time by Serwist.

4. New src/app/serwist/[[...slug]]/route.ts: catch-all Route
   Handler that serves the compiled SW at /serwist/sw.js plus
   Serwist's chunked runtime assets. Catch-all is required
   because Serwist serves multiple files under /serwist/.

5. src/app/layout.tsx: replace <SwRegister /> with
   <SerwistProvider swUrl="/serwist/sw.js"> from
   @serwist/turbopack/react. The provider handles registration
   + lifecycle internally.

6. Delete src/app/_components/core/sw-register.tsx: replaced
   by SerwistProvider.

7. .gitignore: add public/sw.js and public/workbox-*.js
   (defensive — these are build artifacts that change hash
   every build and shouldn't be committed).

8. Untrack public/workbox-3c9d0171.js: stale workbox runtime
   from the previous next-pwa build, hash is build-specific
   so it can never be regenerated correctly.

Not modified (intentionally):
- src/utils/pwa.ts: pure beforeinstallprompt browser-API wrapper,
  library-agnostic, stays as-is.
- pwa-install-overlay / pwa-install-dialog: install UI flow
  unchanged.
- public/manifest.json: still works; layout's metadata.manifest
  points at it.

Verification:
- pnpm install resolved cleanly (added 24 pkgs, removed 209)
- npx tsc --noEmit passes (EXIT=0)
- Serwist provides named exports, not default — withSerwist and
  createSerwistRoute are both named imports
2026-06-17 11:09:36 +08:00
admin d9013a9dc4 Merge branch 'dev' into test 2026-06-17 10:07:15 +08:00
admin bb2ae41232 fix(auth): include OAuth backend-sync phase in isLoading derivation
The AuthState.isLoading derivation in auth-context.tsx covered the
OAuth redirect phase (loadingOAuth) but NOT the backend-sync phase
that runs after the OAuth callback returns
(syncingGoogleBackend / syncingFacebookBackend). Any UI that
read isLoading would incorrectly report "not loading" during
the backend token-exchange round-trip.

Add the two missing state.matches() lines so isLoading is true
for the entire OAuth login window (button click → OAuth redirect
→ callback → backend sync).

Note: in the current OAuth flow the user is on /chat during the
sync phase (NextAuth redirected them there), so the splash/auth
button is not actually visible to reflect the new true value.
The fix is still correct because:

- Conceptually aligned: any in-flight login op is "loading"
- Defense in depth: future UI on /chat that consumes isLoading
  will get accurate feedback
- Non-regression: only makes isLoading more permissive (true more
  often), never false-fires

A visible /chat overlay during the sync phase would be a separate,
larger change — noted in the planning doc but not in this commit.
2026-06-17 10:06:07 +08:00
admin e85963e7bf refactor(auth): rename AuthStatusCheckSubmitted → AuthInit (init-only)
The previous event name implied "re-check status", but the actual
semantics is just "read storage on app start and sync loginStatus into
the machine". It was being dispatched from three sites:

1. AuthStatusChecker mount useEffect (the legit one-time init)
2. AuthStatusChecker loginStatus-watching useEffect (dead code — the
   machine's onDone already writes loginStatus, re-reading storage
   just returns the same value as a no-op)
3. Sidebar post-logout effect (also dead code — AuthReset directly
   sets loginStatus to initialState's "notLoggedIn", and
   userLogoutActor already cleared storage, so the values are
   already aligned)

This commit:

- Renames event AuthStatusCheckSubmitted → AuthInit in:
  * auth-events.ts (type union)
  * auth-machine.ts (handler + transition target: checkingAuthStatus →
    initializing, mirroring UserInit / initializing in user-machine)
  * auth-status-checker.tsx (single dispatch site)
  * root-providers.tsx (one comment)

- Simplifies auth-status-checker.tsx from 74 → ~40 lines:
  * Removes useEffect ② (loginStatus-watching re-check)
  * Removes prevLoginStatusRef + skipNextChangeRef + useAuthState
    (dead loop-guard machinery no longer needed)

- Removes the post-logout re-check dispatch from sidebar-screen.tsx:
  * AuthReset alone is sufficient — userLogoutActor cleared storage
    and AuthReset writes back initialState, so they match by
    construction. No re-verification needed.

Net: -44 lines, no behavior change for the happy paths (startup,
login, logout), one fewer source of false re-checks.
2026-06-17 09:52:45 +08:00
admin 88c26c2889 Merge branch 'dev' into test 2026-06-17 09:51:50 +08:00
admin 2acc005809 feat(pwa): wire up @ducanh2912/next-pwa + register SW in layout
PWA install flow was wired but unusable: pwaUtil.install() calls
deferred.prompt(), but Chrome only fires beforeinstallprompt when a
valid Service Worker is registered + a manifest is linked. The project
had both intent (manifest, usePwaInstall hook) but no live SW —
public/sw.js was a stale workbox build artifact from a previous
attempt with hardcoded chunk URLs that no longer matched any build.

What this commit does:

1. Add @ducanh2912/next-pwa@10.2.9 as devDependency.
2. Wrap nextConfig with withPWA({...}):
   - dest: "public" SW outputs to public/sw.js
   - disable: true skip SW generation in dev (HMR-friendly)
   - register: false don't auto-inject registration
   - workboxOptions.skipWaiting: true + clientsClaim: true — new SW
     takes over immediately
3. Add --webpack flag to dev/build/dev:proxy scripts.
   @ducanh2912/next-pwa is webpack-only; Turbopack (Next 16 default)
   doesn't load webpack plugins. Per user decision to accept the perf
   trade-off in exchange for a working PWA.
4. Delete stale public/sw.js (workbox will regenerate it at build
   time with the correct chunk URLs).
5. Add src/app/_components/core/sw-register.tsx: a no-UI client
   component that registers /sw.js in production. Mounted at the end
   of <body> in layout.tsx. disable: true means dev mode skips
   registration (no /sw.js to fetch anyway).

After this commit + pnpm build, public/sw.js is regenerated with
correct chunk URLs, registered on first prod load, and Chrome fires
beforeinstallprompt — usePwaInstall + pwaUtil.install() then trigger
the native install prompt end-to-end.
2026-06-16 19:36:03 +08:00
admin e632fa7139 fix(auth): correct Facebook Graph field expansion syntax for picture
Facebook Graph API field expansion uses parentheses, not equals signs.
The buggy `picture.type=large` produced HTTP 400 with
`OAuthException code 2500: Syntax error ... at character 32`.

Fix `FIELDS` constant and 2 JSDoc comment lines in facebook-graph.ts
to use `picture.type(large)` per Facebook's documented syntax.

Effect: after Facebook OAuth sync, `fetchFacebookUserData` now
succeeds (HTTP 200), `UserStorage.setAvatarUrl` and
`AuthStorage.setFacebookId` get populated, and the
`[auth-machine] syncFacebookBackendActor: fetchFacebookUserData
failed (continuing anyway)` warning stops firing on the happy path.
2026-06-16 19:22:14 +08:00
admin 851c1d4f11 Merge branch 'dev' into test 2026-06-16 19:06:19 +08:00
admin d6f2cb7c56 fix(pwa): enhance PWA install overlay logic for daily display limits and environment handling 2026-06-16 19:05:50 +08:00
admin 5be40949a1 feat(pwa): implement PwaUtil class for PWA support and installation checks 2026-06-16 18:59:17 +08:00
admin 8d786f1e04 fix(chat): align user bubble to match AI bubble for consistent layout 2026-06-16 18:58:42 +08:00
admin 88b94a9f1a Merge branch 'dev' into test 2026-06-16 18:35:26 +08:00
admin 0a9abc2502 fix(auth): stop parsing logout response body (avoids ZodError on null data)
Root cause: backend `/auth/logout` returns `{ success: true, data: null }`
(no business payload for a fire-and-forget endpoint). The previous
`AuthApi.logout` ran `unwrap(env)` (which passes null through, since
null is defined) and then `LogoutResponse.fromJson(null)`, which
Zod-parses as `z.object(...)` and throws "Invalid input: expected
object, received null".

The error was caught in `AuthRepository.logout` with
`console.warn(... clearing local anyway)` — logout actually still
worked (local clear ran), but a noisy red ZodError hit the console on
every logout click.

Fix: align `AuthApi.logout` with the existing fire-and-forget pattern
used by `register` and `sendCode` — call `httpClient` and ignore the
response body. Drop the now-unused `LogoutResponse` import.

`AuthRepository.logout` already discards the return value, so the
`Promise<LogoutResponse> → Promise<void>` signature change is safe.

Bundled in this commit (unrelated):
- chat-machine.actors.ts / user-machine.ts: removed stale design-doc
  comment blocks (IDE/linter cleanup)
- user-machine.actors.ts: removed redundant `userStorage.clearUserData()`
  from `userLogoutActor` — `authRepo.logout` already clears local
  user data, so this was a no-op duplicate.
- sidebar-screen.tsx: removed one stale comment line.
2026-06-16 18:34:55 +08:00
admin 63704a7309 Merge branch 'dev' into test 2026-06-16 18:24:49 +08:00
admin 8832552321 fix(githooks): write log timestamps in local time (was UTC + Z) 2026-06-16 18:24:27 +08:00
admin 39e7d61c8a fix(auth): re-check auth status after logout; clear NextAuth session after sync
1) Sidebar logout flow:
   - After AuthReset, also dispatch AuthStatusCheckSubmitted so the auth
     machine re-verifies storage (not just resets to initialState). This
     makes sure the redirect to /chat reflects the actual storage state,
     not just the forcibly-cleared context.

2) OAuthSessionSync:
   - After dispatching AuthGoogleSyncSubmitted / AuthFacebookSyncSubmitted,
     call signOut({ redirect: false }) to drop the NextAuth session.
     The OAuth idToken / accessToken has already been handed to the
     backend; clearing the browser-side session prevents lingering OAuth
     credential exposure. Fire-and-forget — once status flips to
     "unauthenticated", the useEffect early-returns so no re-entry.

Note: auth-machine.ts lost a 3-line stale comment block about XState v5
type inference for inline assign; bundled with this commit.
2026-06-16 18:24:23 +08:00
admin a44463b3ee refactor(user): split user-machine into helpers and actors
Mirror the existing chat-module structure (chat-machine.helpers.ts +
chat-machine.actors.ts) for consistency across state-machine modules.

Split:
- user-machine.helpers.ts: pure data layer (no XState dep)
  - userStorage singleton
  - InitData interface
  - toView DTO → UserView mapper
  - readInitData (parallel getUser + getAvatarUrl)
- user-machine.actors.ts: async actor implementations (fromPromise)
  - userRepo / authRepo injection
  - userInitActor / userFetchActor / userLogoutActor
- user-machine.ts: only setup().createMachine() — keeps inline assign
  actions referencing context/event where they belong, imports actors
  (not helpers, per the layered convention).

No behaviour change — only file boundaries moved. Public API (exports of
UserState / UserEvent / initialState / userMachine / UserMachine) stays
identical so external imports are untouched.
2026-06-16 18:12:11 +08:00
admin 634c70a56a chore: verify local-time hook timestamps 2026-06-16 18:05:39 +08:00
admin f6735c75c8 fix(githooks): write log timestamps in local time (was UTC + Z) 2026-06-16 18:05:03 +08:00
admin bde8b99c04 Merge branch 'dev' into test 2026-06-16 17:56:18 +08:00
admin 8b6e38d3cb feat(chat): prepend greeting message when chat history is empty
On first chat open (or when both local and network return empty),
the chat screen would render completely blank, which feels cold and
broken. Prepend a single AI persona greeting as the first message so
the first impression is warm.

Trigger:
- Final returned messages array is empty
- Either network succeeds with empty result, or network fails and local
  is also empty

Design notes:
- Greeting is NOT persisted to local/network — pure UI-layer fallback.
  Once the user sends a real message, network has content and the
  greeting no longer appears.
- If the user closes the app without ever sending a message and reopens,
  the greeting reappears (intentional: "warm first frame on each cold
  start").
- Rendered as an AI bubble (isFromAI: true) so the visual style matches
  the AI persona.
2026-06-16 17:56:03 +08:00
admin 2e238c3df8 Merge branch 'dev' into test 2026-06-16 17:43:39 +08:00
admin c5686d2749 feat(auth): implement Facebook user data fetching from Graph API and persist profile information 2026-06-16 17:43:19 +08:00
admin 82bf917ace feat(auth): enhance AuthStatusChecker with detailed status change handling and prevention of infinite loops 2026-06-16 17:42:37 +08:00
admin 4ee9d6cb81 feat(docs): add git commit guidelines and best practices 2026-06-16 17:31:20 +08:00
admin 200fb1642f fix(auth): surface validation errors in email login/register forms
Validation failure was previously silent — the form's submit() returned
without dispatching or showing anything on screen, so users could not tell
why the login/register button appeared to do nothing.

Changes:
- Add validationError local state in EmailLoginForm and EmailRegisterForm
- Display first validation error via AuthErrorMessage, prioritised over
  the server's globalError (validation errors feel more immediate)
- Clear validation error on field change (standard "error fades as you
  fix it" UX) — implemented as onChange handlers, not useEffect, to
  avoid React's cascading-render anti-pattern
- Add observability to the entire email flow (was previously silent end
  to end): form submit ENTRY, validator rejection, actor ENTRY, actor
  DONE — mirrors the logging style already used by guestLoginActor
- Add entry: assign({ errorMessage: null }) to loadingEmailLogin and
  loadingEmailRegister so stale errors clear on transition into the
  loading state (matches loadingGuestLogin)
2026-06-16 17:29:58 +08:00
admin 2792d0c9c5 feat(chat): implement WebSocket message sending and integrate with chat state machine 2026-06-16 16:50:57 +08:00
admin 6692c5a68a Merge branch 'dev' into test 2026-06-16 16:42:57 +08:00
admin 03a2580023 chore: trigger post-receive hook verification 2026-06-16 16:36:52 +08:00
admin 436a0addcc fix(githooks): pin absolute GIT_DIR and unset GIT_WORK_TREE in post-receive 2026-06-16 16:31:35 +08:00
admin 5764b3c433 feat(ui): replace emoji icons with lucide-react icon components 2026-06-16 16:23:36 +08:00
admin db350aae44 fix(githooks): use absolute-git-dir to find worktree in post-receive
`git rev-parse --show-toplevel` in hook context (cwd=GIT_DIR, GIT_DIR
set) returns the .git path itself without erroring, so the previous
`|| echo $PWD` fallback never triggered and the script silently
cd'd into .git/.

Use `git rev-parse --absolute-git-dir` to reliably get the absolute
git-dir, then its parent is the worktree root. This works in both
interactive shells and hook context.
2026-06-16 15:58:38 +08:00
admin b09e2d093d Merge branch 'dev' into test 2026-06-16 15:53:50 +08:00
admin 594682ba6b chore(images): update auth, chat, icons, and splash images 2026-06-16 15:48:10 +08:00
admin 697dce64b1 chore: remove bold markers from Chinese characters in post-receive hook comments 2026-06-16 15:39:00 +08:00
admin bab731bd21 chore(githooks): simplify post-receive hook repo root detection
Remove the workaround that derived REPO_TOPLEVEL from
`git rev-parse --absolute-git-dir` and fall back to
`git rev-parse --show-toplevel` directly. Also drop the
related comments and debug echo. The simpler approach is
sufficient for the hook's needs and makes the script easier
to maintain.
2026-06-16 15:33:22 +08:00
admin 20cb9e80ed Merge branch 'dev' into test 2026-06-16 15:31:10 +08:00
admin 02b1b713a5 style(splash): set Athelas as primary font and italicize button text
- Prioritize Athelas in --font-system font stack with system fonts as fallback
- Apply italic style to splash button heading and label for typography emphasis
2026-06-16 15:30:23 +08:00
admin dcb6312fa3 feat(chat): add quota-exhausted banner for guest users
Introduce ChatQuotaExhaustedBanner component that displays a pink gradient
banner with an "Unlock your membership to continue" CTA when a guest user's
free chat quota has been exhausted. The banner is shown in ChatScreen when
isGuest is true, quota has loaded, and the chat state machine's
quotaExceededTrigger has been incremented by a quota guard. Default click
navigates to /subscription, with an optional onUnlock callback for
overrides/tests. Styles align with the existing splash and sidebar VIP
card visual language.
2026-06-16 15:18:11 +08:00
admin d5ddb3f97f fix(githooks): cd to worktree root before running build
When git invokes post-receive, cwd is GIT_DIR (not worktree root).
`git rev-parse --show-toplevel` refuses to run from a bare context,
so the script would silently cd into .git/. Use `--absolute-git-dir`
and its parent to find the worktree reliably.

Also: receive.denyCurrentBranch=false means push does NOT auto-update
the worktree, so the hook must do `git reset --hard HEAD` to sync
the source the build will use.
2026-06-16 14:18:25 +08:00
admin fa5678ffdf ci: debug post-receive cwd 2026-06-16 14:15:51 +08:00
admin 704e2292fc ci: post-receive hook live test from plan-mode-exec 2026-06-16 14:12:28 +08:00
admin 9ffa30cc03 style: remove markdown bold syntax from code comments
Remove `**...**` emphasis markers from inline comments and JSDoc blocks across
auth and chat components, machine mappers, and quota helpers. These are
plain code comments, not rendered markdown, so the bold syntax was noise.

Files touched:
- src/app/auth/components/auth-screen.tsx
- src/app/chat/components/chat-header.tsx
- src/app/chat/components/chat-screen.tsx
- chat machine mapper / quota helpers

No functional or behavioral changes.
2026-06-16 14:06:31 +08:00
admin 8553f91e5d ci: trigger post-receive hook smoke test #2 (absolute hooksPath) 2026-06-16 14:02:59 +08:00
admin a370518d5f ci: trigger post-receive hook smoke test 2026-06-16 13:27:47 +08:00
admin c24a7751b7 Merge branch 'dev' into test 2026-06-16 13:24:48 +08:00