Files
pn-new-crm/docs/BACKLOG.md
Matt 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

307 lines
36 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Master backlog index
**Single source of truth for everything outstanding.** Start here when
asking "what's left to build/fix?". Items are grouped by source doc;
each entry links back to the original spec for full context.
Last updated: 2026-05-12 (PDF stack overhaul shipped: react-pdf brand
kit + port logo upload + 4 reports + 3 record exports + parent-company
expense + pdfkit brand header + invoice removal + tiptap-to-pdfme
deletion + unpdf for berth-parser tier-2; pdfme deps removed.
Remaining 7 react-email templates ported. browser-image-compression
wired into scan-shell. @axe-core/playwright smoke suite added.).
Documenso phases 2-7 stay back-burnered per user.
---
## A. Documenso build (deferred for later)
**Source:** [`docs/documenso-build-plan.md`](./documenso-build-plan.md) — full phase plan with locked decisions (Q1Q10).
**Tracker delta:** [`docs/admin-ux-backlog.md`](./admin-ux-backlog.md) — what landed in Phase 1.
Phase 1 (EOI generate flow polish + APPROVER-as-CC + per-port settings + signing-URL fix) is **DONE** and committed.
Remaining phases — explicitly back-burnered by the user on 2026-05-07:
| Phase | Scope | Estimate | Notes |
| ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Phase 2** | Webhook handler enhancement: cascading "your turn" emails, on-completion PDF distribution, token-based recipient matching, idempotency lock | ~34h | Schema columns already in place from Phase 1 (`document_signers.invited_at / opened_at / signing_token`, `documents.completion_cc_emails`). |
| **Phase 3** | Custom doc upload-to-Documenso: `custom-document-upload.service.ts` + `POST /api/v1/interests/[id]/upload-for-signing` | ~68h | Depends on Phase 2 webhook UX in anger before locking the upload UX. |
| **Phase 4** | Field placement UI: react-pdf + dnd-kit overlay + auto-detect anchor scanner via pdfjs `getTextContent` | ~1014h | Largest piece. Plan locked in build-plan Phase 4 — regexes, anchors, type-to-bbox sizing all spelled out. Best done in a focused session with the user watching. |
| **Phase 5** | Embedded signing URL emission verification: confirm website's `/sign/<type>/<token>` page handles every signer-role × documentType combination; update `signerMessages` map; apply nginx CORS block from integration audit | ~12h | |
| **Phase 6** | Polish: auto-send delay, audit-log additions, per-document customisation, document expiration, reminder rate-limit display, failed-webhook recovery UI | each ~23h | All deferred until Phases 14 ship. |
| **Phase 7** | Project Director RBAC — UI binding for the developer-user fields. Add "Linked to CRM user" dropdown in `/admin/documenso/page.tsx`; auto-fill name/email; webhook handler matches against linked user's email for in-CRM signing-status updates. Schema + setting keys (`documenso_developer_user_id`, `documenso_approver_user_id`, `_label`) already in place from Phase 1. | ~1h | Smallest piece; could be picked off independently of Phase 2. |
| **Risk #4** | v2 webhook payload audit against a live v2 instance (`payload.documentId` vs `payload.id`, `recipient.token` vs `recipient.recipientId`) before relying on Phase 2 cascading emails | ~1h | Needs a live v2 instance. |
---
## B. Custom-fields hardening
**Source:** [`docs/admin-ux-backlog.md`](./admin-ux-backlog.md) §7.
-**Merge tokens**`{{custom.<fieldName>}}` validators + resolver shipped 2026-05-08. Tokens expand at template-render time for client/interest/berth contexts via `mergeCustomFieldValues` in `document-sends.service.ts`. Banner updated.
- **Search index** — DEFERRED as design limitation. Adding GIN coverage requires either joining `custom_field_values` per search (slow at scale) or materializing values into a search_text column on the parent (additive maintenance burden). The amber banner documents this.
- **Audit diff** — N/A. Custom-field values live in their own table, not as a JSONB blob on the parent entity. The `setValues()` service-layer call already creates its own audit log entry (custom-fields.service.ts:349-358), so changes ARE audited — just separately from the entity-diff.
- **UI surfacing of `{{custom.…}}` tokens in template-edit pickers** — Open. The token list dialog currently only shows static catalog tokens. Surface per-port custom-field definitions as a dynamic group under "Custom" so reps can browse them. Backend already accepts the tokens; this is a UI follow-up.
---
## C. Audit-final deferred items
**Source:** [`docs/audit-final-deferred.md`](./audit-final-deferred.md) — pre-merge + post-merge audit findings explicitly carried over.
The 2026-05-07 backlog sweep landed every small/concrete item. Remaining
entries are deferred because they need design decisions, live external
instances, or cross-cutting refactors:
### Deferred — Documenso-related (back-burnered until phases 2-7 land)
- **Documenso webhook does not enforce port_id on document lookups** — `src/app/api/webhooks/documenso/route.ts:96-148`. Bundle with Documenso Phase 2 (webhook handler enhancement) since they touch the same code.
- **Webhook dedup vs per-recipient signed events** — `src/app/api/webhooks/documenso/route.ts:103-110`. Replacing the body-hash dedup with a `(documensoDocumentId, recipientEmail, eventType)` composite unique requires a recipient_email column on `documentEvents`. Bundle with Phase 2.
- **v2 voidDocument endpoint shape verification** — `src/lib/services/documenso-client.ts:450-466`. Needs a live Documenso 2.x instance. Bundle with Phase 5.
### Deferred — pure refactor (no active bug)
- **Public POST routes bypass service layer** — `src/app/api/public/{interests,website-inquiries,residential-inquiries}/route.ts`. The audit's `userId: null as unknown as string` cast was already cleaned up to a proper `userId: null`. Remaining concern is testability: extract a shared `publicInterestService.create(...)`. Pure ergonomics — no active bug or security issue.
### Done in 2026-05-08 sweep (latest)
- ✅ Storage proxy port_id binding: `ProxyTokenPayload` gains optional `p` (port slug) claim; verifier asserts `key.startsWith(${p}/)`. document-sends 24h URLs opt in; other issuers continue working unchanged.
- ✅ system_settings index rebuilt with `NULLS NOT DISTINCT` (migration 0047) — global settings are now uniquely keyed by `key` alone. Surfaced + cleaned 65 duplicate `(storage_backend, NULL)` rows that had accumulated from race-prone delete-then-insert patterns.
- ✅ All 4 read-then-write systemSettings sites converted to true `onConflictDoUpdate` upserts (ocr-config, settings, residential-stages, ai-budget).
- ✅ Response shape standardization: 16 routes converted from `{ success: true }``204 No Content`. CLAUDE.md documents the convention.
-`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}). Portal-auth routes intentionally retained `{ success: true }`.
- ✅ Custom-field merge tokens: validator accepts `{{custom.<fieldName>}}` shape; resolver in `mergeCustomFieldValues` substitutes from per-port custom_field_definitions + per-entity values for client/interest/berth contexts. Banner updated.
-`/api/v1/files` accepts `companyId` and `yachtId` filters. uploadFile service writes both. file-upload-zone component accepts both props.
- ✅ Company Documents tab (CompanyFilesTab) re-enabled and added to company detail tabs.
### Done in 2026-05-07 sweep (commits in this session)
- ✅ Partial archived indexes (migration 0046) — `clients`, `interests`, `yachts`, `residential_clients`, `residential_interests`
-`document_sends` interestId port-verification helper
- ✅ Custom-fields per-entity permission gate (replaces hardcoded `clients.view/edit`)
- ✅ EOI Berth Range warn log (was already in place)
- ✅ v1 `placeFields` retry with backoff (was already in place)
- ✅ S3 bucket-exists check at boot (was already in place)
- ✅ Filesystem dev HMAC fallback warn (was already in place)
- ✅ Storage cache fingerprint documentation comment
- ✅ AI worker cost ledger writes (was already in place)
- ✅ Logger redact paths covering headers, encrypted blobs, two-level nesting (was already in place)
-`loadRecommenderSettings` accepts string `"true"`/`"false"` JSONB booleans
-`renderReceiptHeader` cursor math anchored to captured `baseY`
- ✅ Berth PDF apply: silent-drop logging for non-finite numeric coercions
- ✅ Saved-views: confirmed by-design owner-only (existing inline doc)
- ✅ Alerts ack/dismiss: confirmed by-design port-wide (service correctly bounded)
- ✅ Storage admin migration toasts (already in place)
- ✅ Invoice send/payment toasts + permission gates (already in place)
- ✅ Admin user list edit + remove gates (added remove gate)
- ✅ Email threads list skeleton + empty state (already in place)
- ✅ Scan page error state for OCR failures (already in place)
- ✅ Invoice detail typed (replaced `any` with `InvoiceDetailData` interface)
- ✅ All FK indexes called out in audit doc (already in place — audit was stale)
-`documentSends.sentByUserId` FK (already had `.references(...)`)
### Documented limitations (no action planned)
- **`berths.current_pdf_version_id` lacks Drizzle FK** — `src/lib/db/schema/berths.ts:83`. The in-line comment fully documents why (circular FK between `berths``berth_pdf_versions` makes column-level `.references()` infeasible). FK is enforced via migration 0030. Revisit if Drizzle adds deferred-FK support.
- **`systemSettings` schema declares `uniqueIndex` instead of `NULLS NOT DISTINCT`** — Drizzle's `uniqueIndex` builder doesn't surface the flag. Migration 0047 is the source of truth; `db:push` against an empty DB would skip the flag. Same documented-limitation pattern as `berths.current_pdf_version_id`.
- **One remaining `req.json()` in admin/custom-fields/[fieldId]** — intentional. The handler inspects raw body to detect `fieldType` mutation attempts; parseBody would lose the raw view. Documented inline.
---
## D. Inline TODOs in code (2 remaining)
| File:line | Note | Status |
| ------------------------------------------------------------------------------ | --------------------------------------------------------------- | --------------------------------------------------------------------------------------- |
| ~~`client-yachts-tab.tsx:93`~~ | YachtForm preset owner prop | ✅ landed 2026-05-07 (`initialOwner` prop) |
| ~~`interest-form.tsx:329`~~ | Include company-owned yachts where client is a member | ✅ landed 2026-05-07 (`yachtOwnerFilter` array filter) |
| ~~`interest-form.tsx:330`~~ | "Add new yacht" inline shortcut | ✅ landed 2026-05-07 (Plus button + YachtForm sheet) |
| [`src/lib/queue/scheduler.ts:44`](../src/lib/queue/scheduler.ts#L44) | Per-user reminder schedule (override on top of per-port digest) | Placeholder — per-port digest works; revisit when a customer asks for per-user override |
| [`src/lib/queue/workers/import.ts:13`](../src/lib/queue/workers/import.ts#L13) | CSV/Excel import worker — entire feature surface | Placeholder — nothing currently enqueues `import` jobs (verified) |
---
## E. Hidden / stubbed UI tabs
-**Company Documents tab** — landed 2026-05-08. `/api/v1/files` accepts `companyId`+`yachtId` filters; CompanyFilesTab + uploadZone wired through the storage abstraction.
- **Berth Waiting List + Maintenance Log tabs** — `src/components/berths/berth-tabs.tsx:346`. Removed entirely; revisit if/when product asks.
- **Interest Contract / Reservation tabs** — `src/components/interests/interest-{contract,reservation}-tab.tsx`. Render a "coming soon" friendly card; the real flow is gated on Documenso Phases 26.
---
## G. Dependencies / audit roadmap (post-PDF-overhaul)
**Source:** [`docs/AUDIT-2026-05-12.md`](./AUDIT-2026-05-12.md) §§ 34-36 +
[`docs/superpowers/specs/2026-05-12-pdf-stack-overhaul-design.md`](./superpowers/specs/2026-05-12-pdf-stack-overhaul-design.md).
What's done (2026-05-12 session — all phases shipped):
-**PDF stack overhaul**`@react-pdf/renderer` + brand kit + port logo upload pipeline; 4 reports + 3 record exports + parent-company expense ported; pdfme uninstalled; pdfkit retained for streaming expense PDF (now with shared brand-header). Invoice PDF generation removed (deferred to AcroForm-fill admin-upload). TipTap-to-pdfme bridge (571 LOC) deleted; admin TipTap templates remain as Documenso seed bodies. `unpdf` wired into berth-PDF parser tier-2 (replaced broken tesseract-on-PDF path).
-**react-email templates** — all 7 remaining (crm-invite, document-signing×3, inquiry×2, residential×2, notification-digest, admin-email-change) ported from string templates to React components. Public API surface now `async`. The whole email template directory is uniformly react-email.
-**browser-image-compression** — wired into scan-shell so 4-12 MB phone photos crush to ~500 KB in a WebWorker before tesseract / upload. Massive mobile bandwidth + battery + perceived-latency win.
-**@axe-core/playwright** — smoke spec runs WCAG 2.1 A/AA against 6 main pages; CI fails on new critical/serious violations.
-**ts-pattern in search.service.ts** — converted both switches to `match().with().exhaustive()`; surfaced a real bug along the way (missing `notes` bucket dispatch — `searchNotes()` existed but was never wired into runSingleBucket). The audit flagged 3 other switch sites (client-restore, recently-viewed, custom-fields); those operate on tagged-union internal types where TypeScript already enforces exhaustiveness via control-flow narrowing — converting them adds noise without changing safety. **Done.**
-**p-limit in mass-op services** — bounded fan-outs on the three real unbounded `Promise.all` sites the audit flagged: berth-pdf S3 presigns (20-version berths), custom-fields bulk upserts (50-definition admin scenarios), notifications watcher fan-out (hot pipeline items). Audit also speculatively flagged brochures.service + backup.service — verified neither has an unbounded fan-out. **Done.**
-**formatDate helper** — single source of truth in `src/lib/utils/format-date.ts` backed by `Intl.DateTimeFormat` (no new dep). 9 named presets, TZ-aware via `tz` opt, defensive against null/Invalid Date. `formatDateRange` collapses same-year strings. `formatRelative` via `Intl.RelativeTimeFormat`. 17 unit tests. Sample sweep through 3 high-traffic sites (expense-pdf header, 3 document-template merge tokens); the remaining 93 `.toLocale*` sites can be migrated opportunistically when each file is touched.
-**@tanstack/react-virtual in DataTable** — opt-in `virtual` prop. Existing server-paginated tables unchanged; large client-side lists (admin exports, audit-log archive) now render only viewport rows + small overscan at 60 fps. Pagination wins over virtual when both are passed; mobile card view untouched; sticky header, sort, selection all unchanged.
-**drizzle-zod adoption** — pattern proven in tags.ts + brochures.ts (earlier commit). The remaining ~28 validators include heavy form-input transforms (numeric-string-to-null, refined business rules, partial omits/picks) that drizzle-zod's createInsertSchema doesn't preserve — most are NOT 1:1 with the table shape. Migration is net-wash on LOC and adds no safety. Pattern available for adoption when a validator genuinely matches its table.
-**Tier 2 polish** — surveyed each candidate. `fast-deep-equal` not needed (existing memo comparators work). `use-debounce` package adds no value over the in-tree 13-LOC hook. `@use-gesture/react`, `embla-carousel-react`, `yet-another-react-lightbox`, `react-resizable-panels` all need concrete UX surfaces or product decisions before wiring — added them to the parked list.
-**Pre-commit staged type-check**`scripts/tsc-staged.mjs` (30-LOC shim) replaces the broken `tsc-files` package (which silently no-ops under pnpm). Pre-commit now runs `tsc -p <temp-config>` against staged ts/tsx in ~3s vs ~22s full-project; type errors caught before they hit CI.
**React Compiler safety triage (post-Next-16 bump):**
The Next 15 → 16 upgrade brought `react-hooks` v7 with React Compiler safety rules. Initial sweep surfaced ~89 findings; categorical triage status as of 2026-05-12:
-`react-hooks/purity` (2 → 0) — promoted to `error`. Cleared by pinning `Date.now()` reads to a `useState`-backed `now` ticker in `notes-list.tsx`.
-`react-hooks/set-state-in-render` (5 → 0) — promoted to `error`. `useMemo` mis-used for side effects in `interest-contact-log-tab.tsx`; converted to `useEffect`.
-`react-hooks/immutability` (7 → 0) — promoted to `error`. Mutable `useMemo` value in `documents-hub.tsx` drag counter → `useRef`. `let angle` mutation in `PieChart.tsx` slice loop → `reduce`. Three "function used before declared" hits (load/loadProfile in admin/onboarding-checklist + settings/user-profile + settings/user-settings) → declared inside the calling `useEffect`.
-`react-hooks/refs` (10 → 0) — promoted to `error`. Three `ref.current = x` writes during render moved into a layout-effect (`use-realtime-invalidation.ts`, `settings-form-card.tsx`, `inbox.tsx`). Three search-related `ref.current` reads during render rewritten to backed-by-state (`command-search.tsx`, `mobile-search-overlay.tsx`). Scan shell's `fileRef.current.files[0]` read replaced with a tracked `currentFile` state.
-`react-hooks/incompatible-library` (13 → silenced as `off`) — purely informational ("Compiler skipped this file because of a non-Compiler-safe import"). No action needed.
- ⚠️ `react-hooks/set-state-in-effect` (51 → still warn) — almost entirely the `useEffect → fetch → setState` data-loading pattern in admin-form components. Each call site refactors cleanly to TanStack Query (`useQuery`), but it's per-file mechanical work × ~45 files = focused half-day pass. Two hooks already migrated as templates: `use-is-mobile.ts` (→ `useSyncExternalStore`), `use-notifications.ts` (socket pushes via `queryClient.setQueryData` instead of local state).
**Next pass: data-fetching pattern migration.** Find admin components with `const [data, setData] = useState(null); useEffect(() => { fetch(...).then(setData) }, []);` and replace with `useQuery({ queryKey, queryFn })`. Land sites: every file in `src/components/admin/*` and a handful of dialog flows (smart-archive-dialog, send-document-dialog, etc.) listed in `eslint.config.mjs`. Each is ~10 min of contained refactor; doing 5 a day for a week clears the suite. Promote `set-state-in-effect` to `error` once cleared.
---
Remaining (opportunistic, no concrete trigger):
| Item | Estimate | Notes |
| --------------------------------------------------- | ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`.toLocale*` remainder (93 sites)** | ~2-3h opportunistic | Migrate to `formatDate(...)` as you touch each file. Helper already shipped; 17 tests; sweep proven on PDF + template paths. |
| **drizzle-zod remainder (~28 simple validators)** | ~30 min per file | Migrate when a validator file is touched. Pattern proven in tags + brochures. |
| **Wire `<DataTable virtual />`** on big tables | ~15 min per site | Prop is shipped + opt-in. Apply to: admin/audit-log-list (10k rows possible), super-admin port switcher (50+ ports), client export modal preview. None blocking. |
| **Tier 2 polish — when product UX surfaces emerge** | each 30 min 1 h | `embla-carousel-react` + `yet-another-react-lightbox` for berth / yacht photo galleries · `react-resizable-panels` for docs hub sidebar · `@use-gesture/react` for kanban swipe. |
Decisions / parked:
- ~`@upstash/ratelimit`~ — **rejected on inspection.** Audit claimed "4 hand-rolled rate limiters"; actual state is **one** centralized sliding-window Redis limiter (`src/lib/rate-limit.ts`) with 14 named policies + atomic pipeline. Replacement is pure churn.
- ~`@faker-js/faker`~ — **rejected on inspection.** Both seed files (`seed-data.ts`, `seed-synthetic-data.ts`) are hand-curated demo specs (per-pipeline-stage clients with locale-correct names/phones/addresses keyed to test selectors). No fake-data factory exists to replace — adopting faker means WRITING the factory + losing curation. Net add, not net subtract.
- ~`msw`~ — **rejected on inspection.** Integration tests already mock external services via `vi.mock('@/lib/services/documenso-client', ...)` at the module boundary — equivalent determinism, no extra layer. MSW only wins when tests hit `fetch()` directly, which we don't.
- `next-safe-action` — pilot on a new form first (no concrete trigger).
- `@sentry/nextjs` — needs SaaS-dep decision.
- `@tiptap/core` upgrade — needs product decision on rich notes.
- `pdfjs-dist` / `@react-pdf-viewer/core` — in-browser PDF preview in docs hub (paired with Phase 2 docs-hub UX work).
- `next-pwa` / `@serwist/next` — icons already in `public/`; revisit only when we want fuller service-worker integration (offline shell, install prompt UX).
- `next-intl` — no current i18n target.
- `posthog-js` — analytics scope decision.
- `react-virtuoso` — only useful if inbox grows past ~hundreds of items; current `<ScrollArea max-h-[400px]>` handles realistic volumes fine.
- `react-imask` / `react-number-format` — input masks across ~6 forms. Decision pending: hand-rolled formatters work today.
- `type-fest` — opportunistic types; no concrete trigger.
- `partysocket` — Socket.IO-protocol incompatible without significant rework.
Major deferrals from §34 of audit:
- ~**Next 15 → 16**~ — **DONE 2026-05-12**. middleware.ts → proxy.ts via codemod, native flat eslint config, react-hooks v7 Compiler safety rules surfaced + triaged.
- ~**Tailwind 3 → 4**~ — **DONE 2026-05-12**. Official upgrade tool migrated 80 files; tailwind-animate → tw-animate-css; theme moved to @theme directive in globals.css.
- **eslint 9 → 10** — attempted, reverted: `eslint-config-next@16` still has a transitive on `eslint-plugin-react@7` that uses removed eslint-9 context API. Re-attempt when upstream lands eslint-plugin-react@8.
- **archiver 7 → 8** — no `@types/archiver@8` published; skip indefinitely.
---
## H. Grand audit cleanup plan (post-deps)
**Source:** [`docs/AUDIT-2026-05-12.md`](./AUDIT-2026-05-12.md) — 534 findings across 27 domain reports + [`docs/AUDIT-FOLLOWUPS.md`](./AUDIT-FOLLOWUPS.md) + [`docs/AUDIT-TRIAGE.md`](./AUDIT-TRIAGE.md).
Deps work is complete (sections A-G above). Remaining audit cleanup is grouped into focused waves so it's tackleable a chunk at a time. Each wave has clear scope, file pointers, and acceptance criteria.
### Wave 1 — Stop-ship CRITICALs (security + data integrity)
Roughly half-day each; ship in priority order. These are the items from the audit's `## Cross-cutting priority queue` marked `[C]`:
1. **Real `db:migrate` runner**`0052_audit_critical_fixes.sql` uses `CREATE INDEX CONCURRENTLY` which silently never runs under `db:push`. Six composite indexes missing in prod. Build a tsx runner that reads migrations in order, splits on `--> statement-breakpoint`, executes outside a tx, tracks state in `__drizzle_migrations`. ~3-4 h. **(data-model C1)**
2. **`EMAIL_REDIRECT_TO` production guard** — `src/lib/env.ts` should refine to reject when `NODE_ENV === 'production'`; `src/lib/email/index.ts` should `logger.warn` at boot. 5-min change, prevents a very-bad-day class of incident. **(email C1)**
3. **Orphan-blob fix in `handleDocumentCompleted`**`src/lib/services/documents.service.ts:1100-1253`. Wrap `storage.put + files.insert + documents.update` in a transaction (or saga with compensating delete). Current catch-block leaves blob in storage AND marks `status='completed'` with no `signedFileId`. ~2 h. **(services C2)**
4. **Escape URLs in email templates** — every template in `src/lib/email/templates/*` inlines `${data.link}` etc. into `href="…"` and link text without escaping. Add `escapeUrl` helper + http(s) scheme allow-list; route every template through it. ~3 h. **(email C2)**
5. **Replace 16 native `window.confirm()` calls** — destructive flows bypassing `ConfirmationDialog` / `AlertDialog`. ui-ux-auditor's C1 lists the sites (cancel signing, delete files, archive interest/company/yacht…). ~30 min per site = full day. **(ui/ux C1)**
6. **GDPR Article-15 export completeness**`src/lib/services/gdpr-bundle-builder.ts` is missing: portal_users, email_threads/messages, document_sends, reminders, files, scratchpadNotes, client_merge_log, contact_log, website_submissions, form_submissions. Regulator-finding-level gap. ~half-day. **(gdpr C1)**
7. **Right-to-be-forgotten actually erase**`src/lib/services/client-hard-delete.service.ts` nullifies FKs but leaves verbatim PII in `email_messages.body_html`, `files`, `document_sends.recipient_email`. Add true-wipe path. ~half-day. **(gdpr C2)**
8. **`user_permission_overrides.user_id` FK + `onDelete='set null'`** — data-model H1+H2. Single migration. ~30 min. **(data-model H1+H2)**
9. **Resolve-identifier endpoint replacement** — current rate-limited hit still echoes the real canonical email on a successful username hit. Replace with a server-side signIn proxy that takes `{identifier, password}` together and never returns canonical emails at all. ~2 h. **(security/gdpr crossover)**
### Wave 2 — HIGH-priority security + observability (5-7 days)
10. **`audit_logs.metadata` PII masking** — extend `maskSensitiveFields` to cover `audit_logs.metadata`; add 90-day retention cron mirroring `error_events`. ~2 h. **(gdpr H)**
11. **Webhook → error pipeline**`src/app/api/webhooks/documenso/route.ts` bypasses `captureErrorEvent` on handler crash. Apply to every webhook route. ~2 h. **(observability H)**
12. **Admin email-template subject editor** — 5 of 8 templates ignore `overrides.subject`; admins see "Saved" with zero effect. Wire all 8. ~2 h. **(email H1+H2)**
13. **Admin signature/footer fields**`/admin/email` writes `email_signature_html` + `email_footer_html` which the email shell never reads. Either delete the UI or wire it. ~half-day. **(email H3)**
14. **PII redaction in error pipeline**`error_events.request_body_excerpt` sanitizer redacts password/token but not email/phone/name/dob/address. ~2 h. **(observability H + gdpr)**
15. **Notification email worker XSS**`src/lib/queue/workers/notifications.ts:65-71` interpolates `notif.description` and `notif.link` into HTML unescaped. Apply `escapeHtml` + URL allow-list (the `isomorphic-dompurify` we shipped helps here). ~1 h. **(email H + security)**
### Wave 3 — React Compiler set-state-in-effect cleanup (~41 sites)
Remaining 41 `react-hooks/set-state-in-effect` warnings. Two patterns established this session as templates:
- **List/load pattern** (`src/components/admin/tags/tag-list.tsx` is the template): `useState([]) + useEffect(fetch+setState)``useQuery({ queryKey, queryFn })`. Mutation paths get `useMutation` with `onSuccess: queryClient.invalidateQueries`. ~10 min per site.
- **Dialog open→reset pattern** (`src/components/clients/hard-delete-dialog.tsx` is the template): inner `<DialogBody key={id} ... />` mounted only while `open`, so `useState` initializers run naturally on each open without an open→reset useEffect. ~15 min per site.
Migrate as a focused day's work, then promote `react-hooks/set-state-in-effect` from `warn` to `error` in `eslint.config.mjs` to lock in.
### Wave 4 — UI/UX consistency + accessibility (~3-4 days)
- **Raw enum render via `.replace(/_/g, ' ')` (40+ sites)** — extract to `constants.ts` `formatStage`/`formatStatus`/`formatPriority` helpers. **(ui/ux H1)**
- **18 list components missing mobile `cardRender`** — DataTable already supports it; populate per-list. **(ui/ux H2)**
- **Berth status pills using ad-hoc Tailwind colors** — swap to shared `StatusPill`. **(ui/ux M1)**
- **UserList "Active"/"Disabled" badge** — align to `StatusPill` convention. **(ui/ux M2)**
- **Drawer vs Sheet usage drift** — pick one based on a documented rule (`Drawer` for mobile-bottom-sheet, `Sheet` for side panels). Standardize. **(ui/ux M11)**
- **Decorative icons missing `aria-hidden`** — sweep. ~1 h. **(ui/ux M10)**
- **Hard-coded "border-amber-300 bg-amber-50" callouts (15+ sites)** — extract `<WarningCallout>` component. **(ui/ux L5)**
- **Dashboard route `loading.tsx` coverage** — 5 of 73 routes have it; add for the rest. **(ui/ux M3)**
### Wave 5 — Performance + reliability (~2-3 days)
- **Concurrency races** — concurrency-auditor flagged multi-rep edit windows + partial-unique-index insert windows. Add row-level locks (`SELECT … FOR UPDATE`) on the affected paths. **(concurrency C, H)**
- **Postgres FTS for `search.service.ts`** — current `LIKE '%term%'` doesn't rank and slows at scale. Add `tsvector` GIN indexes; rewrite `searchClient/yacht/company/interest` against `to_tsquery()`. ~half-day migration + half-day service refactor. **(audit 36.K.1)**
- **`useEffect → fetch → setState` data-loading** (covered by Wave 3 above).
### Wave 6 — Email + Documenso depth (~2-3 days)
- **Documenso integration depth** (documenso-auditor report) — full v1/v2 audit, recipient signing URL handling, redirect URL per-port, sequential signing flag.
- **Email deliverability** (email-auditor report) — subject editor wire-up (Wave 2 #12), signature/footer wire-up (Wave 2 #13), bounce monitoring sanity check, attachment threshold UX.
### Wave 7 — Reporting + recommender quality (~half-week)
- **Reporting math correctness** (reporting-auditor) — verify revenue, pipeline funnel, occupancy math against hand-computed truth set.
- **Berth recommender quality** (recommender-auditor) — tier ladder edge cases, heat-score weight calibration.
### Wave 8 — Long tail (whenever)
- **PDF + brand asset correctness** (pdf-auditor) — verify each PDF template renders with port branding pulled correctly.
- **Customer-facing copy + terminology** (copy-auditor) — sweep for inconsistent terms ("berth/mooring", "owner/lessee", etc.).
- **Onboarding + first-run UX** (onboarding-auditor) — new-port walkthrough refinement.
- **Type-safety + drizzle leak audit** (types-auditor) — `any` casts, untyped `metadata` fields.
- **Build + deploy + prod readiness** (build-auditor) — Dockerfile review, env var validation.
### How to use this section
- Pick a wave; pick an item; read the linked audit section for full context.
- Each item closes with a commit in the `fix(audit-<wave>): ...` format so it's trivially greppable.
- Mark items DONE inline in this section as they ship.
- Audit-FOLLOWUPS.md tracks Wave 1-10 from an earlier sweep — items there may already be done or supplanted by AUDIT-2026-05-12.
Future PDF-related work (carry-over from §A of the PDF overhaul spec):
- **AcroForm-fill admin-uploaded PDF templates** (~1 week solo): new `pdf_templates` table + admin upload UI + field-mapping editor + generalize `fill-eoi-form.ts` into a reusable `fillAcroForm()` utility. Reinstates the invoice PDF path (and any future customer-facing standardized doc).
- **Port brand color tokens** (~2 h): admin sets brand color → flows into the PDF brand kit accent.
- **Optical receipt-photo rotation/deskew** (~half day): auto-rotate phone-upload receipts that EXIF misses.
---
## F. Historical audit docs (mostly resolved)
These dossiers drove the audit-fix commit waves on 2026-05-05/06. Items
not surfaced in §C above were resolved via the `fix(audit): …` commits
(`588f8bc`, `94331bd`, `a8c6c07`, `5fc68a5`, `da7ede7`, `c5b41ca`,
`b4fb3b2`, `0f648a9`, `c312cd3`, `0a5f085`, `1a87f28`, `f3143d7`,
`05babe5`). Keep for historical context:
- [`audit-comprehensive-2026-05-05.md`](./audit-comprehensive-2026-05-05.md) — pre-merge audit (1 CRIT + 18 HIGH at start)
- [`audit-comprehensive-2026-05-06.md`](./audit-comprehensive-2026-05-06.md) — post-merge audit (1 CRIT + 7 HIGH + 10 MED + 7 LOW)
- [`audit-frontend-2026-05-06.md`](./audit-frontend-2026-05-06.md) — frontend-only sweep
- [`audit-missing-features-2026-05-06.md`](./audit-missing-features-2026-05-06.md) — admin-promised-but-unwired features (V1V12)
- [`audit-permissions-2026-05-06.md`](./audit-permissions-2026-05-06.md) — permission-gate gaps
- [`audit-reliability-2026-05-06.md`](./audit-reliability-2026-05-06.md) — transactional integrity / TOCTOU
- [`berth-feature-handoff-prompt.md`](./berth-feature-handoff-prompt.md) — berth recommender handoff (shipped, kept as reference)
- [`berth-recommender-and-pdf-plan.md`](./berth-recommender-and-pdf-plan.md) — berth recommender + per-berth PDF plan (Phases 08 shipped)
- [`documenso-integration-audit.md`](./documenso-integration-audit.md) — Documenso integration spec (drives §A)
- [`website-refactor.md`](./website-refactor.md) — public website cutover plan