docs(22-06): complete LiveLink review page plan

Task 3 (blocking human-verify) resolved as "verified": a real production
Autotask LiveLink click against ticket 699340 confirmed the numeric-
ticket-id resolver assumption, no ticket_number fallback needed. Marks
REVIEW-01, REVIEW-05, REVIEW-06 complete in REQUIREMENTS.md.
This commit is contained in:
lorentz 2026-07-16 15:39:12 -04:00
parent 5f5d809050
commit 4b2a7713b4
2 changed files with 147 additions and 6 deletions

View file

@ -111,7 +111,7 @@ destructive remediation gated behind explicit human approval.
### Approval UI (LiveLink)
- [ ] **REVIEW-01**: A stable, ticket-ID-addressable Pulse route (e.g.
- [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
@ -127,12 +127,12 @@ destructive remediation gated behind explicit human approval.
- [x] **REVIEW-04**: The page displays the current classification (SPAM/
UNWANTED/THREAT), confidence, reasons, and recommended remediation
action(s)
- [ ] **REVIEW-05**: An operator can approve, remediate, or mark a campaign as
- [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)
- [ ] **REVIEW-06**: An operator without the elevated permission approve/
- [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
@ -203,12 +203,12 @@ Populated during roadmap creation.
| REMED-06 | Phase 20 | Complete |
| NOTE-01 | Phase 21 | Complete |
| ACCESS-01 | Phase 18 | Complete |
| REVIEW-01 | Phase 22 | Pending |
| REVIEW-01 | Phase 22 | Complete |
| REVIEW-02 | Phase 22 | Complete |
| REVIEW-03 | Phase 22 | Pending |
| REVIEW-04 | Phase 22 | Complete |
| REVIEW-05 | Phase 22 | Pending |
| REVIEW-06 | Phase 22 | Pending |
| REVIEW-05 | Phase 22 | Complete |
| REVIEW-06 | Phase 22 | Complete |
**Coverage:**
- v1 requirements: 32 total

View file

@ -0,0 +1,141 @@
---
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)