Wave through the 2026-05-07 backlog of small/concrete audit-final-deferred
items (deferring the Documenso Phases 2-7 build and items needing design
decisions or live external instances).
DB schema:
- Migration 0046 converts 5 composite (port_id, archived_at) indexes to
partial WHERE archived_at IS NULL — clients, interests, yachts, and
both residential tables. Smaller, faster planner choice for the
dominant list-query shape.
Multi-tenant isolation:
- document_sends now verifies recipient.interestId belongs to the port
before landing on the audit row (the surrounding clientId check was
already port-scoped; interestId pollution was the gap).
Routes / API:
- /api/v1/custom-fields/[entityId] requires entityType query param and
gates on the matching resource permission (clients/interests/berths/
yachts/companies). Fixes the cross-resource gap where a user with
clients.view could read company custom-field values.
- Admin user list trash button wrapped in PermissionGate (edit was
already gated; remove was not).
Service polish:
- berth-recommender accepts string-shaped JSONB booleans
('true'/'false') so admin UIs that wrap values as strings don't
silently fall through to defaults.
- expense-pdf renderReceiptHeader anchors all text positions to a
captured baseY rather than reading mutating doc.y after rect+stroke.
Headers no longer drift on the first receipt page after a soft page
break.
- berth-pdf apply: collect non-finite numeric coercion drops + warn-log
them so partial silent drops are observable (was invisible because
the no-fields-supplied check only fires when ALL drop).
- Storage cache fingerprint comment documenting the encrypted-secret
invariant + the explicit invalidation hook.
UI polish:
- invoice-detail typed: replaced two `any` casts with a proper
InvoiceDetailData / LineItem / LinkedExpense interface set.
- YachtForm now accepts initialOwner prop. Wired through:
- client-yachts-tab passes { type: 'client', id: clientId }
- interest-form passes { type: 'client', id: selectedClientId }
- Interest-form yacht picker now includes company-owned yachts where
the selected client is a member (fetches client.companies and feeds
YachtPicker an array filter). Plus an inline "Add new" button that
opens YachtForm pre-bound to the client.
- YachtPicker accepts ownerFilter as single OR array for "match any"
semantics.
BACKLOG.md updated with what landed vs what's still deferred (and why
each deferred item is genuinely larger than this push warrants).
Tests: 1185/1185 vitest, tsc clean.
15 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-07 (after the audit-final-deferred sweep — partial archived indexes, document_sends interestId port-verify, custom-fields per-entity permission gate, recommender bool parsing, expense PDF cursor math, berth PDF silent-drop logging, YachtForm preset-owner + interest form member-company yacht filter + add-new shortcut, invoice detail typed). Many older items in §C and §F were already resolved by earlier fix-audit commit waves; the audit doc was stale.
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 (~ongoing, deferred)
Source: docs/admin-ux-backlog.md §7.
Custom Settings page already shows the amber warning banner. Remediation work:
- Search index — extend the GIN tsvector to include
customFieldValuescontent - Audit diff — extend
diffEntityto walk thecustomFieldValuesblob - Merge tokens — add
{{custom.<fieldName>}}handling at template-render time, plus surface them in the merge-tokens UI
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 — needs design or larger refactor
- Storage proxy token does not bind to port_id —
src/lib/storage/filesystem.ts:73-84. Adding ap(portId) claim is mechanical; the meaningful security gain requires the proxy verifier to look up the file's owning row + assertowner.portId === payload.p. That requires either a routing prefix in the key (currently${portSlug}/...already, so a prefix check is plausible) or a per-table lookup across all owners. Decide which approach before implementing — current state ships withvalidateStorageKey+ per-issuer port scoping, so this is defense-in-depth rather than an open hole. - Documenso webhook does not enforce port_id on document lookups —
src/app/api/webhooks/documenso/route.ts:96-148. Adding port scope requires either including the originating Documenso instance/team id in the lookup (Documenso doesn't surface that on the webhook payload today) OR provingdocuments(documenso_id)is globally unique with a DB constraint and a backfill check. Pick the strategy with the audit doc open. - 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 schema column for recipient_email ondocumentEvents. Right place to do this is alongside Documenso Phase 2 (webhook handler enhancement) since they touch the same code. - v2 voidDocument endpoint shape verification —
src/lib/services/documenso-client.ts:450-466. Needs a live Documenso 2.x instance to confirmPOST /api/v2/envelope/deletebody shape. Bundle with Documenso Phase 5. - Public POST routes bypass service layer —
src/app/api/public/{interests,website-inquiries,residential-inquiries}/route.ts. Multi-route refactor extracting a sharedpublicInterestService.create(...). Worth doing but big enough to deserve its own session. - Inconsistent response shapes — most endpoints return
{ data: ... }, butnotifications/[notificationId]returns{ success: true },website-inquiriesreturns{ id, deduped }. Codebase-wide migration; document a convention in CLAUDE.md first. systemSettingsPK / unique-index drift —src/lib/db/schema/system.ts:119-133. Schema declaresuniqueIndexon(key, port_id), migration useskeyas PK.port_idis nullable so(key, port_id)cannot serve as a PK with default NULLs-not-equal semantics. Reconcile by either makingportIdnon-null with a sentinel ("global") and declaring composite PK, OR by dropping the schema-level unique index and using partial unique indexes for global vs per-port. Either path is a data migration.
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(...))
Still open — small enough to bundle next time
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. Treat as documented limitation; revisit if Drizzle adds deferred-FK support.req.json()withoutparseBodyhelper — admin custom-fields routes useawait req.json(); schema.parse(body)directly. Migrate for uniform 400 error shapes when the surface area calms down.
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 configurable from user_settings |
Open — needs user_settings UI surface |
src/lib/queue/workers/import.ts:13 |
Import job handlers — worker is a stub | Open — entire feature surface |
E. Hidden / stubbed UI tabs
- Company Documents tab —
src/components/companies/company-tabs.tsx:229. Hidden until/api/v1/filesaccepts acompanyIdfilter (schema supports it, validator doesn't). - 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.
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