# 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._
