---
phase: 08-mobile-pwa
verified: 2026-03-26T03:35:51Z
status: gaps_found
score: 11/13 must-haves verified
gaps:
  - truth: "Streaming responses (word-by-word tokens) work on mobile"
    status: failed
    reason: "uuid() in chat-window.tsx line 18 calls itself recursively instead of crypto.randomUUID() — infinite recursion and stack overflow on every HTTPS/localhost chat message send where crypto.randomUUID is available"
    artifacts:
      - path: "packages/portal/components/chat-window.tsx"
        issue: "Line 18: return uuid() should be return crypto.randomUUID() — calls itself instead of the native function"
    missing:
      - "Fix line 18: change `return uuid()` to `return crypto.randomUUID()`"
  - truth: "MOB-02 requirement text satisfied — sidebar collapses to bottom tab bar (requirement text says hamburger menu)"
    status: partial
    reason: "REQUIREMENTS.md MOB-02 specifies 'hamburger menu' but implementation uses a bottom tab bar. The PLAN, SUMMARY, and codebase all consistently implement a tab bar which is a superior mobile UX pattern. The REQUIREMENTS.md description is outdated. This is a documentation mismatch, not a code gap — the implemented UX exceeds the requirement intent."
    artifacts:
      - path: ".planning/REQUIREMENTS.md"
        issue: "MOB-02 text says 'hamburger menu' but codebase implements bottom tab bar per PLAN. REQUIREMENTS.md should be updated to reflect the actual design."
    missing:
      - "Update REQUIREMENTS.md MOB-02 description to say 'bottom tab bar' instead of 'hamburger menu'"
human_verification:
  - test: "Navigate all portal pages at 320px and 768px viewports"
    expected: "No horizontal scroll, no overlapping elements, readable text at both widths"
    why_human: "Visual layout correctness on real or emulated viewports cannot be verified by grep"
  - test: "Tap each bottom tab bar item at 320px width, then open More sheet"
    expected: "Active indicator shows, correct page loads, More sheet slides up with RBAC-filtered items"
    why_human: "Touch tap target size (44px minimum), animation smoothness, and RBAC filtering require real device or browser DevTools interaction"
  - test: "Open chat, tap a conversation, send a message and wait for streaming response on mobile"
    expected: "Full-screen chat with back arrow, tokens appear word-by-word, back arrow returns to list"
    why_human: "Streaming animation and touch navigation flow require browser runtime"
  - test: "Open Chrome DevTools > Application > Manifest"
    expected: "Manifest loads with name=Konstruct, icons at 192/512/maskable, start_url=/dashboard"
    why_human: "PWA installability requires browser DevTools or Lighthouse audit"
  - test: "Enable push notifications via More sheet > bell icon, then close browser tab"
    expected: "Push notification appears when AI Employee responds; tapping it opens the correct conversation"
    why_human: "Push notification delivery requires a running server, VAPID keys, and a real browser push subscription"
---

# Phase 8: Mobile PWA Verification Report

**Phase Goal:** The portal is fully responsive on mobile/tablet devices and installable as a Progressive Web App — operators and customers can manage their AI workforce and chat with employees from any device
**Verified:** 2026-03-26T03:35:51Z
**Status:** gaps_found
**Re-verification:** No — initial verification

## Goal Achievement

### Observable Truths

