Files
pn-new-crm/docs/BACKLOG.md
Matt 449b9497ab fix(uat): batch — timeline overshoot, name-sync, reset-password, dashboard cleanup, queue/seed hygiene + alpha UAT findings doc
UAT findings landed across the last few Playwright + React Grab passes;
single grouped commit so the index doesn't fragment into 30 one-liners.

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

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

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

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

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

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 15:56:11 +02:00

46 KiB
Raw Blame History

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 — full phase plan with locked decisions (Q1Q10). Tracker delta: docs/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 §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 — landed 2026-05-13. Shared <TemplateTokenPicker> (src/components/admin/shared/template-token-picker.tsx) renders the canonical MERGE_FIELDS catalog grouped by scope plus a dynamically-fetched "Custom (port-specific)" group filtered to entityTypes resolvable at send-time (client/interest/berth). Wired into both sales-email-config-card.tsx and document-templates/template-form.tsx so both pickers share the same surface.

C. Audit-final deferred items

Source: docs/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:

  • Documenso webhook does not enforce port_id on document lookupssrc/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 eventssrc/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 verificationsrc/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 layersrc/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 FKsrc/lib/db/schema/berths.ts:83. The in-line comment fully documents why (circular FK between berthsberth_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 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 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 tabssrc/components/berths/berth-tabs.tsx:346. Removed entirely; revisit if/when product asks.
  • Interest Contract / Reservation tabssrc/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 §§ 34-36 + docs/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-checkscripts/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 → 0) — promoted to error in eslint.config.mjs. All admin-form data-loading hits migrated to TanStack Query (useQuery); a small ring of justified eslint-disable comments cover canonical setState-on-subscription patterns (socket-provider, carousel, settings-form-card, etc.). New regressions block CI.

Data-fetching pattern migration: DONE. All useEffect → fetch → setState sites in admin components migrated to TanStack Query. set-state-in-effect is now an ESLint error, so new regressions can't land.


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/ratelimitrejected 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/fakerrejected 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.
  • mswrejected 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 → 16DONE 2026-05-12. middleware.ts → proxy.ts via codemod, native flat eslint config, react-hooks v7 Compiler safety rules surfaced + triaged.
  • Tailwind 3 → 4DONE 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 — 534 findings across 27 domain reports + docs/AUDIT-FOLLOWUPS.md + docs/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 runner0052_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 guardsrc/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 handleDocumentCompletedsrc/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 completenesssrc/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 erasesrc/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)

  1. audit_logs.metadata PII masking — extend maskSensitiveFields to cover audit_logs.metadata; add 90-day retention cron mirroring error_events. ~2 h. (gdpr H)
  2. Webhook → error pipelinesrc/app/api/webhooks/documenso/route.ts bypasses captureErrorEvent on handler crash. Apply to every webhook route. ~2 h. (observability H)
  3. 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)
  4. 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)
  5. PII redaction in error pipelineerror_events.request_body_excerpt sanitizer redacts password/token but not email/phone/name/dob/address. ~2 h. (observability H + gdpr)
  6. Notification email worker XSSsrc/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 (~40 sites remaining)

Remaining react-hooks/set-state-in-effect warnings: 40 (was 41; reduced 2026-05-13). 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; new exemplar: src/components/documents/move-to-folder-dialog.tsx): 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 (~40 × 10-15 min), then promote react-hooks/set-state-in-effect from warn to error in eslint.config.mjs to lock in. NOTE: Warnings only — no functional regressions; promotion blocked solely until 0 warnings remain.

