nexus/.planning/milestones/v1.3-phases/26-pwa-performance/26-VERIFICATION.md
Nexus Dev ffc7b130e4 chore: archive v1.3 phase directories to milestones/
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 03:55:48 +00:00

22 KiB

phase verified status score re_verification gaps human_verification
26-pwa-performance 2026-04-01T00:00:00Z gaps_found 8/10 must-haves verified false
truth status reason artifacts missing
Vite build produces separate vendor chunks for heavy libraries partial vendor-react chunk is 1 byte (newline only) — React and react-dom are NOT extracted from the main entry bundle. The manualChunks config is present in vite.config.ts but @vitejs/plugin-react injects React via automatic JSX runtime in a way that bypasses Rollup's manualChunks splitting. vendor-router, vendor-query, and vendor-markdown ARE correctly extracted. The main entry bundle is 2.1 MB.
path issue
ui/dist/assets/vendor-react-l0sNRNKZ.js 1 byte (newline only) — react and react-dom are bundled into the 2.1 MB index-QG5IJOTw.js main entry instead
path issue
ui/vite.config.ts manualChunks config correct, but Vite/React plugin injects React outside Rollup splitting path
Use modulePreload or dynamic import shim approach for react/react-dom splitting, or accept that React is in the main bundle and verify the 2.1 MB chunk still meets < 2s broadband load target
truth status reason artifacts missing
PWA-02 and PWA-08 requirement tracking is consistent with codebase state failed REQUIREMENTS.md shows PWA-02 (Web App Manifest) and PWA-08 (Add to Home Screen prompt) as unchecked/Pending even though both are fully implemented. The webmanifest is complete and linked in index.html. InstallPromptBanner is wired into ChatPanel and fully functional. The tracking file was not updated after plan completion.
path issue
.planning/REQUIREMENTS.md PWA-02 and PWA-08 checkboxes are unchecked ([ ]) and status column shows 'Pending' — both are actually implemented
Update REQUIREMENTS.md: change '- [ ] **PWA-02**' to '- [x] **PWA-02**' and '| PWA-02 | Phase 26 | Pending |' to '| PWA-02 | Phase 26 | Complete |'
Update REQUIREMENTS.md: change '- [ ] **PWA-08**' to '- [x] **PWA-08**' and '| PWA-08 | Phase 26 | Pending |' to '| PWA-08 | Phase 26 | Complete |'
test expected why_human
Verify < 2s broadband load with 2.1 MB main bundle Page becomes interactive in under 2 seconds on broadband (20+ Mbps); PWA cached load under 1 second Actual load time depends on server, CDN, and network conditions. The 2.1 MB uncompressed main bundle (likely ~400-600 KB gzipped) should meet the target on broadband but cannot be confirmed programmatically without a real browser performance trace.
test expected why_human
Verify PWA installability on a real Android or iOS device Browser displays 'Add to Home Screen' / install prompt. After installation, app opens as a standalone window with no browser chrome, correct Nexus icon, and theme-aware background. PWA installability requires a real browser + HTTPS + manifest validation. Cannot verify programmatically from codebase inspection alone.
test expected why_human
Verify offline message queuing end-to-end With network disabled, typing and sending a message queues it to IndexedDB. On reconnect, the message sends automatically and disappears from the queue. Requires browser interaction to toggle network state and verify IndexedDB persistence + auto-flush.
test expected why_human
Verify push notifications end-to-end (requires VAPID keys configured) After granting notification permission, a server-sent push notification appears as a native notification and clicking it opens the correct URL. Requires VAPID_PUBLIC_KEY and VAPID_PRIVATE_KEY env vars, a real browser with SW registration, and a server running to POST a test push.

Phase 26: PWA & Performance Verification Report

Phase Goal: Nexus is installable as a standalone app on any device, loads under 2 seconds, and works offline — delivering the full chat experience on phone, tablet, and desktop Verified: 2026-04-01 Status: gaps_found (2 gaps — 1 build output issue, 1 tracking inconsistency) Re-verification: No — initial verification

Goal Achievement

Observable Truths

