---
phase: 23-classification-disposition-per-client-automation-gate
plan: 02
type: execute
wave: 1
depends_on: []
files_modified:
  - components/phishing/classification-card.tsx
  - components/phishing/action-area-card.tsx
  - components/phishing/timeline-card.tsx
autonomous: true
requirements: [CLASSDISP-03]
must_haves:
  truths:
    - "A campaign classified USER_AWARENESS renders in ClassificationCard with a distinct badge color (not UNWANTED's amber)"
    - "The acknowledge_user action renders with a human-readable label ('Acknowledge user') wherever action labels appear"
    - "In the manual review UI, acknowledge_user appears as a normal checkbox + Approve action for every company, with no gate-check logic"
    - "A USER_AWARENESS classification entry or campaign_classified audit entry renders in TimelineCard with a distinct (emerald) tint and does NOT crash the review page"
  artifacts:
    - path: "components/phishing/classification-card.tsx"
      provides: "USER_AWARENESS badge variant + acknowledge_user label"
      contains: "USER_AWARENESS"
    - path: "components/phishing/action-area-card.tsx"
      provides: "acknowledge_user ActionParamsForm case + label"
      contains: "acknowledge_user"
    - path: "components/phishing/timeline-card.tsx"
      provides: "USER_AWARENESS VERDICT_TINT entry + defensive .split fallback"
      contains: "USER_AWARENESS"
  key_links:
    - from: "ClassificationCardData['verdict'] union"
      to: "VERDICT_VARIANT_CLASS record"
      via: "new keyed entry"
      pattern: "USER_AWARENESS.*bg-"
    - from: "action-area-card ActionParamsForm switch"
      to: "acknowledge_user case"
      via: "new switch arm mirroring no_action"
      pattern: "case 'acknowledge_user'"
    - from: "TimelineCard VERDICT_TINT lookup"
      to: "USER_AWARENESS tint entry"
      via: "4th record key + fallback on the .split sites"
      pattern: "USER_AWARENESS"
---

<objective>
Surface the new `USER_AWARENESS` verdict and `acknowledge_user` action in the Phase 22 review UI. `ClassificationCard` gets a distinct badge color for the new verdict; `ActionAreaCard` renders `acknowledge_user` as a normal recommended-action checkbox (like every other action) so an operator can approve it manually on the review page for companies that do not have report-to-ticket automation enabled; and `TimelineCard` gets a 4th verdict tint plus a defensive fallback so a USER_AWARENESS verdict no longer crashes the review page's timeline.

Purpose: Operators must be able to see the User Awareness disposition distinctly and manually approve the acknowledgment where automation is off — and the review page must not crash for the exact verdict this phase creates.
Output: Extended verdict/label maps + a defensive tint fallback across three Phase 22 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/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/23-classification-disposition-per-client-automation-gate/23-CONTEXT.md
@.planning/phases/23-classification-disposition-per-client-automation-gate/23-PATTERNS.md

<interfaces>
<!-- Locked identifiers this UI must reference (defined in Plan 01) -->
Verdict literal: `USER_AWARENESS`
Action id: `acknowledge_user`  ->  display label: `Acknowledge user`

From components/phishing/classification-card.tsx:
- `ClassificationCardData['verdict']` type (line 15): `'SPAM' | 'UNWANTED' | 'THREAT'` — add USER_AWARENESS
- `VERDICT_VARIANT_CLASS` record (lines 30-38): SPAM slate, UNWANTED amber, THREAT destructive
- `ACTION_LABEL` record (lines 40-44): block_sender/purge_message/warn_user/no_action/reset_password/isolate_endpoint/disable_forwarding_rule
- StatusBadge consumes a free-form Tailwind class string (variantClass) — a new record key is sufficient, no StatusBadge change

From components/phishing/action-area-card.tsx:
- `ACTION_LABEL` record (lines 67-75)
- `ActionParamsForm` switch (lines 111-246); `no_action` case (lines 126-132) is the template for a no-params action
- Manual approval flow handleApprove -> POST /api/phishing/campaigns/[id]/approve (lines 375-399)

