Commit Graph

684 Commits

Author SHA1 Message Date
d97a08bf5f docs(uat): SHIPPED annotation for PR21 (auth link contrast)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 18:47:57 +02:00
ae8867d832 feat(uat-batch-21): a11y — auth-page link contrast bumped past AA
`text-[#007bff] hover:underline` (light blue, 12-14px) was falling
below WCAG 1.4.3 AA contrast against the auth shell's white card.
Bumped to `text-[#0058b3]` (darker variant of the same hue) and
added `underline underline-offset-2 hover:no-underline` so the link
is always visibly underlined as a backup affordance.

Affects: /login, /reset-password, /set-password, /portal/login,
/portal/forgot-password, portal password-set-form. Button bg colors
(white-text on the same blue) are unchanged — those pass AA at
button sizes.

tsc clean. 1419/1419 vitest pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 18:47:33 +02:00
28eb76a9d8 docs(uat): SHIPPED annotation for PR20 (form-error UX primitives)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 18:45:23 +02:00
ec6f90f335 feat(uat-batch-20): form-error UX primitive — scroll-to-first-error hook + summary banner
Two new building blocks for the platform-wide form-error UX rework.
Expense form adopts both as the validation that the pattern works
before the broader sweep across the ~29 useForm callers.

- `useFormScrollToError(handleSubmit, errors)` — wraps RHF's
  handleSubmit. On validation failure it locates the first errored
  field via `[name="..."]` (or id fallback), walks ancestors to find
  the nearest scrolling container (key for forms inside Sheet /
  Dialog bodies that own their own overflow-y), and
  scrollTo({ behavior: 'smooth' }) + focus({ preventScroll }) on it.
  Type-loose handleSubmit signature so 2-arg and 3-arg useForm()
  callers (input vs transformed types) both work.
- `<FormErrorSummary errors={errors} labels={…}>` — top-of-form alert
  banner listing each failed field as a clickable anchor. Renders
  only when ≥2 errors (single-error case is handled by the hook
  alone). role="alert" aria-live="polite" for SR users.
- expense-form-dialog adopts both: `onSubmitWithScroll(onSubmit)`
  replaces the bare `handleSubmit(onSubmit)`, plus a labelled
  `<FormErrorSummary>` at the top of the form. Closes the loop on
  the silent-no-op zod-refine bug fixed in PR1 (the underlying
  setValue() fix already routes errors through formState; this
  surfaces them visibly).

tsc clean. 1419/1419 vitest pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 18:44:54 +02:00
7d48349a75 docs(uat): SHIPPED annotations for PR19 (a11y + i18n micro-fixes)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 18:41:12 +02:00
72d7803be5 feat(uat-batch-19): a11y th scopes + legend styling + i18n locale fixes
- Raw `<th>` cells gain `scope="col"` so SR users get proper column
  association: berth-interests-tab, bulk-add-berths-wizard,
  clients/bulk-hard-delete-dialog. shadcn `<TableHead>` migration
  would be cleaner but the scope attribute is the minimum-effort fix
  the queue's a11y entry asks for.
- supplemental-info form `<legend>` elements styled with
  `mb-2 px-1 font-semibold` so they read as section headings rather
  than blending into the surrounding fieldset border (default browser
  legend rendering is barely visible).
- payments-section: invalid `'en-EU'` BCP-47 locale → `undefined` to
  honour browser locale.
- ui/calendar: literal `'default'` → `undefined` on the month
  dropdown formatter, same reason.

tsc clean. 1419/1419 vitest pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 18:40:34 +02:00
5a2dabea05 docs(uat): SHIPPED annotations for PR18 (interest-berths defaults + a11y)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 18:36:47 +02:00
05e727f462 feat(uat-batch-18): interest-berths defaults + a11y loading/hint fixes
- `addInterestBerth` insert-time defaults now match the locked
  multi-berth EOI UX (queue B2):
    is_in_eoi_bundle: true   (was false)
    is_specific_interest: matches `isPrimary` (was always true)
  This means a newly-linked berth is covered by the EOI signature by
  default but the public map only shows the primary as "Under Offer"
  until the rep marks others isSpecificInterest. The two existing
  integration tests pass explicit values so they're unaffected.
- A11y: `set-password` form's password-requirements hint linked via
  aria-describedby so SR users hear the rules on focus.
- A11y: Loading fallbacks on set-password / portal/activate /
  supplemental-info wrapped in role="status" aria-live="polite" with
  sr-only "Loading" copy where only a spinner was visible.

tsc clean. 1419/1419 vitest pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 18:35:52 +02:00
1f8bd47a7b docs(uat): SHIPPED annotations for PR17 (layout polish)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 18:32:43 +02:00
8fcbe45d36 feat(uat-batch-17): layout polish — DocumentsHub flush-left, breadcrumb wrap fix, viewport-centered topbar search
- DocumentsHub root container gains `sm:-mx-6 sm:-mt-3 sm:-mb-6` to
  escape the AppShell main padding (`px-6 pt-3 pb-6`). The folder
  column now sits flush against the global app sidebar, reading as an
  extension of navigation rather than a card-inside-a-page. Mobile
  layout retains the AppShell padding.
- Breadcrumbs: each crumb + its trailing separator now share a single
  `<BreadcrumbItem>` instead of being separate `<li>`s. Flex-wrap can
  no longer strand an orphan separator at end-of-line above a wrapped
  child crumb. Drops the standalone `<BreadcrumbSeparator>` usage from
  the consumer; the primitive is still exported for backcompat.
