The soft reset to 77073ba inadvertently staged deletions of all phase 2
and 3 artifacts. This commit restores them from their source commits so
subsequent task commits build on the complete prior-phase foundation:
- components/mobile/{BottomNav,HeaderBar,KpiCardMobile,MoreDrawer,NeedsAttentionStrip,WorkerStatusRow}
- app/mobile/layout.tsx, dashboard/page.tsx, analyzer/page.tsx
- app/api/mobile/dashboard/route.ts
- All .planning/** files from phases 01-04
- CLAUDE.md, app/layout.tsx, app/styles/brand.css, public/manifest.json
7.9 KiB
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: Viewportwith viewportFit: "cover" - app/layout.tsx exports
metadata.manifest = "/manifest.json"(Next.js emits 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
viewportexport (replaces deprecated metadata.viewport) - Next.js 16 metadata.manifest field (auto-emits ) key-files: created:
- public/manifest.json modified:
- app/layout.tsx decisions:
- Next.js 16 separate
- 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 — 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 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:
-
Import upgraded:
import type { Metadata, Viewport } from "next"; -
metadata.manifest = "/manifest.json"added alongside the existingiconsfield — Next.js 16 emits<link rel="manifest" href="/manifest.json" />in the rendered HTML head automatically (satisfies PWA-02 spec wording). -
New
viewportexport: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 rendersviewport-fit=coverin the<meta name="viewport">tag so future phases can use safe-area-inset utilities to paint behind the home indicator.width,initialScale, andthemeColorare 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.jsonexists with name "Pulse", short_name "Pulse", display "standalone", start_url "/mobile", theme_color "#0075AD", background_color "#FFFFFF", and 3 icons. - PWA-02:
app/layout.tsxreferences the manifest viametadata.manifest = "/manifest.json"— Next.js 16 emits the<link rel="manifest">tag automatically. - PWA-03:
app/layout.tsxexportsviewport: ViewportwithviewportFit: "cover"— Next.js rendersviewport-fit=coverin 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 ]→ FOUNDgit log --oneline | grep 3e3df24→ FOUND (Task 1 commit)git log --oneline | grep d196d22→ FOUND (Task 2 commit)