Add PWA support with service worker and offline-first caching - #11
Merged
Conversation
The web client had no manifest, no icons, no worker and nothing offline. Add all four, and derive the cache version from the build so nobody ever edits a version constant to ship an update. How the versioning works: client/build/pwa.ts hashes every file in the finished dist/ and stamps that hash (plus the precache lists) into dist/sw.js. A git SHA would not do — the Docker build copies no .git, so an unpassed BUILD_ID would silently freeze at "dev" and installed clients would never update. A content hash changes exactly when the shipped bytes change, in every build path, configured by nothing. The worker precaches the whole bundle (shell, engine, wasm sim, model, fonts, audio) and carries unchanged entries forward from the previous version, so an update usually re-downloads only what changed. The ~32 MB ffmpeg core is a separate best-effort tier: needed for offline export, but a storage-quota rejection there must not fail the install and take offline boot down with it. Data Saver skips it; the fetch handler picks it up lazily on first use. Updates never interrupt a match. The worker does not call skipWaiting(); src/pwa/register.ts applies a waiting build immediately when the player is on the menu, and otherwise shows a toast and swaps on the way back. Open tabs re-check on load, on refocus, and every 15 minutes. Also fixes two things the PWA depends on: the server marked everything under assets/ immutable for a year, including files copied verbatim out of public/ (audio, fonts, robot.glb), so a replaced sound could never reach a browser; build.rs now flags only content-hashed names. And .webmanifest and .ogg had no mime type. BUILD_ID (the replay-header stamp) now falls back to the working tree's git SHA instead of "dev". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011wwZt4TR3iwDE33Rv7iPiC
The manifest's display:"fullscreen" only applies to an installed launch, so every player in a browser tab — nearly all of them — was still looking at a windowed page. Nothing called the Fullscreen API at all. - game/fullscreen.ts owns the lot: enter/exit/toggle with the webkit fallbacks Safari still needs, a persisted sz-fullscreen preference (on by default, following the savedQuality pattern), and a best-effort landscape lock on phones, which is the orientation the thumbstick and action buttons are laid out for. - Starting a match goes fullscreen, requested synchronously from the click: browsers only grant it while that gesture is still active, so it cannot wait for ensureMode() or the room POST. - A toggle button top-right of the menu, plus Settings > Display. The menu button is what gives touch players access at all — the settings gear is hidden on touch. - pwa/install.ts captures beforeinstallprompt and the menu offers INSTALL APP. On iPhone that is the only route to a chrome-free game, since Safari there has no Fullscreen API. The listener attaches before main() because the event fires while the sim, model and audio are still loading. - Manifest declares orientation: landscape. zz-restart builds its own browser context, so it never got the ?nosw=1 opt-out newGamePage gives every other spec: it was precaching the whole bundle from the disposable server the test then SIGTERMs, and the shutdown notice lost the race against a ~35 MB download, leaving the client on its generic "connection lost". Headless Chromium grants requestFullscreen with no user gesture whatsoever, so the gesture-ordering rule cannot be asserted from a spec; 16-fullscreen.spec.ts says so rather than implying coverage it does not have. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011wwZt4TR3iwDE33Rv7iPiC
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements a complete Progressive Web App (PWA) experience for SmashZone, enabling installation as a standalone app and offline-first functionality with automatic updates.
Summary
This change adds service worker registration, precaching, and automatic update handling to make SmashZone installable and fully functional offline. The implementation includes a content-hash-based versioning system that requires no manual version bumping, automatic update detection and application, and a user-friendly update notification system.
Key Changes
Service Worker (
src/pwa/sw.ts): Self-contained worker that handles precaching, cache versioning, and fetch interception. Implements a three-tier caching strategy:PWA Build Plugin (
build/pwa.ts): Vite plugin that:Service Worker Registration (
src/pwa/register.ts): Handles:skipWaiting()to avoid interrupting matchesUpdate Toast UI (
src/ui/components/UpdateToast.svelte): Non-intrusive notification shown when an update is waiting, with options to reload immediately or waitWeb App Manifest & Icons:
public/manifest.webmanifestwith proper PWA metadatabuild/gen-icons.ts) using PlaywrightBuild Identity (
build/buildid.ts): Resolves build identifier from:BUILD_IDenvironment variable (CI/Docker)Server Updates:
.webmanifestand.oggfilesno-cacheforsw.js(always revalidate),immutablefor content-hashed assetssw.jsE2E Tests (
e2e/specs/15-pwa.spec.ts): Comprehensive PWA testing including:Notable Implementation Details
.gitdirectory by using content hash instead of git SHA for cache versioninghttps://claude.ai/code/session_011wwZt4TR3iwDE33Rv7iPiC