---
phase: 08-mobile-pwa
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
  - packages/portal/package.json
  - packages/portal/next.config.ts
  - packages/portal/app/manifest.ts
  - packages/portal/app/sw.ts
  - packages/portal/app/layout.tsx
  - packages/portal/app/(dashboard)/layout.tsx
  - packages/portal/components/mobile-nav.tsx
  - packages/portal/components/mobile-more-sheet.tsx
  - packages/portal/components/sw-register.tsx
  - packages/portal/components/offline-banner.tsx
  - packages/portal/lib/use-offline.ts
  - packages/portal/public/icon-192.png
  - packages/portal/public/icon-512.png
  - packages/portal/public/icon-maskable-192.png
  - packages/portal/public/apple-touch-icon.png
  - packages/portal/public/badge-72.png
  - packages/portal/messages/en.json
  - packages/portal/messages/es.json
  - packages/portal/messages/pt.json
autonomous: true
requirements:
  - MOB-01
  - MOB-02
  - MOB-04

must_haves:
  truths:
    - "Desktop sidebar is hidden on screens < 768px; bottom tab bar appears instead"
    - "Bottom tab bar has 5 items: Dashboard, Employees, Chat, Usage, More"
    - "More sheet opens with Billing, API Keys, Users, Platform, Settings, Sign Out (RBAC-filtered)"
    - "Main content has bottom padding on mobile to clear the tab bar"
    - "Portal is installable as a PWA with manifest, icons, and service worker"
    - "Offline banner appears when network is lost"
    - "All existing pages remain functional on desktop (no regression)"
  artifacts:
    - path: "packages/portal/components/mobile-nav.tsx"
      provides: "Bottom tab bar navigation for mobile"
      exports: ["MobileNav"]
    - path: "packages/portal/components/mobile-more-sheet.tsx"
      provides: "Bottom sheet for secondary nav items"
      exports: ["MobileMoreSheet"]
    - path: "packages/portal/app/manifest.ts"
      provides: "PWA manifest with K monogram icons"
      exports: ["default"]
    - path: "packages/portal/app/sw.ts"
      provides: "Service worker with Serwist precaching"
    - path: "packages/portal/components/sw-register.tsx"
      provides: "Service worker registration client component"
      exports: ["ServiceWorkerRegistration"]
    - path: "packages/portal/components/offline-banner.tsx"
      provides: "Offline status indicator"
      exports: ["OfflineBanner"]
  key_links:
    - from: "packages/portal/app/(dashboard)/layout.tsx"
      to: "packages/portal/components/mobile-nav.tsx"
      via: "conditional render with hidden md:flex / md:hidden"
      pattern: "MobileNav.*md:hidden"
    - from: "packages/portal/next.config.ts"
      to: "packages/portal/app/sw.ts"
      via: "withSerwist wrapper generates public/sw.js from app/sw.ts"
      pattern: "withSerwist"
    - from: "packages/portal/app/layout.tsx"
      to: "packages/portal/components/sw-register.tsx"
      via: "mounted in body for service worker registration"
      pattern: "ServiceWorkerRegistration"
---

<objective>
Responsive mobile layout foundation and PWA infrastructure. Desktop sidebar becomes a bottom tab bar on mobile, PWA manifest and service worker enable installability, and offline detection provides status feedback.

Purpose: This is the structural foundation that all other mobile plans build on. The layout split (sidebar vs tab bar) affects every page, and PWA infrastructure (manifest + service worker) is required before push notifications or offline caching can work.

Output: Mobile-responsive dashboard layout, bottom tab bar with More sheet, PWA manifest with K monogram icons, Serwist service worker, offline banner component.
</objective>

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

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/08-mobile-pwa/08-CONTEXT.md
@.planning/phases/08-mobile-pwa/08-RESEARCH.md

<interfaces>
<!-- Key types and contracts the executor needs. Extracted from codebase. -->

