docs(22): capture phase context

This commit is contained in:
lorentz 2026-07-16 12:49:01 -04:00
parent ec7c2114bf
commit 47c3510667
2 changed files with 423 additions and 0 deletions

View file

@ -0,0 +1,310 @@
# 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*

View file

@ -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 `<a href>` | ✓ |
| 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)