# PWA Implementation Notes ## Current state The groundwork is already in place: - `public/site.webmanifest` exists and is linked in `layout.ejs` - All icon sizes are present (`favicon.ico`, `favicon-16x16.png`, `favicon-32x32.png`, `apple-touch-icon.png`, `android-chrome-192x192.png`, `android-chrome-512x512.png`) - The app is served from a single origin --- ## Step 1 — Fix the web manifest `public/site.webmanifest` currently has empty `name`/`short_name` and wrong colors. Update it to: ```json { "name": "Bri-Tunes", "short_name": "Bri-Tunes", "description": "Retrofuture music player", "start_url": "/", "display": "standalone", "background_color": "#05060d", "theme_color": "#ff2bd6", "icons": [ { "src": "/static/android-chrome-192x192.png", "sizes": "192x192", "type": "image/png" }, { "src": "/static/android-chrome-512x512.png", "sizes": "512x512", "type": "image/png" } ] } ``` Note: icon `src` paths need the `/static/` prefix since Express serves `public/` at that mount point. The manifest itself is also served from `/static/site.webmanifest`. Also add `theme-color` meta to `src/views/layout.ejs` so the browser chrome matches on mobile: ```html ``` --- ## Step 2 — Write the service worker Create `public/sw.js`. Recommended caching strategy for a server-rendered music app: ### Static shell — cache-first Cache these on install so the app shell loads instantly on repeat visits: ``` /static/css/app.css /static/js/player.js /static/js/bg-grid.js /static/js/player-viz.js /static/js/boot-sequence.js /static/vendor/fonts/Orbitron.woff2 /static/vendor/fonts/Inter.woff2 /static/vendor/three/three.module.min.js /static/favicon.ico /static/android-chrome-192x192.png ``` ### HTML pages — network-first with cache fallback Fetch from network; if offline serve the cached version. Cache `/` on install as the offline fallback page. ### Audio streams (`/stream/*`) — do not cache Audio files are large and frequently requested with HTTP Range headers. The service worker should `return fetch(event.request)` for these and never store them. ### Skeleton implementation ```js const CACHE = 'bri-tunes-v1'; const STATIC = [ '/', '/static/css/app.css', '/static/js/player.js', '/static/js/bg-grid.js', '/static/js/player-viz.js', '/static/js/boot-sequence.js', '/static/vendor/fonts/Orbitron.woff2', '/static/vendor/fonts/Inter.woff2', '/static/vendor/three/three.module.min.js', ]; self.addEventListener('install', (e) => { e.waitUntil(caches.open(CACHE).then((c) => c.addAll(STATIC))); self.skipWaiting(); }); self.addEventListener('activate', (e) => { e.waitUntil( caches.keys().then((keys) => Promise.all(keys.filter((k) => k !== CACHE).map((k) => caches.delete(k))) ) ); self.clients.claim(); }); self.addEventListener('fetch', (e) => { const { request } = e; const url = new URL(request.url); // Never intercept audio streams or cross-origin requests. if (url.pathname.startsWith('/stream/') || url.origin !== self.location.origin) return; // Static assets: cache-first. if (url.pathname.startsWith('/static/')) { e.respondWith( caches.match(request).then((cached) => cached || fetch(request)) ); return; } // HTML pages: network-first, fall back to cache. e.respondWith( fetch(request) .then((res) => { const clone = res.clone(); caches.open(CACHE).then((c) => c.put(request, clone)); return res; }) .catch(() => caches.match(request)) ); }); ``` --- ## Step 3 — Register the service worker Add to `src/views/layout.ejs` just before ``: ```html ``` The service worker file lives at `public/sw.js` and is served at `/static/sw.js`. **Important:** the service worker's scope defaults to its path (`/static/`), which limits it to only intercepting requests under `/static/`. To control the full origin it must be served from `/sw.js` (i.e., at the root, not under `/static/`). This means `sw.js` needs a **dedicated route** in Express rather than being served as a static file: ```js // In src/app.js, before the static middleware: app.get('/sw.js', (req, res) => { res.set('Service-Worker-Allowed', '/'); res.sendFile(path.join(__dirname, '..', 'public', 'sw.js')); }); ``` And register it at the root in the template: ```html ``` --- ## Step 4 — Bump cache version on deploy When CSS/JS changes, increment the cache name (`bri-tunes-v1` → `bri-tunes-v2`). The `activate` handler automatically evicts the old cache. --- ## Files to create / modify | File | Change | |------|--------| | `public/site.webmanifest` | Fill in name, colors, fix icon paths | | `src/views/layout.ejs` | Add `theme-color` meta, add SW registration script | | `src/app.js` | Add `/sw.js` route with `Service-Worker-Allowed: /` header | | `public/sw.js` | New — service worker (see skeleton above) | --- ## Out of scope (optional future work) - **Offline song caching** — let users pin songs for offline playback. Requires an explicit "Save offline" button, manual `cache.put()` calls, storage quota management, and UI to show cached vs. uncached songs. - **Background sync** — retry failed actions (e.g., playlist edits) when connectivity returns. - **Push notifications** — notify users when new songs are added.