Ship-what's-done prep ahead of the prod cutover (launch ~today): - Hide Financial + Marketing report cards from the reports landing (both were "Builder in development" placeholders gated on unbuilt data sources). Sales/Operational/Custom + templates/scheduling/ exports remain live. - Trim the Custom-report card copy to match the shipped basic builder (no group-by/filters yet; the builder page header was already honest). - Hide the Bulk Import mockup from search-nav-catalog + the admin sections browser; /admin/import is now unreachable from the UI. - Correct client-facing doc over-claims (waiting-list "next-in-line notification", Import) in features-list.md + new-system-feature-summary.md. - Un-stale BACKLOG.md (Documenso phases 2-7 confirmed shipped). - Log decisions + deferred work (full importer, full custom-builder, waiting-list, maintenance-log, paper-upload bug) to launch-readiness.md. Deferred-importer design spec added at docs/superpowers/specs/2026-06-01-bulk-import-design.md. Verified: tsc --noEmit clean, eslint clean on changed files, 1512/1519 vitest pass (7 failures are Redis-down, unrelated). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
49 KiB
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 (MOSTLY SHIPPED — see note)
Stale-doc fix (2026-06-01): a feature-completeness sweep confirmed the core of phases 2–7 has since shipped and is wired — cascading "your turn" invites (Phase 2), custom doc upload-to-signing (Phase 3,
custom-document-upload.service.ts+/api/v1/interests/[id]/upload-for-signing), the field-placement UI (Phase 4,upload-for-signing-dialog.tsx), and Project Director user-linking (Phase 7). The integration is treated as feature-complete. The phase table below is kept for history; re-verify the Phase 5/6 polish line-items individually before relying on them.
Source: docs/documenso-build-plan.md — full phase plan with locked decisions (Q1–Q10).
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 | ~3–4h | 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 |
~6–8h | 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 |
~10–14h | 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 |
~1–2h | |
| Phase 6 | Polish: auto-send delay, audit-log additions, per-document customisation, document expiration, reminder rate-limit display, failed-webhook recovery UI | each ~2–3h | All deferred until Phases 1–4 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 viamergeCustomFieldValuesindocument-sends.service.ts. Banner updated. - Search index — DEFERRED as design limitation. Adding GIN coverage requires either joining
custom_field_valuesper 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 canonicalMERGE_FIELDScatalog grouped by scope plus a dynamically-fetched "Custom (port-specific)" group filtered to entityTypes resolvable at send-time (client/interest/berth). Wired into bothsales-email-config-card.tsxanddocument-templates/template-form.tsxso 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:
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 ondocumentEvents. 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'suserId: null as unknown as stringcast was already cleaned up to a properuserId: null. Remaining concern is testability: extract a sharedpublicInterestService.create(...). Pure ergonomics — no active bug or security issue.
Done in 2026-05-08 sweep (latest)
- ✅ Storage proxy port_id binding:
ProxyTokenPayloadgains optionalp(port slug) claim; verifier assertskey.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 bykeyalone. 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
onConflictDoUpdateupserts (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 inmergeCustomFieldValuessubstitutes from per-port custom_field_definitions + per-entity values for client/interest/berth contexts. Banner updated. - ✅
/api/v1/filesacceptscompanyIdandyachtIdfilters. 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_sendsinterestId port-verification helper - ✅ Custom-fields per-entity permission gate (replaces hardcoded
clients.view/edit) - ✅ EOI Berth Range warn log (was already in place)
- ✅ v1
placeFieldsretry 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)
- ✅
loadRecommenderSettingsaccepts string"true"/"false"JSONB booleans - ✅
renderReceiptHeadercursor math anchored to capturedbaseY - ✅ 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
anywithInvoiceDetailDatainterface) - ✅ All FK indexes called out in audit doc (already in place — audit was stale)
- ✅
documentSends.sentByUserIdFK (already had.references(...))
Documented limitations (no action planned)
berths.current_pdf_version_idlacks Drizzle FK —src/lib/db/schema/berths.ts:83. The in-line comment fully documents why (circular FK betweenberths↔berth_pdf_versionsmakes column-level.references()infeasible). FK is enforced via migration 0030. Revisit if Drizzle adds deferred-FK support.systemSettingsschema declaresuniqueIndexinstead ofNULLS NOT DISTINCT— Drizzle'suniqueIndexbuilder doesn't surface the flag. Migration 0047 is the source of truth;db:pushagainst an empty DB would skip the flag. Same documented-limitation pattern asberths.current_pdf_version_id.- One remaining
req.json()in admin/custom-fields/[fieldId] — intentional. The handler inspects raw body to detectfieldTypemutation 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/filesacceptscompanyId+yachtIdfilters; 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 2–6.
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.unpdfwired 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 (missingnotesbucket 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.allsites 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.tsbacked byIntl.DateTimeFormat(no new dep). 9 named presets, TZ-aware viatzopt, defensive against null/Invalid Date.formatDateRangecollapses same-year strings.formatRelativeviaIntl.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
virtualprop. 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-equalnot needed (existing memo comparators work).use-debouncepackage adds no value over the in-tree 13-LOC hook.@use-gesture/react,embla-carousel-react,yet-another-react-lightbox,react-resizable-panelsall 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 brokentsc-filespackage (which silently no-ops under pnpm). Pre-commit now runstsc -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 toerror. Cleared by pinningDate.now()reads to auseState-backednowticker innotes-list.tsx. - ✅
react-hooks/set-state-in-render(5 → 0) — promoted toerror.useMemomis-used for side effects ininterest-contact-log-tab.tsx; converted touseEffect. - ✅
react-hooks/immutability(7 → 0) — promoted toerror. MutableuseMemovalue indocuments-hub.tsxdrag counter →useRef.let anglemutation inPieChart.tsxslice loop →reduce. Three "function used before declared" hits (load/loadProfile in admin/onboarding-checklist + settings/user-profile + settings/user-settings) → declared inside the callinguseEffect. - ✅
react-hooks/refs(10 → 0) — promoted toerror. Threeref.current = xwrites during render moved into a layout-effect (use-realtime-invalidation.ts,settings-form-card.tsx,inbox.tsx). Three search-relatedref.currentreads during render rewritten to backed-by-state (command-search.tsx,mobile-search-overlay.tsx). Scan shell'sfileRef.current.files[0]read replaced with a trackedcurrentFilestate. - ✅
react-hooks/incompatible-library(13 → silenced asoff) — 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 toerrorin 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:
— rejected on inspection. Audit claimed "4 hand-rolled rate limiters"; actual state is one centralized sliding-window Redis limiter (@upstash/ratelimitsrc/lib/rate-limit.ts) with 14 named policies + atomic pipeline. Replacement is pure churn.— rejected on inspection. Both seed files (@faker-js/fakerseed-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.— rejected on inspection. Integration tests already mock external services viamswvi.mock('@/lib/services/documenso-client', ...)at the module boundary — equivalent determinism, no extra layer. MSW only wins when tests hitfetch()directly, which we don't.next-safe-action— pilot on a new form first (no concrete trigger).@sentry/nextjs— needs SaaS-dep decision.@tiptap/coreupgrade — 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 inpublic/; 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@16still has a transitive oneslint-plugin-react@7that uses removed eslint-9 context API. Re-attempt when upstream lands eslint-plugin-react@8. - archiver 7 → 8 — no
@types/archiver@8published; 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]:
- Real
db:migraterunner —0052_audit_critical_fixes.sqlusesCREATE INDEX CONCURRENTLYwhich silently never runs underdb: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) EMAIL_REDIRECT_TOproduction guard —src/lib/env.tsshould refine to reject whenNODE_ENV === 'production';src/lib/email/index.tsshouldlogger.warnat boot. 5-min change, prevents a very-bad-day class of incident. (email C1)- Orphan-blob fix in
handleDocumentCompleted—src/lib/services/documents.service.ts:1100-1253. Wrapstorage.put + files.insert + documents.updatein a transaction (or saga with compensating delete). Current catch-block leaves blob in storage AND marksstatus='completed'with nosignedFileId. ~2 h. (services C2) - Escape URLs in email templates — every template in
src/lib/email/templates/*inlines${data.link}etc. intohref="…"and link text without escaping. AddescapeUrlhelper + http(s) scheme allow-list; route every template through it. ~3 h. (email C2) - Replace 16 native
window.confirm()calls — destructive flows bypassingConfirmationDialog/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) - GDPR Article-15 export completeness —
src/lib/services/gdpr-bundle-builder.tsis 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) - Right-to-be-forgotten actually erase —
src/lib/services/client-hard-delete.service.tsnullifies FKs but leaves verbatim PII inemail_messages.body_html,files,document_sends.recipient_email. Add true-wipe path. ~half-day. (gdpr C2) user_permission_overrides.user_idFK +onDelete='set null'— data-model H1+H2. Single migration. ~30 min. (data-model H1+H2)- 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)
audit_logs.metadataPII masking — extendmaskSensitiveFieldsto coveraudit_logs.metadata; add 90-day retention cron mirroringerror_events. ~2 h. (gdpr H)- Webhook → error pipeline —
src/app/api/webhooks/documenso/route.tsbypassescaptureErrorEventon handler crash. Apply to every webhook route. ~2 h. (observability H) - 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) - Admin signature/footer fields —
/admin/emailwritesemail_signature_html+email_footer_htmlwhich the email shell never reads. Either delete the UI or wire it. ~half-day. (email H3) - PII redaction in error pipeline —
error_events.request_body_excerptsanitizer redacts password/token but not email/phone/name/dob/address. ~2 h. (observability H + gdpr) - Notification email worker XSS —
src/lib/queue/workers/notifications.ts:65-71interpolatesnotif.descriptionandnotif.linkinto HTML unescaped. ApplyescapeHtml+ URL allow-list (theisomorphic-dompurifywe 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.tsxis the template):useState([]) + useEffect(fetch+setState)→useQuery({ queryKey, queryFn }). Mutation paths getuseMutationwithonSuccess: queryClient.invalidateQueries. ~10 min per site. - Dialog open→reset pattern (
src/components/clients/hard-delete-dialog.tsxis the template; new exemplar:src/components/documents/move-to-folder-dialog.tsx): inner<DialogBody key={id} ... />mounted only whileopen, souseStateinitializers 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 toconstants.tsformatStage/formatStatus/formatPriorityhelpers (audit-wave-4). (ui/ux H1) - ✅ 18 list components missing mobile
cardRender— Wave 9.4 covered the 5 actual DataTable consumers withoutcardRender(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
StatusPillin Wave 9.2. (ui/ux M1) - ✅ UserList "Active"/"Disabled" badge — aligned to
StatusPillin Wave 9.2; alsoPortListin 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 addedaria-hiddento 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.tsxcoverage — default[portSlug]/loading.tsxplus 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:
handleDocumentCompletedconcurrent-retry TOCTOU via SELECT FOR UPDATE re-check (C-1),moveFoldercycle-check race via per-port pg_advisory_xact_lock (H-1),upsertInterestBerth23505 → 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— migration0057_search_fts_indexes.sqlshipped in Wave 5. (audit 36.K.1) - ✅
useEffect → fetch → setStatedata-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 ofassets/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), portalleadCategorychip removed (C2),Save Changes→Save changes+Saving...→Saving…codemod (H1, M3), envelope → signing request (M1),Linked prospect→Linked interest,Deal Documents→Interest Documents,Hot Lead→Hot lead(M5). - ✅ Onboarding + first-run UX (onboarding-auditor) — Wave 9.8: fixed wrong setting keys in checklist auto-checks (C1), broken
formshref (C2), compound gate for Documenso EOI readiness (C3), catch-and-log aroundensureSystemRoots(C4), fresh-port berth empty state (H5), admin-sections-browser description (M4). - ✅ Type-safety + drizzle leak audit (types-auditor) — Wave 10.1:
Txtype exported (C-1), berth-detailuseQuery<any>replaced withBerthDetailData(C-2), parseBody adopted across 7 portal/public routes (C-3),toAuditJson<T>helper removed 21as unknown as Record<…>casts (H-5). Drizzle leak check came back clean (no$inferSelectcrossing 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_URLvalidation (H-2), healthcheck PORT templatization (H-5),NODE_ENV=productionin 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:buildCspWithNoncewith'self' 'nonce-<n>' 'strict-dynamic'in prod;next.config.tsfallback header kept for static assets / API JSON. - Error UX (error-ux-auditor):
apiFetchsynthesizes a client-side correlation id for non-JSON 5xx (C3);checkRateLimitfails open on Redis outage so auth doesn't lock (C4);StorageTimeoutError extends Errorwithname='TimeoutError'for classifier hints (H2);errorResponse()adopted across/api/storage/[token],/api/public/website-inquiries, Documenso webhook body cleaned (H5); 17toast.error(err.message)sites swept totoastError(err, …)(C2). - Outbound webhooks (outbound-webhook-auditor): Stripe-style
HMAC(secret, "${ts}.${body}")+X-Webhook-Timestampheader (C1); dead-letter when secret is null (C3); retry policy8 attempts × 30s base exponential(H2); SSRF denylist gains Oracle Cloud192.0.0.192(M1); dispatch-timehttps://assertion (M2). - Storage-pathing (storage-pathing-auditor): berth-PDF presigned-upload key prefixed with
${portSlug}/+portSlugpassed topresignUpload(H1);presignDownloadUrlinfers the slug from the key's first segment when callers don't pass it explicitly — engages the filesystem-proxy port-bindingptoken verifier across every download site (H2). - Search (search-auditor): dead
void wantEmail; void wantPhone;+ unusedlooksLikeEmailhelper removed (H3). - Maintainability (maintainability-auditor M2): swept seven
void <symbol>abandoned-scaffolding markers and their dead imports acrossclients/bulk,interests/bulk,admin/email-templates,admin/website-submissions,alert-rules, andnotes.service.
- BullMQ jobId plumbing (concurrency C-2): stable per-entity jobIds added across
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) —
handleDocumentCompletedalready 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_deliveriestable 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.tssplit (1982 lines → 4 files, ~3-4h)search.service.tssplit (2163 lines → per-bucket files, ~4-6h)notes.service.tsdedup → dispatch table (1121 → ~500 lines, ~3-4h)interest-tabs.tsxsplit (959 lines → 3 files, ~2-3h)expense-pdf.service.tssplit (987 → 3 files, ~2h)command-search.tsxsplit (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_templatestable + admin upload UI + field-mapping editor + generalizefill-eoi-form.tsinto a reusablefillAcroForm()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:
InterestTimeline(src/components/interests/interest-timeline.tsx) reads pre-builtdescriptionstrings from/api/v1/interests/[id]/timeline/route.ts— seebuildAuditDescription+describeUpdateDiff+formatDiffValue. Field-label catalog is partial; FK values are unresolved.EntityActivityFeed(src/components/shared/entity-activity-feed.tsx) — used by clients, companies, yachts, berths, residential clients, residential interests. Builds copy client-side viasentence()+formatValueForField. Catalog is even thinner (onlypipelineStage/source/leadCategory/outcomeget human labels).
Plan-of-work:
- Build a shared
src/lib/audit/format-audit.tswith:FIELD_LABELSper 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"; neveron/off), enums via theformatEnum/STAGE_LABELS/OUTCOME_LABELShelpers insrc/lib/constants.ts, currency+amount pairs, dates viaformatDate. - 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/activityroute 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
EntityActivityFeedto call the same shared formatter on the row'sfieldChanged+oldValue/newValueso 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.typeso 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
InterestTimelineandEntityActivityFeed— both renderers used a container-level absolute line that trailed past the last bubble. Replaced with per-item connectors that omit onisLast.
K. Per-port branded login (multi-tenant UX)
The login / forgot-password / set-password screens currently show the
"first active port" branding via resolveAuthShellBranding(), because
those surfaces have no portId in the URL. With two unrelated ports
(Port Nimara + Port Amador, no umbrella company) this means whichever
port was created first wins the login screen for everyone.
Recommended path: shared instance, Host-header branding. Run a
wildcard subdomain (*.crm.example.com) into the same Next.js app and
have middleware derive the active portSlug from the Host header.
resolveAuthShellBranding() then takes an optional host argument and
resolves by slug instead of "first port". Switcher becomes a
window.location.assign('https://other-port.crm.example.com/dashboard');
session cookies are scoped to the parent domain so super-admins don't
re-auth when hopping.
Open work:
- Wildcard DNS + TLS cert (Cloudflare DNS-01 with
*.crm.example.com). - Cookie domain change:
pn-crm.session_tokenneedsDomain=.example.comset in better-auth config. - Middleware: read host, resolve portSlug, attach to request headers so the auth-shell branding resolver can use it.
- Update
resolveAuthShellBranding()to prefer host-derived port over "first port" fallback. - Port-switcher UI: dropdown in topbar that lists ports the user has access to and navigates cross-subdomain.
- Bootstrap seed: populate
branding_logo_url/_email_background_url/_app_namefor the default port so fresh deploys aren't blank.
Alternative considered: N instances, one per port. Cleaner data / deploy isolation but no UX gain over the shared-instance path. Defer unless an operator demands independent migrations or data residency.
Size: medium (1–2 days incl. cert + cookie work + seed + switcher).
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
WebsiteGlanceTileexists. Source data already flows throughsrc/lib/services/umami.service.tsanduseWebsiteAnalytics. Each new tile = oneKpiTile-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:
audit-comprehensive-2026-05-05.md— pre-merge audit (1 CRIT + 18 HIGH at start)audit-comprehensive-2026-05-06.md— post-merge audit (1 CRIT + 7 HIGH + 10 MED + 7 LOW)audit-frontend-2026-05-06.md— frontend-only sweepaudit-missing-features-2026-05-06.md— admin-promised-but-unwired features (V1–V12)audit-permissions-2026-05-06.md— permission-gate gapsaudit-reliability-2026-05-06.md— transactional integrity / TOCTOUberth-feature-handoff-prompt.md— berth recommender handoff (shipped, kept as reference)berth-recommender-and-pdf-plan.md— berth recommender + per-berth PDF plan (Phases 0–8 shipped)documenso-integration-audit.md— Documenso integration spec (drives §A)website-refactor.md— public website cutover plan