Commit Graph

121 Commits

Author SHA1 Message Date
d15f5509ad docs(audit): progress report for the 2026-05-15 fix wave
All checks were successful
Build & Push Docker Images / lint (push) Successful in 2m6s
Build & Push Docker Images / build-and-push (push) Successful in 1m13s
11 of 13 known issues (A1-A20) fixed and verified; legacy-stage rank
tables in clients.service.ts + berth-recommender.service.ts purged of
9-stage enum keys. 1373/1373 vitest pass.

Remaining catalog (300+ checks) listed by section so it's clear what's
covered vs. still on the to-do list.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 01:22:14 +02:00
3b3ac287e0 docs(audit): comprehensive 320+ check catalog organized by area
All checks were successful
Build & Push Docker Images / lint (push) Successful in 2m6s
Build & Push Docker Images / build-and-push (push) Successful in 22s
Companion to the 2026-05-15 sweep findings. Catalogues every audit-worthy
surface across 19 areas:

  0. Already-known issues (A1-A20 cross-reference)
  1. Legacy stage enum bleed (the deposit_10pct class) — 20 checks
  2. Routes / page reachability — 30 checks
  3. UX consistency (forms, lists, tables, badges, modals, mobile) — 100 checks
  4. Sales workflows happy + edge cases — 52 checks
  5. Admin workflows — 60 checks
  6. Multi-tenancy port isolation — 11 checks
  7. Security — 30 checks
  8. Realtime / sockets — 9 checks
  9. Performance — 14 checks
  10. Documents / files — 22 checks
  11. Audit log surface — 14 checks
  12. Email / SMTP / IMAP — 19 checks
  13. Integrations (Documenso, NocoDB, S3, AI, BullMQ) — 29 checks
  14. Schema / migration — 15 checks
  15. i18n / l10n — 8 checks
  16. Browser / device — 7 checks
  17. Specific behavioral correctness (legacy stage drift, A1 hard-delete fallout, etc) — 22 checks
  18. Data clean-up jobs — 5 checks
  19. CI / dev experience — 13 checks