| #  | Truth                                                                                   | Status        | Evidence                                                                                    |
|----|-----------------------------------------------------------------------------------------|---------------|---------------------------------------------------------------------------------------------|
| 1  | Desktop sidebar is hidden on screens < 768px; bottom tab bar appears instead            | VERIFIED      | `layout.tsx:44` `hidden md:flex` wraps Nav; `MobileNav` has `md:hidden` class              |
| 2  | Bottom tab bar has 5 items: Dashboard, Employees, Chat, Usage, More                     | VERIFIED      | `mobile-nav.tsx:28-33` TAB_ITEMS array + More button at line 78                            |
| 3  | More sheet opens with Billing, API Keys, Users, Platform, Settings, Sign Out (RBAC)     | VERIFIED      | `mobile-more-sheet.tsx:32-38` SHEET_ITEMS with allowedRoles + signOut button at line 107   |
| 4  | Main content has bottom padding on mobile to clear the tab bar                          | VERIFIED      | `layout.tsx:50` `pb-20 md:pb-8` applied to max-w container                                 |
| 5  | Portal is installable as a PWA with manifest, icons, and service worker                 | VERIFIED      | `manifest.ts` exports valid manifest; `sw.ts` uses Serwist; all 5 icon PNGs exist          |
| 6  | Offline banner appears when network is lost                                             | VERIFIED      | `offline-banner.tsx` returns amber banner when `useOnlineStatus()` is false                |
| 7  | All existing pages remain functional on desktop (no regression)                         | HUMAN NEEDED  | Sidebar visible at md+ in `hidden md:flex` div — automated check passes; visual needs human|
| 8  | On mobile, tapping a conversation shows full-screen chat with back arrow header         | VERIFIED      | `chat/page.tsx:271-276` MobileChatHeader rendered when mobileShowChat is true              |
| 9  | Back arrow returns to conversation list on mobile                                       | VERIFIED      | `mobile-chat-header.tsx:24-29` onBack callback; `chat/page.tsx:275` sets mobileShowChat=false |
| 10 | Desktop two-column chat layout is unchanged                                             | VERIFIED      | `chat/page.tsx:244-262` md:w-72 md:block classes preserved on sidebar panel               |
| 11 | Chat input stays visible when iOS virtual keyboard opens                                | VERIFIED      | `chat-window.tsx:266-269` keyboardOffset from useVisualViewport applied to input paddingBottom |
| 12 | Streaming responses (word-by-word tokens) work on mobile                               | FAILED        | `chat-window.tsx:17-18` uuid() calls itself recursively — infinite recursion on send       |
| 13 | User can grant push notification permission from the portal                             | VERIFIED      | `push-permission.tsx` full state machine; embedded in MobileMoreSheet                      |

**Score:** 11/13 truths verified (1 failed, 1 human-needed)

---

### Required Artifacts

| Artifact                                               | Provides                                         | Exists | Substantive | Wired  | Status      |
|--------------------------------------------------------|--------------------------------------------------|--------|-------------|--------|-------------|
| `packages/portal/components/mobile-nav.tsx`            | Bottom tab bar for mobile                        | YES    | YES         | YES    | VERIFIED    |
| `packages/portal/components/mobile-more-sheet.tsx`     | Bottom sheet secondary nav with RBAC             | YES    | YES         | YES    | VERIFIED    |
| `packages/portal/app/manifest.ts`                      | PWA manifest with K monogram icons               | YES    | YES         | YES    | VERIFIED    |
| `packages/portal/app/sw.ts`                            | Serwist SW + push + notificationclick handlers   | YES    | YES         | YES    | VERIFIED    |
| `packages/portal/components/sw-register.tsx`           | Service worker registration                      | YES    | YES         | YES    | VERIFIED    |
| `packages/portal/components/offline-banner.tsx`        | Offline status indicator                         | YES    | YES         | YES    | VERIFIED    |
| `packages/portal/components/mobile-chat-header.tsx`    | Back arrow + agent name for mobile chat          | YES    | YES         | YES    | VERIFIED    |
| `packages/portal/lib/use-visual-viewport.ts`           | Visual Viewport hook for iOS keyboard offset     | YES    | YES         | YES    | VERIFIED    |
| `packages/portal/components/install-prompt.tsx`        | Second-visit PWA install banner (Android + iOS)  | YES    | YES         | YES    | VERIFIED    |
| `packages/portal/components/push-permission.tsx`       | Push opt-in with permission state machine        | YES    | YES         | YES    | VERIFIED    |
| `packages/portal/lib/message-queue.ts`                 | IndexedDB offline message queue                  | YES    | YES         | YES    | VERIFIED    |
| `packages/portal/app/actions/push.ts`                  | Server actions for push (planned artifact)       | NO     | —           | —      | MISSING     |
| `packages/gateway/routers/push.py`                     | Push API endpoints (planned path)                | NO*    | —           | —      | MISSING*    |
| `packages/shared/shared/api/push.py`                   | Push API (actual path — different from plan)     | YES    | YES         | YES    | VERIFIED    |
| `migrations/versions/012_push_subscriptions.py`        | push_subscriptions table migration               | YES    | YES         | YES    | VERIFIED    |
| `packages/portal/public/icon-192.png`                  | PWA icon 192x192                                 | YES    | YES         | —      | VERIFIED    |
| `packages/portal/public/icon-512.png`                  | PWA icon 512x512                                 | YES    | YES         | —      | VERIFIED    |
| `packages/portal/public/icon-maskable-192.png`         | Maskable PWA icon                                | YES    | YES         | —      | VERIFIED    |
| `packages/portal/public/apple-touch-icon.png`          | Apple touch icon                                 | YES    | YES         | —      | VERIFIED    |
| `packages/portal/public/badge-72.png`                  | Notification badge icon                          | YES    | YES         | —      | VERIFIED    |

