fix(pwa): format-detection meta + autoUpdate SW registration (Stage 1)#21
fix(pwa): format-detection meta + autoUpdate SW registration (Stage 1)#21
Conversation
Three research agents converged on a single high-leverage diagnosis that most of yesterday's "iOS PWA reactivity is broken" patching was chasing: Nick's iPhone PWA has been running a stale service worker shell from 2026-04-17 for 6 days while the build was broken, then ran a sequence of hot-swapped modules across 8 deploys without a version-bumped SW to force the shell to reload. Classic iOS standalone PWA symptom cluster — lifecycle hooks look flaky, bind:value seems not to propagate, goto() stalls, send button "does nothing." In reality each deploy was fighting a service-worker cache that never turned over. Three changes, all small, all low-risk: - Add `<meta name="format-detection" content="telephone=no, date=no, address=no, email=no">` to app.html. Safari auto-detects phone numbers / dates / addresses and rewrites the DOM *before* Svelte hydration runs, which breaks hydration parity and makes SvelteKit fall back to client-only re-render. This masquerades as "lifecycle hooks don't fire" but is really hydration aborting. See github.com/sveltejs/svelte/issues/17357. - Bump package.json 0.2.1 → 0.3.0. Cache name derivation in vite-pwa includes the version, so every installed PWA will detect the new SW on next open and replace the old one. - Flip registerType 'prompt' → 'autoUpdate' in vite.config.ts. Silent replacement is the right UX for a personal app; the prompt flow assumes the user can see and tap a banner, but iOS standalone PWAs often can't surface that cleanly. Also see github.com/sveltejs/svelte/issues/12313 — stale SW + dynamic imports is exactly this session's symptom. The three research agents (web-research-analyst, codebase auditor, Architect) all agreed these are the three top-leverage SvelteKit+iOS PWA best practice fixes for this class of issue. Full synthesis in MEMORY/WORK/20260424-112736_oracle-chat-pwa-architect-review/PRD.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
📝 WalkthroughWalkthroughVersion incremented to 0.3.0. Added Safari format-detection meta tag to prevent unwanted DOM rewriting during hydration. Reconfigured PWA service worker to silently auto-update instead of prompting users. Changes
Estimated code review effort🎯 1 (Trivial) | ⏱️ ~5 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
vite.config.ts (1)
18-23: LGTM —autoUpdateis correctly paired with the existing SW.Confirmed the switch is safe against the current codebase:
src/service-worker.tsalready callsself.skipWaiting()andclientsClaim()at module scope, which is exactly whatautoUpdateexpects when usinginjectManifest(no additionalmessageSkipWaitingplumbing needed).- No call sites subscribe to
updatefound/controllerchange/needRefresh, and there is no update-prompt UI anywhere insrc/routes/+layout.svelteorsrc/lib/push.ts, so removing thepromptpath doesn't orphan any listeners.One operational thing worth keeping an eye on post-deploy (no change required here):
With
clientsClaim()+autoUpdate, an open tab can have its controller swapped mid-session when a new build ships. Because the SW only precaches the shell and explicitly does not intercept API requests, in-flight user actions are unaffected, but any futureworkboxruntime caching for navigations would want a page-reload-on-controller-change strategy. Not needed now; flagging so it's on the radar for Stage 2.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@vite.config.ts` around lines 18 - 23, No code change required: keep registerType: 'autoUpdate' in vite.config.ts as-is; just ensure service-worker.ts contains self.skipWaiting() and clientsClaim() at module scope (used with injectManifest) and that there are no listeners for updatefound/controllerchange/needRefresh or UI hooks in +layout.svelte or src/lib/push.ts to avoid orphaned logic—monitor future workbox navigation caching which may need a reload-on-controller-change strategy.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@vite.config.ts`:
- Around line 18-23: No code change required: keep registerType: 'autoUpdate' in
vite.config.ts as-is; just ensure service-worker.ts contains self.skipWaiting()
and clientsClaim() at module scope (used with injectManifest) and that there are
no listeners for updatefound/controllerchange/needRefresh or UI hooks in
+layout.svelte or src/lib/push.ts to avoid orphaned logic—monitor future workbox
navigation caching which may need a reload-on-controller-change strategy.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 729895d1-9508-4a14-80bf-59319a3bf46c
📒 Files selected for processing (3)
package.jsonsrc/app.htmlvite.config.ts
* refactor(chat): Stage 2a — remove cruft, fix desktop composer floating Net -23 lines across 5 files. Reverts several defensive patterns that were added to work around what we thought was an iOS PWA reactivity bug but (per last night's three-agent review) was actually a stale service worker shell. Stage 1 (PR #21) shipped the real SW fix; this PR restores the code to a clean form and fixes one real layout bug that was exposed along the way. Removed: - Server-side User-Agent `isMobile` detection in +layout.server.ts. Pushed a presentation concern into server data + broke DevTools mobile emulation. Replaced with matchMedia in ProjectChats, seeded at script-init time (`typeof window !== 'undefined'`) so SSR gets `false`, client picks up the correct value on hydration. `$effect` handles viewport changes. - `inChatThread` pathname regex in projects/[slug]/+layout.svelte. Layout shouldn't know about child routes. Replaced with a CSS `:has([data-chat-thread])` rule in app.css, scoped to `@media (max-width: 767px)` so desktop keeps the project chrome (where the two-panel layout needs it) while mobile hides it for the full-screen thread takeover. - `textareaEl.value = ''` and `textareaEl.value = previousDraft` DOM writes in chats/[threadId]/+page.svelte. Papering over bind:value which works fine now that Stage 1 keeps hydration stable. - `const text = (textareaEl?.value ?? draft).trim()` in sendCurrentMessage. Same — trust the framework. - Explicit `draft = el.value` in handleTextareaInput. bind:value does this itself. - `isMobile` prop resolution via `$page.data.isMobile`. Back to a simple test override only. Fixed (real bug): - Desktop composer was floating mid-screen instead of pinning to the bottom of the messages pane. Root cause: the tab-content div `flex-1 overflow-y-auto` was missing `min-h-0`, so `h-full` on descendants resolved to content height rather than the viewport slot. Added `min-h-0` to the tab-content div and the desktop ProjectChats root. This is pre-existing, was masked by the broken build, and surfaced when PR #15 unstuck deploys. Restored: - `disabled={sending || draft.trim() === ''}` on the send button. Stage 1 restores hydration reliability, so bind:value propagates and the reactive expression re-evaluates correctly. - CSS `:has()` rule for chat-thread takeover now lives wrapped in a mobile-only media query, addressing the auditor's concern that the project chrome was globally hidden (regressing desktop when directly visiting a thread URL). All 80 unit tests pass. `isMobile: true` prop in mobile-chat.test.ts is still honored via the `props.isMobile` precedence path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(csp): allow 'unsafe-inline' for script-src (unblocks SvelteKit hydration) Root cause of tonight's entire symptom cluster (send button dead, back button dead, buttons unresponsive, hydration "broken" on iOS PWA AND desktop). The Traefik CSP `script-src 'self'` header introduced by PR #15 blocked SvelteKit's own inline hydration script: <script> Promise.all([ import("./_app/immutable/entry/start.<hash>.js"), import("./_app/immutable/entry/app.<hash>.js") ]).then(([kit, app]) => { kit.start(app, element, { node_ids:..., data:..., form:null, error:null }); }); </script> That script is necessarily inline because it carries page-specific hydration data (node_ids, loaded data, form state, error). CSP blocks it → JS never hydrates → no event listeners attach. Browsers render the SSR HTML just fine (so <textarea> accepts text, <a href> links navigate) but <button onclick> handlers are wired up by the blocked hydration and do nothing. This is NOT a Svelte 5 / iOS PWA reactivity bug (the memory I kept citing was based on the same misread from a prior session). It manifested on EVERY platform identically, it was just hidden by the broken build from 4/17 to tonight. Hot-patched live via ssh + sed on KVM 2 earlier; this commit brings the repo in line so the next deploy doesn't revert the fix. Follow-up (non-urgent): swap 'unsafe-inline' for SvelteKit's built-in `kit.csp` config (svelte.config.js), which injects sha256 hashes of the specific inline scripts — strictly more secure than unsafe-inline. Tracked separately. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: nkburdick <nkburdick@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Stage 1 of the chat PWA cleanup
Per three parallel ruthless reviews (researcher + auditor + architect), most of last night's patching was fighting a stale iOS service worker shell, not actual reactivity bugs. Stage 1 = smallest-diff, highest-leverage fixes to test that hypothesis before the bigger refactor.
Changes
<meta name="format-detection">in app.html — disables Safari auto-detection of phone numbers / emails / addresses / dates, which rewrites the DOM before hydration and breaks SvelteKit hydration parity. Citation: svelte#17357.Bump package.json 0.2.1 → 0.3.0 — vite-pwa derives SW cache names from the version, so installed PWAs will detect + swap to a fresh SW on next open.
registerType: 'prompt' → 'autoUpdate' in vite.config.ts — silent replacement is the right UX for iOS standalone PWAs that can't reliably surface an update banner. Citation: svelte#12313 — stale SW + dynamic imports.
Verification
bun run build✅bun run lint✅bun run test:unit✅ 80/80After deploy
Nick force-quits the iPhone PWA, reopens → new SW takes over → retest the send button. If it works, we've confirmed the stale-SW theory and Stage 2 can be a pure cleanup/refactor instead of a bug hunt.
🤖 Generated with Claude Code
Summary by CodeRabbit
Chores
Bug Fixes
New Features