Each check tagged with effort (XS/S/M/L), severity (🔴/🟠/🟡/🟢), and
current coverage (/⚠️//). Recommended priority tiering at the bottom.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 00:54:08 +02:00
ff5e71092e docs(audit): 2026-05-15 comprehensive Playwright sweep findings
All checks were successful
Build & Push Docker Images / lint (push) Successful in 2m7s
Build & Push Docker Images / build-and-push (push) Successful in 22s
Covers super-admin, sales-rep, viewer, portal, catch-up wizard, and the
single-tree responsive shell. 13 findings catalogued with reproduction +
effort estimates, plus a positive-findings section confirming what
shipped is working end-to-end:
  - F22/F23/F25/F44 verified live
  - #67 catch-up wizard runs full transaction (client+interest+clear-override)
  - #26 single-tree shell verified at 390px and 1440px viewports
  - permission gating holds for sales-agent and viewer

Critical issues found:
  - A4 New Client form silently rejects submit when an empty contact row is present (F19 filter runs in mutationFn, too late)
  - A16 file upload at documents-hub root fails: client sends nulls, validator wants strings or absent
  - A17 /api/v1/admin/ports is super-admin-only but apiFetch uses it to bootstrap port-slug→port-id resolution for every user

See docs/audit-2026-05-15.md for the full list.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 00:44:51 +02:00
85bd0d82e1 docs: capture post-audit fix plan from two-round Playwright sweep
24 fixes + 1 new feature, tiered by priority. T0 already shipped in the
previous commit; T1-T4 batches sequenced with effort estimates and file
pointers. Includes the manual-berth-status catch-up workflow design.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 22:38:02 +02:00
a8607ecc9e docs(plan): close Step 9 — recommender simulator deferred
Some checks failed
Build & Push Docker Images / lint (push) Failing after 36s
Build & Push Docker Images / build-and-push (push) Has been skipped
NocoDB inspection (via MCP) confirms the legacy Interests table carries
only the current Sales Process Level value plus point-in-time event
timestamps as text fields — no dedicated stage-change history table.
That isn't enough resolution to replay stages-over-time through the
recommender's tier-ladder + heat-score weights. Simulator deferred
until ~10+ real wins accumulate under the new pipeline, then we can
simulate against actual CRM history.

The existing /admin/berth-recommender heat-weight tuning UI is
sufficient for v1 launch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 15:55:17 +02:00
f86f511e7b docs(plan): lock pre-deploy plan from 2026-05-14 planning session
Single source of truth for everything between today and initial VPS
deploy. Captures every decision reached across the 2026-05-14 rounds:

- Hot-path correctness: canonical active-interest definition, currency-
  aware pipeline value, occupancy=sold, two-card revenue PDF, multi-
  berth EOI mooring rendering via existing Berth Number form field.
- Security gate: portal activation/reset URLs switch to URL fragment.
- Email refactor: drop signature field, per-category send-from routing,
  per-port IMAP bounce monitoring, compose-UI attachment-threshold
  banner.
- Schema: berths.archived_at, clients.metadata.source_inquiry_id,
  email_bounces table.
- UX: externally-signed mark, contract paper-upload endpoint, inquiry
  P-4.5 linkage, quick brochure/PDF download, per-user digest schedule,
  documents-tab N+1 batch fix.
- Bulk berth wizard for new-port setup.
- Four investor charts as toggleable dashboard widgets.
- Mechanical "deal" -> "interest" sweep incl. route rename.

Implementation order + deferred items + operator deploy checklist all
captured. Future agents resuming this work start here.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 14:49:13 +02:00
c44d818144 docs(backlog): mark set-state-in-effect migration as DONE
Wave 3 of the 2026-05-12 audit cleared all ~45 useEffect→fetch→
setState sites; eslint.config.mjs promoted the rule to error in the
same sweep. BACKLOG's "next pass" entry was stale from before that.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 03:51:30 +02:00
bd432fc6c7 docs(backlog): document the deferred-refactor list with rationale
Five engineering refactors and six mechanical service splits the
AUDIT-2026-05-12 dossiers flagged. Assessed against today's reality
(no active webhook subscribers, small DB, low-frequency storage
paths) and explicitly deferred. Listed here so future-me doesn't
re-research them when triaging the audit.

Each entry carries its cost estimate and the trigger condition that
should bring it back onto the roadmap.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 14:18:58 +02:00
4d1fbcd469 feat(documenso-phase-5): pin transformSigningUrl + document website-side coordination
Phase 5 is mostly coordination + verification rather than a code
build — the embedded signing pages live in a different repo. What
lands here:

1. transformSigningUrl hardening — routes through extractSigningToken
   so a bare URL like `https://sig.example.com` no longer produces
   the malformed `<host>/sign/<role>/sig.example.com`. The token
   validator (≥8 URL-safe chars) rejects malformed tails so the
   function falls back to returning the raw URL.

2. 10 unit tests pin the role-segment mapping so a future refactor
   can't silently break the contract with the marketing website's
   /sign/[type]/[token] page. Covers:
     - all five SignerRole → URL segment mappings
     - trailing-slash normalization on the host
     - null host fallback (single-tenant / staging)
     - rejection of non-token-shaped tails

3. docs/documenso-integration-audit.md updated with:
     - Phase 2/3/4/7 landed-work summary (replacing the old
       "deferred" list that was now stale)
     - Phase 5 coordination tracker for the marketing-website side
       (the four edits the website team needs to make — listed
       here so the CRM stays the source of truth on the contract)
     - Phase 6 polish backlog (auto-send delay, document expiration,
       per-document message, reminder display, failed-webhook UI,
       field metadata panel, zoom controls, recipient drag-reorder)

Tests: 21 new transformSigningUrl + signers tests across two files;
full suite 1340 → 1350 ; tsc clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 14:11:50 +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
bc54ea2c3e docs(backlog): mark Wave 10 items DONE; final grand-audit status
Wave 10 closed four more audit dossiers via:
- 10.1 types-auditor (Tx type, BerthDetailData, parseBody, toAuditJson)
- 10.2 build-auditor (server externals, healthcheck, NEXT_PUBLIC_APP_URL)
- 10.3 concurrency-auditor (handleDocumentCompleted TOCTOU, moveFolder
  cycle race, upsertInterestBerth + username 23505 mapping)
- 10.4 aria-hidden mechanical sweep across 267 files

Grand-audit status: every dossier from AUDIT-2026-05-12.md (27 reports)
has either its CRITICAL+HIGH items shipped or is explicitly back-
burnered for the four user-deferred reasons (Documenso phases 2-7,
bounce monitor, manual QA, BullMQ jobId plumbing).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 12:38:31 +02:00
b397f6049d docs(backlog): mark Wave 9 items DONE in master backlog
Wave 9 closed eight focused commits across the ui/ux, pdf, copy, and
onboarding audit findings:
- 9.1 Drawer vs Sheet doctrine
- 9.2 StatusPill adoption
- 9.3 Custom-fields token picker
- 9.4 Mobile cardRender for admin lists
- 9.5 Dashboard loading.tsx coverage
- 9.6 PDF + brand asset correctness
- 9.7 Copy/terminology sweep
- 9.8 Onboarding + first-run UX

Remaining audit work is now: aria-hidden sweep (#69), broader
concurrency lock audit (#72), bounce monitor + Documenso v2 templates
(#75), manual QA of reporting/recommender (#77), types-auditor +
build-auditor (both no-active-trigger).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 12:16:34 +02:00
153f6ac797 fix(audit-wave-9): unified template token picker with custom-field group
Build a shared <TemplateTokenPicker> that renders the canonical
MERGE_FIELDS catalog grouped by scope, plus a dynamically-fetched
"Custom (port-specific)" group surfaced from /api/v1/admin/custom-fields.
The custom group is filtered to entity types the resolver actually
expands at send time (client/interest/berth - see
mergeCustomFieldValues in document-sends.service).

Wire it into both consumers:
- admin/document-templates/template-form.tsx (replaces TEMPLATE_VARIABLES
  list which had drifted from the canonical catalog)
- admin/sales-email-config-card.tsx (replaces flat alphabetical dump)

Closes custom-fields §B "UI surfacing of {{custom.…}} tokens".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 11:57:37 +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
7675a26889 docs(backlog): grand audit cleanup plan in 8 prioritized waves
Closes out the dep-upgrade session by laying out the path from
"deps done" → "audit-clean codebase." Maps the 534 findings in
AUDIT-2026-05-12.md to concrete waves with file pointers, effort
estimates, and acceptance criteria.

Wave 1 — Stop-ship CRITICALs: db:migrate runner, EMAIL_REDIRECT_TO
prod guard, orphan-blob fix, escape URLs in templates, replace
window.confirm calls, GDPR export completeness, right-to-be-forgotten
true erase, FK + onDelete on permission_overrides, resolve-identifier
hardening.

Wave 2 — HIGH security/observability: PII masking in audit_logs,
webhook→error pipeline, admin email template subject editor wire-up,
PII redaction in error pipeline, notification email worker XSS.

Wave 3 — React Compiler set-state-in-effect cleanup (~41 sites).
Two migration patterns from this session as templates.

Wave 4 — UI/UX consistency + a11y.
Wave 5 — Concurrency + Postgres FTS perf.
Wave 6 — Email + Documenso depth.
Wave 7 — Reporting + recommender quality.
Wave 8 — Long tail (PDF, copy, onboarding, types, build).

Also closes out major-version deferrals: Next 15→16 + Tailwind 3→4
now DONE; eslint 9→10 documented as upstream-blocked.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 23:45:21 +02:00
4329db7fc3 fix(compiler): React Compiler safety triage — 5 categories cleared
Cleared 4 rule buckets (37 violations, including 5 real bugs) and
silenced 1 informational bucket from the Next 16 / react-hooks v7
upgrade. Cleared rules promoted from `warn` back to `error` so new
regressions block CI.

Real bug fixes:
- `interest-contact-log-tab.tsx`: `useMemo` used for side effects
  (5 setState calls inside a memo body); converted to `useEffect`.
- `PieChart.tsx`: cumulative `let angle` mutation in a render-phase
  `map`; converted to `reduce` so the slice array is built without
  re-assignment.
- `documents-hub.tsx`: `useMemo(() => ({ count: 0 }))` used as a
  mutable drag counter; converted to `useRef`.
- `notes-list.tsx`: `Date.now()` read during render for note-edit
  countdown (impure) → pinned to a `now` state ticked every 30s.
- `onboarding-checklist.tsx` / `user-profile.tsx` /
  `user-settings.tsx`: `useEffect(() => void load(), [])` with the
  `load` function declared AFTER the effect — relied on hoisting,
  trips Compiler's "access before declared" rule. Declared inside
  the effect.

Pattern fixes (intentional cache-via-ref → state or layout-effect):
- 6 `ref.current = x` writes during render moved into layout
  effects (`use-realtime-invalidation`, `settings-form-card`,
  `inbox`).
- 3 `ref.current` reads during render (search totals cache,
  scanner file ref) rewritten to backed-by-state.
- `use-is-mobile.ts` rewritten on `useSyncExternalStore` to avoid
  the SSR-then-rehydrate setState dance.
- `use-notifications.ts` rewritten to write socket pushes directly
  into the React Query cache via `setQueryData`, removing a local
  state mirror.

Rule config (`eslint.config.mjs`):
- `react-hooks/purity` → error (was warn, cleared)
- `react-hooks/set-state-in-render` → error (was warn, cleared)
- `react-hooks/immutability` → error (was warn, cleared)
- `react-hooks/refs` → error (was warn, cleared)
- `react-hooks/incompatible-library` → off (informational only)
- `react-hooks/set-state-in-effect` → warn (51 remaining, all the
  useEffect→fetch→setState data-fetch pattern; migration to
  useQuery tracked in BACKLOG)

Verified: tsc clean, eslint 0 errors / 69 warnings (down from 105),
vitest 1315/1315, next build green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 23:14:16 +02:00
92975e6bf5 feat(deps): @sentry/nextjs error tracking (DSN-gated, dormant by default)
Wires the Sentry SDK shipped-but-dormant: no-op unless
`NEXT_PUBLIC_SENTRY_DSN` is set in the environment. Production opts
in via the deploy env; dev + CI stay quiet.

- `sentry.client.config.ts` / `sentry.server.config.ts` /
  `sentry.edge.config.ts` — runtime init, each guards on the DSN.
- `instrumentation.ts` — Next 13.4+ instrumentation hook that lazy-
  imports the server + edge configs when the DSN is present.
- `next.config.ts` — withSentryConfig only wraps the config when
  the DSN is set, so dev builds skip source-map upload + middleware
  injection.
- `src/lib/env.ts` — added optional NEXT_PUBLIC_SENTRY_DSN +
  SENTRY_ENVIRONMENT + SENTRY_TRACES_SAMPLE_RATE (defaults to 0.1).

Env vars to add to .env.example (blocked from this commit by the
.env hook — apply manually):

    # Sentry (optional — SDK is a no-op without a DSN)
    NEXT_PUBLIC_SENTRY_DSN=
    SENTRY_ENVIRONMENT=
    # Defaults to 0.1 (10%) when unset
    SENTRY_TRACES_SAMPLE_RATE=

Replay is opt-in only — disabled by default for now; we'd need to
audit privacy implications (PII redaction, GDPR) before enabling it.

Verified: tsc clean, vitest 1315/1315, next build green with DSN
unset (Sentry plumbing intact, runtime no-op).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 22:38:18 +02:00
4879b17cff feat(deps): Next 15 → 16 (proxy.ts rename + native flat ESLint config)
Applied @next/codemod migrations:
- middleware-to-proxy: src/middleware.ts → src/proxy.ts + function rename
- remove-experimental-ppr: no hits
- remove-unstable-prefix: no hits

tsconfig.json picked up Next 16's autofixes:
- jsx: 'preserve' → 'react-jsx'
- include .next/dev/types/**/*.ts (dev-mode route types)
- next-env.d.ts: triple-slash reference → ES import (TS 6 / Next 16 style)

eslint-config-next@16 ships a native flat config, so dropped the
@eslint/eslintrc + FlatCompat shim. eslint.config.mjs now imports
eslint-config-next/core-web-vitals + eslint-config-prettier/flat
directly.

Note on ESLint 10: bumped + reverted. eslint-config-next@16 still
has a transitive eslint-plugin-react@7 that uses the eslint-9
context API (getFilename on context); breaks under eslint 10.
Audit anticipated lockstep — but the transitive isn't ready yet.
Holding at eslint 9.x until upstream lands. Tracked in BACKLOG.

React Compiler safety rules (react-hooks v7) shipped with config-
next 16 surfaced ~89 legitimate findings (set-state-in-effect,
ref-during-render, immutability). Demoted the new rules to `warn`
so the codebase isn't blocked; triage tracked in BACKLOG §G.

Verified: tsc 0 errors, eslint 0 errors / 105 warnings (89 new
Compiler-rule warns + 16 pre-existing), next build clean, custom
server build clean, vitest 1315/1315.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 22:24:51 +02:00
3147923d91 docs(backlog): close out dep adoption — reject upstash/faker/msw with rationale
Three audit-flagged deps rejected on inspection (not parked-pending-
decision):

- @upstash/ratelimit — audit said "4 hand-rolled rate limiters"; actual
  state is one centralized sliding-window limiter with 14 named policies.
- @faker-js/faker — both seed files are hand-curated specs keyed to test
  selectors, not random fake data; faker would mean ADDING a factory.
- msw — vi.mock at the service-module boundary already gives determinism;
  msw only helps when tests hit fetch() directly.

Adds tsc-staged.mjs to the done list. Updates parked list with concrete
rationale per item.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 22:02:13 +02:00
7cc80512da docs(backlog): session wrap — full dependency/refactor roadmap shipped
Closes the 2026-05-12 push through the audit roadmap. Every item from
docs/AUDIT-2026-05-12.md §§34-36 is either shipped, deferred with
rationale, or parked behind a concrete UX/product trigger.

Wins this session (in commit order from 73184c5 onward):
  1. PDF stack overhaul (9 commits + design spec)
  2. react-email migration for all 7 remaining templates
  3. browser-image-compression in scan-shell
  4. @axe-core/playwright smoke a11y gate
  5. ts-pattern + bug-fix in search.service.ts
  6. p-limit on 3 mass-op fan-outs
  7. formatDate helper + 17 unit tests + sample sweep
  8. opt-in react-virtual in DataTable

Also nudges:
  - src/lib/pdf/brand-kit/Header.tsx — eslint-disable on react-pdf
    <Image> for a false-positive jsx-a11y/alt-text warning (PDFs
    don't follow the HTML img alt contract).
  - docs/BACKLOG.md §G — rewritten to reflect what's done + the
    remaining opportunistic work (mostly "migrate as you touch the
    file" callsite sweeps).

Comprehensive audit passing:
  - tsc --noEmit: 0 errors
  - vitest: 1315/1315 passing
  - eslint src/: 0 errors, 16 pre-existing warnings (none new)
  - next build: all routes compile, no broken imports
  - playwright --list: 162 tests across 33 files (incl. the new
    a11y spec)

Branch is shippable; remaining items are opportunistic callsite
sweeps the team can pick up when each file is otherwise being
touched.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 21:42:51 +02:00
63220ad072 docs(backlog): update with PDF/react-email/scan-compress/a11y wins + remainder
Adds a new §G (dependencies / audit roadmap) documenting what landed
in the 2026-05-12 session (PDF stack overhaul, react-email migration,
browser-image-compression, axe-core) and what's left in roughly
decreasing impact-per-hour order. Each remaining item gets an estimate,
a "pattern proven?" note, and a one-line action plan so a future
session can resume without re-reading the entire audit doc.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 21:25:03 +02:00
81a98c6695 docs(superpowers): pdf stack overhaul design (react-pdf + unpdf)
Lays out the plan to replace pdfme with @react-pdf/renderer, add unpdf
for berth-PDF tier-2 rasterization, and add port-level logo upload
(sharp normalization + react-image-crop UI + svgo sanitization +
rasterize-SVG-to-PNG-on-upload).

Scope locked to internal-only PDFs (reports, expenses, record exports).
Invoice + admin TipTap-to-PDF removed entirely; in-app EOI pathway
(pdf-lib AcroForm fill) stays untouched.

14 commits planned. Single source of truth for tokens. Three orthogonal
PDF paths post-migration with no overlap.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 20:36:54 +02:00
d3960af340 feat: warm-up deps — ts-reset, web-vitals, RHF devtool, query-broadcast
Four low-risk adds before the Zod 4 / drizzle-zod headliner:

- @total-typescript/ts-reset: tightens TS stdlib types globally (JSON.parse
  → unknown, fetch().json() → unknown, .filter(Boolean) narrows, Set
  literals respect typed Set targets). Caught 179 latent type errors;
  fixed all production sites (8 files) and added `any` cast escape hatch
  in test files (ESLint exemption scoped to tests/).
- web-vitals + /api/v1/internal/vitals endpoint + WebVitalsReporter
  client component: establishes Core Web Vitals baseline (LCP/INP/CLS/
  FCP/TTFB) via navigator.sendBeacon. Required before optimisation work.
- @hookform/devtools + FormDevtool wrapper: dev-only RHF state inspector,
  lazy-loaded via next/dynamic so the chunk is excluded from prod
  bundles entirely.
- @tanstack/query-broadcast-client-experimental: cross-tab cache sync
  via BroadcastChannel — wired in query-provider.tsx, 1-liner.

Audit doc updated with sections 35 + 36 (PDF stack overhaul + comprehensive
second-pass package sweep) covering ~20 package adoption candidates and
4-5 deprecation candidates.

Verified: tsc clean, vitest 1293/1293 pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 18:16:18 +02:00
82049eea92 deps: bump Tier-A patches + react-day-picker 10 + esbuild 0.28
Successfully bumped:
- bullmq 5.76.6 → 5.76.8
- @tanstack/react-query 5.100.9 → 5.100.10
- @tanstack/react-query-devtools 5.100.9 → 5.100.10
- better-auth 1.6.9 → 1.6.10
- @playwright/test 1.59.1 → 1.60.0
- libphonenumber-js 1.12.43 → 1.13.1
- tailwind-merge 3.5.0 → 3.6.0
- vitest 4.1.5 → 4.1.6
- @vitest/coverage-v8 4.1.5 → 4.1.6
- lint-staged 17.0.3 → 17.0.4
- esbuild 0.27.7 → 0.28.0
- react-grab 0.1.33 → 0.1.34
- react-day-picker 9.14.0 → 10.0.0

react-day-picker 10 verified safe: probed v10 release notes against
src/components/ui/calendar.tsx — we use only v9-canonical APIs that
v10 preserves. Removed the `table` className entry from the wrapper
(v10 dropped it since the renderer is now CSS-grid, not table-based).

Tried + rolled back:
- @hookform/resolvers 3 → 5: stricter input/output inference broke
  every form using <{schema}, any, {schema}> implicit shape. Needs
  per-form refactor; parked.

Verified clean: pnpm audit (prod + dev) = 0 vulnerabilities;
pnpm exec tsc --noEmit clean; vitest 1293/1293 pass.

Remaining outdated (deliberately deferred — see docs/AUDIT-2026-05-12.md §34):
- next/eslint-config-next 15 → 16 (2-4 wk wait)
- zod 3 → 4 (couple with @hookform/resolvers 5; codemod-needed)
- tailwindcss 3 → 4 (focused-afternoon project)
- @types/node ^20.19 stays pinned to match runtime (audit decision)
- archiver 7 stays (no @types/archiver@8 published)
- eslint 9 stays (locked to eslint-config-next 15)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 17:33:24 +02:00
a7d0dd95e2 audit: append Context7-assisted dependency upgrade analysis (§34)
Companion to the deps-auditor report. Per-major pros/cons informed by
upstream changelogs queried via Context7. Sequencing:

- Tier A (patches + esbuild minor) → do now
- Zod 4 + @hookform/resolvers 5 → couple together, codemod-able
- Next 15 → 16 (incl. middleware → proxy.ts rename) → 2-4 weeks
- Tailwind 4 → afternoon project, visual review pages
- Defer indefinitely: archiver 8 (failed last try), react-day-picker 10

Top-line baseline: 0 known vulns; no GPL/AGPL; lockfile reproducible.
Everything below is dev-experience/perf, not security-blocker.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 17:23:19 +02:00
bfed1543b7 audit: Tier 5.4 — wrap moveFolder cycle check + write in a tx
Concurrency-auditor HIGH: the cycle walk + UPDATE used to run as
separate statements. Two concurrent moves (A→B and B→A) could each
pass the walk against the pre-move tree and both write, leaving an
A↔B cycle. Whole sequence now runs inside one db.transaction().

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 17:19:24 +02:00
ad74e4a174 audit: Tier 1/3/6/7 batch — PII redaction, mobile safe-area, perf, build hardening
Tier 1.4: error_events.request_body_excerpt sanitizer now redacts
GDPR-relevant fields (email, phone, dob, address, fullName, firstName,
lastName, postcode, nationalId, etc.) on top of the existing
credential list. A 5xx in /api/v1/clients no longer lands full client
PII in the super-admin inspector.

Tier 3.10: ScanShell <main> now adds pb-[max(1.5rem, env(safe-area-
inset-bottom))]. Mobile-pwa audit caught the Save expense button sitting
flush against the iPhone 14/15 home indicator in standalone PWA mode.

Tier 6.2: dashboard widget-registry now dynamic-imports every
recharts-backed chart widget (berth status, lead source, occupancy
timeline, pipeline funnel, revenue breakdown, source conversion).
~80-150KB initial-bundle savings when reps have charts disabled.
ssr:false because recharts needs window.

Tier 6.3: DataTable wraps the assembled columns in useMemo keyed on
(columns, hasBulkActions). TanStack docs explicitly warn that
rebuilding columns every render resets the table's internal state.

Tier 7.1: Added .dockerignore (was missing — 7.6 GB context with
.env reachable via COPY . .). Excludes git, env files, node_modules,
build artefacts, IDE config, test artefacts, audit docs.

Tier 7.4: Dockerfile.dev now runs as the node user (uid 1000) — was
root. Working dir moves to /home/node/app.

Tier 7.5: docker-compose.prod.yml adds memory limits (2g postgres,
512m redis, 1g crm-app, 1g crm-worker) and json-file log rotation
(max-size, max-file) to every service.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 17:18:35 +02:00
50f48a8b6a audit: Tier 2/3/4 batch — reports math, portal copy, authz escalation guard
Tier 2.2: revenue PDF totalCompleted now filters on outcome='won' —
setInterestOutcome forces stage='completed' for every outcome (incl.
lost + cancelled), so the stage-only filter was including those toward
"TOTAL COMPLETED REVENUE".

Tier 2.3: fetchPipelineData stageCounts adds the missing .groupBy() —
without it Postgres rejects the SELECT (per-stage breakdown was broken
or coercing to ELSE-stage row).

Tier 2.4: hot-deals widget rank ladder fixed two stage-name typos —
'in_comms' → 'in_communication', 'deposit_10' → 'deposit_10pct'. Both
stages were collapsing to the ELSE 0 branch server-side AND rendering
raw enum to the user in hot-deals-card.tsx.

Tier 3.2: portal /portal/interests no longer renders raw enum to
clients. New PORTAL_SIGNING_LABELS table maps every EOI/contract
status to plain English (e.g. "waiting_for_signatures" → "Waiting for
signatures").

Tier 4.1 (CRITICAL): permission-overrides PUT now requires caller-
superset on every `true` write. Admins with only `admin.manage_users`
could previously grant other users leaves they don't hold themselves
(permanently_delete_clients, system_backup). Super-admins bypass.

Tier 4.4: search graph-expansion re-gates every merged bucket by the
destination's view permission. A user with berths.view but no
interests.view searching "A12" no longer sees interest rows surfaced
via expansion.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 17:13:04 +02:00
16ef609e1b audit: Tier 1/3/4/5/7 batch — SSE, gates, dedup, URL escape, FK constraints
Tier 1.6: S3Backend.put now sets ServerSideEncryption=AES256 — closes
the cleartext-at-rest gap for signed contracts, GDPR exports, pg_dumps.

Tier 3.7: New safeUrl() helper in lib/email/shell.ts. Scheme allow-list
(http/https/mailto/tel/relative only — javascript:/data:/vbscript:/file:
rewritten to about:blank) + HTML-attribute escape. Retrofitted across
all 7 transactional templates (crm-invite, portal-auth, document-signing,
notification-digest, residential-inquiry, admin-email-change).

Tier 4.2: /api/v1/alerts GET now gated on admin.view_audit_log.

Tier 4.3: Documenso webhook handler emits captureErrorEvent on catch.
Admin/errors no longer silent on webhook crashes.

Tier 4.6: Inquiry-funnel email dedup is now case-insensitive
(LOWER(value)) and stores normalized email on insert. Capital-letter
resubmissions no longer spawn duplicate client+yacht+interest rows.

Tier 5.6 + data-model H1: migration 0056 adds FK
user_permission_overrides.user_id → user(id) cascade, same for
user_port_roles.userId, plus partial unique index on
user_email_changes pending rows.

Tier 7.6: @types/node bumped from ^25 to ^20.19.0 — matches the runtime.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 17:09:14 +02:00
0baca41693 audit: Tier 0 quick wins — EMAIL_REDIRECT_TO prod guard + storage routing + metadata masking
Tier 0.2: src/lib/env.ts now refuses boot when NODE_ENV=production AND
EMAIL_REDIRECT_TO is set. Sendmail logs the rewrite at warn (was debug)
so dev/staging windows where someone forgets to unset are immediately
visible.

Tier 0.6: backup_jobs.storage_path added to TABLES_WITH_STORAGE_KEYS in
src/lib/storage/migrate.ts. Flipping the storage backend used to
silently orphan every pg_dump artefact — last-resort recovery path is
now actually portable.

Tier 1.7: createAuditLog now runs metadata through maskSensitiveFields
(was only applied to old/new value diffs). Portal-auth, crm-invite,
hard-delete and email-accounts services were writing raw emails into
this column unbounded.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 17:02:10 +02:00
a7b72801be audit: importance-grouped triage companion to AUDIT-2026-05-12.md
10 tiers, every finding cross-referenced. Tier 0 = stop-ship, 8 =
already-fixed, 9 = nice-to-haves. Pick a tier and dig.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 16:58:51 +02:00
bdc9c019a8 audit: append storage-pathing report — all 33 agents now inlined
docs/AUDIT-2026-05-12.md now contains every audit verbatim
(6488 lines). Last to land was the S3-vs-internal-DB routing audit
covering the storage-backend boundary, presigned-URL round-trip,
magic-byte verification on both paths, migrate-storage coverage,
and orphan-blob risk on transaction failure.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 16:53:16 +02:00
4b9743a594 audit: 33-agent comprehensive audit + critical fixes
Full team audit run, all reports verbatim in docs/AUDIT-2026-05-12.md
(5900+ lines, 30+ critical findings). Already-fixed this commit:
- permission-overrides PUT: self-target block + RolePermissions allow-list + cross-tenant guard
- /api/auth/resolve-identifier: rate-limit + synthetic miss-email kill enumeration
- admin email-change: rotates account.accountId + revokes sessions
- middleware: token-gated email confirm/cancel routes whitelisted
- NAV_CATALOG: 10 dead-link sweeps to existing /admin/<x> targets

Feature work landing same commit: optional username sign-in
(migration 0054), per-user permission overrides (0055) with three-state
matrix tabbed inside UserForm, user disable button, role + outcome +
stage label normalisation across the platform, admin email-change
with auto-notification template.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 16:52:35 +02:00
638000bb58 chore: prettier format audit report markdown
Lint-staged reformat after the previous commit added the file. Same
content, prettier's preferred line wrap.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 17:59:43 +02:00
1bdc856589 feat(documents-hub): NewDocumentMenu dropdown + FolderDropZone drag-drop
Replaces the bare "+ New document" Button on the documents hub with a
NewDocumentMenu dropdown so reps explicitly pick between:
- "Upload file" → opens a Dialog with FileUploadZone scoped to the
  current folder + entity context. No signing flow attached.
- "Generate document for signing" → navigates to /documents/new wizard.

Avoids the prior ambiguity where reps clicked "+ New document" intending
to attach a file and were dropped into the Documenso signer wizard.

Also adds FolderDropZone wrapping FlatFolderListing and EntityFolderView.
Dragging files from the OS over the current folder shows a drop overlay;
drop fires N parallel uploads carrying the folder + entity context.
Mirrors the per-entity Files tab UX but works in-place on the hub.

Both surfaces hit /api/v1/files/upload with folderId + entityType/Id +
the legacy clientId/companyId/yachtId FKs so files land on the right
entity AND inside the correct folder.

Also includes the in-flight prettier reformat from lint-staged on a
few previously-touched files (create-document-wizard, file-upload-zone,
admin/documenso/page) and adds the standalone prod-readiness audit
report to docs/superpowers/audits/ for permanent reference.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 17:59:34 +02:00
0e8feb1073 chore: prettier format pass on branch files
Auto-format all files modified during the documents-hub-split feature
branch that were not yet aligned with the project's Prettier config
(single quotes, semicolons, trailing commas).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 13:01:47 +02:00
40e3db237d docs(plans): documents hub split + auto-filed client folders
19-task implementation plan layering on top of Wave 11.B. Builds three
system-managed roots (Clients/Companies/Yachts), per-entity auto-
subfolders, Documenso auto-deposit on completion, owner-aggregated
projection (symmetric reach, file-FK source of truth, defense-in-depth
port_id), and the hub UI rebuild. Hard cutover; backfill via idempotent
script.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 10:57:46 +02:00
5422f11747 chore: prettier formatter drift across recent commits
Prettier reformatting on files touched in the wave 11.B sequence —
markdown italics _underscore-style_, single-line conditionals, minor
whitespace fixes. No semantic changes. .env.example reformatting left
unstaged (blocked by pre-commit hook).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 10:57:37 +02:00
286eb51f81 docs(specs): documents hub split + auto-filed client folders
Design for unifying /documents and /documents/files under a single hub
with stacked Signing/Files sections, owner-grouped aggregation across
the relationship graph, and three system-managed entity-folder roots
(Clients/Companies/Yachts) with lazy per-entity subfolders. Documents
hub stays anchored on document_folders; files gain folder_id; signed
PDFs auto-deposit on Documenso completion. Includes 14+ edge-case
decisions, schema deltas, backfill plan, and implementation surface.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 22:50:31 +02:00
f286c4ef5f docs(plan): progress snapshot at Task 7 — backend complete, UI next
Tasks 1-7 done in subagent-driven mode (11 commits 5bed62da0ffa1b).
The entire DB + service + API layer for folders is shipped: schema,
manage_folders perm, listTree/createFolder/renameFolder/moveFolder/
deleteFolderSoftRescue, validators, all 4 folder routes, the per-doc
move endpoint, and the listDocuments folder filter (with descendant
expansion). Reps can already manage folders end-to-end via direct
API calls.

Records the design decisions made mid-execution: hybrid storage
strategy (UUID-flat + path-style download URLs), permission split,
soft-rescue delete semantics, cycle prevention with port-scoped
ancestor walk, PATCH-body exclusivity via .strict(), and the
updatedAt bump rule (per-doc move yes, bulk soft-rescue no).

Tests at pause: 1213/1213 vitest, tsc clean. Resume prompt + task
ordering for Task 8 onwards included so a fresh session can pick up
without context.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 20:08:28 +02:00
9f3e739c76 docs(plan): add Tasks 18-19 (path-style URLs + organized-bucket importer)
User chose the hybrid storage strategy after reviewing the cost
analysis: storage paths stay UUID-flat (preserves the established
pattern across brochures, berth PDFs, invoices, reports, templates,
expense receipts, and the migrate-storage byte-verbatim copy), but
documents gain a path-style download URL so reps see meaningful
paths in shared links and browser tabs.

Task 18 wires the new /api/v1/documents/[id]/download/[...slug]
catch-all route + a downloadUrl field on list/detail responses.
The slug is validated for truth so a hand-edited URL with a
stale path 404s instead of silently serving the wrong file.

Task 19 is the importer the user mentioned: a one-shot script
that walks an organized legacy bucket, creates matching folder
tree + document rows pointing at existing storage keys verbatim.
Idempotent via the sibling-uniqueness index.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 19:50:28 +02:00
51a60c1b9e docs(plan): documents folders implementation plan
17-task TDD plan covering schema + migration, service (listTree,
create, rename, move with cycle prevention, soft-rescue delete),
validators, API routes, hook, sidebar tree, breadcrumb, actions menu,
move-to-folder dialog, hub wiring (drop signature-only pill, In-
progress tab, dynamic type chips), admin-configurable Expired tab,
Playwright smoke, and CLAUDE.md update. Decisions locked: port-wide
tree, hub tabs stay flat, documents.manage_folders new perm, soft-
rescue on delete (never CASCADE), folder watchers out of scope.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 18:59:30 +02:00
1bfed587b5 docs: website cutover runbook + post-execution status snapshot
Captures the agreed cutover plan (Q6 in the decisions log: double-write
transition window, ~30 days, then NocoDB decommission). The CRM side
is wired today — public berth feed, website-inquiries intake, dual-mode
health probe, WEBSITE_INTAKE_SECRET env var. The runbook documents the
website-repo checklist and rollback path so we can pick it back up
when prep for prod begins.

Refreshes the audit-followups status snapshot to reflect what shipped
this session. Wave 11 is now broken out into A-G subitems so the
remaining group-discussion work is enumerated rather than collapsed.

Note: .env.example separately needs WEBSITE_INTAKE_SECRET added (see
runbook §Endpoints). The husky pre-commit hook blocks .env* files
intentionally — pass via a separate workflow.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 18:38:46 +02:00
7804e9bb17 docs(audit-followups): record 11 decisions from 2026-05-09 review
Replace the open-questions section with a Decisions log capturing the
chosen direction for vocabularies admin, notification prefs, name
fields, public-feed parity, website cutover, status-change prospect
link, trip-label UX, documents folders, and the berth Documents tab
split. Quick-status snapshot updated to reflect that Waves 4-10 are
now ready to start.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 18:34:59 +02:00
502455ac04 chore(format): apply prettier auto-formatting
Pre-commit hook reformatted these files after the substantive commits.
No semantic changes — markdown table alignment, list indentation, and
emphasis style normalisation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 04:11:54 +02:00
aad514a3bd docs(audit): 2026-05-08 mobile audit follow-ups index
Single source of truth for the 2026-05-08 visual-audit work. Owns
status of each item, file pointers, every open question, and a
ready-to-paste prompt for resuming in a fresh session. Items grouped
by wave (the original triage buckets, kept stable across sessions).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 04:11:24 +02:00
8dc16dcd2e fix(audit): non-Documenso backlog sweep — port-binding, NULLS NOT DISTINCT, custom merge tokens, company docs
Some checks failed
Build & Push Docker Images / lint (push) Successful in 1m36s
Build & Push Docker Images / build-and-push (push) Failing after 4m27s
Wave through the remaining audit-final-deferred items that aren't blocked
on the back-burnered Documenso work.

Multi-tenant isolation:
- Storage proxy ProxyTokenPayload gains optional `p` (port slug) claim;
  verifier asserts `key.startsWith(${p}/)`. Defense-in-depth against a
  buggy issuer in some future code path that mixes port scopes — every
  storage key generated by generateStorageKey() already prefixes the
  slug. document-sends opts in for 24h emailed download links; other
  callers continue working unchanged via the optional field.

DB schema reconciliation:
- Migration 0047 rebuilds system_settings unique index with NULLS NOT
  DISTINCT (Postgres 15+) so global settings (port_id IS NULL) are
  uniquely keyed by `key` alone. Surfaced + dedupe'd 65 duplicate
  (storage_backend, NULL) rows that had accumulated from race-prone
  delete-then-insert patterns in ocr-config / settings / residential-
  stages / ai-budget services. All four services converted to true
  onConflictDoUpdate upserts so the race window is closed.

API uniformity:
- Response shape standardization: 16 routes converted from
  `{ success: true }` to 204 No Content. CLAUDE.md documents the
  convention (`{ data: <T> }` for content, 204 for empty mutations,
  portal-auth retains `{ success: true }` for the frontend's auth chain).
- req.json() → parseBody() migration across 9 admin/CRM routes
  (custom-fields, expenses/export ×3, currency convert,
  search/recently-viewed, admin/duplicates, berths/pdf-{upload-url,
  versions, parse-results}). Uniform 400 error shapes for
  ZodError-flagged bodies.

Custom-fields merge tokens (shipped end-to-end):
- merge-fields.ts gains CUSTOM_MERGE_TOKEN_RE + helpers for the
  `{{custom.<fieldName>}}` shape.
- document-templates validator accepts the dynamic shape alongside
  the static catalog tokens.
- document-sends.service mergeCustomFieldValues resolver fetches
  per-port custom_field_definitions for client/interest/berth contexts
  and substitutes stored values keyed by `{{custom.fieldName}}`.
- custom-fields-manager amber banner updated to reflect that merge
  tokens now expand (search index + entity-diff remain documented
  design limitations).

/api/v1/files cross-entity filtering:
- Validator + listFiles + uploadFile accept companyId AND yachtId
  alongside clientId. file-upload-zone propagates both.
- New CompanyFilesTab component mirrors ClientFilesTab; restored as a
  visible Documents tab in company-tabs.tsx (was a hidden stub).

Inline TODOs:
- Reviewed remaining two TODOs (per-user reminder schedule, import
  worker handlers). Both are placeholders for future feature surfaces,
  not bugs — per-port digest works for every customer; nothing
  currently enqueues import jobs (verified). Annotated in BACKLOG.

BACKLOG.md updated to reflect what landed and what's still pending
(Documenso-related items still bundled with the back-burnered phases).

Tests: 1185/1185 vitest, tsc clean.
2026-05-08 02:20:27 +02:00
60365dc3de fix(audit): backlog sweep — partial archived indexes, custom-fields per-entity gate, polish
Some checks failed
Build & Push Docker Images / lint (push) Successful in 1m37s
Build & Push Docker Images / build-and-push (push) Failing after 24s
Wave through the 2026-05-07 backlog of small/concrete audit-final-deferred
items (deferring the Documenso Phases 2-7 build and items needing design
decisions or live external instances).

DB schema:
- Migration 0046 converts 5 composite (port_id, archived_at) indexes to
  partial WHERE archived_at IS NULL — clients, interests, yachts, and
  both residential tables. Smaller, faster planner choice for the
  dominant list-query shape.

Multi-tenant isolation:
- document_sends now verifies recipient.interestId belongs to the port
  before landing on the audit row (the surrounding clientId check was
  already port-scoped; interestId pollution was the gap).

Routes / API:
- /api/v1/custom-fields/[entityId] requires entityType query param and
  gates on the matching resource permission (clients/interests/berths/
  yachts/companies). Fixes the cross-resource gap where a user with
  clients.view could read company custom-field values.
- Admin user list trash button wrapped in PermissionGate (edit was
  already gated; remove was not).

Service polish:
- berth-recommender accepts string-shaped JSONB booleans
  ('true'/'false') so admin UIs that wrap values as strings don't
  silently fall through to defaults.
- expense-pdf renderReceiptHeader anchors all text positions to a
  captured baseY rather than reading mutating doc.y after rect+stroke.
  Headers no longer drift on the first receipt page after a soft page
  break.
- berth-pdf apply: collect non-finite numeric coercion drops + warn-log
  them so partial silent drops are observable (was invisible because
  the no-fields-supplied check only fires when ALL drop).
- Storage cache fingerprint comment documenting the encrypted-secret
  invariant + the explicit invalidation hook.

UI polish:
- invoice-detail typed: replaced two `any` casts with a proper
  InvoiceDetailData / LineItem / LinkedExpense interface set.
- YachtForm now accepts initialOwner prop. Wired through:
  - client-yachts-tab passes { type: 'client', id: clientId }
  - interest-form passes { type: 'client', id: selectedClientId }
- Interest-form yacht picker now includes company-owned yachts where
  the selected client is a member (fetches client.companies and feeds
  YachtPicker an array filter). Plus an inline "Add new" button that
  opens YachtForm pre-bound to the client.
- YachtPicker accepts ownerFilter as single OR array for "match any"
  semantics.

BACKLOG.md updated with what landed vs what's still deferred (and why
each deferred item is genuinely larger than this push warrants).

Tests: 1185/1185 vitest, tsc clean.
2026-05-07 21:45:42 +02:00
a0e68eb060 docs: comprehensive audits + Documenso build plan + admin UX backlog
Six audit documents capture the 2026-05-06 review pass (comprehensive,
frontend, missing-features, permissions, reliability) along with the
Documenso integration audit + locked build plan that drove the bulk
of subsequent feature work.

Adds `docs/admin-ux-backlog.md` as a living tracker for the autonomous
push — every item marked DONE or REMAINING with file pointers and
scope estimates so future sessions can pick up where this one stopped.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:57:53 +02:00
Matt Ciaccio
312779c0c5 fix(security): tier-0 audit blockers (next CVE, role gate, perm traps, key validation, rate limits)
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>
2026-05-05 18:33:13 +02:00