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>
38 KiB
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.jsonundermcpServers."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 forBERTH_*_OPTIONS/_TYPES). Power Capacity and Voltage stay numeric inputs because NocoDB stores them asNumber. Bow Facing isSingleLineTextin NocoDB but constrained to the 4 cardinal values in the CRM dropdown for UX. - Dual-unit fields auto-cross-fill via
linkedUnitonEditableSpecinsrc/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_settingswith composite PK(key, port_id)and<SettingsManager>admin page. Use the same pattern for the new vocabularies. - Turbopack dev:
pnpm devrunsnext dev --turbopack. Cold compiles ~1s boot, ~3s per route. No webpack hooks innext.config.tsso flipping back is one line if needed.
✅ Completed this session
Wave 1 — small confident fixes
- 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. - 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. - 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. {Letter} Dockchip on the berth detail header replaces the bare "A" / "B" text. Colored bymooringLetterDot(). File:src/components/berths/berth-detail-header.tsx.- cmdk wheel-scroll bug — Radix Popover swallowed wheel events on
the country dropdown for macOS users. Added
onWheeltranslator onCommandList+overscroll-contain. Lights up country pickers in Companies, Residential Clients, Clients, Yachts. File:src/components/ui/command.tsx. - Mobile "Columns" button hidden —
ColumnPickeris nowhidden sm:inline-flex. Mobile renders cards (no columns to toggle). File:src/components/shared/column-picker.tsx. - Mobile kanban toggle hidden + auto-fallback — Interest list
hides the table-vs-kanban toggle on small viewports and snaps
viewModeback to'table'if the user's persisted choice was'board'. File:src/components/interests/interest-list.tsx. - Inbox entry removed from mobile More-sheet — email/IMAP feature
is deferred (
sidebar.tsxcalls this out); the More-sheet entry was a dead link. - 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. - "Other" comm-channel UX hint — when a contact's channel is
'other', the inlineLabelfield switches its label/placeholder to "Specify" / "e.g. Telegram, Signal". File:src/components/clients/client-form.tsx:289-302. - End Membership wording — renamed to "Remove from company" in
the company members tab dropdown.
File:
src/components/companies/company-members-tab.tsx:249. - Berth area filter → letter dropdown — was free-text; now a
<Select>constrained toA / B / C / D / E. Label changed to "Dock" to match how the user refers to it. File:src/components/berths/berth-filters.tsx. - 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
- cmdk wheel-scroll — covered in Wave 1 (single shared command).
- 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 forprimaryTimezoneFor). - Browser-detected timezone fallback in user settings: timezone
pre-populates from
Intl.DateTimeFormat().resolvedOptions().timeZoneon first load (was empty before). File:src/components/settings/user-settings.tsx. - Country → timezone auto-fill also fires in user settings when the country changes with no zone set.
- Dropdown widths match trigger —
CountryComboboxandTimezoneComboboxpopover content set tow-[var(--radix-popper-anchor-width)]with sensiblemin-w-*floors so wide triggers get wide popovers. - 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 existingnationality_isovalues. See § Wave 11 / pending — large.
Wave 3 — berth field overhaul (NocoDB enums)
- 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
Numberfields (not enums). Bow Facing isSingleLineText(we still use a 4-value dropdown for UX). BERTH_BOW_FACING_OPTIONSadded tosrc/lib/constants.tsalongside the existingBERTH_*_OPTIONSconstants.toSelectOptions()helper added tosrc/lib/constants.tsfor mapping readonly tuples → shadcn<Select>{value,label}objects.- 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. - Inline-edit
EditableSpecinberth-tabs.tsxnow supportsselectOptions: readonly string[]to render a<Select>variant. - Dimensional auto-conversion —
EditableSpecgained alinkedUnit: { field, multiplier }prop. Saving the imperial value also patches the metric column (× 0.3048). Applied to length, width, draft, nominal boat size, water depth. - Nominal boat size editable — was read-only
<SpecRow>; now an<EditableSpec numeric linkedUnit>so editing ft auto-fills m. - 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
- 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_DOTautomatically. File:src/components/interests/stage-legend.tsx. - Mobile kanban hidden — see Wave 1.
- Notifications nav 404 fixed — More-sheet entry pointed at
/notificationswhich had nopage.tsx. Now points at/notifications/preferencesand is labeled "Notification preferences" — real notifications come via the topbar bell. File:src/components/layout/mobile/more-sheet.tsx.
Wave 10 — settings polish
- 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. - Timezone auto-detect — covered in Wave 2.
- 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.tsmapped 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_dataagainst 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, 117berth_map_datarows inserted. - Spot-checked the public API:
GET /api/public/berthsreturns the correct shape withMap Datapopulated, 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 withIntl.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 insrc/components/interests/interest-card.tsx.berth_status_change_reasons— list shown as quick-pick chips in<StatusChangeDialog>(seeberth-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 istext, so any value can land at the DB layer.expense_categories— current hardcoded list atsrc/lib/constants.ts:EXPENSE_CATEGORIES.document_types— current hardcoded list atsrc/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
notestextarea (fromcompanies.notestext column) AND a Notes tab with the threadedcompanyNotestable. Drop the textarea in favor of the threaded feed only. Same for residential interests. - Note for the schema fix-it list:
companyNotesis missingupdatedAt. Service substitutescreatedAtto keep the read shape uniform — seenotes.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_offerorsold, surface an interest/prospect selector below the reason dropdown so the recorded reason can link to a known deal. Tie intointerest_berthsso the link is bidirectional. Depends on Wave 5 (berth_status_change_reasonsvocabulary). - Documents tagged with company show up in main
/documentsview 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 atuser-settings.tsx:247-258.
- Display name + first / last name fields — current schema only
has
displayName. Adding first/last requires a Drizzle migration onusersoruser_profilesplus 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/preferencespage 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_atcolumn + 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 tos-maxage=300. Acceptable for marketing; bump if needed. - Health endpoint shape:
/api/public/healthcurrently returns{status, timestamp}butCLAUDE.mdclaims{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_berthsand 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_isocolumn for country of residence (visible / primary). - Keep
nationality_isoas an optional secondary field.
Requires:
- Drizzle migration (
alter table clients add column country_iso text). - Migrate existing data: copy
nationality_iso → country_isofor 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.
- Vocabularies admin layout (Wave 5) → New
/admin/vocabulariespage, 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, reusesystem_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.). - Notification preferences placement (Wave 10) → Collapse to
user-settings only. Keep
/notifications/preferencesas a server-side redirect to the user-settings notifications panel for back-compat links. - Display name vs first/last (Wave 10) → Add
first_nameandlast_namecolumns. Don't worry about migrations during dev (we can iterate freely), but write the migration carefully so it applies cleanly when we eventually deploy. Keepdisplay_nameas a derived/optional override. - Public-feed
Priceexposure (Bonus) → No — keep Price internal. Don't add to PublicBerth payload. - 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.
- 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). - Status-change modal → prospect link (Wave 7) → Force
interest pick + auto-create primary
interest_berthsrow. When status moves tounder_offerorsold, the modal surfaces an interest selector below the reason dropdown. Picking an interest creates aninterest_berthsrow withis_primary=trueif one doesn't already exist for that pair. Depends on Wave 5berth_status_change_reasonsvocabulary. - 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. - 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).
- 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.
- Mooring type re-import → ✅ Verified. All 117 records
have
mooring_typepopulated 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)
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/berthsin the browser and search forMap Datain 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.tshas no customwebpack()hook so revertingpnpm devto plainnext devis 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)upsertInterestBerthrace could affect the berth recommender once it's wired into the manual client form (Wave 11.A); (2)system_settingsON DELETE NO ACTIONwill need addressing before any port-deletion flow ships.