> NOTE: `app/actions/push.ts` and `gateway/routers/push.py` are listed in the plan frontmatter but were deliberately implemented differently. Push subscription management is handled via direct `fetch` in `push-permission.tsx` to `/api/portal/push/subscribe`, which is served by `shared/api/push.py` (consistent with all other API routers in this project). The plan's artifact list is outdated; no functional gap exists for push subscription flow. These are documentation mismatches, not code gaps.

---

### Key Link Verification

| From                                          | To                                          | Via                                      | Status     | Details                                                               |
|-----------------------------------------------|---------------------------------------------|------------------------------------------|------------|-----------------------------------------------------------------------|
| `app/(dashboard)/layout.tsx`                  | `components/mobile-nav.tsx`                 | `hidden md:flex` / `md:hidden` pattern   | WIRED      | Line 44: `hidden md:flex` on Nav div; MobileNav at line 57 is always rendered (md:hidden internally) |
| `next.config.ts`                              | `app/sw.ts`                                 | withSerwist wrapper                      | WIRED      | Lines 7-11: withSerwistInit configured with swSrc/swDest             |
| `app/layout.tsx`                              | `components/sw-register.tsx`                | Mounted in body                          | WIRED      | Line 47: `<ServiceWorkerRegistration />` in body                     |
| `app/(dashboard)/chat/page.tsx`               | `components/mobile-chat-header.tsx`         | Rendered when mobileShowChat is true     | WIRED      | Lines 271-276: MobileChatHeader rendered inside mobileShowChat block |
| `components/chat-window.tsx`                  | `lib/use-visual-viewport.ts`                | keyboardOffset applied to input          | WIRED      | Line 32: import; line 77: `const keyboardOffset = useVisualViewport()`; line 268: applied as style |
| `app/(dashboard)/chat/page.tsx`               | `mobileShowChat` state                      | State toggle on conversation select      | WIRED      | Line 154: useState; lines 173-181: handleSelectConversation sets true |
| `app/sw.ts`                                   | push event handler                          | `self.addEventListener('push', ...)`     | WIRED      | Line 24: push event listener; lines 35-46: showNotification call     |
| `app/sw.ts`                                   | notificationclick handler                   | `self.addEventListener('notificationclick', ...)` | WIRED | Line 48: notificationclick listener; deep-link to /chat?id=           |
| `gateway/channels/web.py`                     | `shared/api/push.py`                        | asyncio.create_task(_send_push_notification) | WIRED | Line 462: asyncio.create_task; line 115: imports _send_push          |
| `lib/use-chat-socket.ts`                      | `lib/message-queue.ts`                      | enqueue when offline, drain on reconnect | WIRED      | Line 17: imports; line 105: drainQueue in ws.onopen; line 208: enqueueMessage in send |

---

### Requirements Coverage

| Requirement | Source Plan | Description                                                                  | Status         | Evidence                                                         |
|-------------|-------------|------------------------------------------------------------------------------|----------------|------------------------------------------------------------------|
| MOB-01      | 08-01       | All portal pages render correctly on mobile (320px–480px) and tablet         | HUMAN NEEDED   | Layout wiring verified; visual correctness requires browser test |
| MOB-02      | 08-01       | Sidebar collapses (spec: hamburger menu, impl: bottom tab bar)               | VERIFIED*      | Tab bar implemented — superior to spec'd hamburger; REQUIREMENTS.md text is stale |
| MOB-03      | 08-02       | Chat interface fully functional on mobile — send, stream, scroll history     | FAILED         | mobileShowChat toggle wired; MobileChatHeader present; BUT uuid() recursive bug in chat-window.tsx blocks message send in secure contexts |
| MOB-04      | 08-01       | Portal installable as PWA with icon, splash, service worker                  | VERIFIED       | manifest.ts, sw.ts, all icons, next.config.ts withSerwist all confirmed |
| MOB-05      | 08-03       | Push notifications for new messages when PWA installed                       | VERIFIED       | Full pipeline: PushPermission -> push.py -> 012 migration -> web.py trigger -> sw.ts handler |
| MOB-06      | 08-02       | All touch interactions feel native — no hover-dependent UI on touch devices  | HUMAN NEEDED   | `active:bg-accent` classes present on all interactive items; 44px tap targets in all components; needs real device confirmation |

> *MOB-02: REQUIREMENTS.md says "hamburger menu" — implementation delivers a bottom tab bar per the PLAN design. The tab bar is the intended and superior design; requirement text is stale documentation.

---

### Anti-Patterns Found

