---
phase: 26-pwa-performance
plan: 00
type: execute
wave: 1
depends_on: []
files_modified:
  - ui/public/sw.js
  - ui/src/types/pwa.d.ts
  - ui/src/hooks/useOfflineQueue.test.ts
  - ui/src/hooks/useInstallPrompt.test.ts
  - ui/src/hooks/usePushNotifications.test.ts
  - ui/src/components/PullToRefresh.test.tsx
autonomous: true
requirements:
  - PWA-01
  - PWA-07
  - PERF-05
must_haves:
  truths:
    - "Service worker uses cache-first for static assets and cache-first for navigation"
    - "Old paperclip-v2 cache is deleted on SW activation"
    - "Service worker handles push events and notificationclick events"
    - "PWA TypeScript types exist for BeforeInstallPromptEvent"
    - "Wave 0 test stubs exist for all hooks and components planned in later waves"
  artifacts:
    - path: "ui/public/sw.js"
      provides: "Cache-first service worker with push handler"
      contains: "nexus-v1"
    - path: "ui/src/types/pwa.d.ts"
      provides: "BeforeInstallPromptEvent type declaration"
      contains: "BeforeInstallPromptEvent"
    - path: "ui/src/hooks/useOfflineQueue.test.ts"
      provides: "Test stub for offline queue hook"
      contains: "it.todo"
    - path: "ui/src/hooks/useInstallPrompt.test.ts"
      provides: "Test stub for install prompt hook"
      contains: "it.todo"
  key_links:
    - from: "ui/public/sw.js"
      to: "ui/src/main.tsx"
      via: "SW registration on load event (already wired)"
      pattern: "serviceWorker\\.register"
---

<objective>
Foundation for Phase 26: rewrite the service worker from network-first to cache-first, install dependencies (idb, web-push), create PWA TypeScript types, and scaffold Wave 0 test stubs for all hooks/components coming in later plans.

Purpose: Provides the upgraded SW that enables PERF-05 (cached load < 1s) and the test infrastructure for TDD in subsequent plans.
Output: Upgraded sw.js, pwa.d.ts type declarations, 4 test stub files, idb + web-push installed.
</objective>

<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/26-pwa-performance/26-RESEARCH.md
@.planning/phases/26-pwa-performance/26-UI-SPEC.md
@ui/public/sw.js
@ui/src/main.tsx
</context>

<tasks>

<task type="auto">
  <name>Task 1: Install dependencies, create PWA types, and rewrite service worker</name>
  <files>ui/public/sw.js, ui/src/types/pwa.d.ts</files>
  <read_first>
    - ui/public/sw.js
    - ui/src/main.tsx
    - ui/public/site.webmanifest
    - .planning/phases/26-pwa-performance/26-RESEARCH.md
  </read_first>
  <action>
1. Install dependencies:
   - `pnpm --filter @paperclipai/ui add idb`
   - `pnpm --filter @paperclipai/server add web-push`
   - `pnpm --filter @paperclipai/server add --save-dev @types/web-push`

2. Create `ui/src/types/pwa.d.ts` with:
   - `BeforeInstallPromptEvent` interface extending `Event` with `prompt(): Promise<void>` and `userChoice: Promise<{ outcome: "accepted" | "dismissed" }>` properties
   - Global `WindowEventMap` augmentation adding `beforeinstallprompt: BeforeInstallPromptEvent`

3. Rewrite `ui/public/sw.js` — replace the entire file with a cache-first strategy:
   - Cache name: `"nexus-v1"` (replaces `"paperclip-v2"`)
   - `install` event: call `self.skipWaiting()`, pre-cache `["/", "/index.html"]` into `nexus-v1`
   - `activate` event: delete ALL caches whose name is NOT `"nexus-v1"` (this busts the old `paperclip-v2` cache), then call `self.clients.claim()`
   - `fetch` event handler:
     a. If `url.pathname.startsWith("/api")` — return immediately (network-only, no interception)
     b. If `request.mode === "navigate"` — cache-first: try `caches.match("/")`, fall back to `fetch(request)`
     c. If URL matches static extension regex `/\.(js|css|woff2?|png|svg|ico|webmanifest)$/` — cache-first: try `caches.match(request)`, on miss fetch and clone into `nexus-v1` cache, return response
     d. All other requests: pass through (no interception)
   - `push` event handler: parse `event.data.json()` for `{ title, body, icon, data }`, call `self.registration.showNotification(title, { body, icon: icon || "/android-chrome-192x192.png", badge: "/favicon-32x32.png", data })`
   - `notificationclick` event handler: close notification, `clients.openWindow(event.notification.data?.url || "/")`

