Files
JamBuddy/docs/pwa-firebase-plan.md
T

307 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Plan: PWA + Firebase Hosting for JamBuddy
Status: **proposed**. Each phase is independently shippable; stop after any one.
---
## 0. Why this is low-risk
The Electron shell (`electron/main.cjs`, `preload.cjs`) does **nothing but host a window** — no IPC, no native APIs, `sandbox:false` only to give the renderer full Web Audio. All logic lives in the React renderer, which already runs browser-only via `npm run dev`. So the web build is the same `vite build` output the desktop installer already ships; we just add a manifest + service worker and serve `dist/` over HTTPS. Firebase gives us HTTPS for free, which `getUserMedia` (the mic) requires — that's the only hard web constraint.
Nothing in the app makes network calls today, so there's no API surface to re-host and no secrets to manage.
---
## Phase 1 — PWA scaffolding (no Firebase yet)
Goal: `vite build` produces an installable, offline-capable web app served from any static host.
### 1.1 Add `vite-plugin-pwa`
Only new dependency. Wraps Workbox under a Vite plugin — generates the service worker and injects the manifest. Cheaper than hand-rolling a SW and gets update flow for free.
```js
// vite.config.js
import { VitePWA } from 'vite-plugin-pwa'
export default defineConfig({
plugins: [
react(),
tailwindcss(),
VitePWA({
registerType: 'autoUpdate',
includeAssets: ['favicon.ico', 'apple-touch-icon.png'],
manifest: {
name: 'JamBuddy — WhatTheFlat',
short_name: 'JamBuddy',
description: 'Real-time key and chord detection for live jams.',
theme_color: '#0f0f0f', // matches bg-surface token
background_color: '#0f0f0f',
display: 'standalone',
orientation: 'any',
start_url: '/',
scope: '/',
icons: [ /* see 1.3 */ ],
},
workbox: {
globPatterns: ['**/*.{js,css,html,svg,png,ico,woff2}'],
// Audio is captured live — nothing to cache from a remote. Precache the app shell only.
navigateFallback: '/index.html',
},
}),
],
})
```
### 1.2 Create `public/` directory
Vite serves `public/` at root and copies it verbatim into `dist/`. We need it for:
- `favicon.ico`, `apple-touch-icon.png` (180×180)
- Any static PWA icons referenced by the manifest
- (Optional) `robots.txt` — not needed for an app, skip.
The existing `src/assets/whattheflat-logo.png` is imported in `App.jsx` and bundled by Vite — leave that as-is; **don't** move it. The manifest icons must live in `public/` because they're referenced by URL, not imported.
### 1.3 Generate icon set from the existing logo
Derive all required sizes from `assets/whattheflat-logo.png` (one source image → PNG output). Required manifest entries:
```js
icons: [
{ src: '/icon-192.png', sizes: '192x192', type: 'image/png', purpose: 'any' },
{ src: '/icon-512.png', sizes: '512x512', type: 'image/png', purpose: 'any' },
{ src: '/icon-512-maskable.png', sizes: '512x512', type: 'image/png', purpose: 'maskable' },
]
```
Plus `apple-touch-icon.png` (180×180) in `public/` — iOS ignores the manifest and uses this `<link>`. The maskable variant needs safe-zone padding (~10%) so the logo isn't cropped by Android's circular mask.
One-liner with ImageMagick if available, else export manually from the source PNG:
```bash
magick assets/whattheflat-logo.png -resize 192x192 public/icon-192.png
magick assets/whattheflat-logo.png -resize 512x512 public/icon-512.png
magick assets/whattheflat-logo.png -resize 180x180 public/apple-touch-icon.png
```
### 1.4 Update `index.html`
Two problems with the current `<head>`:
1. **No PWA tags** — add theme-color, manifest link, apple-touch-icon, apple-mobile-web-app-capable, description.
2. **CSP is hardcoded for dev**`connect-src 'self' http://localhost:5173 ws://localhost:5173` and `script-src 'self' 'unsafe-eval'`. The `unsafe-eval` and the localhost entries are Vite dev artifacts; production Vite doesn't need `unsafe-eval`. Keep a CSP (it's good hygiene and the app is genuinely offline) but drop the dev bits:
```html
<meta name="theme-color" content="#0f0f0f" />
<meta name="description" content="Real-time key and chord detection for live jams." />
<link rel="manifest" href="/manifest.webmanifest" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
```
CSP (production):
```
default-src 'self';
script-src 'self';
style-src 'self' 'unsafe-inline';
img-src 'self' data:;
connect-src 'self';
media-src 'self' blob:; <!-- the mic stream lives in a blob -->
```
`blob:` in `media-src` is required because `getUserMedia` streams are blob-backed and the analyser reads them. Verify nothing else regresses — `vite-plugin-pwa` registers the SW from a same-origin script, so `'self'` covers it.
Move the dev-only CSP into a Vite conditional so dev still allows HMR/websocket:
```js
// vite.config.js — inject dev CSP via plugin, or keep a separate dev index template.
```
Simplest: keep one CSP in `index.html` with the production values, and in dev let Vite's own injected tags coexist (HMR works over the same origin websocket; CSP `connect-src 'self'` already allows it). If dev breaks, add `ws://localhost:5173` only in a dev-specific block.
### 1.5 Verify
- `npm run build``dist/` contains `manifest.webmanifest`, `registerSW.js`, `sw.js`, and all `public/` icons.
- Serve `dist/` locally with `npm run preview` and use Chrome DevTools → Application → Manifest (should show icons, installability ✓) and Service Workers (registered).
- Install to desktop / "Add to Home Screen" on mobile.
- Confirm the app still works **fully offline** after first load (airplane mode). Since there are no network calls, the only risk is the SW failing to precache — Workbox handles this.
---
## Phase 2 — Firebase Hosting setup
Goal: serve `dist/` over HTTPS on a `*.web.app` domain (or custom domain).
### 2.1 Install the CLI
```bash
npm install -D firebase-tools
```
### 2.2 Init
```bash
npx firebase init hosting
```
Answer:
- **Public directory:** `dist`
- **Single-page app (rewrite all urls to /index.html):** Yes — though the app has no client-side routing today, this protects future routes and is harmless.
- **Set up automatic builds with GitHub:** **No.** (You're not using GitHub.)
- **File `dist/index.html` already exists — overwrite?** **No.** This is critical — saying yes would clobber the Vite-built index.
This creates:
- `firebase.json`
- `.firebaserc`
### 2.3 `firebase.json` (target shape)
```json
{
"hosting": {
"public": "dist",
"ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
"rewrites": [{ "source": "**", "destination": "/index.html" }],
"headers": [
{
"source": "**/*.@(js|css|svg|png|ico|woff2|webmanifest)",
"headers": [
{ "key": "Cache-Control", "value": "public, max-age=31536000, immutable" }
]
},
{
"source": "/sw.js",
"headers": [
{ "key": "Cache-Control", "value": "no-cache" }
]
},
{
"source": "/index.html",
"headers": [
{ "key": "Cache-Control", "value": "no-cache" }
]
}
]
}
}
```
Why the headers: Vite hashes asset filenames, so JS/CSS/icons are content-addressed → cache them **immortal**. `index.html` and `sw.js` must stay fresh so updates propagate — `no-cache` revalidates them every time. (Note: with `vite-plugin-pwa`'s `autoUpdate`, the SW self-updates; the `no-cache` on `sw.js` is belt-and-braces.)
### 2.4 First deploy
```bash
npm run build
npx firebase deploy --only hosting
```
Output: `https://<project>.web.app`. The mic will now work because the origin is HTTPS.
### 2.5 `.gitignore`
Add `.firebase/` and `firebase-debug.log` (already in a sane gitignore, but confirm).
---
## Phase 3 — (optional) custom domain
In Firebase console → Hosting → Add custom domain. Add the DNS TXT/CNAME records it gives you. Lets Encrypt cert is auto-provisioned. No app change needed.
---
---
## Phase 4 — Web optimization checklist
Things that matter specifically for shipping this app on the web. Most are cheap; do them before the first public deploy.
### 4.1 Drop the dead dependency
`audiomotion-analyzer` is listed in `package.json` but **imported nowhere** in `src/`. Vite tree-shakes unused ESM, so it likely doesn't bloat the production bundle — but it still inflates `npm install` and is misleading. Either:
- **Remove it** from `dependencies` (one-line `npm uninstall audiomotion-analyzer`), or
- If something depends on it being present (it doesn't — grep confirms zero imports), wire it up.
Recommend: remove.
### 4.2 Audit bundle size
```bash
npm run build
npx vite-bundle-visualizer # one-off, no config needed
```
Expected heavy bits: `pitchy` (small, pure JS) and React 19. `electron` / `electron-builder` are devDeps and excluded from the web build. If the visualizer shows anything surprising (e.g. a transitive dep pulling in moment/lodash), trim it.
### 4.3 Code-split the heavy, rarely-used views
`DebugView`, `DrumView`, and `Tuner` are collapsible panels most users never open. They import theory helpers and own audio contexts. Lazy-load them so the initial bundle stays lean:
```jsx
const Tuner = lazy(() => import('./components/Tuner'))
const DebugView = lazy(() => import('./components/DebugView'))
const DrumView = lazy(() => import('./components/DrumView'))
```
Wrap in `<Suspense fallback={null}>`. These are already behind toggle buttons, so there's no UX cost. This is the single biggest web win — the core pitch/chord flow loads first.
### 4.4 Audio permissions & UX on the web
- **HTTPS** — covered by Firebase. `getUserMedia` is blocked on plain HTTP (except `localhost`).
- **User gesture** — the existing "Start Listening" button already provides the gesture the browser requires to call `getUserMedia`. Don't change this; never auto-start listening on load.
- **Mobile Safari quirks** — iOS Safari:
- Requires `audio: { echoCancellation: false }` style may differ; test the existing `AudioCapture` constraints on an iPhone.
- AudioContext must be resumed after a gesture — already handled since capture starts on click.
- `sharedArrayBuffer` is not needed by this app (no worklets), so no COOP/COEP headers required. Good — don't add them; they'd complicate the Firebase static host.
- **Permissions API** — optionally surface "microphone blocked" before the user clicks, using `navigator.permissions.query({ name: 'microphone' })`. The existing `micError` state already covers the denied case; this is a polish step, not required.
### 4.5 Mobile layout
The app uses Tailwind with responsive `lg:` breakpoints already (e.g. the progressions sidebar is `hidden lg:block`). For a real mobile deploy:
- Test the fretboard / piano SVGs at phone widths — they're SVG so they scale, but text labels may crowd.
- The `controls bar` wraps with `flex-wrap` — good. Verify the key-lock select dropdowns are tappable.
- Consider a `display: standalone` install prompt; the manifest already sets `standalone`.
- This is a "play and look at the screen" app — portrait guitar fretboard orientation may want `orientation: 'portrait'` in the manifest instead of `'any'`. Decide based on the primary instrument view.
### 4.6 Performance for the audio hot loop
Already mostly tuned (the dual-analyser design is deliberate). Web-specific notes:
- `requestAnimationFrame` driving the analyser is fine on web — no change.
- `Float32Array` chroma math is cheap. No change needed.
- Don't add a Workbox runtime cache for audio — there's no audio to cache; everything is live.
- If you ever add an AudioWorklet, its file must be served with the correct MIME and same-origin — Workbox precaches it as part of `globPatterns` only if it's emitted to `dist/`. Not needed today.
### 4.7 SEO / social (minimal — this is an app, not content)
- `<title>` and meta description (added in 1.4) are enough for the web.app URL to have a sane share card.
- Add `og:title` / `og:description` / `og:image` to `index.html` if you want a nice link preview (point `og:image` to `/icon-512.png`). Optional.
- No sitemap needed for a single-page offline app.
### 4.8 Keep Electron working
The desktop build must not regress:
- `electron-builder` config in `package.json` is untouched by any of the above.
- The CSP change in `index.html` affects both targets — verify the desktop window still loads (it serves the built `dist/index.html`, so the new production CSP applies there too; that's fine and arguably better).
- `vite-plugin-pwa` is a dev dependency; it only runs at build time and doesn't touch the renderer runtime in a way that breaks Electron.
- Run `npm run electron:dev` after Phase 1 to confirm the Electron path still works.
---
## What stays Electron-only
Nothing in this plan removes or forks code. The Electron and web builds share the exact same `dist/` output. The only per-target differences are:
- Electron adds the `electron/main.cjs` shell and `electron-builder` packaging.
- Web adds the PWA manifest + SW (ignored by Electron) and is served over HTTPS.
If a future feature needs a native API (filesystem dialogs, auto-launch, tray), it would go through Electron's preload/IPC — which currently exposes nothing. At that point you'd add an `isElectron` guard and a `preload` IPC bridge. Not needed for any current feature.
---
## Suggested order of operations
1. **Phase 1.2** — create `public/`, drop in icons (Phase 1.3).
2. **Phase 1.1 + 1.4** — add `vite-plugin-pwa`, update `index.html`, relax CSP.
3. Verify with `npm run preview` + DevTools (Phase 1.5).
4. **Phase 2**`firebase init hosting`, hand-tune `firebase.json`, first deploy.
5. **Phase 4.1** — remove dead `audiomotion-analyzer` dep.
6. **Phase 4.3** — lazy-load the three collapsible views.
7. Polish: mobile pass (4.5), bundle audit (4.2), optional OG tags (4.7).
Each step is independently revertible. Stop after step 4 and you have a working, installable, offline PWA on the web; the rest is optimization.