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>
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 inlineisActiveInterest)client-archive-dossier.service.ts:266-267client-restore.service.ts:189-190, 215client-archive.service.ts:214-215reminders.service.ts:424berths.service.ts:173-174(recommender feasibility check — verify semantics still match)interests.service.ts:1161-1162, 196, 361report-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_htmlsetting + admin field (~10 min) - Per-category send-from routing matrix (~3-4h)
- Bounce monitoring infrastructure (~6-8h):
email_bouncestable 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_bouncestable.- 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.mdto 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, setCRM_INTAKE_SECRETon the website andWEBSITE_INTAKE_SECRETon the CRM, wire website's berth-map fetch + inquiry-submit + health probe perdocs/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/deleteshape, webhook payload (documentIdvsid),recipientIdvstoken. 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:
- Pull every row from legacy NocoDB via MCP.
- Audit messy MinIO storage; tie loose signed PDFs to client/interest/yacht where ownership is recoverable.
- Carry over historical Documenso documents (per-port API key + envelope IDs).
- Map legacy schema → current schema; fill obvious data gaps where the right answer is unambiguous.
- 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_SECRETviaopenssl 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(andsales@portnimara.comif 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:migrateagainst a fresh prod-shaped DB — runner ships in commit544b129; verify it actually runsCREATE INDEX CONCURRENTLYstatements.pnpm tsx scripts/migrate-storage.tsif switching from filesystem → s3 storage backend.- Verify
MULTI_NODE_DEPLOYMENT=trueis set if web + worker run on separate nodes (filesystem backend refuses to start otherwise). - Confirm
EMAIL_REDIRECT_TOis unset in production (src/lib/env.ts:110refuses to start otherwise). - Confirm
DOCUMENSO_API_URLis bare host (no/api/v1suffix) and matches the live Documenso version'sDOCUMENSO_API_VERSION. - Verify
/api/public/health?X-Intake-Secret=...returns 200 withchecks: { 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-zodadoption 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.