fix(audit): A1/A2/A4/A6/A8/A9/A16/A17/A19/A20 from 2026-05-15 sweep

Knocks out 10 of the 13 known issues from yesterday's Playwright audit.

A4 — Client form silently rejected submit when a contact row had an
empty value. The F19 filter ran in mutationFn after zod's
handleSubmit had already short-circuited on min(1). Now wraps the
onSubmit to prune empty rows BEFORE handleSubmit/zod sees them.

A16 — File upload to documents hub root 400'd because FormData.get
returns null for absent fields and zod's .optional() rejects null.
Route handler now coerces null/empty → undefined before parse.

A17 — Added /api/v1/me/ports endpoint that any authenticated user
can hit; client.ts now uses it as the bootstrap port-slug→port-id
resolver. Eliminates the wasteful 400s sales-reps and viewers were
firing on every page load against the super-admin-gated /admin/ports.

A1 — Filter permission_denied actions from the dashboard activity
feed. Still in the audit log; just not noise on the dashboard.

A2 — New LEGACY_STAGE_REMAP table + canonicalizeStage / stageLabelFor
helpers in lib/constants. Activity-feed maps legacy 9-stage enum
values (deposit_10pct, contract_sent, etc.) to their 7-stage labels
on the way out, so historical audit rows read as "Deposit Paid" not
"Deposit 10Pct".

A19 — Same-stage write now returns 204 No Content. Service returns
a STAGE_NOOP sentinel; the route handler translates it.

A9 — Catch-up wizard now derives stage from berth status (under_offer
→ EOI, sold → contract) with a stageOverride state for explicit
user picks. Avoids the set-state-in-effect rule violation.

A20 — OwnerPicker shows a "Client / Company" hint chip on the
trigger when no value is set, so users know the trigger opens a
two-tab picker instead of just a client list.

A8 — Migration 0066 normalizes legacy `statusOverrideMode = 'auto'`
to NULL so the column lives at strictly 3 states.

A6 — file-preview-dialog gets a screen-reader DialogDescription so
the Radix "Missing aria-describedby" warning stops firing on every
preview.

A18 closed as not-a-bug: /api/v1/users genuinely doesn't exist
(Next returns 404); /api/v1/admin/audit exists and 403s.

A5 (Socket.IO dev noise) + A3 (react-grab CSP) left for a separate
pass — both are dev-only cosmetic.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-15 01:12:20 +02:00
parent 3b3ac287e0
commit 0d9208a052
13 changed files with 212 additions and 35 deletions

View File

@@ -113,13 +113,21 @@ export function OwnerPicker({
disabled={disabled}
className={cn('w-full justify-between', !value && 'text-muted-foreground')}
>
<span className="truncate">
{value && (
<span className="mr-2 text-xs opacity-60">
<span className="truncate flex items-center gap-2">
{/* A20: surface the dual-mode (Client/Company) hint even when
* no value is picked yet, so users know the trigger opens a
* two-tab picker — pre-fix the toggle was hidden until the
* popover was open, making the form read as client-only. */}
{value ? (
<span className="text-xs opacity-60">
{value.type === 'client' ? 'Client:' : 'Company:'}
</span>
) : (
<span className="rounded-sm border border-border bg-muted px-1.5 py-px text-[10px] uppercase tracking-wide text-muted-foreground">
Client / Company
</span>
)}
{selectedLabel}
<span className="truncate">{selectedLabel}</span>
</span>
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" aria-hidden />
</Button>