Checker flagged 4 distinct font weights in use together (400/500/600/700)
against a 2-weight max. Consolidate to 400 (Body+Label, differentiated by
size/color) and 700 (Heading+Display, differentiated by size only) —
requires an explicit font-bold override on CardTitle since shadcn's
default is font-semibold. Also addresses two non-blocking flags:
"Reclassify" -> "Reclassify ticket", and aria-label on icon-only Copy
buttons (URL/hash).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6RuWdiUiXrPK6FLBHjtpY
Composes existing Pulse design tokens into a phase-specific contract:
page layout for the ticket-scoped review page and new campaigns list
page, evidence/timeline/classification display specs, and the
approve/remediate/mark-false-positive action-area contract (D-03..D-06
disabled-state and confirmation copy).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6RuWdiUiXrPK6FLBHjtpY
Structural twin of the classify route: requirePermission('phishing',
'analyze') gate, UUID guard, campaign-exists 404 check, delegates to
generateAndPostTriageNote and returns its result verbatim (note text +
per-ticket posted/error status, D-06). No audit-event write — deferred
per 21-CONTEXT.md.
GREEN: generateAndPostTriageNote(campaignId) gathers linked reports,
most-recent classification (NUMERIC confidence coerced to a JS number),
current remediation_actions, and real url indicators via the
reports->messages->indicators join; renders the sanitized note via Plan
01's formatTriageNote, then posts one internal TicketNotes write per
linked ticket with independent per-ticket error capture so a single
write failure never aborts the call (D-05) and note text is always
returned (D-06).
- formatTriageNote renders verdict/confidence/summary/reasons/blast-radius
(both branches)/recommended actions/current remediation state as prose
- Routes indicator URLs through sanitizeUrl and the whole assembled output
through sanitizeNoteText before returning
- Handles null verdict/confidence gracefully
- Exports TriageNoteEvidence interface for Plan 02
- All 9 formatter tests pass
- POST /approve: phishing:approve gated, validates actions array (D-03),
delegates to approveRemediationActions with actor from session
- POST /remediate: phishing:remediate gated, delegates to
remediateApprovedActions (idempotent completion, REMED-03/04)
- Both UUID-guard the campaign id and map RemediationValidationError->400,
RemediationConflictError->409
- Guards against marking false positive when any approved/completed
remediation exists for the campaign (RemediationConflictError, T-20-04)
- Sets campaigns.status='false_positive' and writes one atomic audit row
recording previousStatus + reason (REMED-05, REMED-06)
- Fixes test mock SQL substring match for the D-04 guard query
- Reworded a header comment to avoid a literal "not_implemented" string
that tripped the D-01 grep acceptance check
- approveRemediationActions validates each requested action against the
campaign's latest classification.recommended_actions and materializes
only recommended action types as status='approved' rows plus one
atomic audit row (REMED-01, REMED-02, REMED-06)
- remediateApprovedActions transitions approved rows to 'completed'
(D-01 simulated internal effect, no external provider call), is
idempotent via the status='approved' FOR UPDATE filter (REMED-04),
and fails explicitly on zero remediation_actions rows (REMED-03)
- RemediationValidationError / RemediationConflictError typed error classes
Note: markCampaignFalsePositive (referenced by the already-committed test
file) lands in the next commit (Task 3) — tsc will be clean again once
that lands.
- Single parameterized append-only INSERT into audit_events
- Supports optional injected transaction client for atomic writes
- Documents the four canonical event_type strings for this phase
- requirePermission('phishing','analyze') early-return (same action as /analyze, Phase 18 D-06)
- UUID_RE guard on campaign id before any DB query (T-19-05)
- 404 when campaign id is well-formed but not found
- delegates to classifyCampaign(id) from lib/services/campaign-classifier.ts (Plan 01), returns flat ClassifyResult payload