From packages/portal/components/nav.tsx:
```typescript
type NavItem = {
  href: string;
  label: string;
  icon: React.ElementType;
  allowedRoles?: string[];
};

// Nav items array (lines 43-53):
// Dashboard, Tenants (platform_admin), Employees, Chat, Usage,
// Billing (admin+), API Keys (admin+), Users (admin+), Platform (platform_admin)
// Role filtering: visibleItems = navItems.filter(item => !item.allowedRoles || item.allowedRoles.includes(role))
```

From packages/portal/app/(dashboard)/layout.tsx:
```typescript
// Current layout: flex min-h-screen, <Nav /> sidebar + <main> content
// Wraps with SessionProvider, QueryClientProvider, SessionSync, ImpersonationBanner
```

From packages/portal/app/layout.tsx:
```typescript
// Root layout: Server Component with next-intl provider
// Exports metadata: Metadata
// No viewport export yet (needs viewportFit: 'cover' for safe areas)
```

From packages/portal/next.config.ts:
```typescript
import createNextIntlPlugin from "next-intl/plugin";
const withNextIntl = createNextIntlPlugin("./i18n/request.ts");
const nextConfig: NextConfig = { output: "standalone" };
export default withNextIntl(nextConfig);
// Must compose: withNextIntl(withSerwist(nextConfig))
```

