wulf-pulse/.planning/phases/01-pwa-scaffolding/01-01-SUMMARY.md
lorentz 9658640c04 fix(04-01): restore phase 2/3 work lost by worktree soft-reset
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
2026-05-03 18:01:14 -04:00

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: Viewport with 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 viewport export (replaces deprecated metadata.viewport)
    • Next.js 16 metadata.manifest field (auto-emits ) 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 — 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:

  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:

    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)