diff --git a/docs/superpowers/audits/alpha-uat-master.md b/docs/superpowers/audits/alpha-uat-master.md
index 6ce8ff33..ddcb601f 100644
--- a/docs/superpowers/audits/alpha-uat-master.md
+++ b/docs/superpowers/audits/alpha-uat-master.md
@@ -39,7 +39,7 @@ _Copy tweaks, alignment, single-prop edits, obvious typos._
> - **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.
-> - **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.
+> - **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.
@@ -87,6 +87,8 @@ _Copy tweaks, alignment, single-prop edits, obvious typos._
> - **Sweep: remove em-dashes from all user-facing copy (toast messages, button labels, helper text, banners, dialog descriptions, empty states)** — em-dashes (`—`) feel AI-generated and add visual noise; user reads them as "Claude wrote this." Replace with periods, commas, colons, or simple hyphens depending on context. **Scope:** _src/components_ (every UI string), _src/lib/email/templates_ (email body copy), _src/lib/templates_ (merge-field labels + EOI body), _src/app_ (page-level copy), public form copy, error messages from `src/lib/errors`. **Out of scope (keep em-dashes):** code comments, JSDoc, audit-log entries, structured logging, this UAT findings doc itself (internal docs are fine). **Method:** grep `—` across `src/`, manually triage each match (some are inside JSX, some inside string literals); replace per context. Heuristic: if a user could see the character, replace it. **Effort:** ~2-3h depending on hit count (rough estimate 200-400 instances). Captured 2026-05-21 from UAT. **Going forward:** add an ESLint rule banning `—` in JSX text + string literals inside `src/components` so new code doesn't reintroduce them.
> - **SHIPPED (lint guard only) in 52342ee:** `no-restricted-syntax` rule on `JSXText[value=/—/]` scoped to `src/components` + `src/app`, set to `warn`. 111 existing instances flagged as warnings — sweep remains parked.
> - **SHIPPED (full sweep) in f0dbefc:** 176 em-dashes replaced with " - " across 49 files in `src/components` + `src/app`, skipping pure-comment lines (// /\* \* \*/). Two `—` HTML entity cases (system-monitoring-dashboard + interest-stage-picker) caught separately. Lint rule bumped from `warn` → `error` so new code reintroducing em-dashes in JSX text fails the gate. Templates / audit-logs / structured logging stayed untouched per scope.
+> - **[Captured 2026-05-22] Dev-server unreachable from a phone on the LAN — three bundled fixes shipped in `be261f3`:** (1) `getPortBrandingConfig` normalizes localhost / private-LAN host prefixes (192.168._, 10._, 172.16-31._, 127._, 0.0.0.0) on read so an in-app `` resolves against current origin; both branding upload routes store path-only going forward; new `absolutizeBrandingUrl()` helper re-absolutizes for email shells; DB backfill stripped the 2 existing rows. (2) Socket.IO client connects via `io()` no-URL (window.location.origin) instead of `NEXT_PUBLIC_APP_URL`; server CORS uses a function that allows localhost + private-LAN in dev, locks to APP_URL in prod (mirrors better-auth trustedOrigins). (3) Portal logout route builds redirect from `req.url` instead of `env.APP_URL`. next.config `allowedDevOrigins` widened from a hardcoded IP to 192.168._ / 10._ / 172.16-31.\* wildcards so HMR works across networks without per-network edit (without HMR the login form's React click handler never hydrates and the form falls back to GET, leaking the password into the URL — the symptom that caught this).
+> - **[Captured 2026-05-22] Dashboard build crashed with "Module not found: Can't resolve 'fs'" via dashboard-shell → export-dashboard-pdf-button → dashboard-report-data.service → dashboard.service → @/lib/db → postgres. SHIPPED in `adf4e2b`:** split pure data + types (`PDF_DASHBOARD_WIDGET_IDS`, `PDF_DASHBOARD_WIDGETS`, `PdfDashboardWidgetId`, `PdfDashboardWidgetOption`) into new `src/lib/services/dashboard-report-widgets.ts`; client button imports from there; service re-exports from new file for backwards compat. The resolver (`resolveDashboardReportData`) stays in the service module since it's server-only.
> - **Custom-field form: "Sort Order" needs an explainer tooltip — example of a broader gap** — _src/components/admin/custom-fields/custom-field-form.tsx:298-308_ — surfaces a specific instance of a platform-wide gap: see the next finding for the full sweep. **SHIPPED in 552b966:** Sort Order now uses the FieldLabel primitive (PR4.2) with explainer tooltip. First adoption of the primitive; platform-wide sweep remains parked.
> - **DocumentList DocRow kebab: add "Download" action** — _src/components/documents/document-list.tsx:86-109_ — current kebab has Send-for-Signing (draft only), Move-to-folder, Delete. No Download. Reps reviewing a signed doc from the interest's documents tab have to navigate into the document detail to download. Add a `` at the top of the menu when `doc.signedFileId` is set (or `doc.fileId` for non-Documenso docs like manual uploads), wired to the same `apiFetch('/api/v1/files/[id]/download')` + anchor-click pattern used elsewhere. Permission-gate by `files.download` if that perm exists. ~10 min. Captured 2026-05-21 from UAT. **SHIPPED in 52342ee:** DocRow now renders Download at the top of the kebab when `signedFileId` is set; wired via the existing `triggerUrlDownload` helper from PR1.
> - **InterestEoiTab "Open" link too ambiguous — relabel to "Open in Documents"** — _src/components/interests/interest-eoi-tab.tsx:163_ — the link in the EOI history list goes to `/${portSlug}/documents/${d.id}` (Documents Hub doc detail) but the label just says "Open" + an external-link icon. Rep can't tell where it goes until they hover. Change to `Open in Documents` (or `View in Documents`). Apply the same idiom anywhere else a cross-section navigation link uses bare "Open" — quick grep + sweep. ~5 min. Captured 2026-05-21 from UAT. **SHIPPED in c6dcf49.**
@@ -148,8 +150,8 @@ _Component refactors, multi-file edits, single-service tweaks, new validators._
> **[Umami] Follow-ups parked at end of 2026-05-19 build session:**
>
> - **[Umami] Empty-state nudges on quiet ranges** — _src/components/website-analytics/{top-list.tsx, sessions-list.tsx, weekly-heatmap.tsx, visitor-world-map.tsx}_ — every card currently renders a flat "No data in this range" string when Umami returns nothing. Replace with a guided message that nudges the operator to expand the range — e.g. "No data in the last 7 days. Try 30d or 90d." plus a one-click button that flips the active `DateRange`. The hook stack already accepts a range setter via the URL search params, so this is purely component-level copy + a Button. ~45 min across the 4 cards. Captured 2026-05-19.
-> - **[Umami] Apple Mail privacy disclaimer copy** — _src/app/(dashboard)/[portSlug]/admin/website-analytics/page.tsx_ — the "Track email opens" toggle helper text mentions Apple Mail pre-fetch in passing. Promote it to a bullet list under the field so admins can't miss it (Apple Mail Privacy = over-count; image-blocking clients = under-count; pixel won't fire when EMAIL_REDIRECT_TO is set). ~15 min. Captured 2026-05-19.
-> - **[Umami] Open-rate column on the document_sends list** — _src/components/documents/_ (find the list that renders document*sends rows; might be inside the interest detail Documents tab or in a dedicated sends-list surface), \_src/lib/services/document-sends.service.ts (listSends extension)* — Phase 4b shipped the data (`open_count` + `first_opened_at` on `document_sends`); the list UI doesn't surface it. Add an "Opened" column showing either a check + relative-time ("Opened · 2h ago · 3 opens") or an em-dash. Sort affordance optional. ~1-2 h depending on how many list surfaces exist. Captured 2026-05-19.
+> - **SHIPPED in a7cbee0 (O52):** Sends-log "Not opened" badge carries inline tooltip explaining Apple Mail's privacy protection routes opens through Apple's proxy and can suppress this signal even when the recipient read the email. **[Umami] Apple Mail privacy disclaimer copy** — _src/app/(dashboard)/[portSlug]/admin/website-analytics/page.tsx_ — the "Track email opens" toggle helper text mentions Apple Mail pre-fetch in passing. Promote it to a bullet list under the field so admins can't miss it (Apple Mail Privacy = over-count; image-blocking clients = under-count; pixel won't fire when EMAIL_REDIRECT_TO is set). ~15 min. Captured 2026-05-19.
+> - **SHIPPED in a7cbee0 (O53):** SendRow type extended with `trackOpens` / `openCount` / `firstOpenedAt`; sends-log card renders "Opened × N" badge with first-open timestamp in title, or "Not opened" when tracking on but no opens yet, or no badge when tracking was disabled for that send. **[Umami] Open-rate column on the document_sends list** — _src/components/documents/_ (find the list that renders document*sends rows; might be inside the interest detail Documents tab or in a dedicated sends-list surface), \_src/lib/services/document-sends.service.ts (listSends extension)* — Phase 4b shipped the data (`open_count` + `first_opened_at` on `document_sends`); the list UI doesn't surface it. Add an "Opened" column showing either a check + relative-time ("Opened · 2h ago · 3 opens") or an em-dash. Sort affordance optional. ~1-2 h depending on how many list surfaces exist. Captured 2026-05-19.
> - **[Umami] Verify pixel + tracked-link end-to-end with a real send** — _manual_ — flip the admin toggle on (`email_open_tracking_enabled = true` for port-nimara), send a real sales email to your own address, open it in Mail.app and Gmail web, then confirm: (a) `document_send_opens` row appears, (b) `open_count` + `first_opened_at` increment on the parent row, (c) Umami records an `email-opened` event. Same drill for `/q/` once the composer button (Bucket 3) ships. Cannot be automated — needs a real inbox. Captured 2026-05-19.
> **Outstanding (gaps on shipped work + rapid UAT capture):**
@@ -217,14 +219,14 @@ _Component refactors, multi-file edits, single-service tweaks, new validators._
> - **Duplicate Reminder surfaces on Interest Overview** — _src/components/interests/interest-tabs.tsx_ — the legacy "Reminder" panel (driven by `interest.reminderEnabled / reminderDays / reminderLastFired`) and the new "REMINDERS" section (driven by the `reminders` table via the bell-in-header) both render on the same tab and tell different stories. The legacy field still drives a real backend worker (`processFollowUpReminders` in `reminders.service.ts:428` — creates auto-follow-up reminders when no activity in N days), so we can't just delete the field. Approach: hide the legacy "Reminder" panel from the OverviewTab grid; surface the recurring-follow-up config either as a slim row inside the REMINDERS section or as a setting on the interest detail header. Keep the worker untouched. ~1 h. **SHIPPED in f39f0aa:** legacy panel hidden from Overview; worker untouched. Surfacing the recurring-follow-up config on the detail header is parked.
> - **LinkedBerthsList: no "add another berth" affordance from the card** — _src/components/interests/linked-berths-list.tsx_ — multi-berth interests are first-class (`interest_berths` is the source of truth per CLAUDE.md) but the LinkedBerthsList card doesn't expose an inline "Add a berth" button. Reps have to use the BerthRecommenderPanel below — discoverability gap. Add a CTA button to the card header (gated by `berths.edit`) that opens a picker / sheet to add another `interest_berths` row. ~45 min. **SHIPPED in 3999d4b:** "Add berth" button on the card header; opens a `