Files
konstruct/.planning/phases/08-mobile-pwa/08-03-SUMMARY.md

9.6 KiB

phase, plan, subsystem, tags, requires, provides, affects, tech-stack, key-files, key-decisions, patterns-established, requirements-completed, duration, completed
phase plan subsystem tags requires provides affects tech-stack key-files key-decisions patterns-established requirements-completed duration completed
08-mobile-pwa 03 ui
push-notifications
pwa
service-worker
indexeddb
web-push
vapid
offline-queue
phase provides
08-01 Serwist service worker setup, PWA manifest, app/sw.ts baseline
phase provides
08-02 Mobile nav, MobileMoreSheet, use-chat-socket.ts WebSocket hook
Push notification subscription storage (push_subscriptions DB table, Alembic migration 012)
Push notification API (subscribe, unsubscribe, send endpoints in shared/api/push.py)
Server-side VAPID push delivery via pywebpush
Service worker push + notificationclick handlers with conversation deep-link
PushPermission opt-in component (default/granted/denied/unsupported states)
InstallPrompt second-visit PWA install banner (Android + iOS)
IndexedDB offline message queue (enqueueMessage + drainQueue)
Offline-aware use-chat-socket (enqueues when disconnected, drains on reconnect)
portal
gateway
shared
migrations
added patterns
pywebpush (gateway dependency, server-side VAPID push delivery)
idb (already installed in portal, used for IndexedDB offline queue)
Push notification gate on connected user tracking via in-memory _connected_users dict
VAPID key pair in env (NEXT_PUBLIC_VAPID_PUBLIC_KEY + VAPID_PRIVATE_KEY)
Offline queue
enqueue in IndexedDB when WS disconnected, drain on ws.onopen
Service worker events extend Serwist base with addEventListener (not installSerwist)
urlBase64ToArrayBuffer (not Uint8Array) for VAPID applicationServerKey — TypeScript strict mode requires ArrayBuffer not Uint8Array<ArrayBufferLike>
created modified
packages/shared/shared/models/push.py
packages/shared/shared/api/push.py
migrations/versions/012_push_subscriptions.py
packages/portal/components/push-permission.tsx
packages/portal/components/install-prompt.tsx
packages/portal/lib/message-queue.ts
packages/portal/app/sw.ts
packages/portal/lib/use-chat-socket.ts
packages/portal/app/(dashboard)/layout.tsx
packages/portal/components/mobile-more-sheet.tsx
packages/shared/shared/api/__init__.py
packages/gateway/gateway/main.py
packages/gateway/gateway/channels/web.py
packages/gateway/pyproject.toml
.env / .env.example
Migration is 012 not 010 — migrations 010/011 were used by template data migrations after plan was written
push router lives in shared/api/push.py (not gateway/routers/push.py) — consistent with all other API routers following shared pattern
Push trigger in WebSocket handler: fires asyncio.create_task() when ws_disconnected_during_stream is True — best-effort, non-blocking
urlBase64ToArrayBuffer returns ArrayBuffer not Uint8Array<ArrayBufferLike> — TypeScript strict mode requires this for applicationServerKey
vibrate cast via spread + Record<string,unknown> in sw.ts — lib.webworker types omit vibrate from NotificationOptions despite browser support
InstallPrompt: fixed bottom-20 (above tab bar) — matches position of mobile chat input, only shown on md:hidden
PushPermission embedded in MobileMoreSheet — non-intrusive placement, available when user explicitly opens More panel
Connected user tracking via module-level _connected_users dict — avoids Redis overhead for in-process WS state
Push endpoints follow shared/api/* pattern — mount in gateway main.py via push_router import
Offline queue uses idb openDB with schema upgrade callback — consistent IndexedDB init pattern
asyncio.create_task() for fire-and-forget push from WebSocket handler — never blocks response path
MOB-05
8min 2026-03-26

Phase 08 Plan 03: Push Notifications, Offline Queue, Install Prompt Summary

Web Push notification pipeline (VAPID subscription -> DB storage -> pywebpush delivery -> service worker display), IndexedDB offline message queue with auto-drain on reconnect, and second-visit PWA install banner for Android and iOS.

Performance

  • Duration: 8 min
  • Started: 2026-03-26T03:22:15Z
  • Completed: 2026-03-26T03:30:47Z
  • Tasks: 2
  • Files modified: 15

Accomplishments

  • Complete push notification pipeline: browser subscribes with VAPID key, subscription stored in PostgreSQL, gateway delivers via pywebpush when user's WebSocket disconnects mid-stream
  • IndexedDB offline message queue: messages sent while disconnected are stored and auto-drained on WebSocket reconnection (or when network comes back online)
  • Second-visit PWA install banner handles both Android (beforeinstallprompt API) and iOS (manual Share instructions), dismissable with localStorage persistence
  • Push permission opt-in embedded in MobileMoreSheet — non-intrusive but discoverable

Task Commits

Each task was committed atomically:

  1. Task 1: Push notification backend - 7d3a393 (feat)
  2. Task 2: Push subscription client, service worker handlers, install prompt, offline queue - 81a2ce1 (feat)

Plan metadata: (created in next commit)

Files Created/Modified

Created:

  • packages/shared/shared/models/push.py - PushSubscription ORM model + Pydantic schemas
  • packages/shared/shared/api/push.py - Subscribe/unsubscribe/send API endpoints
  • migrations/versions/012_push_subscriptions.py - push_subscriptions table migration
  • packages/portal/components/push-permission.tsx - Opt-in button with permission state machine
  • packages/portal/components/install-prompt.tsx - Second-visit install banner (Android + iOS)
  • packages/portal/lib/message-queue.ts - IndexedDB offline queue (enqueue + drain)

Modified:

  • packages/portal/app/sw.ts - Added push + notificationclick event handlers
  • packages/portal/lib/use-chat-socket.ts - Offline queue integration (enqueue/drain + online status reconnect)
  • packages/portal/app/(dashboard)/layout.tsx - Mount InstallPrompt
  • packages/portal/components/mobile-more-sheet.tsx - Mount PushPermission
  • packages/shared/shared/api/__init__.py - Export push_router
  • packages/gateway/gateway/main.py - Mount push_router
  • packages/gateway/gateway/channels/web.py - Connected user tracking + push trigger on disconnect
  • packages/gateway/pyproject.toml - Add pywebpush dependency
  • .env / .env.example - VAPID key env vars

Decisions Made

  • Migration numbered 012 (not 010 as planned) — migrations 010 and 011 were already used by template-related data migrations created after the plan was written.
  • Push router placed in shared/api/push.py following all other API routers in the project; plan suggested gateway/routers/push.py but the shared pattern was already established.
  • Push trigger fires via asyncio.create_task() when the WebSocket send raises during streaming — fire-and-forget, never blocks the response path.
  • applicationServerKey uses ArrayBuffer not Uint8Array — TypeScript strict mode requires this distinction for PushManager.subscribe().
  • vibrate option cast via spread to Record<string,unknown> — TypeScript's lib.webworker omits vibrate from NotificationOptions even though all major browsers support it.

Deviations from Plan

Auto-fixed Issues

1. [Rule 1 - Bug] Migration number adjusted from 010 to 012

  • Found during: Task 1 (migration creation)
  • Issue: Migrations 010 and 011 were already used by template data migrations created after the plan was written
  • Fix: Created migration as 012 with down_revision 011
  • Files modified: migrations/versions/012_push_subscriptions.py
  • Verification: Migration file compiles, correct revision chain
  • Committed in: 7d3a393

2. [Rule 1 - Bug] urlBase64ToArrayBuffer returns ArrayBuffer (not Uint8Array)

  • Found during: Task 2 (build verification)
  • Issue: TypeScript strict types reject Uint8Array for applicationServerKey — requires ArrayBuffer
  • Fix: Changed return type and implementation to use ArrayBuffer with Uint8Array view
  • Files modified: packages/portal/components/push-permission.tsx
  • Verification: npm run build passes
  • Committed in: 81a2ce1

3. [Rule 1 - Bug] vibrate option cast in service worker

  • Found during: Task 2 (build verification)
  • Issue: TypeScript lib.webworker types don't include vibrate in NotificationOptions despite browser support
  • Fix: Cast notification options to include vibrate via Record<string,unknown> spread
  • Files modified: packages/portal/app/sw.ts
  • Verification: npm run build passes
  • Committed in: 81a2ce1

Total deviations: 3 auto-fixed (1 migration numbering, 2 TypeScript strict type issues from build verification) Impact on plan: All auto-fixes necessary for correctness and build success. No scope creep.

Issues Encountered

None beyond the auto-fixed TypeScript strict type issues above.

User Setup Required

VAPID keys have been pre-generated and added to .env. For production deployments, generate new keys:

cd packages/portal && npx web-push generate-vapid-keys

Then set NEXT_PUBLIC_VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY, and VAPID_CLAIMS_EMAIL in your environment.

Next Phase Readiness

Phase 08 (Mobile PWA) is now complete — all 3 plans delivered:

  • 08-01: Service worker, offline caching, PWA manifest, web app manifest
  • 08-02: Mobile navigation, chat UI improvements, responsive layout
  • 08-03: Push notifications, offline queue, install prompt

The portal is now a fully-featured PWA with push notifications, offline support, and installability.


Phase: 08-mobile-pwa Completed: 2026-03-26