chore: archive v3.0 milestone files

This commit is contained in:
lorentz 2026-07-17 06:56:49 -04:00
parent 8b04be160f
commit e9101b0d44
6 changed files with 1130 additions and 63 deletions

18
.planning/MILESTONES.md Normal file
View file

@ -0,0 +1,18 @@
# Milestones
## v3.0 Phishing Triage Automation (Shipped: 2026-07-17)
**Phases completed:** 9 phases, 30 plans, 69 tasks
**Key accomplishments:**
- Full phishing-triage data pipeline: durable schema (7 tables, migrations 097-100), idempotent Autotask ticket detection (webhook + daily cron sweep), and a pure RFC822/MIME `.eml` parser producing normalized headers, SPF/DKIM/DMARC verdicts, deduped URLs, and a sanitized body preview — no network calls, no execution of anything found in a message
- Mimecast blast-radius lookup abstraction and a deterministic, zero-LLM SPAM/UNWANTED/THREAT classifier — including a KnowBe4/Breach-Secure-Now simulation allowlist so routine security-awareness tests never cry wolf as THREAT
- Full remediation safety layer: proposed-only actions, permission-gated approve/remediate/mark-false-positive APIs, idempotent re-run, and a single append-only audit trail for every state change — nothing destructive ever auto-executes
- Ticket-ID-addressable Approval UI (`/phishing/tickets/{ticketId}`), a real Autotask LiveLink target confirmed live in production, showing timeline/evidence/classification with approve/remediate/mark-false-positive wired directly to the API layer
- Classification Disposition + Per-Client Automation Gate (Phase 23): a dedicated `USER_AWARENESS` verdict + non-destructive customer-visible acknowledgment note, plus a per-company opt-in automation gate letting an admin choose automatic vs. manual pipeline execution — including a post-ship idempotency fix (closing a duplicate-note bug caught by code review) before milestone close
- Two real code-review-caught defects fixed pre-close during this milestone: a duplicate-campaign bug in Phase 18's grouping service, and the Phase 23 duplicate-acknowledgment-note bug — both proven via regression tests and live-database re-verification, not just a fix commit
**Process note:** Phase 22 (Approval UI) shipped all 6 plans without ever running through formal verification — caught only at this milestone's close. A retroactive verification pass confirmed the code is correct (6/6 requirements), but 5 manual browser click-through checks remain outstanding; see `22-VERIFICATION.md`.
---

View file

@ -17,6 +17,8 @@ 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
@ -233,20 +235,88 @@ Exchange purge may be preferable later).
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 this milestone — see .planning/REQUIREMENTS.md for the full v3.0 requirement list -->
<!-- Hypotheses for v3.0 all validated above — nothing active pending next milestone -->
- `.eml` evidence (when present) can be parsed into normalized, actionable
indicators without executing/fetching anything suspicious
- Duplicate reports of the same campaign can be grouped via a small set of
stable keys (Message-ID → indicator/subject/sender/window → sender/subject/
client/window)
- A rule-based classifier (with a clean LLM plug-point) can produce a useful
SPAM/UNWANTED/THREAT verdict from structured evidence alone
- Remediation can be modeled and gated behind approval without ever executing
destructively in this milestone
None yet — run `/gsd:new-milestone` to define the next milestone's requirements.
### Out of Scope
@ -284,6 +354,23 @@ Exchange purge may be preferable later).
- **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
@ -315,6 +402,11 @@ Exchange purge may be preferable later).
| 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
@ -334,4 +426,4 @@ This document evolves at phase transitions and milestone boundaries.
4. Update Context with current state
---
*Last updated: 2026-07-15 — Phase 15: Data Model, Detection & Ticket Evidence complete*
*Last updated: 2026-07-17 — v3.0 Phishing Triage Automation milestone shipped (Phases 15-23)*

View file

@ -4,7 +4,7 @@
- ✅ **v1.0 Mobile Shell Redesign** — Phases 1-9.1 (shipped 2026-07-10)
- ✅ **v2.0 PAX8 Integration** — Phases 10-14 (shipped 2026-07-12)
- 🚧 **v3.0 Phishing Triage Automation** — Phases 15-21 (in progress)
- **v3.0 Phishing Triage Automation** — Phases 15-23 (shipped 2026-07-17)
## Phases
@ -410,14 +410,15 @@ render, including the manual-resolution workflow for flagged companies.
</details>
### 🚧 v3.0 Phishing Triage Automation (In Progress)
<details>
<summary>✅ v3.0 Phishing Triage Automation (Phases 15-23) - SHIPPED 2026-07-17</summary>
**Milestone 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.
Eight phases follow the domain's natural dependency chain rather than a generic
Nine phases follow the domain's natural dependency chain rather than a generic
foundation→features→polish template. Phase 15 lands the durable data model
(campaigns/reports/messages/indicators/classifications/remediation_actions/
audit_events, migration 097+) together with ticket detection and basic ticket-level
@ -441,7 +442,10 @@ depends on the same Phase 19/20 outputs as Phase 21 but is otherwise independent
it — a LiveLink button in Autotask is a separate configuration surface from the
triage note's content, so Phase 22 does not need Phase 21 to land first; it is
sequenced last only because it is the newest addition to this milestone, not because
of a functional dependency on Phase 21.
of a functional dependency on Phase 21. Phase 23 (Classification Disposition +
Per-Client Automation Gate) was added after live review of a real Breach Secure Now
report surfaced a gap — it depends on Phases 17-22 since it extends the classifier,
the review UI, and the webhook automation path all at once.
- [x] **Phase 15: Data Model, Detection & Ticket Evidence** — New phishing schema (migration 097) + idempotent Autotask ticket scanner + base ticket evidence capture (completed 2026-07-15)
- [x] **Phase 16: EML/MIME Evidence Parser** — Pure RFC822/MIME parser: `.eml` selection (`rfc.eml` over `OriginatingEmail.eml`), normalized headers/URLs/attachments, sanitized body preview, synthetic-fixture tests (completed 2026-07-15)
@ -453,8 +457,6 @@ of a functional dependency on Phase 21.
- [x] **Phase 22: Approval UI (LiveLink)** — Ticket-ID-addressable Pulse page (Autotask LiveLink target) showing campaign timeline, evidence, and classification, with approve/remediate/mark-false-positive wired to the Phase 20 APIs (completed 2026-07-16)
- [x] **Phase 23: Classification Disposition + Per-Client Automation Gate** — Dedicated "User Awareness" verdict for confirmed phishing-simulation-vendor reports (currently forced into generic UNWANTED), plus an admin UI gate controlling per-company whether the phishing pipeline (parse/classify/report-to-ticket) runs automatically or requires manual trigger (completed 2026-07-17)
## Phase Details
### Phase 15: Data Model, Detection & Ticket Evidence
**Goal**: The durable phishing-triage schema exists in Postgres, and Pulse can scan Autotask/Pulse tickets for known phishing/spam-report patterns idempotently, capturing base ticket-level evidence for each candidate.
@ -592,38 +594,6 @@ of a functional dependency on Phase 21.
**UI hint**: no
## Progress
**Execution Order:**
Phases execute in numeric order. v1.0 (Phases 1-9.1) shipped 2026-07-10. v2.0 (Phases 10-14) shipped 2026-07-12. v3.0 phases run 15 → 16 → 17 → 18 → 19 → 20 → 21 → 22 in strict sequence — Phase 17 has no functional dependency on Phase 16 and could run in parallel with it if split across two workstreams, but both must complete before Phase 19; Phase 22 depends only on Phase 19 and Phase 20 and could equally run in parallel with Phase 21.
| Phase | Milestone | Plans Complete | Status | Completed |
|-------|-----------|----------------|--------|-----------|
| 1. PWA Scaffolding | v1.0 | 2/2 | Complete | 2026-07-10 |
| 2. Mobile Shell + More Drawer | v1.0 | 2/2 | Complete | 2026-07-10 |
| 3. Dashboard Restyle | v1.0 | 2/2 | Complete | 2026-07-10 |
| 4. Tickets Restyle | v1.0 | 3/3 | Complete | 2026-07-10 |
| 5. Finance Restyle | v1.0 | 2/2 | Complete | 2026-05-03 |
| 6. Analyzer Feed | v1.0 | 3/3 | Complete | 2026-07-10 |
| 7. Engagement Overview | v1.0 | 3/3 | Complete | 2026-07-10 |
| 7.1. User Timezone Fix | v1.0 | 5/5 | Complete | 2026-07-10 |
| 8. Engagement User Profile | v1.0 | 2/2 | Complete | 2026-07-10 |
| 9. User Profile & Preferences | v1.0 | 6/6 | Complete | 2026-07-10 |
| 9.1. ntfy Backend Fix | v1.0 | 1/1 | Complete | 2026-07-10 |
| 10. PAX8 Client & Auth Foundation | v2.0 | 3/3 | Complete | 2026-07-10 |
| 11. Company, Catalog & Subscription Sync | v2.0 | 3/3 | Complete | 2026-07-11 |
| 12. Orders/Invoices & Company Matching | v2.0 | 5/5 | Complete | 2026-07-11 |
| 13. Scheduler & Admin Toggle | v2.0 | 3/3 | Complete | 2026-07-11 |
| 14. /pax8 UI Surface | v2.0 | 6/6 | Complete | 2026-07-12 |
| 15. Data Model, Detection & Ticket Evidence | v3.0 | 3/3 | Complete | 2026-07-15 |
| 16. EML/MIME Evidence Parser | v3.0 | 3/3 | Complete | 2026-07-15 |
| 17. Mimecast Blast Radius Lookup | v3.0 | 1/1 | Complete | 2026-07-15 |
| 18. Campaign Grouping & Phishing Analysis API | v3.0 | 5/5 | Complete | 2026-07-16 |
| 19. Classification Engine | v3.0 | 2/2 | Complete | 2026-07-16 |
| 20. Remediation, Approval & Audit Safety | v3.0 | 2/2 | Complete | 2026-07-16 |
| 21. Autotask Triage Note | v3.0 | 2/2 | Complete | 2026-07-16 |
| 22. Approval UI (LiveLink) | v3.0 | 6/6 | Complete | 2026-07-16 |
### Phase 22: Approval UI (LiveLink)
**Goal**: A security operator opens an Autotask ticket, clicks a LiveLink button, and lands on a Pulse page scoped to that ticket showing the campaign's timeline, evidence, and classification — with approve/remediate/mark-false-positive actions right there, so no one is calling the Phase 20 APIs by hand.
@ -672,11 +642,46 @@ Plans:
- [x] 23-06-PLAN.md — Idempotent + audited auto-post: autoPostAcknowledgment prevents duplicate customer-visible notes on repeat campaign webhooks (AUTOGATE-03)
</details>
## Progress
**Execution Order:**
Phases execute in numeric order. v1.0 (Phases 1-9.1) shipped 2026-07-10. v2.0 (Phases 10-14) shipped 2026-07-12. v3.0 (Phases 15-23) shipped 2026-07-17 — phases ran 15 → 16 → 17 → 18 → 19 → 20 → 21 → 22 → 23 in strict sequence; Phase 17 had no functional dependency on Phase 16 and could have run in parallel with it if split across two workstreams, but both had to complete before Phase 19; Phase 22 depended only on Phase 19 and Phase 20 and could equally have run in parallel with Phase 21; Phase 23 was a late addition depending on Phases 17-22.
| Phase | Milestone | Plans Complete | Status | Completed |
|-------|-----------|----------------|--------|-----------|
| 1. PWA Scaffolding | v1.0 | 2/2 | Complete | 2026-07-10 |
| 2. Mobile Shell + More Drawer | v1.0 | 2/2 | Complete | 2026-07-10 |
| 3. Dashboard Restyle | v1.0 | 2/2 | Complete | 2026-07-10 |
| 4. Tickets Restyle | v1.0 | 3/3 | Complete | 2026-07-10 |
| 5. Finance Restyle | v1.0 | 2/2 | Complete | 2026-05-03 |
| 6. Analyzer Feed | v1.0 | 3/3 | Complete | 2026-07-10 |
| 7. Engagement Overview | v1.0 | 3/3 | Complete | 2026-07-10 |
| 7.1. User Timezone Fix | v1.0 | 5/5 | Complete | 2026-07-10 |
| 8. Engagement User Profile | v1.0 | 2/2 | Complete | 2026-07-10 |
| 9. User Profile & Preferences | v1.0 | 6/6 | Complete | 2026-07-10 |
| 9.1. ntfy Backend Fix | v1.0 | 1/1 | Complete | 2026-07-10 |
| 10. PAX8 Client & Auth Foundation | v2.0 | 3/3 | Complete | 2026-07-10 |
| 11. Company, Catalog & Subscription Sync | v2.0 | 3/3 | Complete | 2026-07-11 |
| 12. Orders/Invoices & Company Matching | v2.0 | 5/5 | Complete | 2026-07-11 |
| 13. Scheduler & Admin Toggle | v2.0 | 3/3 | Complete | 2026-07-11 |
| 14. /pax8 UI Surface | v2.0 | 6/6 | Complete | 2026-07-12 |
| 15. Data Model, Detection & Ticket Evidence | v3.0 | 3/3 | Complete | 2026-07-15 |
| 16. EML/MIME Evidence Parser | v3.0 | 3/3 | Complete | 2026-07-15 |
| 17. Mimecast Blast Radius Lookup | v3.0 | 1/1 | Complete | 2026-07-15 |
| 18. Campaign Grouping & Phishing Analysis API | v3.0 | 5/5 | Complete | 2026-07-16 |
| 19. Classification Engine | v3.0 | 2/2 | Complete | 2026-07-16 |
| 20. Remediation, Approval & Audit Safety | v3.0 | 2/2 | Complete | 2026-07-16 |
| 21. Autotask Triage Note | v3.0 | 2/2 | Complete | 2026-07-16 |
| 22. Approval UI (LiveLink) | v3.0 | 6/6 | Complete | 2026-07-16 |
| 23. Classification Disposition + Per-Client Automation Gate | v3.0 | 6/6 | Complete | 2026-07-17 |
---
*Roadmap created: 2026-05-03*
*v2.0 phases added: 2026-07-10*
*v3.0 phases added: 2026-07-14 (Phases 15-21), 2026-07-16 (Phase 22)*
*v3.0 phases added: 2026-07-14 (Phases 15-21), 2026-07-16 (Phase 22, Phase 23), shipped 2026-07-17*
*Source spec (v1.0): `docs/superpowers/specs/2026-05-03-mobile-shell-design.md`*
*Source seed (v2.0): `.planning/seeds/SEED-002-pax8-integration.md`*
*Source requirements (v3.0): `.planning/REQUIREMENTS.md`*
*Source requirements (v3.0, archived): `.planning/milestones/v3.0-REQUIREMENTS.md`*
</content>

