# Phase 22: Approval UI (LiveLink) - Context

**Gathered:** 2026-07-16
**Status:** Ready for planning

<domain>
## 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).

</domain>

<decisions>
## 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 `<a href>`. 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.

</decisions>

<canonical_refs>
## 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.

</canonical_refs>

<code_context>
## 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).

</code_context>

<specifics>
## 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.

</specifics>

<deferred>
## 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.

</deferred>

---

*Phase: 22-approval-ui-livelink-addressable-campaign-review-and-approve*
*Context gathered: 2026-07-16*
