---
phase: 22-approval-ui-livelink-addressable-campaign-review-and-approve
plan: 03
type: execute
wave: 1
depends_on: []
files_modified:
  - components/ui/tooltip.tsx
  - components/phishing/url-list.tsx
  - components/phishing/evidence-card.tsx
autonomous: true
requirements: [REVIEW-03]
must_haves:
  truths:
    - "Extracted URLs render as inert monospace text with a copy-to-clipboard button — never as a clickable link (D-09)"
    - "The evidence card shows parsed Headers, URLs, Attachments, Body preview, and Blast Radius tabs for a selected message"
    - "Body preview renders as plain text in a <pre>, never via dangerouslySetInnerHTML"
    - "Blast radius renders an explicit 'unavailable' state when status is unavailable, and counts + per-recipient table when ok"
    - "Attachment metadata (filename, content-type, size, hash) is displayed as plain evidence, as-is, with no extra redaction (D-10)"
  artifacts:
    - path: "components/ui/tooltip.tsx"
      provides: "shadcn tooltip primitive (for later disabled-button explanations)"
    - path: "components/phishing/url-list.tsx"
      provides: "inert copy-only URL list (D-09)"
      exports: ["UrlList"]
    - path: "components/phishing/evidence-card.tsx"
      provides: "tabbed EML evidence display (REVIEW-03)"
      exports: ["EvidenceCard"]
  key_links:
    - from: "components/phishing/url-list.tsx"
      to: "navigator.clipboard"
      via: "writeText on copy button click"
      pattern: "navigator.clipboard.writeText"
---

<objective>
Build the read-only evidence surface for the review page: the shadcn `tooltip` primitive (needed later by the action card), the inert copy-only `UrlList` component enforcing D-09's stricter-than-sanitization posture, and the tabbed `EvidenceCard` rendering parsed EML headers, URLs, attachments, body preview, and Mimecast blast radius (REVIEW-03).

Purpose: This is the phase's biggest XSS-exposure surface (attacker-controlled email content). It is built as a pure presentational component so the review page (plan 06) composes it without embedding rendering logic.
Output: 1 shadcn primitive + 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>
messages evidence shape (from the extended detail route, plan 02):
  headers: { from:{displayName,email,domain}; replyTo; returnPath; to:string[]; cc:string[]; subject; date; messageId; receivedChain:string[]; authResults:{spf?;dkim?;dmarc?}; authResultsOriginal:{...}|null }
  urls: string[]
  attachments: Array<{ filename:string|null; contentType:string|null; size:number; checksum:string|null; related:boolean }>
  bodyPreview: string (plain text, ≤500 chars)

blastRadius (from detail route):
  { status:'unavailable'; reason:'not_configured'|'lookup_failed'; error? }
  | { status:'ok'; matched; delivered; held; rejected; clicked; perRecipient: Array<{recipient; status:'delivered'|'held'|'rejected'|'unknown'}> }

