429 lines
28 KiB
Markdown
429 lines
28 KiB
Markdown
# 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)*
|