Captured 2026-05-22 from a Playwright MCP pass at 5 viewports × 20
surfaces (375 / 768 / 1024 / 1440 / 1920 px). The tablet tier
infrastructure shipped in 6d665d0 + the dashboard PageHeader
stacking fix lit up the tier — these are the residual bugs the
audit surfaced.
3 Bucket 1 quick-fixes:
- Tablet topbar logo trigger doesn't render visibly (search-bar
translate shifts over leading slot + center column min-width
too wide).
- Dashboard PageHeader at exactly 1024 viewport (sidebar present +
lg:flex-row kicks in, crushing the title).
- useIsMobile call-site audit needed (kept as tier !== desktop
alias; some sites want strict mobile-only).
4 Bucket 2 mediums:
- Documents Hub folder rail truncates to 3 chars at tablet.
- Website analytics 6-KPI row too cramped at 1024.
- Pipeline Value mobile (375) per-stage rows overflow right margin.
- Berths list 1024 — only 5-6 of 14 columns fit before h-scroll.
Screenshots local at tmp/visual-audit-2026-05-22/ (gitignored).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The 100 PNGs from the in-progress visual audit pass landed in cb91f78
because tmp/ wasn't gitignored. Removing from HEAD + adding the rule
so future runs stay local. (Original blobs remain reachable from the
prior commit if needed; not worth a destructive filter-branch.)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Same client-server boundary bug class as adf4e2b. berth-range.ts
imported `logger` (-> request-context.ts -> node:async_hooks) for
two debug/warn calls. external-eoi-upload-dialog.tsx is a client
component and imports formatBerthRange — Turbopack chunked
async_hooks into the client bundle and crashed with:
Code generation for chunk item errored
Caused by: the chunking context (unknown) does not support
external modules (request: node:async_hooks)
Surface was the entire interest detail page on every viewport: dev
shell rendered the Turbopack overlay instead of the actual UI, so
the planned visual audit couldn't take any meaningful screenshots.
Replaced logger.debug + logger.warn with a single console.warn that
summarises non-canonical moorings. console.warn is safe in both
server and client contexts and the formatter's failure mode is
non-critical (verbatim passthrough — no data loss).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 13:48:49 +02:00
3 changed files with 29 additions and 10 deletions
<!-- Append findings as: `1. **Title** — _path:line_ — description. (see Audit X#N if applicable)` -->
> **[Captured 2026-05-22 — visual breakpoint audit, 5 viewports × 20 surfaces via Playwright MCP. Screenshots local at `tmp/visual-audit-2026-05-22/<surface>/<viewport>.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.]**
>
> - **Tablet topbar logo trigger doesn't render visibly** — _src/components/layout/app-shell.tsx_ + _src/components/layout/topbar.tsx_ — AppShell mounts a logo button in Topbar's `leadingSlot` prop on tablet (the design intent: click logo → sidebar Sheet slides in). Live render at 768 shows zero affordance in the topbar's left column — only the truncated search-input placeholder is visible. Two likely causes (do both): (a) the topbar's center column is `minmax(420px, 800px)` which starves the left column to ~100px at 768 viewport width with no sidebar; (b) the search container has an unconditional `sm:-translate-x-[calc(var(--width-sidebar)/2)]` that shifts it 128px LEFT to visually-center against a sidebar that doesn't exist on tablet, pulling the search input over the leading-slot. Fix: (a) change center column to `minmax(280px, 600px)` AND (b) gate the translate to `lg:` so it only kicks in when the sidebar is actually present. ~30 min. Captured 2026-05-22 from visual audit.
> - **Dashboard title strip crushed at 1024 viewport** — _src/components/shared/page-header.tsx_ — at exactly 1024 the desktop shell mounts (sidebar takes 256px) AND `PageHeader`'s `lg:flex-row` kicks in, forcing the title cell to compete with the four-button action row (Today/7d/30d/90d/Custom + Export PDF + Rearrange + Customize). Title degrades to `(` and the "Last 30 days" subtitle wraps three-deep ("Last / 30 / days"). Move the horizontal-stack breakpoint from `lg:` to `xl:` so the strip stacks until 1280px, giving the actions row room to wrap without crushing the title at the tightest sidebar-present width. ~15 min. Captured 2026-05-22.
> - **`useIsMobile()` returns `true` on tablet — call-site audit needed** — _src/hooks/use-is-mobile.ts_ + every component currently importing `useIsMobile` — the new `useViewportTier()` is the precise hook; `useIsMobile` was kept as `tier !== 'desktop'` for back-compat, which is what most call sites want ("show short stage labels", "stack vertically"). But a handful intend strict mobile-only behaviour (e.g. drawer-instead-of-popover, bottom-sheet picker) and should migrate to `useViewportTier() === 'mobile'`. Grep `useIsMobile` and triage each per actual intent. ~30 min for the audit + targeted migrations. Captured 2026-05-22.
> **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.
> - **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 `<img src>` 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] 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 `<img src>` 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 `<DropdownMenuItem>` 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.
_Component refactors, multi-file edits, single-service tweaks, new validators._
> **[Captured 2026-05-22 — visual breakpoint audit findings, medium tier]**
>
> - **Documents Hub folder rail collapses to 3-char truncation at tablet (768)** — _src/app/(dashboard)/[portSlug]/documents/page.tsx_ + the folder-rail component — at 768 viewport the rail renders folder names truncated to ~3 chars + ellipsis ("Cli...", "Co...", "Smok...", "Ya..."). Two viable shapes: (a) widen the rail's min-width to ~180px so names fit, accepting that the file panel shrinks; (b) collapse the rail to a slide-over Drawer at tablet (the file panel takes full width; a small "Folders" button toggles the drawer). (b) is more deliberate but more code. Pick (a) first; revisit if file panel feels too cramped. ~1 h for (a), ~2 h for (b). Captured 2026-05-22.
> - **Website analytics KPI cards too cramped at 1024** — _src/components/website-analytics/_ — 6 KPI tiles (Active right now / Visitors / Visits / Pageviews / Bounce rate / Visit duration) render in one row at 1024 with sidebar present, leaving each card ~120px wide. "VISIT DURATION" value truncates to `2.` with " 0%" trailing. Fix: stack into a 3+3 grid at lg, fall back to 2-col at md, single column at sm. ~30 min. Captured 2026-05-22.
> - **Pipeline Value tile per-stage rows overflow right margin at mobile (375)** — _src/components/dashboard/pipeline-value-tile.tsx_ — at 375 the per-stage row layout (label + bar + value + count/probability) crushes the right column; values like "$3,528,000" mash against the right edge of the card. Either truncate value formatting on mobile (compact: `$3.5M`) or stack the value/count/probability vertically below the bar at sm-. ~45 min. Captured 2026-05-22.
> - **Berths list at 1024 only shows 5-6 of 14 columns** — _src/components/berths/berth-columns.tsx_ — Q59 (`whitespace-nowrap` + column min-widths) was the right call for densely-typed cells, but the side effect is that at 1024 with the sidebar present (768 content area), only Mooring # / Area / Status / Latest deal stage / Active interests fit before horizontal scroll. Consider an auto-hide column policy at lg-tier: hide "Pricing valid", "Side pontoon", "Special features", etc. by default at <1280 so the table reads at a glance, with the column picker letting power users re-add. ~1.5 h. Captured 2026-05-22.
> **[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.
'formatBerthRange: non-canonical moorings passed through (verbatim, not range-compressed)',
console.warn(
'formatBerthRange: %d non-canonical moorings passed through (samples: %o)',
passthrough.length,
passthrough.slice(0,3),
);
}
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.