Files
pn-new-crm/docs/AUDIT-FOLLOWUPS.md
Matt 502455ac04 chore(format): apply prettier auto-formatting
Pre-commit hook reformatted these files after the substantive commits.
No semantic changes — markdown table alignment, list indentation, and
emphasis style normalisation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 04:11:54 +02:00

37 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-08 23:00

Wave Topic Status
1 Small confident fixes Done
2 Country dropdown unification + cmdk scroll Done (country/nationality split deferred)
3 Berth field overhaul (NocoDB enums) Done
4 Currency platform-wide 🔴 Not started
5 Configurable enums (admin Vocabularies) 🔴 Not started
6 Notes unification (aggregate-on-read) 🔴 Not started
7 Clients / yachts / companies misc 🟡 Partial (small bits done; large items deferred)
8 Expenses revisit 🔴 Not started
9 Interests + notifications Done
10 Settings polish Done (small bits) — schema-blocked items deferred
11 DEFERRED — group-discussion items 🟣 Awaiting alignment
Bonus Public berth feed (website map) Map data backfilled (117 rows). Field-parity gaps remain — see § Bonus.

Test status: pnpm exec vitest run1185/1185 pass. TS check: pnpm exec tsc --noEmitclean. Git: 23 files modified, 2 new files (no commits yet).


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.


Open questions for the user

These are the questions to address in the next session before further implementation. They're ordered by what unblocks the most work.

  1. Vocabularies admin layout (Wave 5) — single new /admin/vocabularies page or nested under existing admin tabs? Recommendation: one new page, grouped by domain. Easier to discover, cleaner schema for the editor.
  2. Notification preferences placement (Wave 10) — keep both the user-settings notifications panel and /notifications/preferences, or collapse to one? Recommendation: collapse to user-settings only, since that's where every other personal preference lives. Keep the dedicated route as a redirect for back-compat links.
  3. Display vs first / last name (Wave 10) — worth the migration, or keep single-field display name? Recommendation: keep single display name unless you'll be using first/last for invoicing, greetings, or DocSign field-merging. A migration is reversible but never free.
  4. Public-feed Price exposure (Bonus) — should Price be in the public website feed? NocoDB exposed it. Recommendation: yes if it's already shown publicly today; no if the website redacts it. Easiest tell: open the public /berths page on the marketing site and look for the price.
  5. Public-feed remaining fields (Bonus) — add Berth Approved, Water Depth, Width Is Minimum, Water Depth Is Minimum, and metric variants to the PublicBerth payload? Recommendation: yes to all for verbatim NocoDB parity. Trivial code change once confirmed.
  6. Website cutover plan (Bonus) — is the website already pointed at CRM /api/public/berths, or still hitting NocoDB? Need a coordinated DNS-style cutover (env var CRM_PUBLIC_URL per CLAUDE.md). After cutover, do we (a) decommission the NocoDB Berths table, (b) keep it read-only as a cold backup, or (c) double-write for a transition window? Recommendation: (b) for 30 days, then (a).
  7. Status change reason → prospect link flow (Wave 7) — confirm the desired shape. Does picking "Sold to existing prospect" force the user to pick an interest from the list, and does it auto- create an interest_berths.is_primary=true row if the chosen interest isn't already linked to that berth?
  8. Trip label on expenses (Wave 8) — define-events flow vs free text with matched-receipts-on-PDF heuristic? Either is fine; pick based on how reps actually batch trips.
  9. Documents folders (Wave 11.B) — nest depth limit? Per-port folders, or global per company? Recommendation: per-port, nested up to 3 levels (matches typical document-management depth and keeps the tree UI sane).
  10. Berth Documents tab function (Wave 1 carryover) — confirm the explainer paragraph is the right framing, or do you want a "Linked deal documents" section that aggregates EOIs / contracts / etc. from interests on this berth? Recommendation: keep current scope (versioned spec PDF only). Aggregating deal docs is a bigger feature with permission concerns.
  11. Mooring type re-import — added BERTH_MOORING_TYPES to the UI. The seed JSON snapshot may not have included Mooring Type for every berth (NocoDB does). Confirmation that the live import we ran today populated mooring_type for all 117 records. (Spot- checked: yes — the SQL traces show mooring_type = "Side Pier / Med Mooring" etc.)

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.