Files
touchbase/docs/progress/2026-05-02-pwa-and-polish.md
2026-05-02 14:05:30 -04:00

8.3 KiB
Raw Blame History

2026-05-02 — PWA Shell + Polish (Phases D + E)

Companion to BuildLog.md. Predecessor: 2026-05-02-booking-lifecycle.md. Closes Phases D and E of the post-payments UX plan — UX-completeness done.

Milestone

Two small phases bundled because both came in well under estimate. The app is now installable as a PWA (Add to Home Screen on iOS / Android, manifest + service worker + icons), has friendly 404 / error pages instead of Next's default boxes, and the slot grid + admin tables stop crushing on narrow viewports.

This closes the UX-completeness pivot. Backend is feature-complete for v1 minus payments + reminders. Next workstream is 5d Stripe.

What landed

Phase D — PWA shell

Path Role
public/icon.svg Simple TB monogram on dark rounded square — works as favicon and as PWA icon at any size
public/icon-mask.svg Maskable variant — full-bleed background, content in safe area for Android adaptive icons
src/app/manifest.ts Next.js typed Metadata Manifest. name + short_name + display=standalone + theme color + 2 icons (any + maskable)
public/sw.js Hand-rolled service worker. Network-first for HTML, stale-while-revalidate for static assets, never caches API/auth/admin/account/therapist/book paths. Versioned cache name (tb-html-v1, tb-static-v1).
src/app/layout.tsx Added manifest, appleWebApp, icons to metadata. Added viewport.themeColor (light + dark + viewport-fit=cover for iPhone notch). Inline SW registration script gated to NODE_ENV === "production" (avoids stale-chunk weirdness in dev).

Phase E — Polish

Path Role
src/app/not-found.tsx Friendly 404 — required because notFound() is called in several admin/account pages
src/app/error.tsx Global error boundary client component with "Try again" + "Go home" buttons. Error digest displayed for debugging.
Slot grids in /book and /admin/bookings/new Default to grid-cols-3 instead of crushing to 1 col on mobile. Promotes to 4/6 cols at sm/md.
Admin tables (5 files) overflow-hiddenoverflow-x-auto so they scroll on narrow viewports instead of clipping

What's verified

  • pnpm test79/79 green
  • pnpm lint — clean
  • pnpm exec tsc --noEmit — clean
  • Live PWA smoke (curl on dev server):
    • GET /sw.js → 200, application/javascript, 3.3 KB
    • GET /manifest.webmanifest → 200, application/manifest+json, well-formed JSON with TouchBase + display=standalone + theme color + 2 icon refs
    • GET /icon.svg → 200, image/svg+xml, 361 B
    • GET /icon-mask.svg → 200, 437 B
    • HTML head includes: <meta viewport> (with viewport-fit=cover), 2× <meta theme-color> (light + dark), <link rel="manifest">, <meta mobile-web-app-capable>, <meta apple-mobile-web-app-title>, <meta apple-mobile-web-app-status-bar-style>, <link rel="apple-touch-icon">
  • 404 smoke: GET /no-such-page → renders the new not-found page with "404" heading + "We couldn't find that page" + "Back to home" link

Decisions ratified

