You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit bdce7bd
Browse filesBrowse the repository at this point in the historyBrowse files
fix(webapp): keep the token out of page props and clear drafts on sign-out
The document page put the signed-in access token in its page props. The service
worker caches the page and its /_next/data response in the others cache. Its
header rule cannot see a token in a body. The props now carry isAuthed in its
place. The WS token callback reads getSession() only, and it has no fallback
to a prop.
Chat drafts are keyed by workspace and room, with no user id. So the next
person on a shared browser could see unsent text. After signOut() leaves no
session, useSignOut now awaits clearAllComposerDrafts(). That function cancels
queued draft writes first, so the pagehide flush cannot write one back. A
failed sign-out keeps the drafts.
Sign-out also removes the docId, padName, slug and title keys from
localStorage. Nothing reads them, so useDocumentMetadata is deleted.
Refs #429
Refs #444
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Copy file name to clipboardExpand all lines: apps/webapp/CLAUDE.md
+2-2Lines changed: 2 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -206,7 +206,7 @@ Follow industry overlay UX (Material, Apple HIG, Radix/shadcn) and dim-not-lift
206
206
- Workbox `urlPattern` matchers must be self-contained (next-pwa serializes via `Function.toString()` — no module-scope refs). Verify a matcher change under **Node** (the actual Next build runtime), never `bun -e`. Bun's `Function.prototype.toString()` re-serializes with `const` bindings inlined. A matcher that reads `const isSameOrigin = …; return !isSameOrigin` therefore reads back mutated, and throws a false serialization/assertion failure that Node (source-accurate) passes.
207
207
- Deploy-boundary chunk/SW GlitchTip noise: `chunkLoadRecovery.ts` one-shot reload (10s `sessionStorage` cooldown) + `instrumentation-client.ts``beforeSend` drops SW-lifecycle and auto-recovered chunk errors. Keep the chunk pattern lists in sync across both. Rebuild the webapp for `sw.js` changes.
208
208
-**PWA path guards (epic #249).** Keep #257, #259, #260 and #261 open as standing "do not build" cards. Do not add `capture` or a camera UI to the profile photo. Do not add folders; Settings → Documents is the one library, and Home and Command jump are doors into it. Do not add visit-based Recents; Last opened stays an owner-only stamp. The REST list sorts by it on the Owner live list and the Merged list. There, a row the caller does not own sorts as never opened. Joined lists memberships, not visits. Open pads with `router.push`, never `_blank`, so `launch_handler: navigate-existing` holds.
209
-
-**Workbox rule 0 keeps authenticated requests out of Cache Storage.** Any request with a `token` or `Authorization` header is `NetworkOnly`. supabase-js sends `Authorization` even when signed out, so every Supabase REST GET skips the cache. Keep the rule first in `workbox-runtime-caching.js`, ahead of the adapted defaults. The MCP status probe (`/.well-known/oauth-protected-resource`) is `NetworkOnly` right after it, so a cached answer never shows Online while the server is down.
209
+
-**Workbox rule 0 keeps authenticated requests out of Cache Storage.** Any request with a `token` or `Authorization` header is `NetworkOnly`. supabase-js sends `Authorization` even when signed out, so every Supabase REST GET skips the cache. Keep the rule first in `workbox-runtime-caching.js`, ahead of the adapted defaults. The MCP status probe (`/.well-known/oauth-protected-resource`) is `NetworkOnly` right after it, so a cached answer never shows Online while the server is down. The rule reads request headers only. Page props sit in the `others` cache, so they must never carry a token or another secret (#429).
210
210
-**Inbound share is `/receive`.** The manifest `share_target` posts there. `public/service-worker.js` handles only `POST /receive`: it stashes the file and redirects. `/receive` is a utility route and a reserved slug, so it never becomes a pad. Do not add `file_handlers` or change manifest `id` or `launch_handler`.
211
211
212
212
### Next Product APIs
@@ -369,7 +369,7 @@ Follow industry overlay UX (Material, Apple HIG, Radix/shadcn) and dim-not-lift
369
369
-**The patch-versus-invalidate rule.** A remove or an in-place patch keeps the loaded rows equal to a server prefix, so it patches. An insert whose server position the client cannot know must refetch instead. **Duplicate is that case and it must NOT patch.**`duplicateDocument` never writes `lastOpenedAt`, and `documents.service.ts` sorts that column `nulls: 'last'`, so under `lastOpenedAt_desc` a copy belongs at the end, past the loaded window. A patch put it near the top and the refetch then removed it, so the row flashed and vanished. `addDuplicate()` takes no argument for that reason.
370
370
-**A debounce guarding a NETWORK write must `flush()` on unmount. A debounce guarding LOCAL state must `cancel()`.**`NotificationsSection.tsx` cancelled a preference write, and `ModalContent` unmounts its children on close, so closing the panel inside 500 ms discarded the patch silently. Its line is the only caller of `updateNotificationPreferences` in the webapp, so nothing rescued it. `DocumentsSection.tsx` and `ProfileSection.tsx` both cancel correctly, because both guard local state. A sweep that unifies the three would reintroduce the data loss.
371
371
- The cleanup body must stay braced. The flushed function is `async`, so an expression-bodied arrow hands React a Promise as a cleanup value.
372
-
-**Sign-out flushes a pending patch first, and `keepalive` is NOT the fix.** After `signOut()` the session is gone, and `update_notification_preferences` raises `42501 unauthenticated`, so no transport option can save it. The fix is ordering. The open `NotificationsSection` registers its debounced save in `components/settings/utils/pendingPreferenceWrites.ts`, and `useSignOut` awaits `flushPendingPreferenceWrites()` before `signOut()`. Keep that order. Sign-out also unregisters this device's push subscription (`unregisterPushSubscription`) before `signOut()`, best effort, because that RPC needs the session too (#417).
372
+
-**Sign-out flushes a pending patch first, and `keepalive` is NOT the fix.** After `signOut()` the session is gone, and `update_notification_preferences` raises `42501 unauthenticated`, so no transport option can save it. The fix is ordering. The open `NotificationsSection` registers its debounced save in `components/settings/utils/pendingPreferenceWrites.ts`, and `useSignOut` awaits `flushPendingPreferenceWrites()` before `signOut()`. Keep that order. Sign-out also unregisters this device's push subscription (`unregisterPushSubscription`) before `signOut()`, best effort, because that RPC needs the session too (#417). Once `signOut()` leaves no session, `useSignOut` awaits `clearAllComposerDrafts()` and removes the four old localStorage keys (`docId`, `padName`, `slug`, `title`). Only then does it reload (#444).
373
373
-**A setting saved anywhere reaches every open tab and device of that person** (2026-09-30). `hooks/useProfileSync.ts` listens on the private `profile:<uid>` topic and also refreshes on `visibilitychange`, because Realtime keeps no backlog. It copies only `SYNCED_FIELDS` into `authStore.profile`, never `status`, and only from the newest read. It invalidates `notificationPreferencesKey(userId)`, keyed by user so one person never sees another's cache. It mounts as the null `ProfileSync` inside `AppQueryClientRoot`'s provider, above the lazy `DocumentShellInner`, so sync starts with the page. Private topics subscribe through `utils/supabase/subscribePrivateTopic.ts`, because realtime-js `channel()` returns a same-topic channel that is still leaving.
374
374
-**Notifications reads the `useNotificationPreferences` query, never `profile_data`.** Unconfirmed edits sit in `localEdits` and win over the query, so a refetch or the writer's own echo never flips a toggle back. The RPC's merged return goes into the cache with `setQueryData`, after `cancelQueries` stops any read that began before the commit; the writer's own echo refetches. The controls stay live while a save is in flight. `DEFAULT_PREFERENCES` mirrors the SQL fallbacks and leaves out `timezone`, so a missing timezone still reads as missing and gets written.
375
375
-**`ProfileSection` takes a remote change only into a field the person has not edited.** The theme is per browser: `themeStore` rehydrates on the `storage` event and repaints through `applyThemeToDom`. Push subscription and notification permission are per browser too, and never sync.
Copy file name to clipboardExpand all lines: apps/webapp/src/components/chatroom/CLAUDE.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -73,7 +73,7 @@ Moved verbatim out of the repo-root [AGENTS.md](../../../../../AGENTS.md) so it
73
73
- After submit, `cleanupAfterSubmit` in `useComposerSubmit` decides the refocus. On a phone or tablet, `keepKeyboardAfterSubmit` (`isMobile`) is true, so the editor always refocuses. On desktop it refocuses only if the editor was the active element. Neither path refocuses while the emoji panel or the link dialog is open. Enter-key submits keep focus naturally.
74
74
-`useEditor(...)` in `components/chatroom/components/MessageComposer/hooks/useTiptapEditor.ts` is built once per mount. Its `onUpdate` reads `workspaceId` and `channelId` from `draftCtxRef.current`, and an effect keeps that ref current. Do not capture either id in the closure, or drafts cross channels.
75
75
-`prepareOutboundContent` must return stable `htmlChunks: string[]` / `textChunks: string[]` fields on both the happy and empty paths, so callers never need an `as` cast. It must not return a nested `chunks` wrapper.
76
-
-**Draft vs mode memory.** Text/HTML drafts live in IndexedDB (`messageComposerDB` via `syncComposerDraft` / `discardComposerDraft`). When IndexedDB fails to open, `messageComposerDB` keeps them in a module-level `Map` instead. Zustand `*MessageMemory` holds reply/edit/comment UI modes only — do not reintroduce `messageDraftMemory`. Edit and comment modes never write the saved draft. When either mode ends, the composer loads the saved draft again. An edit has its own attachment list under the `::edit` key, so an edit never touches the draft's attachments or their uploads. A comment shows the saved draft's tiles, but the files it adds never enter the saved draft.
76
+
-**Draft vs mode memory.** Text/HTML drafts live in IndexedDB (`messageComposerDB` via `syncComposerDraft` / `discardComposerDraft`). When IndexedDB fails to open, `messageComposerDB` keeps them in a module-level `Map` instead. Zustand `*MessageMemory` holds reply/edit/comment UI modes only — do not reintroduce `messageDraftMemory`. Edit and comment modes never write the saved draft. When either mode ends, the composer loads the saved draft again. An edit has its own attachment list under the `::edit` key, so an edit never touches the draft's attachments or their uploads. A comment shows the saved draft's tiles, but the files it adds never enter the saved draft. The draft key has no user id on purpose. Sign-out clears every draft through `clearAllComposerDrafts`, which cancels queued writes first, because the pagehide flush would write them back (#444).
77
77
-**Mode edges come from one table.**`useComposerModeEdge` turns the mode kind (`none`, `reply`, `edit`, `comment`) into one edge per change. The first edge of a mount starts from `mount`. The pure `composerModeEdgeAction` decides the draft load (`keep`, `fill`, `replace`) and the upload discard. `useComposerAttachmentLifecycle` reads that edge. `useComposerDraft` reads a second edge in which reply counts as none, so a reply toggle never re-runs the draft load. The draft load and the reply and comment upload discard come from the table; do not infer them from a previous-mode ref again. Leaving edit still cancels the `::edit` list through its own effect on `editMessageMemory`.
78
78
- An edge that leaves reply or comment deletes the uploads that the mode added. A switch between reply and comment counts as leaving.
79
79
- Entering a mode never discards, because the failed-comment restore records its files again after the `await`.
0 commit comments