# Pulse Mobile Shell Redesign

## What This Is

A ground-up redesign of Pulse's `/mobile/*` shell — the manager-on-the-go view of
the existing Pulse PSA dashboard. It replaces the current mobile layout, swaps
the standalone `/mobile/nav` page for a Sheet drawer, restyles Dashboard /
Tickets / Finance, and adds two new mobile surfaces (Analyzer feed,
Engagement). Built on the existing Pulse codebase — same routes, same data,
phone-first layouts.

## Core Value

A manager can open Pulse on their phone and, in under 30 seconds, see the
state of the business and triage tickets — without ever needing to switch to
desktop for read-only awareness.

## Current Milestone: v2.0 PAX8 Integration

**Goal:** Sync PAX8 licensing/subscription data into Pulse, read-only, mapped
to Autotask companies, so managers can see subscription costs and seat counts
alongside existing company data.

**Target features:**
- PAX8 client + factory (OAuth2 client-credentials; credentials already
  provisioned)
- Read-only sync of companies, subscriptions, product catalog, and
  orders/invoices into Postgres
- Fuzzy-name matching of PAX8 companies to Autotask companies, with a
  manual-review path for unmatched/ambiguous cases
- New `/pax8` page (company list, subscriptions, cost breakdown)
- Daily sync via the existing scheduler pattern

Unrelated in domain to the v1.0 Mobile Shell Redesign above — this milestone
adds a new backend integration and admin-facing surface, not a mobile change.
See `.planning/seeds/SEED-002-pax8-integration.md` for the exploration that
scoped this milestone. `.planning/seeds/SEED-003-general-pulse-data-assistant.md`
(NL chatbot over Pulse data) is explicitly out of scope here — a separate
future milestone that will consume this data once it exists.

## Requirements

### Validated

<!-- Capabilities that already exist in the Pulse mobile shell. -->

- ✓ `/mobile` route shell with sticky header and bottom nav — existing
- ✓ `/mobile/dashboard`, `/mobile/tickets`, `/mobile/tickets/[id]`,
  `/mobile/finance` routes — existing
- ✓ Standalone `/mobile/nav` page (to be replaced by drawer) — existing
- ✓ Authentication, sync workers, analyzer pipeline, Engagement data sources —
  existing in desktop app and reused here
- ✓ PWA scaffolding (manifest, viewport-fit=cover, `pt-safe`/`pb-safe` utilities)
  — Validated in Phase 1: PWA Scaffolding (PWA-01..04)
- ✓ New `/mobile/layout.tsx` shell — sticky header (WulfMark + Bell placeholder
  + avatar), 5-cell bottom nav (Dashboard/Tickets/Finance/Analyzer + More),
  shared MoreDrawer Sheet — Validated in Phase 2: Mobile Shell + More Drawer
  (SHELL-01..06, NAV-01..03)
- ✓ More drawer (right Sheet) replaces `/mobile/nav` — Mobile sections, Full
  site links (with `ExternalLink` hint), Account + Sign out — Validated in
  Phase 2: Mobile Shell + More Drawer (DRAWER-01..06)
- ✓ Dashboard restyle — 2×2 KPI grid, Needs Attention strip (horizontal scroll),
  worker/backup status row, no charts. Backed by single `/api/mobile/dashboard`
  endpoint returning `kpis`/`needsAttention`/`workers`. Validated in Phase 3:
  Dashboard Restyle (DASH-01..04)
- ✓ Tickets restyle — collapsible URL-synced filter strip, priority-stripe rows,
  cursor-based infinite scroll (~25/page) via IntersectionObserver, Load more
  fallback, detail page header reskinned. Backed by `/api/mobile/tickets` with
  cursor pagination + exported `MobileTicket` / `MobileTicketListResponse` types.
  Validated in Phase 4: Tickets Restyle (TICK-01..07)
- ✓ Finance restyle — `/mobile/finance` adopts new Card + typography scale, KPI
  2×2 grid, 3-cell aging row, Collapsible invoice/payment lists with FinanceRow,
  stacked Top Customers + monthly revenue (no chart). API/data shape unchanged.
  Validated in Phase 5: Finance Restyle (FIN-01..02)