Decision Resolution
PWA library None — hand-rolled public/sw.js (~80 LOC). Reason: zero dependency, full visibility into caching behavior, easy to reason about. next-pwa / @serwist/next are reasonable upgrades when we have specific needs (offline queueing, push notifications, etc.).
Icon format SVG (single file, scaled by browser). Reason: ships in 360 bytes, looks crisp at any size, no asset pipeline. iOS Safari supports SVG apple-touch-icon from iOS 17+; on older iOS the icon falls back to the bookmark default — acceptable for v1. We can add a 180×180 PNG later if needed.
SW caching policy Network-first for HTML; stale-while-revalidate for static; never cache /api/, /admin/, /account/, /therapist/, /book*. Reason: any cached page in those routes could show stale slot availability or stale booking state. Safer to require fresh data.
Versioned cache name tb-html-v1, tb-static-v1. Bump on deploys that should invalidate. Reason: simple manual control vs. fingerprint-based invalidation.
SW registration timing window.addEventListener('load', ...) — runs after the page is interactive so it doesn't compete with first paint. Production-only via inline script gated on NODE_ENV.
mobile-web-app-capable instead of apple-mobile-web-app-capable Next.js's appleWebApp.capable: true emits the modern unprefixed form, which is the spec-current name. Both Apple and Android browsers respect it.
dynamic = "force-dynamic" everywhere Already the default in our pages. SW reinforces this — stale HTML is OK on cold reload but never on subsequent navigation.
Error page is a Client Component Required by Next.js's error boundary spec. Server logs already capture the underlying error; client-side console.error is a backup for browser inspection.
404 + error page styling Match the rest of the app — minimal, Tailwind utility classes, matches dark mode automatically.
Mobile slot grid breakpoint grid-cols-3 sm:grid-cols-4 md:grid-cols-6. 3-across on phone is comfortable thumb territory; 4 on tablet; 6 on desktop.
Admin table overflow overflow-x-auto rather than overflow-hidden. Tradeoff: rounded corners look slightly worse when content overflows, but the alternative (clip) is functionally broken on phone.

Gotchas hit

apple-mobile-web-app-capable missing

Initial smoke didn't find this meta tag. Turned out Next.js (modern versions) emits the unprefixed mobile-web-app-capable instead — which is the current spec form. Both work. No fix needed.

Started with <a href="/"> to avoid importing <Link> for one element. ESLint rule (@next/next/no-html-link-for-pages) caught it. Switched to <Link>.

'event' is defined but never used in SW

Pure JS file linted alongside the rest. The unused param in the install handler triggered the unused-vars rule. Removed the param.

Open questions

  1. Customer-visible brand name (still pending)
  2. Currency
  3. Stripe account ownership — needed for next phase
  4. NEW: should the SW also cache the home page for an "offline" experience? Currently network-first means it works offline ONLY if previously visited. Acceptable for v1.
  5. NEW: PNG fallback for apple-touch-icon for iOS <17? Defer until someone reports the bookmark icon is missing.

Roadmap status

UX-completeness:

  • A — Admin CRUD: done 2026-05-02
  • B — Therapist self-serve: done 2026-05-02
  • C — Booking lifecycle: done 2026-05-02
  • D — PWA shell: done 2026-05-02 (this session, part 1)
  • E — Polish: done 2026-05-02 (this session, part 2)

UX is done. Backend roadmap continues:

  • 5d — Stripe deposit flow + webhook
  • 5e — Email reminders (pg-boss scheduled jobs)

5d — Stripe deposit flow. The biggest remaining backend chunk. Sequence per BuildLog.md notes:

  1. Stripe SDK + env config (test keys — user provides)
  2. Render Stripe Elements on /book/confirm (this is the first place we need a real Client Component for interactive UI — past Client Components have been minimal)
  3. createPaymentIntent for the deposit at createHold time; pass client_secret to the page
  4. Webhook handler at /api/stripe/webhook (verifies signature, updates Payment row, transitions Booking from HOLD to CONFIRMED on payment_intent.succeeded)
  5. Hold expiry job via pg-boss cancels HOLDs whose deposit didn't capture in time

~35 days. After 5d, 5e reminders is small — pg-boss schedule + reminder template + send job (~23 days).

Before starting 5d we'll need:

  • Stripe test secret key + publishable key
  • Webhook signing secret (from Stripe CLI for local dev)

How to resume

cd /Users/noise/Documents/code/touchbase
docker-compose up -d postgres mailpit
pnpm db:seed
pnpm tsx scripts/book-on-behalf.ts alex@example.com "60-minute Swedish" 2026-05-05T10:00
pnpm dev
# Try the customer flow on a phone (or DevTools mobile preview):
#   http://<your-IP>:3000 → tap a service → date → pick time → sign in → confirm
#   Add to Home Screen — PWA icon + standalone display
# Try the admin/therapist flows on a phone — tables now scroll horizontally
# Hit a bad URL like /xyzzy → see the 404 page