docs(22-04): complete ClassificationCard + TimelineCard plan

- Add 22-04-SUMMARY.md documenting the two components delivered
- Log pre-existing unrelated itglue-search.test.ts failures to
  deferred-items.md (out of scope for this plan)
- Mark REVIEW-02, REVIEW-04 complete in REQUIREMENTS.md
This commit is contained in:
lorentz 2026-07-16 14:30:52 -04:00
parent e990a320b2
commit fdc6a52dde
3 changed files with 117 additions and 4 deletions

View file

@ -117,14 +117,14 @@ destructive remediation gated behind explicit human approval.
(LiveLink supplies the ticket ID as dynamic content, not the internal (LiveLink supplies the ticket ID as dynamic content, not the internal
campaign UUID), authenticated via the existing Better Auth session only — campaign UUID), authenticated via the existing Better Auth session only —
no separate token or query-param auth scheme no separate token or query-param auth scheme
- [ ] **REVIEW-02**: The page displays the campaign's timeline — linked - [x] **REVIEW-02**: The page displays the campaign's timeline — linked
reports, classification history, and audit events (classify/approve/ reports, classification history, and audit events (classify/approve/
remediate/mark-false-positive) — in chronological order remediate/mark-false-positive) — in chronological order
- [ ] **REVIEW-03**: The page displays the gathered evidence — parsed EML - [ ] **REVIEW-03**: The page displays the gathered evidence — parsed EML
headers/URLs/attachments, sanitized body preview, and Mimecast blast-radius headers/URLs/attachments, sanitized body preview, and Mimecast blast-radius
data (including an explicit `unavailable` state when Mimecast isn't data (including an explicit `unavailable` state when Mimecast isn't
configured) — never rendering a raw/unsanitized body or unredacted secrets configured) — never rendering a raw/unsanitized body or unredacted secrets
- [ ] **REVIEW-04**: The page displays the current classification (SPAM/ - [x] **REVIEW-04**: The page displays the current classification (SPAM/
UNWANTED/THREAT), confidence, reasons, and recommended remediation UNWANTED/THREAT), confidence, reasons, and recommended remediation
action(s) action(s)
- [ ] **REVIEW-05**: An operator can approve, remediate, or mark a campaign as - [ ] **REVIEW-05**: An operator can approve, remediate, or mark a campaign as
@ -204,9 +204,9 @@ Populated during roadmap creation.
| NOTE-01 | Phase 21 | Complete | | NOTE-01 | Phase 21 | Complete |
| ACCESS-01 | Phase 18 | Complete | | ACCESS-01 | Phase 18 | Complete |
| REVIEW-01 | Phase 22 | Pending | | REVIEW-01 | Phase 22 | Pending |
| REVIEW-02 | Phase 22 | Pending | | REVIEW-02 | Phase 22 | Complete |
| REVIEW-03 | Phase 22 | Pending | | REVIEW-03 | Phase 22 | Pending |
| REVIEW-04 | Phase 22 | Pending | | REVIEW-04 | Phase 22 | Complete |
| REVIEW-05 | Phase 22 | Pending | | REVIEW-05 | Phase 22 | Pending |
| REVIEW-06 | Phase 22 | Pending | | REVIEW-06 | Phase 22 | Pending |

View file

