Commit Graph

13 Commits

Author SHA1 Message Date
4b5f85cb7d fix(audit): comprehensive 2026-05-15 audit fix wave + Documenso v2 polish
Bundles the prior session's 50-task fix sweep (Documenso v2 + EOI/signing-
progress redesign + env-to-admin migration + dev-mode banner) with the
2026-05-18 audit fix wave (3 CRITICAL, 14 HIGH, 28 MEDIUM, 6 LOW).

CRITICAL (3):
 - C-01 interest-berths INNER JOIN -> LEFT JOIN so hard-deleted berths
   no longer silently drop interest links
 - C-02 /setup added to PUBLIC_PATHS; fresh-deploy bootstrap loop fixed
 - C-03 generic PATCH /interests/[id] no longer accepts pipelineStage —
   callers must go through /stage with the override-guard chain

HIGH (14/15):
 - H-01 explicit ON DELETE on previously-implicit NO ACTION FKs across
   interests/documents/reservations/reminders/invoices (migration 0070)
 - H-02 login page reads ?redirect= param with same-origin guard
 - H-03 CRM invite token moves to URL fragment so it never lands in
   nginx access logs / Referer headers
 - H-04 Retry-After header on sign-in-by-identifier 429 (RFC 6585 §4)
 - H-05 toggleAccount writes an audit row
 - H-06 upsertSetting masks any value whose key ends with _encrypted
 - H-07 archiveClient cascade fires per-interest audit rows
 - H-08 createSalesTransporter applies SMTP_TIMEOUTS
 - H-09 AppShell stable children — viewport flip across breakpoint no
   longer destroys in-progress form drafts
 - H-10 portal documents page swaps Unicode glyph status icons for
   Lucide CheckCircle2/XCircle/Circle + aria-labels
 - H-12 list components swap alert(...) for toast.warning(...)
 - H-13 5 icon-only buttons gain aria-label
 - H-14 parseBody treats empty bodies as {}
 - H-15 admin layout renders a 403 panel instead of silent bounce
 - H-11 not applicable — mobile-search-overlay IS a mobile bottom-sheet

MEDIUM (28+):
 - M-MT01-05 defense-in-depth port_id/parent-id filters on UPDATE/DELETE
   WHEREs across custom-fields, notes (all 6 entity types x update +
   delete), client-contacts, yacht ownerClient lookup, webhook reads
 - M-D01 documents-hub realtime event-name typo (file:created -> uploaded)
 - M-EM01 portal-auth emails thread through portId
 - M-EM02 sendEmail accepts cc/bcc params
 - M-EM04 notification_digest catalog key
 - M-IN01 portal presigned download URLs use 4h TTL
 - M-IN02 OpenAI client lazy-instantiated
 - M-IN04 stale pdfme refs updated to pdf-lib AcroForm
 - M-IN05 umami.testConnection returns tagged union
 - M-L01 reservations tenure_type unified with berths
 - M-L02 report-generators canonicalize stage values
 - M-AU01 audit log placeholder copy fixed
 - M-AU04 outcome_set / outcome_cleared distinct audit verbs
 - M-NEW-2 activity feed entity name+type separator
 - M-R01 portal allowlist narrowed + portal_session backstop in proxy
 - M-SC02 companies archived partial index
 - M-SC04 audit_logs.searchText documented as DB-managed
 - M-S01 storage_s3_access_key_encrypted admin field
 - M-U01 audit log empty state uses <EmptyState>
 - M-U09 invoice delete dialog -> <AlertDialog>
 - M-U10 toast.success on ClientForm + InterestForm create/edit
 - M-U11 settings-form-card logo preview alt text
 - M-U14 mobile topbar title on clients/yachts/interests/berths
 - M-U15 Invoices in mobile More-sheet

LOW (6/8):
 - L-AU01 severity defaults for security-relevant verbs
 - L-AU02 +13 missing actions in admin audit filter
 - L-AU03 +7 missing entity types in admin audit filter
 - L-AU04 dead listAuditLogs stubbed
 - L-D02 CLAUDE.md Owner-wins chain tightened