Wave 4 — UI/UX consistency + accessibility (~3-4 days)

  • Raw enum render via .replace(/_/g, ' ') (40+ sites) — extracted to constants.ts formatStage/formatStatus/formatPriority helpers (audit-wave-4). (ui/ux H1)
  • 18 list components missing mobile cardRender — Wave 9.4 covered the 5 actual DataTable consumers without cardRender (admin/tags, admin/roles, admin/ports, admin/document-templates, admin/custom-fields). (ui/ux H2)
  • Berth status pills using ad-hoc Tailwind colors — swapped to shared StatusPill in Wave 9.2. (ui/ux M1)
  • UserList "Active"/"Disabled" badge — aligned to StatusPill in Wave 9.2; also PortList in Wave 9.4. (ui/ux M2)
  • Drawer vs Sheet usage drift — single offender (client-interests-tab) swapped to Sheet; doctrine documented in CLAUDE.md (Wave 9.1). (ui/ux M11)
  • Decorative icons missing aria-hidden — Wave 10.4 mechanical sweep added aria-hidden to 444 self-closing single-line Lucide icons across 267 .tsx files. (ui/ux M10)
  • Hard-coded "border-amber-300 bg-amber-50" callouts (15+ sites)<WarningCallout> shipped in Wave 4. (ui/ux L5)
  • Dashboard route loading.tsx coverage — default [portSlug]/loading.tsx plus tailored detail-page skeletons (Wave 9.5). (ui/ux M3)

Wave 5 — Performance + reliability (~2-3 days)

  • Concurrency races — Wave 10.3 closed the CRITICAL + tractable HIGH items: handleDocumentCompleted concurrent-retry TOCTOU via SELECT FOR UPDATE re-check (C-1), moveFolder cycle-check race via per-port pg_advisory_xact_lock (H-1), upsertInterestBerth 23505 → ConflictError (H-3), username uniqueness 23505 → ConflictError (M-2). Wide-impact items (BullMQ jobId plumbing — C-2) remain deferred. (concurrency C, H)
  • Postgres FTS for search.service.ts — migration 0057_search_fts_indexes.sql shipped in Wave 5. (audit 36.K.1)
  • useEffect → fetch → setState data-loading — covered by Wave 3.

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) — Wave 9.6: wrong-port brand fallback ('Port Nimara'(port)/throw), AcroForm field-drift warnings, EOI form flatten, PDF metadata, sha256 pinning of assets/eoi-template.pdf, berth-range warning noise. Items C-2/C-3 (tiptap-to-pdfme bugs) were eliminated by the 2026-05-12 PDF stack overhaul.
  • Customer-facing copy + terminology (copy-auditor) — Wave 9.7: centralized lib/labels/document-status.ts (C3), portal leadCategory chip removed (C2), Save ChangesSave changes + Saving...Saving… codemod (H1, M3), envelope → signing request (M1), Linked prospectLinked interest, Deal DocumentsInterest Documents, Hot LeadHot lead (M5).
  • Onboarding + first-run UX (onboarding-auditor) — Wave 9.8: fixed wrong setting keys in checklist auto-checks (C1), broken forms href (C2), compound gate for Documenso EOI readiness (C3), catch-and-log around ensureSystemRoots (C4), fresh-port berth empty state (H5), admin-sections-browser description (M4).
  • Type-safety + drizzle leak audit (types-auditor) — Wave 10.1: Tx type exported (C-1), berth-detail useQuery<any> replaced with BerthDetailData (C-2), parseBody adopted across 7 portal/public routes (C-3), toAuditJson<T> helper removed 21 as unknown as Record<…> casts (H-5). Drizzle leak check came back clean (no $inferSelect crossing the API boundary).
  • Build + deploy + prod readiness (build-auditor) — Wave 10.2: socket.io + 6 other native deps added to serverExternalPackages + COPY-in-Dockerfile (C-3), NEXT_PUBLIC_APP_URL validation (H-2), healthcheck PORT templatization (H-5), NODE_ENV=production in builder (M9), image-level HEALTHCHECK (M7). CSP 'unsafe-inline' (H-1) deferred pending nonce middleware infrastructure.
  • Wave 11 — unaddressed-dossier sweep + cross-cutting infra:
    • BullMQ jobId plumbing (concurrency C-2): stable per-entity jobIds added across invoices (send-invoice, invoice-overdue-notify), gdpr-export, webhook-dispatch, expenses, webhooks.service, notifications, inquiry-notifications, reports (generate-report).
    • CSP nonce middleware (build-auditor H-1): per-request nonce in src/proxy.ts:buildCspWithNonce with 'self' 'nonce-<n>' 'strict-dynamic' in prod; next.config.ts fallback header kept for static assets / API JSON.
    • Error UX (error-ux-auditor): apiFetch synthesizes a client-side correlation id for non-JSON 5xx (C3); checkRateLimit fails open on Redis outage so auth doesn't lock (C4); StorageTimeoutError extends Error with name='TimeoutError' for classifier hints (H2); errorResponse() adopted across /api/storage/[token], /api/public/website-inquiries, Documenso webhook body cleaned (H5); 17 toast.error(err.message) sites swept to toastError(err, …) (C2).
    • Outbound webhooks (outbound-webhook-auditor): Stripe-style HMAC(secret, "${ts}.${body}") + X-Webhook-Timestamp header (C1); dead-letter when secret is null (C3); retry policy 8 attempts × 30s base exponential (H2); SSRF denylist gains Oracle Cloud 192.0.0.192 (M1); dispatch-time https:// assertion (M2).
    • Storage-pathing (storage-pathing-auditor): berth-PDF presigned-upload key prefixed with ${portSlug}/ + portSlug passed to presignUpload (H1); presignDownloadUrl infers the slug from the key's first segment when callers don't pass it explicitly — engages the filesystem-proxy port-binding p token verifier across every download site (H2).
    • Search (search-auditor): dead void wantEmail; void wantPhone; + unused looksLikeEmail helper removed (H3).
    • Maintainability (maintainability-auditor M2): swept seven void <symbol> abandoned-scaffolding markers and their dead imports across clients/bulk, interests/bulk, admin/email-templates, admin/website-submissions, alert-rules, and notes.service.

