wulf-pulse/.planning/PROJECT.md

429 lines
28 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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)*