---
phase: 22-approval-ui-livelink-addressable-campaign-review-and-approve
plan: 04
type: execute
wave: 1
depends_on: []
files_modified:
  - components/phishing/classification-card.tsx
  - components/phishing/timeline-card.tsx
autonomous: true
requirements: [REVIEW-02, REVIEW-04]
must_haves:
  truths:
    - "ClassificationCard shows the latest verdict (SPAM/UNWANTED/THREAT), confidence, summary, reasons list, and recommended action chips"
    - "ClassificationCard shows a requires-approval warning Alert when requiresApproval is true"
    - "TimelineCard renders a single chronological list merging linked reports, classification history, and audit events"
  artifacts:
    - path: "components/phishing/classification-card.tsx"
      provides: "read-only classification display (REVIEW-04)"
      exports: ["ClassificationCard"]
    - path: "components/phishing/timeline-card.tsx"
      provides: "chronological timeline renderer (REVIEW-02)"
      exports: ["TimelineCard"]
  key_links:
    - from: "components/phishing/timeline-card.tsx"
      to: "TimelineEntry (from plan 02 detail-route timeline)"
      via: "renders entry.kind discriminant"
      pattern: "kind"
---

<objective>
Build the two remaining read-only display components: `ClassificationCard` (REVIEW-04 — latest verdict/confidence/reasons/recommended actions, with a destructive-action approval warning) and `TimelineCard` (REVIEW-02 — a single chronological list rendering the pre-merged timeline array from the extended detail route).

Purpose: These are pure presentational renderers of data the plan-02 route already shapes; keeping them separate from the interactive action card keeps each component focused.
Output: 2 phishing components.
</objective>

<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/phases/22-approval-ui-livelink-addressable-campaign-review-and-approve/22-UI-SPEC.md
@.planning/phases/22-approval-ui-livelink-addressable-campaign-review-and-approve/22-PATTERNS.md

<interfaces>
classification (latest row, from detail route):
  { id; verdict:'SPAM'|'UNWANTED'|'THREAT'; confidence:string|null; summary:string|null; reasons:string[]; recommendedActions:string[]; requiresApproval:boolean; createdAt }

timeline (from detail route, plan 02 — TimelineEntry[]):
  { kind:'report'; at; reportId; ticketNumber; companyName }
  | { kind:'classification'; at; verdict; confidence }
  | { kind:'audit'; at; eventType; actor; payload }

Verdict badge recipes (UI-SPEC Color table):
  SPAM=bg-slate-500/15 text-slate-600 · UNWANTED=bg-amber-500/15 text-amber-600 · THREAT=bg-destructive/15 text-destructive

Audit event_type → label/icon/tint (UI-SPEC Timeline Spec):
  remediation_approved="{N} action(s) approved by {actor}" CheckCircle2 blue
  remediation_completed="Remediation completed" ShieldCheck green
  campaign_marked_false_positive="Marked as false positive" XCircle slate
  campaign_classified → classification entries; any other → humanized fallback + Circle + muted
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: ClassificationCard (REVIEW-04)</name>
  <files>components/phishing/classification-card.tsx</files>
  <read_first>
    - app/analyzer/reports/[id]/page.tsx (Card section + small presentational sub-component idiom)
    - components/ui/status-badge.tsx (verdict + action chip badge shapes)
    - components/ui/alert.tsx (the requires-approval warning Alert)
    - lib/auth-client.ts (useSession export) and lib/permissions.ts (hasPermission signature — for the Reclassify button gate)
    - .planning/phases/22-approval-ui-livelink-addressable-campaign-review-and-approve/22-UI-SPEC.md (Classification Display Spec)
  </read_first>
  <action>
    Create `components/phishing/classification-card.tsx` (`'use client'`) exporting `ClassificationCard({ campaignId, classification, onReclassified })`. Wrap in `<Card>` with `<CardTitle className="font-bold">` + leading lucide `Sparkles` (`h-4 w-4 mr-2 inline`) reading "Classification". Header row: verdict StatusBadge (per recipe above) + "{confidence}% confidence" inline + a right-aligned "Reclassify ticket" button (`variant="outline"` `size="sm"`) that is HIDDEN (not disabled) unless `hasPermission(role, 'phishing', 'analyze')` where role comes from `useSession()` (`(session?.user as {role?:string})?.role ?? 'user'`); on click it POSTs `/api/phishing/campaigns/${campaignId}/classify` then toasts and calls `onReclassified()`. Summary: `classification.summary` as `text-sm`. Reasons: `<ul className="list-disc pl-5 text-sm text-muted-foreground space-y-1">` one `<li>` per reasons entry. Recommended actions: an informational chip row of humanized labels (block_sender→"Block sender", purge_message→"Purge message", warn_user→"Warn user", no_action→"No action", reset_password→"Reset password", isolate_endpoint→"Isolate endpoint", disable_forwarding_rule→"Disable forwarding rule") as slate/muted StatusBadges — NO checkboxes or editable params here (that surface lives only in ActionAreaCard). If `requiresApproval === true`, render an inline amber-tone `<Alert>`: "This classification recommends a destructive action and requires explicit approval before remediation can proceed." This component returns `null` (renders nothing) when `classification` is null — it does NOT attempt to render a verdict/reasons/actions for a missing classification. Two upstream paths pass a null classification, and plan 06 (not this card) renders the replacement UI: (a) a grouped-but-not-yet-classified campaign — the DEFAULT state, since classification is never auto-triggered by the detection/grouping pipeline — where plan 06 renders a "Classify this campaign" empty-state CTA; and (b) the D-08 ungrouped-report path, where plan 06 renders the D-08 Alert. This card must therefore guard on `classification == null` at the top and return null (never dereference `classification.verdict`/`.reasons`/etc.).
  </action>
  <verify>
    <automated>grep -q "hasPermission" components/phishing/classification-card.tsx && grep -q "font-bold" components/phishing/classification-card.tsx && npx tsc --noEmit --pretty 2>&1 | grep -c "classification-card" | grep -qx 0 && echo OK</automated>
  </verify>
  <acceptance_criteria>
    - `grep -q "requiresApproval" components/phishing/classification-card.tsx` succeeds and an Alert renders on that branch
    - `grep -q "hasPermission(role, 'phishing', 'analyze')" components/phishing/classification-card.tsx` (or equivalent call) — Reclassify gated on analyze
    - `grep -c "Checkbox" components/phishing/classification-card.tsx` returns 0 (no approve surface here)
    - The component early-returns `null` when `classification == null` (no dereference of `classification.verdict`/`.reasons`) — verified by presence of a null/falsy guard before any `classification.` access
    - `npx tsc --noEmit --pretty` reports no error in this file
  </acceptance_criteria>
  <done>ClassificationCard displays verdict/confidence/summary/reasons/recommended-action chips + approval warning; Reclassify gated on analyze permission.</done>
