Sweeps Group A of the 2026-05-21 remaining-plan. Several items the
plan listed as open turned out to already be shipped (annotation gap
in the master doc) — those are confirmed in the commit notes.
Shipped now:
A1 Documenso settings: collapsed `V2_FEATURE_FIELDS` +
`CONTRACT_RESERVATION_FIELDS` (legacy SettingsFormCard) into
`RegistryDrivenForm` sections (`documenso.behavior` +
`documenso.templates`). Every Documenso setting now flows
through the registry path that surfaces the env-fallback /
port / global source badge per field. EOI generation card
retitled to "Templates & signing pathway" since it now covers
EOI + reservation + contract template IDs (registry already
had all three under `documenso.templates`).
A2 WatchersCard empty state: bumped `mb-3 → mb-4 pb-1` so the
"No one is watching yet" line has breathing room above the
"Add a watcher…" select.
A4 /invoices/upload-receipts guide copy: terse luxury-CRM tone.
Drop "Snap a photo", "fancy phone camera", "No typing. No
spreadsheets." Tighten OCR explainer to one sentence;
action-oriented step + best-practices headers.
A5 Pageviews chart X-axis: added `interval="preserveStartEnd"` +
`minTickGap={52}` so multi-week ranges thin out the middle
ticks instead of overlapping. The MM-DD formatter was already
in place from an earlier session.
A7 Inbox doc comment: was stale ("Alerts first, Reminders
second") but the JSX already had Reminders before Alerts.
Fixed the docstring.
A9 CommandList scroll-cap: `max-h-[300px]` now `max-h-[min(300px,
var(--radix-popover-content-available-height,300px))]` so the
cmdk list never extends past the host Popover's available
area. Non-Popover hosts fall through to the 300px static cap.
A10 DropdownMenuContent: `max-h-96` now
`max-h-[min(24rem,var(--radix-dropdown-menu-content-
available-height,24rem))]` for the same available-space
behaviour on long menus near the viewport edge.
A11 Residential InterestsTab (list page): row gets an onClick →
`router.push`; first-cell Link stops propagation so middle-
click / Cmd-click "open in new tab" still works.
A12 StageStepper: gained a stage-name row below the bar showing
every reached stage's short label inline (muted for future
stages). `size="xs"` variant keeps the cramped table-cell
footprint intact (no labels).
Already shipped (just annotation gap in master doc):
A3 EOI "Mark as signed without file" button — line 599 of
interest-eoi-tab.tsx, parent passes onMarkSigned. Master doc
already has `SHIPPED in 52342ee` annotation.
A6 Pageviews vs Sessions explainer — Info popover at line
157-181 of website-analytics-shell.tsx.
A8 BulkAddBerthsWizard CurrencySelect — line 376 (apply-to-all)
+ line 456 (per-row).
Verified: tsc clean, vitest 1454/1454.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sweeps the last ~17 native `<Input type="date"|"datetime-local">`
call sites onto the shared `<DatePicker>` / `<DateTimePicker>`
primitives so date entry is uniform across the app (calendar popover
on desktop, native OS picker on mobile via the primitive's
viewport-aware fallback).
Three patterns handled:
1. Controlled value/onChange — direct swap to <DatePicker
value/onChange>:
audit-log-list.tsx (audit-from / audit-to filters)
reports/generate-report-form.tsx (date range)
scan/scan-shell.tsx (expense date)
reservations/reservation-detail.tsx (end-reservation dialog)
shared/filter-bar.tsx ('date' filter variant)
2. RHF `register('field')` pattern — wrapped in <Controller> with
field.value/field.onChange bridge. The picker's '' → undefined
normalisation kicks in via `field.onChange(v || undefined)`:
berths/berth-form.tsx (tenureStartDate + tenureEndDate)
reservations/berth-reserve-dialog.tsx (startDate)
companies/add-membership-dialog.tsx (startDate)
yachts/yacht-transfer-dialog.tsx (effectiveDate)
invoices/invoice-detail.tsx (paymentDate)
3. RHF + Date-typed schema — same Controller wrap, plus a
Date<->YYYY-MM-DD bridge in the render() since the zod schema
coerces these to Date:
expenses/expense-form-dialog.tsx (expenseDate)
companies/company-form.tsx (incorporationDate)
4. Datetime variants — swapped onto <DateTimePicker>:
interests/interest-contact-log-tab.tsx (occurredAt + followUpAt)
Skipped because they ARE picker primitives or internal date variants:
- ui/date-picker.tsx, ui/date-time-picker.tsx (the primitives)
- shared/inline-editable-field.tsx (the InlineEditableField date variant)
- dashboard/date-range-picker.tsx (its own popover with min/max gating
that doesn't map cleanly onto the shared primitive)
Removed now-unused Input imports from four files.
Verified: tsc clean, vitest 1448/1448.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 1 / commit 11 of 14 — invoices are client-facing documents, and
per the new "no CRM-generated client-facing PDFs" rule (see the design
spec), the in-app pdfme rendering is removed entirely.
Future invoice rendering will use the deferred AcroForm-fill admin-
template feature: admin uploads a PDF template with named form fields,
CRM fills them with invoice data via pdf-lib. Same pattern as the
in-app EOI pathway. Tracked in BACKLOG.md.
Deleted:
- src/lib/services/invoices.ts:generateInvoicePdf (60 LOC)
- src/lib/pdf/templates/invoice-template.ts (entire pdfme template)
- src/app/api/v1/invoices/[id]/generate-pdf/route.ts
- src/components/invoices/invoice-pdf-preview.tsx (regenerate UI)
- "PDF Preview" tab on invoice detail page
- 5 now-unused imports in invoices.ts (files, ports, buildStoragePath,
getStorageBackend, env)
sendInvoice() retained: still queues the send-invoice email job, still
flips status to "sent", still emits the socket event. The PDF-attach
step is gone — downstream consumers either render externally or wait
for the AcroForm-fill feature. The `pdfFileId` column on invoices stays
so existing rows don't break, just never gets written by this code path.
1319/1319 vitest green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Auto-format all files modified during the documents-hub-split feature
branch that were not yet aligned with the project's Prettier config
(single quotes, semicolons, trailing commas).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Builds on the centralised formatter shipped in ee2da8f. Replaces
\`\${currency} \${amount}\` style concatenations across the dashboard
revenue tooltip, command-search invoice/expense fallback labels,
expense-duplicate banner, and the invoice + expense PDF templates.
Drops the duplicate \`currencySymbol\` helper inside expense-pdf.service
in favour of the shared util; the two PDF helpers (renderReceiptHeader,
addReceiptErrorPage) now take a currency code instead of a pre-rendered
symbol so the formatter is the single source for spacing + thousands
separators. Also re-runs Prettier on the files where the prior commit
shipped without it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New `src/lib/utils/currency.ts` is the single source of truth for
display formatting (`formatCurrency`) and the supported-currency
catalog (`SUPPORTED_CURRENCIES`, 10 codes covering the marina market).
New shared components:
- `<CurrencyInput>` — number input with leading symbol prefix and
decimal inputMode, raw number value out via onChange.
- `<CurrencySelect>` — Select dropdown over `SUPPORTED_CURRENCIES`
with symbol + code + label per row, replaces the free-text 3-letter
inputs that let reps type "EURO" or "$$$" into a 3-char ISO column.
Threaded through every money input + display:
- Forms: berth (price/currency), expense (amount/currency), invoice
(currency Select + line-items unit-price + step-3 review totals).
- Reads: berth-card / berth-columns / invoice-card / expense-card /
dashboard KPIs / dashboard revenue-forecast / portal-invoices page.
Each had its own ad-hoc `Intl.NumberFormat` wrapper with slightly
different fallbacks; collapsed onto the shared helper.
`InvoiceLineItems` gained a `currency` prop so the unit-price input
prefix and the subtotal use the parent invoice's currency rather than
hard-coded `en-US` formatting.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wave through the 2026-05-07 backlog of small/concrete audit-final-deferred
items (deferring the Documenso Phases 2-7 build and items needing design
decisions or live external instances).
DB schema:
- Migration 0046 converts 5 composite (port_id, archived_at) indexes to
partial WHERE archived_at IS NULL — clients, interests, yachts, and
both residential tables. Smaller, faster planner choice for the
dominant list-query shape.
Multi-tenant isolation:
- document_sends now verifies recipient.interestId belongs to the port
before landing on the audit row (the surrounding clientId check was
already port-scoped; interestId pollution was the gap).
Routes / API:
- /api/v1/custom-fields/[entityId] requires entityType query param and
gates on the matching resource permission (clients/interests/berths/
yachts/companies). Fixes the cross-resource gap where a user with
clients.view could read company custom-field values.
- Admin user list trash button wrapped in PermissionGate (edit was
already gated; remove was not).
Service polish:
- berth-recommender accepts string-shaped JSONB booleans
('true'/'false') so admin UIs that wrap values as strings don't
silently fall through to defaults.
- expense-pdf renderReceiptHeader anchors all text positions to a
captured baseY rather than reading mutating doc.y after rect+stroke.
Headers no longer drift on the first receipt page after a soft page
break.
- berth-pdf apply: collect non-finite numeric coercion drops + warn-log
them so partial silent drops are observable (was invisible because
the no-fields-supplied check only fires when ALL drop).
- Storage cache fingerprint comment documenting the encrypted-secret
invariant + the explicit invalidation hook.
UI polish:
- invoice-detail typed: replaced two `any` casts with a proper
InvoiceDetailData / LineItem / LinkedExpense interface set.
- YachtForm now accepts initialOwner prop. Wired through:
- client-yachts-tab passes { type: 'client', id: clientId }
- interest-form passes { type: 'client', id: selectedClientId }
- Interest-form yacht picker now includes company-owned yachts where
the selected client is a member (fetches client.companies and feeds
YachtPicker an array filter). Plus an inline "Add new" button that
opens YachtForm pre-bound to the client.
- YachtPicker accepts ownerFilter as single OR array for "match any"
semantics.
BACKLOG.md updated with what landed vs what's still deferred (and why
each deferred item is genuinely larger than this push warrants).
Tests: 1185/1185 vitest, tsc clean.
Two mechanical sweeps closing the audit's HIGH §16 + MED §11 findings:
* 38 client components / 56 toast.error sites converted to
toastError(err) so the new admin error inspector becomes usable from
user-reported issues — every failed inline-edit, save, send, archive,
upload, etc. now carries the request-id + error-code (Copy ID action).
* 26 service files / 62 bare-Error throws converted to CodedError or
the existing AppError subclasses. Adds new error codes:
DOCUMENSO_UPSTREAM_ERROR (502), DOCUMENSO_AUTH_FAILURE (502),
DOCUMENSO_TIMEOUT (504), OCR_UPSTREAM_ERROR (502),
IMAP_UPSTREAM_ERROR (502), UMAMI_UPSTREAM_ERROR (502),
UMAMI_NOT_CONFIGURED (409), and INSERT_RETURNING_EMPTY (500) for
post-insert returning-empty guards.
* Five vitest assertions updated to match the new user-facing wording
(client-merge "already been merged", expense/interest "couldn't find
that …", documenso "signing service didn't respond").
Test status: 1168/1168 vitest, tsc clean.
Refs: docs/audit-comprehensive-2026-05-05.md HIGH §16 (auditor-H Issue 1)
+ MED §11 (auditor-G Issue 1).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Working through the audit-v2 deferred backlog. Each round was tested
(typecheck + 1168/1168 vitest) before moving on.
Round 1 — DB performance + AI cost visibility:
- Add missing FK indexes Postgres doesn't auto-create on
berth_reservations.{interest_id, contract_file_id},
documents.{file_id, signed_file_id}, document_events.signer_id,
document_templates.source_file_id, form_submissions.{form_template_id,
client_id}, document_sends.{brochure_id, brochure_version_id,
sent_by_user_id}. Without these, RESTRICT-checks on parent delete +
reverse-lookups walk the child tables fully. Migration 0037.
- AI worker now writes one ai_usage_ledger row per OpenAI call so admins
can audit spend per port/user/feature and future per-port budgets have
history to read from. Failure to write is logged-not-thrown so the
user-facing email draft is unaffected.
Round 2 — Boot-time + transport hardening:
- S3 backend verifies the bucket exists at startup (or auto-creates
when MINIO_AUTO_CREATE_BUCKET=true). A typo'd bucket name now
surfaces with a clear boot error instead of a vague Minio error
inside the first user-facing request.
- Documenso v1 placeFields: 3-attempt exponential-backoff retry on 5xx
+ network errors, fail-fast on 4xx. Stops one transient flake from
leaving a document with a partial field set.
- FilesystemBackend logs a structured warn-once at boot when the dev
HMAC fallback is in effect, so two processes started with different
BETTER_AUTH_SECRET values are observable (random 401s on file
downloads otherwise).
- Logger redact paths extended to cover *.headers.{authorization,
cookie}, *.config.headers.authorization, encrypted-credential blobs
(secretKeyEncrypted, smtpPassEncrypted, etc.), the Documenso
X-Documenso-Secret header, and 2-level nested forms.
Round 3 — UI feedback + permission gates:
- Storage admin migrate dialog: success toast with row count + error
toast on both dryRun and migrate mutations.
- Invoice detail Send + Record-payment buttons wrapped in
PermissionGate (invoices.send / invoices.record_payment); both
mutations now toast on success/error.
- Admin user list Edit button wrapped in PermissionGate(admin.manage_users).
- Scan-receipt page surfaces an amber warning when OCR fails so reps
know they can fill the form manually instead of staring at a stalled
spinner; the editable form now also opens on scanMutation.isError
/ uploadedFile, not only on success.
- Email threads list now renders skeleton rows during load + shared
EmptyState for the empty case (was a single "Loading…" line).
Round 4 — Service / route correctness:
- documentSends.sent_by_user_id was a free-text NOT NULL column with no
FK. Now nullable + FK to user(id) ON DELETE SET NULL so the audit row
survives a user being hard-deleted. Migration 0038 with a defensive
null-out for any orphan ids before attaching the constraint.
- Saved-views route: documented why withAuth alone is correct (the
service strictly filters by (portId, userId) — owner-only by design).
- Public-interests audit log: replaced "userId: null as unknown as
string" cast with userId: null; AuditLogParams already accepts null
for system-generated events.
- EOI in-app PDF fill: extracted setBerthRange() that, when the
AcroForm field is missing AND the context has a non-empty range
string, logs a structured warn so the deployment gap (live Documenso
template needs the field) is observable instead of silently dropping
the multi-berth range.
Test status: 1168/1168 vitest. tsc clean. Two new migrations
(0037/0038) need pnpm db:push (or migration apply) on the dev DB.
Deferred-doc updated with the remaining open items (bigger refactors).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces every em-dash and en-dash with regular ASCII hyphens
across comments, JSX strings, and dev-facing logs. Mostly cosmetic
but stops the inconsistent mix that crept in over the last few
months (some files used em-dashes in comments, others didn't,
some used both).
Bundles two small dashboard-layout tweaks that touch a couple of
already-modified files:
- (dashboard)/layout.tsx main padding goes from p-6 to pt-3 px-6
pb-6 so page content sits closer to the topbar.
- Sidebar now receives the ports list it needs for the footer
port switcher.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds /invoices/upload-receipts as the dedicated explainer for the
mobile scanner PWA: install instructions for iOS/Android, direct
deep-link button, and a walkthrough of the scan -> verify -> save
flow. Sidebar entry replaces the old "Scan receipt" tab so the
desktop side picks up the install steps before sending users to
the mobile-only surface.
Scanner layout moves PWA manifest + apple-* meta tags from inline
JSX into Next.js's metadata/viewport exports so the App Router
doesn't try to render a second <head>, fixing a hydration error
that surfaced as two console warnings on the scan page.
Scanner shell gains a centered Port Nimara logo header so the
standalone PWA looks branded when launched from the home screen
without the dashboard chrome.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Documents hub signer status now renders via a label map (`Pending`,
`Signed`, `Declined`, …) instead of the raw lowercase enum value.
- Invoice detail formats `dueDate` and `paymentDate` as `MMM d, yyyy`
via `date-fns` instead of leaking raw `2025-03-14` ISO strings, and
swaps the "Payment Method" free-text input for a `Select` of labelled
options (`Bank transfer`, `Credit card`, …) so we never store
`bank_transfer` from a hand-typed field again.
- Interest tabs `MilestoneSection` status badge uses a `humanizeStatus`
helper so values like `waiting_for_signatures` show as
`Waiting For Signatures` (correctly title-cased) instead of being a
lower-snake-case fragment inside an ALL-CAPS pill.
- `OUTCOME_BADGE` in the interest header now has a fall-through that
renders any unknown outcome as a closed-state badge, preventing a
closed interest from looking open just because its enum was added
upstream without a matching label entry.
- Interest timeline route joins the `user` table and returns
`userName` alongside `userId`; the client renders the resolved name
instead of a 36-char UUID. Falls back to `'a teammate'` if the user
row was deleted.
- Invoice "New / Step 3 — Review" replaces the truncated UUID display
with a server-resolved client/company name via a small `useQuery`,
so users can confirm they picked the right billing entity before
submitting.
- New `loading.tsx` for client detail renders a header / tab strip /
card skeleton during the server-component / initial-query window
that previously flashed empty.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Detail pages (clients, yachts, companies, berths, invoices, expenses)
now push their entity name + a back-button toggle to the mobile
topbar via useMobileChrome, replacing the URL UUID fallback that was
rendering before.
Supporting changes:
- useMobileChrome() no longer throws when called outside the
MobileLayoutProvider — desktop-tree consumers get a no-op
setChrome so callers don't have to branch on shell type.
- setChrome is now stable across renders (useCallback) so callers'
useEffect dependency arrays don't infinite-loop.
- DetailPageShell now also pushes its entityName + cleans up on
unmount, and hides its desktop-only sticky header on mobile so it
doesn't double up with the topbar (no current callers, prep for
Phase 4 migration).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Five new <EntityCard> files using the shared <ListCard> shell, wired
into each list page's <DataTable> via cardRender. Desktop view
(lg+) is unchanged.
- YachtCard: Ship icon, owner subtitle (User/Building2 icon by
ownerType), dimensions in meters preferred, hull #,
status pill. No accent bar (status is free-text).
- CompanyCard: Building2 icon, legalName subtitle, country (MapPin)
+ tax id (Hash) meta, member/yacht count line.
- BerthCard: Anchor icon, area subtitle (MapPin), dimensions
meta, status pill. Status-encoded accent bar
(emerald=available, amber=under_offer, slate=sold).
- InvoiceCard: FileText icon, client subtitle, due date (Calendar)
meta, prominent currency-formatted amount. Status
accent bar (emerald=paid, orange=overdue, ...).
- ExpenseCard: Receipt icon, category subtitle, expense date meta,
prominent amount, payment-status pill, "Possible
duplicate" pill when duplicateOf is set. Accent bar
by paymentStatus, overridden to amber when flagged
as duplicate.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds shared <DetailHeaderStrip> wrapper (rounded-xl + gradient-brand-soft + shadow-xs)
and applies it to every legacy domain detail header. Residential client/interest and
invoice detail get an inline gradient strip with eyebrow ('Residential Client',
'Residential Interest', 'Invoice'). Residential bodies normalized to lg:grid-cols-[2fr_1fr]
per spec.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>