# Truth Status Evidence
1 Service worker uses cache-first for static assets and navigation with nexus-v1 cache VERIFIED sw.js lines 1, 40-57: CACHE_NAME = "nexus-v1", caches.match("/") before fetch(request) for navigation, caches.match(request) before fetch for static extensions
2 Old paperclip-v2 cache is deleted on SW activation VERIFIED sw.js: activate event deletes all caches not equal to "nexus-v1"grep -c "paperclip" ui/public/sw.js = 0
3 Service worker handles push and notificationclick events VERIFIED sw.js lines 69-93: self.addEventListener("push", ...) and self.addEventListener("notificationclick", ...) both present
4 All page components in App.tsx are loaded via React.lazy VERIFIED grep -c "lazy(" ui/src/App.tsx = 37; Suspense wraps <Routes> with Skeleton fallback; lazy, Suspense imported from react
5 Vite build produces separate vendor chunks for heavy libraries PARTIAL vendor-router (50 KB), vendor-query (47 KB), vendor-markdown (165 KB) are extracted. vendor-react is 1 byte (empty) — React and react-dom remain in the 2.1 MB main entry chunk
6 On screens < 768px, chat renders as a full-screen MobileChatView VERIFIED ChatPanel.tsx lines 34-276: const isDesktop = useMediaQuery("(min-width: 768px)"), if (!isDesktop) return <MobileChatView />
7 Mobile chat has 48px header, back button, sticky input with safe-area padding VERIFIED MobileChatView.tsx: h-[100dvh] container, aria-label="Back to conversations" on back button, sticky bottom-0 ... pb-[env(safe-area-inset-bottom)] on input bar
8 Pulling down on conversation list triggers refresh after 64px threshold VERIFIED usePullToRefresh.ts line 23: threshold = 64; ChatConversationList.tsx wraps ScrollArea in <PullToRefresh>; navigator.vibrate?.(10) for haptic feedback
9 Offline banner appears with queued message count; messages queue to IndexedDB VERIFIED OfflineBanner.tsx uses useOnlineStatus; useOfflineQueue.ts uses openDB("nexus-offline", ...) with message_queue store; ChatPanel.tsx calls enqueue() when !isOnline
10 PWA install prompt shows with iOS fallback; push notifications end-to-end VERIFIED InstallPromptBanner.tsx: shows when canInstall||isIOS, iOS branch shows Share menu text, 7-day localStorage cooldown. pushService.ts + push.ts routes + usePushNotifications.ts all wired end-to-end

Score: 9/10 truths verified (1 partial on vendor-react splitting)

Required Artifacts