</task>

<task type="auto">
  <name>Task 2: TimelineCard (REVIEW-02)</name>
  <files>components/phishing/timeline-card.tsx</files>
  <read_first>
    - components/ui/status-light.tsx (the 8px h-2 w-2 dot size token to reuse for the rail)
    - .planning/phases/22-approval-ui-livelink-addressable-campaign-review-and-approve/22-UI-SPEC.md (Timeline Spec — sources, event_type→icon/label/tint table, rail visual)
    - .planning/phases/22-approval-ui-livelink-addressable-campaign-review-and-approve/22-PATTERNS.md ("No Analog Found" note for timeline-card — build fresh with div+Tailwind)
  </read_first>
  <action>
    Create `components/phishing/timeline-card.tsx` (`'use client'`) exporting `TimelineCard({ timeline }: { timeline: TimelineEntry[] })` (define the TimelineEntry union locally mirroring the detail route's shape, or import a shared type if plan 02 exports one). Wrap in `<Card>` with `<CardTitle className="font-bold">`. Render a vertical list (already ascending/oldest-first from the server merge — do NOT re-sort) as plain `div`s + Tailwind: a left rail with an 8px dot (`h-2 w-2 rounded-full`, tinted per entry) connected by a 1px `border-l`; right side shows the label (`text-sm`), a timestamp (`font-mono text-xs text-muted-foreground`, relative with the absolute value in a `title` attr), and actor when present. Map each entry.kind: 'report' → "Report linked — Ticket #{ticketNumber} ({companyName})" icon FileText muted; 'classification' → "Classified as {verdict} ({confidence}%)" icon Sparkles tinted per verdict recipe; 'audit' → per the event_type table (remediation_approved→CheckCircle2/blue, remediation_completed→ShieldCheck/green, campaign_marked_false_positive→XCircle/slate, any other→humanized snake_case→Title Case fallback with Circle/muted). No new reusable "Timeline" primitive — this is a one-off layout.
  </action>
  <verify>
    <automated>grep -q "h-2 w-2" components/phishing/timeline-card.tsx && npx tsc --noEmit --pretty 2>&1 | grep -c "timeline-card" | grep -qx 0 && echo OK</automated>
  </verify>
  <acceptance_criteria>
    - `grep -q "remediation_completed" components/phishing/timeline-card.tsx` succeeds (audit event mapping present)
    - `grep -q "border-l" components/phishing/timeline-card.tsx` succeeds (rail present)
    - The component does not call `.sort(` on the timeline array (relies on server ordering) — `grep -c "\.sort(" components/phishing/timeline-card.tsx` returns 0
    - `npx tsc --noEmit --pretty` reports no error in this file
  </acceptance_criteria>
  <done>TimelineCard renders all three merged sources chronologically with per-type icon/label/tint.</done>
</task>

</tasks>

<threat_model>
## Trust Boundaries

| Boundary | Description |
|----------|-------------|
| classification/audit content → browser DOM | verdict/summary/reasons come from the classifier; audit actor/payload from audit_events — rendered as JSX text |

## STRIDE Threat Register

| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-22-10 | Tampering (XSS via classification reasons/summary or audit payload) | classification-card, timeline-card | mitigate | All fields rendered as JSX text (React auto-escapes); no `dangerouslySetInnerHTML` in either component |
| T-22-11 | Elevation of Privilege (Reclassify shown to unauthorized role) | classification-card | mitigate | Reclassify button hidden unless `hasPermission(role,'phishing','analyze')`; server route still enforces requirePermission independently |
| T-22-SC | Tampering | npm/pip/cargo installs | accept | No package-manager installs in this plan (22-RESEARCH — zero new packages) |
</threat_model>

<verification>
- `npx tsc --noEmit --pretty` clean
- `npm test` still green
</verification>

<success_criteria>
ClassificationCard and TimelineCard render latest classification and full chronological timeline respectively, safely and per the UI-SPEC color/typography contract.
</success_criteria>

<output>
Create `.planning/phases/22-approval-ui-livelink-addressable-campaign-review-and-approve/22-04-SUMMARY.md` when done
</output>
