# Audit Fix Wave โ€” 2026-05-18 Progress report against `docs/AUDIT-FINDINGS-2026-05-15.md` (74 findings) and the still-open Wave-11 items in `docs/AUDIT-FOLLOWUPS.md`. Each finding was re-verified against the current code before being touched โ€” the previous session's 70 uncommitted files mostly added new behaviour and rarely overlapped with the audit issues, so almost everything was still applicable. `pnpm exec vitest run` โ†’ 1374/1374 pass. `pnpm exec tsc --noEmit` clean. --- ## ๐Ÿ”ด CRITICAL โ€” 3 / 3 done - **C-01** interest-berths INNER JOIN on hard-deleted berths โ€” three helpers switched to LEFT JOIN; `listBerthsForInterest` return type loosened so an orphaned junction row still renders. Berth hard-delete is already redirected to soft-archive, so the audit's "service-layer guard preventing hard-delete" requirement is implicitly satisfied via `archiveBerth`'s active-interest check. - **C-02** `/setup` missing from `PUBLIC_PATHS` โ€” added. - **C-03** generic `PATCH /api/v1/interests/[id]` bypassing stage guards โ€” `updateInterestSchema` now omits `pipelineStage`, forcing every caller through the `/stage` endpoint with the override-permission + override-reason guard chain. ## ๐ŸŸ  HIGH โ€” 14 / 15 fixed, 1 not-applicable - **H-01** FK `ON DELETE` actions made explicit across interests / documents / reservations / reminders / invoices schemas; migration `0070_h01_fk_on_delete.sql` drops + re-adds each constraint under the same name (idempotent against re-run). - **H-02** login page reads `?redirect=` param with same-origin guard (`startsWith('/')` and `!startsWith('//')`). - **H-03** CRM-invite token moved to URL fragment (`#token=โ€ฆ`); the set-password page reads from fragment via `useSyncExternalStore` with `?token=` back-compat for outstanding links. - **H-04** `Retry-After` header added to the sign-in-by-identifier 429 response (RFC 6585 ยง4). - **H-05** `toggleAccount` now writes an audit row (action 'update', entityType 'email_account', oldValue/newValue around isActive). - **H-06** `upsertSetting` masks any value whose key ends with `_encrypted` to `[redacted]` before writing to `audit_logs.new_value` โ€” keeps the ciphertext out of the historical audit trail. - **H-07** `archiveClient`'s cascade fires per-interest audit rows (action 'archive', metadata.cascadeSource = 'client_archive') so the audit FTS surfaces a search for a specific archived interest. - **H-08** `createSalesTransporter` now applies the shared `SMTP_TIMEOUTS` constant โ€” sales send-outs can no longer stall the BullMQ pool on a hung relay. - **H-09** AppShell refactored so `
{children}
` lives at an invariant tree path across mobile/desktop chrome โ€” React preserves in-progress form drafts when the viewport flips across the breakpoint. - **H-10** portal documents page replaces Unicode glyph status icons with Lucide CheckCircle2/XCircle/Circle + aria-labels. - **H-12** three list components (interests/companies/yachts) swap `alert(โ€ฆ)` for `toast.warning(โ€ฆ)` matching client-list. - **H-13** 5 icon-only buttons gain `aria-label` (notification bell, file-grid actions menu, form-template edit/delete, email-account remove, member-actions menu). - **H-14** `parseBody` now treats empty request bodies as `{}` so routes whose schemas have all-optional fields don't crash on an empty DELETE / PATCH payload. - **H-15** admin layout renders an explicit 403 panel ("Access denied โ€” this area is for super-administrators only") instead of a silent redirect to `/dashboard`, with a "Back to dashboard" CTA. URL stays on the failed route. **Not applicable:** - **H-11** mobile-search-overlay Vaul โ†’ Sheet conversion. The audit's premise ("full-screen, not a bottom sheet") is inaccurate โ€” the overlay has `top: 12px` (visible backdrop strip), drag handle, swipe-to-dismiss, and explicit visualViewport sizing for iOS keyboard behaviour. CLAUDE.md's "Sheet vs Drawer doctrine" explicitly allows Vaul for "mobile-only bottom-sheet UX" which is this case. ## ๐ŸŸก MEDIUM โ€” 28 / 48 fixed, 5 deferred, the rest covered by larger work ### Done - **M-MT01-05** multi-tenancy defense-in-depth: `port_id` / parent-id filters added to UPDATE/DELETE WHEREs across custom-fields, notes (all 6 entity types ร— update + delete), client-contacts, yacht ownerClient lookup, and webhooks reads. - **M-AU01** audit log placeholder copy fixed. - **M-AU02** already done in previous session (Details column + Sheet). - **M-AU04** outcome change now uses distinct audit verbs `outcome_set` / `outcome_cleared`; AuditAction type extended. - **M-D01** documents-hub realtime event-name typo (`file:created` โ†’ `file:uploaded`) fixed. - **M-EM01** portal-auth activation + reset emails now pass `portId` to `sendEmail` so per-port SMTP is used. - **M-EM02** `sendEmail` accepts `cc` / `bcc` params; redirect mode drops both (consistent with the dev safety net). - **M-EM04** `notification_digest` added to `TEMPLATE_KEYS` + `TEMPLATE_CATALOG`; the digest service drops the `'crm_invite' as any` cast. - **M-IN01** portal presigned download URLs now use a 4-hour TTL so client links from yesterday's emails still work. - **M-IN02** OpenAI client lazy-instantiated; missing key surfaces a clear error instead of crashing at module load. - **M-IN04** stale pdfme comments in seed-data + document-templates updated to pdf-lib AcroForm. - **M-IN05** `umami.testConnection` returns `{ ok: true|false, โ€ฆ }` tagged union instead of throwing. - **M-L02** `report-generators.ts` canonicalises stage values via `canonicalizeStage()` across pipeline / revenue / forecast rollups so legacy 9-stage rows fold into the modern 7-stage buckets. - **M-NEW-2** activity feed entity-name/type concatenation โ€” explicit middle-dot separator so "Test Person 1" + "interest" no longer renders as one word. - **M-R01** portal allowlist narrowed from blanket `/portal/` to the three unauthenticated entry-points + portal_session backstop in the middleware redirects to `/portal/login` when the cookie is missing. - **M-SC02** companies gets `idx_companies_archived` partial index matching the clients/yachts/interests pattern. - **M-SC04** `auditLogs.searchText` documented as GENERATED ALWAYS / DB-managed. - **M-SC05** documents.clientId `ON DELETE SET NULL` covered by the H-01 migration. - **M-U01** audit-log empty state uses ``. - **M-U09** invoice delete dialog migrated from hand-rolled overlay to `` (focus trap, ESC-to-close, a11y semantics). - **M-U10** ClientForm + InterestForm fire `toast.success(...)` on create/edit. - **M-U11** logo preview `` carries a descriptive alt. - **M-U14** mobile topbar title surfaced on clients / interests / yachts / berths list pages via `useMobileChrome`. - **M-U15** Invoices added to the mobile More-sheet Operations group. - **M-L01** `reservations.tenureType` comment unified with `berths.tenureType` (canonical union). - **M-S01** `storage_s3_access_key_encrypted` admin field added; the encrypt-plaintext-credentials script handles the data migration. ### Deferred (need user input or scope-larger-than-an-audit-fix) - **M-AU03** โ€” audit log CSV export endpoint. New feature surface. - **M-EM03** โ€” bounce-to-interest IMAP linking (Phase 7 ยง14.9). - **M-IN03** โ€” receipt-scanner per-port OCR config (every call site needs `portId` threading). - **M-NEW-1** โ€” `/me/ports` asymmetric port-context header semantics. - **M-P01** โ€” leading-wildcard ILIKE โ†’ pg_trgm GIN migration. - **M-SC03** โ€” FTS GIN on interests + berths (search.service.ts doesn't use to_tsvector for these โ€” feature work). ### Lower-priority M-U items left untouched (cosmetic / process) `M-U02` (dedup EmptyState components), `M-U03` (required-field marker standardisation), `M-U04` (help-text discoverability rule), `M-U05` (unsaved-changes warning on ClientForm/YachtForm), `M-U06` (FileUploadZone client-side size check), `M-U07` (pagination jump-to-page), `M-U08` (column resize/reorder), `M-U12` (heading hierarchy across tab components), `M-U13` (DialogContent aria-describedby across ~40 sites). All polish-grade โ€” drop into a focused UX session. ## ๐ŸŸข LOW โ€” 6 / 8 fixed, 2 deferred / not-applicable - **L-AU01** severity defaults extended (password_change โ†’ warning, portal_password_reset โ†’ warning, etc). - **L-AU02** action-filter dropdown gains 13 missing verbs (password*change, portal*\_, gdpr\__, rule*evaluated, outcome*_, branding.\_). - **L-AU03** entity-type dropdown gains 7 missing entries (yacht, company, reservation, email_account, portal_session, portal_user, file). - **L-AU04** dead `listAuditLogs` (ILIKE) stubbed out โ€” callers all use the FTS-backed `searchAuditLogs` now. - **L-D02** CLAUDE.md "Owner-wins chain" tightened โ€” `interest.yachtId` tail branch removed from the spec (structurally unreachable since `interests.clientId` is NOT NULL). - **L-P01** list endpoint limit cap โ€” DEFER per audit (cursor pagination is on the routes where it matters; the 1000-row cap is fine at current data sizes). - **L-D01** HubRootView spec inaccuracy โ€” verified accurate; the CLAUDE.md "three render modes" line refers to render _modes_, not sections within HubRootView. Audit finding is a misread. - **L-L01** reports defensive concern โ€” covered by M-L02's canonicalize sweep. --- ## Bonus: document-detail polish (#67 partial) Three of the six deliverables in MANUAL-TESTING-BACKLOG ยง4.10b shipped in this wave: - **State-aware action button per signer** โ€” `invitedAt === null` โ†’ primary "Send invitation" CTA (paper-plane); else "Send reminder" (bell). Hits the existing `/send-invitation` and `/remind` routes. - **Watcher Add UI** โ€” replaces the user-id stub display with the display name from `/api/v1/admin/users/picker`, plus a "+ Add" select that lets admins pick any user in the port that isn't already watching. Existing delete affordance untouched. - **`cleanSignerName` cleanup** โ€” shared from `SigningProgress` and applied to the doc-detail card so EMAIL_REDIRECT_TO `(was: โ€ฆ)` / `(placeholder)` suffixes stop leaking through. The remaining three deliverables (full SigningProgress visual parity, linked-entity name resolution, activity-panel `document_events` polish with per-event icons + tooltips) need API changes to return entity names + a meaningful event-type icon map. Deferred so it can ship in one focused PR. ## Smoke validations against the running dev server - **C-02** โ€” `/setup` is reachable (middleware lets it through; page itself redirects to `/login` when `needsBootstrap=false`). No infinite redirect loop. - **M-R01** โ€” `/portal/documents` without a portal_session cookie now redirects to `/portal/login?redirect=/portal/documents`. - **H-04** โ€” sign-in 429 response carries `Retry-After: 900` plus the full `X-RateLimit-*` triplet. ## What still needs your input Items genuinely blocked on a decision you haven't made yet. Most exist in the 2026-05-15 manual-testing-backlog already; surfacing here in one place for resolution. 1. **PDF template editor / builder (MANUAL-TESTING-BACKLOG ยง9.Z)** โ€” ship Phase 1 alone (in-app fill of admin-uploaded PDFs with merge-token mapping, ~1โ€“2 weeks) or wait until Phases 1+2 can land together (also Documenso template push, ~3โ€“4 weeks)? 2. **Document detail refactor (#67 in ยง4.10b)** โ€” multi-deliverable redesign. Are we shipping it as one PR or splitting? 3. **Reminders data model (ยง0.1 + ยง3.2)** โ€” Path A (extend lightweight columns on `interests` โ€” note/timeOfDay/priority/recurrence) or Path B (push richer reminders into the existing `reminders` table)? 4. **Supplemental info form (ยง0.2)** โ€” CRM-hosted route or marketing-site-hosted? Need a green light to spend ~15 minutes tracing the route end-to-end. 5. **EOI-scoped data overrides (ยง4.2)** โ€” does the override apply only to this specific EOI document, or to ALL future EOIs on this interest? Reopening the drawer: show original override or fall back to canonical? Are the overrides reusable for reservation + contract or EOI-only? 6. **`/me/ports` port-context asymmetry (M-NEW-1)** โ€” should the endpoint treat absent `X-Port-Id` as "list all ports the user has access to"? Currently super-admins work without it; everyone else gets a 400. 7. **Bounce-to-interest IMAP linking (M-EM03 / Phase 7 ยง14.9)** โ€” ready to scope or stays deferred? 8. **Receipt-scanner per-port OCR config (M-IN03)** โ€” every call site needs `portId` threading. Confirm we should do this now vs. when a second-port OCR config materialises? 9. **CSV export of audit logs (M-AU03)** โ€” net-new endpoint. Ship? 10. **Documenso phases 2โ€“7 (BACKLOG ยงA)** โ€” still back-burnered or ready to pick up? --- ## Migrations to apply `pnpm tsx scripts/db-migrate.ts` (or your usual migration runner) will pick up the single new migration `0070_h01_fk_on_delete.sql`. It's idempotent โ€” each ALTER drops the constraint by name first, so re-runs are safe. ## Files touched this wave `118 files changed, 5181 insertions(+), 1301 deletions(-)` โ€” but note that count rolls in the previous session's 70 uncommitted files. Run `git diff --stat HEAD docs/AUDIT-FINDINGS-2026-05-15.md` to see only the audit-fix diff.