# 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: v3.0 Phishing Triage Automation

**Status:** ✅ Shipped 2026-07-17 (9/9 phases, 30/30 plans, all 38 v3.0 requirements Complete)

**Goal:** Detect candidate phishing/spam report tickets in Autotask, extract
and parse original-message evidence, classify each as `SPAM` / `UNWANTED` /
`THREAT`, group duplicate reports into campaigns, and prepare (never
auto-execute) remediation actions behind an explicit human-approval gate.

**Target features:**
- Ticket detector/scanner matching known phishing/spam-report patterns
  (title/body keyword + Microsoft/KnowBe4 report signatures), idempotent
  reprocessing
- `.eml` evidence extraction — prefers `rfc.eml` (original reported message)
  over `OriginatingEmail.eml` (wrapper), case-insensitive name + `message/rfc822`
  content-type matching
- RFC822/MIME parser producing normalized headers (From/Reply-To/Return-Path/
  To/Cc/Subject/Date/Message-ID/Received chain/auth results), URLs, and
  attachment metadata (name/type/size/hash) — no URL detonation
- Campaign grouping/dedupe keyed on Message-ID, then attachment-hash/URL-domain
  + subject + sender + time-window, then sender + normalized subject + client
  + time-window
- Durable data model: campaigns, reports, messages, indicators, classifications,
  remediation_actions, audit_events
- Mimecast blast-radius lookup abstraction (interface + normalized output),
  gracefully `unavailable` when unconfigured — classification still proceeds
  on ticket/email evidence alone
- Classifier service: deterministic rule layer + an LLM-abstraction plug point
  (safe rule-based fallback if no LLM abstraction fits); structured evidence
  in, not raw unbounded email; KnowBe4 simulations not auto-flagged as THREAT
- API surface under `/api/phishing/*`: list/get campaigns, analyze a ticket,
  classify/approve/remediate/mark-false-positive a campaign
- Optional Autotask internal triage note (only if a safe note-write path
  already exists; otherwise return note text via API)
- Remediation safety: destructive actions (purge/delete/block/reset) always
  `requires_approval: true`, logged with approver/timestamp/params/result,
  idempotent on re-run, fail closed if approval/config missing
- Tests: detection patterns, `.eml` selection logic, MIME parsing, campaign
  dedupe, rule classification (SPAM/UNWANTED/THREAT), approval-required
  safety, idempotent reprocessing, API happy-path + permission failures —
  synthetic fixtures only, no real customer email

Unrelated in domain to the v2.0 PAX8 Integration above — this milestone adds a
new security/triage backend feature (Autotask ticket + email evidence, not
subscription billing). No seeds matched this scope; introduced directly from
a detailed spec provided at milestone-start.

**Non-goals:** no automatic tenant-wide purge, no automatic password
reset/session revocation, no URL sandbox/detonation, no fully automated ticket
closure, no assumption that Graph is the eventual purge mechanism (Defender/
Exchange purge may be preferable later).

## 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)
- ✓ `/pax8` UI surface — Companies tab (`DataTable` of 118 PAX8 companies,
  sortable/searchable) with a cost-breakdown drill-down (`DetailModal`
  extended with a `kind='pax8_company'` field group + subscriptions table,
  correctly windowed per-subscription latest billed line — not a single
  global max), and a Needs Review tab (amber-card queue of the 38 unresolved
  matches from Phase 12, candidate-button + manual-search resolve, admin-gated
  write). Live-verified end-to-end: 10 reviews resolved via both resolve
  paths, persisted (`match_method='manual'`, `resolved_at` set) and confirmed
  to survive a real subsequent PAX8 sync untouched; non-admin resolve attempt
  correctly rejected with 403. One critical bug (bigint/Zod mismatch breaking
  the manual-search path) and one unrelated pre-existing app-wide bug
  (`hasPermission()` role-name shadowing, crashing every non-admin permission
  check with a 500 instead of 403) were found and fixed during verification.
  Validated in Phase 14: /pax8 UI Surface (PAX8-12, PAX8-13, PAX8-14)
- ✓ Phishing-triage data model + ticket detection + base evidence — new
  migrations 097 (7-table schema: `campaigns`, `reports`, `messages`,
  `indicators`, `classifications`, `remediation_actions`, `audit_events` —
  `reports` fully designed, the rest stubbed for later phases) and 098
  (disabled-by-default `phishing-sweep` schedule seed). Shared
  `lib/services/phishing-detector.ts` core: plain `.includes()` pattern
  matcher (no regex/ReDoS surface) against the 8 locked title/description
  signatures, content-hash idempotency scoped to title+description only (so
  status/assignee churn never triggers reprocessing), and EVID-01 evidence
  capture (company, requester/reporter, notes, time entries, attachment
  metadata — `is_deleted=false` filtered, never base64 attachment content).
  Wired into both established scan triggers: fire-and-forget hook on the
  `ticket.created` webhook (near-real-time) and a bounded (500-row, 7-day
  window) daily cron sweep for reconciliation. A code-review pass caught and
  fixed a critical bug pre-ship: the webhook path branched on an Autotask
  payload field that is never actually populated, so it silently never
  detected anything — fixed by reading the ticket back from Postgres instead
  (verified independently by phase verification, not just the fix commit).
  Forward-only per this milestone's design (no backlog backfill); the
  reconciliation sweep is disabled by default pending an admin opt-in, same
  convention as `pax8-daily`. Validated in Phase 15: Data Model, Detection &
  Ticket Evidence (DETECT-01, DETECT-02, EVID-01)
- ✓ EML/MIME evidence parser (NEW) — pure, I/O-free `lib/services/eml-parser.ts`
  (`mailparser` + `linkify-it`): three-tier `.eml` attachment selection,
  RFC822/MIME header normalization with hand-rolled SPF/DKIM/DMARC verdicts,
  deduped URL extraction, sanitized/truncated body preview — test-enforced to
  never trigger a network call. `AutotaskClient.getAttachmentContent()`
  fetches the raw bytes; `parseAndStoreMessage()` orchestrates
  list→select→fetch→size-guard→optional-B2-store→parse→persist (one
  `messages` row + per-indicator `indicators` rows). Migration 099
  (`indicators.metadata` JSONB). Validated in Phase 16: EML/MIME Evidence
  Parser (EVID-02, EVID-03, EVID-04)
- ✓ Mimecast blast-radius lookup (NEW) — `getBlastRadius()` composes
  `searchDeliveredMessages`/`getHeldMessages`/`getThreatEvents` into normalized
  matched/delivered/held/rejected/clicked counts + per-recipient status,
  gated by `isMimecastConfigured()`, Redis-cached 5 min, never throws —
  degrades to `status: 'unavailable'` on missing config or lookup failure.
  Validated in Phase 17: Mimecast Blast Radius Lookup (BLAST-01, BLAST-02)
- ✓ Campaign grouping & phishing analysis API (NEW) — `groupReportIntoCampaign`
  called automatically from both the webhook `ticket.created` path and the
  cron sweep so campaigns accumulate without any API call; `POST
  /api/phishing/tickets/{ticket_id}/analyze` for on-demand
  detect→parse→group; `GET /api/phishing/campaigns` (paginated list) and
  `GET /api/phishing/campaigns/{id}` (nested detail), both
  `requirePermission('phishing','read')`-gated. Two report_count/duplicate-
  campaign bugs (CR-02, CR-03) caught by code review and fixed pre-ship,
  proven by regression tests + a live-database re-verification. Validated in
  Phase 18: Campaign Grouping & Phishing Analysis API (CAMP-01, CAMP-02,
  CAMP-03, DETECT-03, ACCESS-01)
- ✓ Classification engine (NEW) — deterministic, zero-LLM
  `lib/services/campaign-classifier.ts`: SPAM/UNWANTED/THREAT verdict,
  KnowBe4/Breach-Secure-Now simulation sender-domain allowlist (exact-or-
  subdomain match only, no substring spoofing), THREAT gate requiring
  delivery + malicious signal, confidence scoring, append-only
  `classifications` insert. On-demand reclassify via `POST
  /api/phishing/campaigns/{id}/classify`. Validated in Phase 19:
  Classification Engine (CLASSIFY-01 through CLASSIFY-06)
- ✓ Remediation, approval & audit safety (NEW) — transactional
  approve/remediate/mark-false-positive service layer, idempotent re-run
  (REMED-04), single append-only audit writer for every state change
  (REMED-06); all 7 action types (quarantine/block/purge/warn/reset/isolate/
  disable-forwarding) remain simulated status-only transitions this
  milestone — no real destructive execution. Validated in Phase 20:
  Remediation, Approval & Audit Safety (REMED-01 through REMED-06)
- ✓ Autotask triage note (NEW) — sanitized formatter (URL query/fragment
  stripping, credential redaction) + `generateAndPostTriageNote(campaignId)`
  posting one internal Autotask note per linked ticket, independent
  per-ticket failure isolation. Validated in Phase 21: Autotask Triage Note
  (NOTE-01)
- ✓ Approval UI / LiveLink (NEW) — ticket-ID-addressable
  `/phishing/tickets/{ticketId}` review page (numeric Autotask ticket id,
  not internal campaign UUID — confirmed live in production as a real
  LiveLink target), composing ClassificationCard/ActionAreaCard/
  EvidenceCard/TimelineCard behind the existing Better Auth session only.
  Evidence card renders parsed EML headers/URLs/attachments/body preview and
  Mimecast blast-radius with zero clickable-link surface and zero raw-HTML
  rendering of attacker-controlled content. Client-side permission gating
  verified to match server-side 1:1. Validated in Phase 22: Approval UI
  (LiveLink) (REVIEW-01 through REVIEW-06) — first formal verification pass
  for this phase was run retroactively at v3.0 close (2026-07-17); 5 manual
  browser click-through checks remain outstanding, see
  `22-VERIFICATION.md`'s `human_verification` list before treating the UI as
  fully signed off in a fresh deployment
- ✓ Classification disposition + per-client automation gate (NEW) — a 4th
  `USER_AWARENESS` verdict for confirmed phishing-simulation-vendor reports
  (previously forced into generic UNWANTED) mapping to a non-destructive
  `acknowledge_user` action that posts a customer-visible thank-you note
  (Autotask `noteType: 18`); a per-Autotask-company opt-in automation gate
  (`auto_parse`/`auto_classify`/`auto_report`, all-off default) with an admin
  UI (`/admin/phishing-automation`) gating the webhook's auto pipeline —
  every other verdict/action still requires manual approval regardless of
  gate state. A post-ship code-review pass caught a duplicate-note bug
  (repeat webhooks re-posting the same acknowledgment as a campaign
  accumulated more reports) and a stale-`completedAt` bug, both fixed via an
  idempotent, audit-persisting `autoPostAcknowledgment()` wrapper before
  milestone close. Validated in Phase 23: Classification Disposition +
  Per-Client Automation Gate (CLASSDISP-01 through CLASSDISP-03,
  AUTOGATE-01 through AUTOGATE-03)

### Active

<!-- Hypotheses for v3.0 all validated above — nothing active pending next milestone -->

None yet — run `/gsd:new-milestone` to define the next milestone's requirements.

### 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.
- **v3.0 codebase footprint (shipped 2026-07-17):** new phishing-triage schema
  spanning migrations 097-100 (`campaigns`, `reports`, `messages`,
  `indicators`, `classifications`, `remediation_actions`, `audit_events`,
  `phishing_automation_gate`); new `/api/phishing/*` and
  `/api/admin/phishing-automation*` route surface; new `/phishing` and
  `/admin/phishing-automation` UI surfaces; new services under
  `lib/services/` (`phishing-detector`, `eml-parser`, `phishing-eml-service`,
  `mimecast-blast-radius`, `campaign-grouping-service`, `campaign-classifier`,
  `remediation-service`, `triage-note-service`/`triage-note-format`,
  `phishing-audit`, `phishing-timeline`, `phishing-ticket-resolver`,
  `phishing-automation-gate`). ~33K LOC inserted, 189 files touched, 30 plans
  across 9 phases, 2026-07-15 → 2026-07-17.
- **Known open item carried into next milestone:** Phase 22's UI was never
  through a human browser click-through pass (only a retroactive code-level
  verification at v3.0 close) — see `22-VERIFICATION.md`'s
  `human_verification` list. Not blocking, but worth closing before this UI
  is treated as fully hardened.

## 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 |
| Zero-LLM deterministic classifier (rule-based, not an LLM abstraction) | Structured, size-bounded evidence in → deterministic verdict out; avoids prompt-injection surface from attacker-controlled email content reaching an LLM | ✓ Good — shipped in Phase 19, no LLM call anywhere in the classification path |
| `acknowledge_user` is the one narrow carve-out from the "all destructive actions require approval" rule | It's a non-destructive thank-you note, not a security action; the automation-gate feature (Phase 23) has nothing to automate without this carve-out | ✓ Good — scoped exclusively to `USER_AWARENESS` verdicts; every other action (7 types) stays manual-approval-gated |
| Per-company automation gate is 3 independent opt-in booleans (parse/classify/report), not one master switch, defaulting all-off | Mirrors Phase 20's proposed-only-by-default safety posture — a client only gets automatic pipeline execution once an admin deliberately opts them in | ✓ Good — shipped in Phase 23, admin UI at `/admin/phishing-automation` |
| KnowBe4/Breach-Secure-Now simulation allowlist is a TypeScript constant, not a DB table | Every rule including the allowlist should be unit-tested pure code, not a runtime-editable table that could silently drift | ✓ Good — `KNOWN_SIMULATION_SENDERS` in `campaign-classifier.ts`, exact-domain-or-subdomain match only (no substring spoofing) |
| Phase 22 shipped without ever running `/gsd:verify-work` | Process gap discovered only at v3.0 milestone close, not during Phase 22 itself | ⚠️ Revisit — retroactive verification found the code correct (6/6 requirements), but 5 human browser click-through checks are still outstanding; run them before treating this UI as fully hardened |

## 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-17 — v3.0 Phishing Triage Automation milestone shipped (Phases 15-23)*
