Files
pn-new-crm/docs/PRE-DEPLOY-PLAN.md
Matt a8607ecc9e
Some checks failed
Build & Push Docker Images / lint (push) Failing after 36s
Build & Push Docker Images / build-and-push (push) Has been skipped
docs(plan): close Step 9 — recommender simulator deferred
NocoDB inspection (via MCP) confirms the legacy Interests table carries
only the current Sales Process Level value plus point-in-time event
timestamps as text fields — no dedicated stage-change history table.
That isn't enough resolution to replay stages-over-time through the
recommender's tier-ladder + heat-score weights. Simulator deferred
until ~10+ real wins accumulate under the new pipeline, then we can
simulate against actual CRM history.

The existing /admin/berth-recommender heat-weight tuning UI is
sufficient for v1 launch.

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

23 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 — NocoDB inspection complete: simulator DEFERRED

NocoDB Interests carries only the current Sales Process Level single-select + a handful of point-in-time event timestamps (EOI Time Sent, Time LOI Sent, clientSignTime, developerSignTime, EOI_Completed_At, finalized_document_sent_at) scattered as text fields. There is no dedicated stage-change history table — only the most recent stage value survives.

The recommender simulator's tier-ladder + heat-score logic depends on "how long did this deal sit at each stage" and "which stage did past deals make it furthest to before falling through." Without an advancement timeline that's not recoverable: every imported interest collapses to one data point.

Decision (2026-05-14): defer the simulator until production accumulates ~10+ won deals under the new pipeline — then the simulator can replay against real CRM history. The existing per-port heat-weight tuning UI in /admin/berth-recommender is sufficient for v1 launch.


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.