Address six QA findings on the offline-sync feature:
1. HIGH — silent data loss: a paused mutation persisted to IndexedDB and
reloaded while still offline never resumed on reconnect. Seed TanStack
onlineManager from navigator.onLine at boot (it defaults to online:true and
only flips on events, so a cold-boot-offline tab wrongly believed it was
online and never got a true online transition), and call
resumePausedMutations() in PersistQueryClientProvider onSuccess after the
persister rehydrates (defaults are registered before, so the restored
mutation has a mutationFn). New offline-resume.test.ts reproduces the full
persist -> reload -> reconnect path.
2. MEDIUM (security) — logout did not durably clear gitmost-rq-cache: the
throttled persister re-wrote the key ~1s after del() with the still-in-memory
snapshot, resurrecting the previous user's data. Freeze the persister
(persistClient becomes a no-op) before clearing/deleting so neither the
clear()-triggered nor any in-flight write can repopulate the key; re-enable
afterwards for the next sign-in session.
3. MEDIUM (UX) — offline create spun forever: the create-note button awaited a
mutateAsync that stays pending while paused. Detect offline, fire-and-forget
the (queued) mutation, show a "saved offline" notice, and gate the spinner on
!isPaused so it no longer hangs.
4. LOW — an uncached page opened offline showed the generic "Error fetching page
data." instead of the offline fallback (offline fetch yields no HTTP status).
Render OfflineFallback when navigator is offline or the error has no status.
5. LOW — logout teardown threw "Cannot read properties of null (reading
'settings')" in full-editor.tsx: optional-chain the (transiently null) user.
6. Tab title "Untitled": investigated — the tab-title derivation in page.tsx is
byte-identical to develop and already reads page.title from REST/cache (the
recommended source); live edits keep it in sync via updatePageData. Not a
tab-title-derivation regression introduced by this PR; no change.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fixes the offline-sync defects QA found on PR #120 (#237/#238/#220).
Blank-shell / white-screen on offline reload (HIGH):
- auth-query.tsx: the useCollabToken retry predicate read
`error.response.status` unguarded. Offline the collab-token POST rejects as
an axios NETWORK error (isAxiosError true, response undefined), so `.status`
threw an uncaught TypeError in the React Query retryer BEFORE React mounted,
white-screening every route. Extracted the predicate as `collabTokenRetry`
and guarded it with optional chaining (`error.response?.status === 404`).
- user-provider.tsx: gated the whole <Layout> on useCurrentUser() and returned
a bare `<></>` on any error, blanking every authenticated route offline even
when cached data existed. Now renders the cached app when a (stale) user is
present and an explicit OfflineFallback when there is no user to fall back on.
- query-persister.ts / make-offline.ts: persist and warm the ['currentUser']
query so the auth gate can hydrate offline (pinned pages now survive relaunch).
Offline structural create/move/comment silently lost on reload (HIGH):
- offline-mutations.ts: register setMutationDefaults (default mutationFns) for
stable mutation keys and tag useCreatePageMutation / useMovePageMutation /
useCreateCommentMutation with those keys. A paused mutation dehydrated to
IndexedDB while offline now has a mutationFn after reload, so
resumePausedMutations() replays it on reconnect instead of no-op'ing.
Tests (client vitest): collabTokenRetry no longer throws on a no-response
network error; UserProvider renders cached children / the offline fallback (not
a blank fragment) on a network error; a rehydrated paused create/move is
replayable via resumePausedMutations; currentUser persist-root coverage.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Carries the still-applicable findings from the PR #116 review into PR #120,
since #120 includes the mobile-bootstrap commit. CORS hardening (removing the
unconditional localhost/capacitor origins) is intentionally left out of scope.
Service worker routing (latent bug fix + testability):
- vite.config.ts: anchor Workbox path matching to a segment boundary
(^/<seg>(/|$)) instead of startsWith, so siblings like /apidocs,
/collaborators, /socket.iox are no longer mis-routed as API/realtime and
forced NetworkOnly; align navigateFallbackDenylist with the same anchors.
- new apps/client/src/pwa/sw-strategy.ts holds the canonical predicates
(isApiPath, isCollabOrSocketPath) + unit tests; the vite.config regexes
mirror it inline (Workbox generateSW serializes urlPattern fns standalone,
so they cannot import the module).
Server CORS (R1 extraction + coverage):
- extract buildCorsAllowlist / isOriginAllowed into cors.util.ts with unit
tests (evil-origin rejected, WebView/no-Origin allowed); main.ts rewired to
use them with byte-for-byte identical behavior.
Privacy — clear offline cache on logout:
- new clear-offline-cache.ts purges the persisted query cache
(idb-keyval gitmost-rq-cache), the Yjs page.* IndexedDB databases, and the
service-worker api-get-cache; wired into handleLogout (best-effort, before
the redirect) so a previous user's private data does not linger locally.
Conventions & docs:
- prettier fixes on main.ts and login.dto.ts.
- CHANGELOG: document offline reading, returnToken opt-in, optional Swagger,
new env vars, logout cache-clear, and the CORS open->allowlist breaking
change.
- docs/mobile-app-plan.md: correct the now-false §2.4 claims and update the
§12 checklist (native cap add ios left unchecked — generated locally,
gitignored).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Implements docs/offline-sync-plan.md milestones M0–M2.
M0 (PWA shell):
- Add vite-plugin-pwa (generateSW, registerType: 'prompt', manifest:false);
NetworkOnly for /api,/collab,/socket.io, NetworkFirst for GET /api,
navigateFallback to index.html.
- Register SW via useRegisterSW with a Mantine update prompt; skip
registration inside Capacitor native WebView (is-capacitor guard).
M1 (harden CRDT body + title into Yjs):
- Lift the per-page Y.Doc/Hocuspocus providers into a shared hook+context so
body and title editors share one doc.
- Move the page title into a dedicated 'title' Yjs fragment (CRDT, offline-
tolerant); drop the REST title save. Server persists the title fragment to
page.title and seeds it for legacy pages (empty-fragment guard); a collab
rename emits a treeUpdate so other users' tree/breadcrumbs refresh.
- Persist the rebuilt ydoc on the content->ydoc path to neutralize the Yjs
duplication trap. Add a 3-state sync indicator.
M2 (offline read/navigation):
- Persist React Query to IndexedDB (idb-keyval persister, version buster,
selected roots only).
- "Make available offline" action warms page, space, tree (root+ancestors+
children) and comments under exact hook keys, plus the page ydoc.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>