Files
pn-new-crm/docs/audit-findings-tmp/09-ux-forms.md
Matt 4b5f85cb7d 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

160 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# UX/Forms/Tables Audit (U-001-100, code-side) — agent #9
**Headline:** Generally consistent (Sheet, AlertDialog, EmptyState, requestId surfacing all good across most surfaces). 4 HIGH gaps: native `alert()` for bulk-action failures, icon-only buttons missing aria-label, unicode glyphs in portal, Vaul Drawer in mobile search overlay. Plus 14 MEDIUM gaps in form discipline + a11y + mobile nav.
**Counts:** 0 critical · 4 high · 14 medium · 0 low
---
## 🟠 HIGH
### U-059: Unicode glyphs as status icons in portal documents page
- **File:** `src/app/(portal)/portal/documents/page.tsx:85-89`
- **What:** Signer status rendered as raw Unicode (`'✓'` signed, `'✗'` declined, `'○'` pending) inside colour-coded `<span>` with no `aria-label`.
- **Why it matters:** A11y — screen readers read literal Unicode names. Per project memory: decorative unicode glyphs are explicitly flagged. `inline-stage-picker.tsx:443` comment confirms the pattern ("was ⚑ unicode glyph — replaced with a Lucide").
- **Suggested fix:** Replace with `<CheckCircle2>` / `<XCircle>` / `<Circle>` Lucide icons + `aria-label`.
### U-066: Vaul Drawer used for mobile search overlay (violates Sheet doctrine)
- **File:** `src/components/search/mobile-search-overlay.tsx:6`
- **What:** `import { Drawer as VaulDrawer } from 'vaul'` — search overlay is a full-screen overlay, not a bottom sheet, but uses Vaul Drawer. CLAUDE.md says Vaul is reserved for mobile-bottom-sheet only (currently `MoreSheet` only).
- **Suggested fix:** Convert to `<Sheet side="bottom">` or `<Dialog>` fullscreen. Visualviewport handling (lines 50-89) becomes redundant once Radix dialog primitive backs it.
### U-076: Native `alert()` for bulk-action failure feedback in 3 lists
- **Files:** `src/components/interests/interest-list.tsx:146`, `src/components/companies/company-list.tsx:73`, `src/components/yachts/yacht-list.tsx:66`
- **What:** Partial-failure feedback via `alert(...)`. `client-list.tsx:145` uses `toast.warning(...)` correctly.
- **Why it matters:** Native alert blocks main thread, can't be styled, fires in tests without suppression.
- **Suggested fix:** Replace with `toast.warning(...)` matching `client-list.tsx`.
### U-079: Icon-only buttons missing aria-label (5 sites)
- **Files:**
- `src/components/notifications/notification-bell.tsx:65` (Bell icon button)
- `src/components/files/file-grid.tsx:121` (MoreHorizontal "…" on file cards)
- `src/components/admin/forms/form-template-list.tsx:102` (Trash button)
- `src/components/email/email-accounts-list.tsx:159` (Trash button)
- `src/components/companies/company-members-tab.tsx:228` (MoreHorizontal)
- **Pattern reference (correct):** `src/components/shared/folder-actions-menu.tsx:96` uses `<span className="sr-only">More folder actions</span>`.
- **Suggested fix:** Add `aria-label` to each, following the folder-actions-menu sr-only pattern.
---
## 🟡 MEDIUM
### U-009: Audit log inline div instead of EmptyState component
- **File:** `src/components/admin/audit/audit-log-list.tsx:524`
- **What:** `<div><p className="text-muted-foreground">No audit log entries found.</p></div>` rather than `<EmptyState title="..." />`.
- **Suggested fix:** Replace with `<EmptyState title="No audit log entries found." />`.
### U-010: Two duplicate EmptyState components with incompatible APIs
- **Files:** `src/components/ui/empty-state.tsx` vs `src/components/shared/empty-state.tsx`
- **What:** `ui/` accepts `{icon: ReactNode, body, actions}`; `shared/` accepts `{icon: ElementType, description, action: {label, onClick}}`. 3 files use `ui/` (admin/reconcile-queue, documents/documents-hub, reservations/reservation-detail), 24 use `shared/`.
- **Suggested fix:** Pick `shared/` as canonical (8× usage); migrate the 3 `ui/` callers and delete `ui/empty-state`.
### U-021: Required-field marker inconsistent
- **Files:** `src/components/clients/client-form.tsx:273`, `src/components/interests/interest-form.tsx:281`
- **What:** Some fields use inline `*`, others have no marker; no `aria-required` on inputs; no consistent pattern.
- **Suggested fix:** Single pattern: `<Label>Field <span aria-hidden>*</span></Label>` + `aria-required="true"` on input.
### U-022: Help-text discoverability inconsistent
- **File:** `src/components/shared/filter-bar.tsx`, `src/components/clients/client-form.tsx`
- **What:** No tooltip pattern; some fields have always-visible muted-foreground hints, some have nothing.
- **Suggested fix:** Document a rule (always-visible for constraints/format hints; tooltips only for icons).
### U-024: Cancel/dismiss without unsaved-changes warning on ClientForm/YachtForm
- **Files:** `src/components/clients/client-form.tsx`, `src/components/yachts/yacht-form.tsx`
- **What:** `InterestForm.requestClose()` (line 123) checks `isDirty` and shows discard AlertDialog; `CompanyForm` also has it. ClientForm and YachtForm don't — sheet closes immediately.
- **Suggested fix:** Add `isDirty` guard + discard AlertDialog matching InterestForm pattern.
### U-031: FileUploadZone size limit not surfaced as client-side check
- **File:** `src/components/files/file-upload-zone.tsx:170`
- **What:** Accept attribute lists extensions; "up to 50MB" copy at line 163; no client-side size check before upload. Server-side check fails silently with "Upload failed" at line 103.
- **Suggested fix:** Wire client-side size check before upload; show clear "File too large" message.
### U-044: No jump-to-page input in pagination
- **File:** `src/components/shared/data-table.tsx:420`
- **Suggested fix:** Add small `<input type="number">` between Previous/Next.
### U-048: No column resize/reorder on DataTable
- **File:** `src/components/shared/data-table.tsx`
- **What:** Visibility supported via `ColumnPicker`; widths fixed; no drag-reorder.
- **Suggested fix:** Opt-in `enableColumnResizing` per table via TanStack Table v8 `onColumnSizingChange`.
### U-069: Invoice delete uses custom overlay, not AlertDialog
- **File:** `src/app/(dashboard)/[portSlug]/invoices/page.tsx:167`
- **What:** Hand-rolled `<div className="fixed inset-0 bg-background/80 backdrop-blur-xs z-50 ...">` rather than `<AlertDialog>` / `<ConfirmationDialog>`. Lacks focus trap, Escape, role="alertdialog".
- **Suggested fix:** Replace with `<ConfirmationDialog>` matching pattern elsewhere.
### U-074: Success toast missing on ClientForm + InterestForm create/edit
- **Files:** `src/components/clients/client-form.tsx:215`, `src/components/interests/interest-form.tsx:235`
- **What:** `onSuccess` invalidates queries + closes sheet, no `toast.success()`. `ComposeDialog.onSuccess:81` does fire one.
- **Suggested fix:** `toast.success(isEdit ? 'Client updated' : 'Client created')`.
### U-080: Logo preview `<img alt="">` should describe state
- **File:** `src/components/admin/shared/settings-form-card.tsx:420`
- **Suggested fix:** Use `alt="Port logo preview"` or dynamic from field label.
### U-081: Heading hierarchy inconsistent within tab components
- **Files:** `email-accounts-list.tsx:114`, `interest-contract-tab.tsx:130/251/291/364` (h2 → h3 → h2 jumps)
- **Suggested fix:** Audit each tab; standardize h2 = primary section, h3 = sub-section; never h2 after h3 at same nesting depth.
### U-086: DialogContent missing aria-describedby on minimal-content dialogs
- **File:** `src/components/email/compose-dialog.tsx:95` and ~40 other dialogs
- **What:** Only `file-preview-dialog.tsx:82` explicitly suppresses the Radix warning.
- **Suggested fix:** Add `<DialogDescription className="sr-only">...</DialogDescription>` or `aria-describedby={undefined}` to suppress.
### U-091: Mobile topbar title blank on list pages
- **Files:** `client-list.tsx`, `yacht-list.tsx`, `interest-list.tsx`, `berth-list.tsx`
- **What:** `useMobileChrome` only called from detail pages. List pages leave topbar in fallback (no title, stale from previous detail page).
- **Suggested fix:** Add `useMobileChrome({ title, showBackButton: false })` per list with cleanup pattern.
### U-093: Invoices missing from mobile navigation
- **File:** `src/components/layout/mobile/more-sheet.tsx:54`
- **What:** Not in `MORE_GROUPS`, not in bottom tabs. Mobile users can only reach via direct URL.
- **Suggested fix:** Add `{ label: 'Invoices', icon: FileText, segment: 'invoices' }` to Operations group.
---
## ✅ Sample passing checks
- U-001-008 list empty states + skeletons clean across clients/yachts/interests/berths/companies/reservations/invoices/email-threads
- U-012 FileUploadZone drag-hover with `border-primary bg-primary/5`
- U-023 field-level errors via react-hook-form `formState.errors` consistent
- U-026 BulkAddBerthsWizard + CatchUpWizard persist state across step nav
- U-027 phone E.164 via `formatAsYouType` emits `{ e164, country }`
- U-029 native `<input type="date">` provides browser calendar + keyboard
- U-033 Combobox keyboard nav inherited from Radix `<Command>` primitives
- U-040 Sort indicators via `getSortIcon` (`ArrowUpDown`/`ArrowUp`/`ArrowDown`)
- U-041/042 Filter chip dismiss + Clear-all in FilterBar
- U-043 page size selector 25/50/100/250/All
- U-049 virtual list via `@tanstack/react-virtual` (`virtual virtualHeightPx={640}` in audit log)
- U-054 STAGE_BADGE in `src/lib/constants.ts:100` — 7 distinct stages with distinct Tailwind colour families
- U-055 outcome badge: won=emerald, lost\_\*=rose, cancelled=slate
- U-057 status-pill covers all required document statuses
- U-060/061 button hierarchy + destructive red consistent
- U-065 Sheet used for forms+previews on both desktop and mobile (23 components)
- U-067 AlertDialog used for destructive confirmations (`useConfirmation`, `ArchiveConfirmDialog`, `ConfirmationDialog`, `BulkHardDeleteDialog`)
- U-070-072 click-outside, Esc, focus-trap, focus-restore all inherited from Radix
- U-073 toast position consistent (sonner top-right)
- U-075 `toastError()` (`src/lib/api/toast-error.ts:43`) surfaces requestId + Copy ID action — used in 89 files
- U-094 iOS safe-area-inset comprehensive (`pb-safe-bottom`, `pt-safe-top`, FAB `calc(env(safe-area-inset-bottom)+86px)`)
- U-097 visualViewport handling on mobile-search-overlay
- U-092 More sheet covers Documents/Interests/Yachts/Companies/Residential/Alerts/Reminders/Expenses/Reservations/Reports/Analytics/Settings/Admin