Captures the agreed cutover plan (Q6 in the decisions log: double-write transition window, ~30 days, then NocoDB decommission). The CRM side is wired today — public berth feed, website-inquiries intake, dual-mode health probe, WEBSITE_INTAKE_SECRET env var. The runbook documents the website-repo checklist and rollback path so we can pick it back up when prep for prod begins. Refreshes the audit-followups status snapshot to reflect what shipped this session. Wave 11 is now broken out into A-G subitems so the remaining group-discussion work is enumerated rather than collapsed. Note: .env.example separately needs WEBSITE_INTAKE_SECRET added (see runbook §Endpoints). The husky pre-commit hook blocks .env* files intentionally — pass via a separate workflow. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
717 lines
38 KiB
Markdown
717 lines
38 KiB
Markdown
# Audit Follow-ups — 2026-05-08 visual audit
|
||
|
||
This is the single index for everything from the 2026-05-08 mobile visual
|
||
audit. Owns: status of each item, file pointers, every open question,
|
||
and a ready-to-paste prompt for resuming in a fresh session.
|
||
|
||
Items are grouped by **wave** (the original triage buckets, kept stable
|
||
across sessions). Numbering inside each wave matches the original audit
|
||
message order where possible.
|
||
|
||
> **If you only have time for one section, read § "Resuming in a fresh
|
||
> session" at the bottom.**
|
||
|
||
---
|
||
|
||
## Quick status snapshot — 2026-05-09 (post-execution)
|
||
|
||
| Wave | Topic | Status |
|
||
| --------- | ------------------------------------------ | ----------------------------------------------------------------------- |
|
||
| 1 | Small confident fixes | ✅ Done |
|
||
| 2 | Country dropdown unification + cmdk scroll | ✅ Done (country/nationality split still deferred — see Wave 11.E) |
|
||
| 3 | Berth field overhaul (NocoDB enums) | ✅ Done |
|
||
| 4 | Currency platform-wide | ✅ Done |
|
||
| 5 | Configurable enums (admin Vocabularies) | ✅ Admin page + read endpoint shipped; consumer wiring is owed |
|
||
| 6 | Notes unification (aggregate-on-read) | ✅ Done — yacht / company / residential aggregators + UI |
|
||
| 7 | Clients / yachts / companies misc | ✅ Status-link flow done; client form expansion still large (Wave 11.A) |
|
||
| 8 | Expenses revisit | ✅ Done — trip-label combobox (free text + past suggestions) |
|
||
| 9 | Interests + notifications | ✅ Done |
|
||
| 10 | Settings polish | ✅ Done — first/last name + collapse notif prefs |
|
||
| 11.A | Manual client form expansion | 🔴 Not started (large) |
|
||
| 11.B | Documents folders (unlimited nesting) | 🔴 Not started — needs deep design (sidebar tree + breadcrumb) |
|
||
| 11.C | Reports system + templates | 🔴 Not started |
|
||
| 11.D | Receipts inline in expense PDF | 🔴 Not started |
|
||
| 11.E | Country / Nationality split on Client form | 🔴 Not started |
|
||
| 11.F | Inquiry triage | 🔴 Deferred |
|
||
| 11.G | Per-port email branding admin UI | 🔴 Deferred |
|
||
| **Bonus** | **Public berth feed (website map)** | ✅ Parity fields shipped; cutover deferred (see runbook) |
|
||
| **Bonus** | **Website cutover runbook** | ✅ Doc shipped (`docs/website-cutover-runbook.md`); execution deferred |
|
||
| **Bonus** | **Berth Documents tab → Spec + Deal** | ✅ Done |
|
||
|
||
Test status: `pnpm exec vitest run` → **1187/1187 pass**.
|
||
TS check: `pnpm exec tsc --noEmit` → **clean**.
|
||
Git: 9 commits this session (Waves 4-10 + admin Vocabularies + status-change link + Berth Documents tab split + decisions log).
|
||
|
||
---
|
||
|
||
## Ground rules / invariants we picked up
|
||
|
||
- **Notes unification model**: aggregate-on-read (option 1 from the
|
||
AskUserQuestion, picked by user). One canonical service per entity
|
||
unions own-notes + related-entity notes; no replication, no schema
|
||
migration.
|
||
- **NocoDB MCP**: connected at `~/.claude.json` under
|
||
`mcpServers."NocoDB Base - Port Nimara"`. Verified Berths schema +
|
||
records pull cleanly. The seed-data JSON snapshot
|
||
(`src/lib/db/seed-data/berths.json`) is also a reasonable fallback
|
||
if the MCP is unavailable.
|
||
- **Berth dropdown values** are now sourced from the NocoDB SingleSelect
|
||
choices verbatim — see `src/lib/constants.ts` (look for
|
||
`BERTH_*_OPTIONS` / `_TYPES`). Power Capacity and Voltage stay numeric
|
||
inputs because NocoDB stores them as `Number`. Bow Facing is
|
||
`SingleLineText` in NocoDB but constrained to the 4 cardinal values
|
||
in the CRM dropdown for UX.
|
||
- **Dual-unit fields** auto-cross-fill via `linkedUnit` on
|
||
`EditableSpec` in `src/components/berths/berth-tabs.tsx`. The user
|
||
edits the imperial value; the metric column is computed × 0.3048 and
|
||
patched in the same request.
|
||
- **Receipts in expense PDF**: user's clarified preference is "PDF
|
||
images should show inline with the relevant expense" — i.e. images
|
||
inline; PDF receipts also rendered inline (one page each, via
|
||
pdfme + `pdf-lib.copyPages`).
|
||
- **Configurable enums**: the existing pattern is `system_settings`
|
||
with composite PK `(key, port_id)` and `<SettingsManager>` admin
|
||
page. Use the same pattern for the new vocabularies.
|
||
- **Turbopack dev**: `pnpm dev` runs `next dev --turbopack`. Cold
|
||
compiles ~1s boot, ~3s per route. No webpack hooks in
|
||
`next.config.ts` so flipping back is one line if needed.
|
||
|
||
---
|
||
|
||
## ✅ Completed this session
|
||
|
||
### Wave 1 — small confident fixes
|
||
|
||
1. **Berth list ordering bug** — `\d+$` regex in the Drizzle SQL
|
||
template was being eaten by JS string literal escape rules
|
||
(`\d` → `d`). Fixed by switching to `[0-9]+$` POSIX class.
|
||
File: `src/lib/services/berths.service.ts:69-72`.
|
||
2. **Dashboard KPI grid removed** — "Total Clients / Active Interests
|
||
/ Pipeline Value / Occupancy Rate" deleted. The four chart widgets
|
||
below (pipeline funnel, occupancy timeline, revenue breakdown,
|
||
lead source) and the activity feed remain.
|
||
File: `src/components/dashboard/dashboard-shell.tsx`.
|
||
3. **Per-dock color stripe on mobile berth cards** — was the _status_
|
||
color, which made every same-dock berth different. Now uses
|
||
`mooringLetterDot()` so the stripe groups by dock letter; status
|
||
conveyed by the existing pill below.
|
||
File: `src/components/berths/berth-card.tsx`.
|
||
4. **`{Letter} Dock` chip** on the berth detail header replaces the
|
||
bare "A" / "B" text. Colored by `mooringLetterDot()`.
|
||
File: `src/components/berths/berth-detail-header.tsx`.
|
||
5. **cmdk wheel-scroll bug** — Radix Popover swallowed wheel events on
|
||
the country dropdown for macOS users. Added `onWheel` translator on
|
||
`CommandList` + `overscroll-contain`. Lights up country pickers in
|
||
Companies, Residential Clients, Clients, Yachts.
|
||
File: `src/components/ui/command.tsx`.
|
||
6. **Mobile "Columns" button hidden** — `ColumnPicker` is now
|
||
`hidden sm:inline-flex`. Mobile renders cards (no columns to
|
||
toggle).
|
||
File: `src/components/shared/column-picker.tsx`.
|
||
7. **Mobile kanban toggle hidden + auto-fallback** — Interest list
|
||
hides the table-vs-kanban toggle on small viewports and snaps
|
||
`viewMode` back to `'table'` if the user's persisted choice was
|
||
`'board'`.
|
||
File: `src/components/interests/interest-list.tsx`.
|
||
8. **Inbox entry removed from mobile More-sheet** — email/IMAP feature
|
||
is deferred (`sidebar.tsx` calls this out); the More-sheet entry was
|
||
a dead link.
|
||
9. **Website Analytics conditional** — desktop sidebar Insights section
|
||
AND mobile MoreSheet hide the Website Analytics nav when Umami
|
||
isn't configured for the port. Reuses `useUmamiActive()`.
|
||
Files: `src/components/layout/sidebar.tsx`,
|
||
`src/components/layout/mobile/more-sheet.tsx`.
|
||
10. **"Other" comm-channel UX hint** — when a contact's channel is
|
||
`'other'`, the inline `Label` field switches its label/placeholder
|
||
to "Specify" / "e.g. Telegram, Signal".
|
||
File: `src/components/clients/client-form.tsx:289-302`.
|
||
11. **End Membership wording** — renamed to "Remove from company" in
|
||
the company members tab dropdown.
|
||
File: `src/components/companies/company-members-tab.tsx:249`.
|
||
12. **Berth area filter → letter dropdown** — was free-text; now a
|
||
`<Select>` constrained to `A / B / C / D / E`. Label changed to
|
||
"Dock" to match how the user refers to it.
|
||
File: `src/components/berths/berth-filters.tsx`.
|
||
13. **Yacht flag → CountryCombobox** — was a free-text 2-letter input
|
||
(`placeholder="e.g. MT"`); now uses the same country picker as
|
||
client / residential.
|
||
File: `src/components/yachts/yacht-form.tsx`.
|
||
|
||
### Wave 2 — country dropdown unification
|
||
|
||
1. **cmdk wheel-scroll** — covered in Wave 1 (single shared command).
|
||
2. **Country → timezone auto-set** in client form: when nationality is
|
||
picked and timezone empty, the primary IANA zone is pre-filled. Skips
|
||
when the user already chose a zone explicitly.
|
||
File: `src/components/clients/client-form.tsx` (look for
|
||
`primaryTimezoneFor`).
|
||
3. **Browser-detected timezone fallback** in user settings: timezone
|
||
pre-populates from `Intl.DateTimeFormat().resolvedOptions().timeZone`
|
||
on first load (was empty before).
|
||
File: `src/components/settings/user-settings.tsx`.
|
||
4. **Country → timezone auto-fill** also fires in user settings when
|
||
the country changes with no zone set.
|
||
5. **Dropdown widths match trigger** — `CountryCombobox` and
|
||
`TimezoneCombobox` popover content set to
|
||
`w-[var(--radix-popper-anchor-width)]` with sensible `min-w-*`
|
||
floors so wide triggers get wide popovers.
|
||
6. **DEFERRED: country/nationality split** on the client form — needs
|
||
a Drizzle migration (`alter table clients add column country_iso
|
||
text`) plus a copy-on-migrate of existing `nationality_iso` values.
|
||
See § Wave 11 / pending — large.
|
||
|
||
### Wave 3 — berth field overhaul (NocoDB enums)
|
||
|
||
1. **Live NocoDB pull via MCP** — confirmed canonical SingleSelect
|
||
choices for: Side Pontoon (10 values), Mooring Type (5),
|
||
Cleat Type (2), Cleat Capacity (2), Bollard Type (2),
|
||
Bollard Capacity (2), Access (5), Area (A–E). Power Capacity and
|
||
Voltage are `Number` fields (not enums). Bow Facing is
|
||
`SingleLineText` (we still use a 4-value dropdown for UX).
|
||
2. **`BERTH_BOW_FACING_OPTIONS`** added to `src/lib/constants.ts`
|
||
alongside the existing `BERTH_*_OPTIONS` constants.
|
||
3. **`toSelectOptions()` helper** added to `src/lib/constants.ts` for
|
||
mapping readonly tuples → shadcn `<Select>` `{value,label}` objects.
|
||
4. **All berth dropdown fields → `<Select>`** in both the modal form
|
||
(`berth-form.tsx`) and the inline-edit detail tabs
|
||
(`berth-tabs.tsx`). Bow facing / side pontoon / mooring type /
|
||
access / cleat type / cleat capacity / bollard type / bollard
|
||
capacity / area / tenure type.
|
||
5. **Inline-edit `EditableSpec`** in `berth-tabs.tsx` now supports
|
||
`selectOptions: readonly string[]` to render a `<Select>` variant.
|
||
6. **Dimensional auto-conversion** — `EditableSpec` gained a
|
||
`linkedUnit: { field, multiplier }` prop. Saving the imperial value
|
||
also patches the metric column (× 0.3048). Applied to length, width,
|
||
draft, nominal boat size, water depth.
|
||
7. **Nominal boat size editable** — was read-only `<SpecRow>`; now an
|
||
`<EditableSpec numeric linkedUnit>` so editing ft auto-fills m.
|
||
8. **Tenure type editable** — was read-only; now an inline-edit Select
|
||
bound to the validator's `'permanent' | 'fixed_term'` set. Will be
|
||
replaced by the per-port configurable list once Wave 5 ships.
|
||
|
||
### Wave 9 — interests + notifications
|
||
|
||
1. **StageLegend popover** — small "Legend" button in the interest
|
||
list filter row decodes the colored stripes on each card to the
|
||
pipeline stage name. Stays in sync with `STAGE_DOT` automatically.
|
||
File: `src/components/interests/stage-legend.tsx`.
|
||
2. **Mobile kanban hidden** — see Wave 1.
|
||
3. **Notifications nav 404 fixed** — More-sheet entry pointed at
|
||
`/notifications` which had no `page.tsx`. Now points at
|
||
`/notifications/preferences` and is labeled "Notification
|
||
preferences" — real notifications come via the topbar bell.
|
||
File: `src/components/layout/mobile/more-sheet.tsx`.
|
||
|
||
### Wave 10 — settings polish
|
||
|
||
1. **Phone input upgraded** — user settings now uses the existing
|
||
shared `<PhoneInput>` (country flag dropdown + AsYouType formatter)
|
||
instead of a plain `<Input type="tel">`. Country state from the
|
||
page seeds the dropdown.
|
||
File: `src/components/settings/user-settings.tsx`.
|
||
2. **Timezone auto-detect** — covered in Wave 2.
|
||
3. **Dropdown widths match trigger** — covered in Wave 2.
|
||
|
||
### Bonus — public berth feed wired to replace NocoDB as source of truth
|
||
|
||
Triggered by user prompt "ensure we are properly wired up to replace
|
||
the NocoDB table as the source of truth for the berth map".
|
||
|
||
**State before audit:**
|
||
|
||
- API endpoints existed (`/api/public/berths`,
|
||
`/api/public/berths/[mooringNumber]`) — wiring fine.
|
||
- `src/lib/services/public-berths.ts` mapped the response shape to
|
||
NocoDB-verbatim keys.
|
||
- Tests passed (`tests/unit/services/public-berths.test.ts`).
|
||
- **Map data was empty: 0 rows in `berth_map_data` against 234 berths
|
||
total (117 per port).** Without polygons the website map literally
|
||
has no shapes to render.
|
||
|
||
**Action taken:**
|
||
|
||
- Ran `pnpm tsx scripts/import-berths-from-nocodb.ts --apply
|
||
--port-slug port-nimara` (after a clean dry-run). Result:
|
||
117 berths updated, 117 `berth_map_data` rows inserted.
|
||
- Spot-checked the public API: `GET /api/public/berths` returns the
|
||
correct shape with `Map Data` populated, byte-for-byte identical
|
||
to NocoDB for berth A1 (`path`, `x`, `y`, `transform`, `fontSize`).
|
||
|
||
**Field-parity gaps still present** (see Wave Bonus pending below).
|
||
|
||
### Misc UI polish
|
||
|
||
- **Berth Documents tab explainer** — added a one-paragraph header
|
||
explaining it's the spec PDF, not deal documents (with a pointer
|
||
to the Interests tab for prospect-linked docs).
|
||
File: `src/components/berths/berth-documents-tab.tsx`.
|
||
|
||
---
|
||
|
||
## 🟡 Pending — medium
|
||
|
||
### Wave 4: currency formatting platform-wide
|
||
|
||
- Build `<CurrencyInput>` shared component (formatted display, raw
|
||
number value). Replace raw `<Input type="number">` price spots in:
|
||
`berth-form.tsx` (price), `expense-form-dialog.tsx` (amount),
|
||
`invoices.tsx` (totals), client deal amounts on dossier / invoice.
|
||
- Currency selector dropdown on expense form (NocoDB has no expense
|
||
currency field, so source from a curated supported-currency list:
|
||
USD / EUR / GBP / CAD / AUD / CHF / JPY / …). Replace the free-text
|
||
3-letter input.
|
||
- Sweep for `${currency} ${amount}` string concatenations and replace
|
||
with `Intl.NumberFormat`.
|
||
|
||
### Wave 5: configurable enum infrastructure
|
||
|
||
We have a `system_settings` table with composite PK `(key, port_id)`
|
||
and an `<SettingsManager>` admin page. Add a "Vocabularies" admin tab
|
||
that exposes per-port vocabularies. Suggested keys grouped by domain:
|
||
|
||
- `interest_temperature_levels` — replaces the hardcoded "HOT" badge.
|
||
Pill is rendered in `src/components/interests/interest-card.tsx`.
|
||
- `berth_status_change_reasons` — list shown as quick-pick chips in
|
||
`<StatusChangeDialog>` (see `berth-detail-header.tsx`). Tied to the
|
||
prospect-picker concept (see Wave 7 below).
|
||
- `berth_tenure_types` — replaces the static
|
||
`'permanent' | 'fixed_term'` validator union. Berths column is
|
||
`text`, so any value can land at the DB layer.
|
||
- `expense_categories` — current hardcoded list at
|
||
`src/lib/constants.ts:EXPENSE_CATEGORIES`.
|
||
- `document_types` — current hardcoded list at
|
||
`src/lib/constants.ts:DOCUMENT_TYPES`.
|
||
- `interest_outcome_statuses` — already exist in schema enum, could
|
||
be overridable.
|
||
- `berth_side_pontoon_options` / `berth_cleat_types` /
|
||
`berth_bollard_types` / `berth_access_options` — currently
|
||
hardcoded to NocoDB values. Worth making editable once a non-Port-
|
||
Nimara port appears with different infrastructure.
|
||
|
||
**Open question (#1)**: see § Open Questions.
|
||
|
||
### Wave 6: notes unification — aggregate-on-read
|
||
|
||
User chose option 1 ("aggregate on read") from the brainstorm. The
|
||
`listForClientAggregated` pattern in `notes.service.ts` (lines
|
||
130–242) already unions a client's notes + interest notes + owned
|
||
yacht notes into a single feed with `source` metadata.
|
||
|
||
Symmetric extensions to add:
|
||
|
||
- `listForYachtAggregated` — yacht own notes + owner client notes
|
||
- linked interest notes.
|
||
- `listForCompanyAggregated` — company own notes + owned yacht notes
|
||
- linked interest notes.
|
||
- `listForResidentialClientAggregated` — residential client notes
|
||
- residential interest notes.
|
||
|
||
UI:
|
||
|
||
- `<NotesList entityType="…">` should render the source-label badge
|
||
(already implemented for clients — copy the pattern).
|
||
- Convert single-textarea spots to entry-list pattern: the
|
||
Companies overview tab has a `notes` textarea (from
|
||
`companies.notes` text column) AND a Notes tab with the threaded
|
||
`companyNotes` table. Drop the textarea in favor of the threaded
|
||
feed only. Same for residential interests.
|
||
- Note for the schema fix-it list: `companyNotes` is missing
|
||
`updatedAt`. Service substitutes `createdAt` to keep the read shape
|
||
uniform — see `notes.service.ts:566`. Fix when convenient.
|
||
|
||
### Wave 7: clients / yachts / companies misc
|
||
|
||
Done in this session:
|
||
|
||
- **Yacht flag** → CountryCombobox (Wave 1).
|
||
- **End Membership** → "Remove from company" (Wave 1).
|
||
- **Berth Documents tab** explainer paragraph.
|
||
|
||
Pending:
|
||
|
||
- **Status change modal — prospect picker**: when user changes berth
|
||
status to `under_offer` or `sold`, surface an interest/prospect
|
||
selector below the reason dropdown so the recorded reason can link
|
||
to a known deal. Tie into `interest_berths` so the link is
|
||
bidirectional. Depends on Wave 5
|
||
(`berth_status_change_reasons` vocabulary).
|
||
- **Documents tagged with company** show up in main `/documents` view
|
||
with company tag — verify after the documents overhaul (Wave 11.B).
|
||
|
||
### Wave 9 follow-up
|
||
|
||
- **HOT/WARM/COLD admin-config** — covered by Wave 5
|
||
(`interest_temperature_levels`).
|
||
- **Color-codes legend**: shipped as a popover. Optional polish: add
|
||
a one-time tooltip on first pageload so users discover it.
|
||
|
||
### Wave 10 follow-up
|
||
|
||
- **Photo upload picker bug**: Playwright captured a `[File chooser]`
|
||
modal when clicking "Upload photo," so the wiring works in headless
|
||
Chromium. User reported "doesn't open" on macOS — possibly a focus
|
||
/ window issue or a content-blocking extension. Need a real-machine
|
||
repro to diagnose. The hidden `<input type="file" ref={fileInputRef}>`
|
||
- `fileInputRef.current?.click()` wiring is at
|
||
`user-settings.tsx:247-258`.
|
||
- **Display name + first / last name fields** — current schema only
|
||
has `displayName`. Adding first/last requires a Drizzle migration on
|
||
`users` or `user_profiles` plus migration of existing data (split
|
||
on first space). **Open question (#3)**: see § Open Questions.
|
||
- **Notification preferences placement** — settings vs notifications
|
||
page. Today notification toggles live on the user-settings page; a
|
||
dedicated `/notifications/preferences` page also exists. **Open
|
||
question (#2)**: see § Open Questions.
|
||
|
||
### Wave Bonus follow-up — public berth feed field parity
|
||
|
||
Map data is now wired. Field gaps the website _might_ consume but we
|
||
don't expose:
|
||
|
||
| NocoDB field | Currently in PublicBerth? | DB has it? | Notes |
|
||
| ---------------------------- | ------------------------- | ---------------------------------- | ----------------------------------------------------------- |
|
||
| `Price` | ❌ | ✅ `berths.price` | Pricing-public is a policy decision. **Open question (#4)** |
|
||
| `Berth Approved` | ❌ | ✅ `berths.berth_approved` | Boolean. Often used to gate "Sold" display |
|
||
| `Water Depth` | ❌ | ✅ `berths.water_depth` | Sometimes shown in tooltip |
|
||
| `Width Is Minimum` | ❌ | ✅ `berths.width_is_minimum` | Modifier for "Width" display |
|
||
| `Water Depth Is Minimum` | ❌ | ✅ `berths.water_depth_is_minimum` | ditto |
|
||
| `Length (Metric)` | ❌ | ✅ `berths.length_m` | Derivable. Website may consume |
|
||
| `Width (Metric)` | ❌ | ✅ `berths.width_m` | ditto |
|
||
| `Draft (Metric)` | ❌ | ✅ `berths.draft_m` | ditto |
|
||
| `Water Depth (Metric)` | ❌ | ✅ `berths.water_depth_m` | ditto |
|
||
| `Nominal Boat Size (Metric)` | ❌ | ✅ `berths.nominal_boat_size_m` | ditto |
|
||
| `CreatedAt` / `UpdatedAt` | ❌ | ✅ timestamps | Cache invalidation hints |
|
||
| `Interests` (count) | ❌ | derivable | Probably internal-only |
|
||
| `Interested Parties` (count) | ❌ | derivable | Probably internal-only |
|
||
|
||
**Plan once questions are answered:** Add the chosen fields to
|
||
`PublicBerth` interface in `src/lib/services/public-berths.ts`, the
|
||
`toPublicBerth()` mapper, and the test fixtures. Trivial; gated only
|
||
by which fields the website actually uses.
|
||
|
||
**Other public-feed concerns to flag**:
|
||
|
||
- **No archive flag**: when a berth is retired the public feed will
|
||
still serve it. Need a `berths.archived_at` column + filter on the
|
||
route. Plan §4.5 hinted at this. Not urgent.
|
||
- **CRM-edit drift vs re-imports**: now that reps can edit berth
|
||
fields (Wave 3), running the import script will skip-edited those
|
||
rows (`updated_at > last_imported_at`) — that's the right design,
|
||
but it means once cutover happens the website **must** call CRM
|
||
`/api/public/berths`, never NocoDB. Coordinate this in the website
|
||
repo. Useful guard already exists: `/api/public/health`.
|
||
- **Cache TTL: 5 min**: when a CRM rep marks a berth `sold`, the
|
||
public website serves "Available" for up to 5 minutes due to
|
||
`s-maxage=300`. Acceptable for marketing; bump if needed.
|
||
- **Health endpoint shape**: `/api/public/health` currently returns
|
||
`{status, timestamp}` but `CLAUDE.md` claims `{env, appUrl}`. One
|
||
of them is stale; the website may expect either shape. Not blocking
|
||
but worth aligning.
|
||
|
||
---
|
||
|
||
## 🔴 Pending — large (group-discussion items, Wave 11)
|
||
|
||
### A. Manual client form expansion
|
||
|
||
User wants "New Client" to support assigning yachts / companies /
|
||
berths inline (without leaving the form), plus a mini-recommender for
|
||
picking a berth at create time.
|
||
|
||
Scope:
|
||
|
||
- "Existing yacht / new yacht" picker.
|
||
- "Existing company / new company" picker.
|
||
- "Open an interest with this client" affordance that wires through
|
||
`interest_berths` and the recommender.
|
||
- Make sure all standard client modal fields (nationality / source /
|
||
preferred contact / timezone / tags) remain present.
|
||
|
||
Multi-component composition with a lot of cross-entity plumbing.
|
||
Estimate fully before starting (likely 2–3 days).
|
||
|
||
### B. Documents section overhaul
|
||
|
||
User wants:
|
||
|
||
- Folders (create / delete / nested).
|
||
- Sort + filter (by date, type, owner).
|
||
- Wider file-type allowlist (PDF + Office + image is current; expand).
|
||
- "Documents in progress" filter (contracts / EOIs awaiting signature,
|
||
things uploaded but unparsed).
|
||
- Drop or rename the "Signature-based only" pill — confusing copy.
|
||
- "Expired" tab admin-configurable visibility.
|
||
- Type-filter dropdown reflects actual types in use (vs the full
|
||
hardcoded list).
|
||
|
||
Refactor of `documents.service.ts` plus a new folders schema
|
||
(`document_folders` table with port-scoped tree).
|
||
|
||
### C. Reports system
|
||
|
||
User asked for:
|
||
|
||
- Defined report types (Pipeline summary / Revenue / Activity log /
|
||
Berth occupancy) with documented data shape per type.
|
||
- Test fixtures for visual QA.
|
||
- Admin "report templates" with field-level checkboxes letting an
|
||
admin compose a custom report shape (toggles for each available
|
||
data field).
|
||
|
||
Infra exists (`/api/v1/reports`) but templates are stubs. A proper
|
||
templating system + per-template field selection adds a few days.
|
||
|
||
### D. Receipts inline in expense PDF
|
||
|
||
User confirmed: image receipts render inline beneath each expense row,
|
||
**and** PDF receipts also render inline (one page each). pdfme
|
||
(already used for EOI) handles both — inline images via the renderer,
|
||
PDF pages via `pdf-lib.copyPages`. Depends on Wave 8 expense form work.
|
||
|
||
### E. Country / Nationality split on Client form
|
||
|
||
Client schema has only `nationalityIso`. User wants:
|
||
|
||
- New `country_iso` column for _country of residence_ (visible
|
||
/ primary).
|
||
- Keep `nationality_iso` as an _optional_ secondary field.
|
||
|
||
Requires:
|
||
|
||
- Drizzle migration (`alter table clients add column country_iso text`).
|
||
- Migrate existing data: copy `nationality_iso → country_iso` for
|
||
every client (current value is more often country of residence in
|
||
practice).
|
||
- Update API validators (`clients.ts`).
|
||
- Update client form UI: primary "Country" CountryCombobox, secondary
|
||
collapsible "Nationality" row.
|
||
- Same for residential clients (parallel schema).
|
||
|
||
### F. Inquiry triage (legacy spec carryover)
|
||
|
||
Per project memory and the "deferred" list at the top of
|
||
`today-2026-05-08.md`: inquiry triage was explicitly deferred. Tied
|
||
into the inquiry routing settings (`inquiry_notification_recipients`,
|
||
`inquiry_contact_email`, `residential_notification_recipients` —
|
||
already in `system_settings`). Pick this back up when ready to
|
||
auto-classify website inquiries.
|
||
|
||
### G. Per-port email branding
|
||
|
||
Also in the deferred list. Templates and settings keys exist
|
||
(per memory note); the admin UI for editing per-port email branding
|
||
overrides remains.
|
||
|
||
---
|
||
|
||
## ✅ Decisions log — 2026-05-09
|
||
|
||
All 11 open questions answered. Implementation implications inline.
|
||
|
||
1. **Vocabularies admin layout (Wave 5)** → **New `/admin/vocabularies`
|
||
page, grouped by domain, admin-only.** User considered exposing to
|
||
non-admins (since reps use them daily) but settled on admin-only as
|
||
the safer default for now. Implementation: new top-level admin
|
||
route + page, reuse `system_settings` `(key, port_id)` composite
|
||
PK. Each vocabulary key gets its own card section (interest temps,
|
||
status-change reasons, tenure types, expense categories, document
|
||
types, etc.).
|
||
2. **Notification preferences placement (Wave 10)** → **Collapse to
|
||
user-settings only.** Keep `/notifications/preferences` as a
|
||
server-side redirect to the user-settings notifications panel for
|
||
back-compat links.
|
||
3. **Display name vs first/last (Wave 10)** → **Add `first_name` and
|
||
`last_name` columns.** Don't worry about migrations during dev (we
|
||
can iterate freely), but write the migration carefully so it
|
||
applies cleanly when we eventually deploy. Keep `display_name` as
|
||
a derived/optional override.
|
||
4. **Public-feed `Price` exposure (Bonus)** → **No — keep Price
|
||
internal.** Don't add to PublicBerth payload.
|
||
5. **Public-feed remaining fields (Bonus)** → **Yes, add all.** Add
|
||
Berth Approved, Water Depth, Width Is Minimum, Water Depth Is
|
||
Minimum, all four metric variants, plus CreatedAt/UpdatedAt to
|
||
PublicBerth + mapper + tests. User noted "not sure if we'll use
|
||
all of them but best to keep them in" — verbatim NocoDB parity.
|
||
6. **Website cutover plan (Bonus)** → **Double-write transition
|
||
window.** Keep both feeds live, write to both for the transition
|
||
period, then decommission NocoDB. Coordinate with website repo
|
||
(`CRM_PUBLIC_URL`).
|
||
7. **Status-change modal → prospect link (Wave 7)** → **Force
|
||
interest pick + auto-create primary `interest_berths` row.**
|
||
When status moves to `under_offer` or `sold`, the modal surfaces
|
||
an interest selector below the reason dropdown. Picking an
|
||
interest creates an `interest_berths` row with `is_primary=true`
|
||
if one doesn't already exist for that pair. Depends on Wave 5
|
||
`berth_status_change_reasons` vocabulary.
|
||
8. **Trip label on expenses (Wave 8)** → **Combobox: free-text on
|
||
first entry, dropdown of existing labels on subsequent entries.**
|
||
No new entity. Source the dropdown from
|
||
`SELECT DISTINCT trip_label FROM expenses WHERE port_id=?`
|
||
ordered by recency. UI is a `<Combobox>` with "Create
|
||
'<typed value>'" affordance.
|
||
9. **Documents folders (Wave 11.B)** → **Per-port, unlimited
|
||
nesting depth — but render carefully.** User wants flexibility;
|
||
we owe a UI design that handles deep trees gracefully (likely
|
||
collapsed-by-default with a breadcrumb header inside the folder
|
||
view rather than always-expanded sidebar tree).
|
||
10. **Berth Documents tab (Wave 1 carryover)** → **Split into two
|
||
tabs: "Spec" (versioned spec PDF) and "Deal Documents"
|
||
(aggregated EOIs/contracts from interests on this berth).**
|
||
Permission scoping: deal docs only show entries the viewer can
|
||
already see via the linked interest.
|
||
11. **Mooring type re-import** → ✅ **Verified.** All 117 records
|
||
have `mooring_type` populated post-import (e.g. "Side Pier / Med
|
||
Mooring"). No action needed.
|
||
|
||
---
|
||
|
||
## File-pointer cheat sheet
|
||
|
||
### Berth-related
|
||
|
||
| Concern | File(s) |
|
||
| ---------------------------------- | ---------------------------------------------------- |
|
||
| Canonical berth enums | `src/lib/constants.ts` (search `BERTH_`) |
|
||
| Berth list ordering SQL | `src/lib/services/berths.service.ts:69-72` |
|
||
| Berth detail inline edit | `src/components/berths/berth-tabs.tsx` |
|
||
| Berth modal form | `src/components/berths/berth-form.tsx` |
|
||
| Berth area filter | `src/components/berths/berth-filters.tsx` |
|
||
| Berth detail header / status modal | `src/components/berths/berth-detail-header.tsx:90` |
|
||
| Berth Documents tab | `src/components/berths/berth-documents-tab.tsx` |
|
||
| Berth list query + sort | `src/lib/services/berths.service.ts:25-140` |
|
||
| Berth import script | `scripts/import-berths-from-nocodb.ts` |
|
||
| Berth import service / parsers | `src/lib/services/berth-import.ts` |
|
||
| Public berth API route | `src/app/api/public/berths/route.ts` |
|
||
| Public berth single route | `src/app/api/public/berths/[mooringNumber]/route.ts` |
|
||
| Public berth mapper | `src/lib/services/public-berths.ts` |
|
||
| Public berth tests | `tests/unit/services/public-berths.test.ts` |
|
||
| Berth seed snapshot | `src/lib/db/seed-data/berths.json` |
|
||
| Berth schema | `src/lib/db/schema/berths.ts` (incl. `berthMapData`) |
|
||
|
||
### Other domains
|
||
|
||
| Concern | File(s) |
|
||
| --------------------------------- | -------------------------------------------------------------------------------------- |
|
||
| Interest stage colors / legend | `src/components/interests/stage-legend.tsx` + `src/lib/constants.ts:STAGE_DOT` |
|
||
| Mobile kanban toggle / fallback | `src/components/interests/interest-list.tsx` |
|
||
| Country / timezone autoset | `src/components/clients/client-form.tsx` + `src/components/settings/user-settings.tsx` |
|
||
| Phone input | `src/components/shared/phone-input.tsx` |
|
||
| Country combobox + scroll patch | `src/components/shared/country-combobox.tsx` + `src/components/ui/command.tsx` |
|
||
| Sidebar Umami gate | `src/components/layout/sidebar.tsx` (search `umamiRequired`) |
|
||
| Mobile More-sheet | `src/components/layout/mobile/more-sheet.tsx` |
|
||
| Notes service (aggregate-on-read) | `src/lib/services/notes.service.ts:130-242` |
|
||
| Notes UI | `src/components/shared/notes-list.tsx` |
|
||
| Settings manager (admin) | `src/components/admin/settings/settings-manager.tsx` |
|
||
| User settings page | `src/components/settings/user-settings.tsx` |
|
||
| Status change dialog | `src/components/berths/berth-detail-header.tsx:90` |
|
||
| Companies members tab | `src/components/companies/company-members-tab.tsx` |
|
||
| Yacht form | `src/components/yachts/yacht-form.tsx` |
|
||
| Client form | `src/components/clients/client-form.tsx` |
|
||
|
||
### Infrastructure
|
||
|
||
| Concern | File(s) |
|
||
| ------------------------------------------- | --------------------------------------------- |
|
||
| Drizzle config / migrations | `drizzle.config.ts`, `src/lib/db/migrations/` |
|
||
| `system_settings` table | `src/lib/db/schema/system.ts:128-147` |
|
||
| Permissions / `withAuth` / `withPermission` | `src/lib/api/helpers.ts` |
|
||
| Body parsing (always use `parseBody`) | `src/lib/api/route-helpers.ts` |
|
||
| Storage backend abstraction | `src/lib/storage/` |
|
||
| Logger (pino) | `src/lib/logger.ts` |
|
||
|
||
---
|
||
|
||
## Resuming in a fresh session
|
||
|
||
When you open a new chat, paste this **prompt** to pick up where this
|
||
session ended:
|
||
|
||
```
|
||
I'm resuming the 2026-05-08 visual audit. Read
|
||
docs/AUDIT-FOLLOWUPS.md first — it has every completed item, every
|
||
pending item, and every open question. Then:
|
||
|
||
1. Skim the "Quick status snapshot" table at the top so you know
|
||
what's done.
|
||
2. Read the "Open questions for the user" list and ask me question
|
||
#N where N is whichever I'll answer first this turn.
|
||
3. Wait for my answers; don't start implementing until I confirm.
|
||
|
||
Key invariants:
|
||
- Notes unification model: aggregate-on-read.
|
||
- Berth dropdown values: NocoDB SingleSelect canon, sourced from
|
||
src/lib/constants.ts (BERTH_*_OPTIONS / _TYPES).
|
||
- Power Capacity & Voltage stay numeric inputs; Bow Facing is a
|
||
constrained 4-value dropdown despite being SingleLineText in
|
||
NocoDB.
|
||
- linkedUnit on EditableSpec auto-fills the metric column on save.
|
||
- system_settings (key, port_id) is the configuration pattern.
|
||
- NocoDB MCP is connected via ~/.claude.json — Berths schema +
|
||
records can be pulled live.
|
||
- Public berth feed (/api/public/berths) now serves Map Data; 117
|
||
berth_map_data rows backfilled in this session.
|
||
- Tests: 1185/1185 passing; tsc clean.
|
||
|
||
The git working tree has 23 modified files + 2 new (no commits yet).
|
||
Don't commit anything until I say so.
|
||
```
|
||
|
||
### Resume commands (cheat sheet)
|
||
|
||
```bash
|
||
cd /Users/matt/Repos/new-pn-crm
|
||
pnpm dev # Turbopack dev (~1s boot)
|
||
|
||
# Tests
|
||
pnpm exec vitest run # Unit + integration (~7s)
|
||
pnpm exec tsc --noEmit # Type check
|
||
pnpm exec playwright test --project=smoke # Smoke (~10min)
|
||
|
||
# NocoDB import (for new berth pulls)
|
||
pnpm tsx scripts/import-berths-from-nocodb.ts --dry-run --port-slug port-nimara
|
||
pnpm tsx scripts/import-berths-from-nocodb.ts --apply --port-slug port-nimara
|
||
|
||
# DB inspect
|
||
PGPASSWORD=changeme psql -h localhost -p 5434 -U crm -d port_nimara_crm
|
||
|
||
# Public-feed sanity check
|
||
curl -s http://localhost:3000/api/public/berths | jq '.pageInfo'
|
||
curl -s http://localhost:3000/api/public/berths/A1 | jq '.'
|
||
```
|
||
|
||
### Verification checklist before committing this session's work
|
||
|
||
- [ ] `pnpm exec vitest run` — 1185/1185 pass.
|
||
- [ ] `pnpm exec tsc --noEmit` — clean.
|
||
- [ ] `pnpm exec playwright test --project=smoke` — passes.
|
||
- [ ] Manual: open `/port-nimara/berths`, confirm sort is A1, A2,
|
||
A3 … A10, A11 (not lex order).
|
||
- [ ] Manual: open a berth detail page, confirm the dock chip reads
|
||
e.g. "A Dock", and the Bow Facing / Side Pontoon / Cleat fields
|
||
render as `<Select>` not `<Input>`.
|
||
- [ ] Manual: pick a country in the user-settings page and confirm
|
||
timezone auto-fills if empty; also confirm the country dropdown
|
||
scrolls with mousewheel on macOS.
|
||
- [ ] Manual: check the mobile More-sheet has no "Inbox" entry, and
|
||
"Notification preferences" deep-links to the correct page.
|
||
- [ ] Manual: open `/api/public/berths` in the browser and search for
|
||
`Map Data` in the response — every row should have it.
|
||
|
||
---
|
||
|
||
## Misc tracking notes
|
||
|
||
- **Backups**: `~/.claude.json.bak.<timestamp>` exists from when the
|
||
NocoDB MCP was added. Delete after a session or two if everything's
|
||
stable.
|
||
- **Turbopack flip**: `next.config.ts` has no custom `webpack()` hook
|
||
so reverting `pnpm dev` to plain `next dev` is one line if needed.
|
||
Default is now `--turbopack`.
|
||
- **Database integrity follow-ups** (separate audit, dated 20:42):
|
||
11 findings (5 critical / 6 important). Logged in
|
||
`.remember/today-2026-05-08.md`. Cross-cuts the work here in two
|
||
spots: (1) `upsertInterestBerth` race could affect the berth
|
||
recommender once it's wired into the manual client form (Wave 11.A);
|
||
(2) `system_settings` `ON DELETE NO ACTION` will need addressing
|
||
before any port-deletion flow ships.
|