O48, O51-O54 from the 2026-05-21 plan. Phase 4a / 3 / 5 marketing-site
work explicitly deferred — they live in the marketing repo + are
blocked on instrumentation that isn't this codebase's to ship.
Shipped:
O48 Tracked-link composer button.
New POST /api/v1/tracked-links mints a redirect-link the rep can
drop into an outgoing email. Body { targetUrl, sendId? }; returns
{ id, slug, targetUrl, url }. Gated on `email.send` (same as the
server-side check on existing send routes). `sendId` lets the
click-tracker attribute back to a specific document_sends row.
<TrackedLinkComposerButton> renders a small inline button (or a
sized default variant) that opens a dialog: rep pastes the
destination URL → Create → gets the public /q/<slug> URL with
a Copy + an "Insert into message" action that calls back to the
parent compose surface. Wired into <SendDocumentDialog>'s
Message body label row so reps can mint + insert without
leaving the dialog.
O51 Quiet-range nudge. WebsiteAnalyticsShell surfaces a small amber
banner when the active range returned <5 visitors so the rep
doesn't think the integration is broken on a fresh port or
off-season range. Threshold keeps the banner off legitimate
traffic.
O52 Apple Mail privacy disclaimer. The sends-log "Not opened" badge
carries an inline tooltip explaining that Apple Mail's privacy
protection routes opens through Apple's proxy and can suppress
this signal even when the recipient read the email.
O53 Open-rate column on the document_sends list. SendRow type
extended with `trackOpens` / `openCount` / `firstOpenedAt`; the
sends-log card chrome renders an "Opened × N" badge with the
first-open timestamp in the title, or "Not opened" when tracking
is on but no opens yet, or no badge at all when tracking was
disabled for that send.
O54 Click-to-filter world map. VisitorWorldMap already supported
`onCountryClick`; wired it through to copy the
`/<portSlug>/clients?nationality=<ISO>` deep-link to the
clipboard with a toast on click. Inline filtering of the
analytics view itself stays parked alongside Phase 5 — the
useUmami* hooks don't yet accept a country filter.
Deferred (not in this repo or blocked):
O47 Phase 4a marketing-site instrumentation — marketing repo work.
O49 Phase 3 Events tab — blocked on 4a.
O50 Phase 5 Funnels + Journeys — blocked on 4a.
Verified: tsc clean, vitest 1454/1454.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sweeps Group A of the 2026-05-21 remaining-plan. Several items the
plan listed as open turned out to already be shipped (annotation gap
in the master doc) — those are confirmed in the commit notes.
Shipped now:
A1 Documenso settings: collapsed `V2_FEATURE_FIELDS` +
`CONTRACT_RESERVATION_FIELDS` (legacy SettingsFormCard) into
`RegistryDrivenForm` sections (`documenso.behavior` +
`documenso.templates`). Every Documenso setting now flows
through the registry path that surfaces the env-fallback /
port / global source badge per field. EOI generation card
retitled to "Templates & signing pathway" since it now covers
EOI + reservation + contract template IDs (registry already
had all three under `documenso.templates`).
A2 WatchersCard empty state: bumped `mb-3 → mb-4 pb-1` so the
"No one is watching yet" line has breathing room above the
"Add a watcher…" select.
A4 /invoices/upload-receipts guide copy: terse luxury-CRM tone.
Drop "Snap a photo", "fancy phone camera", "No typing. No
spreadsheets." Tighten OCR explainer to one sentence;
action-oriented step + best-practices headers.
A5 Pageviews chart X-axis: added `interval="preserveStartEnd"` +
`minTickGap={52}` so multi-week ranges thin out the middle
ticks instead of overlapping. The MM-DD formatter was already
in place from an earlier session.
A7 Inbox doc comment: was stale ("Alerts first, Reminders
second") but the JSX already had Reminders before Alerts.
Fixed the docstring.
A9 CommandList scroll-cap: `max-h-[300px]` now `max-h-[min(300px,
var(--radix-popover-content-available-height,300px))]` so the
cmdk list never extends past the host Popover's available
area. Non-Popover hosts fall through to the 300px static cap.
A10 DropdownMenuContent: `max-h-96` now
`max-h-[min(24rem,var(--radix-dropdown-menu-content-
available-height,24rem))]` for the same available-space
behaviour on long menus near the viewport edge.
A11 Residential InterestsTab (list page): row gets an onClick →
`router.push`; first-cell Link stops propagation so middle-
click / Cmd-click "open in new tab" still works.
A12 StageStepper: gained a stage-name row below the bar showing
every reached stage's short label inline (muted for future
stages). `size="xs"` variant keeps the cramped table-cell
footprint intact (no labels).
Already shipped (just annotation gap in master doc):
A3 EOI "Mark as signed without file" button — line 599 of
interest-eoi-tab.tsx, parent passes onMarkSigned. Master doc
already has `SHIPPED in 52342ee` annotation.
A6 Pageviews vs Sessions explainer — Info popover at line
157-181 of website-analytics-shell.tsx.
A8 BulkAddBerthsWizard CurrencySelect — line 376 (apply-to-all)
+ line 456 (per-row).
Verified: tsc clean, vitest 1454/1454.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaced 174 em-dashes (—) with " - " (space-hyphen-space) across 49
files in src/components + src/app. The em-dash reads as a tell-tale
"AI-generated" marker per the user's design feedback; hyphens with
spaces preserve the connector semantics without the AI tint.
Touched only lines outside pure-comment context (// /* * */). Code
comments, JSDoc, audit-log strings, structured logging strings, and
templates outside the lint scope retain their em-dashes for now —
they're not user-visible.
Also captured two remaining cases that used the `—` HTML entity
instead of the literal character (system-monitoring-dashboard,
interest-stage-picker) — replaced with a plain hyphen.
Bumped the existing `no-restricted-syntax` rule from `warn` → `error`
in eslint.config.mjs scoped to src/components/**/*.tsx +
src/app/**/*.tsx. New code reintroducing em-dashes in JSX text now
fails the lint gate.
Verified: tsc clean, vitest 1448/1448, eslint 0 em-dash warnings.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the read-side Umami integration queued in last week's
website-analytics plan (Phases 1–6 of `docs/website-analytics-flesh-out-plan.md`):
- Realtime panel polls Umami at 5s intervals; world map renders visitor
origins via echarts + `public/world-map/echarts-world.json` topo.
- Sessions list + session-detail-sheet drill-down (per-session event
timeline pulled from `/api/v1/website-analytics`).
- Weekly heatmap (day-of-week × hour-of-day) for engagement timing.
- Metric-detail pages under `/[portSlug]/website-analytics/[metric]`
for pageviews / referrers / events deep-dives.
- Email-pixel write path: `/api/public/email-pixel/[sendId]` 1×1 GIF
beacon backed by `email_open_tracking` (migration 0076); resolves
inline on render in inbox.
- Tracked-link redirect: `/q/[slug]` routes through `tracked_links`
(migration 0077) and forwards to the canonical destination after
logging the click.
- Dashboard `website-glance-tile` now reads from the live Umami service
instead of placeholder data.
Deps: `@umami/node`, `echarts`, `echarts-for-react`, `@types/geojson`,
`@types/topojson-client`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Root cause of recurring dev server hangs:
/api/v1/website-analytics threw CodedError('UMAMI_NOT_CONFIGURED') which
rendered as HTTP 409. React Query default-retries on 4xx (we set retry=1
globally), so every page render fired the umami queries → 409 →
retry → 409. Each request queried system_settings to resolve umami
credentials. Six analytics widgets on the /website-analytics page +
two on the dashboard glance tile × 2 (initial + retry) = 16 system_settings
queries on first paint. Combined with React Query refetching on mount,
the postgres pool (max=20) saturated and the server appeared hung.
Fix: return 200 with `{ data: null, notConfigured: true }` instead of
4xx. Not-configured is a steady empty state, not a transient error —
no retry loop. Updated WebsiteGlanceTile (hides itself) and
WebsiteAnalyticsShell (renders configure-umami CTA) to check the new
notConfigured flag.
Also includes from in-flight work: package.json dev script binds
0.0.0.0 so iPhone on LAN can reach the dev server, and BrandedAuthShell
uses fixed/inset-0 + flex to lock the login surface to the viewport so
iOS Safari doesn't rubber-band-scroll the card.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds /[portSlug]/website-analytics dashboard page (pageviews, top
pages, top referrers) and a per-port admin config UI for the
Umami URL / website-ID / API token. Settings live in system_settings
keyed per-port so a future second port has its own Umami account.
Adds a website glance tile to the main dashboard, a server-side
test-credentials endpoint, and a stable cache key for the active-
visitor poll so React Query doesn't fragment the cache per range.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>