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