---
phase: 22-approval-ui-livelink-addressable-campaign-review-and-approve
plan: 06
subsystem: ui
tags: [nextjs, react, phishing, livelink, autotask, datatable, empty-state]

# Dependency graph
requires:
  - phase: 22 plan 02
    provides: ticket->campaign resolver route, extended campaign-detail route (reports/messages/classifications/remediationActions/blastRadius/timeline), campaigns list route with firstReportTicketId
  - phase: 22 plan 03
    provides: EvidenceCard + UrlList + tooltip primitive
  - phase: 22 plan 04
    provides: ClassificationCard + TimelineCard
  - phase: 22 plan 05
    provides: ActionAreaCard + remediation-default-params
provides:
  - Ticket-scoped LiveLink review page at /phishing/tickets/{ticketId} composing all four phishing cards
  - Minimal campaigns list page at /phishing (D-00)
  - "Phishing" top-level nav entry (D-02)
  - New GET /api/phishing/reports/{report_id} route for the D-08 ungrouped-report evidence state
  - Confirmed (production) numeric-ticket-id LiveLink addressing assumption
affects: [phase-transition, milestone-close]

# Tech tracking
tech-stack:
  added: []
  patterns:
    - "Client-side permission gating via hasPermission(role, resource, action) + useSession() — same function server routes enforce, no separate/relaxed check (REVIEW-06)"
    - "Refetch-after-action (D-04): every mutating action passes `load` as a callback prop instead of optimistic local state mutation"
    - "Ticket-scoped state machine: loading/not-triaged/ungrouped/ready/error, with `ready` further branching on classifications[0] ?? null"

key-files:
  created:
    - app/phishing/tickets/[ticketId]/page.tsx
    - app/phishing/page.tsx
    - app/api/phishing/reports/[report_id]/route.ts
  modified:
    - components/navigation/app-navigation.tsx

key-decisions:
  - "Added a new GET /api/phishing/reports/{report_id} route (not in the plan's declared files_modified) as a Rule 2 deviation — the plan's own D-08 truth ('standalone-report notice + evidence for an ungrouped report') has no other data source once campaignId is null, since the existing campaign-detail route is keyed on campaignId"
  - "Task 3 (blocking human-verify checkpoint) resolved as 'verified': a real production Autotask LiveLink click against ticket 699340 confirmed the URL path segment is the plain numeric ticket ID, matching the resolver's Number(ticket_id) assumption — no ticket_number fallback needed"

patterns-established:
  - "First codebase instance of client-side hasPermission() gating tied 1:1 to the server-side permission check (no bespoke role-string comparisons)"

requirements-completed: [REVIEW-01, REVIEW-05, REVIEW-06]

# Metrics
duration: ~40min
completed: 2026-07-16
---

# Phase 22 Plan 06: LiveLink Review Page, Campaigns List & Nav Entry Summary

**Ticket-scoped `/phishing/tickets/{ticketId}` LiveLink review page composing ClassificationCard/ActionAreaCard/EvidenceCard/TimelineCard with a 5-state loading/not-triaged/ungrouped/ready/error machine, plus a minimal `/phishing` campaigns list and nav entry — numeric-ticket-id LiveLink addressing confirmed live in production.**

## Performance

- **Duration:** ~40 min
- **Completed:** 2026-07-16
- **Tasks:** 3 (2 automated + 1 blocking human-verify checkpoint)
- **Files modified:** 4 (3 created, 1 modified)

## Accomplishments

- Ticket-scoped review page (`app/phishing/tickets/[ticketId]/page.tsx`) resolves ticket→campaign via the plan-02 resolver, renders all five states (loading/not-triaged/ungrouped/ready/error), and inside `ready` branches on `classifications[0] ?? null` into grouped-but-unclassified (Classify CTA, no crash) vs. fully classified (all four cards, explicit props)
- Every mutating action (analyze/classify/approve/remediate/mark-false-positive/reclassify) refetches campaign state via a shared `load()` callback — no optimistic local mutation (D-04)
- Minimal campaigns list page (`app/phishing/page.tsx`) with DataTable, EmptyState, and row-click navigation to the ticket-scoped page
- "Phishing" nav entry added to `components/navigation/app-navigation.tsx`, visible to every role (phishing:read is universal)
- New `GET /api/phishing/reports/{report_id}` route fills the D-08 ungrouped-report evidence gap (see Deviations)
- Task 3 blocking checkpoint resolved: a real production LiveLink click against ticket 699340 (`https://pulse.wulfconsulting.cloud/phishing/tickets/699340`) confirmed the URL renders the plain numeric Autotask ticket ID as the path segment — the resolver's `Number(ticket_id)` assumption is correct, no `ticket_number` fallback needed

## Task Commits

Each task was committed atomically:

1. **Task 1: Ticket-scoped review page (REVIEW-01, REVIEW-05, REVIEW-06)** - `3761312` (feat)
2. **Task 2: Campaigns list page (D-00) + Phishing nav entry (D-02)** - `5f5d809` (feat)
3. **Task 3: LiveLink numeric-ticket-id manual verification** - verified via a real production LiveLink click on ticket 699340 (2026-07-16); no code change required, no separate commit

**Plan metadata:** (this commit)

## Files Created/Modified

