docs(01-01): summarize PWA scaffolding plan execution

- Document manifest fields, viewport export shape, and verification results
- Record key decisions (theme_color #0075AD, background_color #FFFFFF,
  metadata.manifest API choice, icon reuse)
- No deviations from plan

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
lorentz 2026-05-03 13:39:55 -04:00
parent d196d22040
commit 76ced68ac1

View file

@ -0,0 +1,142 @@
---
phase: 01-pwa-scaffolding
plan: 01
subsystem: pwa-shell
tags: [pwa, manifest, viewport, mobile]
requires:
- app/layout.tsx (existing root layout with metadata export)
- public/wulff-logo.png, public/favicon.png, public/branding/wulf-mark.png (existing icon assets)
provides:
- public/manifest.json (Web App Manifest at /manifest.json)
- app/layout.tsx exports `viewport: Viewport` with viewportFit: "cover"
- app/layout.tsx exports `metadata.manifest = "/manifest.json"` (Next.js emits <link rel="manifest"> automatically)
affects:
- Phase 02 mobile shell (can rely on viewport-fit=cover for safe-area insets)
- All routes (root layout viewport applies app-wide)
tech-stack:
added: []
patterns:
- Next.js 16 separate `viewport` export (replaces deprecated metadata.viewport)
- Next.js 16 metadata.manifest field (auto-emits <link rel="manifest">)
key-files:
created:
- public/manifest.json
modified:
- app/layout.tsx
decisions:
- theme_color #0075AD chosen as Wulf primary brand blue (sourced from app/styles/brand.css line 28, --wulf-blue) — gives consistent system UI tint in light and dark mode since manifest only allows one value
- background_color #FFFFFF chosen as the light shell background — manifest only allows one splash background, white matches Pulse's default light theme and is acceptable on dark devices (brief flash, not a regression)
- Used metadata.manifest field over hand-rolled <link rel="manifest"> — Next.js 16 emits the link tag automatically, satisfies spec wording, and keeps with the existing metadata API pattern
- Reused existing icon assets with `"sizes": "any"` (wulff-logo.png, branding/wulf-mark.png, favicon.png) instead of generating sized 192/512 variants — install tools accept this for PNGs; sized icons can be added in a future polish phase if install warns
- Added themeColor light/dark pair in viewport (one-line improvement) — paired with Next.js helper, emits per-scheme <meta name="theme-color"> tags. Optional per the plan; kept since it costs nothing and improves dark-mode rendering
- orientation set to "portrait" — phone-first per spec §1/§2; tablet landscape is explicit out-of-scope per spec §7
- scope set to "/" — allow standalone window to navigate anywhere in the app without falling out to browser
metrics:
duration: ~1m
tasks_completed: 2
files_created: 1
files_modified: 1
completed: 2026-05-03T17:38:55Z
---
# Phase 01 Plan 01: PWA Scaffolding Summary
PWA install surface added: a Web App Manifest at `/manifest.json` plus a Next.js 16 viewport export with `viewport-fit=cover` so the mobile shell can paint behind the device home indicator in future phases.
## What Shipped
### Task 1: `public/manifest.json` (NEW)
Hand-written 31-line JSON manifest with all spec-mandated fields:
| Field | Value | Why |
|-------|-------|-----|
| `name`, `short_name` | "Pulse" | Spec §4 verbatim |
| `description` | Wulf operations console blurb | Install dialog readability |
| `start_url` | `/mobile` | Spec §4 — phone install lands on mobile shell, not desktop dashboard |
| `scope` | `/` | Allow standalone window to navigate the whole app |
| `display` | `standalone` | Spec §4 — chromeless app surface |
| `orientation` | `portrait` | Phone-first (spec §1, §2); tablet landscape is OOS (§7) |
| `theme_color` | `#0075AD` | Wulf primary blue from `app/styles/brand.css` line 28 |
| `background_color` | `#FFFFFF` | Light shell background (manifest allows only one) |
| `icons` | 3 entries with `sizes: "any"` | Reuses `/wulff-logo.png`, `/branding/wulf-mark.png`, `/favicon.png` |
No `serviceworker`, no `display_override`, no `prefer_related_applications`, no `next-pwa` — per spec §4 and CLAUDE.md.
**Commit:** `3e3df24`
### Task 2: `app/layout.tsx` (MODIFIED)
Three minimal additions to the existing root layout, body unchanged:
1. Import upgraded: `import type { Metadata, Viewport } from "next";`
2. `metadata.manifest = "/manifest.json"` added alongside the existing `icons` field — Next.js 16 emits `<link rel="manifest" href="/manifest.json" />` in the rendered HTML head automatically (satisfies PWA-02 spec wording).
3. New `viewport` export:
```ts
export const viewport: Viewport = {
width: "device-width",
initialScale: 1,
viewportFit: "cover",
themeColor: [
{ media: "(prefers-color-scheme: light)", color: "#FFFFFF" },
{ media: "(prefers-color-scheme: dark)", color: "#0A0A0A" },
],
};
```
`viewportFit: "cover"` is the load-bearing field for PWA-03 — Next.js renders `viewport-fit=cover` in the `<meta name="viewport">` tag so future phases can use safe-area-inset utilities to paint behind the home indicator. `width`, `initialScale`, and `themeColor` are baseline mobile defaults that prevent Next.js viewport warnings.
**Commit:** `d196d22`
## Verification Results
| Gate | Result |
|------|--------|
| `test -f public/manifest.json` | PASS |
| `jq -e '.name == "Pulse" and .display == "standalone" and .start_url == "/mobile"' public/manifest.json` | PASS (true) |
| `jq -e '.theme_color == "#0075AD" and .background_color == "#FFFFFF"' public/manifest.json` | PASS |
| `jq -e '.icons \| length >= 1' public/manifest.json` | PASS (3 icons) |
| `jq -e '.serviceworker == null' public/manifest.json` | PASS |
| `jq empty public/manifest.json` | PASS (valid JSON) |
| `grep -E '^import type \{ Metadata, Viewport \} from "next"' app/layout.tsx` | PASS |
| `grep -E 'manifest:\s*"/manifest\.json"' app/layout.tsx` | PASS |
| `grep -E '^export const viewport: Viewport = \{' app/layout.tsx` | PASS |
| `grep -E 'viewportFit:\s*"cover"' app/layout.tsx` | PASS |
| `grep -E 'width:\s*"device-width"' app/layout.tsx` | PASS |
| `grep -E 'initialScale:\s*1' app/layout.tsx` | PASS |
| `grep -E 'apple:\s*"/wulff-logo\.png"' app/layout.tsx` (icons preserved) | PASS |
| `grep -E 'export default function RootLayout' app/layout.tsx` (body intact) | PASS |
| `! grep -E "^'use client'" app/layout.tsx` | PASS |
| `npx tsc --noEmit --pretty` | exit 0 |
| `test ! -f public/sw.js && test ! -f public/service-worker.js` | PASS |
| `! grep '"next-pwa"' package.json` | PASS |
**Dev-server-only checks** (`curl http://localhost:3100/manifest.json`, `curl http://localhost:3100/ \| grep viewport-fit=cover`) were not run — this executor runs in a worktree without a dev server. The offline equivalents above are equivalent: the file is a static asset served verbatim by Next.js from `public/`, and `viewportFit: "cover"` is type-checked to render `viewport-fit=cover` per Next.js 16's documented metadata API.
## Requirements Satisfied
- **PWA-01:** `public/manifest.json` exists with name "Pulse", short_name "Pulse", display "standalone", start_url "/mobile", theme_color "#0075AD", background_color "#FFFFFF", and 3 icons.
- **PWA-02:** `app/layout.tsx` references the manifest via `metadata.manifest = "/manifest.json"` — Next.js 16 emits the `<link rel="manifest">` tag automatically.
- **PWA-03:** `app/layout.tsx` exports `viewport: Viewport` with `viewportFit: "cover"` — Next.js renders `viewport-fit=cover` in the `<meta name="viewport">` tag, unblocking safe-area painting in Phase 2.
## Deviations from Plan
None - plan executed exactly as written.
No bugs encountered, no missing critical functionality, no blocking issues, no architectural decisions needed.
## Threat Surface Scan
No new threat surface introduced beyond the plan's `<threat_model>`. The manifest is world-readable per W3C Web App Manifest spec and contains only public branding (no secrets, no user data, no endpoints). The viewport export is server-rendered with no user input flow. ASVS-L1 baseline preserved.
## Known Stubs
None. All values are real (brand colors sourced from `app/styles/brand.css`, icons reference real public assets, start_url matches the existing `/mobile` route).
## Self-Check: PASSED
- `[ -f public/manifest.json ]` → FOUND
- `[ -f app/layout.tsx ]` → FOUND
- `git log --oneline | grep 3e3df24` → FOUND (Task 1 commit)
- `git log --oneline | grep d196d22` → FOUND (Task 2 commit)