Bundles the prior autonomous-session output that was sitting unstaged:
- Em-dash sweep across src/ + tests/ (en-dash/em-dash to hyphen, ~2280 instances)
- country-flag-icons rollout (CountryFlag component, replaces emoji glyphs that
never rendered on Windows; lazy-loads the 3x2 SVG index as a single chunk
after the per-subpath dynamic-import approach silently failed in webpack)
- Admin IA Phase 1+2: 7-domain regroup, 41 to 38 pages, /admin/berths index,
redirects (ocr to ai, reports to dashboard, invitations to users),
docs/admin-ia-proposal.md
- Per-template email tester (registry + endpoint + UI on Email admin page)
- Cancel-document mode picker (delete-from-Documenso vs keep-for-audit)
- Dashboard PDF report: 25 widgets, SVG charts, date-range picker, 11 resolvers
- Customize-widgets per-region sortables at xl+ (charts/rails/feed); single
flat sortable below xl when the layout stacks; per-viewport saved orders
- Audit doc updates capturing each shipped item
- Lint fixes: react-compiler immutability in DonutChart (reduce instead of
let-reassign), set-state-in-effect disables in CountryFlag and
UploadForSigning preview-bytes effect, unused 'confirm' destructures in
interest contract + reservation tabs, unescaped apostrophe in test-template
card copy
N44, N45, N46 from the 2026-05-21 plan.
Shipped:
N44 Pipeline Value tile respects dashboard timeframe. Tile accepts
optional `range` prop and threads it through
/api/v1/dashboard/kpis?range=<slug> + /forecast?range=<slug>.
Service functions accept optional {from,to} bounds and scope
the pipeline-value SQL to interests created within the window.
New parseRangeSlug helper inverts rangeToSlug. Widget registry
forwards the active dashboard range to the tile.
N45 Clients by country widget. New GET
/api/v1/dashboard/clients-by-country groups non-archived
clients by nationality_iso. <ClientsByCountryWidget> renders a
compact ranked list with mini-bars; rows link to
/clients?nationality=<ISO>. Registered as default-visible rail.
N46 Drag-and-drop dashboard widgets. New
preferences.dashboardWidgetOrder?: string[] on user_profiles;
useDashboardWidgets sorts visibleWidgets by the order
(unlisted ids fall through to registry order) and exposes
setOrder(nextOrder) that PATCHes optimistically.
DashboardShell wires @dnd-kit/core + sortable: Rearrange toggle
turns on per-widget grip handles + sortable-context wraps each
group (charts / rails / feed) so drops stay in-group.
PointerSensor 8px activation distance, KeyboardSensor for a11y.
New <SortableWidget> wraps the render — zero footprint when
off.
Verified: tsc clean, vitest 1454/1454.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
C20–C23 from the 2026-05-21 plan.
Shipped now:
C21 Dimensions ft/m column toggle persisted to user prefs.
`TablePreferences.dimensionUnit` ('ft' | 'm') added to the user-
profiles JSONB. `useTablePreferences` returns `dimensionUnit` +
`setDimensionUnit` alongside hidden/density. New
`getBerthColumns(unit)` factory rewrites the dimensions /
nominalBoatSize / waterDepth cells when ft is requested
(waterDepth converts on-the-fly from the canonical meters
column at 3.2808 ft/m). Berth-list toolbar gains a small
ft/m toggle button next to the density toggle.
C22 ft/m switching on Berth Requirements rows.
`interest-tabs.tsx` Berth-requirements section now honours
`interest.desiredLengthUnit`. Labels flip to "(m)" when set;
value reads from `desired*M` columns; on save, both the chosen-
unit and the canonical counterpart columns are PATCHed (3.28084
ratio) so downstream surfaces (recommender, EOI merge fields)
stay in lockstep. `InterestPatchField` widened with `desired*M`
variants.
C23 Berth list bulk-edit affordance.
New `POST /api/v1/berths/bulk` (mirror of /interests/bulk):
discriminated union of `change_status` / `change_tenure_type` /
`add_tag` / `remove_tag` / `archive`, 500-id cap, per-row
failure reporting, single `berths.edit` permission gate
(no separate `archive` perm exists on berths today). Status
mutations route through `updateBerthStatus` so under-offer /
sold transitions still trigger the primary interest_berths
auto-link + the rules-engine evaluation.
BerthList toolbar wires `bulkActions` on the DataTable —
Change status (Select dialog), Change tenure (permanent /
fixed-term), Add tag, Remove tag, Archive (destructive +
confirmation). Each dialog uses the same `bulkMutation` so
toast + cache-invalidation behaviour is consistent across
actions.
Already shipped (verified):
C20 Berth list rates / pricing valid columns hidden by default —
already in `BERTH_DEFAULT_HIDDEN`.
Verified: tsc clean, vitest 1454/1454.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two complementary UX upgrades on the berth list:
1. Active-interests popover — replaces the plain "Active interests"
count cell with a click-to-expand popover. Each row shows the
linked deal's client name, pipeline stage (with stage-badge tint),
and a primary-star icon. Lazy-loads on first open (30s stale),
capped at 20 entries server-side, sorted most-recently-updated
first. Backed by `GET /api/v1/berths/[id]/active-interests`.
2. Row-density toggle — DataTable gains a `density: 'comfortable' |
'compact'` prop. Compact drops cell vertical padding from py-3 to
py-1.5 so reps can scan many more berths per viewport on the
high-density admin lists.
Persisted alongside hidden-columns in `user_profiles.preferences.
tablePreferences[entityType].density`. Hook returns `density +
setDensity`; defaults to 'comfortable' for users who haven't
chosen. The setter shares the same debounced PATCH with setHidden
so toggling both doesn't multiply the network round-trips.
Toolbar adds a Rows3/Rows4 icon button between the saved-views
dropdown and the ColumnPicker. tooltip + aria-label flip to
communicate the next state.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
L-001 hunt landed these:
- src/lib/services/clients.service.ts — stageRank used pre-refactor
9-stage names exclusively (`contract_signed`, `deposit_10pct`, …).
Every modern 7-stage interest fell to rank 0, making client-list
"most-progressed deal" sort effectively random. Modern values now
own the canonical ranks; legacy aliases map to their 7-stage
equivalents so historical audit data still sorts.
- src/lib/services/berth-recommender.service.ts — STAGE_ORDER had
the same 9-stage shape. LATE_STAGE_THRESHOLD pointed at the (now
nonexistent) `deposit_10pct` slot. Reworked to the 7-stage scale;
threshold now at `deposit_paid` (5).
- Stale comments referencing `deposit_10pct` in schema (clients,
financial) and client-archive services updated to current copy.
- Smart-archive dialog rendered `i.pipelineStage` as raw enum; now
routes through `stageLabelFor` (the new helper added with A2).
Test fixture updates: berth-recommender.test.ts numeric inputs
re-mapped to the new 7-stage scale (eoi_signed=5 → eoi=3, etc.).
1373/1373 vitest pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Payments (deposit / balance / refund records on an interest) used to
share `invoices.record_payment`, which forces a port that doesn't
issue invoices at all to still navigate the invoicing permission
group to grant its sales reps payment-recording rights. Splitting
the resource lets admins gate the two surfaces independently.
The new resource has three actions:
- view — gates the UI affordance (API reads still go through
`interests.view`)
- record — POST / PATCH a payment
- delete — DELETE a payment record
Seed maps updated for all six system roles; existing role rows +
per-user permission overrides are backfilled by migration 0064 so
upgrades don't silently lose access. Two call sites (POST /interests/
[id]/payments, PATCH /payments/[id]) → payments.record; one
(DELETE /payments/[id]) → payments.delete. The PermissionGates on the
payments-section UI swap to the new keys.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Swap the one outlier (client-interests-tab.tsx) from Vaul Drawer to
Sheet side=right so every detail-preview surface uses the same
primitive. Document the doctrine: Sheet for side panels on both desktop
and mobile; Vaul Drawer reserved for mobile-only bottom-sheet UX
(currently just MoreSheet).
Closes ui/ux M11.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mobile + responsive
- berth-form full-width on phones (was 480px fixed → overflowed iPhone)
- currency-input switched to inputMode=decimal with live thousands separator
- client-form Country/Timezone/Source/Preferred-Contact full-width <sm
- contacts row restructured so Primary toggle + Remove get their own strip
- customize-dashboard footer stacks vertically on mobile; Done full-width
- interest-form client/berth pickers no longer cmdk-filter on UUID (typing
"Carlos" now returns Carlos Vega instead of "No clients found")
Data + consistency
- SOURCES + SOURCE_LABELS + formatSource() in lib/constants; 9 surfaces
now resolve interest/client source from one place
- INTEREST_OUTCOMES adds lost_other (picker, badge, timeline)
- Berth options natural-sort A1 → A2 → … → A10 via lib/utils/mooring-sort
- archiver downgraded ^8 → ^7.0.1 so the GDPR export route compiles
- TableBody last-row uses border-b-0 (not border-0); colored left-accent
on the bottom berth row now renders
- Hide Invite-to-Portal until port setting === true (was !== false default-show)
- OwnerPicker primer query resolves entity name on first paint (no more
UUID flash before the popover opens)
Terminology
- Replaced user-facing "Documenso" with "signing service" / "Generated EOI" /
"Manual EOI" in 8 components (admin/internal references kept)
- Plainer status-change copy on berth-detail-header
Forms + editing
- InlineEditableField gained a `date` variant (native picker); applied to
company incorporation date and ready for other YYYY-MM-DD plaintext fields
- Inline source picker on interest-tabs detail (was free text)
- TagPicker self-hides when port has no tags AND nothing is selected
- New ReminderDaysInput with preset chips (1d / 3d / 1wk / 2wk / 1mo / custom)
- Compose dialog follow-up is now a toggle that reveals datetime picker
Pipeline milestones
- changeStageSchema accepts optional milestoneDate; service stamps it on the
matching date column instead of always using now
- MilestoneAdvanceButton popover collects a back-date before stage advance
- Applied to every "Mark X manually" surface on the interest overview
EOI / linked-berths polish
- Add-bypass row aligned inline with toggle descriptions
- Tooltips on "Specifically pitching" / "Mark in EOI bundle" explain their
legal vs. public-map consequences
Surfaces
- Companies list now has the column picker + persisted hidden-column prefs
- NotesList aggregate flag enabled on clients, companies, residential_clients
(yachts already aggregated)
ft/m unit toggle (interim, before drift fix)
- "Berth size desired" gets a section-level ft/m toggle; per-field hint shows
the converted value. Storage stays canonical-ft for now; the drift-safe
persistence migration is the next step.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirrors files.manage_folders. Gates create / rename / move / delete
of document folders, plus moving documents between folders. Reps with
documents.edit but not manage_folders can rename docs in place but
can't reorganise the tree. Admin + sales_manager get the perm by
default; sales_rep + viewer don't.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two related cleanups for the user profile surface area:
(1) Add canonical first_name + last_name columns to user_profiles.
Migration 0049 backfills from display_name by splitting on the
first whitespace run; single-token names land as
(display_name, NULL) so we never throw away existing data.
Display name becomes an optional override (nicknames, vanity
formatting). /api/v1/me PATCH now accepts firstName/lastName,
and the user-settings form surfaces them as the primary inputs
with display name as a secondary "How your name appears" field.
(2) Remove the broken Notifications card from user-settings (it called
PATCH on an endpoint that has GET/PUT only and used a flat shape
vs the actual array shape). Replace with the working
NotificationPreferencesForm + ReminderDigestForm under a
#notifications anchor. /notifications/preferences becomes a
server-side redirect to /settings#notifications for back-compat;
the mobile More-sheet + user-menu Bell entry now deep-link to the
new anchor directly.
Drops the auto-generated drizzle-kit catch-up migration so we're not
sneaking accumulated schema drift into the journal — only the targeted
0049 lands here.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Major interest workflow expansion driven by the rapid-fire UX session.
EOI / Contract / Reservation tabs replace the generic Documents tab when
the deal is at the relevant stage — workspace pattern with active-doc
hero, signing progress, paper-signed upload, and history strip. Stage-
conditional visibility wired through interest-tabs.tsx so the tab set
shrinks/expands as the deal moves through the pipeline.
Contact log: per-interaction structured log (channel/direction/summary/
optional follow-up reminder). New `interest_contact_log` table + service
+ tab UI (timeline with channel-coded icons + compose dialog).
auto-creates a reminder when followUpAt is set.
Berth Interest milestone: first milestone in the OverviewTab's pipeline
strip, completes the moment any berth is linked via the junction. Drives
the "have we captured what they want?" sanity check for general_interest
leads before they move to EOI.
Stage-conditional milestones: past phases collapse into a one-liner
strip, current phase expands, future phases hide behind a "Show
upcoming" toggle. Inline stage picker now defers reason capture to an
override-confirm view (only required for illegal transitions, not the
default flow).
Notes blob → threaded: dropped `interests.notes` column entirely; the
threaded `interest_notes` table is the single source of truth. Latest-
note teaser on Overview links into the dedicated Notes tab. Polymorphic
notes service gains aggregated client view (unions client + interest +
yacht notes with source chips and group-by-source toggle).
Berth interest list overhaul:
- Configurable columns via ColumnPicker (18 toggleable, 5 default-on)
- Natural-sort SQL ORDER BY on mooring number (A1, A2, A10 not A10, A2)
- Per-letter row tinting via colored left-border accent + dot in cell
- Documents tab merged Files (single attachments section)
Topbar improvements:
- Always-visible back arrow on detail pages (path depth > 2)
- Breadcrumb-hint store + useBreadcrumbHint hook so detail pages can
push their entity hierarchy (Clients › Mary Smith › Interest › B17)
- Tighter spacing, softer separators, 160px crumb truncation
DataTable upgrades:
- Page-size selector with All option (validator cap raised to 1000)
- getRowClassName slot for per-row styling (used by berth tinting)
- Fixed Radix SelectItem crash on empty-string values via __any__
sentinel (was crashing every list page that opened a select filter)
Interest list:
- Configurable columns picker
- Stage cell clickable into detail
- TagPicker + SavedViewsDropdown sized h-8 to match adjacent buttons
- Save view moved into ColumnPicker menu; Views button hidden when
no views are saved
- Pipeline kanban board endpoint at /api/v1/interests/board with
minimal projection, 5000-row cap + truncated banner, filter
pass-through
Mobile chrome + sidebar collapse removed (always-expanded design choice).
User management lists super-admins (was inner-joined on user_port_roles
which excluded global super-admins).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Permanent client deletion is now reachable from:
- archived single-client detail page (icon button, gated by new
admin.permanently_delete_clients perm)
- archived clients list bulk action
Both flows are 2-stage: request a 4-digit code (sent to operator's
account email, 10min Redis TTL), then enter both code AND a typed
confirmation (client name single, "DELETE N CLIENTS" bulk). Cascade
strategy preserves audit trails: signed documents, email threads,
files and reminders are detached but retained; addresses, contacts,
notes, portal user, GDPR records, interests and reservations are
deleted via FK cascade or explicit tx delete.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Manual stage override
Sales reps need to skip canTransitionStage rules when the data was
entered out of order — e.g. recording a contract_signed deal whose
earlier stages were never tracked in the system.
- New permission flag interests.override_stage in RolePermissions.
Plumbed through the schema TS type, the role-editor UI, the seed
file's pre-built roles (super_admin/director/sales_manager get it,
sales_agent + viewer don't), and the test factories.
- changeStageSchema gains an optional `override` boolean and the
service checks it before evaluating canTransitionStage. When
override=true the reason field becomes required (min 5 chars) and
is recorded in the audit log.
- The route handler gates `override` on the new permission so a
sales_agent without it can't pass override=true and bypass.
- InterestStagePicker auto-detects when the requested transition is
blocked by the table and switches into "override mode" — shows an
amber warning, requires the reason, button label flips to
"Override stage". When the operator lacks the permission, the
warning is red and the button is disabled.
Residential Partner role
Per the smart-archive scoping conversation: external partners who
handle residential inquiries shouldn't see marina clients, yachts,
berths, or financials. The two residential_* permission groups
already exist; this commit just seeds a pre-built system role
("residential_partner") with those flags + minimal own-reminders, so
admins can invite a partner today via /admin/users without manually
building the permission set.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the five highest-risk findings from
docs/audit-comprehensive-2026-05-05.md so the platform is not exposed
while the rest of the audit backlog (1 CRIT + 18 HIGH + 32 MED + 23 LOW)
is worked through:
* CVE-2025-29927 — bump next 15.1.0 → 15.2.9; nginx strips
X-Middleware-Subrequest at the edge as defense-in-depth.
* Cross-tenant role escalation — POST/PATCH/DELETE on /admin/roles now
require super-admin (was: any holder of admin.manage_users). Adds
shared `requireSuperAdmin(ctx)` helper.
* Silent-403 traps — `documents.edit` and `files.edit` keys added to
RolePermissions; seeded role values updated; migration 0041 backfills
the new keys on every existing roles+port_role_overrides JSONB. File
routes remap the dead `create` action to `upload` / `manage_folders`.
* Berth-PDF / brochure register endpoints — reject body.storageKey
unless it matches the namespace the matching presign endpoint issued
(prevents repointing a tenant's PDF at foreign-port bytes).
* Portal auth rate limits — sign-in 5/15min/(ip,email),
forgot-password 3/hr/IP, activate/reset/set-password 10/hr/IP. Adds
`enforcePublicRateLimit()` for non-`withAuth` routes.
Test status unchanged: 1168/1168 vitest, tsc clean.
Refs: docs/audit-comprehensive-2026-05-05.md (CRITICAL, HIGH §§1–4)
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>