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>
46 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 (deferred for later)
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.
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