Skip to content

fix(pwa): format-detection meta + autoUpdate SW registration (Stage 1)#21

Merged
Nkburdick merged 1 commit intomainfrom
fix/pwa-cache-bust-and-meta
Apr 24, 2026
Merged

fix(pwa): format-detection meta + autoUpdate SW registration (Stage 1)#21
Nkburdick merged 1 commit intomainfrom
fix/pwa-cache-bust-and-meta

Conversation

@Nkburdick
Copy link
Copy Markdown
Owner

@Nkburdick Nkburdick commented Apr 24, 2026

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

  1. <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.

  2. 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.

  3. 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/80
  • Meta tag confirmed in SSR response

After 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

    • Released version 0.3.0
  • Bug Fixes

    • Improved Safari browser compatibility to prevent rendering inconsistencies with formatted content
  • New Features

    • Progressive Web App now automatically installs updates in the background without requiring user confirmation

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>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 24, 2026

📝 Walkthrough

Walkthrough

Version 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

Cohort / File(s) Summary
Version Update
package.json
Version bumped from 0.2.1 to 0.3.0.
Safari Hydration Fix
src/app.html
Added format-detection meta tag with comment to disable Safari's automatic phone number, email, address, and date conversion, preventing pre-hydration DOM mismatches.
PWA Configuration
vite.config.ts
Changed service worker registerType from 'prompt' to 'autoUpdate' with documentation explaining silent update behavior and iOS standalone PWA implications.

Estimated code review effort

🎯 1 (Trivial) | ⏱️ ~5 minutes

Poem

🐰 Version three hops into place,
Safari's format-tricks we erase,
PWA updates dance without a sound,
Silent and smooth, all safe and sound! ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and clearly describes the main changes: adding format-detection meta tag and switching to autoUpdate service worker registration for PWA improvements.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/pwa-cache-bust-and-meta

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
vite.config.ts (1)

18-23: LGTM — autoUpdate is correctly paired with the existing SW.

Confirmed the switch is safe against the current codebase:

  • src/service-worker.ts already calls self.skipWaiting() and clientsClaim() at module scope, which is exactly what autoUpdate expects when using injectManifest (no additional messageSkipWaiting plumbing needed).
  • No call sites subscribe to updatefound / controllerchange / needRefresh, and there is no update-prompt UI anywhere in src/routes/+layout.svelte or src/lib/push.ts, so removing the prompt path 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 future workbox runtime 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

📥 Commits

Reviewing files that changed from the base of the PR and between 7782311 and 216e3f3.

📒 Files selected for processing (3)
  • package.json
  • src/app.html
  • vite.config.ts

@Nkburdick Nkburdick merged commit ec65fa5 into main Apr 24, 2026
2 checks passed
@Nkburdick Nkburdick deleted the fix/pwa-cache-bust-and-meta branch April 24, 2026 20:09
Nkburdick added a commit that referenced this pull request Apr 24, 2026
* 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant