Files
pn-new-crm/docs/PRE-DEPLOY-PLAN.md
Matt f86f511e7b docs(plan): lock pre-deploy plan from 2026-05-14 planning session
Single source of truth for everything between today and initial VPS
deploy. Captures every decision reached across the 2026-05-14 rounds:

- Hot-path correctness: canonical active-interest definition, currency-
  aware pipeline value, occupancy=sold, two-card revenue PDF, multi-
  berth EOI mooring rendering via existing Berth Number form field.
- Security gate: portal activation/reset URLs switch to URL fragment.
- Email refactor: drop signature field, per-category send-from routing,
  per-port IMAP bounce monitoring, compose-UI attachment-threshold
  banner.
- Schema: berths.archived_at, clients.metadata.source_inquiry_id,
  email_bounces table.
- UX: externally-signed mark, contract paper-upload endpoint, inquiry
  P-4.5 linkage, quick brochure/PDF download, per-user digest schedule,
  documents-tab N+1 batch fix.
- Bulk berth wizard for new-port setup.
- Four investor charts as toggleable dashboard widgets.
- Mechanical "deal" -> "interest" sweep incl. route rename.

Implementation order + deferred items + operator deploy checklist all
captured. Future agents resuming this work start here.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 14:49:13 +02:00

22 KiB

Pre-deploy plan — locked 2026-05-14

Source of truth for everything between today and initial VPS deployment. Captures every decision reached in the 2026-05-14 planning session, plus the implementation order, deferred items, and operator checklist.

If a future agent or session resumes this work, start here — do not re-litigate the decisions below without checking the transcript context that produced them.


1. Decisions

1.1 Hot-path correctness (numbers users see)

