23-06-REVIEW.md found two real defects in the just-merged idempotency fix:
- CR-01: autoPostAcknowledgment's audit payload omitted actionId, which the
campaign-detail API requires to derive completedAt — every auto-posted
acknowledge_user row rendered a null completion date in the Action Area UI.
- CR-02: the manual approve/remediate path had no server-side guard against
re-approving acknowledge_user for a campaign that already got auto-posted —
only a client-side UI check prevented the exact duplicate-note bug 23-06
was chartered to close, reachable via a direct API call.
Fixes both: capture RETURNING id from the insert and include it in the audit
payload; add an existence check in approveRemediationActions that rejects
acknowledge_user when already posted for the campaign.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6RuWdiUiXrPK6FLBHjtpY
Replace the unguarded generateAndPostAcknowledgment(campaignId) direct
call in runGatedPhishingStages' auto_report branch with the idempotent
autoPostAcknowledgment(campaignId, 'system:auto_report'). Closes CR-01
(23-REVIEW.md) / Truth #18 (23-VERIFICATION.md): a repeat ticket-create
webhook joining an already-acknowledged USER_AWARENESS campaign now
finds the persisted remediation_actions row and skips the re-post
instead of re-sending the customer-visible thank-you note on every
additional report.
- New autoPostAcknowledgment(campaignId, actor) mirrors the VERIFIED
remediateApprovedActions shape: campaign row lock, idempotency check
against an existing acknowledge_user remediation_actions row, insert +
audit inside one transaction, note post after commit (non-fatal on
failure)
- Closes CR-01/WR-01: gives the auto_report webhook path a persisted
record so a repeat ticket-create webhook joining an already-acked
campaign does not re-insert/re-audit/re-post
- Adds Test A/B/C in remediation-service.test.ts covering first-pass
insert, idempotent skip, and non-fatal note-post failure
- Adds 23-04-SUMMARY.md documenting Task 1 (page) and Task 2 (index tile)
- Records that Task 1's human-verify checkpoint was approved via a
temporary production container rebuild rather than local npm run dev
- Company table with search + type filter (cloned from client-scope pattern)
- Three independent Switch toggles per row: auto-parse, auto-classify, auto-report
- toggle() PATCHes /api/admin/phishing-automation/{companyId} with all three current flags
- Helper caption clarifies stage dependency (informational, not enforced)
- triggerPhishingDetection now captures groupReportIntoCampaign's result and,
when a campaignId exists, calls new runGatedPhishingStages
- runGatedPhishingStages reads the per-company automation gate and
conditionally runs parseAndStoreMessage, classifyCampaign, and (only for
USER_AWARENESS verdicts) generateAndPostAcknowledgment
- each stage isolated in its own try/catch (T-23-09); detection + grouping
remain unconditional (D-07); auto_report never posts any other action
(D-04, T-23-08)
- remediateApprovedActions now captures the transaction's RemediateResult,
then post-commit checks whether an approved acknowledge_user row was
transitioned this pass (alreadyCompleted === false) and, if so, calls
generateAndPostAcknowledgment(campaignId) exactly once
- Call happens outside the DB transaction (network I/O hazard) and is
wrapped in its own try/catch that logs and swallows failures -- the DB
transition has already committed
- Every other action type (block_sender, purge_message, warn_user,
reset_password, isolate_endpoint, disable_forwarding_rule, quarantine)
remains a simulated status-only transition, unchanged
- Updated top-of-file D-01 doc comment to record the narrow D-04 carve-out
- Tests: acknowledge_user IS posted once when remediated, NOT called for
block_sender/warn_user-only remediation, NOT called on idempotent re-run
of an already-completed acknowledge_user row, and a post rejection does
not propagate out of remediateApprovedActions
- New generateAndPostAcknowledgment(campaignId) posts a short, appreciative
thank-you note to every ticket linked to a campaign, using noteType 18
(Client Portal Note, verified live against tenant's TicketNotes field
metadata) so the note is customer-visible; publish stays 1 unchanged
- Body is a fixed template with zero evidence/URL/classification
interpolation (T-23-01) -- not the evidence-dump formatTriageNote() template
- Mirrors generateAndPostTriageNote's per-ticket try/catch-in-loop error
isolation and { noteText, tickets } return shape
- Tests: noteType 18 + publish 1 payload assertion, per-ticket failure
isolation, and zero-linked-reports case
- GET /api/admin/phishing-automation: admin-gated list with COALESCE(...,false) gate defaults
- PATCH /api/admin/phishing-automation/[companyId]: upserts all three flags, actor+timestamp stamped
- DELETE /api/admin/phishing-automation/[companyId]: reverts company to all-OFF default
- Mirrors app/api/admin/company-scope/* route pattern
- Add USER_AWARENESS to the Verdict union in campaign-classifier.ts
- mapVerdictToActions('USER_AWARENESS') returns ['acknowledge_user']; not added to DESTRUCTIVE_ACTIONS so requires_approval computes false
- classifyCampaign's simulation branch now assigns verdict = 'USER_AWARENESS' directly instead of falling through to evaluateSpamVsUnwanted
- deriveDefaultParams('acknowledge_user') returns {} (no operator-editable params)
- Widen TriageNoteEvidence.verdict to admit 'USER_AWARENESS' (pure type widen, no formatting change)
- Tests: classifier simulation fixtures now assert USER_AWARENESS/acknowledge_user/requiresApproval=false; new mapVerdictToActions/computeRequiresApproval/deriveDefaultParams cases
- 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
- 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)
- 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
Checker warning: creating a new '## v3 Requirements' top-level section would
misleadingly read as deferred/future work (that's what '## v2 Requirements'
means in this file). Phase 23 is active v3.0 roadmap work -- instruct adding
a subsection under the existing '## v1 Requirements' heading instead,
mirroring Phase 22's existing subsection.
Verified against TicketNotes/entityInformation/fields on the real tenant:
publish has no client-facing value (1=All Autotask Users, 2=Internal Project
Team, 4=Internal & Co-Managed -- all internal-staff tiers). Client Portal
visibility is controlled by noteType=18 ("Client Portal Note"), not publish.
Corrects D-03's original "flip the publish flag" framing before planning.