From 9ae834f5e64003c8e0d78d7730e84da589579527 Mon Sep 17 00:00:00 2001 From: lorentz Date: Thu, 16 Jul 2026 19:36:11 -0400 Subject: [PATCH] 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,