| File                                                  | Line | Pattern                        | Severity | Impact                                                                   |
|-------------------------------------------------------|------|--------------------------------|----------|--------------------------------------------------------------------------|
| `packages/portal/components/chat-window.tsx`          | 18   | `return uuid()` — recursive call | BLOCKER | Infinite recursion when `crypto.randomUUID` is available (all HTTPS + localhost). Chat messages never send; browser tab crashes or hangs. |

---

### Human Verification Required

#### 1. Responsive Layout at Multiple Viewports (MOB-01)

**Test:** Open Chrome DevTools, toggle device toolbar, test at 320px (iPhone SE), 768px (iPad), 1024px (iPad landscape). Navigate to Dashboard, Employees, Chat, Usage, and Billing pages.
**Expected:** No horizontal scrolling, no overlapping elements, readable text at all widths. Sidebar visible at 768px+, tab bar visible at 320px.
**Why human:** Visual layout correctness (overflow, overlap, text truncation) requires rendered browser output.

#### 2. Bottom Tab Bar Navigation and More Sheet (MOB-02)

**Test:** At 320px, tap each tab (Dashboard, Employees, Chat, Usage). Tap More — verify sheet slides up. Check with `customer_operator` role that Billing/API Keys/Users are hidden in the sheet.
**Expected:** Active indicator on tapped tab; correct page loads; More sheet shows RBAC-filtered items; LanguageSwitcher and push permission toggle visible.
**Why human:** Touch tap feedback, animation smoothness, and RBAC filtering in the rendered session context cannot be verified by static analysis.

#### 3. Mobile Chat Full-Screen Flow (MOB-03)

**Test:** At 320px, navigate to Chat. Tap a conversation — verify full-screen mode with back arrow header and agent name. After fixing the uuid() bug (see Gaps), send a message and verify streaming tokens appear word-by-word. Tap back arrow — verify return to list.
**Expected:** WhatsApp-style navigation; streaming tokens render incrementally; back arrow works.
**Why human:** Streaming animation and back navigation flow require browser runtime. The uuid() bug MUST be fixed first.

#### 4. PWA Installability (MOB-04)

**Test:** Open Chrome DevTools > Application > Manifest. Verify manifest loads with name=Konstruct, icons at 192/512/maskable, start_url=/dashboard, display=standalone. Check Application > Service Workers for registration.
**Expected:** Manifest loads without errors; service worker registered and active; Lighthouse PWA audit score >= 90.
**Why human:** PWA install flow requires a browser with HTTPS or localhost, Lighthouse, or Android device.

#### 5. Push Notifications (MOB-05)

**Test:** Open More sheet, tap bell icon, grant notification permission. Close the browser tab. Trigger an AI Employee response (via API or second browser window). Tap the push notification.
**Expected:** Notification appears on device; tapping notification opens the portal at the correct conversation URL.
**Why human:** Push notification delivery requires running backend services, VAPID keys, and a real browser push subscription. Cannot simulate with grep.

#### 6. Touch Interaction Native Feel (MOB-06)

**Test:** At 320px, tap all buttons, links, and interactive elements throughout the portal. Test tab bar items, More sheet links, chat send button, conversation list items.
**Expected:** Immediate visual feedback on tap (active state); no hover-stuck states; all targets reachable with a finger (>= 44px).
**Why human:** Touch interaction feel, hover-stuck detection, and tap target perception require a real touch device or touch simulation in DevTools.

---

### Gaps Summary

**One blocker gap, one documentation gap.**

**Blocker: Infinite recursion in `uuid()` (chat-window.tsx line 18)**

The `uuid()` helper function in `chat-window.tsx` was written to fall back to a manual UUID generator when `crypto.randomUUID` is unavailable (e.g., HTTP non-secure context). However, the branch that should call `crypto.randomUUID()` calls `uuid()` itself recursively. In any secure context (HTTPS, localhost), `crypto.randomUUID` is always available, so every call to `uuid()` immediately recurses infinitely — causing a stack overflow. Chat messages in the portal require `uuid()` to generate stable IDs for optimistic UI updates (lines 100, 148, 158, 198). The fix is one character: change `return uuid()` to `return crypto.randomUUID()` on line 18.

**Documentation gap: REQUIREMENTS.md MOB-02 text is stale**

The REQUIREMENTS.md describes MOB-02 as "hamburger menu" but the design (defined in the PLAN and implemented in the codebase) uses a bottom tab bar — a more native mobile pattern. This is a documentation-only mismatch; the codebase correctly implements the intended design. Updating REQUIREMENTS.md to say "bottom tab bar" would bring the documentation in sync with the actual implementation.

---

_Verified: 2026-03-26T03:35:51Z_
_Verifier: Claude (gsd-verifier)_