Reused primitives (do not modify): components/ui/{tabs,table,accordion,alert,card,select,status-badge}.tsx, sonner toast, lucide-react (Copy).
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Add tooltip primitive + build UrlList (D-09 inert)</name>
  <files>components/ui/tooltip.tsx, components/phishing/url-list.tsx</files>
  <read_first>
    - components/ui/status-badge.tsx (existing primitive prop shape + cn() styling convention)
    - .planning/phases/22-approval-ui-livelink-addressable-campaign-review-and-approve/22-UI-SPEC.md (Evidence Display Spec §2 — exact url-list markup + copy affordance)
    - .planning/phases/22-approval-ui-livelink-addressable-campaign-review-and-approve/22-PATTERNS.md (section "components/phishing/url-list.tsx")
  </read_first>
  <action>
    First run `npx shadcn add tooltip` (official registry, generates `components/ui/tooltip.tsx` — no npm dependency added; if the CLI prompts, accept defaults). Then create `components/phishing/url-list.tsx` as a `'use client'` component exporting `UrlList({ urls }: { urls: string[] })`. Render each URL as `<code className="font-mono text-xs truncate">{url}</code>` followed by an icon-only copy button (`Button size="icon" variant="ghost"`, lucide `Copy` at `h-3.5 w-3.5`, `aria-label="Copy URL"`) whose onClick calls `navigator.clipboard.writeText(url)` then `toast.success('Copied')`. CRITICAL (D-09): never wrap a URL in `<a>`/`href`, never attach a navigating onClick, never use `<Link>` — copy-to-clipboard only. When `urls.length === 0`, render inline text "No URLs found in this message." (size sm, muted), NOT the full EmptyState component.
  </action>
  <verify>
    <automated>test -f components/ui/tooltip.tsx && grep -q "navigator.clipboard.writeText" components/phishing/url-list.tsx && grep -Lq "href" components/phishing/url-list.tsx && echo OK</automated>
  </verify>
  <acceptance_criteria>
    - `components/ui/tooltip.tsx` exists and exports Tooltip/TooltipTrigger/TooltipContent/TooltipProvider
    - `grep -q "navigator.clipboard.writeText" components/phishing/url-list.tsx` succeeds
    - `grep -c "href" components/phishing/url-list.tsx` returns 0 (no href anywhere)
    - `grep -q "aria-label=\"Copy URL\"" components/phishing/url-list.tsx` succeeds
    - No new dependency added to package.json (git diff of package.json is empty)
  </acceptance_criteria>
  <done>Tooltip primitive present; UrlList renders inert copy-only URLs with zero clickable link surface.</done>
</task>

<task type="auto">
  <name>Task 2: EvidenceCard — tabbed EML evidence (REVIEW-03)</name>
  <files>components/phishing/evidence-card.tsx</files>
  <read_first>
    - components/admin/DetailModal.tsx (card + tabs formatted/raw precedent)
    - components/ui/status-badge.tsx (StatusBadge prop shapes for SPF/DKIM/DMARC + blast-radius status badges)
    - components/phishing/url-list.tsx (task 1 — consumed here)
    - .planning/phases/22-approval-ui-livelink-addressable-campaign-review-and-approve/22-UI-SPEC.md (Evidence Display Spec §1-5, Color table for badge recipes)
    - .planning/phases/22-approval-ui-livelink-addressable-campaign-review-and-approve/22-PATTERNS.md (section for classification/evidence/timeline components)
  </read_first>
  <action>
    Create `components/phishing/evidence-card.tsx` (`'use client'`) exporting `EvidenceCard({ messages, blastRadius })` where `messages` is the extended detail route's message array (each with headers/urls/attachments/bodyPreview + ticketNumber-bearing report linkage) and `blastRadius` is the BlastRadiusResult. Wrap in a shadcn `<Card>` with `<CardHeader><CardTitle className="font-bold">` (explicit font-bold override — CardTitle defaults to font-semibold) and a leading lucide `FileText` icon (`h-4 w-4 mr-2 inline`). If more than one message exists, render an outer `<Select>` ("Report: Ticket #{ticketNumber} — {relative date}") choosing which message populates the tabs; default = most recently linked. Tabs via `components/ui/tabs.tsx`: (1) Headers — 2-col definition-list grid (`grid grid-cols-2 gap-x-6 gap-y-2 text-sm`) of From/Display name/Sender domain/Reply-To/Return-Path/To/Cc/Subject/Date/Message-ID/Received chain (Received chain collapsed under an Accordion item) plus SPF/DKIM/DMARC as StatusBadge (pass=`bg-green-500/15 text-green-600`, fail=`bg-red-500/15 text-red-600`, none/neutral=`bg-slate-500/15 text-slate-600`); Message-ID/Return-Path/email addresses render `font-mono text-xs`. (2) URLs — `<UrlList urls={message.urls} />`. (3) Attachments — shadcn `Table` of filename, content-type (slate StatusBadge), human-readable size, hash (`font-mono text-xs` truncated with `title` full value + icon-only copy button `aria-label="Copy hash"`); render no file content. (4) Body preview — `<pre className="max-h-96 overflow-y-auto whitespace-pre-wrap rounded-md bg-muted p-4 font-mono text-xs">{bodyPreview}</pre>` — NEVER dangerouslySetInnerHTML, never render as HTML. (5) Blast Radius — if `status === 'unavailable'` render the reason-specific copy from the UI-SPEC Copywriting Contract ("Blast radius unavailable — Mimecast isn't configured..." for not_configured; "Blast radius lookup failed: {error}..." for lookup_failed) with no table/CTA; if `status === 'ok'` render a labeled stat row (Matched/Delivered/Held/Rejected/Clicked) above a per-recipient `Table` (recipient `font-mono text-xs`, status StatusBadge: delivered=green, held=amber, rejected=`bg-destructive/15 text-destructive`, unknown=`bg-muted text-muted-foreground`). Every icon-only button carries an explicit aria-label.
  </action>
  <verify>
    <automated>grep -q "whitespace-pre-wrap" components/phishing/evidence-card.tsx && ! grep -q "dangerouslySetInnerHTML" components/phishing/evidence-card.tsx && npx tsc --noEmit --pretty 2>&1 | grep -c "evidence-card" | grep -qx 0 && echo OK</automated>
  </verify>
  <acceptance_criteria>
    - `grep -c "dangerouslySetInnerHTML" components/phishing/evidence-card.tsx` returns 0
    - `grep -q "whitespace-pre-wrap" components/phishing/evidence-card.tsx` succeeds (body preview in a pre)
    - `grep -q "UrlList" components/phishing/evidence-card.tsx` succeeds (URLs tab delegates to the inert component)
    - `grep -q "status === 'unavailable'" components/phishing/evidence-card.tsx` (or equivalent) — explicit unavailable branch present
    - `grep -q "font-bold" components/phishing/evidence-card.tsx` succeeds (CardTitle override)
    - `npx tsc --noEmit --pretty` reports no error in this file
  </acceptance_criteria>
  <done>EvidenceCard renders all 5 tabs safely; body preview inert; blast-radius unavailable state explicit.</done>