- Topbar search visually centered against the full viewport via a
  `translate-x:calc(-var(--width-sidebar)/2)` shift. Grid middle slot
  bumped from `minmax(360px, 640px)` → `minmax(420px, 800px)` and the
  search wrapper from `max-w-md` → `max-w-2xl` so reps actually have
  room to read long results.

tsc clean. 1419/1419 vitest pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 18:31:32 +02:00
9adb80ada4 docs(uat): SHIPPED annotations for PR16 (Overview cleanup)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 18:27:59 +02:00
f39f0aa7bc feat(uat-batch-16): Interest Overview cleanup — hide legacy reminder panel, deprioritize PaymentsSection
Two coordinated layout changes on the interest Overview tab so the
active milestone gets visual priority.

- Legacy `interest.reminderEnabled` panel removed from Overview. The
  field still drives the auto-follow-up worker
  (`processFollowUpReminders`) and the REMINDERS section + bell-in-
  header surface active reminders, so the read-only duplicate panel
  was pure noise. Backend behaviour unchanged; no schema impact.
- PaymentsSection mount relocated from above the milestone strip to
  below it. The active milestone above carries the rep's day-to-day
  attention; deposits-tracking is reference / history once expected.
  Render order: past strip → current milestone(s) → future
  (collapsed) → PaymentsSection → Lead/Source grid. Pre-Reservation
  the section still doesn't render at all (unchanged). Collapsed-bar
  + summary-chip refinement parked.

tsc clean. 1419/1419 vitest pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 18:27:17 +02:00
348dc94858 docs(uat): SHIPPED annotation for PR15 (reusable supplemental token)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 18:24:06 +02:00
b74fc56a3b feat(uat-batch-15): supplemental-info link reusable until expiry
The supplemental-info token now stays valid for re-submissions until
the 14-day TTL expires. Previously the link was single-use:
`applySubmission` required `consumedAt IS NULL`, which locked clients
out of correcting a typo or finishing a partial submission.

- Service: drops the `isNull(consumedAt)` filter; TTL is the sole
  validity check. `consumedAt` is still stamped on each submit so the
  rep / loader can see "last submitted at" context.
- Public form: the "already submitted" lockout screen is removed.
  Instead, when the token has been used before, the form renders with
  the prefill (already reflecting the latest data) plus a soft amber
  banner noting that changes overwrite the previous submission.
- Drive-by em-dash fix on the post-submit thank-you copy (matches the
  Wave-1 lint guard).

tsc clean. 1419/1419 vitest pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 18:23:44 +02:00
4d3d7489bf docs(uat): SHIPPED annotations for PR14 (signature docs rename + tooltip + yacht Transfer)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 18:19:21 +02:00
552b966903 feat(uat-batch-14): InterestDocumentsTab rename, custom-field tooltip, yacht Transfer surface
- InterestDocumentsTab section "Legal documents" renamed to
  "Signature documents" so its scope is unambiguous. The section
  holds Documenso envelopes (EOI / Reservation / Contract); generic
  legal uploads belong in Attachments below.
- Custom-field admin form's "Sort Order" label now uses the
  FieldLabel primitive with an explainer tooltip ("Lower numbers
  render first... use to pin frequently-edited fields to the top").
  First adoption of the FieldLabel primitive shipped in PR4.2.
- Yacht Ownership History tab gains a "Transfer ownership" button:
  in the populated state as a header CTA (perm-gated by yachts.edit),
  in the empty state as the EmptyState action. Reuses the existing
  YachtTransferDialog from the header. Closes the "no way to enter/
  change" UX gap without duplicating the transfer logic.
- Verified the existing row-owner rendering already uses OwnerLink,
  so the row-click affordance was already in place.

tsc clean. 1419/1419 vitest pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 18:18:29 +02:00
610154395a docs(uat): SHIPPED annotation for PR13 (activity feed UUID resolution)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 18:14:52 +02:00
2cb0b99314 feat(uat-batch-13): activity feed resolves user UUIDs to display names
Audit-log rows with user-FK diffs (assignedTo, ownerId, reassignedTo,
createdBy, addedBy, changedBy, transferredBy) previously rendered the
raw user UUID in the activity feed (e.g. "→ mEcsLxo5kyFMyhbOSehxJjY…").
Same gap on the row's actor — the rep had no idea who did what.

- getRecentActivity collects all userIds referenced by either the row's
  actor (auditLogs.userId) or by user-FK diff values, then bulk-fetches
  user_profiles in a single query. Output rows now carry an
  `actorName` field and have their `oldValue`/`newValue` swapped for
  display names on user-FK fields.
- Unknown / deleted users fall back to "Unknown user (#short-uuid)" so
  the audit trail stays useful for forensics.
- ActivityItem client type extended with `actorName`. Existing
  consumers still read the raw `userId` for forensics + deep-link.

tsc clean. 1419/1419 vitest pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 18:14:21 +02:00
f99d2cd9ec docs(uat): SHIPPED annotations for PR12 (env-reveal + stage sortable)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 18:12:04 +02:00
ca51000401 feat(uat-batch-12): password-reveal env messaging + berth Latest-stage sortable
- registry-driven-form password-reveal eye toggle: when the value is
  resolved from env / default fallback (not port / global override),
  the toggle is now disabled with a tooltip explaining "Value comes
  from the environment. Configure in admin to enable reveal." Stops
  the silent-no-op confusion that read as a broken toggle.
- Berth list: 'Latest deal stage' column dropped enableSorting:false.
  Service-side adds a stageSort correlated subquery that ranks each
  berth by the highest active interest's pipelineStage (enquiry=1 →
  contract=7); NULLS LAST regardless of direction so empty rows
  always land at the bottom.

