123 lines
8.3 KiB
Markdown
123 lines
8.3 KiB
Markdown
|
|
# 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-hidden` → `overflow-x-auto` so they scroll on narrow viewports instead of clipping |
|
|||
|
|
|
|||
|
|
## What's verified
|
|||
|
|
|
|||
|
|
- `pnpm test` — **79/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.
|
|||
|
|
|
|||
|
|
### Lint flagged `<a>` instead of `<Link>` in error page
|
|||
|
|
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)
|
|||
|
|
|
|||
|
|
## Recommended next step
|
|||
|
|
|
|||
|
|
**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
|
|||
|
|
|
|||
|
|
~3–5 days. After 5d, **5e reminders** is small — pg-boss schedule + reminder template + send job (~2–3 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
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
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
|
|||
|
|
```
|