From components/phishing/timeline-card.tsx:
- `TimelineEntry` classification-entry `verdict` union (line 10): `'SPAM' | 'UNWANTED' | 'THREAT'` — add USER_AWARENESS
- `VERDICT_TINT` record (lines 17-21): SPAM `'bg-slate-500 text-slate-600'`, UNWANTED `'bg-amber-500 text-amber-600'`, THREAT `'bg-destructive text-destructive'` (dot class + text class, space-separated)
- Classification branch (lines 52-53): `VERDICT_TINT[entry.verdict].split(' ')[0]` / `[1]` — crashes if lookup is undefined
- `campaign_classified` audit case (lines 86-95): casts `payload.verdict as 'SPAM'|'UNWANTED'|'THREAT'|undefined` then `tint.split(' ')`
- NOTE: the timeline SERVICE `lib/services/phishing-timeline.ts` emits `verdict` as plain runtime `string` (line 15, unvalidated) — so a `'USER_AWARENESS'` value reaches this component regardless of the component's own narrowed type; the fallback below is the runtime safety net.
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Add USER_AWARENESS badge variant + acknowledge_user label to ClassificationCard</name>
  <files>components/phishing/classification-card.tsx</files>
  <read_first>
    - components/phishing/classification-card.tsx (ClassificationCardData['verdict'] type line 15, VERDICT_VARIANT_CLASS lines 30-38, ACTION_LABEL lines 40-44, StatusBadge usage line 115)
    - components/ui/status-badge.tsx (CustomVariantProps — confirm variantClass is a free-form class string)
    - .planning/phases/23-classification-disposition-per-client-automation-gate/23-PATTERNS.md (classification-card.tsx section)
  </read_first>
  <action>
    Add `'USER_AWARENESS'` to the `ClassificationCardData['verdict']` union (line 15). Add a `VERDICT_VARIANT_CLASS` entry keyed `USER_AWARENESS` using a distinct, clearly-not-UNWANTED color — use emerald: `'bg-emerald-500/15 text-emerald-600'` (D-01 discretion; distinct from UNWANTED's amber and THREAT's destructive red, signalling a positive/benign disposition). Add `acknowledge_user: 'Acknowledge user'` to the `ACTION_LABEL` record. No StatusBadge component change is required — variantClass is a free-form Tailwind string.
  </action>
  <verify>
    <automated>cd /opt/stacks/pulse && npx tsc --noEmit --pretty</automated>
  </verify>
  <acceptance_criteria>
    - `grep -q "USER_AWARENESS" components/phishing/classification-card.tsx` succeeds in both the verdict union and VERDICT_VARIANT_CLASS
    - `grep -q "acknowledge_user: 'Acknowledge user'" components/phishing/classification-card.tsx` succeeds
    - The USER_AWARENESS variant class differs from the UNWANTED (amber) and THREAT (destructive) entries
    - `npx tsc --noEmit` exits 0 (the Record<verdict,...> is exhaustive so a missing key would fail the build)
  </acceptance_criteria>
  <done>ClassificationCard renders USER_AWARENESS with a distinct emerald badge and labels acknowledge_user; type-check passes.</done>
</task>

