# Manual-testing backlog — 2026-05-15 Source: live walkthrough of the CRM by Matt while testing the Documenso integration end-to-end on `port-amador`. Each item here was either noted mid-stream or surfaced during testing and is queued for a future focused pass. Items already shipped during the same session are listed in the "Reference: shipped this session" appendix for context. Format per item: **what / where / desired state / effort / notes**. Use as a punch list — work top-to-bottom or cherry-pick by area. --- ## 0 · Blocked — needs Matt's decision before pickup Two items can't be picked up until a design call lands. Surfaced here so they don't get buried under implementation chatter. ### 0.1 Reminders data model — lightweight columns vs richer `reminders` table - **Cross-reference:** §3.2 below. - **The choice:** Today every interest carries `reminderEnabled: boolean` + `reminderDays: integer` directly on the row. A separate `reminders` table already exists (see `operations.ts`) with richer fields — `title`, `note`, `priority`, `assignedTo`, `dueAt` (timestamp, not days-from-now), `snoozedUntil`, `googleCalendarEventId`, etc. - **Path A — extend the lightweight columns** on `interests`: migration adds `reminderNote`, `reminderTimeOfDay`, optionally `reminderPriority` and recurrence flags. Stays single-row, every interest gets exactly one reminder hook. - **Path B — push richer reminders into the `reminders` table**: leave `reminders_enabled`/`reminders_days` as the simple per-interest hook (one follow-up tick), use the rich table for everything else (dated tasks, assigned reminders to specific reps, recurring nudges, etc.). Already partially wired — `searchReminders` queries it, `RemindersInbox` likely renders from it. - **Why this is blocked:** path A is faster but creates a parallel data model for a thing that already has a richer home. Path B is the right shape but requires a UI for "create a task on this interest" that doesn't exist yet, and a clear answer to "what does the existing per-interest reminder do once the rich path exists?". - **What I need from Matt:** which path, and (if B) does the per-interest cadence stay or get retired? ### 0.2 Supplemental info form — CRM-hosted vs marketing-site - **Cross-reference:** §8.1 below. - **The unknown:** Clicking "Send supplemental info form" emails the client a one-time link. That link's `/supplemental/` route — does it resolve to a CRM-hosted page (works out of the box) or to the marketing site (which may not have the route deployed yet)? - **Why this is blocked:** every other answer downstream depends on it. If CRM-hosted, this just needs a UX polish pass (§8.2 dual-mode + padding). If marketing-site-hosted, the marketing repo needs the page shipped before any testing makes sense, AND we need to confirm the marketing repo's deploy story for non-Port-Nimara ports. - **What I need from Matt:** a green-light to spend ~15 minutes tracing the route end-to-end. He flagged this as "save for end of pass" so I haven't touched it; capturing here so it doesn't get forgotten. --- ## 1 · Interest detail — Overview tab ### 1.1 Interest timeline detail - **Where:** `src/components/interests/interest-timeline.tsx`, Activity tab on interest detail page. - **Current:** Shows only "Interest created" event. Every subsequent PATCH (berth linked, desired-dims updated, stage moved, qualification confirmed, etc.) is swallowed. - **Desired:** One row per audited mutation — labelled by entity/action — so reps see the full life of the deal without leaving the page. Don't have to show the raw before/after values, just the _fact_ of the change. - **Effort:** Medium. Audit-log rows exist server-side already (`audit_logs` for `entityType IN ('interest','interest_berths','interest_qualifications', …)`). Timeline query is filtering too aggressively or missing entity types. - **Files:** `interest-timeline.tsx`, possibly a service helper that builds the activity stream. ### 1.2 Assigned-to default + permission granularity - **Where:** New-interest creation flow + `AssignedToChip` on detail page. - **Current:** New interests default to "Unassigned"; any user with the right permission can be assigned to. - **Desired:** - Default the assignee to the creating user when that user has the "can-be-assigned-to-sales" permission. - Add a fine-grained per-user toggle so admins / directors / etc. who normally aren't sales-facing can be flipped on as assignable when they're standing in on a deal. - **Broader principle:** every permission should be tunable per-user, not only via role assignment. Already partially supported via `userPermissionOverrides` — make sure "is assignable to sales" surfaces in that override UI. - **Effort:** Medium. Permission-key addition, default-on-create branch in `interests.service.ts`, surface in the user-edit drawer's Permissions tab. - **Files:** `interests.service.ts`, `roles` / `users` admin pages, permissions schema. ### 1.X Interest Overview — inline-edit Email + Phone on Contact section - **Where:** "Contact" section of the OverviewTab in `src/components/interests/interest-tabs.tsx`. - **Current state:** Email + Phone surface as read-only echoes of the client's primary email/phone (`clientPrimaryEmail` / `clientPrimaryPhone` from `getInterestById`). To edit them today the rep has to jump to the client page. - **Desired:** Click-to-edit inline using the canonical client-page components — `InlineEditableField` for email, `` (with the country-flag picker + E.164 normalization) for phone. - **Wiring needed:** - `getInterestById` returns the `contactId` (`client_contacts.id`) for the primary email + primary phone row, plus the existing `value` and `valueE164`. - `PATCH /api/v1/clients/{clientId}/contacts/{contactId}` handler that updates value + valueE164 (phone) or value (email) and re-asserts the primary flag. - On save: invalidate every cache that surfaces this data — `clients/{id}`, `interests/{id}`, `interests/{id}/eoi-context`, and any list endpoint that materializes the email/phone in the row (e.g. client-list, search results, dedup-candidates panel). The "data on the interest record must reflect everywhere it's used" invariant is real here. - **Effort:** Small-medium (~45 min). Service-side return-shape change + new PATCH route + 2 inline-edit components + the cache-invalidation list. ### 1.3 Payments section: stage-aware - **Where:** OverviewTab. - **Status:** Partially shipped — Payments now hidden before reservation stage. - **Outstanding:** When hidden, should we show a stage-specific "what to do next" card instead? Currently the real estate is just empty until the milestone card takes over. Consider an explicit "Next step" card with shortcuts (Send EOI, Generate Reservation, Record deposit, etc.) per stage. ### 1.4 Quick "Log contact" button - **Where:** Add to Overview tab on **interest detail** and **client detail**. - **Current:** Reps have to navigate to the Contact Log tab and click Compose. - **Desired:** A small button (probably next to "Email / Call / WhatsApp" pills) that opens the compose drawer pre-populated. On the **client** variant, allow the rep to optionally attach the new contact log to one of the client's interests for better organization. - **Effort:** Small-medium. Just a button + open the existing compose dialog with a `defaultInterestId` prop. --- ## 2 · Contact-log compose ### 2.1 Convert modal → side drawer - **Where:** `src/components/contact-log/compose-dialog.tsx`. - **Current:** Centered modal. Cramped when adding a follow-up reminder + body. - **Desired:** Convert to `` like the CRM's other form surfaces. More room for the body, reminder, attachments, etc. Matches the Sheet vs Drawer doctrine in CLAUDE.md. - **Effort:** Small (component swap + a few class tweaks). ### 2.2 Contact-log bells & whistles - Voice memo upload + transcription (OpenAI Whisper or local). - Attach a follow-up to a specific interest (covered by 1.4 on the client variant). - Multi-attachment support for screenshots / docs received during the call. - "Outcome" picker (positive / neutral / negative / blocked) for funnel analytics. - Templated quick-snippets ("Left voicemail", "Scheduled walkthrough", etc.). - Editable-after-save with audit-log trail (currently contact logs are immutable per most CRM patterns — confirm desired behaviour). --- ## 3 · Reminders ### 3.1 Standardize across surfaces - **Where:** `InterestForm`, contact-log compose, anywhere else `reminderEnabled` + `reminderDays` are settable. - **Current:** Each surface has its own copy of the cadence picker (`ReminderDaysInput`). The component is shared but each surface configures it independently. - **Desired:** A single per-port `reminder_presets` setting registry that drives the cadence chip options + the default selected cadence across every reminder surface. Admin sets the port's defaults once; every rep-facing surface inherits. - **Effort:** Medium. New registry entry, new `useReminderPresets()` hook, replace hardcoded `PRESETS` array in `ReminderDaysInput`. ### 3.2 Reminder customization (richer fields) - **Where:** `interests.reminderEnabled` + `reminderDays` columns. - **Current:** Toggle + integer days. No note, no priority, no time-of-day, no recurrence. - **Desired (optional schema migration):** - `reminderNote text` — what the reminder is about - `reminderTimeOfDay text` — HH:MM in port timezone - `reminderPriority text` — low/medium/high (mirrors `reminders` table) - `reminderRecurring boolean` + `reminderRecurringDays integer` - **Alternative:** Keep the lightweight model on `interests` and push richer reminders into the existing `reminders` table (already has title/note/priority/ assignedTo/snoozedUntil — see `operations.ts`). - **Decision needed:** which model? Lightweight stays as-is and the rich table is for explicit dated tasks. **Pending Matt's call.** See also the per-EOI reminder control work in [[4.7]] which covers reminder cadence at the document + per-signer granularity. --- ## 4 · EOI generation ### 4.1 Full inline editing in the Generate-EOI drawer - **Where:** `src/components/documents/eoi-generate-dialog.tsx`. - **Status:** Partially shipped: - ✅ Dialog → Sheet conversion - ✅ Inline fix-it form for MISSING name/email/address (uses Input + CountryCombobox; persists via clients PATCH + addresses/contacts POST) - ✅ Inline-edit pencil for name/nationality/yacht-name already exists on `PreviewRow` - **Outstanding:** - **Email** — needs an inline-edit row that PATCHes the matching `client_contacts.value` row. Requires surfacing `contactId` in `eoi-context.ts` response (currently flat `primaryEmail` only) + a `PATCH /api/v1/clients/{id}/contacts/{contactId}` wrapper. - **Phone** — same, plus needs the `` component for formatting (don't show raw E.164 + flag as a plain text field). The data path mirrors email. - **Address** — multi-field (street/city/country). Needs `addressId` in the context payload, then an inline sub-form (3 inputs + CountryCombobox) that PATCHes `/addresses/{addressId}`. - **Yacht dimensions** — Length/Width/Draft should be editable inline with the same ft↔m auto-convert as `YachtForm`. Persists via PATCH /yachts/{id}. - **Effort:** Medium-high (~1-2h focused). Server-side change to enrich `eoi-context.ts` with row IDs, new PATCH wrappers for contacts, multi-field editor component for address. ### 4.2 EOI-scoped data overrides (don't touch the canonical record) - **Where:** EOI Generate drawer. - **Use case:** Sales wants to render an EOI with values **different from** the client/interest record without overwriting the canonical record. Example: the client's billing address is in London (primary on the record), but for this specific EOI they want to use a secondary Monaco address. Today, editing in the drawer PATCHes the canonical record — there's no way to "use this value for this EOI only". - **Desired:** A per-field "use this only for the EOI" toggle (default off). When on: - The field value goes into the generated EOI document - The canonical client/interest record stays untouched - The override value is persisted as a secondary record with a flag (`is_eoi_only: true` or similar), tagged with a note like _"Captured for EOI #{externalId} on {date}"_, so it's auditable + recoverable but doesn't leak into the sales-process surfaces (e.g. doesn't become a candidate for "primary address", doesn't show up in the dedup picker as the address of record). - **Data model implications:** - `client_addresses` and `client_contacts` already support `is_primary`. Could add an `is_eoi_only boolean` (or a more general `scope text` — `'primary' | 'eoi-only' | 'archived'`) to mark these rows. - The eoi-context resolver would need to know to prefer the EOI-only row over the primary when an active EOI override exists; behave normally otherwise. - Yacht / berth dim overrides could be modelled via a sibling `eoi_overrides` JSONB on the interest row, since yachts aren't keyed for multi-instance-per-scope. - **UX:** Each editable row in the drawer gets a small checkbox below the input: _"Use this only on this EOI (don't change the client record)"_. Default unchecked = the current behavior (PATCH record). Checked = persists as EOI-only scoped. - **Effort:** High. Schema change + resolver branch + UI toggle + audit-log story. Worth scoping carefully — easy to introduce subtle "which value won?" bugs in downstream surfaces. - **Open questions:** - Does the override apply only to this specific EOI document, or to ALL future EOIs for this interest? (Lean: this EOI only, by storing the document_id reference on the override row.) - When the rep reopens the Generate drawer after an EOI was previously sent with an override, do we show the original override values or fall back to the canonical record? (Lean: fall back to canonical; force the rep to re-tick if they want the override again.) - Are these overrides reusable for related docs (reservation, contract) or EOI-only? - **Companion: interest-level data overrides (broader scope).** Beyond the EOI-only override above, Matt wants a "set contact/address details for just this interest" toggle on the **interest record itself**. When flipped, the email/phone/address entered overrides what's on the client record for this interest only — and wherever that interest's contact data is shown elsewhere in the app (search results, dedup panel, EOI preview, contact log), the surfaced value is tagged with a small "interest-only" badge so reps understand they're seeing a deal-scoped override, not the client's canonical info. Same data-model shape as the EOI-only case but the `is_interest_only` flag would be keyed on `interest_id` instead of `document_id`. Shares the resolver-precedence model with the EOI flag — interest-only > client-primary at lookup time when the caller is in that interest's scope; client-primary everywhere else. ### 4.3 EOI Address field — composition control + overflow safety - **Where:** `formatAddress()` in `src/lib/services/documenso-payload.ts` + `src/lib/pdf/fill-eoi-form.ts`, EOI source PDF AcroForm `Address` field. - **Now shipping** (this session): the EOI Address field renders as `street, city, REGION, postal, COUNTRY` where REGION is the ISO-3166-2 suffix (e.g. `NY`) and COUNTRY is the alpha-2 ISO code (e.g. `US`). Inline fix-it form on the generate drawer now accepts street + city + region + postal + country. The standalone `Nationality` PDF field has been retired — the resident's country lives on the Address line. - **Still open / deferred:** - **Admin setting** per-port to choose which address pieces are included on the EOI Address line (e.g. allow ports that want to drop the subdivision because their clients are mostly EU where `XX-XX` codes aren't recognised). Backed by a `system_setting` like `eoi_address_components`: `['street','city','subdivision','postal','countryIso']`. Default to all five. - **Dynamic font sizing inside the AcroForm box** — pdf-lib supports `field.setFontSize(...)`; need to measure the rendered string width against the field's available width and step the size down (e.g. 11pt → 9pt → 8pt) until it fits. Currently the PDF's `Address` field has a fixed font size set in the template, so a too-long address line will truncate or overflow. - **Preview check on the generate drawer** that shows a warning when the projected line exceeds a known character threshold so the rep can shorten before signing (e.g. swap the formal street name for an abbreviation). - **Why:** Test cases like `108 Avenue du Trois Septembre, Cap d'Ail, , 98000, FR` already push the box; longer EU/Asian addresses overflow. - **Effort:** Medium. Admin setting is straightforward registry entry + resolver call. Font auto-fit needs measurement helper + PDF-pass change. ### 4.4 Bypass-with-warning button - **Status:** **Intentionally not shipped** — Matt mentioned it then accepted the reasoning (EOI's top legal paragraph requires the missing fields; bypass produces unsignable docs). Note here in case the requirement returns. - **If revisited:** Add a `skipValidation: true` flag on the generate endpoint, surface a confirm modal in the drawer with explicit "this EOI will have blank legal fields" warning. ### 4.5 Documenso recipients — CRM as source of truth for all 3 signers - **Where:** `buildDocumensoPayload` in `src/lib/services/documenso-payload.ts`, per-port settings in `src/lib/settings/registry.ts`. - **Status:** **Next up — actively being shipped.** Matt's call: changes to a signer (e.g. David Mizrahi leaves, replaced by someone else) should happen ONLY in our CRM admin settings — never in the Documenso template UI. - **Documenso v2 behaviour confirmed via OpenAPI + docs:** - Template recipients can be saved with **placeholder values** (e.g. `developer@placeholder.crm` · `Developer (placeholder)`) — Documenso lets you save the template that way. The "Use template" UI gate (and the `/api/v2/template/use` endpoint) requires every slot to have a valid email/name though, so placeholders must be present at use-time. - At `/api/v2/template/use` time, the `recipients` array maps slot `id` → `{email, name}`. **Provided values win for that document only — the template's stored values are untouched.** Slots omitted from the array fall through to the template's stored values. - **Current state of the code:** `buildDocumensoPayload` passes `email: '', name: ''` for developer/approver so template values win. That's the OPPOSITE of what Matt wants — and it relies on v2 silently treating `''` as fall-through, which the spec doesn't actually guarantee. - **Target state:** Resolve real email + name for all 3 slots from per-port CRM settings: - Client slot — already populated from the EOI context (no change). - Developer + Approver slot — resolve in this order: 1. If `documenso__user_id` is set → look up that user's `userProfiles.displayName` + primary email (joined from `user` table). 2. Otherwise → fall back to two new free-text registry settings `documenso__email` + `documenso__name` so ports without a linked CRM user can still pin a static value. - Pass these as real values in the `recipients` array. Template placeholders are then invisible in practice — they exist only to satisfy v2's UI gate. - **Setup Matt needs to do once per port** before shipping: - On the Documenso v2 template, add 3 recipient rows with placeholder values: - `client@placeholder.crm` · `CRM Client (placeholder)` · SIGNER - `developer@placeholder.crm` · `Developer (placeholder)` · SIGNER - `approver@placeholder.crm` · `Approver (placeholder)` · APPROVER - Enable signing order on the template, ordered Client → Developer → Approver. - Save template. Run "Sync from Documenso" in the CRM admin. - Fill in the per-port linked-CRM-user dropdowns (developer + approver). - **Effort:** ~30 min — registry entries + resolver helper that joins to `user`/`userProfiles` + `buildDocumensoPayload` change + tests. ### 4.6 EOI dimensions — unit toggle + per-field emission - **Where:** "Dimensions (L × W × D, ft)" preview row in `src/components/documents/eoi-generate-dialog.tsx` + `buildDocumensoPayload` / `fill-eoi-form` Length / Width / Draft formValues. - **Current state:** - The drawer's preview row hardcodes "ft" in the label and renders `[lengthFt, widthFt, draftFt]` joined with `×`. - `buildDocumensoPayload` and `fill-eoi-form` already emit `Length`, `Width`, `Draft` as **separate formValues** — so per-field send is good. - However the values passed are always in **feet** regardless of which unit the rep originally entered on the yacht. - **Desired:** - **Unit toggle** (ft / m) at the top of the dimensions row in the EOI drawer. - **Default** to whichever unit the rep entered on the yacht record. The yacht row stores both `lengthFt/widthFt/draftFt` AND `lengthM/widthM/draftM` — we need a way to know which was the "source of entry". Either: 1. Add a `dimensions_unit_source: 'ft' | 'm' | null` column on `yachts` that the YachtForm sets when the rep types into either input. Or 2. Heuristic: if `lengthM` is set but `lengthFt` is null → m; vice versa. (Brittle when both are saved.) - The selected unit's value flows into Length/Width/Draft formValues so the rendered EOI matches what the rep entered. - **Effort:** Small-medium (~45 min). Toggle component + dimension formatter + schema column (if going with option 1) + buildDocumensoPayload swap. ### 4.10b Document detail page — full refactor - **Where:** `/[portSlug]/documents/[id]` → `src/components/documents/document-detail.tsx` (~407 lines). - **Symptom:** The page has been there since pre-EOI work but never caught up to the polish we shipped on the EOI tab. Multiple problems compound: confusing "Watchers" section with no Add button, no way to send invitations from this page (only "Remind" buttons that are ambiguous), Linked Entity row shows a bare "Interest →" with no name, Activity panel always says "No events yet" even when the document has dozens of events in `document_events`. - **The full refactor — 6 deliverables (in priority order):** 1. **State-aware action button per signer**, matching the EOI tab's just-shipped pattern: - `invitedAt === null` → primary "Send invitation" button (paper-plane icon, fires the same `send-invitation` route the EOI tab uses, which now handles v2 distribute-or-self-heal). - `invitedAt !== null && status === 'pending'` → outline "Send reminder" button (bell icon). - `status === 'signed'` → no button, signed-when timestamp. - `status === 'declined'` → no button, rose tint card, "Declined on {date}" line. Surfaces the rejection. 2. **Visual parity with `` from the EOI tab.** Avatar circle with cleaned initials (strip `(was: ...)` / `(placeholder)` suffixes), status-icon overlay, color-tinted card per status (pending neutral, opened sky, signed emerald, declined rose), left accent stripe, activity timestamps inline. Promote the existing `` component into a shared element this page also uses — same data shape. 3. **Linked Entity row — clickable name + entity-typed label.** Resolve the polymorphic FK chain on `documents` (`interestId`, `clientId`, `yachtId`, `companyId`, `reservationId`) into a tile showing the entity TYPE + NAME with a `` to its detail page. e.g. "Interest — Matt Ciaccio (Berth A2)" linking to `/interests/`. Multiple linked entities show as a chip row. 4. **Watchers section — copy + Add UI.** - Heading subtitle: "Watchers get an in-app notification on every signing event (opened, signed, declined, completed)." - "Add watcher" combobox picking from CRM users in the port. - Existing watcher rows get a delete (×) button. - Backend: routes already exist (`/api/v1/documents/{id}/watchers` POST + DELETE). 5. **Activity panel — read from `document_events`.** - Reverse-chrono list of every event for this document: created, sent, viewed (by signer name + time), reminder sent (to whom + when), signed (by whom + when), rejected (by whom + when + reason if Documenso passed one), completed, voided, deleted. - Each row: small icon per event type, actor name + relative time + precise tooltip. Mirror the audit-log row pattern. - Empty state only renders when there are genuinely zero rows (not the current "default to empty even when rows exist" behaviour — confirm the read path on `getDocumentDetail` actually returns events). 6. **Cleanup the leaked `(was: )` suffix** on signer name displays — same `cleanSignerName` helper we shipped on SigningProgress applies here. Currently rows render `Matt Ciaccio (was: matt@letsbe.solutions)` which is the EMAIL_REDIRECT_TO redirect leaking through. - **Effort:** Medium-high (~4-6h). Most data is already in DB + routes; this is mostly a UI rebuild + activity-feed read-path fix - adopting the shared SigningProgress component. - **Wires nicely with §9.Y2** (the dev-mode EMAIL_REDIRECT_TO badge initiative) — this page is one of the surfaces that needs the per-row redirect badge when the var is on. ### 4.10 Delete EOI from history — UI on top of the shipped soft-delete backend - **Where:** EOI history list section of `interest-eoi-tab.tsx` (the cancelled / past-EOIs strip) + the document list page. - **Backend status:** ALREADY SHIPPED this session. `deleteDocument` was changed from hard-delete to soft-delete: sets `status='deleted'`, inserts a `documentEvents` row with `eventType='deleted'`, calls `documensoVoid` on the linked envelope (best-effort), and audit-logs the action with old/new status. Refuses to fire while signing is in-progress (`sent` / `partially_signed`) — rep has to cancel first. - **What's missing — frontend:** 1. **Per-row Delete button** on cancelled / expired / completed-with- no-file rows in the EOI history list. Confirms first with a concise modal: "Delete this EOI? The Documenso envelope will be voided and removed from upstream; the audit log keeps a record. This can't be undone." (Stronger copy than cancel because the surface implies permanence even though the docs row stays.) 2. **Filter the primary EOI list** to exclude `status='deleted'` rows so deleted entries don't clutter the timeline. 3. **Surface deleted rows under a "Deleted" filter chip** alongside the existing status chips so the rep can browse history. 4. **Server-side filter check** — `getDocumentsForInterest` / `listDocuments` need a `includeDeleted: boolean` knob (defaults to false). Without this the "Deleted" filter has nothing to query. 5. **Audit-log surface**: deleted docs show up in the interest's activity timeline as "Deleted by {user} on {date}" — confirm this is wired (`createAuditLog` is fired by the service; check the timeline component reads action='delete' on entityType='document'). - **Effort:** Small-medium (~45 min). Pure frontend + one server-side filter knob on the list endpoints. ### 4.12 v2 envelope title — debug why update doesn't stick in Documenso UI - **Where:** `src/lib/services/documenso-client.ts` → `documensoGenerateFromTemplate` v2 branch. The update path is wired: ``` POST /api/v2/envelope/update body { envelopeId, data: { title } } ``` Per Documenso v2 docs that's the correct shape. Title field accepts a string while envelope is in DRAFT (ours is, we update before distribute). - **Symptom:** Documenso's "Documents" list keeps rendering the template's underlying PDF filename (`Port Nimara-Berth-EOI-NDA_October2025_FINAL.pdf`) instead of our intended title (`Matt Ciaccio-EOI-NDA-A2`). Persists after multiple fresh generates with the corrected endpoint shape. - **Hypotheses (ordered most → least likely):** 1. **Documenso UI displays PDF filename even when envelope.title is set.** The list view's "Title" column may prefer the underlying PDF name as a fallback. To rule out: check the envelope detail view (`signatures.letsbe.solutions/t/.../documents/envelope_xxx`) — if the detail header shows `Matt Ciaccio-EOI-NDA-A2`, the API is working and only the list UI is misleading. 2. **Update call returns 200 with `{success: false}` silently.** Our `documensoFetch` only throws on non-2xx HTTP. The verification log line shows what the API actually persisted vs what we sent. `titleMatches: false` here would mean the update is being accepted-but-not-applied (likely a v2 schema validation that drops unknown / malformed fields without erroring). 3. **Field name mismatch.** Maybe v2 internally stores `data.name` not `data.title`, and the docs are stale. Could try `data.title` AND `data.name` in the same body and see if either takes. 4. **Template-bound titles.** v2 might enforce that envelopes created via `/template/use` inherit and lock the template's title — and `envelope/update` is for non-template envelopes (made via `/envelope/create` with a fresh PDF). Workaround in that case: rename the underlying PDF before uploading to the template, OR use `/envelope/create` instead of `/template/use` so we control the source PDF filename per-document. 5. **Auth header on update call.** Docs show `Authorization: api_xxx` but our `documensoFetch` always prefixes `Bearer`. The Bearer prefix works for `/template/use` and `/envelope/distribute`, so this is unlikely — but worth checking the response if hypothesis 2 looks accepted-but-coerced. - **Debug plan (pair w/ Matt):** 1. Tail Next.js dev server console (`pnpm dev` terminal). 2. Generate one fresh EOI from the Overview tab. 3. Capture the two new log lines: - `Documenso envelope title update — response` - `Documenso envelope title update — verification` 4. Decision tree: - `titleMatches: true` → hypothesis 1 wins. Open the envelope detail URL in Documenso, confirm title is right there, file a Documenso UI bug / decide if we care. - `titleMatches: false` → hypothesis 2/3. Inspect raw `updateResponse` body for any validation errors. Try the dual-field POST (`data: { title, name }`) as the next test. - No log lines printed → the update call isn't firing at all. Check the order of operations in the v2 branch + verify `desiredTitle` resolves to a non-empty string at runtime. ### 4.14 Deal pulse + sales process: missing signals, oscillation risk, Regenerate flow - **Where:** `computeDealHealth` in `src/lib/services/deal-health.ts`, pipeline-stage auto-advance (`advanceStageIfBehind` in `interests.service.ts`), EOI cancel flow in `documents.service.ts`, EOI tab UI. - **Three coupled changes from Matt's design question:** 1. **Add positive "EOI sent" signal to deal pulse.** Today `computeDealHealth` only has the NEGATIVE signal "EOI awaiting signature for >14d → -10" (lines 165-178). No `+X` for the moment the EOI is dispatched. Add: when `dateEoiSent` is set AND `eoiDocStatus` IN ('sent', 'partially_signed') AND the doc is NOT cancelled/rejected/deleted AND `<14d` since send → +10. The existing -10 trips automatically once we cross the 14d threshold. 2. **DECISION NEEDED — auto-advance pipelineStage on EOI generate.** Currently generating an EOI doesn't move the stage from `qualified` → `eoi`. Auto-advance via `advanceStageIfBehind` would give an additional +10 to the pulse via `stage_progress` AND make the kanban / pipeline view reflect what's actually happening. **Risk:** introduces stage oscillation if EOI is cancelled later (no auto-rollback). Mitigated by the Regenerate flow below — but only for the typo-fix path; full cancels would still leave the stage at `eoi` until the rep moves it manually. **Matt's call needed: ship the auto-advance or keep stage moves explicit?** 3. **Replace cancel+regenerate with a single "Regenerate" button.** UX win + oscillation defuser: - Single button on the active EOI card, next to "Cancel EOI". - **Pre-invite path** (`invitedAt === null` on every signer): silent in-place replace. Wraps cancel + generate-new in one transaction so `interest.eoiDocStatus`, `dateEoiSent`, deal-pulse score, and pipeline stage never dip between the two calls. The Documenso envelope is voided + a fresh one created; the UI just flashes the new EOI in place. - **Post-invite path** (anyone in the chain has been emailed): warning modal listing each signer's email + `invitedAt` timestamp + a required reason field ("Why are we regenerating? Logged for audit"). Then same in-transaction cancel+generate. - Both paths reopen the EOI generate drawer pre-filled with the current details first, so the rep can fix the wrong-data reason before the new envelope mints. - Captures the actual workflow — Matt's right that ~all cancels are "I made a typo, let me redo" rather than "kill this deal" (the latter goes through Cancel + don't regenerate). - **Effort:** Medium (~3-4h). New deal-pulse signal + decision-gated stage auto-advance hook in `generateAndSignViaDocumensoTemplate` + new POST `/api/v1/documents/{id}/regenerate` route that wraps cancel+generate in a transaction + reopen the EOI generate drawer with prefill + UI button placement. ### 4.15 Sales-process + deal-pulse trigger audit - **Why:** Matt called out that the automatic raising/lowering triggers for both the pipeline-stage advance flow AND the deal-pulse score aren't surfaced anywhere as a holistic list, and likely have gaps (e.g. the missing positive "EOI sent" signal in §4.14 above is one symptom). - **Audit scope — list every place that touches each automatic trigger:** - **Pipeline-stage auto-advance** (`advanceStageIfBehind` callers, plus any direct `changeInterestStage` calls fired by service code rather than the user). Map every call site to the trigger condition + the target stage. Cover: EOI signed-webhook flow, deposit-received auto-advance, contract-signed webhook, reservation-stamped, won/lost outcome flips, manual stage moves. - **Stage auto-rollback** (does any code path move a stage BACKWARD automatically — e.g. when an EOI is cancelled or rejected?). Likely the answer is "no" — confirm in the audit, decide whether that's correct or a gap. - **Deal-pulse signals** (`computeDealHealth` — every `signals.push` - the conditions guarding it). Document each: * `active_engagement` (+5 if any contact-log entries in last 7d) * `contact_recent` (+20 if dateLastContact <=7d) * `contact_warm` (+10 if <=14d) * `contact_stale` (-15 if >=30d) * `stage_progress` (+10/+20/+30, capped, per pipelineStage index) * `stuck_top_funnel` (-10 if firstDays >=30 + stage in enquiry/qualified) * `eoi_awaiting` (-10 if eoiSentDays >=14 + not signed) * `deposit_pending` (-10 if reservation signed >=21d + no deposit) * `contract_awaiting` (-10 if contract sent >=14d + not signed) - **Heat tooltip explainer** — verify the in-product copy matches the actual computation logic (any drift = confusing). - **Gaps to flag as candidates for a fix wave:** - Missing positive signals: EOI sent (§4.14 §1), deposit received, contract signed (the moments of progress should each contribute). - Missing negative signals: signer declined / EOI rejected, interest archived-and-unarchived cycle (zombie deals), reservation cancelled, deposit refunded, berth status change to sold-to-other. - No "signer engagement" pulse signals — even though Documenso fires `RECIPIENT_VIEWED` webhooks. A signer who opened but didn't sign in N days is a stalling-signal worth surfacing. - Stage auto-rollback policy — currently nothing rolls a stage back on EOI cancel; that may or may not be correct. - Reminder cadence on `eoi_awaiting` — currently a single -10 at 14d. Could escalate to -20 at 21d, -30 at 30d. - **Output:** A short audit doc (`docs/deal-pulse-trigger-audit.md`) listing every trigger + a punch-list of gaps to address in follow-up commits. Once that's in front of Matt, he picks which gaps to ship vs which to defer. - **Effort:** Small (audit + doc, ~1h). The fixes themselves are scoped per-gap once the audit is in. ### 4.13 EOI rejection — cascade emails + notifications + UI banner - **Where:** webhook handler `handleDocumentRejected` in `src/lib/services/documents.service.ts` (already wired end-to-end at the data layer) + new UI banner on the EOI card + new cascade-email service. - **What's already wired at the data layer (no change needed here):** - `DOCUMENT_REJECTED` / `DOCUMENT_DECLINED` webhook events are handled by `handleDocumentRejected`. It flips `documentSigners.status = 'declined'` for the rejecting recipient, `documents.status = 'rejected'`, `interests.eoiStatus = 'rejected'`, inserts a `documentEvents` row with `eventType: 'rejected'`, emits `document:rejected` over the socket bus. - **What's missing (what to ship):** 1. **Role-based cascade email** when a signer rejects. Logic per Matt: - **Client rejects** → email developer + approver. The deal is likely dead — internal team needs to know to stop work / close the interest. - **Developer rejects** → email **both** client and approver. Internal sign-off failed; both sides of the table need to know the deal stops here. - **Approver rejects** → email **developer only**. Final-stage internal escalation; client doesn't know yet so the developer can attempt to salvage (renegotiate terms, escalate further) before notifying the client. - Cascade fires inside `handleDocumentRejected` via a new `sendRejectionCascade(documentId, rejectingRole)` helper that reads the doc's signers + the rejecting role + dispatches via the existing `sendSigningInvitation`-adjacent path with a per-port branded "EOI rejected" template. 2. **In-app notification** to: - Interest assignee (always, regardless of who rejected) - The CRM users linked to the developer + approver slots (`documenso_developer_user_id` / `_approver_user_id` if set — fall back to the port admin list if unset). - Notification body: who rejected (signer name + role) + reason if Documenso captured one + deep-link to the EOI tab. 3. **UI banner on the EOI card** when `documents.status = 'rejected'`: - Distinct from the existing CANCELLED state (rose accent stripe on the card top edge + a single-line banner). - Reads: "Rejected by {signerName} ({role}) on {date}". - If Documenso passes a rejection reason, show it on a second line: "Reason: {reason}". - CTAs on the banner: "Reopen this interest (re-negotiate)" + "Archive interest (deal dead)" — both audit-logged. 4. **Per-signer card visual on the SigningProgress component** — the rose tint + X icon overlay already exists for `declined` (was shipped in the signing-progress redesign), so this just naturally surfaces once the webhook fires. No extra UI change. - **Cascade-email template content** needs admin-tunable copy via the registry (new section `email.rejection_templates`) — three keys: `rejection_email_to_internal_subject` / `rejection_email_to_internal_body` (used when developer or approver rejects → emails to client) and `rejection_email_internal_escalation_subject` / `rejection_email_internal_escalation_body` (approver-rejects → developer-only escalation). Default copy ships in the migration. - **Effort:** Medium-high (~3-4h). Service helper + 3-4 email templates + UI banner + notification entries + audit log + tests. ### 4.11 EOI real-time signing-progress tracking — verify wiring end-to-end - **Where:** Documenso webhook handler → `documentSigners.status` updates → realtime broadcast → `SigningProgress` re-renders. - **What's already there:** - Documenso webhook receiver at `/api/webhooks/documenso/route.ts` handles `RECIPIENT_VIEWED` / `RECIPIENT_SIGNED` / `DOCUMENT_SIGNED` / `DOCUMENT_COMPLETED` and updates the matching `document_signers` row (by token or email). - `SigningProgress` polls `/api/v1/documents/{id}/signers` every 30s (see `useQuery` refetchInterval). - **Gaps to verify (Matt to test):** - When a signer opens the doc → does `openedAt` get stamped + does the next poll surface the new "Opened" state (blue tint card + eye icon) within 30s? - When a signer signs → does the status flip to "Signed" (emerald card + checkmark) + the EOI card header counter "X of N signed" update on the same poll? - Socket-based push (not just poll) — `emitToRoom` is wired on doc events; check that the interest detail page subscribes to `port:{portId}` and invalidates the signers query on `document:signer_updated` so the UI updates within seconds rather than waiting for the 30s tick. - **If gaps exist**: implement socket-subscribe → query invalidate on the interest detail or EOI tab so the SigningProgress card updates in real time. - **Effort:** Verification (15 min Playwright) + small fix if missing (~30 min). ### 4.9a Embedded signing host — Test button + verified-at gate - **Architectural rule (Matt 2026-05-15):** all outbound signing-invite emails go through our branded `sendSigningInvitation` template. Documenso never fires its own emails for our envelopes (`meta.distributionMethod: 'NONE'` enforced at distribute time). The link inside our branded email points to either: - The wrapped marketing-site URL (`{embeddedSigningHost}/sign//`) when the host is configured AND **verified working**. - The raw Documenso signing URL otherwise. - **What's wired today:** `wrapBrandedSigningUrl` in `document-signing-emails.service.ts` checks `embeddedSigningHost !== null` and either wraps or passes through. No verification gate. - **What's missing:** the "verified" half. Typo or unreachable marketing host = broken email links + no warning to the rep. - **Ship:** 1. **Test connection button** on the admin `embedded_signing_host` field. Click → server-side fetches `{host}/sign/__probe__` (or an agreed sentinel path) with a short timeout, expects a known 200/404 shape. 2. On success, persist `embedded_signing_host_verified_at` in system_settings. 3. `wrapBrandedSigningUrl` checks `verifiedAt IS NOT NULL && verifiedAt within last 30d` before wrapping; otherwise falls back to raw Documenso URL even if the host is set. 4. Admin UI badge per state: green "Verified {date}", amber "Verified more than 30d ago", red "Not verified — links fall back to Documenso". 5. Saving a new host clears `verifiedAt` so the rep has to re-test after every change. - **Effort:** Medium (~1-2h). New POST endpoint + service helper + registry timestamp column + UI button + branded badge. ### 4.9b Embedded signing — admin help button with setup instructions - **Priority:** LOWEST. Don't touch until everything else in §4 is shipped. - **Why:** Right now the only way a port admin can stand up a NEW marketing site to host our Documenso embedded signing pages is by pinging us. Knowledge is tribal. Adding an in-product help surface makes it self-serve for future ports / contractors. - **What to ship:** - Small `?` / "Setup instructions" button next to the `embedded_signing_host` admin field. Click → opens a side Sheet (right slide-in) with the full how-to. - Content of the how-to: a step-by-step that covers everything a fresh marketing-site project needs to wire up the embed: 1. The `/sign/[type]/[token]` route (signature host page — iframe wrapper for the Documenso UI). 2. The runtime config the route reads (`useRuntimeConfig().public.documensoHost` or equivalent). 3. Env vars the marketing site needs (Documenso instance URL, any CSP / sandbox flags). 4. Post-sign redirect page (the URL Documenso sends signers to after they finish — our `documenso_redirect_url` setting must point at it). 5. DNS / Cloudflare config for the signing subdomain (`signatures.{port-domain}` typically). 6. How to verify end-to-end: generate an EOI, send invitation, open the email, click the wrapped URL, confirm the embed loads + signing flow completes. - **Source repos to analyse before writing the instructions** (so the doc reflects what actually works, not what we think works): - **THIS CRM repo** — `wrapBrandedSigningUrl` in `document-signing-emails.service.ts`, the `embedded_signing_host` + `documenso_redirect_url` registry entries, how the `/sign//` URL shape is generated. - **OLD CRM repo** — any legacy scripts or docs that already captured this integration (don't reinvent if there's prior art). - **Port Nimara website repo** (`/Users/matt/Repos/Port Nimara/Website`) — the actual `/sign/[type]/[token].vue` route that wraps the iframe, the runtime config it reads, env vars it expects. - **Effort:** Small-medium (~1-2h once we commit to doing it). Mostly documentation work + a help Sheet component. No new wiring. - **Pairs with §4.9a** (verified-at gate) — the help instructions should reference the Test button and the verified-at workflow. ### 4.9 Marketing-site embedded signing link - **Where:** `embedded_signing_host` setting + marketing-site `/sign/[type]/[token]` route. - **Current:** Dev tests use raw Documenso URLs (skipping the wrap layer). - **Desired for prod-style testing:** Patch the marketing site's hardcoded `documensoHost = 'https://signatures.portnimara.dev'` (line 142 of `/Users/matt/Repos/Port Nimara/Website/pages/sign/[type]/[token].vue`) to read from `useRuntimeConfig().public.documensoHost` so the iframe can point at the user's testing Documenso instance. - **Effort:** Small (marketing site repo). Out of scope of CRM repo but needed end-to-end. --- ## 5 · Berth recommender ### 5.1 "Add to interest" surfacing - **Where:** `BerthRecommenderPanel` rec cards. - **Current:** Add button only appears inside the expanded card body. Reps scrolling the list have to click each card to reveal it. - **Desired:** Quick-add button on the collapsed card row too (small icon button next to the score). Same `AddBerthToInterestDialog` opens. - **Effort:** Small (just an extra Button in the row header). ### 5.2 Add-berth dialog: in-EOI-bundle toggle - **Where:** `src/components/interests/add-berth-to-interest-dialog.tsx`. - **Current:** Radio between "Pitching specifically" (marks "Under Offer" on public map) and "Just exploring" (internal-only). - **Desired:** Third toggle for `isInEoiBundle` ("genuinely interested — include in the EOI's signed berth range"). Matt mentioned wanting "if they're genuinely interested" alongside the map-marking toggle. - **Effort:** Small. Service already accepts the field; add UI checkbox. --- ## 6 · Global search ### 6.1 Verify coverage gaps - **Status:** Search 500 (reminders bug) shipped. Archived clients confirmed hidden from search per Matt's choice. Search-by-email confirmed working (via `client_contacts` JOIN). - **Unverified — possible gaps:** - **Client address fragments** — `clients.address` JSONB isn't queried in `searchClients`. - **Yacht hull number / registration** — should be in `searchYachts`; needs a spot-check. - **Company tax ID / billing address** — same. - **Effort:** Small per field (ILIKE predicate in the existing SQL block). --- ## 7 · Layout / copy / UX cleanups (small) ### 7.1 Heat / deal-pulse explainer docs page - **Where:** `DealPulseChip` popover has a "Full guide" link to `/docs/deal-pulse`. - **Current:** Route doesn't exist yet. Click 404s. - **Desired:** Static doc page or markdown render explaining the rule-based score in plain English. - **Effort:** Small. ### 7.2 Stage guidance card - **Where:** Overview tab. - **Desired:** For each pipeline stage, a small "next step" card on the Overview that explains what the rep needs to do to move to the next stage, with shortcut buttons (Send EOI, Generate Reservation, Record deposit, etc.). Replaces the empty Payments slot at non-deposit stages. - **Effort:** Medium (stage-aware component + per-stage copy). --- ## 8 · Supplemental info request form ### 8.1 CRM-hosted vs marketing-site - **Status:** Matt asked whether the supplemental-info button on the EOI-not-ready card creates the form on the CRM or relies on the marketing site. Said **save for end of pass**. - **Action when revisited:** Trace `SupplementalInfoRequestButton` → the public form route → confirm whether it's CRM-hosted (good — works out of the box) or marketing-site-hosted (needs the website repo to ship the form). Fix gap if any. ### 8.2 Conditional render + dual-mode copy - **Where:** `SupplementalInfoRequestButton` card (the "Need more info before drafting the EOI?" card on the interest Overview). - **Current:** Always renders the same copy ("Need more info before drafting the EOI?"), regardless of whether the client actually has the required fields. - **Desired (two modes):** - **Missing-data mode** (current copy): when the interest's EOI context is failing the required-field check (name / email / address / yacht / berth not set), show the prompt as it currently reads — the rep should send the form to fill the gaps. - **Confirmation mode** (new): when everything required is already on file, swap to a softer prompt — "Send a one-time form to the client to **confirm** their info?" — for cases where the sales person wants the client to verify the data themselves before the EOI goes out. - **Plus:** Add padding above the header at the top of the card (currently the title sits flush against the parent edge). - **Effort:** Small. Conditional copy branch driven by the same `eoi-context` check the dialog already uses, plus a `pt-6` (or similar) on the CardHeader. --- ## 9 · Reference: shipped this session (for context) Settings / admin: - Env→admin migration registry + resolver + form - "Save N changes" bulk button on every registry-driven form - "Reveal" endpoint for encrypted settings (admin can verify what's saved) - Sync result panel persists across refresh (cached `documenso_eoi_template_sync_report`) - Template-level meta (signing order / distribution method / redirect URL) shown after Sync - EOI generation + EOI templates card migrated to registry-driven form Documenso v2 buildout: - `getTemplate(envelope_id|numeric_id)` + template sync endpoint - AcroForm inspection: downloads each envelope item's PDF, inspects native fields, diffs against CRM expected EOI label set - `prefillFields`-by-ID emission for v2 instances - `updateEnvelope` v2-only wrapper - CC / VIEWER recipient roles + `extraRecipients` - Health check now uses `/api/v2/document` for v2 (was `/api/v1/health` which doesn't exist on v2 cloud) - Sync misrouting bug fixed (recipient IDs were being written to user-ID keys) - User-select dropdown for the developer/approver linked-CRM-user fields - Default name/email placeholders ("David Mizrahi" / "Abbie May") removed; blank now passes `""` to Documenso which falls through to template's stored values Forms / UI: - Yacht dimensions auto-convert ft↔m (paired inputs, single Dimensions section) - Phone input width fixed (CountryCombobox `w-24 shrink-0`; PhoneInput wrapper `w-full`) - Primary contact per-channel (Primary email + Primary phone, etc.) - Yacht picker: dashed-border "Add yacht for this client" prompt when zero yachts - Lead Category + Stage auto-fill on berth pick (create mode, manual-touch wins) - Tags section hidden when port has zero tags - Multi-berth selector on InterestForm (first=primary, extras via `/berths` POST) - Dedup-suggestion panel shows archived state with Restore link - "Open" stage copy purged (replaced with "New Enquiry") - "no AI" mentions removed from DealPulse popover + heat tooltip copy Search / data: - Search 500 fixed (`searchReminders` calling `.toISOString()` on string) - All `archived_at.toISOString()` calls hardened across `search.service.ts` - `match-candidates` returns `archivedAt` so dedup panel can render archived state - New `/api/v1/admin/users/picker` for the user-select dropdown - Search archived behaviour decided: hide (matches Matt's call) Interest detail / pipeline: - Phase classification rewritten — Overview always surfaces a CURRENT milestone - Payments section hidden before reservation stage - DealPulseChip popover (click instead of hover, plain-language explainer, "Full guide" link placeholder) - EOI MilestoneSection footer with `Generate EOI` (opens drawer) + `Open EOI tab` (deep link) buttons - `EoiGenerateDialog` mounted at OverviewTab level (state was wired but component never rendered) - Recommendations tab swapped from legacy "AI" `RecommendationList` to the same rule-based `BerthRecommenderPanel` used on Overview - Recommendations empty-state "Show oversized matches too" button (raises `maxOversizePct` to 1000 so berths beyond strict tolerance surface) - `dimensions` qualification auto-satisfies on yacht-dims OR desired-berth-dims - Area letter dedup, In-EOI-bundle empty-header dedup, Berth Range tooltip spacing fix - Recommendations header shows entered unit (ft or m) - "Mark EOI as sent" → "Mark EOI as sent manually" (Documenso webhook auto-stamps for normal sends) EOI generate drawer (in flight): - Dialog → Sheet conversion - Inline fix-it form for missing name/email/address (uses canonical components, persists via PATCH/POST) - Real upstream error message surfaces ("Cannot generate EOI — missing X, Y, Z") instead of generic "preview failed" - Address inline fix-it now accepts **street, city, region, postal, country** (was just street + city + country); persists postalCode + subdivisionIso to the address row - Rendered EOI Address line format: `street, city, REGION, postal, COUNTRY-ISO` (e.g. `123 Sesame Street, Staten Island, NY, 10306, US`) — shortest comprehensive form to fit the AcroForm box - `Nationality` removed from the required preview rows + Section 2 helper copy; the resident's country code on the Address line carries that meaning now Interest Overview teaser: - "Latest note" no longer renders the raw `authorId` UUID — `getInterestById` now LEFT JOINs `userProfiles` so the teaser shows the author's display name (falls back to "Unknown" if the user row is missing) Documenso integration polish (later in this session): - **CRM-as-source-of-truth signer wiring** — `buildDocumensoPayload` now resolves developer + approver name/email per port via: linked CRM user (`documenso__user_id` → `userProfiles.displayName` - `user.email`) → free-text override (`documenso__email/name`) → legacy `eoi_signers` JSON blob → empty (template wins). Replaces the hardcoded "David Mizrahi" / "Abbie May" placeholders. - **EOI title format** — `-EOI-NDA-` (e.g. `Matt Ciaccio-EOI-NDA-A2`). Tested for single + multi-berth + no-berth. - **v2 title PATCH after `template/use`** — fixed broken endpoint shape: was `PATCH /api/v2/envelope/{id}` with `{title}`; correct is `POST /api/v2/envelope/update` with `{envelopeId, data: {title}}`. Restricted to DRAFT envelopes which is what we always have post-create. - **`document_signers` rows inserted on generate** — was missing; the EOI tab's "Signing progress" panel showed "No signers loaded" forever because the webhook handler only updates existing rows. Now the Documenso `recipients` array from `/template/use` is persisted at create time. - **Interest milestone stamping** — `eoiDocStatus='sent'` + `dateEoiSent` flip immediately on generate so the Overview tab transitions out of the "Generate EOI" prompt without waiting for the next refresh. - **Cache invalidation on generate** — the EOI dialog's onSuccess now invalidates `interests/{id}`, `interests/{id}/eoi-context`, `interests/{id}/timeline`, and the documents predicate so every surface that shows the new EOI state updates without a manual refresh. - **Dimension unit toggle in EOI drawer** — ft/m chip in the Optional section header; defaults to the rep's original entry unit (`yacht.lengthUnit`). Drives the preview-row display + the `Length`/`Width`/`Draft` formValues sent to Documenso / filled into the in-app PDF. - **EOI Address line format** — `street, city, REGION, postal, COUNTRY-ISO` (e.g. `123 Sesame Street, Staten Island, NY, 10306, US`). Inline fix-it form accepts all 5 fields. Nationality requirement gone — the resident's country code on the Address line carries that meaning now. - **Soft-delete documents** — `deleteDocument` now sets `status='deleted'` + voids the upstream envelope (best-effort) + inserts a `documentEvents` row + audit-logs the action. No more hard delete that destroyed event history. - **Signing-progress card redesign** — vertical card list (no connector line), per-status visual treatments (pending/opened/signed/declined), initials-aware avatar with status icon overlay, state-aware action button ("Send invitation" before `invitedAt`, "Send reminder" after), precise timestamps in tooltips, and a Sequential / Concurrent signing-order badge in the EOI card header pulled from the synced template meta with fallback to per-port setting. - **Phone-number formatting in interest Overview Contact section** — uses libphonenumber-js `formatInternational()` so raw E.164 (`+33633219796`) renders as `+33 6 33 21 97 96`. - **Interest detail header** — added "Client page" button next to the Email/Call/WhatsApp quick actions. - **Qualification "dimensions" criterion copy** — "We know the vessel's length, width, and draft" → "Vessel dimensions OR desired berth dimensions are recorded (length, width, draft)" to reflect the auto-satisfy rule. Migration `0067` applied to dev DB. --- ## 9.Y Notifications: auto-mark-as-read on dropdown view - **Where:** `NotificationItem` / notifications dropdown components in `src/components/notifications/`. - **Current:** Notifications stay unread until the user clicks each one or hits "Mark all read". - **Desired:** Auto-mark each notification that was actually rendered in the dropdown as read. Two flavours to pick from: - **(a) On display** — fire `markRead(id)` as each item renders. Pro: matches Slack/Linear pattern. Con: unread bubble drops to 0 the moment the dropdown opens, even before the user has time to scan. - **(b) On dropdown close** — batch-mark every currently-rendered ID when the user closes the dropdown. Pro: bubble + bold styling stay while the dropdown is open so the user can re-find unread items. - **Question for Matt:** Pick (a) or (b)? The skipped (un-rendered) notifications stay unread either way. - **Effort:** Small. New `POST /api/v1/notifications/mark-read-bulk` endpoint (or per-id PATCH × N), useEffect in NotificationItem (option a) or onOpenChange handler on the dropdown (option b). --- ## 9.X Same-stage move emits a confusing notification - **Where:** `changeInterestStage` in `src/lib/services/interests.service.ts` (notification block at ~line 944) and any other call site that emits `interest_stage_changed` (`advanceStageIfBehind`, webhook auto-stamps, Documenso completion handlers). - **Symptom:** Notification panel shows e.g. "Marco Bianchi moved to EOI / Stage changed from EOI to EOI" — same-stage transition surfaces in the inbox even though `changeInterestStage` returns `STAGE_NOOP` for the matching pipeline-stage case. - **Suspect:** A different code path (likely `advanceStageIfBehind` or one of the Documenso-webhook auto-advance helpers) emits a stage-changed notification without checking `oldStage !== newStage`. Or `STAGE_LABELS` has two raw stage codes that map to the same display label so the notification reads "EOI → EOI" when the real codes were different. - **Fix:** Find the emitting site, add an `oldStage === newStage` early-return before `createNotification`. Audit the same guard on every other `interest_stage_changed` emitter. Same-stage events stay invisible to reps (they're already filtered out of the audit log via STAGE_NOOP). --- ### 4.7 Per-EOI reminder controls (with per-signer fine-tune) - **Where:** EOI card footer in `interest-eoi-tab.tsx` ("Reminders are rate-limited (max once per 7 days per signer)" line) + the per-signer rows in `signing-progress.tsx`. - **Current:** Single global per-port `reminder_default_days` setting + doc-level `remindersDisabled` + `reminderCadenceOverride` columns (the schema already has them — see `documents.ts` lines 102-103). No UI surfaces them. - **Desired (tiered controls):** - **EOI-level**: inline toggle to disable reminders entirely for this EOI + an inline "Remind every X days" picker that overrides the port default just for this document. Lives in the EOI card footer. - **Per-signer fine-tune**: each signer card on the signing-progress list gets its own "Remind this signer every X days" override picker AND a per-signer toggle. **Reason from Matt: the developer is known to miss emails — they need a tighter cadence than the rest.** Persists on a new JSONB column `reminder_overrides` on `document_signers` (or `documents.reminder_overrides_by_signer_id` keyed by signer id — either works; signer-level is more discoverable in the UI but locks the schema). - **Resolution chain at reminder-send time** (existing `sendReminderIfAllowed` worker): per-signer override → per-document override → per-port default → null (no auto-reminders). - **Surface the active value on each card**: small italic line under the signer row like "Auto-reminds every 3 days · last reminded 2 days ago" so the rep knows what's happening without digging. - **Effort:** Medium (~1-2h). Backend: new JSONB column + resolver branch in the reminder worker. Frontend: inline pickers on EOI card footer + per-signer cards. The schema columns `remindersDisabled` + `reminderCadenceOverride` are already there for the document level — the per-signer dimension is the only new thing. ### 4.8 Cancel EOI — signature-aware modal + honest "void" copy - **Where:** `src/components/interests/interest-eoi-tab.tsx` (the `Cancel EOI` button at the bottom of the EOI card) and `src/lib/services/documents.service.ts`'s `cancelDocument` (already calls `documensoVoid` correctly — no backend change needed). - **Why this matters:** today the button reads just "Cancel EOI" and pops a single-line confirm. Two real problems: 1. **Documenso doesn't hard-delete on void.** `DELETE /api/v2/envelope/{id}` (and the v1 DELETE) is a **void**, not an erase — Documenso retains the envelope under a "Voided" status for legal audit-trail reasons (industry-standard across DocuSign, HelloSign, Documenso). Reps see the cancelled doc still listed in Documenso and assume the void didn't fire. It did — it just didn't disappear. 2. **No protection against accidentally voiding partially-signed docs.** If 1 of 3 signers already signed, the current single-line confirm doesn't surface that — clicking Cancel discards the captured signature with no warning. - **What to ship:** - **Signature-aware confirmation modal** that scales with state: - **0 signed** → quick confirm. Copy: "Cancel this EOI? The envelope will be voided on Documenso and signers won't be able to access it." One click. - **1+ signed but not all** → fuller warning modal listing `{name} signed {humanRelative(signedAt)}` per already-signed row. Body copy: "If you continue, those collected signatures will be discarded and the envelope voided. This is non-recoverable. Continue?" - **All signed** → block the cancel entirely (the deal is past the decision point). Offer an "Archive on our side" path instead that hides the doc from the active EOI list but leaves the Documenso envelope untouched as the legal record. - **Button copy**: "Cancel & void on Documenso" (replaces just "Cancel EOI") so the rep knows what happens upstream. - **Helper tooltip / footer line** explaining void ≠ delete and that legal traceability is the reason Documenso keeps the audit row. - **After successful void**, hide cancelled docs from the primary EOI tab list. Surface them under a "Cancelled" filter / pill so the rep can browse history without "ghost" clutter. - **Backend:** No change. `cancelDocument` already calls `documensoVoid(documensoId)` which DELETEs against Documenso v1 or v2 depending on the per-port `apiVersion`. Idempotent on 404. - **Effort:** Small (~45 min). New modal component, the signature-count branch logic, copy changes, filter UI on the EOI tab. --- ## 9.Y2 Dev-mode `EMAIL_REDIRECT_TO` badge across all surfaces - **The quirk:** `env.EMAIL_REDIRECT_TO` is a dev/staging guardrail that silently rewrites every outbound email recipient to a single address (matt@letsbe.solutions in current setup) so test EOIs / portal invites / completion emails can't accidentally land in a real client inbox. Hard-blocked in production (`env.ts` refuses to boot when both `NODE_ENV=production` and `EMAIL_REDIRECT_TO` are set). When the var is on, the originals are baked into the recipient `name` field as `(was: )` so they're recoverable for audit. - **Why the user is asking:** today the redirect is invisible in the UI — reps see the SigningProgress card showing `matt@letsbe.solutions` for every recipient and think the per-role admin settings are broken. They aren't — the settings ARE saved correctly, the redirect just rewrites everything on the wire. - **What to ship:** - Add a small `DEV REDIRECT →
` pill in the global header / status bar when the var is set. Single source-of-truth for "I am in dev-redirect mode" so reps don't have to guess. - Surface a per-row "DEV REDIRECT" badge on every UI that shows a recipient email/name pair that the redirect modifies. Tooltip body: "This email would normally go to {original}. The dev-only EMAIL_REDIRECT_TO env var rewrote it to {redirectTo} so no real recipient is contacted. Unset the var to send for real." - **Surfaces that need the badge (enumerate so the implementer doesn't miss any):** 1. `SigningProgress` card on the EOI / Reservation / Contract tabs — each signer row. **The Option A treatment (show parsed `(was: original-email)` under the redirected email) ships now as part of this initiative's first slice; the global pill + other surfaces follow.** 2. Document detail page recipient list. 3. "Send invitation" / "Send reminder" confirmation toasts. 4. EOI generate drawer — preview Section 2 client email row. 5. Contact log → email-out action surfaces (when we add direct email actions; some are placeholders today). 6. Supplemental info request modal — the "request email will be sent to: " preview line. 7. Portal-activation send-out (`scripts/dev-trigger-portal-invite.ts` surfaces UI confirmation; need badge on the admin "resend activation" button). 8. Password-reset / set-password confirmation modals. 9. Signed-PDF completion email composer dialog ("Email signed PDF to all signatories"). 10. Outbound webhook dispatch UI in admin (webhooks are skipped when `EMAIL_REDIRECT_TO` is set, per `workers/webhooks.ts` line 89-104). - **Plumbing:** Add a tiny endpoint `GET /api/v1/system/email-redirect-state` returning `{active: boolean, redirectTo: string | null}`. A React context provider (``) hydrates once at app boot and exposes it to a `` and `` component. Each surface drops the component in next to the relevant recipient. - **Audit-pair extraction helper.** A shared `parseRedirectedRecipient(name): { displayName, originalEmail | null }` so every surface renders the `(was: ...)` consistently rather than doing ad-hoc regex matching like the `cleanSignerName` I just shipped. - **Effort:** Medium-high — backend endpoint + 2 React components + ~10 surface drop-ins. Touchpoint enumeration above means it's parallelisable across a few PRs. - **Defers Option B**: not pursued. The redirect is correct behavior for dev; the goal is making it visible, not hiding it. --- ## 9.Z BIG: in-app PDF field editor + template builder - **Idea (Matt):** Build a browser-side editor inside our admin that lets reps upload a PDF, place fillable fields on it (text / checkbox / date / dropdown / signature), connect each field to a CRM data token from `VALID_MERGE_TOKENS` (`src/lib/templates/merge-fields.ts`), and either use the result for in-app document generation OR push it to Documenso as a template via the API. - **Why it's worth doing:** - Centralizes template management in CRM (no Documenso UI round-trip just to fix a typo / move a field). - Unlocks custom one-off documents (port-specific addenda, info requests, reservation variants) that currently can't be filled by either pathway because there's no template editor. - Same field-to-token mapping logic shared between in-app fill and the Documenso push path. - **Tech is all here — `pdfme` (already in stack) covers ~80%:** - `pdf-lib` (already in stack) handles read/write of AcroForm fields + positioning + types — used for the AcroForm-import bridge below. - `pdf.js` renders PDF pages to a browser canvas at known DPI. - **`pdfme` (already in stack) gives us almost the whole editor for free:** - `Designer` class (`@pdfme/ui`) — drag-and-drop browser editor that mounts into a DOM element, takes any PDF as `basePdf`, lets users place + resize + rename + delete field schemas live. Built-in Ctrl+S save hook + change listeners + page navigation. - Built-in field types — `text`, `image`, `signature`, `checkbox`, `radioGroup`, `qrcode`, barcodes — each with position + size + per-type props (font, alignment, options for radio/dropdown). - `Form` class — preview-with-sample-data is exactly what this does out of the box (covers Matt's "placeholder per field" + "preview with real record" requirements with no extra build). - `generateForm()` — emits a real **interactive AcroForm** PDF (text/checkbox/radioGroup). End users can fill in Acrobat or any PDF viewer. This is the "fillable custom document" path Matt asked about. - `generate()` — flattened output for the "render + email/sign" path. Same template, different output mode. - Plugin API for custom field types (`Plugin` etc). We'd use this to add a "Documenso signing widget placeholder" field type for the Documenso push path. ~50 lines per custom type. - Merge-token catalog already exists at `src/lib/templates/merge-fields.ts` (`VALID_MERGE_TOKENS`). - **What pdfme does NOT cover — what we'd actually build:** - **AcroForm → pdfme schema import bridge** (the one piece of net-new code). pdfme uses its own schema format, not AcroForm. To edit a PDF that was made in Acrobat, we use pdf-lib's `form.getFields()` to read every existing field's name + type + widget rect, generate matching pdfme schemas, strip the AcroForm from the basePdf (so pdfme owns placements). ~50–100 lines. - **CRM merge-token mapping UI.** Repurpose pdfme schema's `name` field as the merge token, OR add a sidecar map keyed by schema id. Add a token-picker dropdown to each schema's pdfme edit panel. ~20 lines. - **Documenso template push.** POST `/api/v2/template/create` with multipart `{PDF, recipient slots, field placements as %-page coords}`. %-coords are computable from pdfme schema positions. - **Phased plan:** - **Phase 1 — MVP (1–2 weeks focused).** Text + checkbox AcroForm fields, in-app fill at generate-time only. New `document_templates` row variant with a `field_map` JSONB (`{fieldName: tokenOrSlot}`). Save path rewrites PDF via `form.createTextField/Checkbox/Dropdown` + `addToPage`. Generation fills AcroForm by name → resolved token value. - **Phase 2 — Documenso template push (+2–3 weeks).** `POST /api/v2/template/create` with multipart `{PDF, recipient slots, field placements as %-page coords}`. Editor grows recipient-slot config + signing-widget overlay placement (Documenso treats sig fields as overlays, not AcroForm — store separately). Returned `templateId` + recipient IDs persisted on our template row so the existing `documenso-template` pathway can use it. - **Phase 3 — Polish (+1–2 weeks).** All field types incl. radio groups, conditional fields, validation rules, drag-snap-to-grid, page-aware preview, dim mode. - **Risk areas to flag upfront:** - PDF coordinate gotchas (pdf-lib origin bottom-left vs canvas top-left; rotated pages; flate-compressed pages need round-trip). - AcroForm subtleties beyond text — radio groups + embedded JS. - Documenso's %-coord placement is finicky; needs a preview iteration loop or "looks right in browser" doesn't = "right when Documenso renders". - **Two additional requirements from Matt:** - **Placeholder / preview text per field.** Each field config gets a "Preview value" input alongside its name + token mapping. Live preview re-renders the current page with placeholders filled (overlay on the pdf.js canvas for live-typing, or pdf-lib fill + re-render). Ship a "Preview with real record" mode too — pick an existing client/interest/etc and render with that record's actual values, so long French street names + non-Latin scripts + joint- signer names actually surface their overflow cases. This is the real-world bench for what would otherwise need a full EOI send to catch. - **Edit existing Acrobat-authored fields.** On upload, read every AcroForm field via `form.getFields()` and render them as already- placed boxes on the canvas. Select / rename / resize / reposition / delete each. Quirk: pdf-lib doesn't expose `setPosition` directly on a widget — workaround is read-properties → delete → recreate at the new coords. Plain text/checkbox/dropdown/radio fields are fully preserved; **calculated fields / embedded JavaScript will not survive a save** (pdf-lib doesn't round-trip JS — flag this loudly in the UI if the loaded PDF contains scripted fields). Acrobat's "appearance streams" need regeneration via `form.updateFieldAppearances()` on save. - This explicitly covers the recurring "open my Acrobat-made EOI source PDF, move the Email field down 6px, save it back" workflow that currently requires bouncing to Acrobat for every pixel-level template adjustment. - **Decision needed from Matt:** Ship Phase 1 alone first (biggest win, shortest path, in-app custom docs immediately) or wait until we can scope Phases 1+2 together? --- ## 9.V Audit log — expandable rows with full detail - **Where:** `/admin/audit` page; `audit-log-list.tsx` table + the already-built `AuditLogCard` component. - **Symptom:** Rows show a placeholder summary (entity, action chip, changed-fields list, actor name + email, short IP) but can't be clicked to expand. The actual `oldValue` / `newValue` / `metadata` / full `userAgent` are stored in DB and even typed on `AuditLogCard` — they're just not surfaced on desktop. - **What to ship:** - Make each row click-to-expand OR open a side Sheet (right slide-in). Side Sheet is cleaner because `oldValue` / `newValue` can be deeply nested JSON that doesn't render well inline. Less reflow on a long-list page. - Detail panel content (everything the audit table currently hides): - Full timestamp with ms precision + relative ("about 1 hour ago") - Action + entity + entityId (clickable deep-link to the entity page where possible) - Actor block: display name, email, userId, port name + slug - Network: full IP, full userAgent string, request-id if present, port context, port + global flags - **Side-by-side oldValue / newValue diff** — JSON pretty-printed with the changed keys highlighted (mirror the `git-diff` look: removed red, added green). Falls back to plain pretty-print for `view` / `delete` events where one side is null. - Metadata block — pretty-printed JSON - Related events: any audit-log siblings emitted within the same request-id, listed compactly so a forensic trace of a multi-step action is one click away. - "Copy as JSON" button on the detail panel for forensic exports. - **Effort:** Medium (~2h). Wire `cardRender` style detail to a Sheet on click + a JSON diff visualiser. The existing `AuditLogCard` already has most of the layout — promote it into the Sheet body. --- ## 9.W Admin email-routing — Sales send-from card bugs - **Where:** `src/components/admin/sales-email-config-card.tsx` + `src/components/admin/email-routing-card.tsx` on the `/admin/email-routing` page. - **Bugs (compound):** 1. **Sales card shows noreply creds as "already saved."** The screenshot field values (`mail.letsbe.solutions`, `noreply@letsbe.solutions`, dotted password) come from `smtp_*_override` (the noreply/transactional keys). The actual `sales_smtp_*` keys are empty in the DB. The form should render empty placeholders when nothing is saved for sales — OR explicitly show a "Inherits from noreply above" badge with a "Configure separately" CTA. Today reps think they've configured sales when they haven't. 2. **No independent Save button.** The Sales card piggy-backs on the overall page save. It's a separate account with separate creds — should have its own Save button keyed to the `sales_smtp_*` subset of settings, mirroring the noreply card pattern. 3. **Description is ambiguous.** Current copy: "SMTP credentials for human-touch outbound (brochures + per-berth PDFs)". Doesn't say this is a separate account from the noreply one. Update to: _"Optional dedicated SMTP for sales-team-initiated emails (brochures, per-berth PDFs, signed-doc completions). Distinct from the noreply transactional account configured above. When unset, all outbound falls back to the noreply account."_ 4. **No Test connection button** on either Sales or Noreply cards. Add `Test connection` that opens an SMTP connection to the configured host:port with the saved password, ATTLS-upgrades when SSL is off, AUTH-LOGINs as the configured user, and reports success / specific error. Existing `documenso/test` and `imap-probe` patterns are a good template. 5. **Bottom-of-page "automated email sending addresses" list lies.** It shows `sales@…` even when sales SMTP is empty (so those flows actually fall through to noreply). The list must read the resolved-effective-address for each flow, not the configured one, and tag each entry "via sales" or "via noreply (sales not configured)". - **Plumbing already in place:** - `sales-email-config.service.ts` SETTING_KEYS has the per-key constants: `sales_smtp_host`, `sales_smtp_port`, `sales_smtp_secure`, `sales_smtp_user`, `sales_smtp_pass_encrypted`. - `email-routing.ts` already routes per-flow (brochure / berth-PDF / signed-doc completion → sales; activation / portal / digest → noreply). The fallback when sales is empty needs to be surface-honest, not silent. - **Effort:** Medium (~2-3h). Form split + new mutation per card + test-connection endpoints (×2) + bottom-list resolver tweak. --- ## 11 · URGENT bugs surfaced 2026-05-15 end-of-session ### 11.1 Webhook can't resolve v2 envelope (numeric vs envelope_xxx ID mismatch) - **Where:** `resolveWebhookDocument` in `src/lib/services/documents.service.ts`. - **Symptom:** Documenso fires webhook after signer signs, our endpoint returns 200, but logs "Document not found for webhook (port-scoped)" and never updates `document_signers.status` or `interest.eoiDocStatus`. UI stays "Awaiting signatures" forever even after all signers have completed in Documenso. Matt's session: signed in Documenso → CRM shows 0/3 signed indefinitely. - **Root cause:** Documenso v2 webhook payload's `payload.id` is the internal NUMERIC pk (e.g. `19`). We store `documents.documenso_id` as the ENVELOPE_XXX string identifier (after the normalizer fix that made title-update + distribute work). The two never match. Documenso webhook payloads do NOT carry the envelope_xxx string identifier at all — per their docs. - **Fix options (Option A recommended):** - **A. Add `documents.documenso_numeric_id` column.** Capture Documenso's numeric `id` from /template/use response at create time (it's there alongside `envelopeId`). Webhook resolver tries `documenso_id` OR `documenso_numeric_id`. Backfill script for existing rows iterates `GET /envelope` to map. - **B. Resolve by `externalId`.** Documenso webhook payload includes `externalId` (we set `loi-`). Add documents column for externalId, resolve by it as fallback. Doesn't help non-EOI doc types that don't set externalId. - **C. Synchronous API translation.** Webhook receiver sees numeric ID → calls Documenso `GET /envelope/{numericId}` → finds the envelope_xxx → resolves our doc. Adds API round-trip per webhook. - **Effort:** Small-medium (~1-2h). Migration + capture-at-create + resolver chain. - **Until shipped:** every signed / viewed / rejected event is silently dropped on the floor. ### 11.2 Sequential signing not actually enforced — envelope is PARALLEL - **Where:** v2 branch of `documensoGenerateFromTemplate` in `src/lib/services/documenso-client.ts`. - **Symptom:** Matt signed as signer #2 (Developer) before signer #1 (Client) on an EOI marked "SEQUENTIAL" in the CRM UI. Documenso accepted both signatures. - **Root cause:** `/template/use` doesn't accept a `meta` field at all — our payload's `meta.signingOrder: 'SEQUENTIAL'` is silently dropped. The envelope inherits the TEMPLATE's stored signingOrder, which defaults to PARALLEL on v2 templates unless explicitly set via the template editor. Our follow-up `/envelope/update` call sets the title but NOT the signingOrder. So envelope ships PARALLEL and any signer can sign at any time. The local "Sequential" badge on the EOI card reads our per-port setting — not the envelope's actual state. - **Fix:** In the v2 generate path, after the title `/envelope/update`, send a second `/envelope/update` with `meta: { signingOrder: }`. Verify it stuck by re-reading the envelope's `documentMeta.signingOrder`. Pair with the existing signing-order display fix (read authoritatively from the sync report / envelope meta, not the local setting). - **Effort:** Small (~30 min). One additional update call + verify step. ### 11.3 Automated emails — full refactor (luxury-port tone + old-system copy + per-port branding) - **Where:** every file under `src/lib/email/templates/`. - **What Matt called out:** - **Tone**: current copy reads unprofessional for a luxury port. Refactor every template to match the tone/voice of the old CRM repo's email templates (locate in: `/Users/matt/Repos/Port Nimara` or related — needs locating). - **Subject format:** "{first_name}, your EOI for {portName} is ready to be signed". Currently a flat title-cased "Expression of Interest ready to sign — Port Amador". - **Per-signer copy:** confirm with Matt whether each role gets unique body copy or a single template that branches by signerRole. If unique → load the old system's per-role copy. - **Signature attribution:** today reads "Thank you, Developer, Port Amador" — the literal placeholder name. Should pull the actual sender's display name + port name (e.g. "Sales Team, Port Amador" or the linked-CRM-user's display name from `documenso_developer_user_id`). - **Per-port branding** (logo, colors, background image): registry has `branding_logo_url`, `branding_primary_color`, `branding_app_name`, `branding_email_header_html`, `branding_email_footer_html`. Verify those flow into the email templates' header — Matt saw Port Nimara branding on a Port Amador EOI which suggests the per-port branding chain isn't being honoured at render time. (User retracted the wrong-port claim — was confused — but the branding question is still valid: confirm `branding_logo_url` per-port resolves correctly.) - **Greeting cleanup**: today renders `Dear Matt Ciaccio (was: matt@letsbe.solutions),` because the redirect-helper bakes the original email into the name. We stripped this at the signer insert step today — confirm the email greeting reads cleanly after the latest fix. - **"Signing happens directly inside our website — your data isn't sent to a third-party signing service"**: misleading if the marketing-site embed isn't set up + the link goes straight to Documenso. Copy should branch on whether the `embedded_signing_host` is configured + verified (pairs with §4.9a). - **Effort:** Large (~1 day). Audit every template, replace tone, rewire branding resolution if broken, refactor the per-role branching logic, verify subject format + signature attribution pull from the right sources. --- ## 10 · Known broken (pre-existing, not from current work) ### 10.1 Documenso webhook integration tests - **Where:** `tests/integration/documenso-webhook-route.test.ts` (4 of 5 tests). - **Symptom:** "Document not found for webhook (port-scoped)" — the secret matches but to the wrong port, so the port-scoped document lookup misses. - **Root cause:** The env-to-admin migration earlier in the session made `DOCUMENSO_WEBHOOK_SECRET` optional in `env.ts`. The test now sends `env.DOCUMENSO_WEBHOOK_SECRET ?? ''`. The receiver's `listDocumensoWebhookSecrets()` prefers per-port DB rows over the env fallback — and a stale port-scoped row in the test DB matches the same shared `your-webhook-secret-min-16-chars` first, capturing the request for the wrong portId. - **Fix:** Have each test seed the secret on its `makePort()` port (insert into `system_settings` with key `documenso_webhook_secret`, value encrypted, portId = the freshly created port). Then the test-created port wins the secret match. - **Effort:** ~10 min. --- ## How to use this doc 1. **Pick an area** (top-of-file index) and work through items in order. 2. Most items reference a specific file or service — start there. 3. The "Effort" tag is rough: Small = ~30 min, Medium = ~1-2h, Medium-high = ~half-day. 4. When you finish an item, move it to a "Done" section at the bottom (or open a PR that references the heading). 5. Items 4.1, 3.1, and 1.1-1.2 are the most user-visible if you want to pick a high-impact first slice. For Matt: the "Decision needed" line in 3.2 (reminders) and the "save for end" note in §8 (supplemental form) need a call before they can be picked up. --- ## After this doc — older audit follow-ups still live elsewhere This file is the **current** testing-cycle backlog (everything found during the Documenso v2 buildout + manual click-through 2026-05-15 onward). Once we work through this list, do NOT close out the audit process — earlier passes have their own punch-lists that still need to be tackled: - **`docs/AUDIT-FOLLOWUPS.md`** — the rolling deferred-item index from every audit so far. Single source of truth across cycles. - **`docs/audit-comprehensive-2026-05-05.md`** + the 2026-05-06 frontend audit + `docs/AUDIT-CATALOG.md` (320+ check catalog) — pre-Documenso findings, many of which haven't been touched yet. - **`docs/audit-final-deferred.md`** — items explicitly punted with rationale; revisit each entry's "should we?" verdict now that surrounding code has moved. - **`docs/audit-2026-05-15.md`** + **`docs/AUDIT-FINDINGS-2026-05-15.md`** — the comprehensive Playwright sweep findings that were partially fixed (A1/A2/A4/A6/A8/A9/A16/A17/A19/A20) but had a long tail still open before the Documenso work began. **When this doc is done**: re-open those audit punch-lists, dedupe anything we accidentally already fixed during Documenso work, and start the next ship cycle from there. Don't lose work between cycles.