diff --git a/.planning/REQUIREMENTS.md b/.planning/REQUIREMENTS.md index df497ff..4a00f38 100644 --- a/.planning/REQUIREMENTS.md +++ b/.planning/REQUIREMENTS.md @@ -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 diff --git a/.planning/phases/22-approval-ui-livelink-addressable-campaign-review-and-approve/22-06-SUMMARY.md b/.planning/phases/22-approval-ui-livelink-addressable-campaign-review-and-approve/22-06-SUMMARY.md new file mode 100644 index 0000000..ba17232 --- /dev/null +++ b/.planning/phases/22-approval-ui-livelink-addressable-campaign-review-and-approve/22-06-SUMMARY.md @@ -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 `` 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 `` 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)