- `app/phishing/tickets/[ticketId]/page.tsx` - LiveLink-addressable ticket review page; state machine + card composition
- `app/api/phishing/reports/[report_id]/route.ts` - new: standalone report evidence + fresh blast-radius lookup for D-08
- `app/phishing/page.tsx` - campaigns list page (DataTable, EmptyState, row-click navigation)
- `components/navigation/app-navigation.tsx` - added `ShieldAlert` import + "Phishing" nav item after PAX8

## Decisions Made

- Task 3 resolved "verified" on the strength of a real production LiveLink click (ticket 699340, 2026-07-16) rather than a synthetic/staging test — the numeric-ticket-id assumption is now confirmed against the live Autotask tenant, closing 22-RESEARCH's Open Question. The page itself rendered blank in that same click-through, but that is a separate, already-acknowledged infra/deploy gap (production container running a build that predates this feature and phases 15-21), not a code-correctness issue — a deploy was in progress separately. Ticket 699340 is also not itself a phishing report, so even post-deploy it would only exercise the D-07 "Not yet triaged" empty state, which is expected.
- Extended the campaign-detail-route bulk-fetch idiom (query by id, `.map()` to camelCase, fresh `getBlastRadius()` call) to a new report-scoped route rather than overloading the existing ticket→campaign resolver's locked `{found, reportId, campaignId, ticketNumber}` response shape (that shape was already implemented and verified in plan 22-02 — widening it risked breaking an already-shipped contract).

## Deviations from Plan

### Auto-fixed Issues

**1. [Rule 2 - Missing Critical Functionality] Added GET /api/phishing/reports/{report_id} for the D-08 ungrouped-report evidence state**
- **Found during:** Task 1 (ticket-scoped review page)
- **Issue:** The plan's own `must_haves.truths` requires "a standalone-report notice + evidence for D-08" (a `reports` row exists but `campaign_id IS NULL` — the narrow race window before grouping runs) and the plan's Task 1 action text says the `ungrouped` state should "fetch that report's evidence for the standalone EvidenceCard." No existing route can supply this: the ticket→campaign resolver route returns only `{found, reportId, campaignId, ticketNumber}` (a locked, already-verified shape from plan 22-02), and the campaign-detail route is keyed on a non-null `campaignId`, which an ungrouped report by definition doesn't have. Without a data source, the D-08 truth could not be satisfied and `<EvidenceCard>` would have nothing to render.
- **Fix:** Added a new, additive `GET /api/phishing/reports/{report_id}` route mirroring the existing campaign-detail route's bulk-fetch idiom (`requirePermission('phishing', 'read')`, UUID validation, report/message/indicator queries, a fresh `getBlastRadius()` lookup using the same sender/recipient/subject/dateWindow derivation), scoped to a single report instead of a campaign. Returns `{ id, ticketId, ticketNumber, companyName, title, createdAt, requesterEmail, messages: EvidenceMessage[], blastRadius }` — the exact shape `<EvidenceCard>` consumes.
- **Files modified:** `app/api/phishing/reports/[report_id]/route.ts` (new)
- **Verification:** `npx tsc --noEmit --pretty` clean; route follows the identical auth/validation/error-handling pattern as the five other existing phishing routes (confirmed via grep comparison against `app/api/phishing/campaigns/[id]/route.ts`)
- **Committed in:** `3761312` (Task 1 commit)

---

**Total deviations:** 1 auto-fixed (1 missing critical functionality)
**Impact on plan:** Necessary for the D-08 ungrouped-report state (an explicit `must_haves.truths` requirement) to actually render evidence instead of crashing or silently omitting it. No scope creep — the new route is additive, doesn't modify any existing route's response shape, and follows established conventions exactly.

## Issues Encountered

- Task 3 (blocking human-verify checkpoint) could not be performed by the executor — it requires a real Autotask LiveLink click against a live production tenant. Resolved externally by the coordinator: a real click against ticket 699340 in production confirmed the numeric-ticket-id assumption. See Decisions Made above for full detail, including the separately-tracked production deploy gap (unrelated to this plan's code).

## User Setup Required

None - no external service configuration required. (The production deploy needed to actually serve this code is tracked separately by the coordinator, outside this plan's scope.)

## Next Phase Readiness

- All three tasks of the final phase-22 plan are complete: REVIEW-01, REVIEW-05, REVIEW-06 requirements now marked Complete in REQUIREMENTS.md.
- REVIEW-03 (EvidenceCard, owned by plan 22-03) remains marked Pending in REQUIREMENTS.md's traceability table — it was not in this plan's declared `requirements` frontmatter and is out of this plan's scope to close; noted here so phase-transition tooling doesn't silently miss it.
- Phase 22 (and the v3.0 Phishing Triage Automation milestone) has no further plans queued after this one, pending the production deploy referenced above and a full manual click-through (per 22-VALIDATION.md's Manual-Only section) once that deploy lands.

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

## Self-Check: PASSED

- FOUND: `app/phishing/tickets/[ticketId]/page.tsx`
- FOUND: `app/phishing/page.tsx`
- FOUND: `app/api/phishing/reports/[report_id]/route.ts`
- FOUND: `components/navigation/app-navigation.tsx`
- FOUND: `.planning/REQUIREMENTS.md` (REVIEW-01/05/06 marked complete)
- FOUND commit: `3761312` (Task 1)
- FOUND commit: `5f5d809` (Task 2)
- Task 3: verified externally via a real production LiveLink click (ticket 699340, 2026-07-16) — no code commit associated (no fallback needed)