From packages/portal/proxy.ts:
```typescript
const CUSTOMER_OPERATOR_RESTRICTED = ["/billing", "/settings/api-keys", "/users", "/admin", "/agents/new"];
const PLATFORM_ADMIN_ONLY = ["/admin"];
```
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Install PWA dependencies, create icons, manifest, service worker, and offline utilities</name>
  <files>
    packages/portal/package.json,
    packages/portal/next.config.ts,
    packages/portal/app/manifest.ts,
    packages/portal/app/sw.ts,
    packages/portal/app/layout.tsx,
    packages/portal/components/sw-register.tsx,
    packages/portal/components/offline-banner.tsx,
    packages/portal/lib/use-offline.ts,
    packages/portal/public/icon-192.png,
    packages/portal/public/icon-512.png,
    packages/portal/public/icon-maskable-192.png,
    packages/portal/public/apple-touch-icon.png,
    packages/portal/public/badge-72.png
  </files>
  <action>
    1. Install PWA dependencies in packages/portal:
       ```bash
       npm install @serwist/next serwist web-push idb
       npm install -D @types/web-push
       ```

    2. Generate PWA icon assets. Create a Node.js script using `sharp` (install as devDep if needed) or canvas to generate the K monogram icons. The "K" should be bold white text on a dark gradient background (#0f0f1a to #1a1a2e with a subtle purple/blue accent). Generate:
       - public/icon-192.png (192x192) — K monogram on gradient
       - public/icon-512.png (512x512) — K monogram on gradient
       - public/icon-maskable-192.png (192x192 with 10% safe-zone padding)
       - public/apple-touch-icon.png (180x180)
       - public/badge-72.png (72x72, monochrome white K on transparent)
       If sharp is complex, create simple SVGs and convert, or use canvas. The icons must exist as real PNG files.

    3. Create `app/manifest.ts` using Next.js built-in manifest file convention:
       ```typescript
       import type { MetadataRoute } from 'next'
       export default function manifest(): MetadataRoute.Manifest {
         return {
           name: 'Konstruct',
           short_name: 'Konstruct',
           description: 'AI Workforce Platform',
           start_url: '/dashboard',
           display: 'standalone',
           background_color: '#0f0f1a',
           theme_color: '#0f0f1a',
           orientation: 'portrait',
           icons: [
             { src: '/icon-192.png', sizes: '192x192', type: 'image/png' },
             { src: '/icon-512.png', sizes: '512x512', type: 'image/png' },
             { src: '/icon-maskable-192.png', sizes: '192x192', type: 'image/png', purpose: 'maskable' },
           ],
         }
       }
       ```

    4. Create `app/sw.ts` — Serwist service worker source:
       ```typescript
       import { defaultCache } from '@serwist/next/worker'
       import { installSerwist } from 'serwist'
       declare const self: ServiceWorkerGlobalScope
       installSerwist({
         precacheEntries: self.__SW_MANIFEST,
         skipWaiting: true,
         clientsClaim: true,
         navigationPreload: true,
         runtimeCaching: defaultCache,
       })
       ```
       NOTE: Push event listeners will be added in Plan 03. Keep this minimal for now.

    5. Update `next.config.ts` — wrap with Serwist:
       ```typescript
       import withSerwistInit from '@serwist/next'
       const withSerwist = withSerwistInit({
         swSrc: 'app/sw.ts',
         swDest: 'public/sw.js',
         disable: process.env.NODE_ENV === 'development',
       })
       export default withNextIntl(withSerwist(nextConfig))
       ```
       Compose order: withNextIntl wraps withSerwist wraps nextConfig.

    6. Create `components/sw-register.tsx` — client component that registers the service worker:
       ```typescript
       "use client"
       import { useEffect } from 'react'
       export function ServiceWorkerRegistration() {
         useEffect(() => {
           if ('serviceWorker' in navigator) {
             void navigator.serviceWorker.register('/sw.js', { scope: '/', updateViaCache: 'none' })
           }
         }, [])
         return null
       }
       ```

    7. Update `app/layout.tsx`:
       - Add `Viewport` export with `viewportFit: 'cover'` to enable safe-area-inset CSS env vars on iOS
       - Mount `<ServiceWorkerRegistration />` in the body (outside NextIntlClientProvider is fine since it uses no translations)
       - Add `<OfflineBanner />` in body (inside NextIntlClientProvider since it needs translations)

    8. Create `lib/use-offline.ts` — hook for online/offline state:
       ```typescript
       import { useState, useEffect } from 'react'
       export function useOnlineStatus() {
         const [isOnline, setIsOnline] = useState(typeof navigator !== 'undefined' ? navigator.onLine : true)
         useEffect(() => {
           const on = () => setIsOnline(true)
           const off = () => setIsOnline(false)
           window.addEventListener('online', on)
           window.addEventListener('offline', off)
           return () => { window.removeEventListener('online', on); window.removeEventListener('offline', off) }
         }, [])
         return isOnline
       }
       ```

    9. Create `components/offline-banner.tsx` — fixed banner at top when offline:
       ```typescript
       "use client"
       // Shows "You're offline" with a subtle amber/red bar at the top of the viewport
       // Uses useOnlineStatus hook. Renders null when online.
       // Position: fixed top-0 z-[60] full width, above everything
       // Add i18n key: common.offlineBanner
       ```
  </action>
  <verify>
    <automated>cd /home/adelorenzo/repos/konstruct/packages/portal && npm run lint</automated>
  </verify>
  <done>
    PWA dependencies installed. Icon PNGs exist in public/. manifest.ts serves at /manifest.webmanifest. sw.ts compiles. Service worker registration component mounted in root layout. Viewport export includes viewportFit cover. Offline banner shows when navigator.onLine is false. Lint passes.
  </done>
</task>

<task type="auto">
  <name>Task 2: Mobile bottom tab bar, More sheet, and responsive dashboard layout</name>
  <files>
    packages/portal/app/(dashboard)/layout.tsx,
    packages/portal/components/mobile-nav.tsx,
    packages/portal/components/mobile-more-sheet.tsx,
    packages/portal/messages/en.json,
    packages/portal/messages/es.json,
    packages/portal/messages/pt.json
  </files>
  <action>
    1. Create `components/mobile-nav.tsx` — bottom tab bar:
       - "use client" component
       - Position: `fixed bottom-0 left-0 right-0 z-50 bg-background border-t`
       - Add `paddingBottom: env(safe-area-inset-bottom)` via inline style for iOS home indicator
       - CSS class: `md:hidden` — only visible below 768px
       - 5 tab items: Dashboard (LayoutDashboard icon, href="/dashboard"), Employees (Users icon, href="/agents"), Chat (MessageSquare icon, href="/chat"), Usage (BarChart2 icon, href="/usage"), More (MoreHorizontal or Ellipsis icon, opens sheet)
       - Active tab: highlighted with primary color icon + subtle indicator dot/pill below icon
       - Use `usePathname()` to determine active state
       - Icons should be the primary visual element with very small text labels below (per user: "solid icons, subtle active indicator, no text labels (or very small ones)")
       - RBAC: Read session role. The 4 navigation items (Dashboard, Employees, Chat, Usage) are visible to all roles. "More" is always visible. RBAC filtering happens inside the More sheet.
       - Tab bar height: ~60px plus safe area. Content padding `pb-16 md:pb-0` on main.

    2. Create `components/mobile-more-sheet.tsx` — bottom sheet for secondary items:
       - "use client" component
       - Use @base-ui/react Dialog as a bottom sheet (position: fixed bottom, slides up with animation)
       - Contains nav links: Billing, API Keys, Users, Platform, Settings, Sign Out
       - Apply RBAC: filter items by role (same logic as Nav — Billing/API Keys/Users visible to platform_admin and customer_admin only; Platform visible to platform_admin only)
       - Include LanguageSwitcher component at the bottom of the sheet
       - Include Sign Out button at the bottom
       - Props: `open: boolean, onOpenChange: (open: boolean) => void`
       - Style: rounded-t-2xl, bg-background, max-h-[70vh], draggable handle at top
       - Each item: icon + label, full-width tap target, closes sheet on navigation

    3. Update `app/(dashboard)/layout.tsx`:
       - Wrap existing `<Nav />` in `<div className="hidden md:flex">` — sidebar hidden on mobile
       - Add `<MobileNav />` after the main content area (it renders with md:hidden internally)
       - Add `pb-16 md:pb-0` to the `<main>` element to clear the tab bar on mobile
       - Reduce padding on mobile: change `px-8 py-8` to `px-4 md:px-8 py-4 md:py-8`
       - Keep all existing providers (SessionProvider, QueryClientProvider, etc.) unchanged

    4. Add i18n keys for mobile nav in all three locale files (en.json, es.json, pt.json):
       - mobileNav.dashboard, mobileNav.employees, mobileNav.chat, mobileNav.usage, mobileNav.more
       - mobileNav.billing, mobileNav.apiKeys, mobileNav.users, mobileNav.platform, mobileNav.settings, mobileNav.signOut
       - common.offlineBanner: "You're offline — changes will sync when you reconnect"
       NOTE: Nav already has these keys under "nav.*" — reuse the same translation keys from the nav namespace where possible to avoid duplication. Only add new keys if the mobile label differs.
  </action>
  <verify>
    <automated>cd /home/adelorenzo/repos/konstruct/packages/portal && npm run build</automated>
  </verify>
  <done>
    Bottom tab bar renders on screens below 768px with 5 items. Desktop sidebar is hidden on mobile. More sheet opens from the More tab with RBAC-filtered secondary items including language switcher and sign out. Main content has bottom padding on mobile. All pages render without layout breakage on both mobile and desktop. Build passes.
  </done>
</task>

</tasks>

<verification>
- `npm run build` passes in packages/portal (TypeScript + Next.js compilation)
- `npm run lint` passes in packages/portal
- PWA manifest accessible (app/manifest.ts exports valid MetadataRoute.Manifest)
- Icon files exist: public/icon-192.png, public/icon-512.png, public/icon-maskable-192.png, public/apple-touch-icon.png, public/badge-72.png
- Service worker source compiles (app/sw.ts)
- Desktop layout unchanged — sidebar visible at md+ breakpoint
- Mobile layout shows bottom tab bar, sidebar hidden
</verification>

<success_criteria>
All portal pages render with the bottom tab bar on mobile (< 768px) and the sidebar on desktop (>= 768px). PWA manifest and service worker infrastructure are in place. Offline banner appears when disconnected.
</success_criteria>

<output>
After completion, create `.planning/phases/08-mobile-pwa/08-01-SUMMARY.md`
</output>