# Item Decision File(s) impacted
1 Pipeline value mixed-currency Convert each berths.price to the port-default currency at display time via currency.service, then sum. src/lib/services/dashboard.service.ts, src/components/dashboard/*
2 "Active interest" definition archivedAt IS NULL AND outcome IS NULL (strictest). Won deals are CLOSED, not active. Extract single activeInterestsWhere(portId) SQL helper; route every site through it. Sweep target — see § 2.1 for list.
3 Occupancy source of truth berth.status = 'sold'. KPI tile + revenue PDF + analytics timeline all derive from this one source. src/lib/services/dashboard.service.ts, src/lib/services/analytics.service.ts, src/lib/services/report-generators.ts
4 Revenue PDF shape Two side-by-side cards on the same page: "Completed revenue (won, gross)" + "Forecast revenue (pipeline-weighted)". Stacks gracefully on portrait. src/lib/services/report-generators.ts
4.5 Multi-berth EOI mooring rendering Populate the existing Documenso Berth Number form field with eoiBerthRange for both single- and multi-berth EOIs (single-berth output is identical to today via formatBerthRange(['A1']) === 'A1'). Drop the unused Berth Range payload key + AcroForm field + merge token. No Documenso admin action needed. src/lib/services/documenso-payload.ts, src/lib/pdf/fill-eoi-form.ts, src/lib/templates/merge-fields.ts, CLAUDE.md

1.2 Security / deploy gates

# Item Decision
5 Portal activation + password-reset token URLs Switch ?token=ABC#token=ABC (URL fragment). Fragment never hits server logs, proxies, or Referer header. Touches email templates + /portal/activate + /portal/reset-password + the set-password page reader.

1.3 Email infrastructure refactor

# Item Decision
6 Admin "Signature HTML" field Delete it. Currently writes email_signature_html to settings; shell.ts only reads emailFooterHtml. Footer covers brand sign-off; signatures are semantically per-user (separate future feature if asked).
7 Per-category send-from routing New admin matrix on /admin/email: each email category (account activation, password reset, notification digest, EOI signing request, brochure send, berth-PDF send, signed-doc completion, sales send-out, manual rep compose) gets a sender dropdown (noreply / sales). Sales option auto-disabled when sales SMTP/IMAP creds aren't set.
8 Bounce monitoring Per-port admin-configurable IMAP polling of one or more sender mailboxes. Parses DSN bounce notifications via mailparser. Writes to new email_bounces table, flags the original document_send / notification / email_thread message as bounced, and emits an in-app notification to the assigned sales rep when a client email bounces.
9 Attachment threshold compose UI On the manual-compose dialog (brochure send, berth-PDF send, rep custom email), show a banner on any attached file above email_attach_threshold_mb that says "will be sent as a 24h signed-link download instead of inline attachment". Also audit current default threshold (10MB) against typical SMTP provider caps.

1.4 Schema additions

# Item Decision
10 berths.archived_at column Add archived_at (timestamp, nullable) + partial index on (port_id) WHERE archived_at IS NULL. Filter /api/public/berths to exclude archived. Add <ArchiveBerth> action in berth detail header (soft-delete with audit log).
11 clients.metadata.source_inquiry_id Add field for inquiry → client linkage so the conversion funnel chart can attribute won deals back to the originating inquiry.
12 email_bounces table Bounce monitoring storage — see #8. Columns: id, port_id, mailbox_address, bounced_address, original_send_type (enum: document_send / notification / email_thread), original_send_id, dsn_status, dsn_action, dsn_diagnostic, received_at, raw_message.
13 Bulk-berth UX 2-step wizard for new-port setup. Step 1: pick dock letter + range + tenure (only genuinely-standard defaults). Step 2: editable table with "apply to selected" multi-row actions + Excel-style drag-fill on numeric columns. Step 3 from earlier rounds folded in.

1.5 UX features

# Item Decision
14 "Mark as signed externally" action On contract / reservation tabs: new action that records the document as signed without uploading a file. Captures optional reason in a warning modal. Advances pipeline + writes audit log. UI shows "⚠ No file on record — signed externally" indicator. Reps can later upload the file if they obtain a copy.
15 Contract paper-upload endpoint Clone the existing EOI external-eoi upload flow into external-contract and external-reservation endpoints. Mirrors the current EOI ergonomics.
16 Inquiry P-4.5 wire-up Make /clients/new?prefill_*&inquiry_id=... hydrate the create-client form from the searchParams and persist inquiry_id to clients.metadata.source_inquiry_id. Conversion funnel chart depends on this linkage.
17 Quick brochure/PDF download Add "Download" buttons on client detail header, interest detail header, berth detail header. Each downloads the current brochure (port-default) / berth PDF / signed contract from storage so the rep can attach to their own email or messenger app.
18 Per-user reminder digest schedule Build the simple version of scheduler.ts:44 placeholder. User-settings dropdown for digest time + days-of-week. Falls back to port-default when unset.
19 Documents tab N+1 batch fix Replace the 4-call sequential walk in listFilesAggregatedByEntity (direct + company + yacht + client) with a single UNION query keyed by entity-relationship. Target: opening Documents tab on a busy client ≤500ms.

1.6 Investor dashboard charts (toggleable widgets)

Priority order. Each chart ships as a separate widget integrated into the existing widget-customization system; disabled by default for reps, enabled by default for admins.

# Widget Notes
20 Total pipeline value of all berths Single big number (port-default currency, conversion at display). Weekly-change sparkline below. Re-uses the #1 currency-conversion helper.
21 Berth interest heatmap + ranked-table view Heatmap shows pier-style grid colored by active-interest count per berth. Paired with a sortable ranked-table view of the same data — table is what exports cleanly to PDF/CSV. Both views toggleable.
22 Pipeline velocity over time Stacked area chart: count of interests in each pipeline stage, weekly. Investors see whether deals are advancing or stalling.
23 Conversion funnel by lead source Enquiry → qualified → EOI → contract → won, broken down by lead_source. Depends on #16 (inquiry → client linkage) for full attribution.

1.7 Mechanical sweeps

# Item Decision
24 "Deal" → "interest" terminology sweep Full sweep. Updates: admin description copy (/admin/qualification-criteria, /admin/documenso), bulk-archive-wizard.tsx placeholders, smart-archive-dialog.tsx, client-columns.tsx comments, and the API route path /api/v1/berths/[id]/deal-documents/api/v1/berths/[id]/interest-documents. Route rename includes caller updates + a 301 redirect on the old path for any external integrations.

2. Implementation order

Branch: main (feat/documents-folders has been fast-forwarded into main; new work continues on main directly).

Test strategy: TDD-where-meaningful (services with behavioral changes — active-interest helper, currency converter, DSN parser). UI and mechanical sweeps covered by full vitest + tsc + lint + playwright smoke at the end.

2.1 Step 1 — Money math sweep (highest leverage)

Extract activeInterestsWhere(portId) helper. Sweep these call sites:

  • dashboard.service.ts (already self-consistent, replace inline isActiveInterest)
  • client-archive-dossier.service.ts:266-267
  • client-restore.service.ts:189-190, 215
  • client-archive.service.ts:214-215
  • reminders.service.ts:424
  • berths.service.ts:173-174 (recommender feasibility check — verify semantics still match)
  • interests.service.ts:1161-1162, 196, 361
  • report-generators.ts:63, 85, 121

Then:

  • Pipeline value currency conversion (dashboard.service.ts:35-47)
  • Occupancy: switch analytics timeline to berths.status = 'sold' (analytics.service.ts:195)
  • Revenue PDF: two-card layout, weighted forecast + won-gross side-by-side (report-generators.ts:109-150)

Estimated effort: ~half day. Single coherent commit set tagged feat(reporting): canonical active-interest + occupancy + currency-aware pipeline value.

2.2 Step 2 — Email infrastructure refactor

  • Drop email_signature_html setting + admin field (~10 min)
  • Per-category send-from routing matrix (~3-4h)
  • Bounce monitoring infrastructure (~6-8h): email_bounces table migration, IMAP poller worker, DSN parser, in-app notification on bounce, admin UI for sender configuration
  • Attachment threshold compose banner + threshold default audit (~1h)

Estimated effort: ~1 day. Multi-commit.

2.3 Step 3 — Schema additions

Single migration + service work:

  • 0065_pre_deploy_schema.sql: berths.archived_at, clients.metadata (already JSONB — convention update only), email_bounces table.
  • Services + admin UI for archive berth + filter on public feed.

Estimated effort: ~2h.

2.4 Step 4 — UX features

  • Externally-signed mark (contract + reservation tabs) + audit log + UI indicator
  • Contract + reservation paper-upload endpoints (clone EOI flow)
  • Inquiry P-4.5 wire-up (prefill form + persist inquiry_id)
  • Quick brochure/berth-PDF download buttons (3 surfaces)
  • Per-user reminder digest schedule
  • Documents tab N+1 batch query fix

Estimated effort: ~1 day. Multi-commit.

2.5 Step 5 — Bulk-berth wizard

Dedicated commit. New /admin/berths/bulk-add route + 2-step wizard component + smart-helpers (apply-to-selected, drag-fill). ~half day.

2.6 Step 6 — Investor dashboard charts

Four toggleable widgets, each its own commit. ~1 day total. Depends on Step 1 (currency converter) and Step 3 (inquiry linkage).

2.7 Step 7 — Terminology sweep

Mechanical. Run last to minimize merge churn. ~2h.

2.8 Step 8 — Portal token fragment switch

Dedicated commit. Email template URL builder, page-side fragment readers, Better Auth integration test. ~1h.

2.9 Step 9 — Optional: NocoDB inspection + recommender simulator

Pre-flight: use the NocoDB MCP to inspect what stage-advancement / win history exists in the legacy interest records.

  • If recoverable: build the "simulate against history" admin tool that replays past wins through current recommender weights. ~half day.
  • If not: defer until production accumulates ~10+ wins. Update BACKLOG.md to reflect.

3. Deferred items (will not block deploy)

3.1 External / operator actions (your side)

  • Coordinate website cutover env vars: generate shared secret with openssl rand -hex 32, set CRM_INTAKE_SECRET on the website and WEBSITE_INTAKE_SECRET on the CRM, wire website's berth-map fetch + inquiry-submit + health probe per docs/website-cutover-runbook.md.
  • Legal review of right-to-be-forgotten scope — anonymize vs true-delete decision. Mechanical fix once policy is set.
  • Documenso v2 endpoint audit against live v2 instance — verify /api/v2/envelope/delete shape, webhook payload (documentId vs id), recipientId vs token. Needs a live v2 instance.

3.2 Deferred indefinitely (no current trigger)

  • Bulk import queue worker (src/lib/queue/workers/import.ts) — superseded by bespoke migration scripts. Delete placeholder when the comprehensive NocoDB migration ships.
  • Auto-calibration of berth-recommender weights — depends on accumulating ≥10 won deals in the new system before it produces meaningful results.

3.3 Comprehensive NocoDB → CRM migration

Separate workstream — its own multi-session project. Scope:

  1. Pull every row from legacy NocoDB via MCP.
  2. Audit messy MinIO storage; tie loose signed PDFs to client/interest/yacht where ownership is recoverable.
  3. Carry over historical Documenso documents (per-port API key + envelope IDs).
  4. Map legacy schema → current schema; fill obvious data gaps where the right answer is unambiguous.
  5. Dry-run + apply against prod DB at initial startup.

Not on the pre-deploy checklist below — handled as a dedicated planning session before the first port-data import.


4. Pre-deploy operator checklist

In rough order. Tick as completed.

4.1 External (operator side)

  • Generate WEBSITE_INTAKE_SECRET via openssl rand -hex 32; configure both CRM and website to use it.
  • Coordinate website-cutover plan with website repo per docs/website-cutover-runbook.md.
  • Provision IMAP credentials for noreply@portnimara.com (and sales@portnimara.com if applicable) so bounce monitoring works at boot.
  • Provision SMTP credentials for both sender addresses; verify each can actually send.
  • DNS + SSL for the CRM domain.
  • Decide RTBF policy (anonymize vs true-delete) with legal; document in docs/runbooks/.

4.2 CRM side (run after code work is complete)

  • pnpm exec vitest run — all pass.
  • pnpm exec tsc --noEmit — clean.
  • pnpm exec eslint . — clean.
  • pnpm exec playwright test --project=smoke — passes.
  • pnpm db:migrate against a fresh prod-shaped DB — runner ships in commit 544b129; verify it actually runs CREATE INDEX CONCURRENTLY statements.
  • pnpm tsx scripts/migrate-storage.ts if switching from filesystem → s3 storage backend.
  • Verify MULTI_NODE_DEPLOYMENT=true is set if web + worker run on separate nodes (filesystem backend refuses to start otherwise).
  • Confirm EMAIL_REDIRECT_TO is unset in production (src/lib/env.ts:110 refuses to start otherwise).
  • Confirm DOCUMENSO_API_URL is bare host (no /api/v1 suffix) and matches the live Documenso version's DOCUMENSO_API_VERSION.
  • Verify /api/public/health?X-Intake-Secret=... returns 200 with checks: { db: 'ok', redis: 'ok' }.

5. What's NOT in this plan

Items explicitly out of scope for this deploy:

  • IMAP-based two-way email sync — feature scope decision, anti-automation stance.
  • AI features (semantic search, auto-summarize, anomaly detection) — anti-automation stance.
  • .toLocale*formatDate() sweep (93 sites) — opportunistic as files are touched.
  • drizzle-zod adoption for the remaining ~28 validators — opportunistic.
  • Reports system + admin-composable report templates (audit-followups Wave 11.C) — post-deploy feature work.
  • Manual client form expansion (Wave 11.A) — post-deploy feature work.
  • Inquiry triage auto-classification (Wave 11.F) — post-deploy feature work.
  • Per-port email branding admin UI (Wave 11.G) — post-deploy feature work.

Last updated: 2026-05-14.