feat(post-audit): Phase 4 polish + Phase 2 wiring + Phase 6 cron + CLAUDE.md

Three of the master plan's "suggested execution order" items shipped this
session; Phase 3b (EOI dialog overrides) deferred — estimate exceeded the
remaining session time.

- Phase 4 polish: yachtId field on <ReminderForm> via the existing
  YachtPicker, Ship-icon subtitle on <ReminderCard>, listReminders filter
  by yachtId, getReminder joins the yacht relation.
- Phase 2 risk-signal data wiring: getInterestById derives the 3 dates
  (dateDocumentDeclined / dateReservationCancelled / dateBerthSoldToOther)
  from document_events / berth_reservations / cross-interest interest_berths
  in parallel — chosen over new schema columns to keep the master plan's
  "no new tables" promise. Threaded through to DealPulseChip.
- Phase 6 cron + UI: src/jobs/processors/imap-bounce-poller.ts polls the
  configured IMAP mailbox (IMAP_* env), matches NDRs to recent
  document_sends rows via recipient + 7-day window, idempotent via
  bounceDetectedAt, fires email_bounced notifications on hard/soft
  (skips OOO). State persisted to system_settings.bounce_poller_state.
  Wired into maintenance queue at */15 * * * *. Admin /admin/sends page
  surfaces the bounce badge + reason inline.
- CLAUDE.md: trimmed 27KB → ~19.5KB (~28% smaller bytes). Prose-heavy
  Documenso webhook / v1-v2 routing / Document folders sections rewritten
  as scannable bullets. Added a new "Working in this repo — skills, MCPs,
  agents" section promoting brainstorming/TDD/debugging/frontend-design
  skills, Context7/Playwright/Serena MCPs, and the Explore/feature-dev
  agents. Documented Phase 2 derivation choice in the data-model section.

Quality gates: 1374/1374 vitest pass, tsc --noEmit clean, lint 0 errors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-18 15:38:37 +02:00
parent a6e79231f3
commit 503207ef68
13 changed files with 561 additions and 123 deletions

View File

@@ -26,6 +26,11 @@ interface SendRow {
brochureId: string | null;
clientId: string | null;
interestId: string | null;
/** Phase 6 — populated by the IMAP bounce poller when a delivery
* failure for this send was matched in the configured mailbox. */
bounceStatus: 'hard' | 'soft' | 'ooo' | null;
bounceReason: string | null;
bounceDetectedAt: string | null;
}
interface ListResponse {
@@ -117,6 +122,21 @@ export function SendsLog() {
Switched to download link
</Badge>
) : null}
{row.bounceStatus ? (
<Badge
className={
row.bounceStatus === 'ooo'
? 'bg-slate-100 text-slate-800'
: 'bg-rose-100 text-rose-800'
}
>
{row.bounceStatus === 'hard'
? 'Hard bounce'
: row.bounceStatus === 'soft'
? 'Soft bounce'
: 'Out of office'}
</Badge>
) : null}
<span
className="text-xs text-muted-foreground"
title={sent.toISOString()}
@@ -143,6 +163,23 @@ export function SendsLog() {
Attachment dropped sent as link. Reason: {row.fallbackToLinkReason}
</div>
) : null}
{row.bounceStatus && row.bounceReason ? (
<div
className={`mt-2 text-sm rounded-md p-2 ${
row.bounceStatus === 'ooo'
? 'text-slate-700 bg-slate-50'
: 'text-rose-700 bg-rose-50'
}`}
>
Bounced
{row.bounceDetectedAt
? ` ${formatDistanceToNow(new Date(row.bounceDetectedAt), {
addSuffix: true,
})}`
: ''}
: {row.bounceReason}
</div>
) : null}
</div>
{row.bodyMarkdown ? (
<Button