- ✓ Analyzer feed (NEW) — `/mobile/analyzer` read-only most-recent-first stream
  of completed AI ticket analyses, with cursor-based infinite scroll
  (~25/page), `kiosk_settings` company scoping, and a phone-friendly summary
  view at `/mobile/analyzer/[id]` (Summary / Next Step / Next Step Rationale +
  "View full analysis" link to desktop). New endpoint
  `/api/mobile/analyzer/feed`; existing `/api/analyzer/analyses/[id]` reused
  for detail. No editing, no re-run, no prompt-tuning on mobile. Validated in
  Phase 6: Analyzer Feed (ANL-01..06)
- ✓ Engagement overview (NEW) — `/mobile/engagement` phone-first refactor
  reachable from the More drawer (NOT the bottom bar). Sticky 3-chip period
  selector (7d/30d/90d), 4 stacked summary cards (active users / total Graph
  hours / total Autotask hours / hours-per-active-user), single-series SVG
  sparkline (no recharts), sortable per-employee list (Hours/Name/Utilization)
  with client-side search and IntersectionObserver infinite scroll. Two new
  endpoints: `/api/mobile/engagement/summary` (totals) and
  `/api/mobile/engagement/trend` (daily-hours time-series). Existing
  `/api/engagement/users` reused as-is. Read-only on mobile. Validated in
  Phase 7: Engagement Overview (ENG-01..05, ENG-09)
- ✓ Per-user IANA timezone — `user.timezone` column (default `'UTC'`),
  `session.user.timezone` exposed via Better Auth `additionalFields`,
  `GET`/`PUT /api/me/timezone` (IANA-validated; allowlist includes `UTC`/`Etc/UTC`/`GMT`),
  shared `getUserTimezone(session)` server helper, shared `useUserTimezone()`
  client hook (single source of truth — 52 client files migrated), all 6
  affected read paths (`/api/dashboard/{overview,trends}`,
  `/api/mobile/{dashboard,finance,engagement/summary,engagement/trend}`)
  switched to `(value AT TIME ZONE 'UTC') AT TIME ZONE $tz` day-boundary math.
  `/api/mobile/finance` hardened with `requireAuth()`. Storage UTC unchanged.
  Engagement `_snapshots`-derived metrics keep UTC bucketing (documented
  carve-out). Validated in Phase 7.1: User Timezone Fix (TZ-01..04)
- ✓ Engagement user profile (NEW) — `/mobile/engagement/[userId]` real-page
  profile replacing the desktop modal pattern. Server-side photo proxy at
  `/api/mobile/engagement/user/[userId]/photo` (`MsGraphClient.getUserPhotoBytes()`,
  `Cache-Control: private, max-age=3600`, neutral 404/502/503). Client page
  composes six new components — Skeleton, Header (photo with onError →
  initials fallback), MetricGrid (2×2), Breakdown (Time/Communication/
  Meetings with conditional Zoom + after-hours), RecentEntries, RecentMeetings.
  Reuses `/api/engagement/user/[userId]` verbatim. Period chips (D7/D30/D90)
  refetch on change. Scroll restoration on back is partial (sessionStorage
  shim added but does not restore reliably; accepted as a known limitation).
  Validated in Phase 8: Engagement User Profile (ENG-06..08)
- ✓ User profile & preferences (NEW) — `/mobile/profile` page with four
  sections: Timezone (Combobox + 400ms debounce + live clock), Theme
  (light/dark/system radio rows persisted to `user.theme` with cross-device
  sync via `ThemeSessionBridge` + desktop `ThemeToggle` write-through),
  Notifications matrix (event_key × channel_type, opt-in default), and
  Channels (personal Teams webhook + ntfy topic with QR code, per-channel
  test-send, inline 400-error display). Three new migrations: 084
  (`user.theme`), 085 (`notification_channels.owner_user_id` + partial unique
  index), 086 (`notify_event_keys` + `user_event_subscriptions`). API surface
  at `/api/me/{theme,channels,channels/[type],channels/[type]/test,notification-subscriptions}`
  all session-scoped via `requireAuth()`; shared `lib/services/personal-channels.ts`
  centralizes validation, ntfy topic minting, and test-send. Notify pipeline
  step extended with `routing: explicit | event-key | hybrid` and 5 fallback
  reasons (`user_muted`, `event_key_unknown`, `recipient_unresolvable`,
  `channel_missing`, `channel_send_failed`); fully backward-compatible with
  existing global notify configs. Admin surfaces: channels page Owner column
  + filter, event-keys CRUD at `/admin/workflow/event-keys`, executions
  browser at `/admin/workflow/executions` with "Show only fallbacks" filter.
  Validated in Phase 9: User Profile & Preferences (PROF-01..04, TZ-CHOOSER-01..02,
  THEME-01..05, CHAN-01..07, SUB-01..04, ROUTE-01..07)