</task>

</tasks>

<threat_model>
## Trust Boundaries

| Boundary | Description |
|----------|-------------|
| attacker-controlled email content → browser DOM | headers, urls, attachment filenames/hashes, and body preview all originate from a reported (potentially malicious) email |

## STRIDE Threat Register

| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-22-07 | Tampering / Information Disclosure (stored XSS via body preview) | evidence-card.tsx | mitigate | Body preview rendered only inside `<pre className="whitespace-pre-wrap">` as JSX text; zero `dangerouslySetInnerHTML` anywhere (React auto-escapes text) |
| T-22-08 | Tampering (clickjacking / drive-by via malicious URL click) | url-list.tsx | mitigate | D-09 — extracted URLs rendered as inert `<code>` text with copy-to-clipboard only; no `<a href>`, no navigating onClick, no `<Link>` |
| T-22-09 | Information Disclosure (rendering raw attachment content) | evidence-card.tsx Attachments tab | mitigate | Only filename/content-type/size/hash metadata rendered; no file content is fetched or displayed (EVID-04) |
| T-22-SC | Tampering | npm/pip/cargo installs | accept | `npx shadcn add tooltip` generates local Radix source from the official registry — not an npm package install; per 22-RESEARCH Package Legitimacy Audit the gate does not apply |
</threat_model>

<verification>
- `npx tsc --noEmit --pretty` clean
- `npm test` still green (no component tests under vitest config)
- Manual: render evidence card with a fixture message and confirm no clickable URLs and plain-text body
</verification>

<success_criteria>
Evidence display renders headers/URLs/attachments/body/blast-radius safely: URLs inert copy-only, body plain-text, blast-radius unavailable state explicit.
</success_criteria>

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