# Alpha UAT Master — Multi-Day Findings > **Status:** living doc — _started 2026-05-18, evolving across many sessions_. Single source of truth for everything the manual Playwright + React-Grab UAT pass surfaces, regardless of which day it landed on. > > **Companion to:** [2026-05-18 Full Codebase Audit](./2026-05-18-full-codebase-audit.md) > > **Methodology:** Live Playwright + React Grab walkthrough of the running CRM (default viewport). Findings dropped into chat are appended here in the matching bucket with file:line evidence where available. Cross-references annotated as `see Audit X#N` (and back-referenced in the audit doc as `→ confirmed in manual #N`). > > **Severity legend (for bugs):** > > - `critical` — data loss, security breach, multi-tenant leak, or hard block on a core flow > - `high` — broken golden path, visible-to-customer regression, or silent prod no-op > - `medium` — UX regression, partial functionality, recoverable error > - `low` — cosmetic, copy, polish > **2026-05-25 status check** — full audit triage + execution pass landed (~18 commits). Shipped in this session: > > **Bucket 1 (Wave A–E):** Interest form yacht auto-select + EOI dialog "View EOI" toast + berth-picker compact label + Documents-tab Generate-EOI removal + role-scoped interest auto-assign + LinkedBerthRowItem dimension cleanup + ExternalEoiUploadDialog prefill (title + signatories from active EOI) + Overview milestone EOI signature progress widget. a11y aria-live on supplemental info + 13-component date/currency locale sweep (en-US/en-GB → runtime). Primary-berth-always-in-bundle service guard strengthened + backfill migration 0083. Onboarding super_admin discoverability (topbar banner + dashboard tile + celebration toast) wired via `/api/v1/admin/onboarding/status` + shared service. Branded post-completion email idempotency regression test. > > **Bucket 2 (Wave A–E):** Documents Hub folder rail min-width fix at tablet 768. Website analytics KPI 6→3 cols at lg + 6 at xl. Pipeline Value tile compact `$3.5M` format at sm-. Form-error UX rollout to 5 highest-impact forms (client/interest/yacht/company/berth). TopList empty-state nudge primitive. Sheet width default `sm:max-w-sm → sm:max-w-md + lg:max-w-xl`. > > **Bucket 2 (Wave F):** Radio field type for admin registry + adopted on `eoi_send_mode` and `documenso_signing_order`. Include-yacht toggle on EOI generate dialog (blanks Section 3 even when a yacht is linked; choice recorded in audit log). External-EOI auto-cancel: replace-or-keep radio shows when a generated EOI is active; replace path voids the upstream Documenso envelope + flips the prior doc to cancelled before the new doc lands. > > **Bucket 3 design docs:** `docs/reports-page-design.md` (~400 lines, 7-PR plan) + `docs/tenancies-design.md` (~350 lines, 7-PR plan). > > **Bucket 3 P1 foundations:** > > - Tenancies module-enabled gate (`tenancies_module_enabled` setting, `tenancies-module.service.ts`, 3 admin endpoints under `/api/v1/admin/tenancies-module/`). > - Reports schema migration 0084 (extends `report_templates` + adds `report_runs` + `report_schedules` tables) + matching Drizzle schema. > > **Bucket 4:** Sheet width sweep (Sheet primitive update covers every site). External-EOI dialog cache collision: dialog was caching `{data:…}` on the same key the parent unwraps, blanking the page on open — fix unwraps to match. External-EOI advance-gate regression test (7 cases). Search popover defensive opaque background. EntityFolderView visual overhaul: shared FileIcon mapping for type-specific colours + inline "Signed" pill from `signedFromDocumentId`. > > **Deferred / queued for follow-up sessions:** > > - Reports P2-P7: API CRUD + BullMQ render+email queues + landing + builder + sub-pages + CSV/PNG outputs + metadata overrides (~37 h). > - Tenancies P2-P7: rename migration + perms seed + webhook auto-create + public-map flip rules + sidebar entry + top-level page + entity tab CTAs + 4 reporting widgets (~36 h). > - B2 Wave G remainder: Documenso redirect URL resolver UI surfacing + dock-letters entity + UploadForSigningDialog field metadata (full bundle) + email-test endpoints + Cancel doc delete-vs-keep is already shipped (cancelMode plumbed through; verify dialog adoption is broad enough). > - B2 form-error sweep: remaining ~23 form surfaces. > - B3 Wave: interest dimensions dual-source, universal file preview Tier 1+2, bulk-price editing UI, web analytics integration, universal upload-with-fields remaining UI sites, 5 remaining PDF resolvers, recharts→ECharts migration. > - B4 bugs: Global-search dropdown translucent still wants live-browser repro to confirm the defensive fix actually addressed the cause. --- ## Bucket 1 — Quick fixes (<15 min) _Copy tweaks, alignment, single-prop edits, obvious typos._ > **[Captured 2026-05-22 — visual breakpoint audit, 5 viewports × 20 surfaces via Playwright MCP. Screenshots local at `tmp/visual-audit-2026-05-22//.png`. Tablet tier (768-1023) infrastructure + dashboard PageHeader stacking shipped in `6d665d0`; findings below are the residue surfaced after that ship lit up the tier.]** > > - **SHIPPED in 2f1e1b5:** Tablet topbar logo trigger doesn't render visibly — center grid column changed to `minmax(280px, 800px)` at base with `lg:` override back to 420px min; search-container `sm:-translate-x-...` gated to `lg:` so it only kicks in when sidebar is inline. Verified at 768 — hamburger affordance visible top-left. > - **SHIPPED in 2f1e1b5:** Dashboard title strip crushed at 1024 viewport — PageHeader horizontal-stack breakpoint moved from `lg:` (1024) to `xl:` (1280) so the strip stays stacked through tablet AND narrowest desktop width. Verified at 1024 — title reads cleanly with action row stacked below. > - **SHIPPED in 2f1e1b5 + follow-up:** `useIsMobile()` call-site audit — only 4 callers: pipeline-chart + pipeline-funnel-chart correctly want `tier !== desktop` (short-label x-axis applies on tablet too, kept as-is); date-picker + date-time-picker were strict mobile-only and now use `useViewportTier() === 'mobile'` so tablet gets the desktop Popover Calendar instead of the native input. > **Outstanding quick-fixes (rapid UAT capture — not yet shipped):** > > - **Rename "Mark in EOI bundle" + add tooltip** — _src/components/interests/linked-berths-list.tsx (or wherever the toggle lives)_ — the toggle controls `interest_berths.is_in_eoi_bundle` (per CLAUDE.md), which decides _which_ of the deal's berths the signed EOI document actually commits to. Today the rep sees a label they can't decode. Rename to something like "Include in EOI" + add an info-tooltip popover explaining "Berths flagged here are covered by the EOI signature. A deal can flag a subset (e.g. 2 of 3 linked berths)." ~10 min. **SHIPPED in db51106:** label renamed to "Include in EOI"; existing tooltip already explained the bundle-vs-signature distinction. > - **Lower supplemental-info-request link TTL to ~2 weeks** — _src/lib/services/_ (token model) — link currently expires ~1 month out (`Wed, 17 Jun 2026` shown for an email sent May 18 = ~30 days). User wants ~14 days. Single constant change. ~5 min. **SHIPPED in db51106:** `TOKEN_TTL_DAYS` 30 → 14 in supplemental-forms.service. > - **Admin Documenso settings: surface env-fallback state** — _src/app/(dashboard)/[portSlug]/admin/_ (Documenso settings page) — `getPortDocumensoConfig` already does the right thing (`adminValue ?? env.DOCUMENSO_API_KEY ?? ''`), but the admin UI doesn't show which fields are filled by the admin entry vs. silently falling back to env. Caused an in-session diagnosis loop where the operator had entered creds on Port Amador but was generating EOIs on Port Nimara — Port Nimara's admin row was empty, so it fell back to a stale env key and threw 401. Recommend a small "Using fallback from env" / "Per-port override active" pill next to each Documenso settings field so the operator can see at a glance which scope is in effect. ~30 min. **SHIPPED in e33313b:** collapsed `V2_FEATURE_FIELDS` + `CONTRACT_RESERVATION_FIELDS` (legacy `SettingsFormCard`) into `RegistryDrivenForm` sections (`documenso.behavior` + the existing `documenso.templates`). Every Documenso setting now flows through the registry path that surfaces the env-fallback / port / global source badge per field via `/api/v1/admin/settings/resolved`. EOI generation card retitled to "Templates & signing pathway" since `documenso.templates` covers EOI + reservation + contract template IDs. > - **InterestDocumentsTab label clarity** — _src/components/interests/interest-documents-tab.tsx_ — the tab has two sections: "Legal documents" (Documenso envelopes — EOI / Reservation / Contract, signature-driven) and "Attachments" (general file uploads). "Legal documents" is misleading — the section is scoped to _signature envelopes_, not any legal doc. A rep uploading externally-signed PDFs (lawyer-prepared addenda, etc.) currently goes into Attachments — fine, but the label gap suggests reps expect "Legal documents" to accept external uploads too. Two paths: (a) rename "Legal documents" → "Signature documents" (or "Contracts & EOI") to scope it correctly, OR (b) allow external uploads into that section (more disruptive — needs file-classification metadata). ~15 min for rename + tooltip; ~2 h for upload route. **SHIPPED (a) in 552b966:** section heading renamed to "Signature documents". > - **Berth recommender: drop the "Tier X" prefix, keep plain-English label + add tooltip** — _src/components/interests/berth-recommender-panel.tsx:181_ (the pill render) and _:94-99_ (`TIER_LABELS` map) — the pill currently renders `Tier A · Open` / `Tier B · Fall-through` / `Tier C · Active interest` / `Tier D · Late stage`. The four tier letters are internal taxonomy from `berth-recommender.service.ts` (A = never had interest, B = past fall-through, C = active interest, D = active in late stage); reps don't speak in tier letters and the suffix label already carries the meaning. Fix: (1) drop the `Tier {rec.tier} · ` prefix in the rendered pill — show just `tier.label` (e.g. "Open" / "Fall-through" / "Active interest" / "Late stage") so the chip is self-explanatory. (2) Wrap the pill in a `Popover` (click) or `Tooltip` (hover) that explains the four-state ladder in plain English: "Recommender state — **Open**: never had interest. **Fall-through**: prior interest didn't close (warm). **Active interest**: another deal is in play. **Late stage**: another deal is near-sold." (3) Optional: a small `?` icon next to the chip so the tooltip is discoverable without hovering. The internal `Tier` type stays as-is in the service (it has semantic value in the SQL ladder + admin settings); only the UI label changes. ~15 min. Captured 2026-05-18 from UAT. **SHIPPED in 203f543:** pill is now a Popover trigger with the plain-English label + HelpCircle icon; popover content explains the 4-state ladder. > - **ChartCard: center the chart vertically when grid row is taller than the chart** — _src/components/dashboard/chart-card.tsx_ — every chart widget (`pipeline-funnel`, `occupancy-timeline`, `lead-source`, `berth-status`, `source-conversion`, …) wraps a fixed-height `ResponsiveContainer` (240-280px) inside `ChartCard`. The Card is `h-full` (stretches to its grid-row height) but the inner content keeps its 240-280px and pins to the top — when a neighbour card in the same row is taller (e.g. Pipeline Value with its full per-stage breakdown), the chart card has visible empty space below the chart. Fix: convert `ChartCard` to a flex-column (``); `CardHeader` keeps natural height; `CardContent` gets `flex-1 flex items-center` so the chart's wrapping div sits vertically centered in the remaining space. ResponsiveContainer stays at its declared fixed height. Affects all chart widgets via one wrapper change — no per-chart edits. ~10 min. Captured 2026-05-18 from UAT. **SHIPPED in 203f543.** > - **UploadForSigningDialog feels cramped — fix inner content distribution + right-size the dialog** — _src/components/documents/upload-for-signing-dialog.tsx:166_ (currently `max-w-5xl` = 1024px) + the recipient-row + form fields inside DialogBody. Visual symptom: dialog renders at full 5xl width but inner content clusters on the left ~60% with truncated email field (`email@examp...` clipped), narrow Document title input, tiny 4-row Optional message textarea, and massive whitespace to the right. Combination makes the dialog feel narrow AND empty. > - **Fix:** > - **(a) Right-size the dialog:** drop to `max-w-3xl` (768px) — content fills naturally instead of swimming in 5xl. > - **(b) Recipient row flex distribution:** `Name` input → `flex-1`, `email` input → `flex-[2]` (~2x name's width — emails are longer), role select → `w-32 shrink-0`, delete icon → `shrink-0`. Today every field is at its intrinsic width with no flex hint, so the row doesn't fill horizontal space. > - **(c) Document title + Optional message inputs:** make sure they have `w-full` on the wrapper so they span the dialog's content width. > - **(d) Optional message textarea:** bump rows from 4 → 6 minimum (`rows={6}` or `min-h-[8rem]`) so reps writing real messages have room. > - **(e) Audit the other steps of the wizard** (select-file, place-fields) for the same content-distribution issues since they share DialogBody. > - **Effort:** ~20-30 min. Captured 2026-05-21 from UAT. **Pairs nicely with:** the platform-wide form-error UX work (Bucket 2) — both touch how form content is presented in dialogs. > - **SHIPPED (width + recipient row + textarea) in 203f543:** dialog widened to `max-w-[1400px] w-[95vw]` so the place-fields step gets the room it needs; recipient row swapped from `grid-cols-12` to a flex layout (Name `flex-1`, Email `flex-[2]`, Role `w-40 shrink-0`, delete `shrink-0`); invitation-message textarea bumped from 3 → 6 rows. Step-adaptive sizing skipped — the new wider dialog works for all three steps without per-step gymnastics. > - **ColumnPicker: add "Hide all columns" symmetric to "Show all columns"** — _src/components/shared/column-picker.tsx:58-60 (`showAll()`) + 116-123 (button render)_ — current picker has a "Show all columns" footer item that clears the hidden set. Add a parallel `hideAll()` that sets `hidden = columns.filter(c => !c.alwaysVisible).map(c => c.id)` — hides every toggleable column while preserving `alwaysVisible` ones. Render a "Hide all columns" footer item next to "Show all columns" with the same visibility gate (only shown when ≥1 toggleable column is currently visible, mirroring the `canShowAll` logic). Since column-picker is shared across every DataTable surface (berths, clients, interests, yachts, companies, reservations, invoices, audit-log, expenses), the fix lands platform-wide automatically. ~5 min. Captured 2026-05-21 from UAT. **SHIPPED in 8f42940:** `hideAll()` + symmetric `canHideAll` gate added; both items render under the same separator. > - **SHIPPED (core) in 03a7521 (K40):** Resolved endpoint widened to accept `?keys=k1,k2,...` so checklist batch-resolves heterogenous registry keys through port → global → env → default in one round-trip. Captures dominant source per step ("env fallback", "global default", "built-in default") surfaced inline under green tick so super-admins see when a step relies on env rather than per-port override. Compound-key gates report weakest sub-key's source. Topbar banner / dashboard tile / weekly nudge / celebration sub-items remain queued. **OnboardingChecklist: auto-check uses raw setting-row presence, not resolver chain → ports using env fallback or global config never auto-tick + super_admin discoverability gap** — _src/components/admin/onboarding-checklist.tsx:32-105 (STEPS def)_ + _src/lib/services/port-config.ts_ (the resolver chain like `getPortDocumensoConfig`) + new dashboard tile + new topbar banner for the discoverability half. Two linked bugs surfaced UAT 2026-05-21. > - **(a) [bug] Auto-check sentinels are too strict.** Examples: > - Email step (line 46) checks `smtp_host_override` — only fires when port has its own override row. Ports using global SMTP (the common case) never auto-tick even though email works. > - Documenso step (lines 58-63) requires ALL of 4 port-level overrides. Per CLAUDE.md, Documenso supports env fallback (`getPortDocumensoConfig` does `adminValue ?? env.DOCUMENSO_API_KEY`), so a working port using env config registers as not-onboarded forever. > - Same pattern likely for storage, settings, etc. — any setting with a resolver chain falls into this trap. > - **Fix:** replace each `autoCheckSettingKey` with an `autoCheckResolver` function (named import from `src/lib/services/port-config.ts` etc.) that runs the full resolver chain and returns `true` when the functional config is complete. New OnboardingStep shape: `{ id, label, description, href, autoCheckResolver?: (portId) => Promise }`. Sentinels stay for steps where direct setting-row presence IS the truth (e.g. branding logo URL). > - Belt-and-braces: surface what's resolving from where directly in the step row (e.g. "Email: ✓ Using global SMTP" vs "Email: ✓ Per-port override"). Closes the "why is this checked?" gap for admins later. > - **(b) [feature] Super_admin discoverability — nudge until onboarding hits 100%.** Today the checklist only appears on the one admin onboarding page; a super_admin who skips that page never sees it. Multi-surface nudges: > - **Topbar banner** when onboarding < 100% — slim chip showing "Setup X% complete · Continue →" (links back to /admin/onboarding). Dismissible per-session (returns next login). Only visible to super_admin. > - **Dashboard rail tile** "Continue setup" — small card on the dashboard widget rail showing the next incomplete step + a button. Disappears entirely at 100%. > - **In-app notification (existing notification infra)** — fires once per week per super_admin until 100%, with a deep-link back to the checklist. "Your setup is X% complete — N items remaining." > - **Onboarding-complete celebration** — small toast + a one-time 🎉 highlight when the 100th item ticks. Acknowledges the finish-line so the nudges going silent feels intentional, not just a bug. > - **Permission gating:** all surfaces gate on `super_admin` (or whatever role the onboarding page itself is gated on) so non-super-admins don't see noise about settings they can't change. > - **Effort:** ~3-4h for (a) (resolver-chain audit + 6-8 step migrations + tests) + ~3-4h for (b) (topbar banner + dashboard tile + notification job + celebration). Total ~6-8h. Captured 2026-05-21 from UAT. > - **Agent audit (a11y + i18n) — 2026-05-21 — 27 findings bundled** — read-only Opus-agent pass over login/dashboard/interest-detail/client-detail/berth-detail/public-form/portal/admin surfaces. Ship as themed sub-PRs, not one mega-PR. > - **a11y — discrete fixes (~3-4h total):** > - Add `aria-label="Row actions for {name}"` on icon-only kebab triggers — _interest-columns.tsx:296_, _client-columns.tsx:301_, _berth-columns.tsx:175_. ~10min. > - Add `aria-label` + `aria-pressed` on Table/Board view toggle — _interest-list.tsx:187-202_. ~5min. > - Add `aria-expanded` + `aria-controls` on the "Show/Hide upcoming milestones" disclosure — _interest-tabs.tsx:484-494_. ~5min. > - Same for recommender "Hide/Add filters" — _berth-recommender-panel.tsx:466-471_. ~3min. > - Fix BrandedAuthShell logo `alt` default (`'Sign in'` shows on every page) — use `alt=""` when no port name OR pass per-page override — _branded-auth-shell.tsx:32,58_. ~10min. > - Mark PDF logo crop image decorative (`alt=""`) — _pdf-logo-uploader.tsx:312-318_. ~3min. > - Add `scope="col"` on raw `` cells (or migrate to shadcn ``) — _berth-interests-tab.tsx:149-154_, _bulk-hard-delete-dialog.tsx:185-186_, _bulk-add-berths-wizard.tsx:226-231_. ~10min. **SHIPPED in 72d7803.** > - Wrap "Loading…" auth fallbacks in `role="status" aria-live="polite"` — _set-password/page.tsx:107_, _portal/activate/page.tsx:9-11_, _supplemental-info/[token]/page.tsx:140-147_. ~10min. **SHIPPED in 05e727f:** all three sites wrapped; supplemental-info also gains sr-only "Loading" copy since only a spinner was visible. > - Add `aria-live` region on supplemental-info async state swaps — _supplemental-info/[token]/page.tsx:150-186_. ~10min. > - Add `