- ✓ PAX8 client + factory (OAuth2 client-credentials, `lib/services/pax8-client.ts`
  + `lib/services/pax8-factory.ts` + `lib/types/pax8.ts`) and the PAX8 schema
  foundation (`migrations/091_pax8_tables.sql` — companies, products,
  subscriptions, orders, order_items, company_match_review). Live auth-proof
  confirmed against the real PAX8 API (`scripts/verify-pax8-auth.ts`).
  Validated in Phase 10: PAX8 Client & Auth Foundation (PAX8-01, PAX8-02)
- ✓ Company, catalog & subscription sync (NEW) — `Pax8SyncService.fullSync`
  (`lib/services/pax8-sync-service.ts`) reads companies/subscriptions/products
  from PAX8 (read-only pagination helpers on `Pax8Client`) and upserts into
  Postgres with dual customer-price/partner-cost columns
  (`migrations/092_pax8_subscription_costs.sql`), a referenced-only readable
  product catalog (name + category via join, not bare SKUs), and soft-delete
  reconciliation on every run. Fire-and-forget trigger at session-gated
  `POST /api/pax8/sync` (409 on concurrent run) with `GET` status/counts.
  Live-verified against the real PAX8 API: 118 companies, 445 subscriptions
  all carrying cost data, 0 unreferenced products, stable across re-run.
  Validated in Phase 11: Company, Catalog & Subscription Sync
  (PAX8-03, PAX8-04, PAX8-05, PAX8-08)
- ✓ Historical PAX8 invoice/order-item costs synced per company with billing
  periods (`pax8_order_items`: 29,311 rows across ~56k live history),
  fuzzy-name Autotask company matching via pg_trgm (0.90 auto-link threshold,
  80 confident auto-matches), no-match/ambiguous companies flagged in
  `pax8_company_match_review` instead of silently guessed (38 unresolved:
  16 no-match, 22 ambiguous), idempotent across re-syncs (resolved matches
  never overwritten). Validated in Phase 12: Orders/Invoices & Company
  Matching (PAX8-06, PAX8-10, PAX8-11)
- ✓ Daily scheduled sync + admin disable toggle — idempotent `pax8-daily`
  cron row (`migrations/096_pax8_daily_schedule.sql`, disabled by default),
  dual-guarded scheduler branch (`lib/services/sync-scheduler.ts`) that
  skips `Pax8SyncService.fullSync('scheduled')` when PAX8 is unconfigured
  or disabled via `integration_settings`, PAX8 registered as a toggleable
  `/admin/integrations` row (`checkConfigOnly` in `integration-health.ts`),
  and a 403 disabled-gate on `POST /api/pax8/sync`. Live-verified: scheduled
  tick fires and runs the full sync (30,385 rows upserted), disable causes a
  skip log with no new sync_history row, re-enable resumes on the next tick
  with no restart. Admin-UI row rendering and the manual-route 403/200 still
  need a human browser session (tracked in `13-HUMAN-UAT.md`). Validated in
  Phase 13: Scheduler & Admin Toggle (PAX8-07, PAX8-09)

### Active

<!-- Hypotheses for this milestone — see .planning/seeds/SEED-002-pax8-integration.md -->

- New `/pax8` page — v2.0 PAX8 Integration milestone (see above); client/auth/
  schema foundation (Phase 10), current-state sync (Phase 11), historical
  orders/invoices + company matching (Phase 12), and scheduler/admin toggle
  (Phase 13) now validated above

### Out of Scope

<!-- From spec §7. Explicit boundaries to prevent scope creep. -->

- PAX8 write access (seat adjustments, placing orders) — read-only in v2.0;
  revisit only if reconciliation surfaces a concrete need to act, not just view
- General natural-language data assistant / chatbot over Pulse data — separate
  future milestone (SEED-003), deliberately not bundled with the PAX8 data sync
- Service worker / offline cache / push notifications — deferred until a clear
  offline use-case lands