Wave 11 — explicitly deferred items (revisited 2026-05-13, deferred again)

Each was flagged by the audit but assessed as not-yet-needed for production correctness. Listed here so future-you doesn't re-research them.

Engineering refactors deferred:

  • Orphan-blob reaper (storage-pathing C2, ~4-6h) — handleDocumentCompleted already has compensating delete for the only frequent orphan path. Other paths (gdpr-export, backup, etc.) are low-frequency. Revisit when storage costs grow.
  • Webhook deliveries reaper (outbound-webhook C2, ~2-3h) — webhook_deliveries table grows unbounded on high-volume events. Zero active webhook subscribers today; revisit when customers actually subscribe.
  • DNS-rebind TOCTOU (outbound-webhook H1, ~2h) — Requires admin AND DNS control on the target host. Defense-in-depth on already-low-risk vector. Revisit before exposing webhooks to external integrators.
  • Streaming pass on backup/migrator/email-compose (storage-pathing H3+H4, ~4-6h) — pg_dump OOM at multi-GB. DB is ~10s of MB today. Revisit when DB grows 100x.
  • Webhook circuit-breaker (outbound-webhook H3, ~3-4h) — Auto-disable webhooks after N consecutive dead-letters. Saturating worker slots requires active webhook subscribers; none today.

Mechanical service splits deferred:

  • documents.service.ts split (1982 lines → 4 files, ~3-4h)
  • search.service.ts split (2163 lines → per-bucket files, ~4-6h)
  • notes.service.ts dedup → dispatch table (1121 → ~500 lines, ~3-4h)
  • interest-tabs.tsx split (959 lines → 3 files, ~2-3h)
  • expense-pdf.service.ts split (987 → 3 files, ~2h)
  • command-search.tsx split (1177 → 5 files, ~3-4h)

Pure code-hygiene work. The files are large but functional. Splitting touches hundreds of imports, risks regression, delivers zero user value. Revisit if/when navigation friction becomes a real bottleneck.

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.

J. Activity / timeline copy normalization