tsc clean. 1419/1419 vitest pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 18:11:17 +02:00
901fc363a5 docs(uat): SHIPPED annotations for PR11 (picker polish + currency + breadcrumb)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 18:07:40 +02:00
2bcf544cbc feat(uat-batch-11): picker polish + BulkAddBerthsWizard currency + DocumentsHub root cleanup
- BulkAddBerthsWizard `priceCurrency` row + apply-to-all swapped from
  freetext Input to the shared CurrencySelect. Same idiom as
  berth-form + expense-form-dialog.
- /api/v1/yachts/autocomplete no longer short-circuits to `[]` when
  the search query is empty — the service returns the top 20
  most-recently-updated yachts so the picker has a useful default
  view the moment it opens. Saves the rep from a dead-end empty
  state.
- YachtPicker gains a fallback useQuery against `/api/v1/yachts/{id}`
  when the selected yacht isn't present in the current autocomplete
  window. Trigger label now shows the real name (was falling back to
  "Yacht <uuid-prefix>" when a parent pre-selected a value from a URL
  param).
- DocumentsHub: breadcrumb row only renders when a folder is
  selected. The "Home / All documents" placeholder was wasted
  vertical space above the PageHeader on the root view.

tsc clean. 1419/1419 vitest pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 18:06:41 +02:00
c18dbbd61b docs(uat): SHIPPED annotations for PR10 (copy polish + a11y)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 18:02:04 +02:00
db511063df feat(uat-batch-10): copy polish, TTL trim, and a11y discrete fixes
- Supplemental-info link TTL trimmed from 30 → 14 days (single
  constant in supplemental-forms.service).
- LinkedBerthsList toggle renamed "Mark in EOI bundle" →
  "Include in EOI"; tooltip aria-label updated to match.
- Icon-only row-action triggers on the interest / client / berth list
  tables gain aria-label (Row actions for <name>) so SR users hear
  the row context.
- Table / Board view toggle on interest list gains aria-label +
  aria-pressed on each variant; wrapper gets role="group".
- Upcoming-milestones disclosure on interest-tabs gains
  aria-expanded + aria-controls; recommender Hide/Add filters
  button matches.
- BrandedAuthShell logo alt no longer defaults to "Sign in" — uses
  the configured `appName` when known, empty string otherwise so
  screen readers don't announce "Sign in" on password-reset /
  set-password pages.

tsc clean. 1419/1419 vitest pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 18:01:17 +02:00
5f937b4551 docs(uat): SHIPPED annotations for PR9 (milestone classifier + backfill)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 17:55:12 +02:00
d8da1f634d feat(uat-batch-9): milestone classifier + skip-ahead backfill controls
Two coordinated UX changes that finally make the rep's manual-stage-
jump workflow legible:

- Milestone phase classifier introduces a "stage-owning milestone"
  rule. When the rep manually advances the deal to Reservation+ but
  earlier sub-statuses are still un-signed, the current-stage
  milestone now stays marked `'current'` (no longer collapses into
  the past-strip / upcoming-accordion based on completion alone).
  Earlier-than-stage milestones bucket to `'past'` so the rep can
  backfill them; later slots stay `'future'`. The previous
  firstIncompleteKey-driven rule still applies in stages without an
  owning milestone (enquiry / qualified / nurturing).
- Skip-ahead backfill control `<MilestoneBackfillButton>` lands in
  the past-milestones strip whenever a milestone's date column is
  null. Opens a DatePicker popover (today default, accepts any past
  date) and PATCHes the relevant date_* column directly via
  useInterestPatch — no stage transition fires.
- `InterestPatchField` extended with the five milestone date keys;
  validator gains `dateDepositReceived` (was the only missing one).

Together this means: a deal manually-advanced from EOI Sent → Deposit
no longer hides Reservation under upcoming-milestones AND the rep can
record the EOI/reservation signing dates without re-triggering the
stage transition.

tsc clean. 1419/1419 vitest pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 17:54:33 +02:00
535ff69fc4 docs(uat): SHIPPED annotations for PR8 (qualification rework)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 17:48:31 +02:00
51ca875665 feat(uat-batch-8): qualification rework — intent auto-confirm + derived-only + collapse-when-done
Three coordinated changes to the per-interest qualification checklist
that collectively trim it from a noisy gate into an out-of-the-way
audit log once the deal moves forward.

- Auto-confirm `intent_confirmed` once `pipelineStage > qualified`.
  Signing an EOI (or later) is the strongest signal of intent; the
  checklist no longer requires a redundant explicit tick. Evidence
  string reads "Stage advanced past Qualified".
- `dimensions` becomes derived-only — explicit ticks no longer
  override removed evidence. When the rep deletes a yacht link or
  clears desired dims, the row un-ticks immediately. Judgement-based
  criteria keep the OR semantic so a manual confirmation survives an
  evidence change.
