Bri-Tunes/PWA.md

201 lines
5.5 KiB
Markdown

# 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
<meta name="theme-color" content="#ff2bd6">
```
---
## 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 `</body>`:
```html
<script>
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/static/sw.js');
}
</script>
```
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
<script>
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js');
}
</script>
```
---
## 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.