From 47c351066782c7721ad20347c405363006d9e001 Mon Sep 17 00:00:00 2001 From: lorentz Date: Thu, 16 Jul 2026 12:49:01 -0400 Subject: [PATCH] docs(22): capture phase context --- .../22-CONTEXT.md | 310 ++++++++++++++++++ .../22-DISCUSSION-LOG.md | 113 +++++++ 2 files changed, 423 insertions(+) create mode 100644 .planning/phases/22-approval-ui-livelink-addressable-campaign-review-and-approve/22-CONTEXT.md create mode 100644 .planning/phases/22-approval-ui-livelink-addressable-campaign-review-and-approve/22-DISCUSSION-LOG.md diff --git a/.planning/phases/22-approval-ui-livelink-addressable-campaign-review-and-approve/22-CONTEXT.md b/.planning/phases/22-approval-ui-livelink-addressable-campaign-review-and-approve/22-CONTEXT.md new file mode 100644 index 0000000..c3fa977 --- /dev/null +++ b/.planning/phases/22-approval-ui-livelink-addressable-campaign-review-and-approve/22-CONTEXT.md @@ -0,0 +1,310 @@ +# Phase 22: Approval UI (LiveLink) - Context + +**Gathered:** 2026-07-16 +**Status:** Ready for planning + + +## Phase Boundary + +A security operator opens an Autotask ticket, clicks a LiveLink button, and +lands on a stable, ticket-ID-addressable Pulse page (`/phishing/tickets/ +{ticketId}`) that resolves the ticket to its campaign and shows that +campaign's timeline, gathered evidence, and current classification — with +approve/remediate/mark-false-positive actions right there, calling the +existing Phase 20 APIs directly. Authenticated via the existing Better Auth +session only, no separate token/query-param auth scheme. + +By explicit user decision during this discussion (see Scope Expansion below), +this phase ALSO includes a minimal `/phishing` campaigns list page + a nav +entry, so the review page has a discoverable entry point beyond LiveLink. +This is additive scope beyond the literal REVIEW-01..06 wording — planner +should add it as its own plan/success-criterion, not silently fold it into +the ticket-scoped page's work. + +Does NOT cover: actually wiring a real remediation provider (that's +REMEDEXEC-01..05, v2 — Phase 20's simulated-effect model stays as-is), any +change to the classification engine (Phase 19) or remediation/audit APIs +(Phase 20) themselves, or the Autotask triage note (Phase 21, independent and +already complete). + + + + +## Implementation Decisions + +### Scope Expansion — Campaigns List Page +- **D-00:** The user explicitly chose to expand this phase's scope beyond + REVIEW-01..06: in addition to the ticket-ID-scoped review page, this phase + also builds a minimal `/phishing` campaigns list page and adds a nav entry + pointing to it. Rationale surfaced during discussion: a nav entry needs + something to point to, and the ticket-scoped page alone isn't + browsable/discoverable without already knowing a ticket ID. This was + flagged as scope creep during discussion and the user confirmed they want + it included now rather than deferred. Planner should treat this as an + additional deliverable with its own success criterion, distinct from + REVIEW-01..06's ticket-scoped page. The list page can be genuinely minimal — + no new requirement beyond "operator can see and click into recent + campaigns" — it should reuse `GET /api/phishing/campaigns` (Phase 18, + already exists, no new endpoint needed). + +### Page Chrome & Navigation +- **D-01:** The review page (and the new list page) use Pulse's full + navigation shell — top `AppNavigation`, `PageHeader`, standard + `container mx-auto px-6 py-6` layout — same chrome as `/admin`, `/analyzer`. + Rejected the standalone/`/mobile`-`/kiosk`-style no-nav-chrome pattern: + this is a page staff may also navigate to directly (via the new list page), + not exclusively a LiveLink drop-in. +- **D-02:** A nav entry is added (exact placement — top-level nav item vs. + a sub-item under an existing menu — is planner's call per DESIGN.md's + existing nav IA conventions). + +### Approve/Remediate Action UX +- **D-03:** The operator selects which recommended action(s) to approve via + a checkbox list (one checkbox per `recommendedActions` entry from the + latest classification), with each action's default params visible and + editable inline. A single "Approve selected" button submits all checked + actions in one `POST /approve` call with the (possibly overridden) params + per action — matching `ApproveActionInput { actionType, params? }` + (`remediation-service.ts`) exactly. No blanket "approve everything" button. +- **D-04:** After any of approve/remediate/mark-false-positive succeeds, the + page refetches the campaign detail endpoint and re-renders from the fresh + response — no optimistic local state mutation. Matches the project's + no-SWR/react-query local-`fetch()` convention (CLAUDE.md). +- **D-05:** Once a campaign has reached a resolved state (any + `remediation_actions` row `completed`, or marked false positive), the + approve/remediate/mark-false-positive buttons remain visible in the layout + but are disabled with a tooltip/inline explanation (e.g. "Already + remediated on {date} by {approver}") — not removed from the DOM. Keeps the + page layout stable across all campaign states rather than reflowing. + +### Permission Gating (carried forward, not re-discussed) +- **D-06:** Button disabled/hidden state for approve/remediate/ + mark-false-positive uses the EXACT same check the API enforces — + `hasPermission(session.user.role, 'phishing', 'approve' | 'remediate')` + from `lib/permissions.ts`, called client-side via `useSession()` + (`lib/auth-client.ts`). This is a plain, DB-free function already safe to + import client-side — no new permission surface, no relaxed/separate model, + directly satisfying REVIEW-06's "never uses a relaxed or separate + permission check" requirement. + +### Not-Yet-Triaged / Ungrouped Handling +- **D-07:** If a ticket ID has no `reports` row yet (Phase 15's scanner/ + on-demand analyze hasn't processed it — a real possibility since LiveLink + can be clicked immediately after ticket creation), the page renders a + friendly empty state ("Not yet triaged") with a button that calls the + existing `POST /api/phishing/tickets/{ticket_id}/analyze` (Phase 18) right + there — turning what would otherwise be a dead end into a one-click fix. + Not a 404. +- **D-08:** If a `reports` row exists but `campaign_id` is still null (the + narrow race window before Phase 18's grouping runs), the page renders that + report's own evidence standalone, with a note that campaign + grouping/classification hasn't completed yet — it does not crash or fall + back to the same empty state as D-07 (there IS evidence to show, just no + campaign wrapper yet). + +### Evidence Rendering Safety +- **D-09:** Extracted URLs (from Phase 16's `indicators`/`messages.urls`) are + rendered as **inert, non-clickable text** — plain monospace with a + copy-to-clipboard affordance, never an ``. This is a stricter + posture than the Phase 21 triage note's URL sanitization (which strips + query strings but still describes URLs in prose); here, on an interactive + page an operator might actually click, the decision is to remove all click + risk entirely rather than rely on sanitization alone. +- **D-10:** Attachment metadata (filename, content-type, size, hash — per + EVID-01/EVID-04, no file content is ever stored) is displayed as plain + evidence, as-is, no extra redaction — consistent with Phase 21's precedent + of treating hashes/filenames as evidence, not secrets requiring redaction. + +### Claude's Discretion (explicitly deferred to research + planner) +- Exact nav placement for the new list page's entry (top-level item vs. + nested under an existing menu) — per DESIGN.md's nav IA table, planner's + call. +- Whether the campaign-detail data comes from extending the existing + `GET /api/phishing/campaigns/[id]` (Phase 18) in place, or a new + page-specific endpoint — the existing endpoint currently omits + `remediation_actions`, `audit_events`, full message evidence (headers/ + URLs/attachments/body_preview), and full classification fields (`reasons`/ + `recommended_actions`/`requires_approval`); research/planner's call which + approach fits better, as long as no other consumer of the existing + minimal shape breaks. +- A new ticket→campaign resolution path is needed for + `/phishing/tickets/{ticketId}` (no such lookup exists today) — whether + this is a new API route (e.g. `GET /api/phishing/tickets/{ticket_id}/ + campaign`) that the page calls, or a direct server-side lookup — planner's + call, but must go through the API-route pattern per CLAUDE.md ("no + `'use server'` actions... everything is API routes called via fetch"). +- Blast-radius freshness: whether the review page calls `getBlastRadius()` + fresh on each page load (matches the Phase 21 triage-note precedent, + costs an extra Mimecast API round-trip per view) or reads whatever the + most recent `classifications` row already persisted in `reasons` (cheaper, + possibly stale) — no strong preference surfaced, planner's call. +- Exact timeline merge/sort implementation for REVIEW-02 (reports + + classification history + audit events, chronological) — a UI/data-shape + detail, not a user-vision question. +- Exact visual treatment of the timeline (vertical timeline component vs. + simple ordered list) — no reference example given, planner's call as long + as chronological order and all three event sources are represented. +- Exact wording/layout of the minimal campaigns list page (columns shown, + filters if any) — no requirement drives this beyond "operator can see and + click into recent campaigns"; planner's call, should stay minimal per D-00. + + + + +## Canonical References + +**Downstream agents MUST read these before planning or implementing.** + +### Requirements & roadmap +- `.planning/REQUIREMENTS.md` — REVIEW-01..06 (full text), traceability table + showing all six mapped to Phase 22, Pending. +- `.planning/ROADMAP.md` §"Phase 22: Approval UI (LiveLink)" — goal, depends-on + (Phase 19 + Phase 20 only), 6 success criteria, `UI hint: yes`. + +### Existing API surface (this phase wires the UI to these — reuse, don't duplicate) +- `app/api/phishing/campaigns/[id]/route.ts` — current `GET` shape (Phase 18); + does NOT yet include `remediation_actions`, `audit_events`, full message/ + indicator evidence, or full classification fields — likely needs extending + (see Claude's Discretion). +- `app/api/phishing/campaigns/[id]/approve/route.ts` — `POST` body + `{ actions: ApproveActionInput[] }`, gated `requirePermission('phishing', + 'approve')`, returns `ApprovedRemediationAction[]`. +- `app/api/phishing/campaigns/[id]/remediate/route.ts` — `POST`, no body, + gated `requirePermission('phishing', 'remediate')`, idempotent, returns + `RemediateResult`. +- `app/api/phishing/campaigns/[id]/mark-false-positive/route.ts` — `POST`, + optional `{ reason? }` body, gated `requirePermission('phishing', + 'approve')`, blocked (409) if approved/completed remediation exists + (Phase 20 D-04). +- `app/api/phishing/campaigns/[id]/triage-note/route.ts` — Phase 21, not + directly used by this page per ROADMAP (separate concern) but same + campaign-scoped route-shape precedent. +- `app/api/phishing/tickets/[ticket_id]/analyze/route.ts` — Phase 18 + on-demand analyze; this is what D-07's empty-state "analyze now" button + calls. +- `app/api/phishing/campaigns/route.ts` — Phase 18 list endpoint; the new + D-00 campaigns list page should reuse this, not build a new one. + +### Schema (this phase reads; writes only via existing service functions) +- `migrations/097_phishing_triage_schema.sql` — full 7-table shape + (`campaigns`, `reports`, `messages`, `indicators`, `classifications`, + `remediation_actions`, `audit_events`) — the page's evidence/timeline + rendering must account for every column shape here. + +### Service layer (read for exact request/response shapes) +- `lib/services/remediation-service.ts` — `ApproveActionInput { actionType, + params? }`, `ApprovedRemediationAction`, `RemediateResult`, + `MarkFalsePositiveResult`, `RemediationValidationError`/ + `RemediationConflictError` — the UI's approve form must produce exactly + this input shape. +- `lib/services/mimecast-blast-radius.ts` — `getBlastRadius()`, + `BlastRadiusResult` (`status: 'ok' | 'unavailable'`) — REVIEW-03's explicit + `unavailable` state requirement. +- `lib/services/triage-note-service.ts` — Phase 21 precedent for gathering a + campaign's full current evidence (reports/classification/remediation/ + indicators join pattern) — useful reference for whatever endpoint this + phase builds/extends. + +### Auth/permissions (reused, not re-decided) +- `lib/permissions.ts` — `phishing: ["read","analyze","approve","remediate"]` + statement; `hasPermission(roleName, resource, action)` — plain function, + safe to call client-side for D-06's button-gating. +- `lib/auth-client.ts` — `useSession()` hook exposes `session.user.role` + client-side. +- `lib/auth-utils.ts` — `requirePermission(resource, action)` — the + server-side check every action route already uses; the UI's client-side + check (D-06) must mirror this, not diverge from it. + +### UI conventions +- `DESIGN.md` — nav IA table (role-gating hides items entirely, no + greyed-out nav variants — precedent, though D-05 explicitly chooses the + opposite for in-page action buttons, which is a different UI element than + top nav); `AppNavigation` returns `null` under `/mobile` — precedent for a + standalone-shell pattern that this phase explicitly did NOT choose (D-01). +- `.planning/codebase/CONVENTIONS.md`, `STRUCTURE.md`, `STACK.md` — general + component/styling/naming conventions (shadcn primitives, kebab-case files, + camelCase API responses, `components/admin/DetailModal.tsx` card+tabs + pattern as a possible reference for evidence display). + +### Prior phase decisions (for consistency) +- `.planning/phases/19-classification-engine/19-CONTEXT.md` — D-08 action + vocabulary (7 types, destructive vs non-destructive), classification shape + this page must render. +- `.planning/phases/20-remediation-approval-audit-safety/20-CONTEXT.md` — + D-01 simulated-effect model, D-02 permission grants, D-03 approval + granularity (this page's approve UX directly implements D-03), D-04 + mark-false-positive state guard. +- `.planning/phases/21-autotask-triage-note/21-CONTEXT.md` — evidence- + gathering join pattern (`triage-note-service.ts`), sanitization precedent + this phase's URL-safety decision (D-09) deliberately goes further than. + + + + +## Existing Code Insights + +### Reusable Assets +- `lib/permissions.ts`'s `hasPermission()` — directly reusable client-side + for D-06's button gating, no new permission-check code needed. +- `lib/services/triage-note-service.ts`'s evidence-gathering SQL joins + (reports → messages → indicators, classifications, remediation_actions) — + the query pattern this phase's data-fetching should mirror. +- `components/admin/DetailModal.tsx` — card + tabs (formatted/raw) pattern; + potential reference for organizing evidence/timeline/classification into + sections on the review page, though this is a full page not a modal. +- `app/api/phishing/campaigns/route.ts` — existing list endpoint, reusable + as-is for the new D-00 list page (no new endpoint needed for that surface). + +### Established Patterns +- `requirePermission(resource, action)` early-return pattern — every new + route this phase might add (e.g. a ticket→campaign resolver) should follow + this Phase 18 D-06 convention. +- Client pages are `'use client'` + `useState`/`useEffect`/`fetch()` — no + SWR/react-query, matches D-04's refetch-after-action approach. +- `sonner` toasts for action feedback (`toast.success()`/`toast.error()`) — + should wrap approve/remediate/mark-false-positive calls. + +### Integration Points +- New review page reads: campaign detail (extended or new endpoint per + Claude's Discretion), and needs a ticket→campaign resolution step before + that. +- New review page writes: calls existing `/approve`, `/remediate`, + `/mark-false-positive` endpoints directly — no new write logic, this + phase is UI + read-endpoint-shaping only. +- New list page reads: existing `GET /api/phishing/campaigns` (Phase 18), + no new endpoint. +- Nav: `components/navigation/app-navigation.tsx` gets a new entry pointing + to the list page (D-02). + + + + +## Specific Ideas + +No literal visual reference/mockup was provided. The user's clearest, +specific preference from this discussion is the URL-safety posture (D-09, +inert copy-only text — explicitly stricter than "just sanitize the query +string") and the mid-discussion scope decision to add a campaigns list page ++ nav entry (D-00) rather than defer it. + + + + +## Deferred Ideas + +- **Campaigns list page filtering/sorting beyond "recent"** — no requirement + drives anything beyond a minimal browsable list (D-00); richer + filtering/search is a future-phase concern if it ever comes up. +- **Real remediation provider wiring** — unchanged from Phase 20, still + `REMEDEXEC-01..05`, v2. +- **False-positive reversibility** — unchanged from Phase 20's deferred item; + not revisited here. + +None beyond the above — discussion otherwise stayed within phase scope. + + + +--- + +*Phase: 22-approval-ui-livelink-addressable-campaign-review-and-approve* +*Context gathered: 2026-07-16* diff --git a/.planning/phases/22-approval-ui-livelink-addressable-campaign-review-and-approve/22-DISCUSSION-LOG.md b/.planning/phases/22-approval-ui-livelink-addressable-campaign-review-and-approve/22-DISCUSSION-LOG.md new file mode 100644 index 0000000..a37f7f3 --- /dev/null +++ b/.planning/phases/22-approval-ui-livelink-addressable-campaign-review-and-approve/22-DISCUSSION-LOG.md @@ -0,0 +1,113 @@ +# Phase 22: Approval UI (LiveLink) - Discussion Log + +> **Audit trail only.** Do not use as input to planning, research, or execution agents. +> Decisions are captured in CONTEXT.md — this log preserves the alternatives considered. + +**Date:** 2026-07-16 +**Phase:** 22-approval-ui-livelink-addressable-campaign-review-and-approve +**Areas discussed:** Page chrome & feel, Approve/remediate action UX, Not-yet-triaged empty state, URL evidence rendering safety + +--- + +## Page chrome & feel + +| Option | Description | Selected | +|--------|-------------|----------| +| Full Pulse shell | Top nav, PageHeader, standard container padding — same chrome as /admin, /analyzer | ✓ | +| Standalone/focused (no top nav) | Distraction-free landing page, like /kiosk, /mobile | | + +**User's choice:** Full Pulse shell. + +| Option | Description | Selected | +|--------|-------------|----------| +| LiveLink only for now | No new nav link anywhere in Pulse | | +| Also worth a nav entry | Add a lightweight link/section for staff to find recent campaigns | ✓ | + +**User's choice:** Also worth a nav entry. +**Notes:** This required a scope-boundary check — a nav entry needs a target page, and REVIEW-01..06 only scope the ticket-ID page. + +| Option | Description | Selected | +|--------|-------------|----------| +| Defer the list page (recommended) | Phase 22 stays scoped to ticket-ID page only; note campaigns-list+nav as a deferred idea | | +| Add a minimal list page now | Expands Phase 22 scope to include a simple /phishing list page + nav link | ✓ | + +**User's choice:** Add a minimal list page now — explicit scope expansion, confirmed after the tradeoff was surfaced. +**Notes:** Captured as D-00 in CONTEXT.md with an explicit note to planner that this is additive scope, not part of REVIEW-01..06. + +--- + +## Approve/remediate action UX + +| Option | Description | Selected | +|--------|-------------|----------| +| Checkbox list + param editor | Each recommended action as a checkbox, editable default params, one "Approve selected" button | ✓ | +| One-click buttons, no override | Each action gets its own "Approve" button using classifier defaults, no override UI | | + +**User's choice:** Checkbox list + param editor. + +| Option | Description | Selected | +|--------|-------------|----------| +| Refetch the campaign data | Re-call the campaign detail endpoint after a successful action | ✓ | +| Optimistic local update | Update local state immediately without a round-trip | | + +**User's choice:** Refetch the campaign data. + +| Option | Description | Selected | +|--------|-------------|----------| +| Disappear, replaced by status | Buttons gone once resolved, replaced by a status line | | +| Stay visible but disabled | Buttons remain, greyed out with a tooltip explaining why | ✓ | + +**User's choice:** Stay visible but disabled. + +--- + +## Not-yet-triaged empty state + +| Option | Description | Selected | +|--------|-------------|----------| +| Friendly empty state + manual trigger | Explains "not yet triaged", offers a button to call the existing on-demand analyze endpoint | ✓ | +| Plain 404 / not found | Simplest — ticket has no report, page says not found | | + +**User's choice:** Friendly empty state + manual trigger. + +| Option | Description | Selected | +|--------|-------------|----------| +| Show the report standalone | Render the report's own evidence with a note that grouping/classification hasn't completed | ✓ | +| Treat same as not-yet-triaged | Show the same empty/pending state, one code path | | + +**User's choice:** Show the report standalone. + +--- + +## URL evidence rendering safety + +| Option | Description | Selected | +|--------|-------------|----------| +| Inert copy-only text | Plain monospace text with copy-to-clipboard, never an `` | ✓ | +| Clickable but sanitized | Real link with query strings/fragments stripped | | + +**User's choice:** Inert copy-only text. + +| Option | Description | Selected | +|--------|-------------|----------| +| Plain evidence display | Filename/type/size/hash shown as-is | ✓ | +| Filename hidden, hash-only | Only show type/size/hash, hide filename as extra caution | | + +**User's choice:** Plain evidence display. + +--- + +## Claude's Discretion + +- Exact nav placement for the new list page's entry (top-level vs. nested under an existing menu) +- Whether the campaign-detail data comes from extending the existing `GET /api/phishing/campaigns/[id]` in place, or a new page-specific endpoint +- Whether a new ticket→campaign resolution route is a dedicated endpoint or resolved another way (must still go through the API-route pattern) +- Blast-radius freshness — fresh `getBlastRadius()` call per page view vs. reading what the classification already persisted +- Exact timeline merge/sort implementation and visual treatment (vertical timeline vs. simple ordered list) +- Exact columns/layout of the minimal campaigns list page + +## Deferred Ideas + +- Campaigns list page filtering/sorting beyond a minimal "recent" view +- Real remediation provider wiring (unchanged from Phase 20 — REMEDEXEC-01..05, v2) +- False-positive reversibility (unchanged from Phase 20's deferred item)