Files
pn-new-crm/docs/AUDIT-FOLLOWUPS.md
Matt 1bfed587b5 docs: website cutover runbook + post-execution status snapshot
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>
2026-05-09 18:38:46 +02:00

38 KiB
Raw Blame History

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 run1187/1187 pass. TS check: pnpm exec tsc --noEmitclean. 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 (\dd). 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 hiddenColumnPicker 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 triggerCountryCombobox 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 (AE). 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-conversionEditableSpec 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 130242) 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 23 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 ''" 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

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)

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.