28 KiB
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
.emlevidence extraction — prefersrfc.eml(original reported message) overOriginatingEmail.eml(wrapper), case-insensitive name +message/rfc822content-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
unavailablewhen 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,
.emlselection 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
- ✓
/mobileroute shell with sticky header and bottom nav — existing - ✓
/mobile/dashboard,/mobile/tickets,/mobile/tickets/[id],/mobile/financeroutes — existing - ✓ Standalone
/mobile/navpage (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-safeutilities) — Validated in Phase 1: PWA Scaffolding (PWA-01..04) - ✓ New
/mobile/layout.tsxshell — 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 (withExternalLinkhint), 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/dashboardendpoint returningkpis/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/ticketswith cursor pagination + exportedMobileTicket/MobileTicketListResponsetypes. Validated in Phase 4: Tickets Restyle (TICK-01..07) - ✓ Finance restyle —
/mobile/financeadopts 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/analyzerread-only most-recent-first stream of completed AI ticket analyses, with cursor-based infinite scroll (~25/page),kiosk_settingscompany 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/engagementphone-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/usersreused as-is. Read-only on mobile. Validated in Phase 7: Engagement Overview (ENG-01..05, ENG-09) - ✓ Per-user IANA timezone —
user.timezonecolumn (default'UTC'),session.user.timezoneexposed via Better AuthadditionalFields,GET/PUT /api/me/timezone(IANA-validated; allowlist includesUTC/Etc/UTC/GMT), sharedgetUserTimezone(session)server helper, shareduseUserTimezone()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 $tzday-boundary math./api/mobile/financehardened withrequireAuth(). 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/profilepage with four sections: Timezone (Combobox + 400ms debounce + live clock), Theme (light/dark/system radio rows persisted touser.themewith cross-device sync viaThemeSessionBridge+ desktopThemeTogglewrite-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 viarequireAuth(); sharedlib/services/personal-channels.tscentralizes validation, ntfy topic minting, and test-send. Notify pipeline step extended withrouting: explicit | event-key | hybridand 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/executionswith "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)
- filter, event-keys CRUD at
- ✓ PAX8 client + factory (OAuth2 client-credentials,
lib/services/pax8-client.tslib/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 onPax8Client) 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-gatedPOST /api/pax8/sync(409 on concurrent run) withGETstatus/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 inpax8_company_match_reviewinstead 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-dailycron row (migrations/096_pax8_daily_schedule.sql, disabled by default), dual-guarded scheduler branch (lib/services/sync-scheduler.ts) that skipsPax8SyncService.fullSync('scheduled')when PAX8 is unconfigured or disabled viaintegration_settings, PAX8 registered as a toggleable/admin/integrationsrow (checkConfigOnlyinintegration-health.ts), and a 403 disabled-gate onPOST /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 in13-HUMAN-UAT.md). Validated in Phase 13: Scheduler & Admin Toggle (PAX8-07, PAX8-09) - ✓
/pax8UI surface — Companies tab (DataTableof 118 PAX8 companies, sortable/searchable) with a cost-breakdown drill-down (DetailModalextended with akind='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_atset) 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—reportsfully designed, the rest stubbed for later phases) and 098 (disabled-by-defaultphishing-sweepschedule seed). Sharedlib/services/phishing-detector.tscore: 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=falsefiltered, never base64 attachment content). Wired into both established scan triggers: fire-and-forget hook on theticket.createdwebhook (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 aspax8-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.emlattachment 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 (onemessagesrow + per-indicatorindicatorsrows). Migration 099 (indicators.metadataJSONB). Validated in Phase 16: EML/MIME Evidence Parser (EVID-02, EVID-03, EVID-04) - ✓ Mimecast blast-radius lookup (NEW) —
getBlastRadius()composessearchDeliveredMessages/getHeldMessages/getThreatEventsinto normalized matched/delivered/held/rejected/clicked counts + per-recipient status, gated byisMimecastConfigured(), Redis-cached 5 min, never throws — degrades tostatus: '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) —
groupReportIntoCampaigncalled automatically from both the webhookticket.createdpath and the cron sweep so campaigns accumulate without any API call;POST /api/phishing/tickets/{ticket_id}/analyzefor on-demand detect→parse→group;GET /api/phishing/campaigns(paginated list) andGET /api/phishing/campaigns/{id}(nested detail), bothrequirePermission('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-onlyclassificationsinsert. On-demand reclassify viaPOST /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, see22-VERIFICATION.md'shuman_verificationlist before treating the UI as fully signed off in a fresh deployment - ✓ Classification disposition + per-client automation gate (NEW) — a 4th
USER_AWARENESSverdict for confirmed phishing-simulation-vendor reports (previously forced into generic UNWANTED) mapping to a non-destructiveacknowledge_useraction that posts a customer-visible thank-you note (AutotasknoteType: 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-completedAtbug, both fixed via an idempotent, audit-persistingautoPostAcknowledgment()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
None yet — run /gsd:new-milestone to define the next milestone's requirements.
Out of Scope
- 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, keepmax-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. SeeCLAUDE.md,ARCHITECTURE.md,DESIGN.mdin 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
ExternalLinkhint. - 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.tsxhas 3 bottom-nav tabs and a/mobile/navpage. 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/phishingand/admin/phishing-automationUI surfaces; new services underlib/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'shuman_verificationlist. 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
mastercleanly. - No service worker / no offline: don't introduce
next-pwaor 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
| 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):
- Requirements invalidated? → Move to Out of Scope with reason
- Requirements validated? → Move to Validated with phase reference
- New requirements emerged? → Add to Active
- Decisions to log? → Add to Key Decisions
- "What This Is" still accurate? → Update if drifted
After each milestone (via /gsd-complete-milestone):
- Full review of all sections
- Core Value check — still the right priority?
- Audit Out of Scope — reasons still valid?
- Update Context with current state
Last updated: 2026-07-17 — v3.0 Phishing Triage Automation milestone shipped (Phases 15-23)