Bonus — Document detail polish (#67 partial, 3/6 deliverables):
 - state-aware action button per signer
 - watcher Add UI with display-name resolution
 - cleanSignerName cleanup

Prior session work bundled in:
 - Documenso v2 webhook + envelope-ID normalization + sequential signing
 - SigningProgress UI redesign (avatars, per-signer state, timestamps)
 - env->admin settings registry + RegistryDrivenForm + encrypted creds
 - Embedded-signing card + Test connection + setup help
 - Dev-mode EMAIL_REDIRECT_TO banner
 - Pipeline rules admin page
 - Sales email config card
 - Audit log details Sheet
 - EOI tab: Finalising badge, absolute timestamps, sequential indicator
 - Notes pipeline_stage_at_creation (migration 0069)
 - Documenso numeric ID dual-key webhook (migration 0068)
 - Dimensions criterion copy (migration 0067)

Tests: 1374/1374 vitest pass. tsc clean. lint clean.

See docs/AUDIT-FIX-WAVE-2026-05-18.md for the full progress report and
the user-input items still pending.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 13:28:50 +02:00
080e1fa454 perf(audit-log): wire DataTable virtual prop on audit-log-list
Audit log entries accumulate via cursor pagination — the user can
load many pages into the same client-side array. With virtual=true
the table only renders the visible viewport rows (plus overscan), so
a 10k-row session stays at 60fps instead of choking on a full DOM
write per "Load more" click.

The other two BACKLOG candidates (super-admin port switcher, client
export modal preview) aren't present in the current codebase — the
super-admin route group hasn't been built and the export modal is
download-only. Skip until those surfaces exist.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 03:50:34 +02:00
ebdd8408bf fix(audit-wave-11): dossier sweep — error-ux + webhook + storage + search + maintainability
Final pass over the unaddressed AUDIT-2026-05-12 dossiers, taking the
tractable Critical/High items from each:

error-ux-auditor (5 items)
- C2: 17 toast.error(err.message) sites swept to toastError(err, …) so
  every user-visible failure carries a copy-paste Reference ID
- C3: apiFetch synthesizes a client-side correlation id when a 5xx
  comes back with a non-JSON body (reverse-proxy HTML pages); message
  becomes "The server is unreachable. Please try again." with code
  UPSTREAM_UNREACHABLE
- C4: checkRateLimit fails OPEN when Redis is unavailable so an outage
  no longer 500s login + portal sign-in; logged at warn so monitoring
  catches it
- H2: StorageTimeoutError (name='TimeoutError') replaces the plain
  Error throw in s3.ts withTimeout — error-classifier hints fire now
- H5: errorResponse() adopted across /api/storage/[token],
  /api/public/website-inquiries, and the Documenso webhook body (drops
  the "Invalid secret" reconnaissance string)

outbound-webhook-auditor (5 items)
- C1: signature is now HMAC(secret, `${ts}.${body}`) with the
  timestamp surfaced as X-Webhook-Timestamp so receivers can reject
  replays outside a freshness window
- C3: dead-letter with reason missing_signing_secret when secret is
  null (defence-in-depth against DB tampering / future migration
  mistakes)
- H2: webhooks queue bumped to maxAttempts=8 with 30 s base
  exponential backoff so a 30 s receiver blip during a deploy no
  longer dead-letters every in-flight event; per-queue
  backoffDelayMs added to QUEUE_CONFIGS
- M1: SSRF denylist gains Oracle Cloud metadata 192.0.0.192
- M2: dispatch-time https:// assertion before fetch, so a bad DB edit
  can't slip plaintext through

storage-pathing-auditor (2 items)
- H1: berth-PDF presigned-upload keys now `${portSlug}/berths/…/…`
  with portSlug threaded into backend.presignUpload — engages the
  filesystem-proxy port-binding `p` token verifier
- H2: presignDownloadUrl auto-derives portSlug from the key's first
  segment when callers don't pass it, so all 8 download sites engage
  the `p`-token guard without per-site plumbing

search-auditor (1 item)
- H3: removed dead void wantEmail; void wantPhone; pair plus the
  unused looksLikeEmail helper — the bucket-reorder it was scaffolded
  for was never wired

maintainability-auditor (1 item)
- M2: swept seven abandoned `void <symbol>` markers and their dead
  imports across clients/bulk, interests/bulk, admin/email-templates,
  admin/website-submissions, alert-rules, and notes.service

Deferred to future work (substantial refactors, schema migrations, or
multi-file UI work):
- error-ux M3-M8 (global-error.tsx, per-route loading.tsx coverage,
  ErrorBanner component, /api/ready route, worker DLQ admin surface)
- maintainability C1-C4 (documents/search/notes service splits,
  interest-tabs split — multi-hour refactors)
- currency C1-H5 (mixed-currency dashboard aggregation, FX history
  table, rounding policy) — wait for second non-USD port
- outbound-webhook C2 (deliveries reaper job), H1 (DNS-rebind TOCTOU
  with undici Agent), H3 (circuit-breaker), H5 (presigned-post-policy)
- storage-pathing C2 (orphan reaper), H3-H5 (streaming + content-type
  binding)

Tests: 1315/1315 vitest  ; tsc clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 13:27:32 +02:00
c8ea9ec0a0 fix(audit-wave-10): aria-hidden sweep on decorative Lucide icons (#69)
Mechanical codemod added \`aria-hidden\` to 444 self-closing single-line
Lucide icon JSX elements across 267 .tsx files in:

- shared/, layout/, dashboard/
- admin/ (all sections)
- clients/, berths/, yachts/, companies/, interests/, documents/
- reminders/, reservations/, residential/, expenses/, email/

The regex targeted only the safe pattern \`<IconName className="..." />\`
(no other props, self-closing, capitalized component name). Every match
inspected is a decorative companion to visible text or sits inside a
button whose accessible name comes from \`aria-label\` / sr-only text
— the icon itself should not be announced.

Screen readers no longer double-read the icon + the adjacent label
text (e.g. "Pencil Pencil Edit" → just "Edit"). The existing
@axe-core/playwright smoke test (\`20-accessibility.spec.ts\`) continues
to pass.

Test suite stays at 1315/1315 vitest. typescript clean.

Closes task #69 (aria-hidden sweep) from the AUDIT-2026-05-12 follow-ups
backlog.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 12:37:22 +02:00
4233aa3ac3 fix(audit-wave-9): standardize on Sheet for previews; doctrine in CLAUDE.md
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>
2026-05-13 11:50:07 +02:00
5c8c12ba1f feat: autonomous backlog push — admin UX overhaul + storage parity + residential parity + Documenso Phase 1
Some checks failed
Build & Push Docker Images / lint (push) Successful in 1m32s
Build & Push Docker Images / build-and-push (push) Failing after 32s
Massive multi-area push driven by docs/admin-ux-backlog.md. Every byte
path now goes through getStorageBackend() so signed EOIs, contracts,
brochures, berth PDFs, files, avatars, branding logos, and DB backups
all work identically on S3 and filesystem backends.

USER SETTINGS (rebuild)
  - Country + Timezone selectors with cross-defaulting
  - Browser-detected timezone banner ("Looks like you're in Europe/Paris…")
  - Email change with verification flow (user_email_changes table,
    OLD-address cancel link + NEW-address confirm link)
    + EMAIL_CHANGE_INSTANT=true dev shortcut
  - Password reset triggered via better-auth requestPasswordReset
  - Profile photo upload + crop (square 256×256) via shared
    <ImageCropperDialog> + /api/v1/me/avatar

BRANDING
  - Shared <ImageCropperDialog> using react-easy-crop
  - Logo upload + crop in /admin/branding (writes via
    /api/v1/admin/settings/image -> storage backend)
  - Email header/footer HTML defaults injectable via "Insert default"
  - SettingsFormCard new field types: timezone (combobox), image-upload

STORAGE ADMIN OVERHAUL
  - S3 config form FIRST, swap action SECOND
  - Test connection before any switch
  - Two-button switch: "Switch + migrate" vs "Switch only" with
    warning modals
  - runMigration() honours skipMigration flag
  - /api/ready + system-monitoring health check use the active
    storage backend instead of always probing MinIO
  - Filesystem backend already had full feature parity — verified

BACKUP MANAGEMENT (real)
  - New backup_jobs table (id / status / trigger / size / storage_path)
  - runBackup() service spawns pg_dump --format=custom, streams to
    active storage backend via getStorageBackend().put()
  - /admin/backup page: trigger, history, download .dump for restore
  - Super-admin gated

AI ADMIN PANEL
  - /admin/ai consolidates master switch + monthly token cap +
    provider credentials
  - Per-feature settings (OCR, berth-PDF parser, recommender)
    linked from the same page

ONBOARDING WIZARD
  - /admin/onboarding now real with auto-checked steps
  - Reads each setting key + lists endpoint (roles/users/tags) to
    decide completion
  - Manual checkboxes for steps without an auto-detect signal
  - Progress bar + Mark done/Mark incomplete buttons
  - State persisted in system_settings.onboarding_manual_status

RESIDENTIAL PARITY (full)
  - New residential_client_notes + residential_interest_notes tables
    (mirror marina-side shape)
  - Polymorphic notes.service.ts extended (verifyParent, listForEntity,
    create, update, delete) for residential_clients/_interests
  - <NotesList> component accepts the new entity types
  - 4 new note endpoints (GET/POST/PATCH/DELETE for clients + interests)
  - 2 new activity endpoints (residential clients + interests)
  - residential-client-tabs.tsx + residential-interest-tabs.tsx use
    DetailLayout (Overview / Interests / Notes / Activity)
  - residential-client-detail-header.tsx mirrors marina-side strip
  - useBreadcrumbHint wired into both detail components
  - Configurable Assigned-to dropdown (residential_interests.view perm)

CONFIGURABLE RESIDENTIAL STAGES
  - residential-stages.service.ts with list / save / orphan-check
  - /api/v1/residential/stages GET/PUT
  - /admin/residential-stages admin UI with reassign-on-remove modal
  - Validators relaxed from z.enum to z.string

DOCUMENSO PHASE 1
  - Schema: document_signers.invited_at / opened_at /
    last_reminder_sent_at / signing_token (+ idx_ds_signing_token)
  - Schema: documents.completion_cc_emails (text[]) +
    auto_reminder_interval_days (int)
  - transformSigningUrl() now maps SignerRole -> URL segment via
    ROLE_TO_URL_SEGMENT (approver->cc, witness->witness) — fixes
    Risk #5 where approver invites landed on /sign/error
  - POST /api/v1/documents/[id]/send-invitation with auto-pick of
    next pending signer
  - Per-port settings: documenso_developer_label / _approver_label
    + documenso_developer_user_id / _approver_user_id (Phase 7
    Project Director RBAC binding fields)

ADMIN UX RAPID-FIRE
  - Sidebar collapse removed (always-expanded design)
  - Audit log: input sizes (h-9), date pickers w-44, action cell
    sub-label so single-row entries aren't blank
  - Sales email config: token list <details> + tooltips on
    threshold + body fields
  - Custom Settings card: long-form description
  - Reminder digest timezone uses TimezoneCombobox
  - Port form: currency dropdown (10 common currencies) + timezone
    combobox + brand color picker
  - Permissions count badge opens modal with granted/denied per
    resource
  - Role names display-normalized via prettifyRoleName
  - Tag form: native input type=color
  - Custom Fields page: amber heads-up about non-integration
  - Settings manager: select field type + fallthrough_policy as dropdown
  - Storage admin S3 fields ship as proper password + boolean

LIST PAGES
  - Residential client list: clickable email/phone (mailto/tel/wa.me)
  - Residential interests + Documents Hub search inputs sized h-9

CURRENCY API
  - scripts/test-currency-api.ts verifies live Frankfurter fetch
    -> DB upsert -> getRate -> convert. Inverse-rate drift <=0.001

TESTS
  - 1185/1185 vitest passing
  - tsc clean
  - eslint 0 errors (16 pre-existing warnings)

Note: WEBSITE_INTAKE_SECRET added to .env.example but committed
separately due to pre-commit hook policy on .env* files.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 21:02:12 +02:00
Matt Ciaccio
b4fb3b2ca6 fix(audit): MEDIUMs sweep — mobile More-sheet, portal profile, inline override, dialog UX, ext-EOI gate
R2-M11: mobile More-sheet missing 4 destinations. Added Reservations,
Notifications, Residential, Website analytics — anyone using mobile
chrome to triage on the go can now reach those domains.

R2-M12: portal had no profile / change-password surface. New
/portal/profile page with read-only contact details + a
ChangePasswordForm component, backed by a new POST
/api/portal/auth/change-password endpoint and
changePortalPassword() service function. Audits both ok and failure
cases at warning severity. Added Profile to PortalNav.

R2-M1: portal dashboard "My Memberships" tile had no href and no
/portal/memberships route — dead-end on tap. Hidden until a
memberships page ships; the count remains in the underlying data.

R2-M7: InlineStagePicker never sent override:true so users with
interests.override_stage couldn't actually use the perm from the
inline chip — they had to fall back to the modal picker. Now the
picker auto-detects when a transition isn't legal AND the user has
override_stage, sets override:true, and supplies a default reason.

Frontend M2: hard-delete-dialog confirm stage now has a "Send a new
code" link in case the original expired before the user could enter
it. Avoids forcing a full Cancel + reopen.

Frontend M4: audit-log-list date-range validation. From > To now
shows an inline error and skips the request rather than firing an
empty-range query that surfaces "no entries found".

R2-M6: external-EOI route now requires interests.edit AND
documents.upload_signed (defense-in-depth) — uploading a signed EOI
mutates interest state, so the upload-signed perm alone shouldn't
let a custom role flip an interest.

1175/1175 vitest passing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 22:38:59 +02:00
Matt Ciaccio
5fc68a5f34 fix(audit): frontend HIGHs — surface fetch errors, kill href=#, invalidate queries, toast over alert
R2-H10: webhook-delivery-log and audit-log-list both swallowed fetch
errors silently — failed loads showed spinner forever or stale data.
Both now set a loadError state, show an inline retry banner, and fire
a toast.error. Same applies to audit-log loadMore.

R2-H11: audit-log-card rendered as `<a href="#">` — tapping on mobile
inserted `#` in the URL and scrolled to top (back-button trap).
ListCard now treats `href` as optional and renders a non-link `<div>`
when omitted; audit-log-card no longer passes href.

R2-H12: smart-archive-dialog only invalidated ['clients'] / ['berths']
/ ['interests']. Detail header kept showing Archived=false until hard
reload. Now also invalidates ['clients', clientId] and removes the
['client-archive-dossier', clientId] cache so re-open re-fetches.

R2-H13: client-list bulk mutation used native alert() on partial
failure (blocking the page) and had no onError handler. Replaced with
toast.warning / toast.success / toast.error.

1175/1175 vitest passing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 22:18:14 +02:00
Matt Ciaccio
d2171ea79b feat(audit): comprehensive logging — auth events, severity, source, IP
Audit log was previously silent on authentication and on background
work. This wires:

- Login (success + failed) and logout via a wrapper around better-auth's
  [...all] handler. Failed logins are severity 'warning' and carry the
  attempted email so brute-force attempts surface in the inspector.
- New severity (info|warning|error|critical) and source (user|auth|
  system|webhook|cron|job) columns on audit_logs. permission_denied
  defaults to 'warning', hard_delete to 'critical'.
- Webhook delivery success/failure/DLQ/retry now write audit rows
  alongside the webhook_deliveries detail table.
- IP address is now visible as a column in the inspector (was already
  captured at the helper level).
- Audit UI: severity badges per row, severity + source dropdowns, IP
  column, expanded action filter covering hard-delete, webhook events,
  job/cron events.

Migration 0044 adds the two columns + their port-scoped indexes.
1175/1175 vitest passing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 20:35:34 +02:00
Matt Ciaccio
8699f81879 chore(style): codebase em-dash sweep + minor layout polish
Some checks failed
Build & Push Docker Images / lint (push) Failing after 1m18s
Build & Push Docker Images / build-and-push (push) Has been skipped
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>
2026-05-04 22:57:01 +02:00
Matt Ciaccio
bcea28cd71 feat(mobile): mobile cards for reminders, audit log, users
Three new <EntityCard> files using the shared <ListCard> shell, wired
into each list page's <DataTable> via cardRender.

  - ReminderCard:   Bell icon, related-entity subtitle (User/Anchor/
                    FileText icon by entity type), due-date meta with
                    past-due flag, accent bar (rose=past-due,
                    amber=pending, slate=snoozed, emerald=done).
                    Snooze/Complete/Edit/Delete in actions menu.
  - AuditLogCard:   Action icon (Plus/Pencil/Trash2/Eye), entity
                    title, "{verb} by {actor}" subtitle, timestamp
                    meta, optional changed-field chip line. Accent
                    bar by action (created=emerald, updated=blue,
                    deleted=rose). Immutable, no actions menu.
  - UserCard:       Initials avatar, displayName/email, role meta
                    (Shield icon), last-login distance, "Inactive"
                    pill when deactivated. Accent bar (violet=
                    super_admin, slate=inactive, none=active).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 15:39:06 +02:00
Matt Ciaccio
f52d21df83 feat(phase-b): ship analytics dashboard, alerts, scanner PWA, dedup, audit view
Phase B (Insights & Alerts) PR4-11 in one drop. Builds on the schema +
service skeletons committed in PRs 1-3.

PR4  Analytics dashboard — 4 chart types (funnel/timeline/breakdown/source),
     date-range picker (today/7d/30d/90d), CSV+PNG export per card.
PR5  Alert rail UI + /alerts page — topbar bell w/ live count, dashboard
     right-rail, three-tab page (active/dismissed/resolved), socket-driven
     invalidation. Bell lazy-loads list on popover open to keep cold pages
     fast in non-dashboard routes.
PR6  EOI queue tab on documents hub — filters to in-flight EOIs, count
     surfaces in tab label.
PR7  Interests-by-berth tab on berth detail — replaces the stub.
PR8  Expense duplicate detection — BullMQ job runs scan on create, yellow
     banner on detail w/ Merge / Not-a-duplicate, transactional merge
     consolidates receipts and archives the source.
PR9  Receipt scanner PWA + multi-provider AI — port-scoped /scan route in
     its own (scanner) group with no dashboard chrome, dynamic per-port
     manifest, OpenAI + Claude provider abstraction, admin OCR settings
     page (port-level + super-admin global default w/ opt-in fallback),
     test-connection endpoint, manual-entry fallback when no key is
     configured. Verify form always shown before save — no ghost rows.
PR10 Audit log read view — swap to tsvector full-text search on the
     existing GIN index, cursor pagination, filters for entity/action/user
     /date range, batched actor-email resolution.
PR11 Real-API tests — opt-in receipt-ocr.spec (admin save+test, optional
     real-receipt parse via REALAPI_RECEIPT_FIXTURE) and alert-engine
     socket-fanout spec gated behind RUN_ALERT_ENGINE_REALAPI. Both skip
     cleanly without their gate envs so CI stays green.

Test totals: vitest 690 -> 713, smoke 130 -> 138, realapi +2 opt-in.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 17:21:55 +02:00
8df8ded46c Add user settings, audit log, berth CRUD, and missing endpoints
- PATCH /api/v1/me: self-service profile update (name, phone, timezone)
- User settings page with profile editor + notification preferences
- Audit log API with filtering (entity, action, user, date range)
- Audit log page with search, entity type, and action filters
- Berth create/delete: POST /api/v1/berths + DELETE /api/v1/berths/[id]
- Client duplicates endpoint: GET /api/v1/clients/duplicates?name=
- Replace settings and audit stub pages with real implementations

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 19:45:56 -04:00