- getBlastRadius(input, options?) accepts an optional injected MimecastClient
and cacheScope; an injected client bypasses the global isMimecastConfigured()
gate since it carries its own credentials
- cache key namespaced by cacheScope to prevent cross-tenant collisions
- deliveredResult.error (previously swallowed) now rethrown so the outer
catch converts it to status: unavailable / reason: lookup_failed --
defense-in-depth against Bug 1 (future end-date rejected by Mimecast)
- test mock hygiene: getMimecastClientMock now cleared in beforeEach
- Fake tenant client via options.client bypasses getMimecastClient
- Injected tenant client runs fan-out even when global env unconfigured
- searchDeliveredMessages error field now expected to degrade to unavailable/lookup_failed
All 6 plans merged: pure-logic extraction (resolver/default-params/timeline),
API read surface, evidence/classification/timeline cards, action-area card,
and the ticket-scoped review page + campaigns list + nav entry. Blocking
LiveLink checkpoint verified against a real Autotask ticket (numeric ticket
ID confirmed, no ticket_number fallback needed).
Task 3 (blocking human-verify) resolved as "verified": a real production
Autotask LiveLink click against ticket 699340 confirmed the numeric-
ticket-id resolver assumption, no ticket_number fallback needed. Marks
REVIEW-01, REVIEW-05, REVIEW-06 complete in REQUIREMENTS.md.
- app/phishing/page.tsx: minimal DataTable-backed campaigns list, row click
navigates to /phishing/tickets/{firstReportTicketId}, EmptyState when no
campaigns exist yet
- components/navigation/app-navigation.tsx: add flat "Phishing" nav item
(ShieldAlert icon) immediately after PAX8, visible to all roles (every
role has phishing:read)
- app/phishing/tickets/[ticketId]/page.tsx: resolves ticket->campaign via
the plan-02 resolver route, drives a loading/not-triaged/ungrouped/ready/
error state machine, branches ready into grouped-but-unclassified
(Classify CTA, no ClassificationCard/ActionAreaCard) vs. classified (all
four cards with explicit props), refetches after every action (D-04),
session-only auth (no token/query-param scheme)
- app/api/phishing/reports/[report_id]/route.ts (new, additive): thin
report-scoped evidence + fresh blast-radius lookup for the D-08
ungrouped-report state, which has no campaignId to key the existing
campaign-detail route on — added as a Rule 2 dependency since the plan's
own D-08 truth ("standalone-report notice + evidence") has no other data
source
- Alias campaigns table as c, add correlated subquery for the earliest
linked report's ticket_id so the list page can navigate a row click
straight to /phishing/tickets/{firstReportTicketId}
- Additive only: count query, limit/offset, requirePermission gate, and
the { items, total, limit, offset } envelope all unchanged
- Add GatedButton: buttons stay in the DOM (D-05) always, wrapped in a
Tooltip explanation when disabled
- Gate all three actions with hasPermission(role, 'phishing', 'approve'|
'remediate') from lib/permissions.ts — the identical check the server
routes enforce, never a bespoke role === 'admin' string check (REVIEW-06)
- Derive resolved = campaignStatus === 'false_positive' OR any completed
remediation_actions row; resolved-state tooltip mirrors the completed
action's approver/date or the campaign's updated_at
- Remediate: AlertDialog confirmation listing approved action count/types,
then POST /remediate (no body), refetch on success
- Mark as false positive: AlertDialog with optional reason Textarea, then
POST /mark-false-positive { reason? }; disabled reason mirrors the
server's 409 guard exactly (approved/completed remediation blocks it)
- New GET /api/phishing/tickets/{ticket_id}/campaign wraps
resolveTicketToCampaign() from plan 22-01
- requirePermission('phishing','read') gate, Number.isFinite param
validation, D-07: found:false at 200 (not 404) for untriaged tickets
- Create ActionAreaCard with a null-guard on classification (renders an
informational note, never dereferences recommendedActions, for the
default grouped-but-unclassified state)
- Render one checkbox row per recommended action with an always-visible
params form pre-filled via deriveDefaultParams(actionType, evidence)
- Submit exact ApproveActionInput[] to POST /approve; purge_message
mailboxes is edited as a comma-separated string and normalized to
string[] at submit time
- Refetch via onActionComplete() on success (D-04, no optimistic mutation)
- Add 22-04-SUMMARY.md documenting the two components delivered
- Log pre-existing unrelated itglue-search.test.ts failures to
deferred-items.md (out of scope for this plan)
- Mark REVIEW-02, REVIEW-04 complete in REQUIREMENTS.md
- components/phishing/evidence-card.tsx renders Headers, URLs,
Attachments, Body preview, and Blast Radius tabs for a selected
message
- Body preview renders inside a <pre className="whitespace-pre-wrap">
as plain JSX text only, never via a raw-HTML injection prop
- URLs tab delegates to UrlList (D-09 inert copy-only)
- Blast radius renders explicit unavailable-state copy or a
matched/delivered/held/rejected/clicked stat row + per-recipient
table when ok
- CardTitle explicitly overridden with font-bold per UI-SPEC typography
- Merges reports/classifications/audit-events into one ascending list
(relies on server ordering, no client-side sort)
- Per-kind icon/label/tint: FileText for reports, Sparkles tinted by
verdict for classifications, event_type table for audit rows
(remediation_approved/completed, campaign_marked_false_positive,
campaign_classified, humanized fallback for anything else)
- 8px rail dot + border-l connector per UI-SPEC Timeline Spec
- Discriminated union TimelineEntry with report/classification/audit variants
- Ascending sort by createdAt with stable report<classification<audit tie-break
- Covers ascending chronological sort across reports/classifications/audit
- Asserts discriminant kind + source fields per variant, plus stable tie-break order
- Pure switch over no_action/warn_user/block_sender/purge_message/
reset_password/isolate_endpoint/disable_forwarding_rule
- Unknown/future action types fall through to {} rather than throwing
- Renders latest classification verdict/confidence/summary/reasons
- Recommended-action chips (informational, no checkboxes)
- Requires-approval warning Alert when requiresApproval is true
- Reclassify button gated on hasPermission(role, 'phishing', 'analyze')
- Returns null when classification is missing (empty-state handled by plan 06)
- npx shadcn add tooltip generates components/ui/tooltip.tsx (official
registry, no npm dependency added)
- components/phishing/url-list.tsx renders extracted URLs as inert
<code> text with copy-to-clipboard only — no <a>/href, no <Link>,
no navigating onClick per D-09
- Pure lookup: reports row for a ticket id -> found/reportId/campaignId/ticketNumber
- Parameterized query only (WHERE ticket_id = $1), no requirePermission/NextResponse
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