- Checklist auto-collapses when fully confirmed: header shows ✓ All
  confirmed (label · label) with a chevron; rep clicks to expand and
  inspect or untick. Forced-expanded whenever an item is still
  outstanding. ARIA-controlled.
- `qualification.service` gains a `pipelineStage` column-select and
  threads it through `AutoCtx`; `DERIVED_ONLY_KEYS` Set sentinel
  drives the new merge semantic.

tsc clean. 1419/1419 vitest pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 17:47:38 +02:00
b9d388a362 docs(uat): SHIPPED annotations for PR7 (Wave-2 polish batch)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 17:42:44 +02:00
c6dcf49e18 feat(uat-batch-7): Wave-2 polish — Open-in-Documents, berth label, residential, NotesList parity
- InterestEoiTab history link renamed "Open" → "Open in Documents"
  so the cross-section nav target is unambiguous.
- DocumentDetail Interest link sub-text now shows the derived
  `berthLabel` (formatBerthRange of the in-EOI-bundle subset, falling
  back to primary, then all linked berths). The link no longer
  duplicates the Client name; falls back to clientName or "No berths
  linked" when no berths exist.
- New /<port>/residential/page.tsx redirects to /residential/clients
  so the breadcrumb's Residential link works.
- Residential interests list — whole row is now a Link target (was
  hidden behind a trailing "View" link); hover + border accent on the
  full row.
- Expenses PageHeader description "Track and manage port expenses" →
  "Track and manage business expenses" (drop the redundant "port",
  same audit pattern flagged in the queue).
- DropdownMenu base content capped at `max-h-96` (was the Radix
  available-height variable, which stretched menus edge-to-edge); the
  existing internal scroll handles overflow.
- Yacht Overview Notes block: replaced the legacy single-field
  textarea with the threaded `<NotesList entityType="yachts">` for
  parity with clients/interests/companies. Legacy `yacht.notes`
  column stays in schema for EOI/contract merge-field path.

tsc clean. 1419/1419 vitest pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 17:41:02 +02:00
a673b6cec2 docs(uat): SHIPPED annotations for PR6 (structured signatories + signers)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 17:35:32 +02:00
301375a3c3 feat(uat-batch-6): external-EOI structured signatories + X/Y signed counter
Replace the freetext CSV signer-names field with a structured recipient
editor (name / email / role per row). Service now persists each
non-CC signatory as a `document_signers` row pre-stamped
`status='signed'` so the document-detail "X / Y signed" badge counts
correctly for manually-uploaded EOIs.

- ExternalEoiInput gains a structured `signatories` field; legacy
  `signerNames` retained for back-compat. Role enum:
  `client | developer | rep | witness | cc`.
- uploadExternallySignedEoi inserts `document_signers` rows for every
  non-CC entry inside the existing transaction.
- documentEvents.completed event records both shapes for full audit
  fidelity.
- POST /api/v1/interests/[id]/external-eoi parses the `signatories`
  JSON multipart field defensively; malformed payloads fall back to
  signerNames.
- Dialog UI: per-row Name / Email / Role inputs with add / remove.
  Seeds from interest's clientName + clientPrimaryEmail via a
  signatoriesOverride/null pattern (React-Compiler safe — no
  setState-in-effect).

tsc clean. 1419/1419 vitest pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 17:34:59 +02:00
7cdfed27fa docs(uat): SHIPPED annotations for PR5 (UI polish batch)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 17:29:53 +02:00
203f543e60 feat(uat-batch-5): UI polish — dialog width, chart centering, recommender pill, audit link, inbox reorder
Six surgical Wave-2-3 wins:

- UploadForSigningDialog: dialog widened to max-w-[1400px] w-[95vw] so
  the place-fields step actually has room; recipient row converts from
  fixed grid to flex (name flex-1, email flex-[2] for the longer
  string, role w-40, delete shrink-0); invitation-message textarea
  rows 3 → 6.