- Tablet breakpoint (`md:max-w-2xl`) — noted as follow-up, keep `max-w-lg`
- Real notification list behind the Bell icon — placeholder only this iteration
- Mobile editing on Engagement (user detail) or Analyzer (re-run, prompt edits)
  — read-only on mobile by design
- Charts / recharts on the mobile Dashboard — not earning their weight on
  small widths
- Restyling or replacing the desktop pages reachable from the More drawer —
  desktop pages stay as they are

## Context

- **Brownfield project.** Pulse is a Next.js 16 + React 19 PSA dashboard for
  Wulf Consulting. Existing codebase fully mapped at `.planning/codebase/*.md`.
  See `CLAUDE.md`, `ARCHITECTURE.md`, `DESIGN.md` in repo root.
- **Audience.** Wulf Consulting managers using Pulse on iOS/Android during the
  workday — the shell is for status checks and triage, not full editing.
- **Mobile is not a replacement for desktop.** Pages where mobile editing
  isn't justified link out via the More drawer with an `ExternalLink` hint.
- **Source of truth for this work.** `docs/superpowers/specs/2026-05-03-mobile-shell-design.md`
  — 8-section spec with explicit build order. Phases should follow it.
- **Existing mobile shell is small.** Current `app/mobile/layout.tsx` has 3
  bottom-nav tabs and a `/mobile/nav` page. This redesign rebuilds it in
  place — no `/mobile-v2`, no parallel routes.
- **Engagement and Analyzer pages on desktop are large** (~1300 + ~650 lines
  for engagement; analyzer pipeline already has a desktop UI). Mobile
  surfaces reuse the data sources but build phone-first layouts from scratch.

## Constraints

- **Tech stack**: Next.js 16 App Router, React 19, Tailwind 4, shadcn/ui — match
  existing Pulse conventions (no new state libraries, no SWR/react-query, no
  ORM, no Zod in API routes unless required)
- **Routes**: Keep all existing `/mobile/*` paths. Replace files in place. No
  new top-level routes outside `/mobile/`.
- **Build order**: Each spec step ships independently — no big-bang merge.
  Phase boundaries should let each step land on `master` cleanly.
- **No service worker / no offline**: don't introduce `next-pwa` or a custom
  SW in this iteration.
- **Auth**: existing Better Auth + middleware handles `/mobile/*`. No new
  auth surface.
- **Data sources**: reuse existing endpoints where possible; add
  `/api/mobile/*` only when an existing list endpoint doesn't return the
  needed shape (e.g., analyzer feed).

## Key Decisions

<!-- Decisions that constrain future work. -->

| Decision | Rationale | Outcome |
|----------|-----------|---------|
| Rebuild `/mobile` in place (no `/mobile-v2`) | Spec §2 — keep canonical URLs, avoid parallel maintenance burden | — Pending |
| Bottom nav = 4 tabs + More (5 cells), Engagement in More | Spec §3.1, §6.5 — managers don't check Engagement as often as the four primary surfaces | — Pending |
| Ship each spec step as its own phase | Spec §8 — independent ship reduces merge risk and keeps reviews focused | — Pending |
| Bell icon is a placeholder only | Spec §5.1 — real notification list deferred; keep keyboard-accessible button so future phase can wire it | — Pending |
| No service worker in this iteration | Spec §4 — defer until a clear offline use-case lands | — Pending |
| Engagement mobile is a real refactor, not a thin adaptation | Spec §6.5 — desktop's wide tables and modals don't translate; build phone-first from same data sources | — Pending |
| Mobile user-detail is a page, not a modal | Spec §6.5 — back gesture needs real navigation history | — Pending |

## Evolution

This document evolves at phase transitions and milestone boundaries.

**After each phase transition** (via `/gsd-transition`):
1. Requirements invalidated? → Move to Out of Scope with reason
2. Requirements validated? → Move to Validated with phase reference
3. New requirements emerged? → Add to Active
4. Decisions to log? → Add to Key Decisions
5. "What This Is" still accurate? → Update if drifted

**After each milestone** (via `/gsd-complete-milestone`):
1. Full review of all sections
2. Core Value check — still the right priority?
3. Audit Out of Scope — reasons still valid?
4. Update Context with current state

---
*Last updated: 2026-07-11 — Phase 13: Scheduler & Admin Toggle complete*
