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>
CHANGELOG: stop presenting the service-worker API cache as an active offline
store (/api is NetworkOnly) — describe it as a defensive purge of the legacy
api-get-cache from older clients; add an explicit upgrade note that the new CORS
allowlist rejects previously-allowed cross-domain REST clients until their origin
is added to CORS_ALLOWED_ORIGINS.
test(offline): cover make-offline ancestor-walk + dedup — a real-ancestor case
exercising the ancestorId===pageId guard (page warmed once), the dedup of
repeated tree failures into a single "tree" label, and the "breadcrumbs" label
when the breadcrumbs lookup rejects.
test(auth): cover clearOfflineCache in handleLogout — purged exactly once before
window.location.replace, and a thrown purge error does not block the redirect.
conventions: use pageKeys.detail() instead of raw ["pages", …] literals in
title-editor and use-page-collab-providers.
cleanup: remove the dead emit() in title-editor (the gateway ignores it; the
cross-user tree refresh is server-side via the Yjs title fragment); drop the
trivial Array.isArray(tiptapExtensions) test (schema is exercised transitively).
refactor: extract the shared page.<id> Yjs doc-name convention into
pageYdocName()/PAGE_YDOC_NAME_PREFIX so the editor providers, offline warm, and
offline purge can no longer drift apart.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Service worker (vite-plugin-pwa/Workbox): add /share/, /mcp, and /robots.txt
to navigateFallbackDenylist so the SPA app-shell never shadows those
server-rendered routes (they mirror the server static-serve exclude list — the
share SEO/OG HTML, the MCP endpoint, and robots.txt must come from the server).
- Remove the dead /api GET NetworkFirst Workbox rule (api-get-cache): offline
reads are served by the persisted TanStack Query cache (IndexedDB) + y-indexeddb,
never by an SW HTTP cache, so caching GET /api only risked stale responses. All
/api is now NetworkOnly. clearOfflineCache still deletes any legacy api-get-cache
defensively (comment updated to note it is no longer created).
- CORS: drop the cleartext 'http://localhost' native-WebView origin. The Capacitor
shell uses the secure scheme (capacitor.config cleartext:false, default Android
scheme https, iOS hosted via CAP_SERVER_URL), so no native client uses it;
allowing it only widened the credentialed-CORS surface. Keeps capacitor://,
ionic://, and https://localhost.
- docs/mobile-bootstrap.md: replace the inaccurate 'hand-rolled service worker'
description with the real Workbox generateSW setup (prompt registration via
virtual:pwa-register, production-only, denylist, NetworkOnly, RQ/y-indexeddb
offline reads) and drop http://localhost from the CORS origins list.
Co-Authored-By: Claude Opus 4.8 <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>