View file

@ -2,16 +2,16 @@
gsd_state_version: 1.0
milestone: v3.0
milestone_name: Phishing Triage Automation
status: milestone_complete
stopped_at: Milestone complete (Phase 23 was final phase)
last_updated: 2026-07-17T03:33:49.428Z
last_activity: 2026-07-17 -- Phase 23 execution started
status: Awaiting next milestone
stopped_at: Phase 23 context gathered
last_updated: "2026-07-17T10:55:33.806Z"
last_activity: 2026-07-17 — Milestone v3.0 completed and archived
progress:
total_phases: 9
completed_phases: 8
completed_phases: 9
total_plans: 30
completed_plans: 30
percent: 89
percent: 100
---
# Project State
@ -25,12 +25,10 @@ See: .planning/PROJECT.md (updated 2026-07-14)
## Current Position
Phase: 23
Plan: Not started
Status: Milestone complete
Last activity: 2026-07-17
Progress: [░░░░░░░░░░] 0%
Phase: Milestone v3.0 complete
Plan: —
Status: Awaiting next milestone
Last activity: 2026-07-17 — Milestone v3.0 completed and archived
## Performance Metrics
@ -162,3 +160,7 @@ Last session: 2026-07-16T22:37:09.979Z
Stopped at: Phase 23 context gathered
Resume file: .planning/phases/23-classification-disposition-per-client-automation-gate/23-CONTEXT.md
</content>
## Operator Next Steps
- Start the next milestone with /gsd-new-milestone

View file