Every "Activity" or "Timeline" surface across the app currently leaks raw schema details — camelCase field names, UUID values, boolean on/off — straight into the user-visible copy. Real examples seen in production:

  • Updated owner → mEcsLxo5kyFMyhbOSehxJjYSSD7CiLvv (user UUID)
  • Updated primary berth → a53e3b1d-d589-4f11-9f7b-3b3a3c1ebb8e (berth UUID)
  • Updated primary berth → a53e..., isInEoiBundle → on (raw camelCase + boolean)

Two distinct renderers need a single source of truth:

  1. InterestTimeline (src/components/interests/interest-timeline.tsx) reads pre-built description strings from /api/v1/interests/[id]/timeline/route.ts — see buildAuditDescription + describeUpdateDiff + formatDiffValue. Field-label catalog is partial; FK values are unresolved.
  2. EntityActivityFeed (src/components/shared/entity-activity-feed.tsx) — used by clients, companies, yachts, berths, residential clients, residential interests. Builds copy client-side via sentence() + formatValueForField. Catalog is even thinner (only pipelineStage / source / leadCategory / outcome get human labels).

Plan-of-work:

  • Build a shared src/lib/audit/format-audit.ts with:
    • FIELD_LABELS per entity type (interest, client, company, yacht, berth, residential_*) covering every column we actually surface in audits. Today's gaps: isInEoiBundle, isSpecificInterest, isPrimary, assignedTo, currentOwnerType/Id, companyId, parentCompanyId, mooringNumber, priceCurrency, all the *_at/date fields beyond the EOI/contract handful.
    • Value formatter that handles: booleans contextually (e.g. isInEoiBundle: true → "added to EOI bundle" / false → "removed from EOI bundle"; never on/off), enums via the formatEnum/STAGE_LABELS/OUTCOME_LABELS helpers in src/lib/constants.ts, currency+amount pairs, dates via formatDate.
    • FK resolution: take a Record<fkField, displayName> lookup that callers prefill (mooring number for berthId, user name for assignedTo, client name for clientId, etc.) so values render as "→ Anna Schmidt" not "→ mEcs…".
  • Update /timeline (interests) AND the 6 /activity route handlers to: (a) collect FK ids per row, (b) batch-resolve in one query per FK type, (c) pass the lookup into the shared formatter. The audit log itself stores IDs — resolution happens at read time so historical entries stay correct even after renames/deletes (in which case fall back to "(deleted yacht)" etc.).
  • Migrate EntityActivityFeed to call the same shared formatter on the row's fieldChanged + oldValue/newValue so the strikethrough+arrow rendering uses the same vocabulary.
  • Audit-log writes that have meaningful application context but don't fit the column-diff model (e.g. interest-berth flag toggles, EOI bundle membership changes) probably should set metadata.type so the formatter can route to a dedicated phrase ("Added berth A12 to EOI bundle", "Made A12 the primary berth") instead of best-effort diffing.

Acceptance: spot-check the timeline tab on a recently-edited interest, client, yacht, company, and berth. No UUIDs visible; no camelCase field names; no on/off booleans without context; all enum values render in their human label.

Done while scoping (cosmetic fix):

  • Vertical-connector overshoot in InterestTimeline and EntityActivityFeed — both renderers used a container-level absolute line that trailed past the last bubble. Replaced with per-item connectors that omit on isLast.

I. Dashboard widget wishlist

User-driven enhancements to the customizable main dashboard (src/components/dashboard/widget-registry.tsx). Each entry is a new opt-in tile users can add via the widget picker.

  • More website-analytics stats cards — expand the dashboard widget catalogue with additional Umami-backed tiles users can pick from (e.g. unique visitors, avg session duration, bounce rate, top country, top referrer of the day, mobile vs desktop split, pages-per-visit, returning vs new). Today only WebsiteGlanceTile exists. Source data already flows through src/lib/services/umami.service.ts and useWebsiteAnalytics. Each new tile = one KpiTile-shaped component + a registry entry. Size: small per tile, scope grows with the catalogue.

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: