---
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
