Documenso reliability + signer-UX bundle from the 2026-05-26 live UAT.
Each piece detailed in docs/superpowers/audits/active-uat.md with full
file:line + root cause + alternatives.
Webhook + poll convergence
- DocumensoRecipient (webhook payload type) gains rejectionReason +
declineReason. The DOCUMENT_REJECTED / DOCUMENT_DECLINED handler
coalesces them at the boundary so downstream code sees one stable
field. Empty/whitespace normalised to null.
- DocumensoDocument.recipients[] (normalized client output) gains
rejectionReason. normalizeDocument coalesces v2 + v1 field names the
same way so poller consumers see identical shape.
- handleDocumentRejected signature gains rejectionReason. Stored on
document_events.eventData, persisted in audit_logs metadata, quoted
inline in the in-CRM rep notification (truncated 120 chars; full
reason still on the audit row). New 'transfer' AuditAction added
alongside.
- signature-poll job now handles REJECTED / DECLINED. Previously only
SIGNED / COMPLETED / EXPIRED were reconciled, so a missed rejection
webhook (stale tunnel URL is the typical dev cause) left documents
stuck in 'sent' forever. The 5-min poll cycle now closes that gap —
webhook becomes an optimisation, not a correctness requirement.
placeFields rollback gap
- custom-document-upload.service moved the synchronous field-placement
map() INSIDE the same try/catch that wraps placeFields(). Previously
the map's throw bubbled past the catch-and-rollback block, leaving
Documenso with a live envelope + recipients but no fields, and the
CRM document row stuck in 'sent' with no signing UI for the signers.
Logger captures looked-up email + map keys on miss for diagnosis.
- Comment documents Documenso's by-email dedupe semantic so future
readers don't reintroduce the per-recipient-row map assumption.
UploadForSigningDialog recipient UX
- New RECIPIENT_ROLE_META + RecipientRoleBadge helpers. Placement-step
sidebar list rebuilt as a two-line layout (name + role badge / email
on its own line) so duplicate-named recipients are visually
distinguishable. FieldSidePanel dropdown SelectItem mirrors the same
stacked shape.
- "Recipient" label renamed to "Assign this field to" with an explainer
paragraph below.
SigningProgress copy-link parity
- Copy-link button now always renders for pending signers (disabled +
explainer tooltip when signingUrl not yet issued). Reps can copy
even when the URL hasn't been distributed via email yet.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bundles the prior autonomous-session output that was sitting unstaged:
- Em-dash sweep across src/ + tests/ (en-dash/em-dash to hyphen, ~2280 instances)
- country-flag-icons rollout (CountryFlag component, replaces emoji glyphs that
never rendered on Windows; lazy-loads the 3x2 SVG index as a single chunk
after the per-subpath dynamic-import approach silently failed in webpack)
- Admin IA Phase 1+2: 7-domain regroup, 41 to 38 pages, /admin/berths index,
redirects (ocr to ai, reports to dashboard, invitations to users),
docs/admin-ia-proposal.md
- Per-template email tester (registry + endpoint + UI on Email admin page)
- Cancel-document mode picker (delete-from-Documenso vs keep-for-audit)
- Dashboard PDF report: 25 widgets, SVG charts, date-range picker, 11 resolvers
- Customize-widgets per-region sortables at xl+ (charts/rails/feed); single
flat sortable below xl when the layout stacks; per-viewport saved orders
- Audit doc updates capturing each shipped item
- Lint fixes: react-compiler immutability in DonutChart (reduce instead of
let-reassign), set-state-in-effect disables in CountryFlag and
UploadForSigning preview-bytes effect, unused 'confirm' destructures in
interest contract + reservation tabs, unescaped apostrophe in test-template
card copy
Phase 3b — EOI dialog field overrides:
- New EoiOverridesInput shape (clientEmail / clientPhone / yachtName)
threaded through generate-and-sign validator + both pathways
(in-app pdf-lib fill, Documenso template generate).
- src/lib/services/eoi-overrides.service.ts applies side-effects in one
transaction: useOnlyForThisEoi writes documents.override_* and stops;
setAsDefault demotes the prior primary + promotes (existing contactId)
or inserts + promotes (fresh value); neither flag inserts a non-primary
client_contacts row for future dropdown reuse.
- Document override columns persisted post-insert, with a 1-minute
source_document_id backfill on freshly inserted contact rows.
- eoi-context route returns available.{emails, phones} so the dialog
can render combobox options.
- <OverridableContactField> in eoi-generate-dialog.tsx renders the
combobox + manual input + 2 checkboxes per field with mutually
exclusive intent semantics.
Phase 3c — yacht spawn from EOI dialog:
- YachtForm gains createExtras + onCreated callbacks; the EOI dialog
opens it as a nested Sheet pre-filled with the linked client as owner.
On save the new yacht is stamped source='eoi-generated' and the
interest is PATCHed with the new yachtId so the EOI context reflows.
Phase 3d — promote-to-primary + audit + [EOI] badge:
- POST /api/v1/clients/:id/contacts/:contactId/promote-to-primary
(transactional demote+promote via promoteContactToPrimary).
- src/lib/audit.ts AuditAction type adds eoi_field_override,
promote_to_primary, eoi_spawn_yacht (DB column is free-text).
- ContactsEditor surfaces an [EOI] badge on non-primary rows where
source='eoi-custom-input'.
Phase 4 — worker + TOD picker:
- processOverdueReminders refactored to UPDATE...RETURNING with a
fired_at IS NULL gate so parallel workers can't double-fire. Uses
the idx_reminders_due_unfired partial index from migration 0072.
- /settings gets a "Default reminder time" time-of-day picker; the
value lands in user_profiles.preferences.digestTimeOfDay (validated
HH:MM at the route). <ReminderForm> seeds its dueAt from this
preference via a React-Query me-prefs fetch.
Phase 6 hardening:
- IMAP bounce poller strips whitespace from IMAP_PASS so a copy-paste
of Google Workspace's 16-char App Password formatted as
"abcd efgh ijkl mnop" still authenticates. Workspace activation
procedure documented in MASTER-PLAN §Phase 6 (was previously written
to CLAUDE.md, which was bloat — moved to the plan).
Quality gates: 1374/1374 vitest, tsc clean, lint 0 errors.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
- A1: idempotency gate in handleDocumentCompleted (prevents duplicate files on Documenso retry)
- A3: LEFT JOIN port_id move to outer WHERE (uses idx_docs_signed_file_id)
- G-C5: contract_sent / contract_signed auto-advance triggers in sendDocument + handleDocumentCompleted
- 0-byte signed PDF guard before storage.put
- portId in outer catch + poll worker
- Sanitize storagePath/storageBucket in aggregated files API
- Audit log for handleDocumentCompleted file insert
- Replace em-dashes in aggregated group labels with colons
- G-I6: delete orphaned hub-counts route + getHubTabCounts service fn
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Auto-format all files modified during the documents-hub-split feature
branch that were not yet aligned with the project's Prettier config
(single quotes, semicolons, trailing commas).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>