From 7b644e475f68fb0e448df7d045cb63510d8ff70b Mon Sep 17 00:00:00 2001 From: lorentz Date: Thu, 14 May 2026 07:30:51 -0400 Subject: [PATCH] docs(planning): sync STATE/ROADMAP + capture untracked phase plans before master merge - STATE/ROADMAP/config updated to reflect Phase 09.1 execution - 09-01 plan refreshed (gap-closure detail) - 09-02..09-05 plans updated during execution - Add untracked 09-06 plan + 01-01/01-02 PWA scaffolding plans (orphaned from earlier sessions) Co-Authored-By: Claude Opus 4.7 (1M context) --- .planning/ROADMAP.md | 2 +- .planning/STATE.md | 20 +- .planning/config.json | 3 +- .../phases/01-pwa-scaffolding/01-01-PLAN.md | 374 +++++++++++ .../phases/01-pwa-scaffolding/01-02-PLAN.md | 286 +++++++++ .../09-01-PLAN.md | 57 +- .../09-02-PLAN.md | 95 ++- .../09-03-PLAN.md | 233 ++++++- .../09-04-PLAN.md | 395 ++++-------- .../09-05-PLAN.md | 589 ++++++++++-------- .../09-06-PLAN.md | 523 ++++++++++++++++ 11 files changed, 1951 insertions(+), 626 deletions(-) create mode 100644 .planning/phases/01-pwa-scaffolding/01-01-PLAN.md create mode 100644 .planning/phases/01-pwa-scaffolding/01-02-PLAN.md create mode 100644 .planning/phases/09-user-profile-preferences-new/09-06-PLAN.md diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 7b98aa2..ab9f0d4 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -210,7 +210,7 @@ Decimal phases appear between their surrounding integers in numeric order. 4. `/mobile/profile` QR code and subscribe link target `process.env.NEXT_PUBLIC_NTFY_BASE_URL || 'https://ntfy.wulfconsulting.cloud'`; help line under custom-topic Input reads "Topic must start with `pulse-me-`" 5. `npx tsc --noEmit --pretty` and `npx vitest run lib/services/pipeline-steps/notify.test.ts` both pass (mute semantics intact) **Plans**: 1 plan -- [ ] 09.1-01-PLAN.md — Personal-channels regex/prefix/bearer + propagate to notify/approval/digest send paths + ProfileChannelsSection QR & copy +- [x] 09.1-01-PLAN.md — Personal-channels regex/prefix/bearer + propagate to notify/approval/digest send paths + ProfileChannelsSection QR & copy **UI hint**: no (backend-heavy; one component edit for QR/link target) ## Progress diff --git a/.planning/STATE.md b/.planning/STATE.md index 58b6161..1c34678 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -4,14 +4,14 @@ milestone: v1.0 milestone_name: milestone status: executing stopped_at: Phase 9 UI-SPEC approved -last_updated: "2026-05-11T01:51:19.856Z" -last_activity: 2026-05-11 +last_updated: "2026-05-11T10:38:01.535Z" +last_activity: 2026-05-11 -- Phase 09.1 execution started progress: - total_phases: 10 + total_phases: 11 completed_phases: 10 - total_plans: 30 + total_plans: 31 completed_plans: 30 - percent: 100 + percent: 97 --- # Project State @@ -21,14 +21,14 @@ progress: See: .planning/PROJECT.md (updated 2026-05-03) **Core value:** A manager can open Pulse on their phone and, in under 30 seconds, see the state of the business and triage tickets — without ever needing to switch to desktop for read-only awareness. -**Current focus:** Phase 09 — user-profile-preferences-new +**Current focus:** Phase 09.1 — ntfy-backend-fix ## Current Position -Phase: 09 -Plan: Not started -Status: Executing Phase 09 -Last activity: 2026-05-11 +Phase: 09.1 (ntfy-backend-fix) — EXECUTING +Plan: 1 of 1 +Status: Executing Phase 09.1 +Last activity: 2026-05-11 -- Phase 09.1 execution started Progress: [░░░░░░░░░░] 0% diff --git a/.planning/config.json b/.planning/config.json index 040e7b2..eb3a86b 100644 --- a/.planning/config.json +++ b/.planning/config.json @@ -25,7 +25,8 @@ "text_mode": false, "research_before_questions": false, "discuss_mode": "discuss", - "skip_discuss": false + "skip_discuss": false, + "_auto_chain_active": false }, "hooks": { "context_warnings": true diff --git a/.planning/phases/01-pwa-scaffolding/01-01-PLAN.md b/.planning/phases/01-pwa-scaffolding/01-01-PLAN.md new file mode 100644 index 0000000..6a48d35 --- /dev/null +++ b/.planning/phases/01-pwa-scaffolding/01-01-PLAN.md @@ -0,0 +1,374 @@ +--- +phase: 01-pwa-scaffolding +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - public/manifest.json + - app/layout.tsx +autonomous: true +requirements: + - PWA-01 + - PWA-02 + - PWA-03 + +must_haves: + truths: + - "Visiting /manifest.json returns valid JSON with name 'Pulse', short_name 'Pulse', display 'standalone', start_url '/mobile'" + - "The manifest theme_color matches the Wulf brand blue (#0075AD) and background_color matches the light shell" + - "app/layout.tsx references the manifest so Chrome/Safari pick it up automatically (via metadata.manifest or )" + - "app/layout.tsx exports a viewport object whose viewportFit is 'cover' so the rendered contains 'viewport-fit=cover'" + - "Installing Pulse to a phone home screen launches a chromeless app that opens to /mobile" + artifacts: + - path: "public/manifest.json" + provides: "Web App Manifest — name, short_name, display, start_url, theme/background, icons" + contains: '"display": "standalone"' + - path: "app/layout.tsx" + provides: "Root layout exporting metadata (with manifest) and viewport (with viewportFit:'cover')" + contains: "viewportFit" + key_links: + - from: "app/layout.tsx" + to: "public/manifest.json" + via: "metadata.manifest or " + pattern: "manifest" + - from: "public/manifest.json" + to: "/mobile" + via: "start_url field" + pattern: '"start_url"\s*:\s*"/mobile"' + - from: "app/layout.tsx (viewport export)" + to: "rendered " + via: "Next.js viewport export → viewport-fit=cover in DOM" + pattern: "viewportFit" +--- + + +Add the PWA install surface: a valid Web App Manifest at `/manifest.json`, a manifest reference from the root layout, and a viewport export with `viewportFit: 'cover'` so future shell phases can paint behind the device home indicator. + +Purpose: PWA-01, PWA-02, PWA-03 — make Pulse installable to a phone home screen and have the install land on `/mobile` in standalone (chromeless) mode. No service worker, no offline. + +Output: `public/manifest.json` (new file) and an updated `app/layout.tsx` that adds the manifest reference and a Next 16 viewport export. Verifiable by `curl http://localhost:3100/manifest.json` and grep on `app/layout.tsx`. + + + +@$HOME/.claude/get-shit-done/workflows/execute-plan.md +@$HOME/.claude/get-shit-done/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/ROADMAP.md +@.planning/STATE.md +@.planning/REQUIREMENTS.md +@docs/superpowers/specs/2026-05-03-mobile-shell-design.md +@CLAUDE.md +@app/layout.tsx +@app/globals.css +@app/styles/brand.css + + + + +**Existing `app/layout.tsx` shape (what is currently there):** +- Imports `Metadata` from `next` (already imported). +- Exports `const metadata: Metadata = { title, description, icons: { icon: [...], shortcut: '/favicon.png', apple: '/wulff-logo.png' } }`. +- Does NOT currently export `viewport`. Next 16 expects a separate `viewport` export of type `Viewport` from `next`. +- The metadata object already has an `icons` field. Do NOT remove it; the PWA `manifest` field is added alongside `icons`. + +**Brand colors (already defined in `app/styles/brand.css`):** +- Wulf primary blue: `#0075AD` (oklch `0.540 0.136 233.3`). This is the `theme_color`. +- Light shell background: white (`#FFFFFF`). This is the `background_color` (the manifest only allows one; the light shell is the standard splash background). + +**Existing icon assets (in `/public`):** +- `/public/wulff-logo.png` — square PNG, used today as `metadata.icons.apple` (apple-touch-icon). +- `/public/favicon.png` — square PNG. +- `/public/branding/wulf-mark.png` — Wulf "W" mark, square PNG. +- `/public/branding/wulf-wordmark.png` — Wulf "Pulse" wordmark. +None of these have explicit pixel sizes verified, but they're used today and PWA install tools accept them with `"sizes": "any"`. + +**Next.js 16 metadata API for manifest:** +- The recommended way to reference a manifest is `metadata.manifest = '/manifest.json'` in the metadata export. Next emits `` automatically. This satisfies the spec wording (``) without hand-rolling the link tag. +- Alternative: hand-roll `` inside ``. Either approach is acceptable per the spec; prefer `metadata.manifest` because the file already uses the metadata API. + +**Next.js 16 viewport API:** +- Import: `import type { Viewport } from 'next'`. +- Export: `export const viewport: Viewport = { ... }` (separate from `metadata`; Next 16 deprecated `metadata.viewport`). +- The `viewportFit` field is camelCase in TS; Next emits `viewport-fit=cover` in the rendered `` tag. +- Reasonable default fields: `width: 'device-width'`, `initialScale: 1`, `viewportFit: 'cover'`. Do NOT add `maximumScale` or `userScalable: false` (accessibility). + +**Theme color / dark mode caveat:** +- The manifest only allows one `theme_color`. Use the Wulf blue `#0075AD` so the system UI tint matches the brand in both light and dark modes. +- Optionally also add a viewport `themeColor` array with `media: '(prefers-color-scheme: dark)'` variants in the viewport export. This is a Next.js helper that emits `` per-scheme. NOT required for PWA-01..03; only add if it falls out naturally. + +**Verification commands the executor will use:** +- `curl -sf http://localhost:3100/manifest.json | jq .` (dev server must be running) +- `grep -E "viewportFit|viewport-fit" app/layout.tsx` +- `grep -E 'manifest:|rel="manifest"' app/layout.tsx` +- `npx tsc --noEmit --pretty` (must pass) + + + + + + + Task 1: Create public/manifest.json + public/manifest.json + + - docs/superpowers/specs/2026-05-03-mobile-shell-design.md (§4 — exact field requirements) + - .planning/REQUIREMENTS.md (PWA-01) + - app/styles/brand.css (line 28: `--wulf-blue` is `#0075AD` — this is theme_color) + - app/globals.css (lines 47-48: light theme `--background` is white; lines 82-83: dark theme background) + - public/ directory listing — confirm `wulff-logo.png`, `favicon.png`, `branding/wulf-mark.png` exist + + +Create `public/manifest.json` (new file) with exactly this JSON shape. Hand-write the file; do not use a generator. + +```json +{ + "name": "Pulse", + "short_name": "Pulse", + "description": "Wulf Consulting operations console — tickets, RMM, backups, and analytics on the go.", + "start_url": "/mobile", + "scope": "/", + "display": "standalone", + "orientation": "portrait", + "theme_color": "#0075AD", + "background_color": "#FFFFFF", + "icons": [ + { + "src": "/wulff-logo.png", + "sizes": "any", + "type": "image/png", + "purpose": "any" + }, + { + "src": "/branding/wulf-mark.png", + "sizes": "any", + "type": "image/png", + "purpose": "any" + }, + { + "src": "/favicon.png", + "sizes": "any", + "type": "image/png", + "purpose": "any" + } + ] +} +``` + +Notes on the choices (so a reviewer doesn't have to ask): +- `name` and `short_name` both "Pulse" — matches spec §4 verbatim. +- `start_url: "/mobile"` — spec §4 verbatim. The phone install lands on the mobile shell, not the desktop dashboard. +- `scope: "/"` — allow the standalone window to navigate anywhere in the app without falling out to the browser. (Spec doesn't specify; root scope is the safe default for an installed PSA console.) +- `display: "standalone"` — spec §4 verbatim. Chromeless app surface. +- `orientation: "portrait"` — phone-first per the spec's overall framing (§1, §2). Tablet landscape is explicit out-of-scope (§7). +- `theme_color: "#0075AD"` — Wulf brand blue from `app/styles/brand.css` line 28 (`--wulf-blue`). Matches the `--primary` token in both light and dark modes (oklch values resolve to this brand blue, slightly lifted for dark). +- `background_color: "#FFFFFF"` — light shell background. Manifest only allows one value; the iOS/Android splash uses this. White matches Pulse's default theme on light devices and is acceptable on dark devices (brief flash, not a regression). +- `icons` — three entries reusing existing assets in `/public`. Using `"sizes": "any"` because the assets are not explicitly sized — install tools accept this for PNGs and pick the largest. Do NOT generate new icon PNGs in this task; reuse what's there. (A future polish phase can add density-specific 192/512 icons if install warns.) + +Do NOT: +- Add a `serviceworker` field (no SW in v1, spec §4 explicit). +- Add `display_override` or `prefer_related_applications` (not needed; not in spec). +- Add `categories` or `lang` (cosmetic; not in spec scope). +- Reference `next-pwa` or any plugin (forbidden by spec §4 and CLAUDE.md). +- Edit any existing migration, lib/, or component file. + +The file must be served directly by Next.js as a static asset — placing it at `public/manifest.json` makes it available at `http://localhost:3100/manifest.json`. + + + test -f public/manifest.json && jq -e '.name == "Pulse" and .short_name == "Pulse" and .display == "standalone" and .start_url == "/mobile" and .theme_color == "#0075AD" and .background_color == "#FFFFFF" and (.icons | length) >= 1' public/manifest.json + + + - File `public/manifest.json` exists. + - `jq -r .name public/manifest.json` outputs `Pulse`. + - `jq -r .short_name public/manifest.json` outputs `Pulse`. + - `jq -r .display public/manifest.json` outputs `standalone`. + - `jq -r .start_url public/manifest.json` outputs `/mobile`. + - `jq -r .theme_color public/manifest.json` outputs `#0075AD`. + - `jq -r .background_color public/manifest.json` outputs `#FFFFFF`. + - `jq -e '.icons | length >= 1' public/manifest.json` exits 0. + - `jq -e '.icons[0].src' public/manifest.json` outputs a path beginning with `/` (e.g., `/wulff-logo.png`). + - File is valid JSON: `jq empty public/manifest.json` exits 0. + - No `serviceworker` field present: `jq -e '.serviceworker == null' public/manifest.json` exits 0. + - When dev server is running on port 3100: `curl -sf http://localhost:3100/manifest.json` exits 0 and the body equals the file contents. + + + `public/manifest.json` exists, is valid JSON, contains the spec-mandated fields with the values above, references at least one icon from `/public`, and is reachable at `http://localhost:3100/manifest.json` when the dev server is running. + + + + + Task 2: Add manifest reference and viewport export to app/layout.tsx + app/layout.tsx + + - app/layout.tsx (current file — already exports `metadata: Metadata`, no `viewport` export yet) + - docs/superpowers/specs/2026-05-03-mobile-shell-design.md (§4 — viewport-fit=cover wording) + - .planning/REQUIREMENTS.md (PWA-02, PWA-03) + - public/manifest.json (created in Task 1 — must exist before this task ships) + + +Edit `app/layout.tsx` (do NOT create a new file). Two changes, both at the top of the file alongside the existing `metadata` export. The body of `RootLayout` is unchanged. + +**Change 1 — add `manifest: '/manifest.json'` to the existing `metadata` object.** + +The current export looks like: + +```ts +export const metadata: Metadata = { + title: "Pulse · Operations console", + description: "Wulf Consulting operations console — tickets, RMM, IT Glue, backups, and analytics in one place.", + icons: { + icon: [ + { url: "/favicon.png", sizes: "any" }, + { url: "/wulff-logo.png", sizes: "32x32", type: "image/png" }, + ], + shortcut: "/favicon.png", + apple: "/wulff-logo.png", + }, +}; +``` + +Add a `manifest` field alongside `icons`. The result should be: + +```ts +export const metadata: Metadata = { + title: "Pulse · Operations console", + description: "Wulf Consulting operations console — tickets, RMM, IT Glue, backups, and analytics in one place.", + manifest: "/manifest.json", + icons: { + icon: [ + { url: "/favicon.png", sizes: "any" }, + { url: "/wulff-logo.png", sizes: "32x32", type: "image/png" }, + ], + shortcut: "/favicon.png", + apple: "/wulff-logo.png", + }, +}; +``` + +Next.js 16 emits `` automatically from this field — this satisfies the spec wording (`` from §4) without hand-rolling the tag. + +**Change 2 — add a `Viewport` import and a separate `viewport` export.** + +Update the `next` type import on line 1. The current import is: + +```ts +import type { Metadata } from "next"; +``` + +Change it to: + +```ts +import type { Metadata, Viewport } from "next"; +``` + +Then, immediately after the `metadata` export (and before `export default function RootLayout(...)`), add: + +```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" }, + ], +}; +``` + +Notes on the choices: +- `viewportFit: 'cover'` — the only field PWA-03 strictly requires. Emits `viewport-fit=cover` in the rendered `` tag. With this set, Phase 2's safe-area-inset utilities can paint behind the home indicator. +- `width: 'device-width'` and `initialScale: 1` — standard mobile viewport defaults; they were absent before and Next 16 would warn without them. Adding them here removes the warning and makes the viewport explicit. +- `themeColor` — paired light/dark values for the system browser chrome (status bar tint). Light = white (matches manifest `background_color`); dark = `#0A0A0A` (close to the existing `--background` oklch `0.145 0 0` in `app/globals.css` line 83). This is OPTIONAL for PWA-03 (the manifest's `theme_color` already covers the install chrome), but it's a one-line improvement that ships better dark-mode rendering and costs nothing. Keep it; remove if it ever conflicts with a future per-page override. +- Do NOT add `maximumScale`, `userScalable: false`, or `minimumScale` — accessibility regression. + +Do NOT: +- Touch the `RootLayout` function body. +- Touch the `ThemeProvider`, `AppNavigation`, `CommandPalette`, `TaglineFooter`, `Toaster`, or `AuthProvider` imports. +- Add any `` JSX (no hand-rolled `` tag — let Next emit it from `metadata.manifest`). +- Touch the `IBM_Plex_Sans` / `IBM_Plex_Mono` font setup. +- Add `'use client'` — root layout is a server component. + + + grep -q 'manifest: "/manifest.json"' app/layout.tsx && grep -q 'viewportFit: "cover"' app/layout.tsx && grep -q 'import type { Metadata, Viewport } from "next"' app/layout.tsx && grep -q 'export const viewport: Viewport' app/layout.tsx && npx tsc --noEmit --pretty 2>&1 | tee /tmp/tsc-out && ! grep -E "app/layout\\.tsx.*error" /tmp/tsc-out + + + - `grep -E '^import type \{ Metadata, Viewport \} from "next"' app/layout.tsx` matches one line (or `Metadata` and `Viewport` both appear in a single named-import line from `next`). + - `grep -E 'manifest:\s*"/manifest\.json"' app/layout.tsx` matches one line inside the `metadata` object. + - `grep -E '^export const viewport: Viewport = \{' app/layout.tsx` matches exactly one line. + - `grep -E 'viewportFit:\s*"cover"' app/layout.tsx` matches one line inside the `viewport` export. + - `grep -E 'width:\s*"device-width"' app/layout.tsx` matches one line. + - `grep -E 'initialScale:\s*1' app/layout.tsx` matches one line. + - The `metadata.icons` object is unchanged (still contains `apple: "/wulff-logo.png"`): `grep -E 'apple:\s*"/wulff-logo\.png"' app/layout.tsx` matches. + - The `RootLayout` default export is unchanged: `grep -E 'export default function RootLayout' app/layout.tsx` matches. + - No `'use client'` pragma added: `! grep -E "^'use client'" app/layout.tsx`. + - Type check passes: `npx tsc --noEmit --pretty` exits 0 (or, if other files have unrelated pre-existing errors, no error rows mention `app/layout.tsx`). + - When dev server is running: viewing http://localhost:3100/ source contains `viewport-fit=cover` (e.g. `curl -s http://localhost:3100/ | grep -E 'viewport-fit=cover'` exits 0). Optional manual check; not strictly required for the automated gate. + + + `app/layout.tsx` exports both `metadata` (now with `manifest: "/manifest.json"`) and `viewport` (with `viewportFit: "cover"`, `width: "device-width"`, `initialScale: 1`, and themeColor light/dark pair). Type check passes. The `RootLayout` body is unchanged. PWA-02 (manifest reference) and PWA-03 (viewport-fit=cover) are satisfied. + + + + + + +## Trust Boundaries + +| Boundary | Description | +|----------|-------------| +| Browser ↔ static asset (/manifest.json) | Public client read of a manifest. No auth, no input. | +| Browser ↔ rendered HTML head | Public client read of `` and ``. | + +## STRIDE Threat Register + +| Threat ID | Category | Component | Disposition | Mitigation Plan | +|-----------|----------|-----------|-------------|-----------------| +| T-01-01 | Information Disclosure | public/manifest.json | accept | Manifest is intended to be world-readable per W3C Web App Manifest spec. Contains only public app branding (name, theme color, icon paths) — no secrets, no user data, no endpoints. | +| T-01-02 | Tampering | app/layout.tsx viewport export | accept | Server-rendered; no user input flows into the viewport meta. No injection vector. | +| T-01-03 | Denial of Service | manifest fetch | accept | Static file served by Next.js; same risk profile as `/favicon.png`. No new attack surface. | + +**Summary:** No new threat surface introduced. `manifest.json` is public per W3C spec; viewport meta is a public client hint; no auth, data, or endpoints are introduced. ASVS-L1 baseline preserved. + + + +With dev server running (`npm run dev` → port 3100), all of the following must pass: + +```bash +# Manifest is reachable and well-formed +curl -sf http://localhost:3100/manifest.json | jq -e '.name == "Pulse" and .display == "standalone" and .start_url == "/mobile"' + +# Manifest is referenced from root layout (Next emits the link tag automatically) +curl -s http://localhost:3100/ | grep -E 'rel="manifest"' + +# Viewport meta includes viewport-fit=cover +curl -s http://localhost:3100/ | grep -E 'viewport-fit=cover' + +# Type check passes +npx tsc --noEmit --pretty + +# No service worker file shipped (negative check — must be absent) +test ! -f public/sw.js && test ! -f public/service-worker.js + +# next-pwa is not in dependencies +! grep -E '"next-pwa"' package.json +``` + + + +- `public/manifest.json` exists with name "Pulse", short_name "Pulse", display "standalone", start_url "/mobile", theme_color "#0075AD", background_color "#FFFFFF", and at least one icon (PWA-01). +- `app/layout.tsx` references the manifest via `metadata.manifest = "/manifest.json"`, which makes Next.js emit `` in the rendered HTML head (PWA-02). +- `app/layout.tsx` exports `viewport: Viewport` with `viewportFit: "cover"` so the rendered `` tag contains `viewport-fit=cover` (PWA-03). +- `npx tsc --noEmit --pretty` passes. +- No service worker file or `next-pwa` dependency introduced. + + + +After completion, create `.planning/phases/01-pwa-scaffolding/01-01-SUMMARY.md` documenting: +- Files created/modified (paths and one-line descriptions) +- The exact `theme_color` and `background_color` values chosen (and why — Wulf brand blue + light shell background) +- The viewport export shape (so Phase 2 knows it can rely on `viewport-fit=cover` being present) +- Verification results (manifest curl, viewport grep, tsc result) +- Any deviations from the plan and rationale + diff --git a/.planning/phases/01-pwa-scaffolding/01-02-PLAN.md b/.planning/phases/01-pwa-scaffolding/01-02-PLAN.md new file mode 100644 index 0000000..7c2dab8 --- /dev/null +++ b/.planning/phases/01-pwa-scaffolding/01-02-PLAN.md @@ -0,0 +1,286 @@ +--- +phase: 01-pwa-scaffolding +plan: 02 +type: execute +wave: 1 +depends_on: [] +gap_closure: true +files_modified: + - app/styles/brand.css +autonomous: true +requirements: + - PWA-04 + +must_haves: + truths: + - "A shared @utility named pt-safe is defined in app/styles/brand.css that applies padding-top: env(safe-area-inset-top)" + - "A shared @utility named pb-safe is defined in app/styles/brand.css that applies padding-bottom: env(safe-area-inset-bottom)" + - "Phase 2's sticky header can opt into safe-area-inset-top padding by adding the pt-safe class" + - "Phase 2's fixed bottom nav can opt into safe-area-inset-bottom padding by adding the pb-safe class" + - "The Tailwind 4 build accepts the new @utility blocks (no CSS syntax errors, npm run build succeeds)" + artifacts: + - path: "app/styles/brand.css" + provides: "Two new @utility blocks (pt-safe, pb-safe) sitting alongside the existing num/metric/surface/rule/chrome/tagline utilities" + contains: "@utility pt-safe" + key_links: + - from: "app/styles/brand.css (@utility pt-safe)" + to: "rendered CSS class .pt-safe" + via: "Tailwind 4 @utility block — Tailwind compiles @utility name { ... } into a class .name { ... }" + pattern: "@utility pt-safe" + - from: "app/styles/brand.css (@utility pb-safe)" + to: "rendered CSS class .pb-safe" + via: "Tailwind 4 @utility block" + pattern: "@utility pb-safe" + - from: "app/globals.css" + to: "app/styles/brand.css" + via: "@import './styles/brand.css' on line 125 (already wired — no change required)" + pattern: '@import "./styles/brand.css"' +--- + + +Close the PWA-04 gap from Phase 01 verification by adding shared safe-area `@utility` blocks to `app/styles/brand.css`. Phase 2's sticky header and fixed bottom nav need to opt into `env(safe-area-inset-top)` / `env(safe-area-inset-bottom)` padding so content paints correctly under the iOS home indicator and Android gesture bar when `viewport-fit=cover` is in effect (already shipped by 01-01). + +Purpose: PWA-04 — make a safe-area utility available so any sticky top/bottom bar can opt in. ROADMAP Phase 1 SC #3 requires the utility to be **available in Phase 1**; Phase 2's contract (SHELL-05, SHELL-06) only mandates **consumption**. This plan restores the broken phase boundary identified by `01-VERIFICATION.md`. + +Output: `app/styles/brand.css` updated with two new `@utility` blocks (`pt-safe`, `pb-safe`) appended to the existing utility section. No other files touched. Verifiable by `grep -E '@utility (pt-safe|pb-safe)' app/styles/brand.css` and `npm run build`. + +Why `app/styles/brand.css` (not `app/globals.css`): +- All named project utilities (`num`, `num-lg`, `num-xl`, `metric-label`, `surface-brand`, `surface-brand-ink`, `rule-brand`, `text-chrome`, `border-chrome`, `tagline`, `has-mark-watermark`) already live there. +- `brand.css` is already imported into `globals.css` (line 125) — no extra wiring needed. +- Keeps utilities co-located so Phase 2 has a single file to scan when looking for project helpers. +- `globals.css` is reserved for Tailwind imports, `@theme inline` token mapping, and `:root` / `.dark` variable definitions — adding utility classes there would muddy that separation. + +Note on traceability: this plan claims `PWA-04` in its `requirements` frontmatter, restoring the orphaned-requirement state flagged by `01-VERIFICATION.md`. The executor's SUMMARY (`01-02-SUMMARY.md`) should explicitly call out that PWA-04 is now satisfied, closing the requirements traceability table. + + + +@$HOME/.claude/get-shit-done/workflows/execute-plan.md +@$HOME/.claude/get-shit-done/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/ROADMAP.md +@.planning/REQUIREMENTS.md +@.planning/phases/01-pwa-scaffolding/01-VERIFICATION.md +@.planning/phases/01-pwa-scaffolding/01-01-SUMMARY.md +@docs/superpowers/specs/2026-05-03-mobile-shell-design.md +@CLAUDE.md +@app/styles/brand.css +@app/globals.css + + + + +**Tailwind 4 `@utility` syntax (already in use in this project):** +- This project is Tailwind 4 with NO `tailwind.config.*` file. Custom utilities are declared inline in CSS using the `@utility` at-rule. +- Pattern: `@utility name { /* CSS declarations */ }` — Tailwind compiles this to a class `.name { ... }` that participates in the variant system (`hover:name`, `md:name`, etc.). +- See existing examples in `app/styles/brand.css` lines 70-140 (e.g., `@utility num { ... }`, `@utility metric-label { ... }`, `@utility surface-brand { ... }`). +- Each `@utility` block holds plain CSS property declarations. No `@apply` is required for simple `padding-*` cases. + +**`env()` CSS environment variables for safe areas:** +- `env(safe-area-inset-top)` — top safe-area inset (e.g., iPhone notch / Dynamic Island area). +- `env(safe-area-inset-bottom)` — bottom safe-area inset (e.g., iPhone home indicator area). +- Browser-side CSS feature; no JavaScript involvement. Falls back to `0` on browsers/devices without safe-area insets. +- Requires `` to take non-zero values. **This is already shipped by 01-01** (`viewportFit: "cover"` in `app/layout.tsx`). + +**Existing `app/styles/brand.css` structure (line numbers from current file):** +- Lines 1-21: file header / brand documentation comment. +- Lines 23-52: `:root` overrides (`--wulf-blue`, etc.) for the light theme. +- Lines 54-62: `.dark` overrides. +- Lines 64-68: `/* === Utility classes === */` section header comment. +- Lines 70-140: existing `@utility` blocks — `num`, `num-lg`, `num-xl`, `metric-label`, `surface-brand`, `surface-brand-ink`, `rule-brand`, `text-chrome`, `border-chrome`, `tagline`. +- Lines 142-147: `/* === Wolf-mark watermark === */` section header comment. +- Lines 149-152: `@utility has-mark-watermark`. +- Lines 154-170: `.mark-watermark` plain rule + `.dark .mark-watermark` override. +- **Insertion point for new utilities:** after the `tagline` utility (line 140) and **before** the watermark section header (line 142). This keeps utilities grouped before the watermark block, which has its own thematic header. + +**`app/globals.css` import wiring (already in place — DO NOT change):** +- Line 125: `@import "./styles/brand.css";` — pulls `brand.css` into the global stylesheet at the end. Anything added to `brand.css` is automatically available app-wide. No additional wiring needed. + +**Spec wording (`docs/superpowers/specs/2026-05-03-mobile-shell-design.md` §5/§6):** +- The mobile shell's sticky top header must respect `env(safe-area-inset-top)`. +- The fixed bottom nav must respect `env(safe-area-inset-bottom)` (often combined with the bottom-nav height). +- The spec accepts either a named utility or Tailwind 4 arbitrary values (`pt-[env(safe-area-inset-top)]`). + +**Why ship a named utility (not arbitrary values):** +- Phase 2 will use these classes in 2+ places (header, bottom nav, drawer footer, possibly modals). A named utility is one source of truth — if the iOS rules ever change (e.g., add `max(env(safe-area-inset-top), 0.5rem)`), it's a one-line edit instead of a multi-file find-and-replace. +- `pt-safe` / `pb-safe` reads more clearly in JSX class lists than `pt-[env(safe-area-inset-top)]`. +- ROADMAP Phase 1 SC #3 explicitly mentions "shared utility class" as one acceptable form — picking that form removes ambiguity for Phase 2. + +**Verification commands the executor will use:** +- `grep -E '@utility pt-safe' app/styles/brand.css` +- `grep -E '@utility pb-safe' app/styles/brand.css` +- `grep -E 'env\(safe-area-inset-top\)' app/styles/brand.css` +- `grep -E 'env\(safe-area-inset-bottom\)' app/styles/brand.css` +- `npm run build` (CSS @utility blocks must parse — broken syntax fails the Tailwind compile step in Next.js) +- `npx tsc --noEmit --pretty` (sanity check; CSS doesn't affect TS but pre-existing baseline must hold) + + + + + + + Task 1: Append pt-safe and pb-safe @utility blocks to app/styles/brand.css + app/styles/brand.css + + - app/styles/brand.css (the entire file — confirm line numbers above match current state; the exact insertion point is between the existing `tagline` utility and the watermark section header) + - app/globals.css lines 1-5 and 125 (confirm `brand.css` is still imported; no change needed) + - .planning/phases/01-pwa-scaffolding/01-VERIFICATION.md (the gap source — frontmatter `gaps[0].missing`) + - .planning/REQUIREMENTS.md line 16 (PWA-04 wording) + + +Edit `app/styles/brand.css`. Append two new `@utility` blocks **after** the existing `@utility tagline { ... }` block (which ends around line 140) and **before** the `/* === Wolf-mark watermark === */` section header comment (around line 142). Do NOT touch any other part of the file. + +Insert exactly this block (including the leading section comment and the two `@utility` definitions): + +```css +/* === Safe-area insets ================================================= + * + * Opt-in padding helpers for sticky top / fixed bottom bars on devices + * with notches, dynamic islands, or gesture home indicators. Pair with + * the viewport-fit=cover viewport meta (set in app/layout.tsx) — without + * that, env(safe-area-inset-*) resolves to 0 and these utilities are + * no-ops, which is the desired fallback on non-PWA / non-mobile contexts. + * + * Usage: + *
// header clears notch + *