feat(pwa): installable PWA + offline message outbox (LC-98) #160

Merged
nrupard merged 1 commit from feat/lc-98-pwa-offline-outbox into main 2026-05-20 17:11:14 +02:00
Owner

Summary

Makes lets-chat an installable PWA and adds an offline outbox so messages typed without a connection queue and send automatically on reconnect (LC-98).

PWA install

  • New server/assets/manifest.webmanifest: name, display: standalone, start_url: /, theme/background colors, icons at 192 + 512 PNG, a maskable 512, and the SVG. Icons generated from the existing favicon.
  • base.html links the manifest + adds theme-color meta and apple-touch-icon.
  • Browsers surface "Install lets-chat"; the installed app launches standalone with its own icon.

Service worker (sw.js)

Alongside the existing push / notificationclick handlers (preserved verbatim):

  • Versioned shell cache: pre-caches CSS, HTMX bundle, icons, manifest, and an offline.html fallback. Cache name embeds the build's asset version. routes/push.rs now serves sw.js dynamically, substituting __ASSET_VERSION__, so each deploy gets a fresh cache that the activate handler evicts. Served with Cache-Control: no-cache.
  • Fetch router: network-first for navigations (falls back to the cached page, then offline.html); cache-first for /assets.
  • Offline outbox: intercepts POSTs to /room/{id}/messages (+ thread variant; DMs share the room endpoint). On network error it stores the body in IndexedDB and returns a synthetic success so the composer clears, then replays on reconnect via Background Sync (where supported) plus an explicit drain on the page's online event. Replay failures (room gone, muted) are marked failed and surfaced with Retry / Discard.

Page client (outbox.js)

Registers the SW unconditionally (independent of Web Push), relays drain/retry/discard, and renders the SW's queue state into a global banner (#lc-outbox) plus an inline composer hint (#lc-composer-net). The SW owns the IndexedDB queue as the single source of truth.

Notes / open questions from the issue

  • Outbox covers text + attachment-bearing messages (the message-create endpoint). Reactions and edits are out of scope for this pass.
  • iOS Safari: PWA push works only when installed to the home screen; standard platform constraint, no code change.
  • Web Push is unchanged; the existing registration reuses the always-registered worker.
  • Drive-by: fixed a clippy manual_repeat_n lint in db/analytics.rs that the toolchain flagged on this branch.

Tests

server/tests/routes_pwa.rs: version-substituted worker (content type, no-cache, placeholder gone), retained push + fetch/outbox handlers, and PWA head wiring on the login page. Outbox queue/replay logic is browser/IndexedDB behaviour, exercised manually. just check, just test, just test-saas all green.

🤖 Generated with Claude Code

## Summary Makes lets-chat an installable PWA and adds an offline outbox so messages typed without a connection queue and send automatically on reconnect (LC-98). ## PWA install - New `server/assets/manifest.webmanifest`: name, `display: standalone`, `start_url: /`, theme/background colors, icons at 192 + 512 PNG, a maskable 512, and the SVG. Icons generated from the existing favicon. - `base.html` links the manifest + adds `theme-color` meta and `apple-touch-icon`. - Browsers surface "Install lets-chat"; the installed app launches standalone with its own icon. ## Service worker (`sw.js`) Alongside the existing push / notificationclick handlers (preserved verbatim): - **Versioned shell cache**: pre-caches CSS, HTMX bundle, icons, manifest, and an `offline.html` fallback. Cache name embeds the build's asset version. `routes/push.rs` now serves `sw.js` dynamically, substituting `__ASSET_VERSION__`, so each deploy gets a fresh cache that the `activate` handler evicts. Served with `Cache-Control: no-cache`. - **Fetch router**: network-first for navigations (falls back to the cached page, then `offline.html`); cache-first for `/assets`. - **Offline outbox**: intercepts POSTs to `/room/{id}/messages` (+ thread variant; DMs share the room endpoint). On network error it stores the body in IndexedDB and returns a synthetic success so the composer clears, then replays on reconnect via Background Sync (where supported) plus an explicit drain on the page's `online` event. Replay failures (room gone, muted) are marked failed and surfaced with Retry / Discard. ## Page client (`outbox.js`) Registers the SW unconditionally (independent of Web Push), relays drain/retry/discard, and renders the SW's queue state into a global banner (`#lc-outbox`) plus an inline composer hint (`#lc-composer-net`). The SW owns the IndexedDB queue as the single source of truth. ## Notes / open questions from the issue - Outbox covers text + attachment-bearing messages (the message-create endpoint). Reactions and edits are out of scope for this pass. - iOS Safari: PWA push works only when installed to the home screen; standard platform constraint, no code change. - Web Push is unchanged; the existing registration reuses the always-registered worker. - Drive-by: fixed a clippy `manual_repeat_n` lint in `db/analytics.rs` that the toolchain flagged on this branch. ## Tests `server/tests/routes_pwa.rs`: version-substituted worker (content type, `no-cache`, placeholder gone), retained push + fetch/outbox handlers, and PWA head wiring on the login page. Outbox queue/replay logic is browser/IndexedDB behaviour, exercised manually. `just check`, `just test`, `just test-saas` all green. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(pwa): installable PWA + offline message outbox (LC-98)
All checks were successful
Check / clippy + fmt + tests (pull_request) Successful in 1m16s
2fdd311f40
Make lets-chat an installable PWA and let messages typed while offline queue and send automatically on reconnect.

PWA install: add server/assets/manifest.webmanifest (name, icons at 192/512 + maskable + SVG, display standalone, start_url /, theme + background colors) and link it from base.html along with a theme-color meta and apple-touch-icon. Icons are generated from the existing favicon. Browsers now surface an "Install lets-chat" prompt and the installed app launches standalone with its own icon.

Service worker (server/assets/sw.js) gains, alongside the existing push / notificationclick handlers: a versioned shell cache (pre-caches CSS, the HTMX bundle, icons, manifest, and an offline fallback page), a fetch router (network-first for navigations with an offline.html fallback, cache-first for /assets), and an offline outbox. The cache name embeds the build's asset version: routes/push.rs now serves sw.js dynamically, substituting __ASSET_VERSION__ so each deploy gets a fresh cache that the activate handler evicts. The worker is served with Cache-Control: no-cache so the script itself always revalidates.

Offline outbox: the SW intercepts POSTs to /room/{id}/messages (and the thread variant; DMs share the room endpoint). On a network error it stores the request body in IndexedDB and returns a synthetic success so the composer clears, then replays the queue on reconnect via Background Sync (where supported) and an explicit drain triggered by the page's online event. Messages that fail on replay (room gone, muted, ...) are marked failed and surfaced with Retry / Discard controls. server/assets/outbox.js is the page-side client: it registers the SW unconditionally (independent of Web Push), relays drain/retry/discard commands, and renders the SW's queue state into a global banner plus an inline composer hint. The SW owns the IndexedDB queue as the single source of truth.

Web Push behaviour is unchanged: the push and notificationclick handlers are preserved verbatim, and the existing push registration reuses the now-always-registered worker.

Also fixes a clippy manual_repeat_n lint in db/analytics.rs surfaced by the toolchain on this branch.

Tests: server/tests/routes_pwa.rs covers the version-substituted worker (content type, no-cache header, placeholder gone), retention of the push + fetch/outbox handlers, and the PWA head wiring on the unauthenticated login page. just check, just test, and just test-saas all green.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
nrupard deleted branch feat/lc-98-pwa-offline-outbox 2026-05-20 17:11:15 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/lets-chat!160
No description provided.