Bundles the rest of the in-flight work from this UAT round into one
checkpoint. Each sub-area is independent; see the headings below.
UAT polish (drained 11 findings from active-uat.md):
- Dialog primitive default bumped sm:max-w-xl/lg:max-w-3xl →
sm:max-w-2xl/lg:max-w-4xl so multi-field forms + PDF previews
aren't cramped at 1440-1920px.
- Notes tab badge aggregation: new countFor{Client,Yacht,Company}
Aggregated helpers in notes.service mirror the listFor*Aggregated
symmetric-reach joins. yacht-tabs + company-tabs render the
badge; client-tabs already had badge support.
- Supplemental-info form polish bundle: BrandedAuthShell gains a
`width: 'sm' | 'md'` prop (md uses min-h-dvh scroll instead of
fixed inset-0 pin so long forms scroll naturally). Form picks up
port branding (logoUrl + backgroundUrl + appName) via
loadByToken. Address fields completed (street + city + region +
postal + country). Port name eyebrow + success-state copy added.
- new-document-menu Upload-file landing toast: per-file completion
emits toast.success with action link to the destination entity
or folder.
- interest-tabs OverviewTab "from client" pill on Email + Phone
rows via new EditableRow `inheritedFrom` prop.
- create-document-wizard subject picker → segmented button strip
(5 types visible at once).
Launch infra:
- UTM column wiring (Init 1b step 4): migration
0089_website_submissions_utm.sql adds utm_source/medium/campaign/
term/content + composite index (port_id, utm_source, received_at)
for per-campaign rollups. website-inquiries intake accepts the
five fields. Residential intake intentionally untouched per audit
scope.
- Invoicing module gate (Init 1c spike): new
invoices-module.service + invoices layout guard + registry entry
invoices_module_enabled (default false). Audit conclusion in
launch-readiness.md: payments table is canonical money path;
/invoices flow is parallel infrastructure now hidden by default.
Smart-back navigation refactor:
- Replaced breadcrumb component with history-aware Back button.
New route-labels.ts + use-smart-back hook +
navigation-history-tracker so back falls through to the parent
route when there's no prior page in history.
- Sidebar / topbar / mobile-topbar adopt the new pattern; old
breadcrumb-store kept for back-compat consumers but the
breadcrumbs component is gone.
- 6 detail pages (admin/errors per-id + codes, invoices/
upload-receipts, reports kind, tenancies detail, analytics
metric, client detail) migrated.
Trackers + docs:
- docs/launch-readiness.md — master pre-launch tracker. Includes
the reports gap audit (cross-cutting filter set, Marketing +
Financial blockers, custom builder remaining entities, scheduled
CSV/XLSX, template scope picker).
- docs/superpowers/audits/active-uat.md — 15 findings flipped
OPEN → SHIPPED locally with fix-applied notes; 4 OPEN remaining
(each blocked on user input or cross-repo).
- CLAUDE.md — minor session notes carried forward.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
518 lines
27 KiB
Markdown
518 lines
27 KiB
Markdown
# 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) and `docs/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):
|
||
|
||
1. ✅ Foundation + UX overhaul — landing page (within existing
|
||
design system); charts library audit done; ExcelJS installed.
|
||
2. ✅ 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._
|
||
3. ❌ Marketing report — NOT BUILT. Pending Init 1b cutover.
|
||
4. ❌ Financial report — NOT BUILT. Pending Init 1c decision on
|
||
whether to enable the invoices module (currently default OFF).
|
||
5. ⚠️ Custom (ad-hoc) report builder — partial ship.
|
||
6. ✅ Scheduled reports with optional emailing — BullMQ poll +
|
||
render path live; recipients optional; PDF-only output.
|
||
7. ✅ Templates — load / modify / save / save-as / URL deep-link.
|
||
|
||
Open considerations carried forward:
|
||
|
||
- **Chart library mix.** Project already has `recharts` (simple bar/line/pie)
|
||
and `echarts` (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.tsx` shell.
|
||
|
||
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. Spec calls for it on every report. Not on any.
|
||
- ❌ **Rep multi-select filter** — exists implicitly via the single-rep
|
||
leaderboard collapse, but no explicit multi-select dropdown.
|
||
- ❌ **Source multi-select filter** — Sales has lead-category + outcome
|
||
filters; the spec also calls for a generic `source` filter (website /
|
||
referral / broker / manual) on every report.
|
||
- ❌ **Empty-state copy per report** — currently shows a skeleton; spec
|
||
wants a "this report needs data first" hint pointing at the right
|
||
onboarding step.
|
||
|
||
#### Phase 2 — Sales report gaps
|
||
|
||
- ❌ **Operational-style filter set on Sales** — beyond stage / lead-cat /
|
||
outcome that shipped, the cross-cutting filters above (period
|
||
comparison, rep multi-select, source multi-select) are missing.
|
||
|
||
#### Phase 2 — Operational report gaps
|
||
|
||
- ❌ **Operational-specific filters**: berth area · tenure type ·
|
||
document type · status filter. None of the four exist. The spec calls
|
||
these out as drill-down affordances for the heatmap + tables.
|
||
|
||
#### 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 (LAUNCH-BLOCK if Financial is in beta scope)
|
||
|
||
Not built. Spec at `docs/reports-content-spec.md` § Report 02 calls for:
|
||
|
||
- 7 KPIs (revenue collected, pipeline value, deposits, outstanding AR,
|
||
overdue AR, expenses, net contribution)
|
||
- 6 charts (revenue by month stacked, quarterly/yearly toggle, EOI →
|
||
Deposit → Contract funnel, AR aging, cash flow line, expense
|
||
breakdown donut)
|
||
- 4 tables (outstanding invoices, recent payments, refund/write-off
|
||
log, expense ledger)
|
||
- Filters: invoice kind, payment status, currency, billing entity type
|
||
|
||
**Blocker:** depends on the invoices module being in use. Per Init 1c
|
||
spike, the module is default OFF and the canonical money path is the
|
||
per-interest Payments tab. **Decision needed**: ship Financial with
|
||
data from `payments` only (no invoice surface) OR flip invoices module
|
||
on for PN + train rep + ship Financial. Today the report would be 90%
|
||
empty.
|
||
|
||
#### 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 `CustomEntityDefinition` to
|
||
`src/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 by `clients.view_pii`. Today all listed columns are
|
||
available to anyone with `reports.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.
|
||
`nextRunFor` in `report-schedules.service.ts` switches on the enum;
|
||
extend to support a `cron_expression` mode.
|
||
- ❌ **Scheduled CSV / XLSX** — only PDF is wired through the worker
|
||
(`renderStandaloneReportRun` in `report-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 `visibility`
|
||
column 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 captures `createdBy`; 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.ts`
|
||
- `src/lib/email/tracking-pixel.ts`. Sales sends with
|
||
`trackOpens=true` get a 1×1 pixel; opens record to
|
||
`email_send_opens` and cross-post to Umami.
|
||
- **Umami integration**: `@umami/node` installed; `src/lib/services/umami.service.ts`
|
||
is the wrapper. Outcome events (EOI sent, deposit received, etc.)
|
||
already cross-post into Umami.
|
||
- **Website inquiry intake endpoint**: `/api/public/website-inquiries`
|
||
in the CRM, paired with `/api/public/residential-inquiries`. Both
|
||
validate + dual-write into `website_submissions`.
|
||
- **Website posting code**: `Port Nimara/Website/server/utils/crmIntake.ts:72`
|
||
has the matching POST. Just needs the env var to point at the new
|
||
CRM.
|
||
|
||
What's NOT connected yet:
|
||
|
||
1. **Website env `CRM_INTAKE_URL`** still 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 in
|
||
`website_submissions` + auto-routes to the inquiry-triage queue.
|
||
2. **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 into
|
||
`website_submissions` with original `receivedAt` timestamps,
|
||
re-links to existing CRM clients via dedup (email/phone).
|
||
3. **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.
|
||
4. **UTM column wiring**. ✅ CRM-side SHIPPED — migration `0089_website_submissions_utm.sql`
|
||
adds `utm_source / utm_medium / utm_campaign / utm_term / utm_content`
|
||
to `website_submissions` plus a `(port_id, utm_source, received_at)`
|
||
composite index for per-campaign rollups. `/api/public/website-inquiries`
|
||
accepts the five fields in the request body and persists them on
|
||
insert. **Pending website-side change**: the marketing site's
|
||
`crmIntake.ts` POST must forward UTM params from the form's query
|
||
string / cookies. **Pending residential parity**: residential
|
||
inquiries (`/api/public/residential-inquiries`) don't go through
|
||
`website_submissions`; if Marketing report needs UTM attribution on
|
||
residential leads too, add the same columns to `residential_clients`
|
||
in 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:
|
||
|
||
1. **`payments` table — canonical, in active use.** Schema comment at
|
||
`src/lib/db/schema/pipeline.ts:75` is unambiguous: "The CRM does
|
||
NOT generate invoices — clients pay banks directly. We record that
|
||
money was received." Linked to `interests`. `recordPayment`
|
||
auto-advances pipeline to `deposit_paid` when the cumulative
|
||
deposit total hits `depositExpectedAmount`. This is the surface
|
||
reps actually use; payments are recorded from the per-interest
|
||
**Payments** tab.
|
||
2. **`invoices` + `invoice_line_items` table — 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 to `invoices/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 → invoices` PDF).
|
||
|
||
### 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_settings` registry entry `invoices_module_enabled` (boolean,
|
||
port-scoped, default `false`) — added to
|
||
`src/lib/settings/registry.ts`.
|
||
- New module-gate service `src/lib/services/invoices-module.service.ts`
|
||
with `isInvoicesModuleEnabled(portId)` (same shape as
|
||
`isExpensesModuleEnabled`).
|
||
- 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 `payments` flow 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:** OPEN · Awaiting kickoff
|
||
|
||
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` /
|
||
`TaskCreate` with `subagent_type` set per dimension. Each agent writes
|
||
findings to a per-dimension scratch file under
|
||
`docs/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 memory
|
||
`feedback_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 (with `WEBSITE_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_url` etc.; ensure the website assets
|
||
match.
|
||
|
||
Open work (needs user input on priority):
|
||
|
||
- Wire the website's contact form to `/api/public/website-inquiries`
|
||
with the new payload shape.
|
||
- Add the `WEBSITE_INTAKE_SECRET` to 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=60` on 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 coverage
|
||
- `tests/e2e/destructive` — archive/delete/cancel paths
|
||
- `tests/e2e/realapi` — opt-in real Documenso + IMAP round-trip
|
||
- `tests/e2e/visual` — pixel-diff baselines
|
||
|
||
Pre-launch test gaps to fill (proposed):
|
||
|
||
1. **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.
|
||
2. **EOI generation parity** between both pathways (in-app
|
||
`fill-eoi-form` vs Documenso template). Same `EoiContext` should
|
||
produce equivalent PDFs.
|
||
3. **Multi-berth EOI rendering** — berth range formatter assertion
|
||
(`A1-A3, B5-B7` from `interest_berths`).
|
||
4. **Documenso webhook idempotency** — replay the same `DOCUMENT_COMPLETED`
|
||
webhook three times; assert single `files.folder_id` write + no
|
||
duplicate audit-log rows.
|
||
5. **Storage backend swap** — switch port to filesystem, generate EOI,
|
||
verify file lands; switch back to S3, confirm migrate script moves
|
||
the blob correctly.
|
||
6. **Visual snapshot refresh** for the new Reports UI + back-button
|
||
smart-back changes (this conversation).
|
||
7. **Mobile parity** for the entire sales journey (different Playwright
|
||
project or `--config` variant).
|
||
|
||
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
|
||
|
||
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/`):
|
||
|
||
1. **`probe-minio.ts`** — scan the legacy MinIO bucket, list every blob,
|
||
try to extract a client / interest / berth identifier from filename
|
||
patterns. Produce `docs/migration/minio-blob-inventory.csv` with
|
||
`key, size_bytes, mime, probable_entity_type, probable_entity_id, confidence`.
|
||
2. **`backfill-eoi-pdfs.ts`** — for each inventoried blob with confidence
|
||
≥ HIGH, copy from legacy MinIO into the new storage backend, create a
|
||
matching `files` row + `documents` row, deposit into the right
|
||
entity folder via the existing `ensureEntityFolder` helper. Idempotent
|
||
via `legacy_minio_key` column (add via migration if missing).
|
||
3. **`reconcile-nocodb.ts`** — diff the live NocoDB tables against our
|
||
imported state; report rows added/changed/deleted since last import.
|
||
4. **`preflight-cutover.sh`** — orchestrator script that runs the three
|
||
above in order, writes a final report.
|
||
|
||
Cutover plan:
|
||
|
||
1. Freeze writes on the old system (NocoDB read-only, portal
|
||
maintenance page).
|
||
2. Run `preflight-cutover.sh` against frozen sources.
|
||
3. 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).
|
||
4. DNS / website pointer flip.
|
||
5. 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.
|