@ -0,0 +1,263 @@
# Requirements Archive: v3.0 Phishing Triage Automation
**Archived:** 2026-07-17
**Status:** SHIPPED
For current requirements, see `.planning/REQUIREMENTS.md`.
---
# Requirements: Pulse — v3.0 Phishing Triage Automation
**Defined:** 2026-07-14
**Core Value:** A manager/security operator can see every phishing/spam report ticket
automatically triaged, deduplicated into campaigns, and classified — with any
destructive remediation gated behind explicit human approval.
## v1 Requirements
### Detection
- [x] **DETECT-01**: System scans recent Autotask/Pulse tickets and flags candidates
matching known phishing/spam-report patterns (title/body: "Phishing Report",
"Spam Alert", "Phishing Alert - Email Security Report", "KnowBe4 Phish Alert
Report", "Source: KnowBe4 Phish Alert Button", "userSubmissionsReportMessage",
"reported message destinations", "Microsoft directly")
- [x] **DETECT-02**: Re-scanning does not reprocess a ticket already ingested unless
its source ticket data has changed since last processed (idempotent)
- [x] **DETECT-03**: An operator can trigger analysis of one specific ticket by ID
on demand (`POST /api/phishing/tickets/{ticket_id}/analyze`) instead of waiting
for the scheduled scan
### Evidence Extraction
- [x] **EVID-01**: For each candidate ticket, the system extracts ticket ID/number,
company, requester/reporter, title, description, notes, relevant time entries,
and attachment metadata
- [x] **EVID-02**: When multiple `.eml` attachments exist, the system prefers
`rfc.eml` as the original reported message over `OriginatingEmail.eml`
(wrapper/context), matching case-insensitively and by `message/rfc822`
content-type, not filename alone
- [x] **EVID-03**: The system parses the selected original email's RFC822/MIME
structure into normalized headers (From, display name, sender email/domain,
Reply-To, Return-Path, To, Cc, Subject, Date, Message-ID, Received chain,
SPF/DKIM/DMARC authentication results), extracted URLs, and attachment
metadata (name, content-type, size, hash)
- [x] **EVID-04**: The system stores a sanitized/truncated body preview alongside
raw evidence, and never executes or fetches any URL found in a message
### Campaign Grouping
- [x] **CAMP-01**: Reports are grouped into a campaign using original Message-ID
first, then attachment-hash/URL-domain + subject + sender + time-window, then
sender + normalized subject + client + time-window as fallback keys
- [x] **CAMP-02**: A campaign can accumulate many linked ticket reports and
recipients over time as duplicates are detected
- [x] **CAMP-03**: An operator can list campaigns and view a single campaign's
full detail (linked reports, messages, indicators, classification history)
via API (`GET /api/phishing/campaigns`, `GET /api/phishing/campaigns/{id}`)
### Blast Radius (Mimecast)
- [x] **BLAST-01**: The system can query a Mimecast blast-radius abstraction for
message delivery data (matched/delivered/held/rejected/clicked counts,
per-recipient status) when Mimecast is configured, keyed on message ID,
sender, recipient/reporter, subject, and date window
- [x] **BLAST-02**: When Mimecast is not configured, the system records
`status: unavailable` for that lookup and classification proceeds using
ticket/email evidence alone — it never blocks on missing Mimecast config
### Classification
- [x] **CLASSIFY-01**: The system classifies a campaign as exactly one of
`SPAM` / `UNWANTED` / `THREAT`, with confidence, a short summary,
evidence-backed reasons, recommended actions, and a `requires_approval` flag
- [x] **CLASSIFY-02**: Any classification recommending a destructive action
(purge, block, delete, reset, etc.) always sets `requires_approval: true`
- [x] **CLASSIFY-03**: When evidence is incomplete (no Mimecast data, no `.eml`,
etc.), confidence is lowered and the missing evidence is named in the reasons
- [x] **CLASSIFY-04**: Known/expected KnowBe4 security-awareness simulations are
not classified as `THREAT` absent contrary evidence
- [x] **CLASSIFY-05**: An operator can (re-)trigger classification of a campaign
via API (`POST /api/phishing/campaigns/{id}/classify`)
- [x] **CLASSIFY-06**: The classifier accepts structured, size-bounded evidence
(not raw unbounded email bodies) — long bodies are redacted/truncated before
reaching any AI layer, and IT Glue-sourced evidence goes through the existing
redacted search path if referenced
### Remediation & Approval Safety
- [x] **REMED-01**: Recommended remediation actions are recorded as `proposed`
but never executed automatically in this milestone
- [x] **REMED-02**: An authorized operator can approve a campaign's remediation
via API (`POST /api/phishing/campaigns/{id}/approve`), recording approver,
timestamp, and the exact approved action parameters
- [x] **REMED-03**: `POST /api/phishing/campaigns/{id}/remediate` proceeds only
for approved actions against a configured, non-destructive-by-default
provider path; otherwise it returns `not_implemented`/an explicit failure —
it never silently succeeds without taking or logging an action
- [x] **REMED-04**: Re-running remediation against an already-completed action
does not duplicate the destructive effect (idempotent)
- [x] **REMED-05**: An operator can mark a campaign as a false positive via API
(`POST /api/phishing/campaigns/{id}/mark-false-positive`)
- [x] **REMED-06**: Every state-changing action (classify, approve, remediate,
mark-false-positive) is recorded as an audit event with actor, event type,
and payload
### Autotask Integration
- [x] **NOTE-01**: If Pulse already has a safe Autotask note-writing method, the
system can post an internal triage note summarizing classification, evidence,
blast radius, and recommended actions (sanitized — no raw secrets/tokens/full
malicious URL query strings); otherwise the note text is returned via API
without writing anything to Autotask
### Access Control
- [x] **ACCESS-01**: All `/api/phishing/*` endpoints enforce existing Pulse auth
conventions (`requireAuth`/`requirePermission`), with approve/remediate
requiring elevated permission beyond plain read access
### Approval UI (LiveLink)
- [x] **REVIEW-01**: A stable, ticket-ID-addressable Pulse route (e.g.
`/phishing/tickets/{ticketId}`) resolves the ticket to its campaign and
renders that campaign's review page, suitable as an Autotask LiveLink target
(LiveLink supplies the ticket ID as dynamic content, not the internal
campaign UUID), authenticated via the existing Better Auth session only —
no separate token or query-param auth scheme
- [x] **REVIEW-02**: The page displays the campaign's timeline — linked
reports, classification history, and audit events (classify/approve/
remediate/mark-false-positive) — in chronological order
- [x] **REVIEW-03**: The page displays the gathered evidence — parsed EML
headers/URLs/attachments, sanitized body preview, and Mimecast blast-radius
data (including an explicit `unavailable` state when Mimecast isn't
configured) — never rendering a raw/unsanitized body or unredacted secrets
- [x] **REVIEW-04**: The page displays the current classification (SPAM/
UNWANTED/THREAT), confidence, reasons, and recommended remediation
action(s)
- [x] **REVIEW-05**: An operator can approve, remediate, or mark a campaign as
a false positive directly from the page, calling the existing
`/api/phishing/campaigns/{id}` approve/remediate/mark-false-positive
endpoints and reflecting the resulting state (e.g. a remediated campaign
shows as remediated, not re-offered for approval)
- [x] **REVIEW-06**: An operator without the elevated permission approve/
remediate already require sees those actions disabled or hidden rather than
a failed request; the page enforces no separate or relaxed permission model
from the underlying APIs
### Classification Disposition + Automation Gate
- [x] **CLASSDISP-01**: The classifier assigns a dedicated `USER_AWARENESS`
verdict to campaigns confirmed as phishing-simulation-vendor (KnowBe4/Breach
Secure Now) reports, replacing the previous forced-`UNWANTED` disposition
for these confirmed-simulation cases
- [x] **CLASSDISP-02**: `USER_AWARENESS` maps to a new non-destructive
`acknowledge_user` action that posts a customer-visible thank-you note
(Autotask `noteType: 18`, "Client Portal Note") to the reporting employee
- [x] **CLASSDISP-03**: The campaign review UI surfaces `USER_AWARENESS` and
`acknowledge_user` distinctly from the existing SPAM/UNWANTED/THREAT
verdicts and their actions
- [x] **AUTOGATE-01**: A per-company `phishing_automation_gate` table and
admin-gated `GET`/`PATCH`/`DELETE` API let an operator read and set three
independent opt-in automation flags (`auto_parse`, `auto_classify`,
`auto_report`) per Autotask company, defaulting to all-OFF when no row
exists
- [x] **AUTOGATE-02**: An `/admin/phishing-automation` page lists companies
with three independent per-company `Switch` toggles (one per automation
stage), backed by the `AUTOGATE-01` API
- [x] **AUTOGATE-03**: When a company's automation gate stages are enabled,
the Autotask webhook automatically runs the gated parse→classify→acknowledge
chain for that company's phishing reports, with the `acknowledge_user`
auto-post carve-out narrowly scoped to `USER_AWARENESS` verdicts only — all
other verdicts/actions still require manual approval regardless of gate
state
## v2 Requirements
Deferred to future release. Tracked but not in current roadmap.
### Remediation Execution
- **REMEDEXEC-01**: Actually execute Mimecast sender/domain/URL block (behind
approval, once REMED-01..06 land and are trusted)
- **REMEDEXEC-02**: Microsoft Graph mailbox search/delete/move for delivered
copies
- **REMEDEXEC-03**: Microsoft Defender/Exchange purge (may be preferable to
Graph — needs its own evaluation)
- **REMEDEXEC-04**: Auto-create/associate an Autotask parent incident ticket
- **REMEDEXEC-05**: Auto-close duplicate reports once a campaign is resolved
### Enrichment
- **ENRICH-01**: URL reputation lookup / safe expansion of shortened links
(without detonation)
- **ENRICH-02**: LLM-backed classification layer wired into the
`CLASSIFY-*` rule engine (rule layer ships first; this plugs in behind the
same interface)
## Out of Scope
| Feature | Reason |
|---------|--------|
| Automatic tenant-wide mailbox purge | Destructive; requires proven accuracy and explicit approval infrastructure first (REMED-01..04) |
| Automatic password reset / session revocation | Too high-blast-radius for an MVP triage tool; human-in-the-loop only |
| URL detonation / sandbox execution | Security risk of interacting with malicious infra; reputation-only enrichment deferred to v2 |
| Fully automated ticket closure | Accuracy not yet proven; operator closes tickets manually in this milestone |
| Assuming Microsoft Graph is the eventual purge mechanism | Defender/Exchange purge may be preferable; decision deferred to whichever v2 remediation-execution phase picks it up |
| Real customer `.eml` fixtures in tests | Privacy/security — all test fixtures are synthetic |
## Traceability
Populated during roadmap creation.
| Requirement | Phase | Status |
|-------------|-------|--------|
| DETECT-01 | Phase 15 | Complete |
| DETECT-02 | Phase 15 | Complete |
| DETECT-03 | Phase 18 | Complete |
| EVID-01 | Phase 15 | Complete |
| EVID-02 | Phase 16 | Complete |
| EVID-03 | Phase 16 | Complete |
| EVID-04 | Phase 16 | Complete |
| CAMP-01 | Phase 18 | Complete |
| CAMP-02 | Phase 18 | Complete |
| CAMP-03 | Phase 18 | Complete |
| BLAST-01 | Phase 17 | Complete |
| BLAST-02 | Phase 17 | Complete |
| CLASSIFY-01 | Phase 19 | Complete |
| CLASSIFY-02 | Phase 19 | Complete |
| CLASSIFY-03 | Phase 19 | Complete |
| CLASSIFY-04 | Phase 19 | Complete |
| CLASSIFY-05 | Phase 19 | Complete |
| CLASSIFY-06 | Phase 19 | Complete |
| REMED-01 | Phase 20 | Complete |
| REMED-02 | Phase 20 | Complete |
| REMED-03 | Phase 20 | Complete |
| REMED-04 | Phase 20 | Complete |
| REMED-05 | Phase 20 | Complete |
| REMED-06 | Phase 20 | Complete |
| NOTE-01 | Phase 21 | Complete |
| ACCESS-01 | Phase 18 | Complete |
| REVIEW-01 | Phase 22 | Complete |
| REVIEW-02 | Phase 22 | Complete |
| REVIEW-03 | Phase 22 | Complete |
| REVIEW-04 | Phase 22 | Complete |
| REVIEW-05 | Phase 22 | Complete |
| REVIEW-06 | Phase 22 | Complete |
| CLASSDISP-01 | Phase 23 | Complete |
| CLASSDISP-02 | Phase 23 | Complete |
| CLASSDISP-03 | Phase 23 | Complete |
| AUTOGATE-01 | Phase 23 | Complete |
| AUTOGATE-02 | Phase 23 | Complete |
| AUTOGATE-03 | Phase 23 | Complete |
**Coverage:**
- v1 requirements: 38 total
- Mapped to phases: 38 (Phases 15-23)
- Unmapped: 0 ✓
---
*Requirements defined: 2026-07-14*
*Traceability populated: 2026-07-14 — ROADMAP.md Phases 15-21*
*Last updated: 2026-07-16 — backfilled Phase 23 CLASSDISP-*/AUTOGATE-* entries (23-03)*

View file