Artifact Expected Status Details
ui/public/sw.js Cache-first SW with push handler VERIFIED nexus-v1 cache, cache-first for static+nav, push+notificationclick handlers present
ui/src/types/pwa.d.ts BeforeInstallPromptEvent type VERIFIED BeforeInstallPromptEvent interface + WindowEventMap augmentation
ui/src/App.tsx Lazy-loaded page routes with Suspense VERIFIED 37 lazy( calls, Suspense wraps Routes, Skeleton fallback
ui/vite.config.ts Manual chunk splitting for vendors VERIFIED (partial) manualChunks config present; vendor-router/query/markdown extracted; vendor-react empty
ui/src/components/MobileChatView.tsx Full-screen mobile chat layout VERIFIED 283 lines; h-[100dvh], back button, PullToRefresh list view, sticky input
ui/src/components/PullToRefresh.tsx Touch gesture wrapper VERIFIED 81 lines; Loader2 spinner with "Pull to refresh" / "Release to refresh" text
ui/src/hooks/usePullToRefresh.ts Touch gesture logic VERIFIED 86 lines; 64px threshold, haptic feedback via navigator.vibrate?.(10)
ui/src/hooks/useMediaQuery.ts Responsive breakpoint hook VERIFIED 29 lines; window.matchMedia + addEventListener("change"); SSR-safe
ui/src/hooks/useInstallPrompt.ts Captures beforeinstallprompt event VERIFIED 47 lines; captures event, iOS detection via userAgent, promptInstall()
ui/src/hooks/useOfflineQueue.ts IndexedDB queue with auto-flush VERIFIED 96 lines; openDB("nexus-offline"), message_queue store, flush on online event
ui/src/hooks/useOnlineStatus.ts Reactive navigator.onLine state VERIFIED 26 lines; navigator.onLine initial state, online/offline listeners
ui/src/components/InstallPromptBanner.tsx PWA install prompt UI VERIFIED 74 lines; 7-day cooldown, iOS text variant, "Add to Home Screen" CTA
ui/src/components/OfflineBanner.tsx Offline status banner VERIFIED 46 lines; amber light/dark theming, WifiOff icon, queue count display
packages/db/src/schema/push_subscriptions.ts Push subscription DB table VERIFIED pgTable, pg-core imports, endpoint/p256dh/auth columns, endpoint index
packages/db/src/migrations/0055_create_push_subscriptions.sql SQL migration VERIFIED CREATE TABLE IF NOT EXISTS "push_subscriptions" + CREATE INDEX
server/src/services/pushService.ts VAPID config + sendPush helper VERIFIED initVapid, getVapidPublicKey, saveSubscription, removeSubscription, sendPushToAll with 410/404 stale cleanup
server/src/routes/push.ts Push API routes VERIFIED GET /vapid-public-key, POST /subscribe, DELETE /subscribe
ui/src/hooks/usePushNotifications.ts Push subscription management VERIFIED pushManager.subscribe, urlBase64ToUint8Array, isSupported guard
ui/src/components/NotificationPermissionPrompt.tsx Permission request UI VERIFIED "Stay in the loop" heading, agentResponseCount >= 3 gate, localStorage dismiss
From To Via Status Details
ui/public/sw.js ui/src/main.tsx SW registration on load event VERIFIED main.tsx line 27: navigator.serviceWorker.register("/sw.js")
ui/src/App.tsx ui/src/pages/* React.lazy(() => import('./pages/...')) VERIFIED 37 lazy imports; named-export pages use .then(m => ({ default: m.X })) pattern
ui/vite.config.ts node_modules manualChunks vendor splitting PARTIAL router/query/markdown chunks created; vendor-react is 1 byte (React not extracted)
ui/src/components/ChatPanel.tsx ui/src/components/MobileChatView.tsx Conditional render on useMediaQuery VERIFIED ChatPanel.tsx lines 15+34+275-276: import, isDesktop = useMediaQuery("(min-width: 768px)"), if (!isDesktop) return <MobileChatView />
ui/src/components/ChatConversationList.tsx ui/src/components/PullToRefresh.tsx PullToRefresh wrapper around conversation list VERIFIED ChatConversationList.tsx line 6+159: import + <PullToRefresh onRefresh={...} enabled={isMobile}>
ui/src/hooks/useOfflineQueue.ts idb openDB for IndexedDB access VERIFIED Line 1: import { openDB } from "idb", line 15: openDB(DB_NAME, 1, ...)
ui/src/components/InstallPromptBanner.tsx ui/src/hooks/useInstallPrompt.ts canInstall + promptInstall from hook VERIFIED Line 3: import { useInstallPrompt }, line 30: destructured and used
ui/src/components/OfflineBanner.tsx ui/src/hooks/useOnlineStatus.ts isOnline state for show/hide VERIFIED Wired via internal useOnlineStatus() call
ui/src/components/ChatPanel.tsx OfflineBanner + InstallPromptBanner Rendered at top of ChatPanel JSX VERIFIED Lines 16-17: imports; line 286: <OfflineBanner queuedCount={queuedCount} />, line 439: <InstallPromptBanner />
ui/src/hooks/usePushNotifications.ts server/src/routes/push.ts POST /api/push/subscribe VERIFIED usePushNotifications.ts line ~56+: calls pushApi.subscribe(sub.toJSON()) which POSTs to /api/push/subscribe
server/src/services/pushService.ts web-push webPush.sendNotification() VERIFIED Line 76: await webPush.sendNotification({ endpoint, keys: { p256dh, auth } }, ...)
server/src/routes/push.ts packages/db/src/schema/push_subscriptions.ts Drizzle insert/delete on push_subscriptions VERIFIED push.ts imports and queries pushSubscriptions table via saveSubscription/removeSubscription
server/src/app.ts server/src/routes/push.ts Route mounting at /api/push VERIFIED app.ts line 155: api.use("/push", pushRoutes(db)), line 302: initVapid()

Data-Flow Trace (Level 4)

Artifact Data Variable Source Produces Real Data Status
OfflineBanner.tsx queuedCount useOfflineQueue.ts → IndexedDB message_queue store → queuedCount state Yes — IndexedDB store count FLOWING
useOfflineQueue.ts flush → chatApi.postMessage chatApi.ts line 56+: postMessage method makes POST request Yes — real API call FLOWING
MobileChatView.tsx messages, streamingMessage Via ChatPanel context (useChatMessages, useStreamingChat) Yes — same hooks as desktop FLOWING
ChatConversationList.tsx conversation list useChatConversations hook (React Query) Yes — real DB-backed API FLOWING
NotificationPermissionPrompt.tsx agentResponseCount ChatPanel.tsx line 53: useMemo filtering messages array by role === "assistant" Yes — derived from live messages FLOWING
pushService.ts → sendPushToAll subscription rows db.select(...).from(pushSubscriptions) Drizzle query Yes — PostgreSQL DB query FLOWING

Behavioral Spot-Checks

Behavior Command Result Status
SW cache name is nexus-v1 grep "nexus-v1" ui/public/sw.js Found on line 1 PASS
No paperclip references in SW grep -c "paperclip" ui/public/sw.js 0 PASS
App.tsx has 37+ lazy imports grep -c "lazy(" ui/src/App.tsx 37 PASS
vendor-router chunk extracted ls -la ui/dist/assets/vendor-router*.js 50 KB PASS
vendor-react chunk extracted ls -la ui/dist/assets/vendor-react*.js 1 byte (empty) FAIL
SW registered in main.tsx grep "serviceWorker.register" ui/src/main.tsx Found line 27 PASS
Push routes mounted in app.ts grep "pushRoutes|/push" server/src/app.ts Lines 34+155 PASS
Stale 410/404 cleanup in pushService grep "410|404" server/src/services/pushService.ts Lines 81-84 PASS
PullToRefresh 64px threshold grep "threshold = 64" ui/src/hooks/usePullToRefresh.ts Line 23 PASS
Safe-area padding on ChatInput grep "safe-area-inset-bottom" ui/src/components/ChatInput.tsx Line 179 PASS

Requirements Coverage

Requirement Source Plan Description Status Evidence
PWA-01 26-00, 26-03 Service worker offline capability + message queue SATISFIED sw.js cache-first SW; useOfflineQueue IndexedDB queue in ChatPanel
PWA-02 26-03 Web App Manifest installable on all platforms SATISFIED (tracking gap) site.webmanifest complete with display:standalone, maskable icon, theme_color; linked in index.html with Apple PWA meta tags. REQUIREMENTS.md checkbox incorrectly shows unchecked.
PWA-03 26-02 Responsive layout adapts to phone/tablet/desktop SATISFIED MobileChatView.tsx for mobile; desktop ChatPanel unchanged; useMediaQuery breakpoint
PWA-04 26-02 Mobile-optimized input: touch targets, sticky bar, keyboard-aware SATISFIED ChatInput.tsx: min-h-[44px] min-w-[44px] send button, pb-[env(safe-area-inset-bottom)]; MobileChatView.tsx: sticky input bar
PWA-05 26-02 Pull-to-refresh on mobile conversation list SATISFIED PullToRefresh.tsx + usePullToRefresh.ts wired into ChatConversationList.tsx
PWA-06 26-04 Push notifications for agent mentions/completions SATISFIED Full stack: DB schema + migration + server service + routes + client hook + permission prompt
PWA-07 26-00 App icon, splash screen, Nexus branding SATISFIED site.webmanifest has 192px + 512px (maskable) icons; index.html has apple-touch-icon + apple-mobile-web-app-* meta tags; theme-aware theme_color in manifest and JS script in index.html
PWA-08 26-03 Add to Home Screen prompt SATISFIED (tracking gap) InstallPromptBanner.tsx wired into ChatPanel.tsx; fires on beforeinstallprompt (Chrome/Android first eligible visit) or shows iOS Share instructions; 7-day dismiss cooldown. REQUIREMENTS.md checkbox incorrectly shows unchecked.
PERF-01 26-01 Initial load < 2s broadband, < 5s on 3G PARTIAL Route splitting working (37 lazy page chunks). vendor-react not extracted — React in 2.1 MB main bundle. vendor-router/query/markdown correctly split. Needs human verification of actual load times.
PERF-05 26-00, 26-01 PWA cached load < 1s SATISFIED (needs human confirm) Cache-first SW pre-caches / and /index.html; static assets cached on first load; SW serves from cache on revisit. Actual timing requires browser measurement.

Orphaned requirements: None — all 10 phase 26 requirements appear in at least one plan's requirements: field.

Tracking inconsistency: PWA-02 and PWA-08 are implemented but REQUIREMENTS.md shows them as "Pending". This is a documentation gap, not an implementation gap.

Anti-Patterns Found

File Line Pattern Severity Impact
ui/dist/assets/vendor-react-l0sNRNKZ.js 1 1-byte file (newline only) — React not split into vendor chunk Warning React and react-dom (~140 KB gzipped) remain in 2.1 MB main entry bundle. Browser cannot cache React separately from app code. Reduces cache stability benefit for PERF-05.
.planning/REQUIREMENTS.md checkbox rows PWA-02 and PWA-08 marked [ ] Pending Info Tracking inconsistency only — both are implemented. No code impact.

Human Verification Required

1. Confirm < 2s load time with 2.1 MB main bundle

Test: Open Chrome DevTools, set network throttling to "Fast 3G", navigate to the Nexus app. Open the Performance tab and measure Time to Interactive (TTI). Expected: TTI under 5 seconds on Fast 3G; under 2 seconds on broadband (no throttling). Why human: Actual load performance depends on server, CDN, and real browser behavior. The 2.1 MB uncompressed main bundle (likely ~400-600 KB gzipped) should meet the target but cannot be confirmed without a live browser performance trace.

2. PWA installability on Android / iOS device

Test: On an Android device (Chrome), visit the app over HTTPS. Observe whether the "Add to Home Screen" prompt or mini-infobar appears. On iOS (Safari), open the Share menu and confirm "Add to Home Screen" is visible. Expected: Prompt appears on Android; Share menu option visible on iOS. After installing, app opens as standalone (no URL bar). Why human: PWA installability requires: HTTPS, service worker active, manifest with all required fields, and browser eligibility checks. These cannot be verified without a real device + live server.

3. Offline message queue end-to-end

Test: Open the app in a conversation. Open DevTools > Network > Offline mode. Type a message and send. Confirm toast "Message queued" appears and the OfflineBanner shows. Re-enable network. Confirm message sends and disappears from queue. Expected: Message enqueued to IndexedDB when offline; auto-flushed and delivered on reconnect; OfflineBanner auto-dismisses 3 seconds after reconnect. Why human: Requires browser network toggle interaction and IndexedDB observation.

4. Push notifications end-to-end (requires VAPID keys)

Test: Configure VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY, VAPID_SUBJECT env vars (generate with npx web-push generate-vapid-keys). Start the server. In a browser, grant notification permission via the NotificationPermissionPrompt (appears after 3 agent responses). Use a test script calling sendPushToAll(db, null, { title: "Test", body: "Hello" }). Verify a native browser notification appears. Expected: Native notification appears; clicking it opens the correct URL. Why human: Requires VAPID key configuration, a running server, and a real browser with SW registration.


Gaps Summary

2 gaps identified:

Gap 1 — vendor-react chunk empty (Warning): The manualChunks configuration correctly specifies "vendor-react": ["react", "react-dom"] in vite.config.ts, and three other vendor chunks (router, query, markdown) were successfully extracted. However, the vendor-react chunk in the build output is 1 byte (a newline). This is a known interaction between @vitejs/plugin-react and Rollup's manualChunks — the automatic JSX runtime injected by the Vite React plugin routes React through a different import path that bypasses the chunk assignment. React and react-dom remain bundled in the 2.1 MB main entry file. This means: (a) browsers cannot cache React separately from app code; (b) the main entry is larger than it would be with successful splitting. Three other vendor chunks ARE correctly extracted and cached separately. The lazy page splitting (37 pages as separate chunks) IS working correctly and is the primary load-time optimization. The impact on PERF-01 is uncertain and needs human measurement.

Gap 2 — REQUIREMENTS.md tracking inconsistency (Info): PWA-02 (Web App Manifest) and PWA-08 (Add to Home Screen prompt) are marked as "Pending" / unchecked in REQUIREMENTS.md. Both are actually implemented: the webmanifest file is complete with all required fields (display:standalone, maskable icon, theme_color), properly linked in index.html with Apple PWA meta tags. The InstallPromptBanner is wired into ChatPanel, handles both Chrome/Android (beforeinstallprompt) and iOS (Share menu instructions), and includes a 7-day localStorage dismiss cooldown. Plan 03 claims both requirements in its requirements: field. The REQUIREMENTS.md was not updated after plan completion. This is a documentation gap only — no code changes required, only REQUIREMENTS.md checkbox updates.


Verified: 2026-04-01 Verifier: Claude (gsd-verifier)