- ChartCard becomes flex-col with flex-1 + items-center on CardContent
  so charts vertically center when neighbouring cards make the row
  taller (e.g. Pipeline Value's full breakdown).
- Berth recommender pill: drops the "Tier {letter} · " prefix; shows
  just the plain-English label ("Open" / "Fall-through" / "Active
  interest" / "Late stage") as a Popover trigger that explains the
  4-state ladder. HelpCircle icon makes the tooltip discoverable.
- Activity feed gains a "See all" link in the header pointing at
  /<port>/admin/audit, permission-gated by `admin.view_audit_log`.
- Inbox section order swaps to Reminders above Alerts (rep-noted
  priority); PageHeader title flips to "Reminders & Alerts". Section
  ids, deep-link hashes, and localStorage open-state keys untouched.
- Inbox ReminderList (embedded mode only): "New Reminder" button now
  shares the filter row (right-aligned via ml-auto) instead of
  occupying its own dedicated row above the filters.

tsc clean. 1419/1419 vitest pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 17:28:20 +02:00
70c7d84dea docs(uat): SHIPPED annotations for PR4 (a11y primitives + click-to-preview)
Annotate 4 finding entries:
- em-dash lint guard (sweep parked)
- DocumentList Download in kebab
- WatchersCard empty-state padding
- EOI empty-state Mark Signed button
- Platform-wide click-to-preview (FileGrid + DocumentList; 2 remaining surfaces parked)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 17:21:33 +02:00
52342ee45d feat(uat-batch-4): a11y form primitives + click-to-preview + EOI empty-state + lint guards
- FieldError primitive (role=alert, aria-live) — used by Wave 3
  form-error UX work.
- FieldLabel primitive (Label + Info-tooltip slot) — foundational for
  the platform-wide admin-settings tooltip audit.
- ESLint guard against em-dash in user-facing JSX text inside
  src/components + src/app (warning, not error; 111 existing instances
  flagged for follow-up sweep).
- FileGrid card body becomes click-to-preview button (was hidden under
  a kebab); aria-label per row; kebab keeps Download/Rename/Delete.
- DocumentList: title cell on rows with signedFileId opens
  FilePreviewDialog; kebab gains Download action (was missing
  per UAT). Single FilePreviewDialog instance lifted to the parent.
- DocumentList type extended with signedFileId.
- EOI empty state: third ghost button "Mark signed without file"
  wired to existing MarkExternallySignedDialog (parity with
  reservation tab).
- Watcher empty-state padding fix on document-detail.

tsc clean. 1419/1419 vitest. lint clean on touched files.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 17:20:13 +02:00
6a4f4ea1dd docs(uat): SHIPPED annotations for PR3 (primitives)
Annotate ColumnPicker, FileInputButton, and DatePicker / DateTimePicker
entries with the 8f42940 summary. Notes the deferred sweeps:
- 15+ remaining date-input sites
- raw-input file sweep was a no-op (audit showed only 1 actual
  default-UI site, already migrated)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 17:11:02 +02:00
8f42940c52 feat(uat-batch-3): wave-1 primitives — DatePicker, DateTimePicker, FileInputButton, ColumnPicker hideAll
Builds the foundational primitives that subsequent waves depend on.
None of these introduce new deps — date-fns, react-day-picker, and
shadcn Calendar were already in the tree.

- `<DatePicker>` and `<DateTimePicker>` in src/components/ui — desktop
  popover wrapping the existing shadcn Calendar (caption-dropdown nav
  so reps can jump months/years for the SkipAheadBanner backfill UX),
  mobile native input via useIsMobile. Drop-in for `<Input type=date>`
  / `<Input type=datetime-local>`.
- `<FileInputButton>` in src/components/ui — styled Button + hidden
  input, replaces browser-default file picker UI. Most queued sweep
  sites already used the hidden-input + Button-trigger pattern; the
  primitive lands for any new caller plus consistent filename display
  + clear button.
- ColumnPicker `hideAll()` footer item — symmetric to existing
  `showAll()`, with the same visibility gate. Lands platform-wide via
  the shared component.
- Migrated highest-leverage call sites to the new primitives:
  * MilestoneAdvanceButton (backfill UX)
  * Reminder form (datetime-local → DateTimePicker)
  * Snooze dialog (datetime-local → DateTimePicker)
  * External-EOI upload dialog (date + file picker)
  * Payments section (received-on date)
- Remaining 15+ date-input call sites parked for a follow-up sweep —
  several use react-hook-form `register` patterns that need careful
  migration to the new controlled-value contract.

tsc clean. 1419/1419 vitest pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 17:10:02 +02:00
69444878ab docs(uat): SHIPPED annotations for PR2 (external-EOI bundle)
Annotate B4 #5 with the 6cdb9af summary of what landed (a/b/c/d +
default title) and what's deferred (e — edit metadata UI bundles with
later signing-flow rework).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 17:02:12 +02:00
6cdb9af6b2 fix(uat-batch-2): external-EOI five-bug bundle (a/b/c/d) + presign filename override
Tackles the linked B4 #5 findings on the external-EOI flow. Item (e)
[Edit metadata affordance per row] is deferred to a later wave so it
can share infra with the broader signing-flow rework.

- (a) lying toast: uploadExternallySignedEoi now returns
  { stageChanged, newStage }. Client toasts conditionally so a
  Reservation+ deal that uploads paper-signing evidence no longer
  claims the stage advanced.
- (b) View downloads instead of previewing: SignedPdfActions takes an
  onView callback; InterestEoiTab lifts a single FilePreviewDialog and
  passes the callback down. Click-View opens the in-app preview rather
  than the presigned URL (which the storage backend served as
  attachment).
- (c) UUID filename on download: getDownloadUrl now passes the
  canonical filename through presignDownloadUrl; S3 backend adds a
  response-content-disposition override (filename + UTF-8 filename*)
  to the presign. Filesystem backend already passed it through.
- (d) Discarded dateEoiSigned: external-eoi service splits document-
  metadata writes (always — dateEoiSigned, eoiStatus='signed') from
  stage advance (gated on past-EOI). Also fires
  evaluateRule('eoi_signed') so berth-rules stay in sync when an EOI
  is filed manually.
- Default title for external-EOI dialog now derives
  "External EOI — <Client> — <berth range> — <date>" via the existing
  formatBerthRange helper; rep can override.

tsc clean. 1419/1419 vitest pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 17:01:35 +02:00
abbaf406ab docs(uat): SHIPPED annotations for PR1 batch + accumulated UAT findings
PR1 batch (2d57417) covered 7 Wave-1 blockers; each finding entry now
carries an inline `**SHIPPED in 2d57417:**` line summarizing what
landed and (where applicable) what remains parked for later waves
(backfill scripts, nested-folder migration, platform-wide form-error
audit).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 16:52:59 +02:00
2d574172ec fix(uat-batch-1): wave-1 blocker bugs — supplemental gate, file FK, downloads, search dedup, notes stale, expense form, vocab
Surgical fixes for the 7 UAT blockers that prevent productive forward
testing. Each item has a corresponding entry in alpha-uat-master.md.

- supplemental-info route relocated out of (portal) so it bypasses the
  isPortalDisabledGlobally() kill-switch. URL unchanged.
- file upload service derives client_id/company_id/yacht_id from
  (entityType, entityId) when not explicitly passed, so interest-tab
  uploads no longer land with client_id=NULL and stay visible in the
  Attachments list.
- triggerBlobDownload / triggerUrlDownload helpers in src/lib/utils
  attach the anchor to the DOM before click so Chromium honours the
  download attribute; 7 sites refactored, file-named downloads stop
  arriving as bare UUIDs.
- search-nav-catalog dedupes by href at the result-collection layer so
  the same href can no longer surface twice in the command-K dropdown
  (kills the React duplicate-key warning); /admin/templates entries
  merged into a single richer-keyword variant.
- NotesList gains a parentInvalidateKey prop, wired through all five
  callers (interest, client, yacht, company, residential client/
  interest) so the Overview "Latest note" teaser refreshes when a note
  is added in the Notes tab.
- expense-form-dialog: setValue('receiptFileIds') / setValue(
  'noReceiptAcknowledged') on upload/clear/checkbox so the schema-level
  refine sees the field and Create stops silently no-op'ing on submit.
- bulk-add-berths-wizard: side-pontoon dropdown now reads through
  useVocabulary('berth_side_pontoon_options') instead of a wrong local
  enum ('Port', 'Starboard', 'Bow', 'Stern') — wizard data now matches
  the rest of the platform + honours admin-editable per-port overrides.

tsc clean. 1419/1419 vitest. lint clean on touched files.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 16:50:58 +02:00
449b9497ab fix(uat): batch — timeline overshoot, name-sync, reset-password, dashboard cleanup, queue/seed hygiene + alpha UAT findings doc
UAT findings landed across the last few Playwright + React Grab passes;
single grouped commit so the index doesn't fragment into 30 one-liners.

User & auth:
- `user-settings`: name now updates the avatar + topbar menu after save
  (was reading stale session).
- `me/password-reset`: 3 bugs (token validation, error response shape,
  redirect chain).
- Admin user permission-overrides route honours the same envelope as
  the rest of the admin surface.

Dashboard:
- Removed obsolete `revenue-breakdown-chart` + `dashboard-widgets-card`
  (replaced by the customisable widget grid).
- Strip `revenue_breakdown` from analytics route + use-analytics +
  service + integration test so nothing renders an empty card.
- Activity log timeline overshoot fix (`interest-timeline` +
  `entity-activity-feed`).
- Tightened tiles: active-deals, berth-heat-widget, pipeline-value, kpi-tile.
- `dev-mode-banner`: derive dismissed state synchronously instead of
  via an effect (set-state-in-effect lint rule).

Forms & lists (assorted polish):
- client / company / yacht / interest / reminder forms — validation +
  empty-state copy + tab transitions.
- companies/yachts list tweaks; berth recommender panel; qualification
  checklist; supplemental info request button.

Infra & misc:
- Queue workers (ai / email / notifications) — log shape +
  per-job timeout consistency.
- Auth / brochures / users schema small adjustments; seeds reflect
  permissions matrix changes.
- Scan shell + scanner manifest + AI admin page small fixes.
- `next.config.transpilePackages` adds `echarts`/`zrender`/`echarts-for-react`
  (recommended config from echarts-for-react inside Next).

Docs:
- `docs/superpowers/audits/alpha-uat-master.md` — single rolling
  cross-cutting UAT findings doc (per CLAUDE.md convention).
- `docs/BACKLOG.md`: dashboard stats cards (§I) + activity-log
  normalization (§J).
- 2026-05-18 audit log updated with this batch.
- `CLAUDE.md` — small manual UAT scaffold notes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 15:56:11 +02:00
8c669e2918 feat(berths): bulk price update + per-berth price API
Two new endpoints lift price editing out of the full berth-update form:

- `PATCH /api/v1/berths/[id]/price` — single-berth price edit triggered
  inline from the berth list / detail (no need to open the heavy edit
  modal just to retag a price).
- `POST /api/v1/berths/bulk-update-prices` — multi-row update from a
  selection in the berth list; transactional, audit-logged per row.

Berth list column gets an inline price-edit affordance backed by the
single-berth endpoint; the bulk action lives in the row-selection
toolbar.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 15:54:27 +02:00
b4bf9cca3f feat(branding): multi-tenant brand naming + per-port email shell + auth UI continuity
Removes the last hardcoded "Port Nimara" references so a tenant cloning
the deploy with a fresh slug sees their own brand throughout.

Browser + native chrome:
- `generateMetadata` reads `branding_app_name` from the first port row
  so the browser tab title, apple-web-app title, and template literal
  reflect the tenant (fallback "CRM" until DB is seeded).
- Mobile topbar derives the brand-mark initials from the port slug
  ("port-nimara" → "PN", "marina-alpha" → "MA") — no code edit on clone.