@ -0,0 +1,687 @@
# Roadmap: Pulse
## Milestones
- ✅ **v1.0 Mobile Shell Redesign** — Phases 1-9.1 (shipped 2026-07-10)
- ✅ **v2.0 PAX8 Integration** — Phases 10-14 (shipped 2026-07-12)
- ✅ **v3.0 Phishing Triage Automation** — Phases 15-23 (shipped 2026-07-17)
## Phases
**Phase Numbering:**
- Integer phases (1, 2, 3): Planned milestone work
- Decimal phases (2.1, 2.2): Urgent insertions (marked with INSERTED)
Decimal phases appear between their surrounding integers in numeric order.
<details>
<summary>✅ v1.0 Mobile Shell Redesign (Phases 1-9.1) - SHIPPED 2026-07-10</summary>
Eight phases mirror the deliberate build order in the source spec
(`docs/superpowers/specs/2026-05-03-mobile-shell-design.md` §8). Each phase
shipped independently to `master` — no big-bang merge. Phase 1 laid PWA
metadata and safe-area utilities. Phase 2 rebuilt `app/mobile/layout.tsx`
with the new header, 5-cell bottom nav, and More drawer (deleting
`/mobile/nav` in the same change). Once the shell landed, Phases 37 were
independent restyles/new pages; Phase 8 followed Phase 7 because the user
profile is reached from the Engagement overview. All work happened in place
under `/mobile/*` — no `/mobile-v2`, no parallel routes.
- [x] **Phase 1: PWA Scaffolding** — Manifest, viewport meta, and safe-area utilities so the shell installs and paints under the home indicator
- [x] **Phase 2: Mobile Shell + More Drawer** — New `app/mobile/layout.tsx` (header + 5-cell bottom nav) and Sheet drawer that replaces `/mobile/nav`
- [x] **Phase 3: Dashboard Restyle** — 2×2 KPI grid, Needs Attention strip, worker/backup status row (no charts)
- [x] **Phase 4: Tickets Restyle** — Collapsible URL-synced filters, priority-bar rows, cursor-based infinite scroll, detail header reskin
- [x] **Phase 5: Finance Restyle** — Adopt new Card + typography scale, swap wide tables for stacked lists (completed 2026-05-03)
- [x] **Phase 6: Analyzer Feed (NEW)**`/mobile/analyzer` read-only stream + `/api/mobile/analyzer/feed`
- [x] **Phase 7: Engagement Overview (NEW)**`/mobile/engagement` phone-first overview reachable from the More drawer
- [x] **Phase 7.1: User Timezone Fix (INSERTED — urgent)** — Per-user IANA timezone column + viewer-tz date math so dashboards and filters render the right "today"
- [x] **Phase 8: Engagement User Profile (NEW)**`/mobile/engagement/[userId]` real-page profile that replaces the desktop modal pattern
- [x] **Phase 9: User Profile & Preferences (NEW)**`/mobile/profile` settings page (timezone chooser, theme, mobile push, Teams + ntfy channels)
- [x] **Phase 9.1: ntfy Backend Fix (INSERTED — urgent)** — Personal ntfy channels target the company ntfy server with bearer auth + `pulse-me-` prefix (UAT gap closure)
### Phase 1: PWA Scaffolding
**Goal**: A manager who taps "Add to Home Screen" gets a standalone Pulse icon that opens to the mobile shell with content respecting the device safe areas.
**Depends on**: Nothing (first phase)
**Requirements**: PWA-01, PWA-02, PWA-03, PWA-04
**Success Criteria** (what must be TRUE):
1. Visiting `/manifest.json` returns valid JSON with `name: "Pulse"`, `display: "standalone"`, `start_url: "/mobile"`, and theme/background colors matching the app shells
2. The root `app/layout.tsx` references the manifest via `<link rel="manifest">` and the viewport meta includes `viewport-fit=cover`
3. A safe-area utility (Tailwind arbitrary values or shared class) is available so any sticky top/bottom bar can opt into `env(safe-area-inset-top)` / `env(safe-area-inset-bottom)` padding
4. Installing Pulse to a phone home screen launches a chromeless app pointed at `/mobile` (no service worker, no offline)
**Plans**: 2 plans
- [x] 01-01-PLAN.md — Web App Manifest + viewport-fit=cover (PWA-01, PWA-02, PWA-03)
- [x] 01-02-PLAN.md — Safe-area `pt-safe` / `pb-safe` @utility blocks in brand.css (PWA-04, gap closure)
**UI hint**: no
### Phase 2: Mobile Shell + More Drawer
**Goal**: Every `/mobile/*` page renders inside a new layout — sticky header (Wulf mark + Bell placeholder + avatar), scrollable content, and a 5-cell bottom nav whose fifth control opens a Sheet drawer that fully replaces `/mobile/nav`.
**Depends on**: Phase 1
**Requirements**: SHELL-01, SHELL-02, SHELL-03, SHELL-04, SHELL-05, SHELL-06, NAV-01, NAV-02, NAV-03, DRAWER-01, DRAWER-02, DRAWER-03, DRAWER-04, DRAWER-05, DRAWER-06
**Success Criteria** (what must be TRUE):
1. On any `/mobile/*` route the user sees a sticky header with the Wulf wordmark linking to `/mobile/dashboard`, a Bell icon button (keyboard-focusable, no menu), and a compact avatar — no page title in the header
2. A fixed bottom bar exposes four primary tabs (Dashboard, Tickets, Finance, Analyzer) plus a More cell; tapping a tab routes to its page and the active tab uses `text-primary` based on `pathname.startsWith(href)`
3. Tapping More (or the header avatar) opens a single Sheet drawer with three sections — Mobile sections (Engagement), Full site (Quotes, Configuration Items, Backup Status, Ticket Digest, Admin/Sync — each with an `ExternalLink` hint), and Account (current user read-only + Sign out)
4. Tapping Sign out in the drawer signs the user out and lands them on `/auth/sign-in`
5. `app/mobile/nav/page.tsx` no longer exists; visiting `/mobile/nav` does not render the old standalone nav page
6. Page content scrolls under the sticky header and is not hidden behind the bottom nav (bottom padding accounts for nav height + safe-area inset)
**Plans**: 2 plans
- [x] 02-01-PLAN.md — Build mobile shell components (HeaderBar, BottomNav, MoreDrawer) + analyzer placeholder (SHELL-02..04, SHELL-06, NAV-01..03, DRAWER-01..05)
- [x] 02-02-PLAN.md — Wire new components into app/mobile/layout.tsx, delete app/mobile/nav/page.tsx (SHELL-01, SHELL-05, DRAWER-06)
**UI hint**: yes
### Phase 3: Dashboard Restyle
**Goal**: A manager opening `/mobile/dashboard` sees the state of the business at a glance — four KPIs, items needing attention, and a worker/backup status row — with no charts.
**Depends on**: Phase 2
**Requirements**: DASH-01, DASH-02, DASH-03, DASH-04
**Success Criteria** (what must be TRUE):
1. Dashboard renders a 2×2 grid of four primary KPI cards drawn from desktop hero stats (no 1×4 row, no charts)
2. Below the grid, a "Needs Attention" horizontally-scrollable strip surfaces overdue tickets, failed backups, and stalled workflows; tapping a card opens its detail view
3. A compact status row shows analyzer worker, RMM worker, and backup-success-rate; tapping any element opens the corresponding desktop admin page
4. The page contains no recharts/chart components on phone widths
**Plans**: 2 plans
- [x] 03-01-PLAN.md — /api/mobile/dashboard reshape + KpiCardMobile/NeedsAttentionStrip/WorkerStatusRow components (DASH-01, DASH-02, DASH-03)
- [x] 03-02-PLAN.md — Replace /mobile/dashboard page body with 3-section layout, no charts (DASH-01, DASH-02, DASH-03, DASH-04)
**UI hint**: yes
### Phase 4: Tickets Restyle
**Goal**: A manager triages tickets on a phone with a collapsible filter bar that deep-links via URL, priority-coloured rows, and infinite scroll — and the detail page header matches the new shell.
**Depends on**: Phase 2
**Requirements**: TICK-01, TICK-02, TICK-03, TICK-04, TICK-05, TICK-06, TICK-07
**Success Criteria** (what must be TRUE):
1. The Tickets page opens with the filter strip collapsed; expanding it reveals status, priority, queue, and an assigned-to-me toggle, and changing any filter updates the URL query string (deep link works on reload)
2. Each list row has a left-edge stripe matching priority (Critical/High/Medium/Low → red/orange/amber/slate) and shows ticket #, title, company, age, and assignee
3. Single-tapping a row navigates to `/mobile/tickets/[id]`
4. Scrolling to the bottom of the list automatically loads the next ~25 rows (no Next button); a "Load more" fallback button is also visible/focusable for accessibility
5. The detail page header uses the new shell styling (Wulf mark, breadcrumb back) while the body remains largely unchanged
**Plans**: 3 plans
- [x] 04-01-PLAN.md — /api/mobile/tickets cursor rewrite + TicketFilterStrip + TicketRowSkeleton components (TICK-01, TICK-02, TICK-05)
- [x] 04-02-PLAN.md — Replace app/mobile/tickets/page.tsx with URL-synced filters, priority-stripe rows, IntersectionObserver infinite scroll (TICK-01..TICK-06)
- [x] 04-03-PLAN.md — Reskin in-page header of app/mobile/tickets/[id]/page.tsx (back chevron + breadcrumb + ExternalLink) (TICK-07)
**UI hint**: yes
### Phase 5: Finance Restyle
**Goal**: A manager reading AR / invoice / payment status on a phone sees properly spaced cards and stacked lists instead of squished wide tables — same data, new shell.
**Depends on**: Phase 2
**Requirements**: FIN-01, FIN-02
**Success Criteria** (what must be TRUE):
1. `/mobile/finance` adopts the new Card and typography scale — no horizontal overflow, spacing legible on small phones
2. Sections that previously rendered wide tables on phone widths now render as stacked lists (no new sections, no new data sources)
**Plans**: 2 plans
- [x] 05-01-PLAN.md — FinanceRow + FinanceSkeleton helper components (FIN-01, FIN-02)
- [x] 05-02-PLAN.md — Rewrite app/mobile/finance/page.tsx to KPI grid + stacked lists + shadcn Collapsibles (FIN-01, FIN-02)
**UI hint**: yes
### Phase 6: Analyzer Feed (NEW)
**Goal**: A manager taps the Analyzer tab and skims a most-recent-first stream of AI ticket analyses, opening any one to a phone-friendly summary view that links out to desktop for full details.
**Depends on**: Phase 2
**Requirements**: ANL-01, ANL-02, ANL-03, ANL-04, ANL-05, ANL-06
**Success Criteria** (what must be TRUE):
1. Tapping the Analyzer tab in the bottom nav lands on `/mobile/analyzer` and shows a most-recent-first list of AI ticket analyses
2. Each row shows ticket #, title, the analyzer's one-line summary, a confidence badge, and a stage indicator (Triage → Analyze → Deep Review)
3. Tapping a row opens a mobile summary view rendering Summary, Next Step, and Next Step Rationale, with a "View full analysis" link out to the desktop analyzer page
4. The mobile feed never exposes editing, re-run, or prompt-tuning controls (read-only by design)
5. The list reads from `analyzer_analyses` via `/api/mobile/analyzer/feed` (or a reused list endpoint that already returns the right shape)
**Plans**: 3 plans
- [x] 06-01-PLAN.md — /api/mobile/analyzer/feed endpoint with cursor pagination + kiosk_settings scoping (ANL-01, ANL-02, ANL-06)
- [x] 06-02-PLAN.md — AnalyzerFeedRow/StagePips/ConfidenceBadge/RowSkeleton components + replace /mobile/analyzer placeholder with feed list page (ANL-01, ANL-02, ANL-05, ANL-06)
- [x] 06-03-PLAN.md — /mobile/analyzer/[id] detail page reading existing /api/analyzer/analyses/[id] (ANL-03, ANL-04, ANL-05)
**UI hint**: yes
### Phase 7: Engagement Overview (NEW)
**Goal**: A manager reaches Engagement from the More drawer and sees a phone-first overview — period chips, stacked summary cards, a sortable per-employee list, and one compact sparkline.
**Depends on**: Phase 2
**Requirements**: ENG-01, ENG-02, ENG-03, ENG-04, ENG-05, ENG-09
**Success Criteria** (what must be TRUE):
1. The Mobile sections row in the More drawer links to `/mobile/engagement`; the Analyzer is on the bottom bar but Engagement is not
2. The overview page shows a period selector (today / 7d / 30d) sticky just below the H1, with active period clearly indicated
3. Summary cards (active users, total Graph hours, total Autotask hours, hours-per-active-user) render single-column stacked — no 4-up grid on phone widths
4. The per-employee list renders as stacked rows (avatar/initials, name, role, hours bar) with a search input and a sort control above (sort by hours, name, utilization)
5. A single compact "hours trend" sparkline renders at the top of the list, scoped to the selected period — no multi-series chart
**Plans**: 3 plans
- [x] 07-01-PLAN.md — /api/mobile/engagement/summary + /api/mobile/engagement/trend endpoints with period whitelist + requireAuth (ENG-03, ENG-05)
- [x] 07-02-PLAN.md — Engagement* mobile components (PeriodChips, SummaryCard, HoursSparkline, SortChips, SearchInput, UserRow, UserRowSkeleton + getInitials utility) (ENG-02, ENG-03, ENG-04, ENG-05)
- [x] 07-03-PLAN.md — app/mobile/engagement/page.tsx orchestration (period/sort state, IntersectionObserver, empty/error/not-configured states) (ENG-01, ENG-02, ENG-03, ENG-04, ENG-05, ENG-09)
**UI hint**: yes
### Phase 7.1: User Timezone Fix (INSERTED — urgent)
**Goal**: A user opening Pulse sees dashboards, filters, and "today/this week" date math computed in their own IANA timezone — not server UTC — so reports stop showing yesterday's data as today (and vice versa). Persistence layer remains UTC; only the read/display path changes.
**Depends on**: Nothing structural (Better Auth users table extension + read-path changes)
**Requirements**: TZ-01, TZ-02, TZ-03, TZ-04
**Success Criteria** (what must be TRUE):
1. Each user has an IANA timezone (e.g. `America/New_York`) persisted server-side; default = `process.env.DEFAULT_TIMEZONE || 'UTC'` for users with no value yet
2. Mobile and desktop dashboards, ticket filters, finance views, and engagement period selectors compute day/week boundaries against the viewer's timezone — not UTC and not the browser's local zone (browser zone may differ from the user's chosen zone, e.g. travel)
3. Authenticated `GET /api/me/timezone` returns the user's tz; `PUT /api/me/timezone` accepts an IANA string and rejects anything not in `Intl.supportedValuesOf('timeZone')`
4. A shared client hook (`useUserTimezone()`) reads the value from `useSession()` so all components use a single source of truth — no per-page `Intl` calls scattered around
5. Existing UTC-stored data stays untouched (no destructive migration); only formatting and range-bucketing change
**Plans**: 6 plans
- [x] 07.1-01-PLAN.md — Add timezone column to user table + Better Auth additionalField (TZ-01)
- [x] 07.1-02-PLAN.md — /api/me/timezone GET + PUT with IANA validation (TZ-03)
- [x] 07.1-03-PLAN.md — Server-side read paths use user.timezone for day/week/month boundaries; auth-gates /api/mobile/finance; migrates /api/dashboard/trends (TZ-02)
- [x] 07.1-04-PLAN.md — useUserTimezone() client hook + reported-bug-surface mobile page migration + codebase-wide audit (TZ-04, TZ-02 client portion)
- [x] 07.1-05-PLAN.md — Codebase-wide useUserTimezone() adoption per the Plan 04 audit (TZ-04 SC#4 single-source-of-truth at codebase scale)
**UI hint**: no (this is a data/plumbing phase; the picker UI is part of Phase 9)
### Phase 8: Engagement User Profile (NEW)
**Goal**: From the Engagement overview, a manager taps an employee row and arrives at a real, shareable profile page — single-column phone-first — and the device back gesture returns them to the overview.
**Depends on**: Phase 7
**Requirements**: ENG-06, ENG-07, ENG-08
**Success Criteria** (what must be TRUE):
1. Tapping a row in the per-employee list navigates to `/mobile/engagement/[userId]` (segment form, shareable URL)
2. The profile is a real page (not a modal) — the device/browser back gesture returns to the overview at the same scroll position
3. The profile renders single-column: identity header → period selector → key metrics (compact) → activity breakdown list → recent items, sourced from the existing engagement profile data endpoints (no new data)
**Plans**: 2 plans
- [x] 08-01-PLAN.md — MS Graph user-photo proxy at /api/mobile/engagement/user/[userId]/photo (ENG-06; D-25, D-26)
- [x] 08-02-PLAN.md — Mobile profile page at /mobile/engagement/[userId] + 6 EngagementProfile* components (ENG-06, ENG-07, ENG-08)
**UI hint**: yes
### Phase 9: User Profile & Preferences (NEW)
**Goal**: A logged-in user reaches a profile/settings page from the More drawer and can configure timezone (chooser UI for TZ-01), theme (light/dark/system, persisted server-side for cross-device consistency), mobile push notifications (per-event toggles, delivered via the ntfy phone app per the no-SW constraint), and personal notification channels (Teams webhook URL, Pulse-minted ntfy topic). Changes persist per-user and the existing notify pipeline routes through these per-user channels for events the user is subscribed to.
**Depends on**: Phase 7.1 (timezone schema), Phase 2 (More drawer)
**Requirements**: PROF-01, PROF-02, PROF-03, PROF-04, TZ-CHOOSER-01, TZ-CHOOSER-02, THEME-01, THEME-02, THEME-03, THEME-04, THEME-05, CHAN-01, CHAN-02, CHAN-03, CHAN-04, CHAN-05, CHAN-06, CHAN-07, SUB-01, SUB-02, SUB-03, SUB-04, ROUTE-01, ROUTE-02, ROUTE-03, ROUTE-04, ROUTE-05, ROUTE-06, ROUTE-07
**Canonical refs:**
- `docs/superpowers/specs/2026-05-03-mobile-shell-design.md` §4, §5.1, §7 (no-SW constraint, drawer Account section, deferred items)
- `lib/services/pipeline-steps/notify.ts` (existing channel-only notify; Phase 9 adds `route_to_user`)
- `lib/auth.ts` `additionalFields` (Phase 7.1 precedent for `theme` column exposure)
- `migrations/033_create_pipeline_engine_tables.sql` (`notification_channels`, `pipeline_steps` shapes)
- `migrations/083_add_user_timezone.sql` (column-on-user precedent from Phase 7.1)
- `app/api/me/timezone/route.ts` (per-user API conventions to mirror for `/api/me/theme`, `/api/me/channels`, `/api/me/notification-subscriptions`)
- `components/mobile/MoreDrawer.tsx` (Account section gains "Profile & preferences" link)
- `components/theme-toggle.tsx` + `components/theme-provider.tsx` (next-themes write-through path)
**Success Criteria** (what must be TRUE):
1. Tapping Profile/Account in the More drawer routes to `/mobile/profile` (real page, not modal); the page renders four sections in order — Timezone, Theme, Notifications, Channels — each gated by `requireAuth()` and saving per-user (PROF-01..04)
2. Theme persists server-side and applies on sign-in across devices via the `theme` column on `user` (Better Auth additionalFields), with next-themes still handling FOUC and the desktop `ThemeToggle` writing through to the server (THEME-01..05)
3. Each user can configure one Teams webhook URL and one Pulse-minted ntfy topic; both are test-sent on save and admins have full read+edit access via `/admin/workflow/channels` (CHAN-01..07)
4. The Notifications section renders a per-event × per-channel matrix sourced from `notify_event_keys`; defaults to enabled (opt-out model); writes via `/api/me/notification-subscriptions` (SUB-01..04)
5. `lib/services/pipeline-steps/notify.ts` honors an optional `route_to_user` block on each notify step — resolving the user via a registered resolver, checking the subscription matrix, sending via the personal channel, and falling back to the step's `channel_id` on no-channel/send-failure (recorded as `user_route_fallback`) but skipping silently when the user has the toggle muted (ROUTE-01..07)
**Plans**: 6 plans
- [x] 09-01-PLAN.md — Schema foundation: theme column, owner_user_id, notify_event_keys, user_event_subscriptions (THEME-01, THEME-05, CHAN-01, SUB-01, SUB-02)
- [x] 09-02-PLAN.md — /api/me/* endpoints: theme, channels (Teams + ntfy), notification-subscriptions matrix (THEME-02, CHAN-02..05, CHAN-07, SUB-04)
- [x] 09-03-PLAN.md — notify.ts route_to_user branch + resolver registry + fallback semantics (ROUTE-01..06)
- [x] 09-04-PLAN.md — /mobile/profile UI part 1: page shell + drawer link + Timezone/Theme/Notifications Cards + Channels placeholder (PROF-01..04, TZ-CHOOSER-01..02, THEME-03, SUB-03)
- [x] 09-05-PLAN.md — /mobile/profile UI part 2: real Channels Card (Teams + ntfy + QR) + ThemeSessionBridge + ThemeToggle write-through (CHAN-02..05, CHAN-07, THEME-04)
- [x] 09-06-PLAN.md — Admin surfaces: channels Owner column + filter, event-keys CRUD page, NEW /admin/workflow/executions with fallback filter (CHAN-06, SUB-01, ROUTE-07)
**UI hint**: yes
### Phase 9.1: ntfy Backend Fix (INSERTED — urgent)
**Goal**: A logged-in user enabling mobile push from `/mobile/profile` gets a topic published to `https://ntfy.wulfconsulting.cloud` (not the public `ntfy.sh`) with bearer auth via `NTFY_PULSE_TOKEN`, using the `pulse-me-` reserved prefix so personal channels never collide with the `noc-*` / `soc-*` namespaces reserved for NOC/SOC operations.
**Depends on**: Phase 9 (personal channels feature must exist)
**Requirements**: CHAN-03, CHAN-05, CHAN-07, ROUTE-04 (gap closure — re-targeting the existing implementation)
**Source**: `.planning/phases/09-user-profile-preferences-new/09-HUMAN-UAT.md` Test 1 — diagnosed gap
**Success Criteria** (what must be TRUE):
1. `mintNtfyTopic()` returns `pulse-me-XXXXXXXX`; `NTFY_TOPIC_RE` enforces `^pulse-me-[A-Za-z0-9-]{6,64}$`; custom topics matching `pulse-`, `noc-`, `soc-`, or arbitrary names are rejected
2. All four ntfy publish paths used for personal channels (`sendChannelTest`, `pipeline-steps/notify.ts sendNtfy`, `pipeline-steps/approval.ts` ntfy branch, `ticket-digest-service.ts deliver()` ntfy branch) target `${NTFY_BASE_URL || 'https://ntfy.wulfconsulting.cloud'}` and send `Authorization: Bearer ${NTFY_PULSE_TOKEN}` when `channel.owner_user_id` is set
3. Global / admin ntfy channels (`owner_user_id IS NULL`) preserve their existing `channel.config.server_url` / `channel.config.auth_token` behavior — out-of-scope per gap diagnosis
4. `/mobile/profile` QR code and subscribe link target `process.env.NEXT_PUBLIC_NTFY_BASE_URL || 'https://ntfy.wulfconsulting.cloud'`; help line under custom-topic Input reads "Topic must start with `pulse-me-`"
5. `npx tsc --noEmit --pretty` and `npx vitest run lib/services/pipeline-steps/notify.test.ts` both pass (mute semantics intact)
**Plans**: 1 plan
- [x] 09.1-01-PLAN.md — Personal-channels regex/prefix/bearer + propagate to notify/approval/digest send paths + ProfileChannelsSection QR & copy
**UI hint**: no (backend-heavy; one component edit for QR/link target)
</details>
<details>
<summary>✅ v2.0 PAX8 Integration (Phases 10-14) - SHIPPED 2026-07-12</summary>
**Milestone Goal:** Sync PAX8 licensing/subscription data into Pulse, read-only,
mapped to Autotask companies, so managers can see subscription costs and seat
counts alongside existing company data.
This milestone follows the codebase's existing external-integration pattern
(`<name>-client.ts` + `<name>-factory.ts` + numbered migration + sync service +
scheduler entry + admin toggle). Phase 10 stands up auth + schema in isolation
so the OAuth2 client-credentials flow is proven before anything is built on
top of it. Phase 11 syncs the "current state" entities (companies, catalog,
subscriptions). Phase 12 adds historical cost data (orders/invoices) and the
fuzzy-name company-matching pass, since matching needs companies to already
exist. Phase 13 wires the combined sync into the daily scheduler and the
`/admin/integrations` toggle — deliberately last among the backend phases so
it schedules the *complete* sync, not a partial one. Phase 14 ships the
`/pax8` page, which needs Phase 12's data and match state to have something to
render, including the manual-resolution workflow for flagged companies.
- [x] **Phase 10: PAX8 Client & Auth Foundation** — OAuth2 client-credentials auth, `isPax8Configured()`, and the PAX8 schema migration (completed 2026-07-10)
- [x] **Phase 11: Company, Catalog & Subscription Sync** — Read-only sync of current-state companies, product catalog, and subscriptions into Postgres (completed 2026-07-11)
- [x] **Phase 12: Orders/Invoices & Company Matching** — Historical cost sync plus fuzzy-name auto-matching (with flagging) of PAX8 companies to Autotask companies (completed 2026-07-11)
- [x] **Phase 13: Scheduler & Admin Toggle** — Daily `pax8-daily` cron entry and an on/off switch in `/admin/integrations` (completed 2026-07-11)
- [x] **Phase 14: /pax8 UI Surface** — New page listing companies/subscriptions/cost breakdown, plus manual resolution of flagged company matches (completed 2026-07-12)
### Phase 10: PAX8 Client & Auth Foundation
**Goal**: Pulse can authenticate to the PAX8 API via OAuth2 client-credentials, and the Postgres schema for all four PAX8 entities exists — proving the integration pattern before any sync logic is built on top of it.
**Depends on**: Nothing (first phase of v2.0)
**Requirements**: PAX8-01, PAX8-02
**Success Criteria** (what must be TRUE):
1. `lib/services/pax8-factory.ts` exports `isPax8Configured()`, returning `true` only when the PAX8 client ID and secret env vars are both set, `false` otherwise
2. `getPax8Client()` performs an OAuth2 client-credentials token exchange against `api.pax8.com/v1` and successfully calls a read-only endpoint (e.g., list companies) using the resulting bearer token
3. Calling the client with missing/invalid credentials throws a clear, typed error rather than failing silently or crashing the process — matching the existing `is<Name>Configured()` + throw-if-missing pattern used by other integrations
4. A new numbered migration creates the PAX8 tables (companies, subscriptions, products/catalog, orders, and a company-match/review table) using `IF NOT EXISTS`, ready for Phase 11+ to populate
**Plans**: 3 plans
- [x] 10-01-PLAN.md — PAX8 types + OAuth2 client (token exchange, audience, cache) + factory (isPax8Configured/getPax8Client) + mocked tests (PAX8-01, PAX8-02)
- [x] 10-02-PLAN.md — migrations/091_pax8_tables.sql (6 PAX8 tables, IF NOT EXISTS) + apply to dev DB (PAX8-01, PAX8-02)
- [x] 10-03-PLAN.md — verify-pax8-auth.ts live auth-proof (SC#2) + CLAUDE.md/INTEGRATIONS.md docs (PAX8-01, PAX8-02)
**UI hint**: no
### Phase 11: Company, Catalog & Subscription Sync
**Goal**: PAX8 companies, the product catalog, and current subscriptions are synced into Postgres and are human-readable (not raw SKU IDs) — the "current state" half of the integration.
**Depends on**: Phase 10
**Requirements**: PAX8-03, PAX8-04, PAX8-05, PAX8-08
**Success Criteria** (what must be TRUE):
1. Running the sync populates a companies table with every PAX8 company (PAX8 ID, name, and other identifying fields)
2. Running the sync populates a product/catalog table (SKUs, categories) and a subscriptions table (product, seat count, billing term) per company
3. A synced subscription row displays a readable product name and category by joining to the catalog table — not a bare SKU/product ID
4. No code path in the PAX8 client or this sync service issues a write (POST/PUT/PATCH/DELETE) to the PAX8 API — every call is a read, verified by inspection of the client's exposed methods
**Plans**: 3 plans
- [x] 11-01-PLAN.md — Migration 092 subscription cost columns + extend pax8 types + read-only client pagination helpers (PAX8-04, PAX8-05, PAX8-08)
- [x] 11-02-PLAN.md — pax8-sync-service.ts (companies + subscriptions + referenced-only catalog + soft-delete reconciliation) + /api/pax8/sync fire-and-forget route (PAX8-03, PAX8-04, PAX8-05, PAX8-08)
- [x] 11-03-PLAN.md — Read-only invariant proof + live sync run DB verification checkpoint (PAX8-03, PAX8-04, PAX8-05, PAX8-08)
**UI hint**: no
### Phase 12: Orders/Invoices & Company Matching
**Goal**: Pulse has historical PAX8 cost data for reconciliation over time, and every PAX8 company is automatically linked to its Autotask counterpart or explicitly flagged for review — never silently guessed.
**Depends on**: Phase 11
**Requirements**: PAX8-06, PAX8-10, PAX8-11
**Success Criteria** (what must be TRUE):
1. Running the sync populates an orders/invoices table with historical line items (not just current-state seat counts), enabling cost-over-time comparisons
2. At sync time, each PAX8 company is automatically matched to an Autotask company by fuzzy name similarity when a sufficiently confident match exists, and the match is persisted
3. A PAX8 company with no match, or with multiple similarly-scored Autotask candidates, is persisted with a flagged/needs-review status instead of being auto-assigned
4. Re-running the sync does not overwrite a match that has already been manually confirmed/resolved (idempotent with respect to human decisions)
**Plans**: 5 plans
- [x] 12-01-PLAN.md — Migration 093 (pg_trgm + pax8_order_items/pax8_companies columns) + Pax8Invoice/Pax8InvoiceItem types (PAX8-06, PAX8-10, PAX8-11)
- [x] 12-02-PLAN.md — pax8-client listAllInvoices/listAllInvoiceItems + tests + live field-mapping spot-check (PAX8-06)
- [x] 12-03-PLAN.md — pax8-company-matcher.ts (pg_trgm similarity, 0.90 threshold, tie/empty/idempotency policy) + tests (PAX8-10, PAX8-11)
- [x] 12-04-PLAN.md — syncOrders + syncCompanyMatches wired into Pax8SyncService.fullSync + sync-service tests (PAX8-06, PAX8-10, PAX8-11)
- [x] 12-05-PLAN.md — Live full-sync verification of all 4 success criteria + human-verify checkpoint (PAX8-06, PAX8-10, PAX8-11)
**UI hint**: no
### Phase 13: Scheduler & Admin Toggle
**Goal**: PAX8 sync runs automatically once a day like every other Pulse integration, and can be turned on or off from `/admin/integrations` without a container restart.
**Depends on**: Phase 12
**Requirements**: PAX8-07, PAX8-09
**Success Criteria** (what must be TRUE):
1. A `pax8-daily` (or equivalently named) entry exists in the sync scheduler and fires once per day, running the full companies + catalog + subscriptions + orders sync in sequence
2. PAX8 appears as a toggleable row on `/admin/integrations`, backed by the `integration_settings` table like every other integration
3. Disabling PAX8 from that UI stops future scheduled sync runs (respecting the existing health-cache window, or immediately per the PATCH-clears-cache convention) and records `disabled_by`, `disabled_at`, and an optional `disabled_reason`
4. Re-enabling PAX8 resumes scheduled sync at the next cron tick with no code deploy or container restart required
**Plans**: 3 plans
- [x] 13-01-PLAN.md — Migration 096 pax8-daily seed + dual-guarded scheduler branch + CLAUDE.md precedent note (PAX8-07, PAX8-09)
- [x] 13-02-PLAN.md — checkConfigOnly('pax8') admin-integrations row + POST /api/pax8/sync 403 disabled-gate (PAX8-09)
- [x] 13-03-PLAN.md — Live verification checkpoint of Phase 13 SC#1-4 (PAX8-07, PAX8-09)
**UI hint**: no
### Phase 14: /pax8 UI Surface
**Goal**: A manager can open `/pax8` and see PAX8 companies with their subscriptions and a cost breakdown, and an admin can resolve any flagged/ambiguous company match directly from that page — no psql required.
**Depends on**: Phase 12
**Requirements**: PAX8-12, PAX8-13, PAX8-14
**Success Criteria** (what must be TRUE):
1. `/pax8` lists PAX8 companies together with their current subscriptions
2. Each company shows a cost breakdown (e.g., by subscription/product) built from the synced subscription and order/invoice data
3. Flagged/ambiguous company matches appear in a distinct, clearly-labeled review section on `/pax8` rather than being mixed silently into the main list
4. From that review section, an admin can pick the correct Autotask company for a flagged PAX8 company; the resolution persists and is respected (not overwritten) by future syncs
**Plans**: 6 plans
- [x] 14-01-PLAN.md — GET /api/pax8/companies list + /api/pax8/companies/[id] cost-breakdown (requireAuth) (PAX8-13)
- [x] 14-02-PLAN.md — /api/pax8/company-matches queue + admin-gated resolve route + extracted resolver service & test (PAX8-12, PAX8-14)
- [x] 14-03-PLAN.md — DetailModal additive extension: kind prop + PAX8_COMPANY_GROUPS + subscriptions cost-breakdown section (PAX8-13)
- [x] 14-04-PLAN.md — /pax8 page shell + Companies tab (DataTable + DetailModal drill-down) + top-level nav entry (PAX8-13)
- [x] 14-05-PLAN.md — Needs Review tab (review cards, candidate + manual-search resolve, count badge) + companies-list auth hardening (PAX8-14, PAX8-12)
- [x] 14-06-PLAN.md — Automated gates + human verification of all 4 SCs and the view/resolve permission split (PAX8-12, PAX8-13, PAX8-14)
**UI hint**: yes
</details>
<details>
<summary>✅ v3.0 Phishing Triage Automation (Phases 15-23) - SHIPPED 2026-07-17</summary>
**Milestone 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.
Nine phases follow the domain's natural dependency chain rather than a generic
foundation→features→polish template. Phase 15 lands the durable data model
(campaigns/reports/messages/indicators/classifications/remediation_actions/
audit_events, migration 097+) together with ticket detection and basic ticket-level
evidence, since every later service writes to that schema. Phase 16 is the pure,
testable RFC822/MIME `.eml` parser — it has no dependency on detection beyond the
schema, but campaign grouping depends on its output (Message-ID, indicators), so it
must land before Phase 18. Phase 17 (Mimecast blast-radius) has no dependency on the
parser or on campaigns — it only needs the Phase 15 schema — so it's sequenced here
as an independent unit that could equally have been built in parallel with Phase 16
by a second workstream. Phase 18 is the first phase to expose `/api/phishing/*`
routes (campaign list/get, on-demand ticket analysis) and is where ACCESS-01's
auth convention is established for every phishing endpoint that follows. Phase 19
(classification) depends on both Phase 17's blast-radius output and Phase 18's
campaign data as inputs — it cannot run before either. Phase 20 (remediation/
approval/audit) depends on campaigns existing (Phase 18) and classifications
existing (Phase 19), since you can't approve or gate an action that doesn't
reference either. Phase 21 (Autotask triage note) is last because its note content
summarizes classification, blast radius, and recommended/approved remediation state
— it has nothing to summarize until Phases 19 and 20 exist. Phase 22 (Approval UI)
depends on the same Phase 19/20 outputs as Phase 21 but is otherwise independent of
it — a LiveLink button in Autotask is a separate configuration surface from the
triage note's content, so Phase 22 does not need Phase 21 to land first; it is
sequenced last only because it is the newest addition to this milestone, not because
of a functional dependency on Phase 21. Phase 23 (Classification Disposition +
Per-Client Automation Gate) was added after live review of a real Breach Secure Now
report surfaced a gap — it depends on Phases 17-22 since it extends the classifier,
the review UI, and the webhook automation path all at once.
- [x] **Phase 15: Data Model, Detection & Ticket Evidence** — New phishing schema (migration 097) + idempotent Autotask ticket scanner + base ticket evidence capture (completed 2026-07-15)
- [x] **Phase 16: EML/MIME Evidence Parser** — Pure RFC822/MIME parser: `.eml` selection (`rfc.eml` over `OriginatingEmail.eml`), normalized headers/URLs/attachments, sanitized body preview, synthetic-fixture tests (completed 2026-07-15)
- [x] **Phase 17: Mimecast Blast Radius Lookup** — Blast-radius abstraction with graceful `unavailable` degradation when Mimecast isn't configured (completed 2026-07-15)
- [x] **Phase 18: Campaign Grouping & Phishing Analysis API** — Message-ID-first dedupe/grouping, on-demand single-ticket analysis, and the first `/api/phishing/*` routes with the ACCESS-01 auth convention (blocking gap CR-03 found via live verification 2026-07-16 — duplicate campaign on single-report re-analyze — see 18-VERIFICATION.md) (completed 2026-07-16)
- [x] **Phase 19: Classification Engine** — Deterministic SPAM/UNWANTED/THREAT rule classifier over bounded structured evidence, KnowBe4-simulation guard, (re-)trigger API (completed 2026-07-16)
- [x] **Phase 20: Remediation, Approval & Audit Safety** — Proposed-only remediation actions, approve/remediate/mark-false-positive APIs, idempotent re-run, full audit trail (completed 2026-07-16)
- [x] **Phase 21: Autotask Triage Note** — Sanitized internal triage note posted via existing safe note-write path, or returned via API if no such path exists (completed 2026-07-16)
- [x] **Phase 22: Approval UI (LiveLink)** — Ticket-ID-addressable Pulse page (Autotask LiveLink target) showing campaign timeline, evidence, and classification, with approve/remediate/mark-false-positive wired to the Phase 20 APIs (completed 2026-07-16)
- [x] **Phase 23: Classification Disposition + Per-Client Automation Gate** — Dedicated "User Awareness" verdict for confirmed phishing-simulation-vendor reports (currently forced into generic UNWANTED), plus an admin UI gate controlling per-company whether the phishing pipeline (parse/classify/report-to-ticket) runs automatically or requires manual trigger (completed 2026-07-17)
### Phase 15: Data Model, Detection & Ticket Evidence
**Goal**: The durable phishing-triage schema exists in Postgres, and Pulse can scan Autotask/Pulse tickets for known phishing/spam-report patterns idempotently, capturing base ticket-level evidence for each candidate.
**Depends on**: Nothing (first phase of v3.0)
**Requirements**: DETECT-01, DETECT-02, EVID-01
**Success Criteria** (what must be TRUE):
1. A new migration (`migrations/097_*.sql` or next available number) creates `campaigns`, `reports`, `messages`, `indicators`, `classifications`, `remediation_actions`, and `audit_events` tables with `IF NOT EXISTS`, ready for every later phase to read/write
2. Running the ticket scanner against Autotask/Pulse tickets flags candidates matching the known title/body patterns ("Phishing Report", "Spam Alert", "Phishing Alert - Email Security Report", "KnowBe4 Phish Alert Report", "Source: KnowBe4 Phish Alert Button", "userSubmissionsReportMessage", "reported message destinations", "Microsoft directly") and persists a `reports` row per candidate
3. Re-scanning tickets that haven't changed since last processed does not reprocess or duplicate their `reports` rows; a ticket whose Autotask data changed since last processed IS reprocessed (idempotent on ticket state, not just ticket ID)
4. Each flagged ticket's stored evidence includes ticket ID/number, company, requester/reporter, title, description, notes, relevant time entries, and attachment metadata (EVID-01)
**Plans**: 3 plans
- [x] 15-01-PLAN.md — Migration 097: 7-table phishing-triage schema (reports fully designed, others stubbed) (DETECT-01, DETECT-02, EVID-01)
- [x] 15-02-PLAN.md — phishing-detector.ts core: pattern matcher + content-hash idempotency + EVID-01 evidence capture + reports upsert (DETECT-01, DETECT-02, EVID-01)
- [x] 15-03-PLAN.md — Wiring: webhook fire-and-forget hook + bounded cron sweep service + scheduler branch + migration 098 seed (DETECT-01, DETECT-02)
**UI hint**: no
### Phase 16: EML/MIME Evidence Parser
**Goal**: Given a ticket's attachments, Pulse selects the correct original reported message and parses its RFC822/MIME structure into normalized, actionable evidence — without ever executing or fetching anything from the message.
**Depends on**: Phase 15 (messages/indicators tables to persist output into)
**Requirements**: EVID-02, EVID-03, EVID-04
**Success Criteria** (what must be TRUE):
1. Given synthetic fixtures with both `rfc.eml` and `OriginatingEmail.eml` present, the selection logic picks `rfc.eml` as the original reported message, matching case-insensitively and by `message/rfc822` content-type — not filename alone
2. Parsing a synthetic `.eml` fixture produces normalized headers (From, display name, sender email/domain, Reply-To, Return-Path, To, Cc, Subject, Date, Message-ID, Received chain, SPF/DKIM/DMARC results), a list of extracted URLs, and attachment metadata (name, content-type, size, hash)
3. The parser never executes or fetches any URL found in a message — verified by tests asserting no outbound network calls happen during parsing
4. Parsed output includes a sanitized/truncated body preview stored alongside the raw evidence, distinct from the full raw body
5. `npx vitest run` for the new parser test file passes using synthetic fixtures only (no real customer email)
**Plans**: 3 plans
- [x] 16-01-PLAN.md — Deps (mailparser + linkify-it) + pure EML parser: 3-tier selection, RFC822/MIME normalization, structured SPF/DKIM/DMARC verdicts, sanitized preview, no-network + size-guard tests (EVID-02, EVID-03, EVID-04)
- [x] 16-02-PLAN.md — Supporting infra: AutotaskClient.getAttachmentContent (items[0]), b2 EML_OBJECT_KEY_REGEX + parameterized key validation, migration 099 indicators.metadata JSONB (EVID-03, EVID-04; D-05, D-07)
- [x] 16-03-PLAN.md — phishing-eml-service orchestration: list→select→fetch→B2 (gated)→parse→persist messages/indicators, end-to-end no-network + graceful-degrade tests (EVID-03, EVID-04; D-05, D-06, D-07)
**UI hint**: no
### Phase 17: Mimecast Blast Radius Lookup
**Goal**: Pulse can ask "how far did this message spread" via a Mimecast blast-radius abstraction when Mimecast is configured, and gets a clean `unavailable` signal — never a crash or a block — when it isn't.
**Depends on**: Phase 15 (schema to store lookup results against)
**Requirements**: BLAST-01, BLAST-02
**Success Criteria** (what must be TRUE):
1. When Mimecast is configured, querying the blast-radius abstraction for a message (keyed on message ID, sender, recipient/reporter, subject, and date window) returns normalized delivery data — matched/delivered/held/rejected/clicked counts and per-recipient status
2. When Mimecast is not configured, the same lookup call returns `status: unavailable` synchronously rather than throwing, timing out, or blocking the caller
3. The lookup follows the existing `lib/services/` factory convention (`getMimecastClient()` + `isMimecastConfigured()`-equivalent) so Phase 19's classifier can call it without knowing whether Mimecast is present
**Plans**: 1 plan
- [x] 17-01-PLAN.md — isMimecastConfigured() gate + mimecast-blast-radius.ts fan-out/merge/cache orchestration + tests (BLAST-01, BLAST-02)
**UI hint**: no
### Phase 18: Campaign Grouping & Phishing Analysis API
**Goal**: Duplicate reports of the same phishing/spam campaign are automatically grouped and accumulate over time, and an operator can trigger analysis of a specific ticket or browse campaigns through a properly access-controlled `/api/phishing/*` surface.
**Depends on**: Phase 16 (parsed Message-ID/indicators to key grouping on)
**Requirements**: CAMP-01, CAMP-02, CAMP-03, DETECT-03, ACCESS-01
**Success Criteria** (what must be TRUE):
1. Two reports sharing the same original Message-ID are grouped into the same campaign; absent a shared Message-ID, reports sharing attachment-hash/URL-domain + subject + sender within a time window are grouped instead; absent that too, sender + normalized subject + client + time-window groups them as the final fallback
2. A campaign accumulates additional linked ticket reports and recipients as new duplicate reports arrive over time, without ever creating a second campaign for the same underlying report
3. `POST /api/phishing/tickets/{ticket_id}/analyze` runs detection + evidence extraction + campaign grouping for one specific ticket on demand and returns the resulting campaign linkage, instead of waiting for the next scheduled scan
4. `GET /api/phishing/campaigns` lists campaigns and `GET /api/phishing/campaigns/{id}` returns full detail (linked reports, messages, indicators, classification history)
5. Every `/api/phishing/*` route introduced in this phase calls `requireAuth()` (or `requirePermission()`) and rejects an unauthenticated/unauthorized request with 401/403 — establishing the auth convention every later phishing endpoint (Phases 19-21) must also follow
**Plans**: 3 plans (2 waves)
- [x] 18-01-PLAN.md — Campaign grouping service (tiered match + transactional find-or-create) + tests + phishing permission resource (CAMP-01, CAMP-02, ACCESS-01)
- [x] 18-02-PLAN.md — POST /api/phishing/tickets/{id}/analyze + wire groupReportIntoCampaign into webhook + cron sweep automatic paths (DETECT-03, CAMP-01, CAMP-02, ACCESS-01)
- [x] 18-03-PLAN.md — GET /api/phishing/campaigns list + GET /api/phishing/campaigns/{id} nested detail (CAMP-03, ACCESS-01)
**UI hint**: no
### Phase 19: Classification Engine
**Goal**: Every campaign gets a deterministic SPAM/UNWANTED/THREAT verdict, built from bounded structured evidence (never raw unbounded email), that correctly flags destructive-action recommendations for approval and doesn't cry wolf on routine KnowBe4 simulations.
**Depends on**: Phase 17 (blast-radius input), Phase 18 (campaign data input + auth convention)
**Requirements**: CLASSIFY-01, CLASSIFY-02, CLASSIFY-03, CLASSIFY-04, CLASSIFY-05, CLASSIFY-06
**Success Criteria** (what must be TRUE):
1. Classifying a campaign returns exactly one of `SPAM` / `UNWANTED` / `THREAT` with confidence, a short summary, evidence-backed reasons, recommended actions, and a `requires_approval` flag
2. A classification whose recommended actions include any destructive action (purge/block/delete/reset) always has `requires_approval: true` — proven by a test asserting the invariant can't be produced any other way
3. Classifying a campaign with incomplete evidence (no Mimecast data, no `.eml`) lowers confidence and names the specific missing evidence in the reasons
4. A synthetic KnowBe4 security-awareness-simulation fixture is not classified as `THREAT` absent contrary evidence
5. `POST /api/phishing/campaigns/{id}/classify` (re-)triggers classification, enforces the Phase 18 auth convention, and the classifier only ever receives structured, size-bounded evidence — long bodies are redacted/truncated before reaching any AI layer, and IT Glue-sourced evidence (if referenced) goes through the existing redacted `lib/services/analyzer/itglue-search.ts` path
**Plans**: 2 plans (2 waves)
- [x] 19-01-PLAN.md — campaign-classifier.ts deterministic rule engine (evidence gather + D-03/D-04/D-06 rules + D-05 confidence + D-08 actions + append-only INSERT) + vitest suite + synthetic KnowBe4/BSN fixtures (CLASSIFY-01, CLASSIFY-02, CLASSIFY-03, CLASSIFY-04, CLASSIFY-06)
- [x] 19-02-PLAN.md — POST /api/phishing/campaigns/[id]/classify route (requirePermission analyze + UUID guard + classifyCampaign delegation) (CLASSIFY-05)
**UI hint**: no
### Phase 20: Remediation, Approval & Audit Safety
**Goal**: Remediation actions are proposed, never auto-executed, and every approve/remediate/mark-false-positive action is gated by elevated permission, idempotent on re-run, and fully audited.
**Depends on**: Phase 18 (campaigns to act against), Phase 19 (classifications to approve/act on)
**Requirements**: REMED-01, REMED-02, REMED-03, REMED-04, REMED-05, REMED-06
**Success Criteria** (what must be TRUE):
1. Recommended remediation actions are persisted with status `proposed`, and no code path in this milestone executes one automatically
2. `POST /api/phishing/campaigns/{id}/approve` records approver, timestamp, and the exact approved action parameters, and is gated behind a permission level above plain read access (beyond the Phase 18 baseline)
3. `POST /api/phishing/campaigns/{id}/remediate` proceeds only for already-approved actions against a configured, non-destructive-by-default provider path; otherwise it returns `not_implemented`/an explicit failure and never silently succeeds without taking or logging an action
4. Re-running remediation against an already-completed action does not duplicate the destructive effect — proven by a test that calls remediate twice and asserts a single effect/log entry
5. `POST /api/phishing/campaigns/{id}/mark-false-positive` exists, and every state-changing action (classify, approve, remediate, mark-false-positive) writes an `audit_events` row recording actor, event type, and payload
**Plans**: 2 plans (2 waves)
- [x] 20-01-PLAN.md — phishing-audit.ts writeAuditEvent + remediation-service.ts approve/remediate/mark-false-positive orchestrators (idempotent, audited, D-04 guard) + vitest suite (REMED-01..06)
- [x] 20-02-PLAN.md — lib/permissions.ts approve/remediate grant (D-02) + approve/remediate/mark-false-positive routes + classify audit wiring (REMED-02, REMED-03, REMED-04, REMED-05, REMED-06)
**UI hint**: no
### Phase 21: Autotask Triage Note
**Goal**: Once a campaign is classified, a human-readable, sanitized internal triage note either gets posted to the Autotask ticket (if a safe write path already exists) or is returned via API for manual use — never a raw/unsanitized dump, never a silent no-op.
**Depends on**: Phase 19 (classification content to summarize), Phase 20 (recommended/approved remediation state to include)
**Requirements**: NOTE-01
**Success Criteria** (what must be TRUE):
1. If Pulse has a safe existing Autotask note-writing method, triggering note generation for a classified campaign posts an internal triage note summarizing classification, evidence, blast radius, and recommended actions to the originating ticket
2. The posted (or returned) note text is sanitized — no raw secrets/tokens/full malicious URL query strings appear in it
3. If no safe note-writing path exists, the same note content is returned via the API response instead of attempting any Autotask write, and no partial/unsanitized write is ever attempted as a fallback
**Plans**: 2 plans
- [x] 21-01-PLAN.md — Pure text layer: triage-note-sanitize (URL query/secret stripping) + triage-note-format (TriageNoteEvidence + formatTriageNote) with Vitest coverage (NOTE-01)
- [x] 21-02-PLAN.md — triage-note-service (evidence gather + per-ticket TicketNotes post loop + partial-failure result) + POST /api/phishing/campaigns/[id]/triage-note route (NOTE-01)
**UI hint**: no
### Phase 22: Approval UI (LiveLink)
**Goal**: A security operator opens an Autotask ticket, clicks a LiveLink button, and lands on a Pulse page scoped to that ticket showing the campaign's timeline, evidence, and classification — with approve/remediate/mark-false-positive actions right there, so no one is calling the Phase 20 APIs by hand.
**Depends on**: Phase 19 (classification + recommended action to display), Phase 20 (approve/remediate/mark-false-positive APIs the page calls)
**Requirements**: REVIEW-01, REVIEW-02, REVIEW-03, REVIEW-04, REVIEW-05, REVIEW-06
**Success Criteria** (what must be TRUE):
1. A stable, ticket-ID-addressable Pulse route (e.g. `/phishing/tickets/{ticketId}`) resolves the ticket to its campaign and renders that campaign's review page — suitable as an Autotask LiveLink target (LiveLink supplies the ticket ID as dynamic content; it does not know the internal campaign UUID), using the existing Better Auth session with no separate token/query-param auth
2. The page shows the campaign's timeline — linked reports, classification history, and audit events (classify/approve/remediate/mark-false-positive) — in chronological order
3. The page shows the gathered evidence — parsed EML headers/URLs/attachments (Phase 16), sanitized body preview, and Mimecast blast-radius data (Phase 17, including an explicit `unavailable` state when Mimecast isn't configured) — never rendering a raw/unsanitized body or unredacted secrets
4. The page shows the current classification (SPAM/UNWANTED/THREAT), confidence, reasons, and recommended remediation action(s) from Phase 19
5. Approve, remediate, and mark-false-positive buttons call the Phase 20 APIs directly from the page and reflect the resulting state (e.g. a remediated campaign shows as remediated, not re-offered for approval)
6. An operator without the elevated permission REMED-02/ACCESS-01 already require sees the approve/remediate actions disabled or hidden rather than a failed request; the page never uses a relaxed or separate permission check from the underlying APIs
**Plans**: 6 plans
- [x] 22-01-PLAN.md — Pure testable logic: ticket->campaign resolver, 7-action default-params, timeline merge (REVIEW-01, REVIEW-02, REVIEW-04)
- [x] 22-02-PLAN.md — Backend routes: new ticket->campaign resolver + extend campaign-detail (evidence/timeline/classification/blast radius) + list firstReportTicketId (REVIEW-01..04)
- [x] 22-03-PLAN.md — Evidence display: shadcn tooltip + inert UrlList (D-09) + tabbed EvidenceCard (REVIEW-03)
- [x] 22-04-PLAN.md — ClassificationCard + TimelineCard (REVIEW-02, REVIEW-04)
- [x] 22-05-PLAN.md — ActionAreaCard: approve/remediate/mark-false-positive with server-identical permission gating (REVIEW-05, REVIEW-06)
- [x] 22-06-PLAN.md — Review page + campaigns list page + nav entry (REVIEW-01, REVIEW-05, REVIEW-06)
**UI hint**: yes
### Phase 23: Classification Disposition + Per-Client Automation Gate
**Goal:** Add a dedicated "User Awareness" verdict for confirmed phishing-simulation-vendor (KnowBe4/Breach Secure Now) reports — today forced into the generic UNWANTED bucket despite the classifier already detecting the simulation vendor and explicitly skipping the THREAT tier — and add an admin UI gate page letting an admin choose, per Autotask company, whether the phishing pipeline's parse/classify/report-to-ticket stages run automatically (now that the previously-dead Autotask webhook is fixed) or require the existing manual Analyze/Classify/triage-note triggers.
**Requirements**: CLASSDISP-01, CLASSDISP-02, CLASSDISP-03, AUTOGATE-01, AUTOGATE-02, AUTOGATE-03
**Depends on:** Phase 17, Phase 18, Phase 19, Phase 20, Phase 21, Phase 22
**Plans:** 6/6 plans complete
Plans:
**Wave 1**
- [x] 23-01-PLAN.md — USER_AWARENESS verdict + acknowledge_user action + customer-visible note writer (noteType 18) (CLASSDISP-01, CLASSDISP-02)
- [x] 23-02-PLAN.md — Review UI: USER_AWARENESS badge + acknowledge_user manual action (CLASSDISP-03)
- [x] 23-03-PLAN.md — Migration 100 phishing_automation_gate + admin GET/PATCH/DELETE API (AUTOGATE-01)
**Wave 2** *(blocked on Wave 1 completion)*
- [x] 23-04-PLAN.md — /admin/phishing-automation page (3-toggle company table) + admin index tile (AUTOGATE-02)
- [x] 23-05-PLAN.md — Gate reader + gated parse->classify->acknowledge webhook chain (D-04 carve-out) (AUTOGATE-03)
**Gap closure** *(from 23-VERIFICATION.md, Truth #18 / CR-01)*
- [x] 23-06-PLAN.md — Idempotent + audited auto-post: autoPostAcknowledgment prevents duplicate customer-visible notes on repeat campaign webhooks (AUTOGATE-03)
</details>
## Progress
**Execution Order:**
Phases execute in numeric order. v1.0 (Phases 1-9.1) shipped 2026-07-10. v2.0 (Phases 10-14) shipped 2026-07-12. v3.0 (Phases 15-23) shipped 2026-07-17 — phases ran 15 → 16 → 17 → 18 → 19 → 20 → 21 → 22 → 23 in strict sequence; Phase 17 had no functional dependency on Phase 16 and could have run in parallel with it if split across two workstreams, but both had to complete before Phase 19; Phase 22 depended only on Phase 19 and Phase 20 and could equally have run in parallel with Phase 21; Phase 23 was a late addition depending on Phases 17-22.
| Phase | Milestone | Plans Complete | Status | Completed |
|-------|-----------|----------------|--------|-----------|
| 1. PWA Scaffolding | v1.0 | 2/2 | Complete | 2026-07-10 |
| 2. Mobile Shell + More Drawer | v1.0 | 2/2 | Complete | 2026-07-10 |
| 3. Dashboard Restyle | v1.0 | 2/2 | Complete | 2026-07-10 |
| 4. Tickets Restyle | v1.0 | 3/3 | Complete | 2026-07-10 |
| 5. Finance Restyle | v1.0 | 2/2 | Complete | 2026-05-03 |
| 6. Analyzer Feed | v1.0 | 3/3 | Complete | 2026-07-10 |
| 7. Engagement Overview | v1.0 | 3/3 | Complete | 2026-07-10 |
| 7.1. User Timezone Fix | v1.0 | 5/5 | Complete | 2026-07-10 |
| 8. Engagement User Profile | v1.0 | 2/2 | Complete | 2026-07-10 |
| 9. User Profile & Preferences | v1.0 | 6/6 | Complete | 2026-07-10 |
| 9.1. ntfy Backend Fix | v1.0 | 1/1 | Complete | 2026-07-10 |
| 10. PAX8 Client & Auth Foundation | v2.0 | 3/3 | Complete | 2026-07-10 |
| 11. Company, Catalog & Subscription Sync | v2.0 | 3/3 | Complete | 2026-07-11 |
| 12. Orders/Invoices & Company Matching | v2.0 | 5/5 | Complete | 2026-07-11 |
| 13. Scheduler & Admin Toggle | v2.0 | 3/3 | Complete | 2026-07-11 |
| 14. /pax8 UI Surface | v2.0 | 6/6 | Complete | 2026-07-12 |
| 15. Data Model, Detection & Ticket Evidence | v3.0 | 3/3 | Complete | 2026-07-15 |
| 16. EML/MIME Evidence Parser | v3.0 | 3/3 | Complete | 2026-07-15 |
| 17. Mimecast Blast Radius Lookup | v3.0 | 1/1 | Complete | 2026-07-15 |
| 18. Campaign Grouping & Phishing Analysis API | v3.0 | 5/5 | Complete | 2026-07-16 |
| 19. Classification Engine | v3.0 | 2/2 | Complete | 2026-07-16 |
| 20. Remediation, Approval & Audit Safety | v3.0 | 2/2 | Complete | 2026-07-16 |
| 21. Autotask Triage Note | v3.0 | 2/2 | Complete | 2026-07-16 |
| 22. Approval UI (LiveLink) | v3.0 | 6/6 | Complete | 2026-07-16 |
| 23. Classification Disposition + Per-Client Automation Gate | v3.0 | 6/6 | Complete | 2026-07-17 |
---
*Roadmap created: 2026-05-03*
*v2.0 phases added: 2026-07-10*
*v3.0 phases added: 2026-07-14 (Phases 15-21), 2026-07-16 (Phase 22, Phase 23), shipped 2026-07-17*
*Source spec (v1.0): `docs/superpowers/specs/2026-05-03-mobile-shell-design.md`*
*Source seed (v2.0): `.planning/seeds/SEED-002-pax8-integration.md`*
*Source requirements (v3.0, archived): `.planning/milestones/v3.0-REQUIREMENTS.md`*
</content>