Do NOT modify `ui/src/main.tsx` — SW registration is already correct.
Do NOT modify `ui/public/site.webmanifest` — manifest is already complete (PWA-02, PWA-07 already satisfied).
  </action>
  <verify>
    <automated>grep -q "nexus-v1" ui/public/sw.js && grep -q "BeforeInstallPromptEvent" ui/src/types/pwa.d.ts && echo "PASS"</automated>
  </verify>
  <acceptance_criteria>
    - `grep "nexus-v1" ui/public/sw.js` returns the cache name
    - `grep "paperclip" ui/public/sw.js` returns nothing (old name fully removed)
    - `grep "cache-first" ui/public/sw.js` or the cache-first pattern (caches.match before fetch) is present for static assets
    - `grep "push" ui/public/sw.js` shows push event listener
    - `grep "notificationclick" ui/public/sw.js` shows notification click handler
    - `grep "BeforeInstallPromptEvent" ui/src/types/pwa.d.ts` returns the type declaration
    - `pnpm --filter @paperclipai/ui exec -- node -e "require('idb')"` succeeds (idb installed)
    - `pnpm --filter @paperclipai/server exec -- node -e "require('web-push')"` succeeds (web-push installed)
  </acceptance_criteria>
  <done>Service worker rewritten with cache-first strategy and nexus-v1 cache name. idb and web-push installed. PWA types declared.</done>
</task>

<task type="auto">
  <name>Task 2: Create Wave 0 test stubs for Phase 26 hooks and components</name>
  <files>ui/src/hooks/useOfflineQueue.test.ts, ui/src/hooks/useInstallPrompt.test.ts, ui/src/hooks/usePushNotifications.test.ts, ui/src/components/PullToRefresh.test.tsx</files>
  <read_first>
    - ui/src/components/SwipeToArchive.test.tsx
    - .planning/phases/26-pwa-performance/26-RESEARCH.md
  </read_first>
  <action>
Create 4 test stub files using `it.todo()` (not `it.skip()`) — consistent with Phase 21-25 convention.

1. `ui/src/hooks/useOfflineQueue.test.ts`:
   - `describe("useOfflineQueue")` with:
     - `it.todo("enqueues message when offline")`
     - `it.todo("flushes queue on online event")`
     - `it.todo("stops flushing on first failed POST")`
     - `it.todo("returns queued message count")`

2. `ui/src/hooks/useInstallPrompt.test.ts`:
   - `describe("useInstallPrompt")` with:
     - `it.todo("captures beforeinstallprompt event")`
     - `it.todo("returns canInstall=true when event captured and not standalone")`
     - `it.todo("returns canInstall=false when already installed (standalone)")`
     - `it.todo("calls prompt() on the deferred event when promptInstall is called")`

3. `ui/src/hooks/usePushNotifications.test.ts`:
   - `describe("usePushNotifications")` with:
     - `it.todo("subscribes when permission is granted")`
     - `it.todo("does not subscribe when permission is denied")`
     - `it.todo("sends subscription to server via POST /api/push/subscribe")`

4. `ui/src/components/PullToRefresh.test.tsx`:
   - Add `// @vitest-environment jsdom` pragma at top (mirrors SwipeToArchive.test.tsx pattern)
   - `describe("PullToRefresh")` with:
     - `it.todo("calls onRefresh after drag exceeds 64px threshold")`
     - `it.todo("does not trigger when scrollTop is not 0")`
     - `it.todo("resets pull distance on touch end below threshold")`

All test stubs should have minimal imports — no service mocks until implementation plans wire them up.

NOTE: No MobileNavBar.test.tsx is created — the existing MobileBottomNav in Layout.tsx already handles global mobile navigation. If tests are needed for MobileBottomNav, they would belong to a separate plan.
  </action>
  <verify>
    <automated>pnpm --filter @paperclipai/ui test --run useOfflineQueue useInstallPrompt usePushNotifications PullToRefresh 2>&1 | grep -E "todo|Tests" | head -10</automated>
  </verify>
  <acceptance_criteria>
    - All 4 test files exist at the specified paths
    - Each file uses `it.todo()` not `it.skip()`
    - `PullToRefresh.test.tsx` has `// @vitest-environment jsdom` at top
    - `pnpm --filter @paperclipai/ui test --run` passes (todos are not failures)
  </acceptance_criteria>
  <done>4 Wave 0 test stubs created covering PWA-01, PWA-03, PWA-06, PWA-08. All test files use it.todo() and pass vitest run.</done>
</task>

</tasks>

<verification>
- `grep "nexus-v1" ui/public/sw.js` confirms cache name
- `grep -c "paperclip" ui/public/sw.js` returns 0
- All 4 test stub files exist and use `it.todo()`
- `pnpm --filter @paperclipai/ui test --run` passes
- `idb` appears in `ui/package.json` dependencies
- `web-push` appears in `server/package.json` dependencies
</verification>

<success_criteria>
Service worker upgraded to cache-first with nexus-v1 cache. Dependencies installed. PWA types declared. All Wave 0 test stubs created and passing.
</success_criteria>

<output>
After completion, create `.planning/phases/26-pwa-performance/26-00-SUMMARY.md`
</output>
