From af8a5bd438e554229776e668bcfd7582f2d97183 Mon Sep 17 00:00:00 2001
From: lorentz
Date: Thu, 16 Jul 2026 19:35:22 -0400
Subject: [PATCH 1/5] feat(23-02): add USER_AWARENESS badge variant +
acknowledge_user label to ClassificationCard
- Add USER_AWARENESS to ClassificationCardData verdict union
- Add emerald VERDICT_VARIANT_CLASS entry for USER_AWARENESS (distinct from UNWANTED amber and THREAT destructive)
- Add acknowledge_user: 'Acknowledge user' to ACTION_LABEL
---
components/phishing/classification-card.tsx | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/components/phishing/classification-card.tsx b/components/phishing/classification-card.tsx
index a6fc5a5..34b3730 100644
--- a/components/phishing/classification-card.tsx
+++ b/components/phishing/classification-card.tsx
@@ -12,7 +12,7 @@ import { hasPermission } from '@/lib/permissions';
export interface ClassificationCardData {
id: string;
- verdict: 'SPAM' | 'UNWANTED' | 'THREAT';
+ verdict: 'SPAM' | 'UNWANTED' | 'THREAT' | 'USER_AWARENESS';
confidence: string | null;
summary: string | null;
reasons: string[];
@@ -31,6 +31,7 @@ const VERDICT_VARIANT_CLASS: Record =
SPAM: 'bg-slate-500/15 text-slate-600',
UNWANTED: 'bg-amber-500/15 text-amber-600',
THREAT: 'bg-destructive/15 text-destructive',
+ USER_AWARENESS: 'bg-emerald-500/15 text-emerald-600',
};
const ACTION_LABEL: Record = {
@@ -41,6 +42,7 @@ const ACTION_LABEL: Record = {
reset_password: 'Reset password',
isolate_endpoint: 'Isolate endpoint',
disable_forwarding_rule: 'Disable forwarding rule',
+ acknowledge_user: 'Acknowledge user',
};
function humanizeAction(actionType: string): string {
From 547372e57aa68fb32a841ccba3ac873470d989a3 Mon Sep 17 00:00:00 2001
From: lorentz
Date: Thu, 16 Jul 2026 19:35:38 -0400
Subject: [PATCH 2/5] feat(23-02): add acknowledge_user manual action case to
ActionAreaCard
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Add acknowledge_user: 'Acknowledge user' to ACTION_LABEL
- Add case 'acknowledge_user' to ActionParamsForm mirroring no_action (no-params form)
- Renders as a normal checkbox + Approve action for every company; no automation-gate logic added (D-04) — the auto-approval carve-out lives only in the webhook path (Plan 05)
---
components/phishing/action-area-card.tsx | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/components/phishing/action-area-card.tsx b/components/phishing/action-area-card.tsx
index 40fe6b5..99f38bc 100644
--- a/components/phishing/action-area-card.tsx
+++ b/components/phishing/action-area-card.tsx
@@ -72,6 +72,7 @@ const ACTION_LABEL: Record = {
reset_password: 'Reset password',
isolate_endpoint: 'Isolate endpoint',
disable_forwarding_rule: 'Disable forwarding rule',
+ acknowledge_user: 'Acknowledge user',
};
function humanizeAction(actionType: string): string {
@@ -130,6 +131,12 @@ function ActionParamsForm({
No parameters — informational verdict, no remediation needed.
);
+ case 'acknowledge_user':
+ return (
+
+ No parameters — posts a customer-visible thank-you note to the reporting employee.
+
+ );
case 'warn_user':
return (
From 9ae834f5e64003c8e0d78d7730e84da589579527 Mon Sep 17 00:00:00 2001
From: lorentz
Date: Thu, 16 Jul 2026 19:36:11 -0400
Subject: [PATCH 3/5] feat(23-02): add USER_AWARENESS support to TimelineCard,
prevent review-page crash
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Add USER_AWARENESS to classification-entry verdict union and VERDICT_TINT (emerald, matching ClassificationCard)
- Widen campaign_classified audit-case cast to include USER_AWARENESS
- Add defensive fallback on both .split(' ') tint sites so any unrecognized runtime verdict string can never crash the render (T-23-12) — phishing-timeline.ts emits verdict as an unvalidated plain string
---
components/phishing/timeline-card.tsx | 22 ++++++++++++++++------
1 file changed, 16 insertions(+), 6 deletions(-)
diff --git a/components/phishing/timeline-card.tsx b/components/phishing/timeline-card.tsx
index f1bcf81..3c23897 100644
--- a/components/phishing/timeline-card.tsx
+++ b/components/phishing/timeline-card.tsx
@@ -7,17 +7,23 @@ import { cn } from '@/lib/utils';
export type TimelineEntry =
| { kind: 'report'; at: string; reportId: string; ticketNumber: string | null; companyName: string | null }
- | { kind: 'classification'; at: string; verdict: 'SPAM' | 'UNWANTED' | 'THREAT'; confidence: string | null }
+ | {
+ kind: 'classification';
+ at: string;
+ verdict: 'SPAM' | 'UNWANTED' | 'THREAT' | 'USER_AWARENESS';
+ confidence: string | null;
+ }
| { kind: 'audit'; at: string; eventType: string; actor: string | null; payload: unknown };
interface TimelineCardProps {
timeline: TimelineEntry[];
}
-const VERDICT_TINT: Record<'SPAM' | 'UNWANTED' | 'THREAT', string> = {
+const VERDICT_TINT: Record<'SPAM' | 'UNWANTED' | 'THREAT' | 'USER_AWARENESS', string> = {
SPAM: 'bg-slate-500 text-slate-600',
UNWANTED: 'bg-amber-500 text-amber-600',
THREAT: 'bg-destructive text-destructive',
+ USER_AWARENESS: 'bg-emerald-500 text-emerald-600',
};
function humanizeEventType(eventType: string): string {
@@ -46,11 +52,15 @@ function renderEntry(entry: TimelineEntry): {
if (entry.kind === 'classification') {
const confidence = entry.confidence != null ? ` (${entry.confidence}%)` : '';
+ // Defensive fallback: phishing-timeline.ts emits verdict as a plain
+ // unvalidated runtime `string`, so a value outside this component's
+ // narrowed union must never crash the render (T-23-12).
+ const tint = VERDICT_TINT[entry.verdict] ?? 'bg-muted-foreground text-muted-foreground';
return {
label: `Classified as ${entry.verdict}${confidence}`,
icon: Sparkles,
- dotClass: VERDICT_TINT[entry.verdict].split(' ')[0],
- textClass: VERDICT_TINT[entry.verdict].split(' ')[1],
+ dotClass: tint.split(' ')[0],
+ textClass: tint.split(' ')[1],
};
}
@@ -84,8 +94,8 @@ function renderEntry(entry: TimelineEntry): {
textClass: 'text-slate-600',
};
case 'campaign_classified': {
- const verdict = payload.verdict as 'SPAM' | 'UNWANTED' | 'THREAT' | undefined;
- const tint = verdict ? VERDICT_TINT[verdict] : 'bg-muted-foreground text-muted-foreground';
+ const verdict = payload.verdict as 'SPAM' | 'UNWANTED' | 'THREAT' | 'USER_AWARENESS' | undefined;
+ const tint = (verdict && VERDICT_TINT[verdict]) || 'bg-muted-foreground text-muted-foreground';
return {
label: verdict ? `Classified as ${verdict}` : 'Campaign classified',
icon: Sparkles,
From 44bbe90ae097ade3576bee18709da125cf930218 Mon Sep 17 00:00:00 2001
From: lorentz
Date: Thu, 16 Jul 2026 19:36:53 -0400
Subject: [PATCH 4/5] docs(23-02): complete Review UI USER_AWARENESS +
acknowledge_user support plan
Co-Authored-By: Claude Sonnet 5
Claude-Session: https://claude.ai/code/session_01W6RuWdiUiXrPK6FLBHjtpY
---
.../23-02-SUMMARY.md | 100 ++++++++++++++++++
1 file changed, 100 insertions(+)
create mode 100644 .planning/phases/23-classification-disposition-per-client-automation-gate/23-02-SUMMARY.md
diff --git a/.planning/phases/23-classification-disposition-per-client-automation-gate/23-02-SUMMARY.md b/.planning/phases/23-classification-disposition-per-client-automation-gate/23-02-SUMMARY.md
new file mode 100644
index 0000000..e90c427
--- /dev/null
+++ b/.planning/phases/23-classification-disposition-per-client-automation-gate/23-02-SUMMARY.md
@@ -0,0 +1,100 @@
+---
+phase: 23-classification-disposition-per-client-automation-gate
+plan: 02
+subsystem: ui
+tags: [react, typescript, phishing, tailwind, classification]
+
+# Dependency graph
+requires:
+ - phase: 22-approval-ui-livelink-addressable-campaign-review-and-approve
+ provides: ClassificationCard, ActionAreaCard, TimelineCard components and the campaign review page they render on
+provides:
+ - USER_AWARENESS verdict rendered with a distinct emerald badge in ClassificationCard
+ - acknowledge_user action label + no-params ActionParamsForm case in ClassificationCard and ActionAreaCard
+ - USER_AWARENESS verdict tint (emerald) in TimelineCard plus a defensive fallback on both `.split(' ')` tint sites
+affects: [23-classification-disposition-per-client-automation-gate, review-ui]
+
+# Tech tracking
+tech-stack:
+ added: []
+ patterns:
+ - "New verdict/action literal added to a component: extend the union type, the Record lookup table, and the ACTION_LABEL map together — TypeScript's exhaustive-Record-key check on Record is what catches a forgotten entry"
+
+key-files:
+ created: []
+ modified:
+ - components/phishing/classification-card.tsx
+ - components/phishing/action-area-card.tsx
+ - components/phishing/timeline-card.tsx
+
+key-decisions:
+ - "Used emerald (bg-emerald-500/15 text-emerald-600 in ClassificationCard; bg-emerald-500 text-emerald-600 dot+text format in TimelineCard) for USER_AWARENESS — distinct from UNWANTED's amber and THREAT's destructive red, signalling a positive/benign disposition (D-01 discretion, plan's own suggestion)"
+ - "acknowledge_user renders as a normal manual checkbox+Approve action in ActionAreaCard with zero automation-gate/per-company logic — the auto-approval carve-out lives only in the webhook path (Plan 05), confirmed via grep for automation_gate/auto_report/auto_parse/auto_classify returning no matches"
+ - "TimelineCard's VERDICT_TINT lookup was made defensive with a fallback (?? / || 'bg-muted-foreground text-muted-foreground') on both .split(' ') call sites, because the underlying phishing-timeline.ts service emits verdict as an unvalidated runtime string — any future/unrecognized verdict can never crash the review page again (T-23-12 mitigation)"
+
+patterns-established:
+ - "Verdict-keyed Record lookups (VERDICT_VARIANT_CLASS, VERDICT_TINT) must add a defensive fallback wherever the source data crossing the type boundary is a validated-at-the-type-level-only runtime string"
+
+requirements-completed: [CLASSDISP-03]
+
+# Metrics
+duration: 8min
+completed: 2026-07-16
+---
+
+# Phase 23 Plan 02: Review UI USER_AWARENESS + acknowledge_user Support Summary
+
+**Extended ClassificationCard, ActionAreaCard, and TimelineCard with a distinct emerald USER_AWARENESS verdict, an acknowledge_user manual action, and a defensive VERDICT_TINT fallback that prevents the review page from crashing on the new verdict.**
+
+## Performance
+
+- **Duration:** 8 min
+- **Started:** 2026-07-16T23:28:00Z
+- **Completed:** 2026-07-16T23:36:22Z
+- **Tasks:** 3 completed
+- **Files modified:** 3
+
+## Accomplishments
+- ClassificationCard now renders `USER_AWARENESS` with a distinct emerald badge (`bg-emerald-500/15 text-emerald-600`) and labels `acknowledge_user` as "Acknowledge user"
+- ActionAreaCard renders `acknowledge_user` as a normal recommended-action checkbox + Approve flow (no-params form), with zero gate-check/automation-gate logic added to the manual review UI
+- TimelineCard renders a `USER_AWARENESS` classification or `campaign_classified` audit entry with a matching emerald tint, and both `.split(' ')` tint-derivation sites now fall back to a neutral muted-foreground tint instead of throwing on any unrecognized runtime verdict string
+
+## Task Commits
+
+Each task was committed atomically:
+
+1. **Task 1: Add USER_AWARENESS badge variant + acknowledge_user label to ClassificationCard** - `af8a5bd` (feat)
+2. **Task 2: Add acknowledge_user manual action case to ActionAreaCard** - `547372e` (feat)
+3. **Task 3: Add USER_AWARENESS support to TimelineCard (prevents review-page crash)** - `9ae834f` (feat)
+
+**Plan metadata:** committed with this SUMMARY.md (worktree mode — orchestrator merges and records final metadata commit)
+
+## Files Created/Modified
+- `components/phishing/classification-card.tsx` - Added USER_AWARENESS to verdict union + VERDICT_VARIANT_CLASS (emerald); added acknowledge_user to ACTION_LABEL
+- `components/phishing/action-area-card.tsx` - Added acknowledge_user to ACTION_LABEL; added `case 'acknowledge_user'` to ActionParamsForm (no-params, mirrors no_action)
+- `components/phishing/timeline-card.tsx` - Added USER_AWARENESS to classification-entry verdict union + VERDICT_TINT (emerald); widened campaign_classified audit-case cast; added defensive `?? '...' ` / `|| '...'` fallback on both `.split(' ')` tint sites
+
+## Decisions Made
+- Emerald was chosen for USER_AWARENESS in both cards to keep a consistent visual language for the new disposition across the review page (matches the plan's own suggested color).
+- No architectural changes were needed — all three tasks were additive Record-entry/union-literal extensions plus one defensive-programming fix (TimelineCard fallback), all within Rule 1/Rule 2 deviation bounds implied by the plan itself (the plan explicitly calls out the crash risk and fallback as part of the task, not a deviation).
+
+## Deviations from Plan
+
+None - plan executed exactly as written. The TimelineCard defensive fallback was explicitly specified in the plan's task 3 action text, not an executor-discovered deviation.
+
+## Issues Encountered
+
+None. `npx tsc --noEmit --pretty` passed with zero errors after each task, and all specified `grep` acceptance criteria (USER_AWARENESS presence, acknowledge_user label, absence of gate-related terms in action-area-card.tsx, presence of the fallback pattern) were verified directly.
+
+## User Setup Required
+
+None - no external service configuration required.
+
+## Next Phase Readiness
+- The review UI can now render every verdict and action introduced by Plan 01's classification/action schema without risk of a runtime crash.
+- Plan 05 (webhook auto-approval carve-out) can proceed independently — this plan intentionally added no automation-gate logic to the manual UI, per D-04.
+- No blockers for downstream plans in this phase.
+
+---
+*Phase: 23-classification-disposition-per-client-automation-gate*
+*Completed: 2026-07-16*
From 50d0def2aeeadb68df867be867b204f8b098c066 Mon Sep 17 00:00:00 2001
From: lorentz
Date: Thu, 16 Jul 2026 19:37:07 -0400
Subject: [PATCH 5/5] docs(23-02): append self-check result to SUMMARY
Co-Authored-By: Claude Sonnet 5
Claude-Session: https://claude.ai/code/session_01W6RuWdiUiXrPK6FLBHjtpY
---
.../23-02-SUMMARY.md | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/.planning/phases/23-classification-disposition-per-client-automation-gate/23-02-SUMMARY.md b/.planning/phases/23-classification-disposition-per-client-automation-gate/23-02-SUMMARY.md
index e90c427..136cabc 100644
--- a/.planning/phases/23-classification-disposition-per-client-automation-gate/23-02-SUMMARY.md
+++ b/.planning/phases/23-classification-disposition-per-client-automation-gate/23-02-SUMMARY.md
@@ -98,3 +98,7 @@ None - no external service configuration required.
---
*Phase: 23-classification-disposition-per-client-automation-gate*
*Completed: 2026-07-16*
+
+## Self-Check: PASSED
+
+All created/modified files verified present on disk; all task commits (`af8a5bd`, `547372e`, `9ae834f`) and the SUMMARY commit (`44bbe90`) verified present in git log.