- `documenso-payload` default redirect URL is `""` so Documenso falls
  back to its own post-sign page instead of routing every tenant's
  signers to portnimara.com; per-port `redirectUrl` setting still wins.
- Server-startup log uses generic "CRM server listening".

Email + auth shell:
- New `auth-shell-branding.ts` resolves logo / background / appName once
  per request from `system_settings`; used by both the email shell and
  the auth-pages SSR layout.
- `auth-branding-provider` wraps `/login`, `/reset-password`, `/set-password`,
  portal `/portal/*` so the branded shell hydrates with the same assets
  the inbox sees.
- `me/email` change email uses the branded shell instead of inline HTML
  with "Port Nimara CRM" baked into copy.
- Admin branding page adds an email-preview card (POSTs to
  `/api/v1/admin/branding/email-preview`) so an admin can spot-check
  their templates before going live.
- `/api/public/files/[id]` exposes branding-category files anonymously
  so inbox images (no session cookie) can render; any other category
  still flows through authenticated `/api/v1/files/[id]/preview`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 15:54:10 +02:00
bac253b360 feat(analytics): Umami website-analytics suite — world map, realtime, sessions, heatmap, pixel tracking, tracked links
Adds the read-side Umami integration queued in last week's
website-analytics plan (Phases 1–6 of `docs/website-analytics-flesh-out-plan.md`):

- Realtime panel polls Umami at 5s intervals; world map renders visitor
  origins via echarts + `public/world-map/echarts-world.json` topo.
- Sessions list + session-detail-sheet drill-down (per-session event
  timeline pulled from `/api/v1/website-analytics`).
- Weekly heatmap (day-of-week × hour-of-day) for engagement timing.
- Metric-detail pages under `/[portSlug]/website-analytics/[metric]`
  for pageviews / referrers / events deep-dives.
- Email-pixel write path: `/api/public/email-pixel/[sendId]` 1×1 GIF
  beacon backed by `email_open_tracking` (migration 0076); resolves
  inline on render in inbox.
- Tracked-link redirect: `/q/[slug]` routes through `tracked_links`
  (migration 0077) and forwards to the canonical destination after
  logging the click.
- Dashboard `website-glance-tile` now reads from the live Umami service
  instead of placeholder data.

Deps: `@umami/node`, `echarts`, `echarts-for-react`, `@types/geojson`,
`@types/topojson-client`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 15:53:41 +02:00
292800b643 docs(claude-md): manual UAT scaffold trigger
When the user starts a "manual testing" / "UAT" walkthrough,
auto-scaffold docs/superpowers/audits/YYYY-MM-DD-manual-uat-findings.md
with the standard buckets (quick fixes / medium / features / bugs /
cross-references) so I don't have to re-paste the layout each session.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 19:03:35 +02:00
1b8dacfa54 docs(audit): full codebase audit — 128 findings across 16 areas
Spawned 16-agent sonnet[1m] audit team covering schemas (people/orgs,
pipeline, docs+infra), APIs (public, admin, v1 CRUD, webhooks/auth/
storage), services (EOI/Documenso, domain, observability), background
jobs, UI (admin, entity), and cross-cutting security/performance/tests-
deps. 13 of 16 agents delivered detailed JSON reports; A1/F1/B3 audited
inline after their agents stalled. E1/E2 (admin + entity UI) couldn't
complete in a single spawn — flagged for re-attempt with narrower scope.

Top findings:
- 5 CRITICAL: send-invoice and invoice-overdue-notify silently no-op
  (D1#1); 5 maintenance crons including database-backup scheduled but
  unimplemented (D1#2); tenure-expiry-check ditto (D1#3); GDPR export
  bundles not deleted on RTBF (C3#1, gap in A.7 shipped today);
  residential_clients has no hard-delete path at all (C3#2).
- 15 HIGH including: /api/public/interests doesn't validate portId
  (B1#1, cross-tenant injection); documents.documenso_id has zero
  index (A3#1, every webhook is a full scan); better-auth rate limit
  is in-memory (B4#1, multi-replica bypass); generateAndSignViaInApp
  omits portId on Documenso calls (C1#1); custom-doc-upload calls
  placeFields after distribute (C1#2); {{eoi.berthRange}} +
  {{reservation.*}} tokens never resolved (C1#3); recommender SQL/JS
  stage-scale off-by-one (C2#1); getClientById runs 6 queries serial
  (F2#1); no CI pipeline + zero tests on client-hard-delete (F3#1,2).
- 36 medium, 53 low, 19 info.

Triage groups in the doc:
  Tier S: 7 ship-stopping bugs (today)
  Tier 1: ~12 high-severity items (this week)
  Tier 2: ~36 medium (next sprint)
  Tier 3: ~53 low (rolling)
  Tier 4: re-spawn E1+E2 with narrower scope

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 18:38:10 +02:00
b3f87563c6 feat(audit-cleanup): finish all 15 outstanding items from verified backlog
Audit cleanup completion plan, all tiers shipped:

Tier 1 (security + data integrity)
- A.7 RTBF true wipe: redact email_messages body/subject/addresses for
  threads owned by deleted client; redact document_sends.recipient_email;
  collect file storage keys + delete blobs post-commit.
- A.8 user_permission_overrides FK: documented inline why cascade is
  correct (not set-null as audit suggested) — overrides have no value
  without their user.
- W2.14 PII redaction: camelCase normalization in audit.ts +
  error-events.service.ts isSensitiveKey; added city/postal/country/
  birth fragments. firstName/lastName/dateOfBirth/postalCode etc. now
  caught in BOTH masker paths. 12 new test cases lock the coverage.

Tier 2 (Documenso completion + refactor)
- C.2: documentEvents.recipient_email column + partial unique index for
  per-recipient webhook dedup (migration 0075). handleDocumentSigned
  now sets recipient_email on insert.
- Phase 2: completion_cc_emails distribution. handleDocumentCompleted
  reads documents.completionCcEmails, filters out signer-duplicates
  case-insensitively, fans signed PDF out to non-signer recipients.
- C.4: extracted createPublicInterest() service from the 346-line
  api/public/interests route. Route becomes a thin shell (rate-limit,
  port resolution, audit log, email fan-out). The trio creation logic
  is now unit-testable without an HTTP fixture.
- Phase 4: POST /api/v1/document-templates/[id]/detect-fields wired
  to document-field-detector.detectFields(). Sparkles "Auto-detect"
  button added to template-editor.tsx — maps DetectedField → marker
  with best-guess merge token (DATE / NAME / EMAIL); user retags.

Tier 3 (reporting + recommender snapshot lockfiles)
- W7.reports: extracted rollupStageRevenue / rollupStageCounts /
  computeTotalForecast / computeOccupancyRate / rollupBerthStatusCounts
  into src/lib/services/report-math.ts (pure functions). 16 new tests
  including an inline-snapshot lockfile on a representative 7-stage
  forecast. report-generators.ts now delegates.
- W7.recommender: 18 new toMatchSnapshot tripwires on classifyTier
  boundaries + computeHeat at canonical input points.

Tier 4 (rolling)
- W6.attach: fixed outdated CLAUDE.md claim — threshold banner is
  informational and never depended on IMAP; bounce monitoring (the
  IMAP poller) is separate.
- D.1 + D.2: documented deferral inline with full why-not-build-it
  reasoning so a future engineer sees the rationale.
- G.1: representative formatDate sweep (audit-log-list, user-list,
  document-templates merge tokens, document-signing email). Rest of
  the ~100 sites stay rolling.

Quality gates: 1420/1420 vitest (46 new tests above baseline of 1374),
tsc clean, 0 lint errors.

Plan: docs/superpowers/plans/2026-05-18-audit-cleanup-completion.md
Migration: 0075_c2_document_events_recipient_email.sql (applied to dev DB).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 18:22:36 +02:00
ef0dc5abc4 feat(post-audit): finish Phase 3 / 4 / 5 / 7 — remaining work
Phase 3 — EOI overrides (now ☑):
- Address override field with the same per-component input UX as the
  canonical address form (line1/line2/city/state/postal + ISO
  subdivision + CountryCombobox). Two-checkbox intent semantics
  identical to email/phone — useOnlyForThisEoi writes only to
  documents.override_client_address_* columns; setAsDefault promotes
  to the canonical client_addresses primary inside the override
  transaction; neither flag inserts a non-primary address row for
  future reuse. eoi-context route now returns available.addresses so
  the dialog can render the picker over existing rows.
- yachts.source_document_id backfill — yachts spawned via EOI run
  BEFORE generateAndSign creates the document row, so source_document_id
  stayed NULL. Mirrored the bounded-recent backfill pattern from
  contacts into persistDocumentOverrides for both client_addresses and
  yachts (every row inserted in the last 60s with NULL source_document_id
  and the right source flag gets attributed).
- Audit-log filter chips for the new verbs — eoi_field_override,
  promote_to_primary, eoi_spawn_yacht now appear in /admin/audit
  dropdown + get human labels in the card view.

Phase 4 — reminders inline section (now ☑):
- New <RemindersInline> shared component shows the 3-5 most recent
  open reminders for an entity. Mounted on Overview tab of yacht /
  client / interest detail. Empty state hints at the header button
  rather than duplicating it.

Phase 5 — email tone (now ☑ across all 8 templates):
- admin-email-change, crm-invite, inquiry-sales-notification,
  residential-inquiry — voice + sign-off match the 4 shipped earlier
  ("Dear X", "With warm regards, The {portName} Team", sentence-case
  subjects). Snapshot tests deferred — they'd need a 2nd-port fixture
  set up to catch port-name leaks; templates are correct in review.

Phase 7 — PDF editor (now ☑):
- 7.1 polish: unsaved-changes guard (beforeunload + "Unsaved changes"
  badge), ResizeObserver-driven responsive PDF width, required-tokens-
  unplaced indicator reading template.mergeFields.
- 7.2 drag-to-move with on-page clamping.
- 7.2 four-corner resize handles with min-size enforcement.
- 7.2 right-click context delete via onContextMenu.
- 7.2 multi-page navigation + per-page marker filter.
- 7.2 live preview endpoint POST /api/v1/document-templates/[id]/preview
  runs the in-app pdf-lib fill against the supplied interest, uploads
  to a transient previews/ key, returns a 15-min presigned URL.
- 7.2 new-PDF upload POST /api/v1/document-templates/[id]/source-pdf
  takes multipart FormData, magic-byte verifies %PDF-, parses page
  count via pdf-lib, swaps documentTemplates.sourceFileId. Editor
  warns when the new page count truncates the prior set.

Quality gates: 1374/1374 vitest, tsc clean, lint 0 errors.

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