@ -0,0 +1,98 @@
---
phase: 22-approval-ui-livelink-addressable-campaign-review-and-approve
plan: 04
subsystem: ui
tags: [react, nextjs, shadcn, phishing, tailwind, lucide]
# Dependency graph
requires:
- phase: 22-01/02/03
provides: campaign detail data shape (classification + timeline arrays) these components render
provides:
- "ClassificationCard — read-only latest-verdict display with Reclassify action (REVIEW-04)"
- "TimelineCard — chronological merged-event renderer (REVIEW-02)"
affects: [22-05, 22-06]
# Tech tracking
tech-stack:
added: []
patterns:
- "CardTitle className=\"font-bold\" override for the 700-weight card heading (UI-SPEC Typography contract)"
- "Client-side hasPermission(role, 'phishing', <action>) gate via useSession(), hiding (not disabling) the affordance"
- "Null-classification early-return guard — page-level empty states (plan 06) own the replacement UI, not the card"
key-files:
created:
- components/phishing/classification-card.tsx
- components/phishing/timeline-card.tsx
modified: []
key-decisions:
- "TimelineCard defines its own local TimelineEntry union (per plan's fallback instruction) rather than importing from plan 02, since plan 04 has no depends_on and plan 02's extended detail route may not exist in this worktree yet."
- "Audit event_type 'campaign_classified' is rendered with the same Sparkles/verdict-tinted treatment as 'classification' kind entries (per the plan's <interfaces> note: \"campaign_classified → classification entries\"), distinct from the generic humanized fallback used for any other/future event_type."
- "remediation_approved action count derived defensively from payload.actionIds.length, falling back to payload.actions.length, then 0 — matches the two audit-payload shapes actually written by lib/services/remediation-service.ts."
patterns-established:
- "Timeline rail: 8px h-2 w-2 rounded-full dot + 1px border-l connector, built with plain divs/Tailwind (UI-SPEC explicitly calls this a one-off, not a new primitive)."
requirements-completed: [REVIEW-02, REVIEW-04]
# Metrics
duration: 12min
completed: 2026-07-16
---
# Phase 22 Plan 04: ClassificationCard + TimelineCard Summary
**Two read-only presentational components — ClassificationCard (verdict/confidence/reasons/action-chips + approval warning, Reclassify gated on `phishing:analyze`) and TimelineCard (chronological reports+classifications+audit-events merge) — built per the UI-SPEC color/typography contract with zero new dependencies.**
## Performance
- **Duration:** ~12 min
- **Started:** 2026-07-16T18:18:00Z
- **Completed:** 2026-07-16T18:29:48Z
- **Tasks:** 2/2 completed
- **Files modified:** 2 created
## Accomplishments
- `ClassificationCard` renders the latest classification's verdict badge, confidence, summary, reasons list, and informational recommended-action chips; shows an amber `Alert` when `requiresApproval` is true; hides (not disables) the Reclassify button unless the session role has `phishing:analyze`; returns `null` when `classification` is missing so the review page's empty states (plan 06) own that surface.
- `TimelineCard` merges `report` / `classification` / `audit` timeline entries into a single ascending vertical list (relies on server ordering, never calls `.sort()`), with a per-kind icon/label/tint mapping including the full `audit_events.event_type` table from the UI-SPEC (`remediation_approved`, `remediation_completed`, `campaign_marked_false_positive`, `campaign_classified`, and a humanized fallback for anything else).
## Task Commits
1. **Task 1: ClassificationCard (REVIEW-04)** - `14adddf` (feat)
2. **Task 2: TimelineCard (REVIEW-02)** - `e990a32` (feat)
**Plan metadata:** commit pending (this SUMMARY + REQUIREMENTS)
## Files Created/Modified
- `components/phishing/classification-card.tsx` - `ClassificationCard({ campaignId, classification, onReclassified })` — verdict `StatusBadge`, confidence, summary, reasons `<ul>`, recommended-action chips, requires-approval `Alert`, permission-gated Reclassify button that POSTs `/api/phishing/campaigns/{id}/classify`.
- `components/phishing/timeline-card.tsx` - `TimelineCard({ timeline })` — locally-defined `TimelineEntry` union, per-kind rendering, 8px rail-dot + `border-l` layout, `date-fns` `formatDistanceToNow` for relative timestamps with the absolute value in a `title` attribute.
## Decisions Made
- Defined `TimelineEntry` locally in `timeline-card.tsx` rather than importing a shared type, since this plan has `depends_on: []` and plan 02's extended detail route (which shapes this array) is not guaranteed to exist yet in a parallel-wave worktree. The union mirrors the plan's `<interfaces>` block exactly (`kind: 'report' | 'classification' | 'audit'`).
- Treated audit `event_type: 'campaign_classified'` as its own case (Sparkles icon, verdict-tinted per `payload.verdict`) rather than falling through to the generic "any other" fallback, per the plan's explicit `<interfaces>` note ("campaign_classified → classification entries").
## Deviations from Plan
None — plan executed exactly as written. One out-of-scope, pre-existing test failure was discovered while running the plan's `npm test` verification step and logged (not fixed) per the executor's scope-boundary rule — see `## Deferred Issues` below.
## Deferred Issues
- `lib/services/analyzer/itglue-search.test.ts` has 2 failing assertions (`docs.length` mismatches in "tolerates per-call failures" tests). This file is unrelated to either component built in this plan and was not modified by any phase-22 plan. Logged to `.planning/phases/22-approval-ui-livelink-addressable-campaign-review-and-approve/deferred-items.md`; not fixed (out of scope). All other 394 tests pass.
## Verification Results
- `npx tsc --noEmit --pretty` — clean, no errors anywhere in the repo (including both new files).
- `npm test` — 394/396 passing; the 2 pre-existing failures are unrelated to this plan (see Deferred Issues above).
- Task-level acceptance criteria (grep checks for `hasPermission`, `font-bold`, `requiresApproval`, zero `Checkbox` occurrences, `h-2 w-2`, `remediation_completed`, `border-l`, zero `.sort(` occurrences) all passed as specified in the plan.
## Known Stubs
None — both components are fully wired to the props/interfaces specified in the plan; no hardcoded empty values or placeholder text.
## Threat Flags
None — both components render all classification/audit content as JSX text (React auto-escaping, no `dangerouslySetInnerHTML`), matching threat register items T-22-10 (XSS mitigation) and T-22-11 (Reclassify button gated client-side on `hasPermission`, with the server route independently enforcing the same permission per the plan's threat model).
## Self-Check: PASSED

View file

@ -0,0 +1,15 @@
# Deferred Items — Phase 22
Items discovered during execution that are out of scope for the current plan
(pre-existing, unrelated to the files this plan touches). Logged, not fixed,
per the executor's scope-boundary rule.
## 22-04: Pre-existing `itglue-search.test.ts` failures
- **Discovered during:** Plan 22-04 (`ClassificationCard` + `TimelineCard`), running `npm test` for the plan's verification step.
- **File:** `lib/services/analyzer/itglue-search.test.ts`
- **Symptom:** 2 failing assertions —
- `itglueSearch > tolerates per-call failures (flex asset errors, configurations still returns)`: `expect(result.docs.length).toBe(2)` received `1`
- `itglueSearch > tolerates per-call failures (configurations errors, flex still returns)`: `expect(result.docs.length).toBe(1)` received `0`
- **Scope:** Unrelated to this plan's files (`components/phishing/classification-card.tsx`, `components/phishing/timeline-card.tsx`). Not touched by plan 22-04 or any prior phase-22 plan.
- **Action:** Not fixed — out of scope per executor scope-boundary rules. All other 394 tests pass; `npx tsc --noEmit --pretty` is clean.