# 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 (decisions locked) | 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 | 🟡 Ready — design locked; start with `` | | 5 | Configurable enums (admin Vocabularies) | 🟡 Ready — `/admin/vocabularies`, admin-only | | 6 | Notes unification (aggregate-on-read) | 🟡 Ready — extend pattern to yachts/companies/residential | | 7 | Clients / yachts / companies misc | 🟡 Partial (status-link flow ready; client form expansion still large) | | 8 | Expenses revisit | 🟡 Ready — combobox trip label (free text → autocomplete) | | 9 | Interests + notifications | ✅ Done | | 10 | Settings polish | 🟡 Ready — first/last name + collapse notif prefs | | 11 | DEFERRED — group-discussion items | 🟡 Most items now decided; client-form expansion + reports still large | | **Bonus** | **Public berth feed (website map)** | 🟡 Add parity fields (no Price); double-write cutover plan | Test status: `pnpm exec vitest run` → **1185/1185 pass**. TS check: `pnpm exec tsc --noEmit` → **clean**. 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 `` admin page. Use the same pattern for the new vocabularies. - **Turbopack dev**: `pnpm dev` runs `next dev --turbopack`. Cold compiles ~1s boot, ~3s per route. No webpack hooks in `next.config.ts` so flipping back is one line if needed. --- ## ✅ Completed this session ### Wave 1 — small confident fixes 1. **Berth list ordering bug** — `\d+$` regex in the Drizzle SQL template was being eaten by JS string literal escape rules (`\d` → `d`). Fixed by switching to `[0-9]+$` POSIX class. File: `src/lib/services/berths.service.ts:69-72`. 2. **Dashboard KPI grid removed** — "Total Clients / Active Interests / Pipeline Value / Occupancy Rate" deleted. The four chart widgets below (pipeline funnel, occupancy timeline, revenue breakdown, lead source) and the activity feed remain. File: `src/components/dashboard/dashboard-shell.tsx`. 3. **Per-dock color stripe on mobile berth cards** — was the _status_ color, which made every same-dock berth different. Now uses `mooringLetterDot()` so the stripe groups by dock letter; status conveyed by the existing pill below. File: `src/components/berths/berth-card.tsx`. 4. **`{Letter} Dock` chip** on the berth detail header replaces the bare "A" / "B" text. Colored by `mooringLetterDot()`. File: `src/components/berths/berth-detail-header.tsx`. 5. **cmdk wheel-scroll bug** — Radix Popover swallowed wheel events on the country dropdown for macOS users. Added `onWheel` translator on `CommandList` + `overscroll-contain`. Lights up country pickers in Companies, Residential Clients, Clients, Yachts. File: `src/components/ui/command.tsx`. 6. **Mobile "Columns" button hidden** — `ColumnPicker` is now `hidden sm:inline-flex`. Mobile renders cards (no columns to toggle). File: `src/components/shared/column-picker.tsx`. 7. **Mobile kanban toggle hidden + auto-fallback** — Interest list hides the table-vs-kanban toggle on small viewports and snaps `viewMode` back to `'table'` if the user's persisted choice was `'board'`. File: `src/components/interests/interest-list.tsx`. 8. **Inbox entry removed from mobile More-sheet** — email/IMAP feature is deferred (`sidebar.tsx` calls this out); the More-sheet entry was a dead link. 9. **Website Analytics conditional** — desktop sidebar Insights section AND mobile MoreSheet hide the Website Analytics nav when Umami isn't configured for the port. Reuses `useUmamiActive()`. Files: `src/components/layout/sidebar.tsx`, `src/components/layout/mobile/more-sheet.tsx`. 10. **"Other" comm-channel UX hint** — when a contact's channel is `'other'`, the inline `Label` field switches its label/placeholder to "Specify" / "e.g. Telegram, Signal". File: `src/components/clients/client-form.tsx:289-302`. 11. **End Membership wording** — renamed to "Remove from company" in the company members tab dropdown. File: `src/components/companies/company-members-tab.tsx:249`. 12. **Berth area filter → letter dropdown** — was free-text; now a `` `{value,label}` objects. 4. **All berth dropdown fields → `` variant. 6. **Dimensional auto-conversion** — `EditableSpec` gained a `linkedUnit: { field, multiplier }` prop. Saving the imperial value also patches the metric column (× 0.3048). Applied to length, width, draft, nominal boat size, water depth. 7. **Nominal boat size editable** — was read-only ``; now an `` 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 `` (country flag dropdown + AsYouType formatter) instead of a plain ``. 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 `` shared component (formatted display, raw number value). Replace raw `` 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 `` 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 `` (see `berth-detail-header.tsx`). Tied to the prospect-picker concept (see Wave 7 below). - `berth_tenure_types` — replaces the static `'permanent' | 'fixed_term'` validator union. Berths column is `text`, so any value can land at the DB layer. - `expense_categories` — current hardcoded list at `src/lib/constants.ts:EXPENSE_CATEGORIES`. - `document_types` — current hardcoded list at `src/lib/constants.ts:DOCUMENT_TYPES`. - `interest_outcome_statuses` — already exist in schema enum, could be overridable. - `berth_side_pontoon_options` / `berth_cleat_types` / `berth_bollard_types` / `berth_access_options` — currently hardcoded to NocoDB values. Worth making editable once a non-Port- Nimara port appears with different infrastructure. **Open question (#1)**: see § Open Questions. ### Wave 6: notes unification — aggregate-on-read User chose option 1 ("aggregate on read") from the brainstorm. The `listForClientAggregated` pattern in `notes.service.ts` (lines 130–242) already unions a client's notes + interest notes + owned yacht notes into a single feed with `source` metadata. Symmetric extensions to add: - `listForYachtAggregated` — yacht own notes + owner client notes - linked interest notes. - `listForCompanyAggregated` — company own notes + owned yacht notes - linked interest notes. - `listForResidentialClientAggregated` — residential client notes - residential interest notes. UI: - `` 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 `` - `fileInputRef.current?.click()` wiring is at `user-settings.tsx:247-258`. - **Display name + first / last name fields** — current schema only has `displayName`. Adding first/last requires a Drizzle migration on `users` or `user_profiles` plus migration of existing data (split on first space). **Open question (#3)**: see § Open Questions. - **Notification preferences placement** — settings vs notifications page. Today notification toggles live on the user-settings page; a dedicated `/notifications/preferences` page also exists. **Open question (#2)**: see § Open Questions. ### Wave Bonus follow-up — public berth feed field parity Map data is now wired. Field gaps the website _might_ consume but we don't expose: | NocoDB field | Currently in PublicBerth? | DB has it? | Notes | | ---------------------------- | ------------------------- | ---------------------------------- | ----------------------------------------------------------- | | `Price` | ❌ | ✅ `berths.price` | Pricing-public is a policy decision. **Open question (#4)** | | `Berth Approved` | ❌ | ✅ `berths.berth_approved` | Boolean. Often used to gate "Sold" display | | `Water Depth` | ❌ | ✅ `berths.water_depth` | Sometimes shown in tooltip | | `Width Is Minimum` | ❌ | ✅ `berths.width_is_minimum` | Modifier for "Width" display | | `Water Depth Is Minimum` | ❌ | ✅ `berths.water_depth_is_minimum` | ditto | | `Length (Metric)` | ❌ | ✅ `berths.length_m` | Derivable. Website may consume | | `Width (Metric)` | ❌ | ✅ `berths.width_m` | ditto | | `Draft (Metric)` | ❌ | ✅ `berths.draft_m` | ditto | | `Water Depth (Metric)` | ❌ | ✅ `berths.water_depth_m` | ditto | | `Nominal Boat Size (Metric)` | ❌ | ✅ `berths.nominal_boat_size_m` | ditto | | `CreatedAt` / `UpdatedAt` | ❌ | ✅ timestamps | Cache invalidation hints | | `Interests` (count) | ❌ | derivable | Probably internal-only | | `Interested Parties` (count) | ❌ | derivable | Probably internal-only | **Plan once questions are answered:** Add the chosen fields to `PublicBerth` interface in `src/lib/services/public-berths.ts`, the `toPublicBerth()` mapper, and the test fixtures. Trivial; gated only by which fields the website actually uses. **Other public-feed concerns to flag**: - **No archive flag**: when a berth is retired the public feed will still serve it. Need a `berths.archived_at` column + filter on the route. Plan §4.5 hinted at this. Not urgent. - **CRM-edit drift vs re-imports**: now that reps can edit berth fields (Wave 3), running the import script will skip-edited those rows (`updated_at > last_imported_at`) — that's the right design, but it means once cutover happens the website **must** call CRM `/api/public/berths`, never NocoDB. Coordinate this in the website repo. Useful guard already exists: `/api/public/health`. - **Cache TTL: 5 min**: when a CRM rep marks a berth `sold`, the public website serves "Available" for up to 5 minutes due to `s-maxage=300`. Acceptable for marketing; bump if needed. - **Health endpoint shape**: `/api/public/health` currently returns `{status, timestamp}` but `CLAUDE.md` claims `{env, appUrl}`. One of them is stale; the website may expect either shape. Not blocking but worth aligning. --- ## 🔴 Pending — large (group-discussion items, Wave 11) ### A. Manual client form expansion User wants "New Client" to support assigning yachts / companies / berths inline (without leaving the form), plus a mini-recommender for picking a berth at create time. Scope: - "Existing yacht / new yacht" picker. - "Existing company / new company" picker. - "Open an interest with this client" affordance that wires through `interest_berths` and the recommender. - Make sure all standard client modal fields (nationality / source / preferred contact / timezone / tags) remain present. Multi-component composition with a lot of cross-entity plumbing. Estimate fully before starting (likely 2–3 days). ### B. Documents section overhaul User wants: - Folders (create / delete / nested). - Sort + filter (by date, type, owner). - Wider file-type allowlist (PDF + Office + image is current; expand). - "Documents in progress" filter (contracts / EOIs awaiting signature, things uploaded but unparsed). - Drop or rename the "Signature-based only" pill — confusing copy. - "Expired" tab admin-configurable visibility. - Type-filter dropdown reflects actual types in use (vs the full hardcoded list). Refactor of `documents.service.ts` plus a new folders schema (`document_folders` table with port-scoped tree). ### C. Reports system User asked for: - Defined report types (Pipeline summary / Revenue / Activity log / Berth occupancy) with documented data shape per type. - Test fixtures for visual QA. - Admin "report templates" with field-level checkboxes letting an admin compose a custom report shape (toggles for each available data field). Infra exists (`/api/v1/reports`) but templates are stubs. A proper templating system + per-template field selection adds a few days. ### D. Receipts inline in expense PDF User confirmed: image receipts render inline beneath each expense row, **and** PDF receipts also render inline (one page each). pdfme (already used for EOI) handles both — inline images via the renderer, PDF pages via `pdf-lib.copyPages`. Depends on Wave 8 expense form work. ### E. Country / Nationality split on Client form Client schema has only `nationalityIso`. User wants: - New `country_iso` column for _country of residence_ (visible / primary). - Keep `nationality_iso` as an _optional_ secondary field. Requires: - Drizzle migration (`alter table clients add column country_iso text`). - Migrate existing data: copy `nationality_iso → country_iso` for every client (current value is more often country of residence in practice). - Update API validators (`clients.ts`). - Update client form UI: primary "Country" CountryCombobox, secondary collapsible "Nationality" row. - Same for residential clients (parallel schema). ### F. Inquiry triage (legacy spec carryover) Per project memory and the "deferred" list at the top of `today-2026-05-08.md`: inquiry triage was explicitly deferred. Tied into the inquiry routing settings (`inquiry_notification_recipients`, `inquiry_contact_email`, `residential_notification_recipients` — already in `system_settings`). Pick this back up when ready to auto-classify website inquiries. ### G. Per-port email branding Also in the deferred list. Templates and settings keys exist (per memory note); the admin UI for editing per-port email branding overrides remains. --- ## ✅ Decisions log — 2026-05-09 All 11 open questions answered. Implementation implications inline. 1. **Vocabularies admin layout (Wave 5)** → **New `/admin/vocabularies` page, grouped by domain, admin-only.** User considered exposing to non-admins (since reps use them daily) but settled on admin-only as the safer default for now. Implementation: new top-level admin route + page, reuse `system_settings` `(key, port_id)` composite PK. Each vocabulary key gets its own card section (interest temps, status-change reasons, tenure types, expense categories, document types, etc.). 2. **Notification preferences placement (Wave 10)** → **Collapse to user-settings only.** Keep `/notifications/preferences` as a server-side redirect to the user-settings notifications panel for back-compat links. 3. **Display name vs first/last (Wave 10)** → **Add `first_name` and `last_name` columns.** Don't worry about migrations during dev (we can iterate freely), but write the migration carefully so it applies cleanly when we eventually deploy. Keep `display_name` as a derived/optional override. 4. **Public-feed `Price` exposure (Bonus)** → **No — keep Price internal.** Don't add to PublicBerth payload. 5. **Public-feed remaining fields (Bonus)** → **Yes, add all.** Add Berth Approved, Water Depth, Width Is Minimum, Water Depth Is Minimum, all four metric variants, plus CreatedAt/UpdatedAt to PublicBerth + mapper + tests. User noted "not sure if we'll use all of them but best to keep them in" — verbatim NocoDB parity. 6. **Website cutover plan (Bonus)** → **Double-write transition window.** Keep both feeds live, write to both for the transition period, then decommission NocoDB. Coordinate with website repo (`CRM_PUBLIC_URL`). 7. **Status-change modal → prospect link (Wave 7)** → **Force interest pick + auto-create primary `interest_berths` row.** When status moves to `under_offer` or `sold`, the modal surfaces an interest selector below the reason dropdown. Picking an interest creates an `interest_berths` row with `is_primary=true` if one doesn't already exist for that pair. Depends on Wave 5 `berth_status_change_reasons` vocabulary. 8. **Trip label on expenses (Wave 8)** → **Combobox: free-text on first entry, dropdown of existing labels on subsequent entries.** No new entity. Source the dropdown from `SELECT DISTINCT trip_label FROM expenses WHERE port_id=?` ordered by recency. UI is a `` 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 ### Berth-related | Concern | File(s) | | ---------------------------------- | ---------------------------------------------------- | | Canonical berth enums | `src/lib/constants.ts` (search `BERTH_`) | | Berth list ordering SQL | `src/lib/services/berths.service.ts:69-72` | | Berth detail inline edit | `src/components/berths/berth-tabs.tsx` | | Berth modal form | `src/components/berths/berth-form.tsx` | | Berth area filter | `src/components/berths/berth-filters.tsx` | | Berth detail header / status modal | `src/components/berths/berth-detail-header.tsx:90` | | Berth Documents tab | `src/components/berths/berth-documents-tab.tsx` | | Berth list query + sort | `src/lib/services/berths.service.ts:25-140` | | Berth import script | `scripts/import-berths-from-nocodb.ts` | | Berth import service / parsers | `src/lib/services/berth-import.ts` | | Public berth API route | `src/app/api/public/berths/route.ts` | | Public berth single route | `src/app/api/public/berths/[mooringNumber]/route.ts` | | Public berth mapper | `src/lib/services/public-berths.ts` | | Public berth tests | `tests/unit/services/public-berths.test.ts` | | Berth seed snapshot | `src/lib/db/seed-data/berths.json` | | Berth schema | `src/lib/db/schema/berths.ts` (incl. `berthMapData`) | ### Other domains | Concern | File(s) | | --------------------------------- | -------------------------------------------------------------------------------------- | | Interest stage colors / legend | `src/components/interests/stage-legend.tsx` + `src/lib/constants.ts:STAGE_DOT` | | Mobile kanban toggle / fallback | `src/components/interests/interest-list.tsx` | | Country / timezone autoset | `src/components/clients/client-form.tsx` + `src/components/settings/user-settings.tsx` | | Phone input | `src/components/shared/phone-input.tsx` | | Country combobox + scroll patch | `src/components/shared/country-combobox.tsx` + `src/components/ui/command.tsx` | | Sidebar Umami gate | `src/components/layout/sidebar.tsx` (search `umamiRequired`) | | Mobile More-sheet | `src/components/layout/mobile/more-sheet.tsx` | | Notes service (aggregate-on-read) | `src/lib/services/notes.service.ts:130-242` | | Notes UI | `src/components/shared/notes-list.tsx` | | Settings manager (admin) | `src/components/admin/settings/settings-manager.tsx` | | User settings page | `src/components/settings/user-settings.tsx` | | Status change dialog | `src/components/berths/berth-detail-header.tsx:90` | | Companies members tab | `src/components/companies/company-members-tab.tsx` | | Yacht form | `src/components/yachts/yacht-form.tsx` | | Client form | `src/components/clients/client-form.tsx` | ### Infrastructure | Concern | File(s) | | ------------------------------------------- | --------------------------------------------- | | Drizzle config / migrations | `drizzle.config.ts`, `src/lib/db/migrations/` | | `system_settings` table | `src/lib/db/schema/system.ts:128-147` | | Permissions / `withAuth` / `withPermission` | `src/lib/api/helpers.ts` | | Body parsing (always use `parseBody`) | `src/lib/api/route-helpers.ts` | | Storage backend abstraction | `src/lib/storage/` | | Logger (pino) | `src/lib/logger.ts` | --- ## Resuming in a fresh session When you open a new chat, paste this **prompt** to pick up where this session ended: ``` I'm resuming the 2026-05-08 visual audit. Read docs/AUDIT-FOLLOWUPS.md first — it has every completed item, every pending item, and every open question. Then: 1. Skim the "Quick status snapshot" table at the top so you know what's done. 2. Read the "Open questions for the user" list and ask me question #N where N is whichever I'll answer first this turn. 3. Wait for my answers; don't start implementing until I confirm. Key invariants: - Notes unification model: aggregate-on-read. - Berth dropdown values: NocoDB SingleSelect canon, sourced from src/lib/constants.ts (BERTH_*_OPTIONS / _TYPES). - Power Capacity & Voltage stay numeric inputs; Bow Facing is a constrained 4-value dropdown despite being SingleLineText in NocoDB. - linkedUnit on EditableSpec auto-fills the metric column on save. - system_settings (key, port_id) is the configuration pattern. - NocoDB MCP is connected via ~/.claude.json — Berths schema + records can be pulled live. - Public berth feed (/api/public/berths) now serves Map Data; 117 berth_map_data rows backfilled in this session. - Tests: 1185/1185 passing; tsc clean. The git working tree has 23 modified files + 2 new (no commits yet). Don't commit anything until I say so. ``` ### Resume commands (cheat sheet) ```bash cd /Users/matt/Repos/new-pn-crm pnpm dev # Turbopack dev (~1s boot) # Tests pnpm exec vitest run # Unit + integration (~7s) pnpm exec tsc --noEmit # Type check pnpm exec playwright test --project=smoke # Smoke (~10min) # NocoDB import (for new berth pulls) pnpm tsx scripts/import-berths-from-nocodb.ts --dry-run --port-slug port-nimara pnpm tsx scripts/import-berths-from-nocodb.ts --apply --port-slug port-nimara # DB inspect PGPASSWORD=changeme psql -h localhost -p 5434 -U crm -d port_nimara_crm # Public-feed sanity check curl -s http://localhost:3000/api/public/berths | jq '.pageInfo' curl -s http://localhost:3000/api/public/berths/A1 | jq '.' ``` ### Verification checklist before committing this session's work - [ ] `pnpm exec vitest run` — 1185/1185 pass. - [ ] `pnpm exec tsc --noEmit` — clean. - [ ] `pnpm exec playwright test --project=smoke` — passes. - [ ] Manual: open `/port-nimara/berths`, confirm sort is A1, A2, A3 … A10, A11 (not lex order). - [ ] Manual: open a berth detail page, confirm the dock chip reads e.g. "A Dock", and the Bow Facing / Side Pontoon / Cleat fields render as ``. - [ ] 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.` 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.