fix(audit): comprehensive 2026-05-15 audit fix wave + Documenso v2 polish
Bundles the prior session's 50-task fix sweep (Documenso v2 + EOI/signing-
progress redesign + env-to-admin migration + dev-mode banner) with the
2026-05-18 audit fix wave (3 CRITICAL, 14 HIGH, 28 MEDIUM, 6 LOW).
CRITICAL (3):
- C-01 interest-berths INNER JOIN -> LEFT JOIN so hard-deleted berths
no longer silently drop interest links
- C-02 /setup added to PUBLIC_PATHS; fresh-deploy bootstrap loop fixed
- C-03 generic PATCH /interests/[id] no longer accepts pipelineStage —
callers must go through /stage with the override-guard chain
HIGH (14/15):
- H-01 explicit ON DELETE on previously-implicit NO ACTION FKs across
interests/documents/reservations/reminders/invoices (migration 0070)
- H-02 login page reads ?redirect= param with same-origin guard
- H-03 CRM invite token moves to URL fragment so it never lands in
nginx access logs / Referer headers
- H-04 Retry-After header on sign-in-by-identifier 429 (RFC 6585 §4)
- H-05 toggleAccount writes an audit row
- H-06 upsertSetting masks any value whose key ends with _encrypted
- H-07 archiveClient cascade fires per-interest audit rows
- H-08 createSalesTransporter applies SMTP_TIMEOUTS
- H-09 AppShell stable children — viewport flip across breakpoint no
longer destroys in-progress form drafts
- H-10 portal documents page swaps Unicode glyph status icons for
Lucide CheckCircle2/XCircle/Circle + aria-labels
- H-12 list components swap alert(...) for toast.warning(...)
- H-13 5 icon-only buttons gain aria-label
- H-14 parseBody treats empty bodies as {}
- H-15 admin layout renders a 403 panel instead of silent bounce
- H-11 not applicable — mobile-search-overlay IS a mobile bottom-sheet
MEDIUM (28+):
- M-MT01-05 defense-in-depth port_id/parent-id filters on UPDATE/DELETE
WHEREs across custom-fields, notes (all 6 entity types x update +
delete), client-contacts, yacht ownerClient lookup, webhook reads
- M-D01 documents-hub realtime event-name typo (file:created -> uploaded)
- M-EM01 portal-auth emails thread through portId
- M-EM02 sendEmail accepts cc/bcc params
- M-EM04 notification_digest catalog key
- M-IN01 portal presigned download URLs use 4h TTL
- M-IN02 OpenAI client lazy-instantiated
- M-IN04 stale pdfme refs updated to pdf-lib AcroForm
- M-IN05 umami.testConnection returns tagged union
- M-L01 reservations tenure_type unified with berths
- M-L02 report-generators canonicalize stage values
- M-AU01 audit log placeholder copy fixed
- M-AU04 outcome_set / outcome_cleared distinct audit verbs
- M-NEW-2 activity feed entity name+type separator
- M-R01 portal allowlist narrowed + portal_session backstop in proxy
- M-SC02 companies archived partial index
- M-SC04 audit_logs.searchText documented as DB-managed
- M-S01 storage_s3_access_key_encrypted admin field
- M-U01 audit log empty state uses <EmptyState>
- M-U09 invoice delete dialog -> <AlertDialog>
- M-U10 toast.success on ClientForm + InterestForm create/edit
- M-U11 settings-form-card logo preview alt text
- M-U14 mobile topbar title on clients/yachts/interests/berths
- M-U15 Invoices in mobile More-sheet
LOW (6/8):
- L-AU01 severity defaults for security-relevant verbs
- L-AU02 +13 missing actions in admin audit filter
- L-AU03 +7 missing entity types in admin audit filter
- L-AU04 dead listAuditLogs stubbed
- L-D02 CLAUDE.md Owner-wins chain tightened
Bonus — Document detail polish (#67 partial, 3/6 deliverables):
- state-aware action button per signer
- watcher Add UI with display-name resolution
- cleanSignerName cleanup
Prior session work bundled in:
- Documenso v2 webhook + envelope-ID normalization + sequential signing
- SigningProgress UI redesign (avatars, per-signer state, timestamps)
- env->admin settings registry + RegistryDrivenForm + encrypted creds
- Embedded-signing card + Test connection + setup help
- Dev-mode EMAIL_REDIRECT_TO banner
- Pipeline rules admin page
- Sales email config card
- Audit log details Sheet
- EOI tab: Finalising badge, absolute timestamps, sequential indicator
- Notes pipeline_stage_at_creation (migration 0069)
- Documenso numeric ID dual-key webhook (migration 0068)
- Dimensions criterion copy (migration 0067)
Tests: 1374/1374 vitest pass. tsc clean. lint clean.
See docs/AUDIT-FIX-WAVE-2026-05-18.md for the full progress report and
the user-input items still pending.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 13:28:50 +02:00
'use client' ;
import { useMemo , useState } from 'react' ;
import { useMutation , useQueryClient } from '@tanstack/react-query' ;
import { AlertTriangle , Loader2 , XCircle } from 'lucide-react' ;
import { toast } from 'sonner' ;
import {
Dialog ,
DialogContent ,
DialogDescription ,
DialogFooter ,
DialogHeader ,
DialogTitle ,
} from '@/components/ui/dialog' ;
import { Button } from '@/components/ui/button' ;
import { Checkbox } from '@/components/ui/checkbox' ;
import { Label } from '@/components/ui/label' ;
import { Textarea } from '@/components/ui/textarea' ;
import { apiFetch } from '@/lib/api/client' ;
import { toastError } from '@/lib/api/toast-error' ;
interface Signer {
id : string ;
signerName : string ;
signerEmail : string ;
signerRole : string ;
status : string ;
}
interface EoiCancelDialogProps {
documentId : string ;
signers : Signer [ ] ;
open : boolean ;
onOpenChange : ( open : boolean ) = > void ;
}
/ * *
* Cancel - with - notify modal . Two variants by signedCount :
* - 0 signed : simple confirm with optional reason . Cancel button .
* - 1 + signed : list each signer with a checkbox so the rep picks
* who to email . Pre - checks the signers who have signed ( they ' re
* the most - affected ) — rep can opt out .
*
* In both cases the reason textarea is optional and ( when present )
* gets inlined into the cancellation email body + the audit log .
*
* On confirm : POST / api / v1 / documents / [ id ] / cancel with
* { reason , notifyRecipients : [ signerId , . . . ] }
* The server voids the envelope , marks status = cancelled , sends the
* branded cancellation email to each picked recipient .
* /
export function EoiCancelDialog ( { documentId , signers , open , onOpenChange } : EoiCancelDialogProps ) {
const queryClient = useQueryClient ( ) ;
const [ reason , setReason ] = useState ( '' ) ;
const [ notifyIds , setNotifyIds ] = useState < Set < string > > ( ( ) = > {
// Default: pre-check the signers who have signed — they're the
// recipients most likely to want to know. Pending signers can be
// notified too but the rep needs to opt them in.
return new Set ( signers . filter ( ( s ) = > s . status === 'signed' ) . map ( ( s ) = > s . id ) ) ;
} ) ;
const signedCount = useMemo ( ( ) = > signers . filter ( ( s ) = > s . status === 'signed' ) . length , [ signers ] ) ;
const cancelMutation = useMutation ( {
mutationFn : ( ) = >
apiFetch ( ` /api/v1/documents/ ${ documentId } /cancel ` , {
method : 'POST' ,
body : {
reason : reason.trim ( ) || null ,
notifyRecipients : Array.from ( notifyIds ) ,
} ,
} ) ,
onSuccess : ( ) = > {
queryClient . invalidateQueries ( { predicate : ( q ) = > q . queryKey [ 0 ] === 'documents' } ) ;
toast . success (
notifyIds . size > 0
? ` EOI cancelled. ${ notifyIds . size } signer ${ notifyIds . size === 1 ? '' : 's' } notified. `
: 'EOI cancelled.' ,
) ;
onOpenChange ( false ) ;
// Reset internal state so a second open of the dialog starts clean.
setReason ( '' ) ;
setNotifyIds ( new Set ( ) ) ;
} ,
onError : ( err ) = > toastError ( err ) ,
} ) ;
const toggle = ( id : string ) = > {
setNotifyIds ( ( prev ) = > {
const next = new Set ( prev ) ;
if ( next . has ( id ) ) next . delete ( id ) ;
else next . add ( id ) ;
return next ;
} ) ;
} ;
return (
< Dialog open = { open } onOpenChange = { onOpenChange } >
< DialogContent className = "sm:max-w-md" >
< DialogHeader >
< DialogTitle className = "flex items-center gap-2" >
< AlertTriangle className = "size-4 text-amber-600" aria - hidden / > Cancel this EOI ?
< / DialogTitle >
< DialogDescription >
{ signedCount === 0
? 'No signatures have been collected yet. The signing service will be told to void this envelope.'
: ` ${ signedCount } signer ${ signedCount === 1 ? ' has' : 's have' } already signed. The envelope will be voided and pick the signers you want to notify by email below. ` }
< / DialogDescription >
< / DialogHeader >
{ signedCount > 0 && (
< div className = "space-y-2 rounded-md border bg-muted/30 p-3" >
< p className = "text-xs font-semibold uppercase tracking-wide text-muted-foreground" >
Notify
< / p >
< ul className = "space-y-1.5" >
{ signers . map ( ( s ) = > (
< li key = { s . id } className = "flex items-center gap-2 text-sm" >
< Checkbox
id = { ` notify- ${ s . id } ` }
checked = { notifyIds . has ( s . id ) }
onCheckedChange = { ( ) = > toggle ( s . id ) }
/ >
< Label htmlFor = { ` notify- ${ s . id } ` } className = "flex-1 cursor-pointer font-normal" >
< span className = "font-medium" > { s . signerName || s . signerEmail } < / span > { ' ' }
< span className = "text-xs text-muted-foreground" >
· { s . signerRole }
{ s . status === 'signed' ? ' · already signed' : ' · pending' }
< / span >
< / Label >
< / li >
) ) }
< / ul >
< p className = "text-xs italic text-muted-foreground" >
2026-05-21 20:02:58 +02:00
Leave all unchecked to cancel silently - no emails will be sent .
fix(audit): comprehensive 2026-05-15 audit fix wave + Documenso v2 polish
Bundles the prior session's 50-task fix sweep (Documenso v2 + EOI/signing-
progress redesign + env-to-admin migration + dev-mode banner) with the
2026-05-18 audit fix wave (3 CRITICAL, 14 HIGH, 28 MEDIUM, 6 LOW).
CRITICAL (3):
- C-01 interest-berths INNER JOIN -> LEFT JOIN so hard-deleted berths
no longer silently drop interest links
- C-02 /setup added to PUBLIC_PATHS; fresh-deploy bootstrap loop fixed
- C-03 generic PATCH /interests/[id] no longer accepts pipelineStage —
callers must go through /stage with the override-guard chain
HIGH (14/15):
- H-01 explicit ON DELETE on previously-implicit NO ACTION FKs across
interests/documents/reservations/reminders/invoices (migration 0070)
- H-02 login page reads ?redirect= param with same-origin guard
- H-03 CRM invite token moves to URL fragment so it never lands in
nginx access logs / Referer headers
- H-04 Retry-After header on sign-in-by-identifier 429 (RFC 6585 §4)
- H-05 toggleAccount writes an audit row
- H-06 upsertSetting masks any value whose key ends with _encrypted
- H-07 archiveClient cascade fires per-interest audit rows
- H-08 createSalesTransporter applies SMTP_TIMEOUTS
- H-09 AppShell stable children — viewport flip across breakpoint no
longer destroys in-progress form drafts
- H-10 portal documents page swaps Unicode glyph status icons for
Lucide CheckCircle2/XCircle/Circle + aria-labels
- H-12 list components swap alert(...) for toast.warning(...)
- H-13 5 icon-only buttons gain aria-label
- H-14 parseBody treats empty bodies as {}
- H-15 admin layout renders a 403 panel instead of silent bounce
- H-11 not applicable — mobile-search-overlay IS a mobile bottom-sheet
MEDIUM (28+):
- M-MT01-05 defense-in-depth port_id/parent-id filters on UPDATE/DELETE
WHEREs across custom-fields, notes (all 6 entity types x update +
delete), client-contacts, yacht ownerClient lookup, webhook reads
- M-D01 documents-hub realtime event-name typo (file:created -> uploaded)
- M-EM01 portal-auth emails thread through portId
- M-EM02 sendEmail accepts cc/bcc params
- M-EM04 notification_digest catalog key
- M-IN01 portal presigned download URLs use 4h TTL
- M-IN02 OpenAI client lazy-instantiated
- M-IN04 stale pdfme refs updated to pdf-lib AcroForm
- M-IN05 umami.testConnection returns tagged union
- M-L01 reservations tenure_type unified with berths
- M-L02 report-generators canonicalize stage values
- M-AU01 audit log placeholder copy fixed
- M-AU04 outcome_set / outcome_cleared distinct audit verbs
- M-NEW-2 activity feed entity name+type separator
- M-R01 portal allowlist narrowed + portal_session backstop in proxy
- M-SC02 companies archived partial index
- M-SC04 audit_logs.searchText documented as DB-managed
- M-S01 storage_s3_access_key_encrypted admin field
- M-U01 audit log empty state uses <EmptyState>
- M-U09 invoice delete dialog -> <AlertDialog>
- M-U10 toast.success on ClientForm + InterestForm create/edit
- M-U11 settings-form-card logo preview alt text
- M-U14 mobile topbar title on clients/yachts/interests/berths
- M-U15 Invoices in mobile More-sheet
LOW (6/8):
- L-AU01 severity defaults for security-relevant verbs
- L-AU02 +13 missing actions in admin audit filter
- L-AU03 +7 missing entity types in admin audit filter
- L-AU04 dead listAuditLogs stubbed
- L-D02 CLAUDE.md Owner-wins chain tightened
Bonus — Document detail polish (#67 partial, 3/6 deliverables):
- state-aware action button per signer
- watcher Add UI with display-name resolution
- cleanSignerName cleanup
Prior session work bundled in:
- Documenso v2 webhook + envelope-ID normalization + sequential signing
- SigningProgress UI redesign (avatars, per-signer state, timestamps)
- env->admin settings registry + RegistryDrivenForm + encrypted creds
- Embedded-signing card + Test connection + setup help
- Dev-mode EMAIL_REDIRECT_TO banner
- Pipeline rules admin page
- Sales email config card
- Audit log details Sheet
- EOI tab: Finalising badge, absolute timestamps, sequential indicator
- Notes pipeline_stage_at_creation (migration 0069)
- Documenso numeric ID dual-key webhook (migration 0068)
- Dimensions criterion copy (migration 0067)
Tests: 1374/1374 vitest pass. tsc clean. lint clean.
See docs/AUDIT-FIX-WAVE-2026-05-18.md for the full progress report and
the user-input items still pending.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 13:28:50 +02:00
< / p >
< / div >
) }
< div className = "space-y-1.5" >
< Label htmlFor = "cancel-reason" className = "text-xs font-semibold uppercase tracking-wide" >
Reason ( optional )
< / Label >
< Textarea
id = "cancel-reason"
value = { reason }
onChange = { ( e ) = > setReason ( e . target . value ) }
placeholder = "e.g. Yacht owner changed terms; will resend a fresh EOI."
className = "min-h-[80px] resize-y"
maxLength = { 2000 }
/ >
< p className = "text-xs text-muted-foreground" >
Appears in the cancellation email ( if you notify anyone ) and the audit log .
< / p >
< / div >
< DialogFooter className = "gap-2" >
< Button variant = "outline" onClick = { ( ) = > onOpenChange ( false ) } >
Keep EOI
< / Button >
< Button
variant = "destructive"
onClick = { ( ) = > cancelMutation . mutate ( ) }
disabled = { cancelMutation . isPending }
className = "gap-1.5 [&_svg]:size-3.5"
>
{ cancelMutation . isPending ? (
< Loader2 className = "animate-spin" aria - hidden / >
) : (
< XCircle / >
) }
Cancel EOI
< / Button >
< / DialogFooter >
< / DialogContent >
< / Dialog >
) ;
}