<task type="auto">
  <name>Task 2: Add acknowledge_user manual action case to ActionAreaCard</name>
  <files>components/phishing/action-area-card.tsx</files>
  <read_first>
    - components/phishing/action-area-card.tsx (ACTION_LABEL lines 67-75, ActionParamsForm switch lines 111-246, no_action case lines 126-132, handleApprove flow lines 375-399)
    - .planning/phases/23-classification-disposition-per-client-automation-gate/23-PATTERNS.md (action-area-card.tsx section + watch-out flag #2)
  </read_first>
  <action>
    Add `acknowledge_user: 'Acknowledge user'` to this file's `ACTION_LABEL` record. Add a `case 'acknowledge_user':` arm to `ActionParamsForm`'s switch, mirroring the `no_action` no-params case — render a short muted paragraph explaining "No parameters — posts a customer-visible thank-you note to the reporting employee." acknowledge_user must render exactly like every other recommended action here (checkbox + Approve button via the existing handleApprove -> POST approve flow). Do NOT add any automation-gate / per-company gate-check logic to this component (watch-out flag #2 / D-04): the auto-approval carve-out lives only in the webhook path (Plan 05); the manual review UI always shows acknowledge_user as a normal approvable action for every company regardless of gate state.
  </action>
  <verify>
    <automated>cd /opt/stacks/pulse && npx tsc --noEmit --pretty</automated>
  </verify>
  <acceptance_criteria>
    - `grep -q "case 'acknowledge_user'" components/phishing/action-area-card.tsx` succeeds
    - `grep -q "acknowledge_user: 'Acknowledge user'" components/phishing/action-area-card.tsx` succeeds
    - No new reference to phishing_automation_gate, auto_report, or any gate table/flag appears in this component (grep -Ei "automation_gate|auto_report|auto_parse|auto_classify" returns nothing)
    - `npx tsc --noEmit` exits 0
  </acceptance_criteria>
  <done>acknowledge_user renders as a normal manual checkbox+Approve action with a no-params form; no gate logic added to the manual UI; type-check passes.</done>
</task>

<task type="auto">
  <name>Task 3: Add USER_AWARENESS support to TimelineCard (prevents review-page crash)</name>
  <files>components/phishing/timeline-card.tsx</files>
  <read_first>
    - components/phishing/timeline-card.tsx (TimelineEntry classification verdict line 10, VERDICT_TINT record lines 17-21, classification .split sites lines 52-53, campaign_classified audit case lines 86-95)
    - lib/services/phishing-timeline.ts (TimelineEntry.verdict is plain `string` at runtime line 15 — unvalidated, so a USER_AWARENESS value flows straight into this component)
    - .planning/phases/23-classification-disposition-per-client-automation-gate/23-PATTERNS.md (classification-card.tsx section — mirror the emerald choice)
  </read_first>
  <action>
    TimelineCard independently declares `verdict: 'SPAM' | 'UNWANTED' | 'THREAT'` and `VERDICT_TINT: Record<'SPAM'|'UNWANTED'|'THREAT', string>`, then does `VERDICT_TINT[entry.verdict].split(' ')[0]`. Because the timeline service (`phishing-timeline.ts`) emits `verdict` as plain runtime `string`, a `'USER_AWARENESS'` value hits `VERDICT_TINT['USER_AWARENESS']` = undefined and `undefined.split(' ')` throws — crashing the review page for exactly this phase's verdict. Fix: (1) add `'USER_AWARENESS'` to the classification-entry `verdict` union (line 10). (2) Add a 4th `VERDICT_TINT` entry keyed `USER_AWARENESS` and widen the Record key type to include it — use emerald to match Task 1's ClassificationCard choice, in this file's dot+text format: `USER_AWARENESS: 'bg-emerald-500 text-emerald-600'`. (3) Widen the `campaign_classified` audit-case cast (line 87) to include `'USER_AWARENESS'`. (4) Defensively guard BOTH `.split(' ')` sites — the classification branch (lines 52-53) and the audit branch (the `tint` var, lines 88-93) — with a fallback so ANY unexpected runtime verdict string can never crash: e.g. `const tint = VERDICT_TINT[entry.verdict] ?? 'bg-muted-foreground text-muted-foreground';` then `.split(' ')`, and in the audit case `const tint = (verdict && VERDICT_TINT[verdict]) || 'bg-muted-foreground text-muted-foreground';`. Do not change layout or any other behavior.
  </action>
  <verify>
    <automated>cd /opt/stacks/pulse && npx tsc --noEmit --pretty</automated>
  </verify>
  <acceptance_criteria>
    - `grep -q "USER_AWARENESS" components/phishing/timeline-card.tsx` succeeds in both the verdict union and VERDICT_TINT
    - `grep -c "?? 'bg-muted-foreground text-muted-foreground'\||| 'bg-muted-foreground text-muted-foreground'" components/phishing/timeline-card.tsx` >= 1 (defensive fallback covering the .split sites)
    - The USER_AWARENESS tint differs from UNWANTED (amber) and THREAT (destructive)
    - `npx tsc --noEmit` exits 0
  </acceptance_criteria>
  <done>TimelineCard renders a USER_AWARENESS classification/audit entry with a distinct emerald tint and no longer crashes on an unrecognized verdict; type-check passes.</done>
</task>

</tasks>

<threat_model>
## Trust Boundaries

| Boundary | Description |
|----------|-------------|
| operator browser -> review UI | operator triggers approve of acknowledge_user; server-side permission gate (Phase 20/22) unchanged |

## STRIDE Threat Register

| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-23-03 | Elevation of Privilege | action-area-card acknowledge_user manual approve | accept | This plan adds no new permission path — acknowledge_user routes through the existing handleApprove -> POST approve flow whose server-side requirePermission gate (Phase 20) is unchanged; UI-only display change |
| T-23-12 | Denial of Service | TimelineCard render on new verdict | mitigate | The 4th VERDICT_TINT entry plus a fallback on both `.split` sites means an unrecognized/new runtime verdict string can never throw and crash the review page |
</threat_model>

<verification>
- `npx tsc --noEmit --pretty` passes (exhaustive Record keys enforce completeness)
- USER_AWARENESS badge distinct from UNWANTED; acknowledge_user labeled and manually approvable; no gate logic in action-area-card; TimelineCard renders USER_AWARENESS without crashing
</verification>

<success_criteria>
- USER_AWARENESS renders with a distinct badge color in the review page
- acknowledge_user shows as a normal manual checkbox+Approve action for every company with no gate-check logic in the component
- The review-page timeline renders a USER_AWARENESS classification/audit entry without crashing
</success_criteria>

<output>
Create `.planning/phases/23-classification-disposition-per-client-automation-gate/23-02-SUMMARY.md` when done
</output>
