diff --git a/.claude/skills/pulse-overshell-b2-evidence/SKILL.md b/.claude/skills/pulse-overshell-b2-evidence/SKILL.md deleted file mode 100644 index 2f75f7d..0000000 --- a/.claude/skills/pulse-overshell-b2-evidence/SKILL.md +++ /dev/null @@ -1,146 +0,0 @@ ---- -name: pulse-overshell-b2-evidence -description: Use when running PowerShell on a Pulse-managed Windows endpoint via Datto RMM Overshell, uploading large output to Backblaze B2, and retrieving the parsed evidence — covers both the stdout-only and B2-transport pipelines, the dispatch API, webhook receiver, presigned download, and where results land in rmm_executions. ---- - -# Pulse Overshell → B2 → Retrieve - -Pulse has two PowerShell evidence pipelines, both terminating in the same -`rmm_executions` table. Pick the right one for the payload size, dispatch -via the API, then read the row back. - -## Decision: which transport? - -| Output size | Transport (`transport` column) | How results come back | -|---|---|---| -| ≤ ~50 KB stdout | `overshell_stdout` (default) | Worker polls Datto job → `raw_stdout` + `parsed_evidence` | -| Large (event logs, dumps) | `b2_upload` (LogLift) | Collector gzips → B2 PUT → webhook → Pulse downloads + slims | - -**There is no ad-hoc PowerShell input.** Only registered `RmmScript`s in -`lib/services/rmm/scripts/index.ts` can run. To add one, create -`lib/services/rmm/scripts/.ts` with a `parseOutput` that JSON-parses -a `ConvertTo-Json -Compress` tail, then add to `_all` in `index.ts` and -bump `version`. Registry test enforces uniqueness + credential-shape ban. - -## Dispatch (both transports) - -`POST /api/rmm/executions` (requires `rmm.execute` permission — admin/super-admin only): - -```ts -// site-anchored (runs on the client's WNP endpoint) -{ scriptId: 'get-ad-health', target: { type: 'site_anchor', companyId: 29861375 } } - -// asset-self (runs on a specific Datto device) -{ scriptId: 'loglift-eventlogs', - target: { type: 'asset_self', deviceUid: 'f7a8…', assetType: 'configuration', assetId: '12345' } } -``` - -Returns `{ executionId, status: 'queued' | 'running', ... }`. Rate limit: -**50 executions per user per 24h** — trips before `runQuickJob` and logs -to `analyzer_cost_audit` with `decision='blocked'`. Hard timeout: 5 min. - -## How LogLift uses B2 - -For `loglift-eventlogs` (the only B2-transport script today), Pulse -passes the collector four `runQuickJob` variables — `RunId`, `ClientId` -(Datto site uid), `WebhookUrl` (`${BETTER_AUTH_URL}/api/rmm/loglift/upload`), -`WebhookSecret` (`OPENCLAW_API_KEY`). The collector script (registered -inside Datto, not stored in Pulse) gzips its JSON and PUTs to: - -``` -{datto_site_uid}/{computer_name}/eventlogs_{YYYYMMDD_HHMMSS}.json.gz -``` - -…in bucket `B2_BUCKET` (default `wulf-audits`, region `us-west-002`). -Object-key shape is enforced by `OBJECT_KEY_REGEX` in -`lib/services/b2/client.ts` — anything else is rejected at the webhook. - -Then it POSTs to `/api/rmm/loglift/upload` with `x-openclaw-key: $WebhookSecret` -and the metadata body (`runId`, `clientId`, `computerName`, `objectKey`, -`summary`, etc. — see `docs/loglift-eventlog-pipeline-spec.md` for the -exact shape). Pulse calls `downloadToBuffer(objectKey)`, gunzips, runs -`redact()`, slims to `parsed_evidence`, and flips the row to `complete`. - -## Retrieving results - -### From an executionId you already have - -```ts -const r = await fetch(`/api/rmm/executions/${id}`).then(r => r.json()); -// r.execution.status, .raw_stdout (overshell_stdout), .parsed_evidence (both), .evidence_object_key (b2_upload) -``` - -### From SQL - -```sql --- Most-recent successful run per (company, script): -SELECT DISTINCT ON (target_company_id, script_id) - id, target_company_id, script_id, target_hostname, transport, - evidence_object_key, completed_at, jsonb_pretty(parsed_evidence) AS parsed - FROM rmm_executions - WHERE status = 'complete' - AND completed_at >= NOW() - INTERVAL '14 days' - ORDER BY target_company_id, script_id, completed_at DESC; -``` - -### Re-fetching the original gzip from B2 - -The slim payload is in Postgres; the **full gzip stays in B2 forever** for -forensic replay. From a Node script or API route: - -```ts -import { downloadToBuffer, presignDownload } from '@/lib/services/b2/client'; - -// In-process (capped at 25 MB by MAX_DOWNLOAD_BYTES): -const buf = await downloadToBuffer(row.evidence_object_key); - -// Or hand a short-lived URL to a human / external tool: -const url = presignDownload(row.evidence_object_key, 600); // 10-minute GET -``` - -`presignDownload` / `presignUpload` validate `OBJECT_KEY_REGEX` and sign -with SigV4 against `B2_KEY_ID` / `B2_APP_KEY` from env. They throw -`B2NotConfiguredError` if either is unset. - -## Where things live - -- Dispatch: `lib/services/rmm/executor.ts` (`queueExecution`) -- Worker (stdout polling + timeout sweep): `lib/services/rmm/worker.ts` -- LogLift receiver: `lib/services/rmm/loglift-receiver.ts` → - `app/api/rmm/loglift/upload/route.ts` -- B2 SigV4 client: `lib/services/b2/client.ts` -- Script registry: `lib/services/rmm/scripts/index.ts` -- Settings (component_uids, variable name): `lib/services/rmm/settings.ts` → - `/admin/rmm-overshell` for the UI + "Re-discover" buttons -- Schema: `migrations/077_rmm_overshell.sql` + `078_loglift_uploads.sql` - (adds `transport`, `evidence_object_key`, `run_id`) -- Specs: `docs/rmm-overshell-evidence-spec.md`, `docs/loglift-eventlog-pipeline-spec.md` - -## Gotchas - -- **The worker is a side-effect import.** Anything that needs it running - must `import '@/lib/services/rmm/worker'` somewhere on the server. The - POST `/api/rmm/executions` route already does this. Don't eager-import - from hot paths or shared utilities. -- **Worker ignores `transport='b2_upload'` rows for stdout polling.** The - webhook is the completion event. If the webhook never arrives, the - 5-minute timeout sweep marks the row `timeout`. -- **Redaction runs twice.** `lib/services/analyzer/itglue-redact.ts:redact()` - strips anything keyed `/password|secret|key|token|credential|api[_-]?key/i` - before persistence and again before the LLM prompt sees it. Don't try - to surface those fields — they're gone by the time you read the row. -- **B2 download cap is 25 MB hard.** Refuses content-length over the cap - and aborts mid-stream if it exceeds. Decompress cap is 100 MB ISIZE - (zip-bomb defense). -- **Object-key shape is strict.** Custom prefixes won't work — must be - `{client_id_or_uuid}/{computer_name}/eventlogs_{timestamp}.json.gz`. - If you need a new payload type, add a new key regex + a new transport - rather than loosening the existing one. -- **Auto-audit fires only on single-match Configurations.** Multi-match - hostnames land the evidence but skip the audit (logged with - `rmm.loglift.matched`, no `audit_triggered`). -- **Webhook is not HMAC-signed** — `x-openclaw-key` is the auth boundary. - If you expose the endpoint to a new collector, rotate `OPENCLAW_API_KEY`. -- **`B2_*` env vars are required.** `isB2Configured()` checks only - `B2_KEY_ID + B2_APP_KEY`; bucket/region/endpoint fall back to - `wulf-audits` / `us-west-002` / `s3.us-west-002.backblazeb2.com`. diff --git a/.gitignore b/.gitignore index 1a36f3a..4cbda39 100644 --- a/.gitignore +++ b/.gitignore @@ -44,14 +44,3 @@ routes.ts # fonts *.woff2 - -# local scratch data (real customer/financial data — never commit) -dev/fin/ -dev/seubert-laptops.csv - -# Claude Code local runtime state — agent worktree checkouts, personal -# permission overrides, and the scheduler lock are per-machine, not project -# config. .claude/skills/ (project skills) is intentionally NOT ignored. -.claude/worktrees/ -.claude/settings.local.json -.claude/scheduled_tasks.lock diff --git a/.planning/MILESTONES.md b/.planning/MILESTONES.md deleted file mode 100644 index 9f6e92f..0000000 --- a/.planning/MILESTONES.md +++ /dev/null @@ -1,18 +0,0 @@ -# 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`. - ---- diff --git a/.planning/PROJECT.md b/.planning/PROJECT.md index ed98d6e..ea7399d 100644 --- a/.planning/PROJECT.md +++ b/.planning/PROJECT.md @@ -15,58 +15,6 @@ A manager can open Pulse on their phone and, in under 30 seconds, see the state of the business and triage tickets — without ever needing to switch to desktop for read-only awareness. -## Current Milestone: v3.0 Phishing Triage Automation - -**Status:** ✅ Shipped 2026-07-17 (9/9 phases, 30/30 plans, all 38 v3.0 requirements Complete) - -**Goal:** Detect candidate phishing/spam report tickets in Autotask, extract -and parse original-message evidence, classify each as `SPAM` / `UNWANTED` / -`THREAT`, group duplicate reports into campaigns, and prepare (never -auto-execute) remediation actions behind an explicit human-approval gate. - -**Target features:** -- Ticket detector/scanner matching known phishing/spam-report patterns - (title/body keyword + Microsoft/KnowBe4 report signatures), idempotent - reprocessing -- `.eml` evidence extraction — prefers `rfc.eml` (original reported message) - over `OriginatingEmail.eml` (wrapper), case-insensitive name + `message/rfc822` - content-type matching -- RFC822/MIME parser producing normalized headers (From/Reply-To/Return-Path/ - To/Cc/Subject/Date/Message-ID/Received chain/auth results), URLs, and - attachment metadata (name/type/size/hash) — no URL detonation -- Campaign grouping/dedupe keyed on Message-ID, then attachment-hash/URL-domain - + subject + sender + time-window, then sender + normalized subject + client - + time-window -- Durable data model: campaigns, reports, messages, indicators, classifications, - remediation_actions, audit_events -- Mimecast blast-radius lookup abstraction (interface + normalized output), - gracefully `unavailable` when unconfigured — classification still proceeds - on ticket/email evidence alone -- Classifier service: deterministic rule layer + an LLM-abstraction plug point - (safe rule-based fallback if no LLM abstraction fits); structured evidence - in, not raw unbounded email; KnowBe4 simulations not auto-flagged as THREAT -- API surface under `/api/phishing/*`: list/get campaigns, analyze a ticket, - classify/approve/remediate/mark-false-positive a campaign -- Optional Autotask internal triage note (only if a safe note-write path - already exists; otherwise return note text via API) -- Remediation safety: destructive actions (purge/delete/block/reset) always - `requires_approval: true`, logged with approver/timestamp/params/result, - idempotent on re-run, fail closed if approval/config missing -- Tests: detection patterns, `.eml` selection logic, MIME parsing, campaign - dedupe, rule classification (SPAM/UNWANTED/THREAT), approval-required - safety, idempotent reprocessing, API happy-path + permission failures — - synthetic fixtures only, no real customer email - -Unrelated in domain to the v2.0 PAX8 Integration above — this milestone adds a -new security/triage backend feature (Autotask ticket + email evidence, not -subscription billing). No seeds matched this scope; introduced directly from -a detailed spec provided at milestone-start. - -**Non-goals:** no automatic tenant-wide purge, no automatic password -reset/session revocation, no URL sandbox/detonation, no fully automated ticket -closure, no assumption that Graph is the eventual purge mechanism (Defender/ -Exchange purge may be preferable later). - ## Requirements ### Validated @@ -119,213 +67,18 @@ Exchange purge may be preferable later). `/api/mobile/engagement/trend` (daily-hours time-series). Existing `/api/engagement/users` reused as-is. Read-only on mobile. Validated in Phase 7: Engagement Overview (ENG-01..05, ENG-09) -- ✓ Per-user IANA timezone — `user.timezone` column (default `'UTC'`), - `session.user.timezone` exposed via Better Auth `additionalFields`, - `GET`/`PUT /api/me/timezone` (IANA-validated; allowlist includes `UTC`/`Etc/UTC`/`GMT`), - shared `getUserTimezone(session)` server helper, shared `useUserTimezone()` - client hook (single source of truth — 52 client files migrated), all 6 - affected read paths (`/api/dashboard/{overview,trends}`, - `/api/mobile/{dashboard,finance,engagement/summary,engagement/trend}`) - switched to `(value AT TIME ZONE 'UTC') AT TIME ZONE $tz` day-boundary math. - `/api/mobile/finance` hardened with `requireAuth()`. Storage UTC unchanged. - Engagement `_snapshots`-derived metrics keep UTC bucketing (documented - carve-out). Validated in Phase 7.1: User Timezone Fix (TZ-01..04) -- ✓ Engagement user profile (NEW) — `/mobile/engagement/[userId]` real-page - profile replacing the desktop modal pattern. Server-side photo proxy at - `/api/mobile/engagement/user/[userId]/photo` (`MsGraphClient.getUserPhotoBytes()`, - `Cache-Control: private, max-age=3600`, neutral 404/502/503). Client page - composes six new components — Skeleton, Header (photo with onError → - initials fallback), MetricGrid (2×2), Breakdown (Time/Communication/ - Meetings with conditional Zoom + after-hours), RecentEntries, RecentMeetings. - Reuses `/api/engagement/user/[userId]` verbatim. Period chips (D7/D30/D90) - refetch on change. Scroll restoration on back is partial (sessionStorage - shim added but does not restore reliably; accepted as a known limitation). - Validated in Phase 8: Engagement User Profile (ENG-06..08) -- ✓ User profile & preferences (NEW) — `/mobile/profile` page with four - sections: Timezone (Combobox + 400ms debounce + live clock), Theme - (light/dark/system radio rows persisted to `user.theme` with cross-device - sync via `ThemeSessionBridge` + desktop `ThemeToggle` write-through), - Notifications matrix (event_key × channel_type, opt-in default), and - Channels (personal Teams webhook + ntfy topic with QR code, per-channel - test-send, inline 400-error display). Three new migrations: 084 - (`user.theme`), 085 (`notification_channels.owner_user_id` + partial unique - index), 086 (`notify_event_keys` + `user_event_subscriptions`). API surface - at `/api/me/{theme,channels,channels/[type],channels/[type]/test,notification-subscriptions}` - all session-scoped via `requireAuth()`; shared `lib/services/personal-channels.ts` - centralizes validation, ntfy topic minting, and test-send. Notify pipeline - step extended with `routing: explicit | event-key | hybrid` and 5 fallback - reasons (`user_muted`, `event_key_unknown`, `recipient_unresolvable`, - `channel_missing`, `channel_send_failed`); fully backward-compatible with - existing global notify configs. Admin surfaces: channels page Owner column - + filter, event-keys CRUD at `/admin/workflow/event-keys`, executions - browser at `/admin/workflow/executions` with "Show only fallbacks" filter. - Validated in Phase 9: User Profile & Preferences (PROF-01..04, TZ-CHOOSER-01..02, - THEME-01..05, CHAN-01..07, SUB-01..04, ROUTE-01..07) -- ✓ PAX8 client + factory (OAuth2 client-credentials, `lib/services/pax8-client.ts` - + `lib/services/pax8-factory.ts` + `lib/types/pax8.ts`) and the PAX8 schema - foundation (`migrations/091_pax8_tables.sql` — companies, products, - subscriptions, orders, order_items, company_match_review). Live auth-proof - confirmed against the real PAX8 API (`scripts/verify-pax8-auth.ts`). - Validated in Phase 10: PAX8 Client & Auth Foundation (PAX8-01, PAX8-02) -- ✓ Company, catalog & subscription sync (NEW) — `Pax8SyncService.fullSync` - (`lib/services/pax8-sync-service.ts`) reads companies/subscriptions/products - from PAX8 (read-only pagination helpers on `Pax8Client`) and upserts into - Postgres with dual customer-price/partner-cost columns - (`migrations/092_pax8_subscription_costs.sql`), a referenced-only readable - product catalog (name + category via join, not bare SKUs), and soft-delete - reconciliation on every run. Fire-and-forget trigger at session-gated - `POST /api/pax8/sync` (409 on concurrent run) with `GET` status/counts. - Live-verified against the real PAX8 API: 118 companies, 445 subscriptions - all carrying cost data, 0 unreferenced products, stable across re-run. - Validated in Phase 11: Company, Catalog & Subscription Sync - (PAX8-03, PAX8-04, PAX8-05, PAX8-08) -- ✓ Historical PAX8 invoice/order-item costs synced per company with billing - periods (`pax8_order_items`: 29,311 rows across ~56k live history), - fuzzy-name Autotask company matching via pg_trgm (0.90 auto-link threshold, - 80 confident auto-matches), no-match/ambiguous companies flagged in - `pax8_company_match_review` instead of silently guessed (38 unresolved: - 16 no-match, 22 ambiguous), idempotent across re-syncs (resolved matches - never overwritten). Validated in Phase 12: Orders/Invoices & Company - Matching (PAX8-06, PAX8-10, PAX8-11) -- ✓ Daily scheduled sync + admin disable toggle — idempotent `pax8-daily` - cron row (`migrations/096_pax8_daily_schedule.sql`, disabled by default), - dual-guarded scheduler branch (`lib/services/sync-scheduler.ts`) that - skips `Pax8SyncService.fullSync('scheduled')` when PAX8 is unconfigured - or disabled via `integration_settings`, PAX8 registered as a toggleable - `/admin/integrations` row (`checkConfigOnly` in `integration-health.ts`), - and a 403 disabled-gate on `POST /api/pax8/sync`. Live-verified: scheduled - tick fires and runs the full sync (30,385 rows upserted), disable causes a - skip log with no new sync_history row, re-enable resumes on the next tick - with no restart. Admin-UI row rendering and the manual-route 403/200 still - need a human browser session (tracked in `13-HUMAN-UAT.md`). Validated in - Phase 13: Scheduler & Admin Toggle (PAX8-07, PAX8-09) -- ✓ `/pax8` UI surface — Companies tab (`DataTable` of 118 PAX8 companies, - sortable/searchable) with a cost-breakdown drill-down (`DetailModal` - extended with a `kind='pax8_company'` field group + subscriptions table, - correctly windowed per-subscription latest billed line — not a single - global max), and a Needs Review tab (amber-card queue of the 38 unresolved - matches from Phase 12, candidate-button + manual-search resolve, admin-gated - write). Live-verified end-to-end: 10 reviews resolved via both resolve - paths, persisted (`match_method='manual'`, `resolved_at` set) and confirmed - to survive a real subsequent PAX8 sync untouched; non-admin resolve attempt - correctly rejected with 403. One critical bug (bigint/Zod mismatch breaking - the manual-search path) and one unrelated pre-existing app-wide bug - (`hasPermission()` role-name shadowing, crashing every non-admin permission - check with a 500 instead of 403) were found and fixed during verification. - Validated in Phase 14: /pax8 UI Surface (PAX8-12, PAX8-13, PAX8-14) -- ✓ Phishing-triage data model + ticket detection + base evidence — new - migrations 097 (7-table schema: `campaigns`, `reports`, `messages`, - `indicators`, `classifications`, `remediation_actions`, `audit_events` — - `reports` fully designed, the rest stubbed for later phases) and 098 - (disabled-by-default `phishing-sweep` schedule seed). Shared - `lib/services/phishing-detector.ts` core: plain `.includes()` pattern - matcher (no regex/ReDoS surface) against the 8 locked title/description - signatures, content-hash idempotency scoped to title+description only (so - status/assignee churn never triggers reprocessing), and EVID-01 evidence - capture (company, requester/reporter, notes, time entries, attachment - metadata — `is_deleted=false` filtered, never base64 attachment content). - Wired into both established scan triggers: fire-and-forget hook on the - `ticket.created` webhook (near-real-time) and a bounded (500-row, 7-day - window) daily cron sweep for reconciliation. A code-review pass caught and - fixed a critical bug pre-ship: the webhook path branched on an Autotask - payload field that is never actually populated, so it silently never - detected anything — fixed by reading the ticket back from Postgres instead - (verified independently by phase verification, not just the fix commit). - Forward-only per this milestone's design (no backlog backfill); the - reconciliation sweep is disabled by default pending an admin opt-in, same - convention as `pax8-daily`. Validated in Phase 15: Data Model, Detection & - Ticket Evidence (DETECT-01, DETECT-02, EVID-01) -- ✓ EML/MIME evidence parser (NEW) — pure, I/O-free `lib/services/eml-parser.ts` - (`mailparser` + `linkify-it`): three-tier `.eml` attachment selection, - RFC822/MIME header normalization with hand-rolled SPF/DKIM/DMARC verdicts, - deduped URL extraction, sanitized/truncated body preview — test-enforced to - never trigger a network call. `AutotaskClient.getAttachmentContent()` - fetches the raw bytes; `parseAndStoreMessage()` orchestrates - list→select→fetch→size-guard→optional-B2-store→parse→persist (one - `messages` row + per-indicator `indicators` rows). Migration 099 - (`indicators.metadata` JSONB). Validated in Phase 16: EML/MIME Evidence - Parser (EVID-02, EVID-03, EVID-04) -- ✓ Mimecast blast-radius lookup (NEW) — `getBlastRadius()` composes - `searchDeliveredMessages`/`getHeldMessages`/`getThreatEvents` into normalized - matched/delivered/held/rejected/clicked counts + per-recipient status, - gated by `isMimecastConfigured()`, Redis-cached 5 min, never throws — - degrades to `status: 'unavailable'` on missing config or lookup failure. - Validated in Phase 17: Mimecast Blast Radius Lookup (BLAST-01, BLAST-02) -- ✓ Campaign grouping & phishing analysis API (NEW) — `groupReportIntoCampaign` - called automatically from both the webhook `ticket.created` path and the - cron sweep so campaigns accumulate without any API call; `POST - /api/phishing/tickets/{ticket_id}/analyze` for on-demand - detect→parse→group; `GET /api/phishing/campaigns` (paginated list) and - `GET /api/phishing/campaigns/{id}` (nested detail), both - `requirePermission('phishing','read')`-gated. Two report_count/duplicate- - campaign bugs (CR-02, CR-03) caught by code review and fixed pre-ship, - proven by regression tests + a live-database re-verification. Validated in - Phase 18: Campaign Grouping & Phishing Analysis API (CAMP-01, CAMP-02, - CAMP-03, DETECT-03, ACCESS-01) -- ✓ Classification engine (NEW) — deterministic, zero-LLM - `lib/services/campaign-classifier.ts`: SPAM/UNWANTED/THREAT verdict, - KnowBe4/Breach-Secure-Now simulation sender-domain allowlist (exact-or- - subdomain match only, no substring spoofing), THREAT gate requiring - delivery + malicious signal, confidence scoring, append-only - `classifications` insert. On-demand reclassify via `POST - /api/phishing/campaigns/{id}/classify`. Validated in Phase 19: - Classification Engine (CLASSIFY-01 through CLASSIFY-06) -- ✓ Remediation, approval & audit safety (NEW) — transactional - approve/remediate/mark-false-positive service layer, idempotent re-run - (REMED-04), single append-only audit writer for every state change - (REMED-06); all 7 action types (quarantine/block/purge/warn/reset/isolate/ - disable-forwarding) remain simulated status-only transitions this - milestone — no real destructive execution. Validated in Phase 20: - Remediation, Approval & Audit Safety (REMED-01 through REMED-06) -- ✓ Autotask triage note (NEW) — sanitized formatter (URL query/fragment - stripping, credential redaction) + `generateAndPostTriageNote(campaignId)` - posting one internal Autotask note per linked ticket, independent - per-ticket failure isolation. Validated in Phase 21: Autotask Triage Note - (NOTE-01) -- ✓ Approval UI / LiveLink (NEW) — ticket-ID-addressable - `/phishing/tickets/{ticketId}` review page (numeric Autotask ticket id, - not internal campaign UUID — confirmed live in production as a real - LiveLink target), composing ClassificationCard/ActionAreaCard/ - EvidenceCard/TimelineCard behind the existing Better Auth session only. - Evidence card renders parsed EML headers/URLs/attachments/body preview and - Mimecast blast-radius with zero clickable-link surface and zero raw-HTML - rendering of attacker-controlled content. Client-side permission gating - verified to match server-side 1:1. Validated in Phase 22: Approval UI - (LiveLink) (REVIEW-01 through REVIEW-06) — first formal verification pass - for this phase was run retroactively at v3.0 close (2026-07-17); 5 manual - browser click-through checks remain outstanding, see - `22-VERIFICATION.md`'s `human_verification` list before treating the UI as - fully signed off in a fresh deployment -- ✓ Classification disposition + per-client automation gate (NEW) — a 4th - `USER_AWARENESS` verdict for confirmed phishing-simulation-vendor reports - (previously forced into generic UNWANTED) mapping to a non-destructive - `acknowledge_user` action that posts a customer-visible thank-you note - (Autotask `noteType: 18`); a per-Autotask-company opt-in automation gate - (`auto_parse`/`auto_classify`/`auto_report`, all-off default) with an admin - UI (`/admin/phishing-automation`) gating the webhook's auto pipeline — - every other verdict/action still requires manual approval regardless of - gate state. A post-ship code-review pass caught a duplicate-note bug - (repeat webhooks re-posting the same acknowledgment as a campaign - accumulated more reports) and a stale-`completedAt` bug, both fixed via an - idempotent, audit-persisting `autoPostAcknowledgment()` wrapper before - milestone close. Validated in Phase 23: Classification Disposition + - Per-Client Automation Gate (CLASSDISP-01 through CLASSDISP-03, - AUTOGATE-01 through AUTOGATE-03) ### Active - + -None yet — run `/gsd:new-milestone` to define the next milestone's requirements. +- [ ] Engagement user profile (NEW) — `/mobile/engagement/[userId]` real-page + profile replacing the desktop modal pattern (Phase 8 — ENG-06..08) ### Out of Scope -- PAX8 write access (seat adjustments, placing orders) — read-only in v2.0; - revisit only if reconciliation surfaces a concrete need to act, not just view -- General natural-language data assistant / chatbot over Pulse data — separate - future milestone (SEED-003), deliberately not bundled with the PAX8 data sync - Service worker / offline cache / push notifications — deferred until a clear offline use-case lands - Tablet breakpoint (`md:max-w-2xl`) — noted as follow-up, keep `max-w-lg` @@ -354,23 +107,6 @@ None yet — run `/gsd:new-milestone` to define the next milestone's requirement - **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 @@ -402,11 +138,6 @@ None yet — run `/gsd:new-milestone` to define the next milestone's requirement | 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 @@ -426,4 +157,4 @@ This document evolves at phase transitions and milestone boundaries. 4. Update Context with current state --- -*Last updated: 2026-07-17 — v3.0 Phishing Triage Automation milestone shipped (Phases 15-23)* +*Last updated: 2026-05-04 — Phase 7 complete (Engagement Overview)* diff --git a/.planning/REQUIREMENTS.md b/.planning/REQUIREMENTS.md new file mode 100644 index 0000000..461adca --- /dev/null +++ b/.planning/REQUIREMENTS.md @@ -0,0 +1,206 @@ +# Requirements: Pulse Mobile Shell Redesign + +**Defined:** 2026-05-03 +**Core Value:** A manager can open Pulse on their phone and, in under 30 seconds, see the state of the business and triage tickets — without ever needing to switch to desktop for read-only awareness. +**Source spec:** `docs/superpowers/specs/2026-05-03-mobile-shell-design.md` + +## v1 Requirements + +Requirements for this milestone. Each maps to a spec section and a roadmap phase. + +### PWA — Progressive Web App scaffolding (spec §4) + +- [ ] **PWA-01**: `public/manifest.json` exists with name "Pulse", short_name "Pulse", `display: "standalone"`, `start_url: "/mobile"`, theme/background colors matching dark and light shells +- [ ] **PWA-02**: Manifest is referenced from `app/layout.tsx` via `` +- [ ] **PWA-03**: Viewport meta in `app/layout.tsx` includes `viewport-fit=cover` +- [ ] **PWA-04**: Header and bottom tab bar respect `env(safe-area-inset-top)` and `env(safe-area-inset-bottom)` (Tailwind arbitrary values or shared utility class) + +### Shell — `/mobile` layout (spec §5) + +- [x] **SHELL-01**: New `app/mobile/layout.tsx` replaces the current layout (rebuild in place — no parallel `/mobile-v2`) +- [ ] **SHELL-02**: Sticky top header: `bg-background/95 backdrop-blur` + bottom border; left = Wulf mark + "Pulse" wordmark linked to `/mobile/dashboard`; no page title in header +- [ ] **SHELL-03**: Header right slot — `Bell` icon button (placeholder, no menu/badge, `aria-label="Notifications"`, empty `onClick`, keyboard-accessible) +- [ ] **SHELL-04**: Header right slot — compact user avatar (`h-7 w-7`); tapping opens the More drawer +- [x] **SHELL-05**: `
` content area is scrollable with bottom padding equal to bottom-nav height + safe-area inset +- [ ] **SHELL-06**: Fixed bottom nav: `border-t bg-background`, `max-w-lg mx-auto` wrapper, 5 cells (4 tabs + More) + +### NAV — Bottom tab bar (spec §3.1) + +- [ ] **NAV-01**: Four equal-width primary tabs: Dashboard (`LayoutDashboard`), Tickets (`Ticket`), Finance (`DollarSign`), Analyzer (`Sparkles`) +- [ ] **NAV-02**: Tabs route to `/mobile/dashboard`, `/mobile/tickets`, `/mobile/finance`, `/mobile/analyzer` +- [ ] **NAV-03**: Active state uses `text-primary`, inactive uses `text-muted-foreground`; active detection via `pathname.startsWith(href)` + +### DRAWER — More Sheet drawer (spec §3.2) + +- [ ] **DRAWER-01**: Fifth bottom-bar control labeled "More" with `Menu` icon opens a shadcn `Sheet` +- [ ] **DRAWER-02**: Sheet uses a single consistent side (`right` or `bottom`) — pick one and stay consistent +- [ ] **DRAWER-03**: Drawer top section "Mobile sections" lists Engagement (`/mobile/engagement`) +- [ ] **DRAWER-04**: Drawer middle section "Full site" lists desktop-only pages (Quotes, Configuration Items, Backup Status, Ticket Digest, Admin / Sync) each with `ExternalLink` icon +- [ ] **DRAWER-05**: Drawer bottom section "Account" shows current user (avatar + email, read-only) and a Sign out action that calls `signOut()` then `router.push('/auth/sign-in')` +- [x] **DRAWER-06**: `app/mobile/nav/page.tsx` is deleted in the same change that ships the drawer + +### DASH — Mobile Dashboard (spec §6.1) + +- [ ] **DASH-01**: 2×2 KPI grid with four primary metric cards drawn from desktop dashboard hero stats +- [ ] **DASH-02**: "Needs Attention" horizontal-scroll strip of compact cards (overdue tickets, failed backups, stalled workflows); tapping a card opens its detail view +- [ ] **DASH-03**: Compact backup/worker status row showing analyzer worker, RMM worker, and backup-success-rate; read-only; tap opens desktop admin page +- [ ] **DASH-04**: No charts/recharts on the mobile Dashboard + +### TICK — Mobile Tickets list (spec §6.2) + +- [ ] **TICK-01**: Collapsible filter strip at top (`Collapsible` from shadcn), default collapsed; expanded shows status, priority, queue, assigned-to-me toggle +- [ ] **TICK-02**: Filter state syncs to URL query string for deep-linking +- [ ] **TICK-03**: List rows have left-edge color stripe by priority (Critical/High/Medium/Low → red/orange/amber/slate); body shows ticket #, title, company, age, assignee +- [ ] **TICK-04**: Single-tap on a row opens detail page +- [ ] **TICK-05**: Cursor-based infinite scroll (~25 per page) replaces pagination; next page triggers when last row enters viewport via IntersectionObserver +- [ ] **TICK-06**: "Load more" fallback button present for accessibility +- [ ] **TICK-07**: Detail page (`/mobile/tickets/[id]`) header reskinned to match new shell (Wulf mark, breadcrumb back); body kept largely as-is + +### FIN — Mobile Finance (spec §6.3) + +- [x] **FIN-01**: Page restyled with new Card and typography scale; spacing fixed for small phones +- [x] **FIN-02**: Wide tables on mobile widths replaced with stacked lists; no new data, no new sections + +### ANL — Mobile Analyzer feed (spec §6.4) — NEW PAGE + +- [ ] **ANL-01**: `/mobile/analyzer` route exists (read-only feed, most-recent-first stream of AI ticket analyses) +- [ ] **ANL-02**: Each list row shows ticket #, title, analyzer one-line summary, confidence badge, stage indicator (Triage → Analyze → Deep Review) +- [ ] **ANL-03**: Tapping a row opens a mobile summary view rendering Summary, Next Step, Next Step Rationale (all already produced by the analyzer pipeline) +- [ ] **ANL-04**: Summary view includes "View full analysis" link out to the desktop analyzer page +- [ ] **ANL-05**: No editing, no re-run, no prompt tuning on mobile +- [ ] **ANL-06**: Source data via `/api/mobile/analyzer/feed` (or reuse an existing list endpoint if it returns the right shape) reading from `analyzer_analyses` + +### ENG — Mobile Engagement (spec §6.5) — NEW PAGES + +- [ ] **ENG-01**: `/mobile/engagement` overview page (real refactor, not a thin adaptation of the ~1300-line desktop page) +- [ ] **ENG-02**: Period selector chip row (today / 7d / 30d) sticky just below the page H1 +- [ ] **ENG-03**: Summary cards stacked single-column (active users, total Graph hours, total Autotask hours, hours-per-active-user) — no 4-up grid on phone widths +- [ ] **ENG-04**: Per-employee list as stacked rows (avatar/initials, name, role, hours bar) with sort control above (sort by hours, name, utilization) and search input +- [ ] **ENG-05**: Top of list shows compact "hours trend" sparkline scoped to the selected period; no multi-series chart on mobile +- [ ] **ENG-06**: User profile is `/mobile/engagement/[userId]` (segment form preferred for shareable URLs); single-column layout: identity header → period selector → key metrics (compact) → activity breakdown list → recent items +- [ ] **ENG-07**: User profile is a real page, not a modal — replaces desktop user-detail modal pattern on mobile so back gesture works +- [ ] **ENG-08**: Profile reuses existing engagement profile data endpoints; no new data +- [ ] **ENG-09**: Engagement is reachable from the More drawer, NOT the bottom bar + +### TZ — User Timezone (Phase 7.1, urgent insertion) + +- [ ] **TZ-01**: Better Auth users table extended with a `timezone` field (IANA string). Default for users with no value = `process.env.DEFAULT_TIMEZONE || 'UTC'`. Existing rows backfill to the default; UTC remains the storage timezone for all date columns +- [ ] **TZ-02**: Date math that powers dashboards, ticket filters, finance views, and engagement period selectors computes day/week boundaries against the viewing user's `timezone` — NOT UTC, NOT the browser's local zone (browsers may differ from the user's chosen tz, e.g. travel) (Exception: `engagement_snapshots`-derived metrics — active users D7/D30/D90, MS Graph hours — bucket by UTC at sync time and remain UTC-bucketed in this phase; ≤24h drift is accepted because engagement is an admin-overview surface, not an operational date display. Per-user snapshot bucketing is deferred to a future phase if needed.) +- [ ] **TZ-03**: `GET /api/me/timezone` (auth required) returns `{ timezone, source: 'user' | 'default' }`. `PUT /api/me/timezone` accepts `{ timezone }`, validates against `Intl.supportedValuesOf('timeZone')`, persists to the user row, returns the new value +- [ ] **TZ-04**: Shared client hook `useUserTimezone()` reads tz from `useSession()` (Better Auth additionalField). All date-formatting and range-bucketing in mobile + desktop pages goes through this hook — no scattered `Intl.DateTimeFormat` instantiations with hardcoded zones + +## v2 Requirements + +Acknowledged but deferred. Not in this milestone's roadmap. + +### NOTIF — Notifications + +- **NOTIF-01**: Real notification list behind the Bell icon (replaces SHELL-03 placeholder) +- **NOTIF-02**: Notification badge logic on the Bell icon + +### TABLET — Tablet breakpoint + +- **TABLET-01**: `md:max-w-2xl mx-auto` wrapper for tablet widths + +### OFFLINE — Offline support + +- **OFFLINE-01**: Service worker for offline cache +- **OFFLINE-02**: Push notifications (requires SW) + +### EDIT — Mobile editing + +- **EDIT-01**: Mobile editing on Engagement user detail +- **EDIT-02**: Mobile re-run / prompt edits on Analyzer + +## Out of Scope + +Explicitly excluded for v1. Documented to prevent scope creep. + +| Feature | Reason | +|---------|--------| +| Service worker / offline cache / push notifications | No clear offline use-case yet — defer until one lands (spec §4, §7) | +| Tablet breakpoint (`md:max-w-2xl`) | Noted as follow-up; keep `max-w-lg` for v1 (spec §4, §7) | +| Real notification list behind the Bell | Placeholder only this iteration; future phase wires it (spec §5.1, §7) | +| Mobile editing on Engagement user detail | Read-only on mobile by design (spec §6.5, §7) | +| Mobile re-run / prompt tuning on Analyzer | Read-only on mobile by design (spec §6.4, §7) | +| Charts / recharts on mobile Dashboard | Not earning their weight on small widths (spec §6.1, §7) | +| Restyling/replacing desktop pages reachable from More drawer | Desktop pages stay as-is (spec §7) | +| Multi-series chart on mobile Engagement overview | Replaced by single sparkline (spec §6.5) | +| Modal-based user detail on mobile | Replaced by real page so back gesture works (spec §6.5) | +| `/mobile-v2` parallel directory | Rebuild `/mobile` in place — keep canonical URLs (spec §2) | +| Per-user-tz `engagement_snapshots` bucketing (TZ-02 carve-out) | Engagement metrics derived from `engagement_snapshots` (active users D7/D30/D90, MS Graph hours) are bucketed by UTC at sync time. Per-user re-bucketing would require either per-request re-bucket (expensive) or per-user snapshot rebuild (doubles storage). ≤24h drift accepted on this admin-overview surface. May revisit in a future phase. | + +## Traceability + +Updated during roadmap creation. + +| Requirement | Phase | Status | +|-------------|-------|--------| +| PWA-01 | Phase 1 | Pending | +| PWA-02 | Phase 1 | Pending | +| PWA-03 | Phase 1 | Pending | +| PWA-04 | Phase 1 | Pending | +| SHELL-01 | Phase 2 | Complete | +| SHELL-02 | Phase 2 | Pending | +| SHELL-03 | Phase 2 | Pending | +| SHELL-04 | Phase 2 | Pending | +| SHELL-05 | Phase 2 | Complete | +| SHELL-06 | Phase 2 | Pending | +| NAV-01 | Phase 2 | Pending | +| NAV-02 | Phase 2 | Pending | +| NAV-03 | Phase 2 | Pending | +| DRAWER-01 | Phase 2 | Pending | +| DRAWER-02 | Phase 2 | Pending | +| DRAWER-03 | Phase 2 | Pending | +| DRAWER-04 | Phase 2 | Pending | +| DRAWER-05 | Phase 2 | Pending | +| DRAWER-06 | Phase 2 | Complete | +| DASH-01 | Phase 3 | Pending | +| DASH-02 | Phase 3 | Pending | +| DASH-03 | Phase 3 | Pending | +| DASH-04 | Phase 3 | Pending | +| TICK-01 | Phase 4 | Pending | +| TICK-02 | Phase 4 | Pending | +| TICK-03 | Phase 4 | Pending | +| TICK-04 | Phase 4 | Pending | +| TICK-05 | Phase 4 | Pending | +| TICK-06 | Phase 4 | Pending | +| TICK-07 | Phase 4 | Pending | +| FIN-01 | Phase 5 | Complete | +| FIN-02 | Phase 5 | Complete | +| ANL-01 | Phase 6 | Pending | +| ANL-02 | Phase 6 | Pending | +| ANL-03 | Phase 6 | Pending | +| ANL-04 | Phase 6 | Pending | +| ANL-05 | Phase 6 | Pending | +| ANL-06 | Phase 6 | Pending | +| ENG-01 | Phase 7 | Pending | +| ENG-02 | Phase 7 | Pending | +| ENG-03 | Phase 7 | Pending | +| ENG-04 | Phase 7 | Pending | +| ENG-05 | Phase 7 | Pending | +| ENG-09 | Phase 7 | Pending | +| ENG-06 | Phase 8 | Pending | +| ENG-07 | Phase 8 | Pending | +| ENG-08 | Phase 8 | Pending | + +**Coverage:** +- v1 requirements: 47 total +- Mapped to phases: 47 +- Unmapped: 0 ✓ + +**Per-phase counts:** +- Phase 1 (PWA Scaffolding): 4 requirements +- Phase 2 (Mobile Shell + More Drawer): 15 requirements +- Phase 3 (Dashboard Restyle): 4 requirements +- Phase 4 (Tickets Restyle): 7 requirements +- Phase 5 (Finance Restyle): 2 requirements +- Phase 6 (Analyzer Feed): 6 requirements +- Phase 7 (Engagement Overview): 6 requirements +- Phase 8 (Engagement User Profile): 3 requirements + +--- +*Requirements defined: 2026-05-03* +*Last updated: 2026-05-07 — TZ-02 amended with engagement_snapshots carve-out (Phase 7.1 revision)* + + \ No newline at end of file diff --git a/.planning/RETROSPECTIVE.md b/.planning/RETROSPECTIVE.md deleted file mode 100644 index abd3ae8..0000000 --- a/.planning/RETROSPECTIVE.md +++ /dev/null @@ -1,59 +0,0 @@ -# Project Retrospective - -*A living document updated after each milestone. Lessons feed forward into future planning.* - -## Milestone: v3.0 — Phishing Triage Automation - -**Shipped:** 2026-07-17 -**Phases:** 9 (15-23) | **Plans:** 30 | **Sessions:** several, spanning 2026-07-14 through 2026-07-17 - -### What Was Built -- Full phishing-triage pipeline: detection → EML/MIME parsing → Mimecast blast-radius → deterministic classification → remediation/approval/audit → Autotask triage note → ticket-ID-addressable LiveLink review UI -- A per-Autotask-company automation gate (Phase 23) letting an admin opt companies into a fully automatic parse→classify→acknowledge webhook pipeline while every destructive action stays manual-approval-gated -- Zero-LLM, deterministic classifier with a hardcoded KnowBe4/Breach-Secure-Now simulation-vendor allowlist — avoids both an LLM prompt-injection surface and cry-wolf false THREATs on routine security-awareness tests - -### What Worked -- Code review (`/gsd:code-review`) caught two real, non-obvious bugs late in the milestone: a duplicate-campaign grouping bug in Phase 18, and — during this final session — a customer-visible duplicate-note bug in the Phase 23 gap-closure plan itself, plus a second-order defect (missing `actionId` breaking `completedAt` derivation) and a third (no server-side guard on the manual approval path re-triggering the same bug class). All three were fixed and re-verified before shipping, not left for a future session. -- The `--gaps` gap-closure flow (plan → check → execute → re-verify) worked cleanly for a narrowly-scoped, single-defect fix (Phase 23's AUTOGATE-03 idempotency bug) — one new plan, one wave, no disruption to the 5 already-shipped plans in that phase. -- Independent verification via direct Mimecast API calls (not just trusting a ticket's free-text description) caught that one "confirmed Breach Secure Now" report had no actual corroborating inbound message in Mimecast's logs — a real, actionable finding a code-only review would have missed. - -### What Was Inefficient -- Phase 22 (Approval UI) shipped all 6 plans and was marked "complete" without ever running `/gsd:verify-work` — this was only discovered at milestone-close time, during the REQUIREMENTS.md traceability check, not during Phase 22 itself. Retroactive verification found the code correct, but this was luck, not process — a phase should never reach "complete" status without a VERIFICATION.md. -- The decision-coverage gate flagged the same class of false-positive three times this milestone (Phase 18 gap closure, initial Phase 23 planning, Phase 23 gap-closure planning): CONTEXT.md decisions substantively implemented but not cited with a literal `D-NN:` prefix under a designated heading. Every instance required manual spot-check-and-override. The gate's designed escape hatch (cite by ID, or mark `[informational]`) isn't being followed by planners in practice — worth either relaxing the gate's matching heuristic or making the citation requirement more prominent in planner instructions. -- Several quick-tasks and REQUIREMENTS.md traceability rows (CLASSIFY-01..06, REVIEW-03) sat with stale "Pending" status for days after their phases actually shipped — pure bookkeeping lag with no functional impact, but it made the milestone-close audit noisier than it needed to be and could mask a real gap next time if not caught. - -### Patterns Established -- Idempotency fixes for auto-triggered customer-visible side effects should mirror the existing manual-path pattern in the same file (state write + audit row in one transaction, side-effect call post-commit in its own try/catch) rather than inventing a new shape — this made the Phase 23 gap-closure plan and its review both fast and low-risk. -- When a ticket/ticket-derived claim needs independent confirmation, query the actual upstream system (Mimecast trace/held-message logs) directly rather than trusting a technician's free-text paraphrase of what they saw. - -### Key Lessons -1. A phase reaching "6/6 plans complete" is not the same as "verified" — REQUIREMENTS.md traceability status and an actual VERIFICATION.md file should be checked as a gate, not assumed, especially before a milestone close. -2. Code review after a gap-closure fix is not optional busywork — it found real, non-obvious follow-on defects in freshly-written idempotency code twice in this milestone alone. -3. When multiple planning sessions hit the same gate override for the same underlying reason (decision-coverage citation format), that's a signal to fix the gate or the planner convention, not to keep manually overriding it. - -### Cost Observations -- Sessions: several across 2026-07-14 → 2026-07-17 -- Notable: the final session (gap-closure plan → execute → code-review-catch-and-fix → re-verify → milestone close) ran end-to-end in one sitting, including catching and fixing 2 code-review blockers and running a retroactive Phase 22 verification — the layered gate structure (plan-checker → code-review → verifier) did its job of catching defects before they shipped as "done." - ---- - -## Cross-Milestone Trends - -### Process Evolution - -| Milestone | Sessions | Phases | Key Change | -|-----------|----------|--------|------------| -| v1.0 | multiple | 11 (1-9.1) | Closed manually, no MILESTONES.md/archive convention yet | -| v2.0 | multiple | 5 (10-14) | Closed manually, same as v1.0 | -| v3.0 | several | 9 (15-23) | First milestone closed via `/gsd:complete-milestone` — established MILESTONES.md, `.planning/milestones/` archive, and this RETROSPECTIVE.md | - -### Cumulative Quality - -| Milestone | Tests | Coverage | Zero-Dep Additions | -|-----------|-------|----------|-------------------| -| v3.0 | 439/441 repo-wide (2 pre-existing, unrelated failures) | `lib/services/**` per CLAUDE.md's stated scope | 2 (`mailparser`, `linkify-it`) | - -### Top Lessons (Verified Across Milestones) - -1. Verification gates (plan-checker, code-review, phase-verifier) catch real defects late-stage — don't skip them even under time pressure to close a milestone. -2. Bookkeeping status fields (REQUIREMENTS.md checkboxes, traceability tables) drift from reality if not updated at the moment a phase actually ships — reconcile them explicitly at milestone close rather than assuming they're current. diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 4de2126..ca2e98d 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -1,723 +1,203 @@ -# Roadmap: Pulse +# Roadmap: Pulse Mobile Shell Redesign -## Milestones +## Overview -- ✅ **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) +Eight phases mirror the deliberate build order in the source spec +(`docs/superpowers/specs/2026-05-03-mobile-shell-design.md` §8). Each phase +ships independently to `master` — no big-bang merge. Phase 1 lays PWA +metadata and safe-area utilities. Phase 2 rebuilds `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 lands, Phases 3–7 are +independent restyles/new pages and may be executed in parallel; Phase 8 +follows Phase 7 because the user profile is reached from the Engagement +overview. All work happens in place under `/mobile/*` — no `/mobile-v2`, +no parallel routes. ## 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. -
-✅ v1.0 Mobile Shell Redesign (Phases 1-9.1) - SHIPPED 2026-07-10 - -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 3–7 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 +- [ ] **Phase 1: PWA Scaffolding** — Manifest, viewport meta, and safe-area utilities so the shell installs and paints under the home indicator +- [ ] **Phase 2: Mobile Shell + More Drawer** — New `app/mobile/layout.tsx` (header + 5-cell bottom nav) and Sheet drawer that replaces `/mobile/nav` +- [ ] **Phase 3: Dashboard Restyle** — 2×2 KPI grid, Needs Attention strip, worker/backup status row (no charts) +- [ ] **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 6: Analyzer Feed (NEW)** — `/mobile/analyzer` read-only stream + `/api/mobile/analyzer/feed` +- [ ] **Phase 7: Engagement Overview (NEW)** — `/mobile/engagement` phone-first overview reachable from the More drawer +- [ ] **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" +- [ ] **Phase 8: Engagement User Profile (NEW)** — `/mobile/engagement/[userId]` real-page profile that replaces the desktop modal pattern +- [ ] **Phase 9: User Profile & Preferences (NEW)** — `/mobile/profile` settings page (timezone chooser, theme, mobile push, Teams + ntfy channels) + +## Phase Details ### 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 `` 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) - +**Plans**: 5 plans +- [ ] 07.1-01-PLAN.md — Add timezone column to user table + Better Auth additionalField (TZ-01) +- [ ] 07.1-02-PLAN.md — /api/me/timezone GET + PUT with IANA validation (TZ-03) +- [ ] 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) +- [ ] 07.1-04-PLAN.md — useUserTimezone() client hook + reported-bug-surface mobile page migration + codebase-wide audit (TZ-04, TZ-02 client portion) +- [ ] 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) - +**Plans**: TBD **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. +**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), and personal notification channels (Teams webhook URL, 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) - +**Requirements**: TBD — flesh out via `/gsd-discuss-phase 9` before planning **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) - + 1. Tapping Profile/Account in the More drawer routes to `/mobile/profile` (real page, not modal) + 2. Page exposes four sections — Timezone, Theme, Notifications, Channels — each persisting per-user; cross-device consistent; gated by `requireAuth()` + 3. Notification routing in `lib/services/pipeline-steps/notify.ts` resolves per-user channel preferences when the event has a user owner, falling back to global channels otherwise +**Plans**: TBD **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) - -
- -
-✅ v2.0 PAX8 Integration (Phases 10-14) - SHIPPED 2026-07-12 - -**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 -(`-client.ts` + `-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 `isConfigured()` + 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 - -
- -
-✅ v3.0 Phishing Triage Automation (Phases 15-23) - SHIPPED 2026-07-17 - -**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) - -
- ## 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. +Phases execute in numeric order. Phase 2 unblocks Phases 3–7 (any order, parallelizable). Phase 8 follows Phase 7. -| 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 | - -### Phase 24: AWS Route 53 DNS Sync - -**Goal:** Sync DNS zones/records from AWS Route 53 into Postgres, support full CRUD back to Route 53 from Pulse, track record-level changes over time, log every sync and CRUD operation for audit, and integrate into the existing per-system sync section (scheduler, admin UI, health checks) alongside Autotask/Datto RMM/Veeam. AWS credentials are resolved via BWS (Bitwarden Secrets Manager), not plaintext env vars. -**Requirements**: SC-1, SC-2, SC-3, SC-4, SC-5, SC-6 (the numbered Success Criteria below serve as this phase's requirement IDs — this project has no REQUIREMENTS.md) -**Depends on:** Phase 23 -**Plans:** 7/7 plans complete - -Plans: -**Wave 1** - -- [x] 24-01-PLAN.md — Foundation: AWS SDK install, migration 102 (zones/records/history/audit tables), shared types, credential factory, BWS + DNS-egress checkpoint *(wave 1)* - -**Wave 2** *(blocked on Wave 1 completion)* - -- [x] 24-02-PLAN.md — Route53SyncService: zone/record mirror sync with pagination, soft-delete, and `sync_detected_drift` change history *(wave 2)* -- [x] 24-03-PLAN.md — Record validation (D-01 NS/SOA allowlist, AWS error sanitizer) + pending/committed/failed audit lifecycle persistence *(wave 2)* -- [x] 24-04-PLAN.md — Health check: auth probe + D-12 live NS-delegation comparison, registered in integration-health *(wave 2)* - -**Wave 3** *(blocked on Wave 2 completion)* - -- [x] 24-05-PLAN.md — `/api/route53/*` read routes, sync trigger, and CRUD write routes with `requireAdmin()` gating *(wave 3)* -- [x] 24-06-PLAN.md — Scheduler entries (`route53-incremental`, `route53-full`) + `/admin/sync` tile *(wave 3)* - -**Wave 4** *(blocked on Wave 3 completion)* - -- [x] 24-07-PLAN.md — `/admin/sync/route53` detail page, record editor dialog, end-to-end phase verification *(wave 4)* - -**Success Criteria:** - -1. Route 53 hosted zones and records sync into Postgres on a schedule, matching AWS as source of truth -2. Create/update/delete operations initiated from Pulse propagate to Route 53 via the AWS API -3. Every sync and CRUD operation is logged with actor, timestamp, and before/after values -4. Record-level change history is queryable (not just current state) -5. AWS credentials are resolved via BWS at runtime — never persisted in plaintext env vars -6. Integration appears in the existing sync admin UI/scheduler alongside other integrations +| Phase | Plans Complete | Status | Completed | +|-------|----------------|--------|-----------| +| 1. PWA Scaffolding | 1/2 | Executing | - | +| 2. Mobile Shell + More Drawer | 0/TBD | Not started | - | +| 3. Dashboard Restyle | 0/2 | Not started | - | +| 4. Tickets Restyle | 0/3 | Not started | - | +| 5. Finance Restyle | 2/2 | Complete | 2026-05-03 | +| 6. Analyzer Feed | 0/3 | Not started | - | +| 7. Engagement Overview | 0/3 | Not started | - | +| 7.1. User Timezone Fix | 0/5 | Not started | - | +| 8. Engagement User Profile | 0/TBD | Not started | - | +| 9. User Profile & Preferences | 0/TBD | Not started | - | --- *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`* +*Source spec: `docs/superpowers/specs/2026-05-03-mobile-shell-design.md`* + \ No newline at end of file diff --git a/.planning/STATE.md b/.planning/STATE.md index 27acac9..a03bce7 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -1,61 +1,65 @@ --- gsd_state_version: 1.0 -milestone: v3.0 -milestone_name: Phishing Triage Automation -status: completed -stopped_at: Phase 24 complete -last_updated: "2026-08-06T03:18:19.527Z" -last_activity: 2026-08-06 -- Phase 24 marked complete +milestone: v1.0 +milestone_name: milestone +status: executing +stopped_at: Phase 7 UI-SPEC approved +last_updated: "2026-05-07T11:01:40.811Z" +last_activity: 2026-05-07 -- Phase 7.1 planning complete progress: total_phases: 10 - completed_phases: 10 - total_plans: 37 - completed_plans: 37 - percent: 100 + completed_phases: 7 + total_plans: 22 + completed_plans: 17 + percent: 77 --- # Project State ## Project Reference -See: .planning/PROJECT.md (updated 2026-07-14) +See: .planning/PROJECT.md (updated 2026-05-03) -**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. -**Current focus:** Phase 24 complete — AWS Route 53 DNS sync live in production +**Core value:** A manager can open Pulse on their phone and, in under 30 seconds, see the state of the business and triage tickets — without ever needing to switch to desktop for read-only awareness. +**Current focus:** Phase 07 — Engagement Overview (NEW) ## Current Position -Phase: 24 (aws-route-53-dns-sync-track-changes-crud-operations-full-aud) — COMPLETE -Plan: 7 of 7 -Status: Phase 24 complete -Last activity: 2026-08-06 -- Phase 24 marked complete +Phase: 8 +Plan: Not started +Status: Ready to execute +Last activity: 2026-05-07 -- Phase 7.1 planning complete + +Progress: [░░░░░░░░░░] 0% ## Performance Metrics **Velocity:** -- Total plans completed: 66 (v1.0: 42, v2.0: 20 across phases 10-14 — see per-phase table) +- Total plans completed: 17 - Average duration: — -- Total execution time: 0.0 hours (v3.0) +- Total execution time: 0.0 hours **By Phase:** | Phase | Plans | Total | Avg/Plan | |-------|-------|-------|----------| -| 01-09.1 (v1.0) | 34 | - | - | -| 10-14 (v2.0) | 20 | - | - | -| 15-21 (v3.0) | TBD | - | - | -| 15 | 3 | - | - | -| 18 | 5 | - | - | -| 21 | 2 | - | - | -| 23 | 6 | - | - | +| 01 | 2 | - | - | +| 02 | 2 | - | - | +| 03 | 2 | - | - | +| 04 | 3 | - | - | +| 05 | 2 | - | - | +| 06 | 3 | - | - | +| 07 | 3 | - | - | **Recent Trend:** -- Last 5 plans: — (v2.0 closed 2026-07-12; v3.0 not yet executed) +- Last 5 plans: — - Trend: — *Updated after each plan completion* +| Phase 02-mobile-shell-more-drawer P02 | 8 | 4 tasks | 2 files | +| Phase 05-finance-restyle P02 | 2 | 2 tasks | 1 files | ## Accumulated Context @@ -64,50 +68,15 @@ Last activity: 2026-08-06 -- Phase 24 marked complete Decisions are logged in PROJECT.md Key Decisions table. Recent decisions affecting current work: -- v3.0 roadmap: 7 phases (15-21), each a hard dependency on at least one - predecessor except Phase 17 (Mimecast blast-radius), which only depends on - the Phase 15 schema and could be built in parallel with Phase 16 (EML - parser) if split across two workstreams — sequenced after 16 here for a - single execution thread - -- The durable schema (campaigns/reports/messages/indicators/classifications/ - remediation_actions/audit_events) lands in Phase 15, before any service - that writes to it — new migration, next number after 096 (097+) - -- ACCESS-01 is mapped to Phase 18 (the first phase introducing - `/api/phishing/*` routes) rather than a standalone terminal phase; every - later phishing endpoint (19, 20, 21) is expected to continue enforcing the - same `requireAuth`/`requirePermission` convention as a success-criteria - carry-forward, not a re-mapped requirement - -- Classification (Phase 19) is sequenced after both Phase 17 (blast-radius) - and Phase 18 (campaigns) since it needs both as inputs - -- Remediation/approval/audit (Phase 20) is sequenced after Phase 18 - (campaigns) and Phase 19 (classifications) — can't approve/gate an action - that doesn't reference either - -- Autotask triage note (Phase 21) is last — its content summarizes - classification + blast radius + recommended/approved remediation state, - so it has nothing to summarize until Phases 19-20 exist - -### Roadmap Evolution - -- Phase 22 added: 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. Depends on Phase 19 + Phase 20 only; not functionally - dependent on Phase 21 despite being numbered after it. - -- Phase 23 added: 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 - for per-company automatic vs. manual phishing pipeline execution. Surfaced - live during Phase 22 review of a real Breach Secure Now report (ticket - 699415) — the classifier already detects the simulation vendor and skips - the THREAT tier, but has no distinct outcome to reflect it. - -- Phase 24 edited: edited fields: title, goal, success_criteria (tidied up phase.add output; AWS Route 53 DNS sync via BWS credentials, full CRUD + audit logging, integrated into existing sync infra) +- Roadmap: Phases mirror the spec's 8-step build order so each step ships independently to `master` (spec §8) +- Phase 2 unblocks Phases 3–7; Phases 3–7 are mutually independent and can be parallelized; Phase 8 depends on Phase 7 +- All work happens in place under `/mobile/*` — no `/mobile-v2`, no parallel routes (spec §2) +- [Phase 02-mobile-shell-more-drawer]: Single useState in mobile layout.tsx for drawer open state — no Zustand/Context per CLAUDE.md constraint +- [Phase 02-mobile-shell-more-drawer]: Tailwind 4 pb-[calc(theme(spacing.16)+env(safe-area-inset-bottom))] arbitrary value works without inline-style fallback +- [Phase 02-mobile-shell-more-drawer]: No redirect on /mobile/nav deletion — standard 404 per DRAWER-06 spec +- [Phase 05-finance-restyle]: D-09: Monthly revenue renders as stacked list, not recharts bar chart (DASH-04 precedent — no charts on mobile) +- [Phase 05-finance-restyle]: D-02: Revenue YTD folded into Paid MTD caption; separate Revenue YTD card removed +- [Phase 05-finance-restyle]: D-08: Aging bucket palette locked: amber/orange/destructive token — no raw red/yellow Tailwind classes ### Pending Todos @@ -115,61 +84,10 @@ None yet. ### Blockers/Concerns -- 2026-07-15 — Phase 18 gap closure (18-04): decision-coverage gate flagged D-02/D-03/D-04 (grouping-parameter decisions from original discuss-phase) as not literally cited in any plan's `must_haves`/`truths`. Overridden and proceeded — these decisions were already implemented in 18-01 (24h window, subject normalization, no-merge behavior) and independently confirmed correct by both 18-REVIEW.md and 18-VERIFICATION.md. Citation gap only, not an implementation gap. - -- 2026-07-16 — Phase 23 planning: decision-coverage gate flagged D-01/D-02/D-03/D-04/D-05/D-06/D-08 (7 of 8 decisions) as not literally cited by `D-NN:` prefix in any plan's `must_haves`/`truths`. Overridden and proceeded — spot-checked plan 23-01's `must_haves.truths` directly and confirmed it substantively describes D-01 through D-04's content (USER_AWARENESS verdict, acknowledge_user action, noteType 18 customer-visible note, manual-path real posting) without the literal citation prefix; the plan-checker's second-pass review (after the blocker-revision cycle) independently confirmed via live source reads that all 8 decisions map to implementing tasks with no contradictions. Same citation-format gap as the Phase 18 precedent above, not an implementation gap. - -- 2026-07-16 — Phase 23 gap-closure planning (`--gaps`, plan 23-06): re-running the decision-coverage gate against the phase directory re-surfaced the same 7 pre-existing citation-format misses from the initial planning override above (D-01/D-02/D-03/D-04/D-05/D-06/D-08 — all already implemented and VERIFIED per 23-VERIFICATION.md's Observable Truths #1-15). Not a new gap and not introduced by 23-06 (which only touches AUTOGATE-03/webhook idempotency and cites no CONTEXT.md decisions of its own). Overridden and proceeded for the same reason as the original override — retroactively reformatting 5 already-executed, already-shipped plans to add literal `D-NN:` citations under designated headings is out of scope for a narrow gap-closure run targeting Truth #18 only. - -### Quick Tasks Completed - -| # | Description | Date | Commit | Directory | -|---|-------------|------|--------|-----------| -| 260712-ash | Add PAX8 to admin sync overview page + detail page | 2026-07-12 | 6ed6c66 | [260712-ash-add-pax8-to-the-admin-sync-overview-page](./quick/260712-ash-add-pax8-to-the-admin-sync-overview-page/) | -| 260716-n46 | Fix Mimecast blast-radius future-end-date swallow bug + multi-tenant gap (per-company tenant resolution) | 2026-07-16 | 9951e53 | [260716-n46-fix-mimecast-blast-radius-date-window-fu](./quick/260716-n46-fix-mimecast-blast-radius-date-window-fu/) | -| 260716-pgr | Fix confidence display bug (0-1 scale rendered as raw percent, e.g. "1%" instead of "100%") | 2026-07-16 | 3f16268 | [260716-pgr-fix-confidence-display-bug-in-classifica](./quick/260716-pgr-fix-confidence-display-bug-in-classifica/) | -| 260717-a19 | Fix phishing simulation-vendor allowlist gaps (3 missing KnowBe4 domains), auto-parse timing race (retry on ticket.update), and parseAndStoreMessage idempotency; reclassified 6 stale Seubert campaigns (all flipped UNWANTED → USER_AWARENESS) | 2026-07-17 | cf04f07 | [260717-a19-fix-phishing-simulation-vendor-allowlist](./quick/260717-a19-fix-phishing-simulation-vendor-allowlist/) | -| 260717-v6c | Add "Mark as accidental report" action to the phishing Action Area — closes out a campaign and posts a fixed customer-facing note to the reporter (distinct from the silent "Mark as false positive" action) | 2026-07-18 | 565a0c1 | [260717-v6c-add-a-mark-as-accidental-report-action-t](./quick/260717-v6c-add-a-mark-as-accidental-report-action-t/) | -| 260718-7v8 | Fix Mimecast blast-radius false positives — date-scope `getHeldMessages()` and add a sender-domain relevance guard so unrelated held mail in a recipient's queue no longer inflates held/matched counts or overwrites a genuinely delivered recipient's status | 2026-07-18 | b7d6be4 | [260718-7v8-fix-mimecast-blast-radius-held-message-f](./quick/260718-7v8-fix-mimecast-blast-radius-held-message-f/) | -| 260718-9qg | Add self-contained `QBO_INTEGRATION_HANDOFF.md` documenting Pulse's QuickBooks Online OAuth2 flow, token storage/refresh, sandbox/production API base URLs, and gotchas (deletion-diffing, CSRF state gap, NEXTAUTH_URL legacy var) for a new app's team | 2026-07-18 | ea8a36b | [260718-9qg-create-a-quickbooks-online-integration-h](./quick/260718-9qg-create-a-quickbooks-online-integration-h/) | -| 260721-fy8 | Fix missing `mimecast-sync`/`qbo` scheduler dispatch branches (both silently fell through to a generic Autotask full sync) and reschedule `mimecast-sync` off the 2am 3-way cron collision with `qbo-sync-2am` and `veeam-full` | 2026-07-21 | db7db98 | [260721-fy8-fix-mimecast-and-qbo-sync-scheduler-disp](./quick/260721-fy8-fix-mimecast-and-qbo-sync-scheduler-disp/) | -| 260721-mmf | Fix Mimecast blast-radius query scope — dropped the single-recipient `to`/`recipient` filter from `searchDeliveredMessages`/`getHeldMessages` so the fan-out returns every delivered/held message across the whole tenant for a campaign's sender+subject+date-window, not just whether it reached the original reporter's mailbox | 2026-07-21 | 534eda3 | [260721-mmf-fix-mimecast-blast-radius-scope](./quick/260721-mmf-fix-mimecast-blast-radius-scope/) | -| 260721-n49 | Fix `gatherCampaignEvidence()` (used by auto-classification on ticket creation) to resolve the reporting company's own `mimecast_tenants` row before calling `getBlastRadius()`, mirroring the campaign-detail route's existing per-tenant resolution — previously it always used the global env-configured (Wulf) tenant, silently returning wrong-tenant (often empty) blast-radius data for any company with its own registered Mimecast tenant | 2026-07-21 | 9f12cd6 | [260721-n49-fix-classifier-mimecast-tenant-scope](./quick/260721-n49-fix-classifier-mimecast-tenant-scope/) | - -## Deferred Items - -Items acknowledged and carried forward from previous milestone close: - -| Category | Item | Status | Deferred At | -|----------|------|--------|-------------| -| Follow-up | Tablet breakpoint (`md:max-w-2xl`) on mobile shell | Deferred | v1.0 close | -| Follow-up | Real notification list behind Bell icon | Deferred | v1.0 close | -| Follow-up | Scroll restoration on Engagement profile back navigation (partial fix only) | Deferred | v1.0 close | - -Items acknowledged and deferred at v3.0 milestone close on 2026-07-17 (pre-flight open-artifact audit — none are v3.0/phishing gaps): - -| Category | Item | Status | -|----------|------|--------| -| quick_task | 260519-0oz-add-qbo-createpayment-createdeposit-fh-r | Complete on disk (PLAN+SUMMARY present); missing STATE.md log entry — QBO financial work, unrelated to v3.0 | -| quick_task | 260521-fci-stopgap-nightly-reconciliation-for-stale | Complete on disk (PLAN+SUMMARY present); missing STATE.md log entry — QBO financial work, unrelated to v3.0 | -| quick_task | 260521-foj-fix-weekly-full-fk-error-widen-companies | Complete on disk (PLAN+SUMMARY present); missing STATE.md log entry — Autotask sync fix, unrelated to v3.0 | -| quick_task | 260712-ash-add-pax8-to-the-admin-sync-overview-page | Already logged in Quick Tasks Completed table above — audit flag is a false positive | -| quick_task | 260716-n46-fix-mimecast-blast-radius-date-window-fu | Already logged in Quick Tasks Completed table above — audit flag is a false positive | -| quick_task | 260716-pgr-fix-confidence-display-bug-in-classifica | Already logged in Quick Tasks Completed table above — audit flag is a false positive | -| seed | SEED-001-wulf-standards-engine | Dormant by design — future work, not in v3.0 scope | -| seed | SEED-002-pax8-integration | Dormant by design — future work, not in v3.0 scope | -| seed | SEED-003-general-pulse-data-assistant | Dormant by design — future work, not in v3.0 scope | -| uat_gap | Phase 18 18-HUMAN-UAT.md | Marked `resolved`, 0 pending scenarios — stale status label only | -| verification_gap | Phase 19 19-VERIFICATION.md (`human_needed`) | No actual gaps — 2 non-blocking human-decision items (a regression-guard curl check already traced correct by static analysis, and a THREAT-escalation policy question already implicitly resolved by Phases 20-23 shipping on top of that code without issue) | -| verification_gap | Phase 22 22-VERIFICATION.md (`human_needed`, first-ever verification pass, run at this milestone close) | Code-level: 6/6 requirements confirmed correct by direct source read (REVIEW-01 through REVIEW-06, including REVIEW-03's evidence-sanitization requirement — no `dangerouslySetInnerHTML`, no anchor tags on indicator URLs, explicit Mimecast `unavailable` branch). 5 manual browser click-through checks remain outstanding (full page state-machine, approve/remediate/mark-false-positive live flow, non-privileged-role gating, live Mimecast-unavailable render, URL-inertness/clipboard) — deferred rather than run via Playwright per user choice at 2026-07-17 milestone close. See 22-VERIFICATION.md frontmatter `human_verification` list before relying on this UI in a new deployment. | +None yet. ## Session Continuity -Last session: 2026-08-05T22:31:11.414Z -Stopped at: Phase 24 context gathered -Resume file: .planning/phases/24-aws-route-53-dns-sync-track-changes-crud-operations-full-aud/24-CONTEXT.md - - -## Operator Next Steps - -- Start the next milestone with /gsd-new-milestone +Last session: 2026-05-04T02:27:14.876Z +Stopped at: Phase 7 UI-SPEC approved +Resume file: .planning/phases/07-engagement-overview-new/07-UI-SPEC.md diff --git a/.planning/config.json b/.planning/config.json index 74e39b0..040e7b2 100644 --- a/.planning/config.json +++ b/.planning/config.json @@ -25,8 +25,7 @@ "text_mode": false, "research_before_questions": false, "discuss_mode": "discuss", - "skip_discuss": false, - "_auto_chain_active": false + "skip_discuss": false }, "hooks": { "context_warnings": true @@ -36,4 +35,4 @@ "agent_skills": {}, "mode": "yolo", "granularity": "standard" -} +} \ No newline at end of file diff --git a/.planning/deferred-items.md b/.planning/deferred-items.md deleted file mode 100644 index 684d950..0000000 --- a/.planning/deferred-items.md +++ /dev/null @@ -1,28 +0,0 @@ -# Deferred Items - -Out-of-scope discoveries logged during plan execution (not fixed — see SCOPE BOUNDARY in executor rules). - -## Phase 10-01 - -- **Pre-existing `npx tsc --noEmit` errors in `lib/services/sync-scheduler.ts`** (lines 446, 450): - `Cannot find module '@/lib/services/appgate-factory'` / `'@/lib/services/appgate-sync-service'`. - Cause: this worktree's base commit (`8b975be`) already references `appgate-factory.ts` / - `appgate-sync-service.ts` from `sync-scheduler.ts` (committed in `badd718`), but those two - files themselves are untracked/uncommitted in the main repo working tree (confirmed via - `git status` — `?? lib/services/appgate-factory.ts` etc.), so they don't exist in this - worktree's checkout. Unrelated to plan 10-01 (PAX8 client/factory/types) — not touched or - caused by this plan's changes. Left as-is per the scope boundary rule. - -- **Pre-existing `npm test` failures in `lib/services/analyzer/itglue-search.test.ts`** - (2 of 8 tests fail: "tolerates per-call failures" cases, `docs.length` mismatches). - Neither `itglue-search.ts` nor `itglue-search.test.ts` was touched by this plan (last - modified in commit `a0a6e7f`, predating this worktree's base `8b975be`). Unrelated to - plan 10-01 — left as-is per the scope boundary rule. - -## Phase 10-03 - -- **Same pre-existing `npx tsc --noEmit` errors in `lib/services/sync-scheduler.ts`** - (lines 446, 450 — missing `appgate-factory` / `appgate-sync-service` modules) still present, - confirmed again during Task 1 verification. `scripts/verify-pax8-auth.ts` itself type-checks - clean (no errors reported for that file). Unrelated to this plan's files - (`scripts/verify-pax8-auth.ts`, `CLAUDE.md`) — left as-is per the scope boundary rule. diff --git a/.planning/milestones/v3.0-REQUIREMENTS.md b/.planning/milestones/v3.0-REQUIREMENTS.md deleted file mode 100644 index b8d0bd7..0000000 --- a/.planning/milestones/v3.0-REQUIREMENTS.md +++ /dev/null @@ -1,263 +0,0 @@ -# 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)* diff --git a/.planning/milestones/v3.0-ROADMAP.md b/.planning/milestones/v3.0-ROADMAP.md deleted file mode 100644 index 4aaa2fa..0000000 --- a/.planning/milestones/v3.0-ROADMAP.md +++ /dev/null @@ -1,687 +0,0 @@ -# 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. - -
-✅ v1.0 Mobile Shell Redesign (Phases 1-9.1) - SHIPPED 2026-07-10 - -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 3–7 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 `` 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) - -
- -
-✅ v2.0 PAX8 Integration (Phases 10-14) - SHIPPED 2026-07-12 - -**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 -(`-client.ts` + `-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 `isConfigured()` + 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 - -
- -
-✅ v3.0 Phishing Triage Automation (Phases 15-23) - SHIPPED 2026-07-17 - -**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) - -
- -## 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`* - diff --git a/.planning/phases/01-pwa-scaffolding/01-01-SUMMARY.md b/.planning/phases/01-pwa-scaffolding/01-01-SUMMARY.md new file mode 100644 index 0000000..b652ff6 --- /dev/null +++ b/.planning/phases/01-pwa-scaffolding/01-01-SUMMARY.md @@ -0,0 +1,142 @@ +--- +phase: 01-pwa-scaffolding +plan: 01 +subsystem: pwa-shell +tags: [pwa, manifest, viewport, mobile] +requires: + - app/layout.tsx (existing root layout with metadata export) + - public/wulff-logo.png, public/favicon.png, public/branding/wulf-mark.png (existing icon assets) +provides: + - public/manifest.json (Web App Manifest at /manifest.json) + - app/layout.tsx exports `viewport: Viewport` with viewportFit: "cover" + - app/layout.tsx exports `metadata.manifest = "/manifest.json"` (Next.js emits automatically) +affects: + - Phase 02 mobile shell (can rely on viewport-fit=cover for safe-area insets) + - All routes (root layout viewport applies app-wide) +tech-stack: + added: [] + patterns: + - Next.js 16 separate `viewport` export (replaces deprecated metadata.viewport) + - Next.js 16 metadata.manifest field (auto-emits ) +key-files: + created: + - public/manifest.json + modified: + - app/layout.tsx +decisions: + - theme_color #0075AD chosen as Wulf primary brand blue (sourced from app/styles/brand.css line 28, --wulf-blue) — gives consistent system UI tint in light and dark mode since manifest only allows one value + - background_color #FFFFFF chosen as the light shell background — manifest only allows one splash background, white matches Pulse's default light theme and is acceptable on dark devices (brief flash, not a regression) + - Used metadata.manifest field over hand-rolled — Next.js 16 emits the link tag automatically, satisfies spec wording, and keeps with the existing metadata API pattern + - Reused existing icon assets with `"sizes": "any"` (wulff-logo.png, branding/wulf-mark.png, favicon.png) instead of generating sized 192/512 variants — install tools accept this for PNGs; sized icons can be added in a future polish phase if install warns + - Added themeColor light/dark pair in viewport (one-line improvement) — paired with Next.js helper, emits per-scheme tags. Optional per the plan; kept since it costs nothing and improves dark-mode rendering + - orientation set to "portrait" — phone-first per spec §1/§2; tablet landscape is explicit out-of-scope per spec §7 + - scope set to "/" — allow standalone window to navigate anywhere in the app without falling out to browser +metrics: + duration: ~1m + tasks_completed: 2 + files_created: 1 + files_modified: 1 + completed: 2026-05-03T17:38:55Z +--- + +# Phase 01 Plan 01: PWA Scaffolding Summary + +PWA install surface added: a Web App Manifest at `/manifest.json` plus a Next.js 16 viewport export with `viewport-fit=cover` so the mobile shell can paint behind the device home indicator in future phases. + +## What Shipped + +### Task 1: `public/manifest.json` (NEW) + +Hand-written 31-line JSON manifest with all spec-mandated fields: + +| Field | Value | Why | +|-------|-------|-----| +| `name`, `short_name` | "Pulse" | Spec §4 verbatim | +| `description` | Wulf operations console blurb | Install dialog readability | +| `start_url` | `/mobile` | Spec §4 — phone install lands on mobile shell, not desktop dashboard | +| `scope` | `/` | Allow standalone window to navigate the whole app | +| `display` | `standalone` | Spec §4 — chromeless app surface | +| `orientation` | `portrait` | Phone-first (spec §1, §2); tablet landscape is OOS (§7) | +| `theme_color` | `#0075AD` | Wulf primary blue from `app/styles/brand.css` line 28 | +| `background_color` | `#FFFFFF` | Light shell background (manifest allows only one) | +| `icons` | 3 entries with `sizes: "any"` | Reuses `/wulff-logo.png`, `/branding/wulf-mark.png`, `/favicon.png` | + +No `serviceworker`, no `display_override`, no `prefer_related_applications`, no `next-pwa` — per spec §4 and CLAUDE.md. + +**Commit:** `3e3df24` + +### Task 2: `app/layout.tsx` (MODIFIED) + +Three minimal additions to the existing root layout, body unchanged: + +1. Import upgraded: `import type { Metadata, Viewport } from "next";` +2. `metadata.manifest = "/manifest.json"` added alongside the existing `icons` field — Next.js 16 emits `` in the rendered HTML head automatically (satisfies PWA-02 spec wording). +3. New `viewport` export: + + ```ts + export const viewport: Viewport = { + width: "device-width", + initialScale: 1, + viewportFit: "cover", + themeColor: [ + { media: "(prefers-color-scheme: light)", color: "#FFFFFF" }, + { media: "(prefers-color-scheme: dark)", color: "#0A0A0A" }, + ], + }; + ``` + + `viewportFit: "cover"` is the load-bearing field for PWA-03 — Next.js renders `viewport-fit=cover` in the `` tag so future phases can use safe-area-inset utilities to paint behind the home indicator. `width`, `initialScale`, and `themeColor` are baseline mobile defaults that prevent Next.js viewport warnings. + +**Commit:** `d196d22` + +## Verification Results + +| Gate | Result | +|------|--------| +| `test -f public/manifest.json` | PASS | +| `jq -e '.name == "Pulse" and .display == "standalone" and .start_url == "/mobile"' public/manifest.json` | PASS (true) | +| `jq -e '.theme_color == "#0075AD" and .background_color == "#FFFFFF"' public/manifest.json` | PASS | +| `jq -e '.icons \| length >= 1' public/manifest.json` | PASS (3 icons) | +| `jq -e '.serviceworker == null' public/manifest.json` | PASS | +| `jq empty public/manifest.json` | PASS (valid JSON) | +| `grep -E '^import type \{ Metadata, Viewport \} from "next"' app/layout.tsx` | PASS | +| `grep -E 'manifest:\s*"/manifest\.json"' app/layout.tsx` | PASS | +| `grep -E '^export const viewport: Viewport = \{' app/layout.tsx` | PASS | +| `grep -E 'viewportFit:\s*"cover"' app/layout.tsx` | PASS | +| `grep -E 'width:\s*"device-width"' app/layout.tsx` | PASS | +| `grep -E 'initialScale:\s*1' app/layout.tsx` | PASS | +| `grep -E 'apple:\s*"/wulff-logo\.png"' app/layout.tsx` (icons preserved) | PASS | +| `grep -E 'export default function RootLayout' app/layout.tsx` (body intact) | PASS | +| `! grep -E "^'use client'" app/layout.tsx` | PASS | +| `npx tsc --noEmit --pretty` | exit 0 | +| `test ! -f public/sw.js && test ! -f public/service-worker.js` | PASS | +| `! grep '"next-pwa"' package.json` | PASS | + +**Dev-server-only checks** (`curl http://localhost:3100/manifest.json`, `curl http://localhost:3100/ \| grep viewport-fit=cover`) were not run — this executor runs in a worktree without a dev server. The offline equivalents above are equivalent: the file is a static asset served verbatim by Next.js from `public/`, and `viewportFit: "cover"` is type-checked to render `viewport-fit=cover` per Next.js 16's documented metadata API. + +## Requirements Satisfied + +- **PWA-01:** `public/manifest.json` exists with name "Pulse", short_name "Pulse", display "standalone", start_url "/mobile", theme_color "#0075AD", background_color "#FFFFFF", and 3 icons. +- **PWA-02:** `app/layout.tsx` references the manifest via `metadata.manifest = "/manifest.json"` — Next.js 16 emits the `` tag automatically. +- **PWA-03:** `app/layout.tsx` exports `viewport: Viewport` with `viewportFit: "cover"` — Next.js renders `viewport-fit=cover` in the `` tag, unblocking safe-area painting in Phase 2. + +## Deviations from Plan + +None - plan executed exactly as written. + +No bugs encountered, no missing critical functionality, no blocking issues, no architectural decisions needed. + +## Threat Surface Scan + +No new threat surface introduced beyond the plan's ``. The manifest is world-readable per W3C Web App Manifest spec and contains only public branding (no secrets, no user data, no endpoints). The viewport export is server-rendered with no user input flow. ASVS-L1 baseline preserved. + +## Known Stubs + +None. All values are real (brand colors sourced from `app/styles/brand.css`, icons reference real public assets, start_url matches the existing `/mobile` route). + +## Self-Check: PASSED + +- `[ -f public/manifest.json ]` → FOUND +- `[ -f app/layout.tsx ]` → FOUND +- `git log --oneline | grep 3e3df24` → FOUND (Task 1 commit) +- `git log --oneline | grep d196d22` → FOUND (Task 2 commit) diff --git a/.planning/phases/01-pwa-scaffolding/01-02-SUMMARY.md b/.planning/phases/01-pwa-scaffolding/01-02-SUMMARY.md new file mode 100644 index 0000000..631e33e --- /dev/null +++ b/.planning/phases/01-pwa-scaffolding/01-02-SUMMARY.md @@ -0,0 +1,174 @@ +--- +phase: 01-pwa-scaffolding +plan: 02 +subsystem: pwa-scaffolding +gap_closure: true +tags: [css, tailwind4, mobile, pwa, safe-area] +requirements_satisfied: [PWA-04] +roadmap_criteria_satisfied: ["Phase 1 SC #3 — safe-area utility available"] +dependency_graph: + requires: [] + provides: + - "@utility pt-safe (padding-top: env(safe-area-inset-top))" + - "@utility pb-safe (padding-bottom: env(safe-area-inset-bottom))" + affects: + - "Phase 2 (mobile shell) — sticky header (SHELL-05) and fixed bottom nav (SHELL-06) consume these utilities" +tech_stack: + added: [] + patterns: + - "Tailwind 4 @utility blocks (already in use across brand.css)" + - "CSS env(safe-area-inset-*) — browser-native, falls back to 0" +key_files: + created: [] + modified: + - app/styles/brand.css +decisions: + - "Named utilities (pt-safe / pb-safe) over arbitrary values (pt-[env(safe-area-inset-top)]) — single source of truth, clearer JSX, easy future tweak if iOS rules change" + - "brand.css over globals.css — co-located with all other named project utilities (num, metric-label, surface-brand, tagline, etc.); already imported by globals.css line 125" + - "Top + bottom only (no pl-safe / pr-safe) — manifest pins orientation to portrait; left/right insets only matter in landscape on notched devices; speculative until a consumer asks" + - "Plain env() (not max(env(), 0px)) — env() already returns 0 on devices without insets; max() wrapper is a no-op" +metrics: + duration: "~5 min" + completed: 2026-05-03 + tasks_completed: 1 + files_modified: 1 + commits: 1 +--- + +# Phase 01 Plan 02: PWA-04 Safe-Area Utility Gap Closure Summary + +**One-liner:** Adds shared `pt-safe` / `pb-safe` Tailwind 4 `@utility` blocks to `app/styles/brand.css`, closing the orphaned PWA-04 requirement so Phase 2's sticky header and fixed bottom nav can opt into iOS notch / Android home-indicator padding via `env(safe-area-inset-*)`. + +## Requirements Satisfied + +- **PWA-04** — Header and bottom tab bar respect `env(safe-area-inset-top/bottom)` (Tailwind arbitrary values or shared utility class). **Closed** by shipping `@utility pt-safe` and `@utility pb-safe` in `app/styles/brand.css`. This restores the orphaned-requirement state flagged by `01-VERIFICATION.md` (where 01-01 had declared `requirements: [PWA-01, PWA-02, PWA-03]` only and silently deferred PWA-04 to Phase 2). +- **ROADMAP Phase 1 Success Criterion #3** — "Shared safe-area utility class available" — satisfied by the same two `@utility` blocks. + +## What Changed + +### Files Modified + +- `app/styles/brand.css` — appended one section comment block + two `@utility` definitions between the existing `@utility tagline` (ends line 140) and the `/* === Wolf-mark watermark === */` section header (now line 165). Net: **+23 lines, 0 deletions.** + +### Exact Diff (additive only) + +```css +/* === Safe-area insets ================================================= + * + * Opt-in padding helpers for sticky top / fixed bottom bars on devices + * with notches, dynamic islands, or gesture home indicators. Pair with + * the viewport-fit=cover viewport meta (set in app/layout.tsx) — without + * that, env(safe-area-inset-*) resolves to 0 and these utilities are + * no-ops, which is the desired fallback on non-PWA / non-mobile contexts. + * + * Usage: + *
// header clears notch + *