deployment-plan.md gains a full env-var reference (CRM + website) and the cutover env-flip sequence; launch-readiness.md gets the 2026-06-02 closeout; BACKLOG.md adds the deferred integration-health-panel idea (section L). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
38 KiB
Launch Readiness — Pre-Prod Initiative
Scope: the user enumerated five launch-blocking initiatives on 2026-05-27. This doc is the single home for all of them so we can track progress without losing items between sessions. Companion to
docs/superpowers/audits/active-uat.md(which keeps the live UAT findings) anddocs/BACKLOG.md(master backlog index).Status tags per item:
OPEN | IN PROGRESS | SHIPPED in <hash> | BLOCKED | DEFERRED.
Initiative 1 — Reports overhaul
Status: IN PROGRESS · Active phase
Goals (per user, 2026-05-27):
- Cover all four report categories: Sales performance, Financial, Marketing / funnel, Operational.
- Template system: load template → modify → re-save OR save as new.
- Rich data density: more charts, more graphs, more KPIs.
- Output formats: PDF + CSV + Excel for each report.
- Scheduled reports: cron-driven; auto-email is optional (so the admin can schedule a run without forcing an email blast).
- Custom builder: full ad-hoc (pick entity, columns, filters, group-by), save as template — but quality-first; we don't ship a janky composer.
- UI/UX: stunning, fluid, beautiful. Within the existing white/navy brand language — no off-brand experimental themes.
Decisions locked (2026-05-27):
- Currency: port branding default
- Rep visibility: port-scoped admin setting (default depends on team size; PN is single-rep so default = full team)
- AR aging buckets: standard 30-day (current / 1-30 / 31-60 / 61-90 / 90+)
- Custom builder entity scope: all 10 entities
- Pulse data: fold into Sales report
- Inquiry-link audit: yes, audit + fix; no website-repo edits required for the audit itself (link logic is server-side)
- Scope cut for launch: Sales + Operational ship first as fully-functional reports; Marketing + Financial ship in tandem with their data sources being wired (see Initiatives 2c + 2d below).
Phases (status snapshot 2026-05-27):
- ✅ Foundation + UX overhaul — landing page (within existing design system); charts library audit done; ExcelJS installed.
- ✅ Sales Performance + Operational builders — full report pages with KPIs / charts / tables; client-side Export to CSV + Excel + PDF; server-side PDF endpoint for branded output. See gaps below.
- ❌ Marketing report — NOT BUILT. Pending Init 1b cutover.
Beta gate (2026-06-02): the
marketingkind inreports/[kind]/page.tsxnow returnsnotFound()(viaUNAVAILABLE_NEW_KINDS) instead of the "in development" placeholder, so the beta reports surface reads as complete — the landing page only advertises Sales / Operational / Financial / Custom, and the hand-typed/reports/marketingURL 404s. Remove theUNAVAILABLE_NEW_KINDSentry when this report ships. Decision: keep the reports page live for beta rather than hiding it behind a module toggle — 3 of 4 reports are fully built + verified (export, templates, scheduling) and strictly beat the dashboard-only fallback. - ✅ Financial report — SHIPPED in
b690fb8d. Built on the canonical payments + expenses tables (invoices module stays OFF); the invoice-centric spec was reframed onto the payments model ("outstanding AR" → expected-deposit shortfall; "AR aging" → outstanding deposits by deal age). 7 KPIs, 6 charts, 4 tables, port- currency normalised, 1y default range, templates + export. Marketing is the only remaining unbuilt report. - ⚠️ Custom (ad-hoc) report builder — partial ship.
- ✅ Scheduled reports with optional emailing — BullMQ poll + render path live; recipients optional; PDF-only output.
- ✅ Templates — load / modify / save / save-as / URL deep-link.
Open considerations carried forward:
- Chart library mix. Project already has
recharts(simple bar/line/pie) andecharts(heatmaps, funnels, complex). Lean on each where it fits; don't add a third unless something specific is missing. - PDF cover-page treatment. Each report PDF should open with a
branded cover (port logo, title, date range, generated-on stamp). Reuse
the existing
branded-document.tsxshell.
Working spec: docs/reports-content-spec.md (per-category KPIs +
charts + tables proposed; updated as we walk through each).
Reports — what's left (gap audit 2026-05-27)
Comparing the working spec against shipped code, here's the bucketed backlog. Items marked LAUNCH-BLOCK are needed for the beta cutover; everything else is post-launch polish unless promoted.
Cross-cutting capabilities (apply to every report)
- ⚠️ Period comparison toggle — "this period vs prior period" delta
arrows on KPI cards. Sales: SHIPPED locally (2026-05-31) — a
"Compare to prior period" toggle in the header computes an
equal-length preceding window (
previousPeriodBounds), the API recomputes KPIs for that window behind?compare=1, and the five window-derived tiles (Won, Lost, Win rate, Avg time-to-close, New leads) render colour-correct "vs prior" deltas. Point-in-time tiles (Active interests, Pipeline value) intentionally have no delta. Persisted in the saved-template config. TDD'd:previousPeriodBounds+computeSalesKpiComparisonunit tests. Operational already rendered period-start deltas. Still open: the spec's "on every report" — Operational uses a different "vs period start" baseline; reconcile the two semantics if a single consistent comparison is wanted. - ✅ Rep multi-select filter — SHIPPED in
b97f6e94(Sales). Dynamic "Assigned to" multi-select populated from a window-independentgetRepFilterOptions(distinct assigned reps port-wide); hidden when the port has no assigned interests. - ✅ Source multi-select filter — SHIPPED in
b97f6e94(Sales). Static Source multi-select (website / manual / referral / broker / other) allowlisted againstSOURCES. Both filters thread through the 5 filtered Sales queries via a pure, unit-testedparseSalesFilters. Still open: replicate both on Operational + the other report pages. - ✅ Empty-state copy per report — SHIPPED (2026-06-02). A
window-independent
hasDataflag on the Sales / Operational / Financial routes drives a shared<ReportEmptyState>hero (named icon- one-line body + onboarding action button) when the port has no
underlying data at all — distinct from the per-chart "no data in this
window" states, which already degraded gracefully. Targets: Sales →
Interests, Operational → Berths, Financial → Expenses. Spec:
docs/superpowers/specs/2026-06-02-reports-polish-design.md.
- one-line body + onboarding action button) when the port has no
underlying data at all — distinct from the per-chart "no data in this
window" states, which already degraded gracefully. Targets: Sales →
Interests, Operational → Berths, Financial → Expenses. Spec:
Phase 2 — Sales report gaps
- ✅ Operational-style filter set on Sales — stage / lead-cat /
outcome + period comparison + rep multi-select + source multi-select
all shipped (rep/source in
b97f6e94). Sales filter set is complete.
Phase 2 — Operational report gaps
- ⚠️ Operational-specific filters: Area SHIPPED (2026-06-02) —
a berth-area scope (
parseOperationalFilters+getOperationalAreaOptions, threaded through the 5 berth-derived service fns) re-queries the berth-count KPIs, occupancy-by-area, utilisation heatmap, and vacant lists for the selected areas; trend + tenancy/signing/docs panels stay port-wide with a "scoped to {areas}" caption. Browser-verified (area A: total berths 117→11). Status / tenure type / document type deferred — Status proved a light filter here (can't retro-apply to historical trend charts; the vacant lists are available-by-definition); seedocs/superpowers/specs/2026-06-02-reports-polish-design.md.
Phase 3 — Marketing report (LAUNCH-BLOCK if Marketing is in beta scope)
Not built. Spec at docs/reports-content-spec.md § Report 03 calls for:
- 6 KPIs (inquiries, inquiry→interest %, inquiry→EOI %, inquiry→won %, top source, avg time-to-respond)
- 6 charts (inquiries by source donut, source ROI stacked bar, full
funnel, conversion trend, country geo map via
react-simple-maps, time-to-respond histogram) - 3 tables (top-converting sources, recent inquiries, stuck inquiries)
- Filters: specific source, mooring, UTM campaign
Blocker: depends on the website actually sending UTM params (Init 1b step 4 — CRM-side shipped, website-side pending) AND on inquiry data flowing from the new intake endpoint (Init 1b step 1 — pending website env flip).
Phase 4 — Financial report ✅ SHIPPED in b690fb8d
Decision taken (2026-06-02): ship on the canonical payments +
expenses tables; invoices module stays OFF. The invoice-centric spec
(§ Report 02) was reframed onto the payments model so the report is
populated rather than 90% empty:
- 7 KPIs: revenue collected (net of refunds), deposits, balance, pipeline (expected deposits), outstanding deposits (expected−collected on open deals = the AR analogue), expenses, net contribution.
- 6 charts: revenue by month (deposit/balance, with month/quarter/year toggle), collection funnel (EOI → deposit → contract → won), outstanding deposits by deal age (AR-aging analogue, no invoice due dates exist), cash flow (inflow vs outflow), expense breakdown donut.
- 4 tables: outstanding deposits, recent payments, refund/write-off log, expense ledger.
- All money normalised to port currency; 1y default range; templates + CSV/XLSX/PDF export.
Follow-up (deferred, not launch-blocking): if the user later flips the invoices module ON, add invoice-sourced AR (due dates → true aging)
- the invoice/payment-status/billing-entity filters from the original spec. Browser-verified against live data (0 payment rows in dev → revenue $0 correct; 165 expenses populate the expense surfaces).
Phase 5 — Custom builder gaps
v1 ships 4 entities; full spec wants 10 + advanced composition.
- ❌ Missing entities: yachts, companies, invoices, expenses,
documents, websiteSubmissions, payments. Each is a registry-only
extension — add a
CustomEntityDefinitiontosrc/lib/reports/custom/registry.ts. ~30 min per entity. - ❌ Filters beyond date range — spec wants per-column filter rows (column → operator → value, AND/OR between rows). Today only the date range filter exists.
- ❌ Group by + aggregate — single group-by dimension + per-column aggregate (count / sum / avg / min / max). Today only a flat list.
- ❌ Column sort — pick a column + direction. Today rows return
with the registry's hardcoded
orderBy. - ❌ Live preview as you build — spec wants debounced re-render on filter / column change. Today the rep clicks "Run query" to fetch.
- ❌ Column whitelist per role — PII columns (
email,phone) should be gated byclients.view_pii. Today all listed columns are available to anyone withreports.export. - ❌ Run-once vs Save-as-template — the spec asks for three buttons on save (Run once / Save as template / Update existing). Today only the template-save path exists.
Phase 6 — Scheduled runs gaps
- ❌ Custom cron strings — three hardcoded cadences (weekly Mon 9 ·
monthly 1st 9 · quarterly 1st 9). Spec implies arbitrary cron.
nextRunForinreport-schedules.service.tsswitches on the enum; extend to support acron_expressionmode. - ❌ Scheduled CSV / XLSX — only PDF is wired through the worker
(
renderStandaloneReportRuninreport-render.service.ts). For CSV/XLSX, the worker would need to either run the existing client-side exporter server-side (drop ExcelJS into the worker bundle) or build format-specific server renderers.
Phase 7 — Templates gaps
- ❌ "Modified ●" indicator — when the rep changes view state after loading a template, the active-template badge currently just clears. Spec wants a visible "modified" marker so they know they've drifted.
- ❌ Personal vs port-wide scope — schema has the
visibilitycolumn with'private' | 'team'but the UI always saves as port-wide. The Save dialog needs a scope picker. - ❌ "Owned by" attribution — templates with
visibility='team'should show creator name. Schema capturescreatedBy; UI doesn't surface it. - ❌ Promote-to-port-wide affordance — once shipped, a "Share with team" action on personal templates that flips visibility.
Net launch-readiness for reports
If the launch scope is Sales + Operational only, reports are launch-ready with the polish items above as post-launch follow-ups.
If the launch scope includes Marketing + Financial, both reports need to be built AND their data plumbing finished (Init 1b website flip + UTM forwarding for Marketing; invoices module + rep training for Financial).
The cross-cutting filter set (period comparison, rep / source multi-select, empty-state copy) is the highest-value polish that's visible on every report — call it ~6-8 hours of work spread across both shipped report pages + the shared FilterBar component.
Initiative 1b — Marketing data pipeline cutover
Status: OPEN · Blocks the Marketing report
The CRM has the full infrastructure for marketing intake + attribution; it's just not connected end-to-end.
What's built:
- Email-open pixel tracking:
src/app/api/public/email-pixel/[sendId]/route.tssrc/lib/email/tracking-pixel.ts. Sales sends withtrackOpens=trueget a 1×1 pixel; opens record toemail_send_opensand cross-post to Umami.
- Umami integration:
@umami/nodeinstalled;src/lib/services/umami.service.tsis the wrapper. Outcome events (EOI sent, deposit received, etc.) already cross-post into Umami. - Website inquiry intake endpoint:
/api/public/website-inquiriesin the CRM, paired with/api/public/residential-inquiries. Both validate + dual-write intowebsite_submissions. - Website posting code:
Port Nimara/Website/server/utils/crmIntake.ts:72has the matching POST. Just needs the env var to point at the new CRM.
What's NOT connected yet:
- Website env
CRM_INTAKE_URLstill points at the old portal (or isn't set). Flipping this is a ~5-min config change inside the website Nuxt deploy. After flip, every website inquiry lands inwebsite_submissions+ auto-routes to the inquiry-triage queue. - Backfill of historical inquiries from the old portal so the
Marketing report has launch-day history rather than starting from
zero. Reads from
client_portal_v2's inquiry table, inserts intowebsite_submissionswith originalreceivedAttimestamps, re-links to existing CRM clients via dedup (email/phone). - Umami funnel events on the marketing site itself. The Umami
project exists; what's unclear is whether the marketing site is
firing
event:calls on key actions (form submitted, brochure downloaded, virtual-tour started). Audit needed. - UTM column wiring. ✅ CRM-side SHIPPED — migration
0089_website_submissions_utm.sqladdsutm_source / utm_medium / utm_campaign / utm_term / utm_contenttowebsite_submissionsplus a(port_id, utm_source, received_at)composite index for per-campaign rollups./api/public/website-inquiriesaccepts the five fields in the request body and persists them on insert. Pending website-side change: the marketing site'scrmIntake.tsPOST must forward UTM params from the form's query string / cookies. Pending residential parity: residential inquiries (/api/public/residential-inquiries) don't go throughwebsite_submissions; if Marketing report needs UTM attribution on residential leads too, add the same columns toresidential_clientsin a follow-up.
Sequencing:
- Step 1 is the cutover unblock (do during launch window itself).
- Step 2 is part of Initiative 5 (data migration).
- Step 3 is a website-side audit (Initiative 3).
- Step 4 is a small CRM-side schema add (one migration + 4 column reads). Decision pending: ship at launch or defer to Phase 2.
Initiative 1c — Invoicing audit-and-finish
Status: SPIKE COMPLETE · Module-toggle shipped · Financial report deferred
Audit findings (2026-05-27 spike)
The CRM has two parallel money-receiving flows in active code:
paymentstable — canonical, in active use. Schema comment atsrc/lib/db/schema/pipeline.ts:75is unambiguous: "The CRM does NOT generate invoices — clients pay banks directly. We record that money was received." Linked tointerests.recordPaymentauto-advances pipeline todeposit_paidwhen the cumulative deposit total hitsdepositExpectedAmount. This is the surface reps actually use; payments are recorded from the per-interest Payments tab.invoices+invoice_line_itemstable — orphaned in the UI. Full builder (line items, PDF, send, mark-paid) exists at/[portSlug]/invoices/new. The sidebar nav entry was removed earlier; only the page itself can link toinvoices/new. Dev DB has zero rows. The standalone surface is parallel infrastructure for the rare case where an operator wants to invoice a client directly from the CRM, plus the employee-expense-report flow (expenses → invoicesPDF).
Decision (per the existing "intentionally manual elsewhere" branch)
Ship a port-level module toggle, default OFF, identical pattern to the Tenancies and Expenses toggles. The Financial report stays deferred from launch since the canonical Payments tab feeds the Sales report (which is shipping) — separate Financial dashboard adds no value when there's no second money-receiving flow.
What shipped (2026-05-27):
system_settingsregistry entryinvoices_module_enabled(boolean, port-scoped, defaultfalse) — added tosrc/lib/settings/registry.ts.- New module-gate service
src/lib/services/invoices-module.service.tswithisInvoicesModuleEnabled(portId)(same shape asisExpensesModuleEnabled). - Layout-level guard at
src/app/(dashboard)/[portSlug]/invoices/layout.tsx— every/invoices/*route renders<ModuleDisabledPage>when the port hasn't opted in. Admins can flip on from Admin → Settings; historical rows preserved.
What's NOT changed:
- API endpoints (
/api/v1/invoices/*) still respond — historical PDF links + send-flow webhooks keep resolving regardless of the toggle. - The
paymentsflow is untouched and continues to be the canonical money-received path. - The expense → invoice flow (employee expense reports) is
unaffected since employee-expense PDFs flow through a different
surface (
/expenses) that lives behind its own module gate.
Follow-up: if the user later wants per-port branded
client-facing invoicing from inside the CRM, the surface is ready to
turn on with no schema work — just flip invoices_module_enabled = true.
Initiative 2 — Multi-agent codebase audit
Status: ✅ COMPLETE (2026-06-02) — audit + full remediation shipped.
17-lane multi-agent audit (3 workflow passes + adversarial verification +
completeness critic) produced 85 distinct findings (4 CRITICAL / 17
HIGH / 29 MEDIUM / 35 LOW), all triaged and remediated across 28
fix(audit) commits; 84 fixed, L21 verified a false positive. tsc-clean,
1103/1103 unit tests green. Two DB-schema migrations (M23 invoice
numeric(12,2), M25 client_contacts email unique index) deferred with
their code fixes shipped. Full report + per-finding fix mapping:
docs/audits/2026-06-02/findings-master.md (§ Remediation status).
User ask: "deep, multi-agent audit of all routes, naming, text, UX, and … dig through the entire code of everything in the system (especially related to the sales process) and find any issues in the logic or how the functionality interacts with each other, how data is shared and persists where needed. Also a deep security audit."
Audit dimensions (use one specialised agent per dimension, in parallel):
| # | Dimension | Specialised agent | Output |
|---|---|---|---|
| 1 | Sales pipeline logic | feature-dev:code-explorer |
Trace every stage transition; verify auto-advance rules, EOI gating, deposit handling, contract signing. Look for stale enum references (the 9→7 stage migration left some bugs). |
| 2 | Cross-entity data flow | feature-dev:code-explorer |
Map polymorphic ownership (yacht/company), interest_berths (multi-berth), document folders (aggregated projection), notes (4-table dispatch). Find divergence between docs and code. |
| 3 | Security | security-review (existing skill) |
OWASP API Top 10, auth bypass, IDOR, injection, secret leakage, GDPR exposure. Multi-tenant boundary checks (port_id at every join). |
| 4 | API surface consistency | code-review:code-review |
{ data: T } envelope adherence, errorResponse(error) usage, parseBody(req, schema) usage, 204 vs JSON, withAuth+withPermission composition. |
| 5 | UI/UX consistency | frontend-design:frontend-design review |
Visual inconsistencies, copy/text issues, accessibility, mobile parity, brand drift, em-dashes, generic SaaS slop. |
| 6 | Schema vs code divergence | feature-dev:code-explorer |
Migrations vs Drizzle schema files vs service helpers — find any column the DB has that no service touches, or any service field with no migration. |
| 7 | Documenso integration | feature-dev:code-explorer |
Full v1↔v2 path coverage, webhook idempotency, template field mapping, EOI generation (both pathways), error recovery. |
| 8 | Storage & file lifecycle | feature-dev:code-explorer |
S3↔filesystem switching, file orphans, signed-URL expiry, GDPR export coverage, magic-byte validation everywhere. |
Coordination:
- Use a single coordinator session that fans out via
Agent/TaskCreatewithsubagent_typeset per dimension. Each agent writes findings to a per-dimension scratch file underdocs/audits/2026-05-27/<dimension>.md, then the coordinator consolidates into a single triage doc with severity tags. - Pass
model: "opus"on every agent spawn — Sonnet/Haiku context windows compact too fast under MCP baseline (per memoryfeedback_subagent_context_bloat).
Output: docs/audits/2026-05-27/findings-master.md with per-finding
severity (CRITICAL | HIGH | MED | LOW), file:line refs, and
recommended fix. Critical + High get fixed before launch.
Initiative 3 — Marketing website integration
Status: OPEN · Needs scope clarification
User ask: "make our relevant edits to the marketing website to prepare for the deployment and integration of our new system."
The marketing site lives in /Users/matt/Repos/Port Nimara/Website
(separate Nuxt repo). Integration touch points the CRM exposes today:
/api/public/berths+/api/public/berths/[mooringNumber]— feeds the marketing site's berth list / detail. Status precedence Sold > Under Offer > Available is already wired./api/public/health— dual-mode health check; the website should call the authenticated variant (withWEBSITE_INTAKE_SECRET) on startup so it refuses to start when pointed at the wrong CRM env./api/public/website-inquiries— intake endpoint for the contact form; dual-writes inquiry into the CRM.- Inquiry email ownership — at cutover, inquiry emails move from
the website to the CRM (per memory
project_email_ownership_at_cutover). Templates + settings keys already exist; berth public endpoint + admin recipient UI still needed (per existing memory). - Cover photography + branding assets — the new system uses
branding_email_background_urletc.; ensure the website assets match.
Open work (needs user input on priority):
- Wire the website's contact form to
/api/public/website-inquirieswith the new payload shape. - Add the
WEBSITE_INTAKE_SECRETto the website's env, point at the authenticated/api/public/health. - Update berth-detail page to consume the new
/api/public/berths/...shape (the JSON mirrors the legacy NocoDB shape so this should be a no-op — VERIFY). - Replace any hard-coded "noreply@portnimara.com" sender on the website side with the CRM-controlled From address (so per-port branding wins).
- Confirm the website's caching headers don't fight ours
(
s-maxage=300, stale-while-revalidate=60on berth endpoints).
Initiative 4 — End-to-end testing
Status: OPEN · Needs scope clarification
User ask: "end to end testing of all sales functions, generating EOIs/documents (especially), ensuring all UX/UI is fluid, beautiful, relevant and helps the user go through the sales process effortlessly."
Existing infrastructure (per CLAUDE.md):
tests/e2e/smoke— fast click-through (~10 min, ~125 specs)tests/e2e/exhaustive— deeper UI coveragetests/e2e/destructive— archive/delete/cancel pathstests/e2e/realapi— opt-in real Documenso + IMAP round-triptests/e2e/visual— pixel-diff baselines
Pre-launch test gaps to fill (proposed):
- End-to-end sales journey (single Playwright spec, real-API): new inquiry → qualified → EOI generated (Documenso) → client signs → developer countersigns → reservation → deposit recorded → contract generated → contract signed → tenancy auto-created → berth marked sold. Assert every stage transition + every email fires.
- EOI generation parity between both pathways (in-app
fill-eoi-formvs Documenso template). SameEoiContextshould produce equivalent PDFs. - Multi-berth EOI rendering — berth range formatter assertion
(
A1-A3, B5-B7frominterest_berths). - Documenso webhook idempotency — replay the same
DOCUMENT_COMPLETEDwebhook three times; assert singlefiles.folder_idwrite + no duplicate audit-log rows. - Storage backend swap — switch port to filesystem, generate EOI, verify file lands; switch back to S3, confirm migrate script moves the blob correctly.
- Visual snapshot refresh for the new Reports UI + back-button smart-back changes (this conversation).
- Mobile parity for the entire sales journey (different Playwright
project or
--configvariant).
Each gap above becomes one or two new spec files. Coordinate with Initiative 2's audit so we don't double-test.
Initiative 5 — Data migration (legacy → new)
Status: OPEN · High effort · Likely blocker for cutover
Infra cutover plan:
docs/deployment-plan.md— prod deploy of the CRM tocrm.portnimara.com(nginx + certbot + registry-image compose), Gitea/CI access, and the Documenso backup + safe-upgrade procedure. Access (SSH + Gitea API) established 2026-05-31; no prod changes without explicit approval. Deployment creds inprivate/deployment-creds.md(gitignored).
User ask: "start pulling all existing prod data from the old system and connected systems (we'll have to backfill the EOIs by pulling them through MinIO — it's a fucking mess so I'll really need your help automating/speeding up that process) and initiate a preliminary switch over."
Sources to drain:
| Source | Storage | Entities | Notes |
|---|---|---|---|
| Old NocoDB tables | Postgres / NocoDB | Clients, yachts, companies, interests, berths, EOIs (metadata) | Already imported in earlier migration; verify currency vs prod NocoDB. |
Old portal (client_portal_v2) |
Nuxt + Postgres | Portal users, signing history, sent invitations | Need to confirm what hasn't been migrated yet. |
| MinIO (legacy bucket) | Object storage | EOI PDFs (signed + unsigned), receipts, contracts | The "fucking mess" — naming is inconsistent, organisation unclear, need to map each blob back to its CRM entity. |
| Documenso v1 (live) | Documenso server | In-flight signing envelopes + signed PDFs | Migration question: do we cut new EOIs to v2 and let v1 envelopes finish, or migrate the in-flight? |
| Email archives | IMAP / mail server | Inquiry replies, signing reminders, deposit confirmations | Probably out of scope for cutover (read-only history). |
Migration script plan (write under scripts/migration/):
probe-minio.ts— scan the legacy MinIO bucket, list every blob, try to extract a client / interest / berth identifier from filename patterns. Producedocs/migration/minio-blob-inventory.csvwithkey, size_bytes, mime, probable_entity_type, probable_entity_id, confidence.backfill-eoi-pdfs.ts— for each inventoried blob with confidence ≥ HIGH, copy from legacy MinIO into the new storage backend, create a matchingfilesrow +documentsrow, deposit into the right entity folder via the existingensureEntityFolderhelper. Idempotent vialegacy_minio_keycolumn (add via migration if missing).reconcile-nocodb.ts— diff the live NocoDB tables against our imported state; report rows added/changed/deleted since last import.preflight-cutover.sh— orchestrator script that runs the three above in order, writes a final report.
Cutover plan:
- Freeze writes on the old system (NocoDB read-only, portal maintenance page).
- Run
preflight-cutover.shagainst frozen sources. - Manual reconciliation of probe-minio rows where confidence < HIGH (likely a few hundred blobs — the user explicitly flagged this is manual labour, automation helps but doesn't replace it).
- DNS / website pointer flip.
- Watch error_events for 24h; rollback plan = re-enable old system writes and stop the cutover commit.
Cross-initiative open questions
- When to wrap the launch audit doc. I'd suggest: after Initiative 2's findings are triaged AND Initiatives 3-5 reach IN PROGRESS. At that point this file becomes the launch-day-runbook.
- Who's the launch sponsor / decision-maker? Different from "user / matt"? Affects who signs off on cutover.
- Soft launch vs hard cutover? Hard cutover is simpler operationally but risky; soft launch (parallel writes for a week) is safer but requires the old system to keep accepting writes for longer.
2026-06-01 — Feature-completeness sweep & launch-prep decisions
A read-only sweep (ahead of the ~same-day launch) checked the whole
platform for half-built / stubbed surfaces beyond the known Reports
gaps. It resolved two stale-doc contradictions: Documenso signing
phases 2–7 are fully built and wired (BACKLOG.md §A is stale on
this), and the interest Contract/Reservation tabs are fully built
(not "coming soon" cards). Findings + decisions below.
Decision (per Matt, 2026-06-01): launch is ~today, so ship what's done, hide what's not, defer the big builds — do NOT revert to the old desktop-spreadsheet reports (a downgrade), and do NOT rush the unproven full builds onto a same-day prod launch.
Shipped today (launch-prep, low-risk; SHIPPED)
- Hid Financial + Marketing report cards from the reports landing
(
reports/page.tsx) — both were "Builder in development" placeholders gated on unbuilt data sources (Init 1b/1c). The reports section ships with the working Sales + Operational + Custom reports + templates + scheduling + PDF/CSV/Excel exports. The basic Custom builder already covers the old desktop-report use case (entity + columns + date range + export) — parity-plus, not a regression. - Trimmed the Custom-report card copy so it stops promising group-by/filters/dimensions it doesn't yet have (the builder page header was already honest).
- Hid the Bulk Import mockup from nav + search
(
admin-sections-browser.tsx,search-nav-catalog.ts). The static/admin/importmockup is now unreachable from the UI (route still resolves by direct URL). - Corrected client-facing doc over-claims in
features-list.md+new-system-feature-summary.md(removed the waiting-list "next-in-line notification" claim — built but hidden; removed Import from the admin-pages list, 43→42).
Deferred to post-launch (tracked here; none launch-blocking)
- Full Bulk CSV/XLSX importer — design APPROVED + spec written:
docs/superpowers/specs/2026-06-01-bulk-import-design.md(generic engine + per-entity adapter registry; 7 entities; column-mapping, dry-run, dedup, per-batch undo). Cutover data migration runs through the existing CLI scripts (import-berths-from-nocodb.ts+ the Initiative 5 migration scripts), so the UI importer is not needed for launch. - Full Custom-report builder — group-by + aggregates, sort, per-column filter rows (AND/OR), debounced live preview, the remaining 6 of 10 entities, per-role PII column whitelist. Architecture decided (per-column expression map + generic Drizzle query composer); spec deferred. Basic builder ships as-is.
- Berth Waiting List — ✅ SHIPPED in
8be7a6e2.WaitingListManagertab un-hidden + wired. Still deferred: the availability-triggered next-in-line notification (today only anotifyPrefcolumn is stored; no sender exists). - Berth Maintenance Log — ✅ SHIPPED in
8be7a6e2. UI tab mirroring the waiting-list manager, on the existing API + service. - Contract/Reservation paper-upload misroute (BUG) — ✅ SHIPPED in
d98aa5cc. Added contract/reservation paper-upload endpoints + pointedExternalEoiUploadDialogat the right one per docType, so a paper-signed contract/reservation no longer files as an EOI. - Marketing + Financial reports — remain unbuilt + now hidden; gated on Init 1b (website UTM/inquiry cutover) and Init 1c (invoices-module decision) respectively.
2026-06-02 (session) — Pre-deployment closeout
Driving toward cutover. Decisions + state captured this session.
Decisions locked
- Email ownership = the new CRM, flag-gated (option A). Registrant
confirmations + staff alerts move from the website to the CRM. The CRM
intake path sends them behind a per-port flag (default OFF); the website
keeps sending until cutover, then flips off in one coordinated step (no
gap, no double-send). Preserve asymmetry: berth/residence = confirmation
- alert; contact form = alert only. Prove end-to-end before the flip.
Cross-ref:
project_email_ownership_at_cutovermemory.
- alert; contact form = alert only. Prove end-to-end before the flip.
Cross-ref:
feature/video-headersis excluded from this launch (per Matt) — a separate workstream, do not touch. Launch base =main.- Berth-read swap is a cutover-time flip, not now. Until cutover NocoDB is the live source staff update; the public map must mirror it.
- UTM capture is cookieless (no consent banner exists). In-memory only; no cookie / localStorage / sessionStorage (ePrivacy/PECR). Session-scoped.
Marketing website (repo: code.portnimara.com/ron/website.git, branch main)
Local main was 1 unpushed commit ahead (the CRM dual-write) + an uncommitted
Umami refactor; nothing to pull from the remote. Reconciled:
- Committed the Umami refactor (
7e111b3,d03fcee) to set a clean base.mainis NOT yet pushed (unpushed since 2026-05-04). - Inquiry dual-write: pre-existing, dormant (no-ops without env). Contract
verified matching CRM
/api/public/website-inquiries(header, payload,kindenum, idempotent UUID). - Berth-read swap: SHIPPED locally, default-OFF.
server/utils/berths.tsreads CRM/api/public/berths(+ single) whenCRM_BERTHS_ENABLEDtruthy andCRM_INTAKE_URLset; else NocoDB.register.tsskips the NocoDB interest->berth link in CRM mode (UUID ids; CRM links via dual-write).PublicBerthconfirmed a verbatim superset of the websiteBerthtype incl.Map Data{path,x,y,transform,fontSize} -> the CRM fully backs the website berth map. vue-tsc: type-clean. - UTM forwarding: SHIPPED locally, cookieless.
plugins/utm.client.tsreads utm_* from the landing URL into memory and adds anx-utmheader on /api/register + /api/contact POSTs;server/utils/utm.ts#readUtmparses it;crmIntake.tsforwards the five fields (CRM schema already accepts them). Zero form-component changes; a misfire degrades to null UTM (no breakage). vue-tsc: type-clean. - Uncommitted: the berth-swap + UTM changes are in the working tree, not yet committed (awaiting go).
- Pre-existing bugs noted (not fixed):
pages/berths/[mooringNumber].vue:123uses.MooringNumber(should be["Mooring Number"]);berths-item/introduction.vue:32mis-indexes"Water Depth". The website repo has no typecheck/lint step (recommend wiring one pre-deploy).
Env flips required at cutover (website)
CRM_INTAKE_URL+CRM_INTAKE_SECRET(turn on inquiry dual-write delivery)CRM_BERTHS_ENABLED=1(switch the public berth map/list to the CRM)- website email sending OFF + CRM email flag ON (single owner)
Closeout sequence
- Local website (no prod risk): berth swap DONE, UTM DONE.
- CRM-side email ownership build (flag-gated) + website email-off toggle; prove end-to-end. NEXT.
- [GATED] Documenso v1.13.1 -> v2.11.0 prod upgrade (dry-run passed 2026-06-01; Phases A-E sober/scheduled, per-step approval).
- [GATED] Prod CRM deploy (Phase 1: nginx/certbot/compose/.env). Inputs needed: Postgres own/shared, deploy dir, registry token, Documenso API token.
- [GATED] Data migration + cutover (MinIO EOI backfill, final NocoDB reconcile, freeze + website env flips) in a maintenance window, explicit go.
- Post-launch: M23/M25 migrations; e2e in CI; website typecheck/lint.