Annotate ColumnPicker, FileInputButton, and DatePicker / DateTimePicker
entries with the 8f42940 summary. Notes the deferred sweeps:
- 15+ remaining date-input sites
- raw-input file sweep was a no-op (audit showed only 1 actual
default-UI site, already migrated)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Annotate B4 #5 with the 6cdb9af summary of what landed (a/b/c/d +
default title) and what's deferred (e — edit metadata UI bundles with
later signing-flow rework).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PR1 batch (2d57417) covered 7 Wave-1 blockers; each finding entry now
carries an inline `**SHIPPED in 2d57417:**` line summarizing what
landed and (where applicable) what remains parked for later waves
(backfill scripts, nested-folder migration, platform-wide form-error
audit).
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>
Spawned 16-agent sonnet[1m] audit team covering schemas (people/orgs,
pipeline, docs+infra), APIs (public, admin, v1 CRUD, webhooks/auth/
storage), services (EOI/Documenso, domain, observability), background
jobs, UI (admin, entity), and cross-cutting security/performance/tests-
deps. 13 of 16 agents delivered detailed JSON reports; A1/F1/B3 audited
inline after their agents stalled. E1/E2 (admin + entity UI) couldn't
complete in a single spawn — flagged for re-attempt with narrower scope.
Top findings:
- 5 CRITICAL: send-invoice and invoice-overdue-notify silently no-op
(D1#1); 5 maintenance crons including database-backup scheduled but
unimplemented (D1#2); tenure-expiry-check ditto (D1#3); GDPR export
bundles not deleted on RTBF (C3#1, gap in A.7 shipped today);
residential_clients has no hard-delete path at all (C3#2).
- 15 HIGH including: /api/public/interests doesn't validate portId
(B1#1, cross-tenant injection); documents.documenso_id has zero
index (A3#1, every webhook is a full scan); better-auth rate limit
is in-memory (B4#1, multi-replica bypass); generateAndSignViaInApp
omits portId on Documenso calls (C1#1); custom-doc-upload calls
placeFields after distribute (C1#2); {{eoi.berthRange}} +
{{reservation.*}} tokens never resolved (C1#3); recommender SQL/JS
stage-scale off-by-one (C2#1); getClientById runs 6 queries serial
(F2#1); no CI pipeline + zero tests on client-hard-delete (F3#1,2).
- 36 medium, 53 low, 19 info.
Triage groups in the doc:
Tier S: 7 ship-stopping bugs (today)
Tier 1: ~12 high-severity items (this week)
Tier 2: ~36 medium (next sprint)
Tier 3: ~53 low (rolling)
Tier 4: re-spawn E1+E2 with narrower scope
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Audit cleanup completion plan, all tiers shipped:
Tier 1 (security + data integrity)
- A.7 RTBF true wipe: redact email_messages body/subject/addresses for
threads owned by deleted client; redact document_sends.recipient_email;
collect file storage keys + delete blobs post-commit.
- A.8 user_permission_overrides FK: documented inline why cascade is
correct (not set-null as audit suggested) — overrides have no value
without their user.
- W2.14 PII redaction: camelCase normalization in audit.ts +
error-events.service.ts isSensitiveKey; added city/postal/country/
birth fragments. firstName/lastName/dateOfBirth/postalCode etc. now
caught in BOTH masker paths. 12 new test cases lock the coverage.
Tier 2 (Documenso completion + refactor)
- C.2: documentEvents.recipient_email column + partial unique index for
per-recipient webhook dedup (migration 0075). handleDocumentSigned
now sets recipient_email on insert.
- Phase 2: completion_cc_emails distribution. handleDocumentCompleted
reads documents.completionCcEmails, filters out signer-duplicates
case-insensitively, fans signed PDF out to non-signer recipients.
- C.4: extracted createPublicInterest() service from the 346-line
api/public/interests route. Route becomes a thin shell (rate-limit,
port resolution, audit log, email fan-out). The trio creation logic
is now unit-testable without an HTTP fixture.
- Phase 4: POST /api/v1/document-templates/[id]/detect-fields wired
to document-field-detector.detectFields(). Sparkles "Auto-detect"
button added to template-editor.tsx — maps DetectedField → marker
with best-guess merge token (DATE / NAME / EMAIL); user retags.
Tier 3 (reporting + recommender snapshot lockfiles)
- W7.reports: extracted rollupStageRevenue / rollupStageCounts /
computeTotalForecast / computeOccupancyRate / rollupBerthStatusCounts
into src/lib/services/report-math.ts (pure functions). 16 new tests
including an inline-snapshot lockfile on a representative 7-stage
forecast. report-generators.ts now delegates.
- W7.recommender: 18 new toMatchSnapshot tripwires on classifyTier
boundaries + computeHeat at canonical input points.
Tier 4 (rolling)
- W6.attach: fixed outdated CLAUDE.md claim — threshold banner is
informational and never depended on IMAP; bounce monitoring (the
IMAP poller) is separate.
- D.1 + D.2: documented deferral inline with full why-not-build-it
reasoning so a future engineer sees the rationale.
- G.1: representative formatDate sweep (audit-log-list, user-list,
document-templates merge tokens, document-signing email). Rest of
the ~100 sites stay rolling.
Quality gates: 1420/1420 vitest (46 new tests above baseline of 1374),
tsc clean, 0 lint errors.
Plan: docs/superpowers/plans/2026-05-18-audit-cleanup-completion.md
Migration: 0075_c2_document_events_recipient_email.sql (applied to dev DB).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 3 — EOI overrides (now ☑):
- Address override field with the same per-component input UX as the
canonical address form (line1/line2/city/state/postal + ISO
subdivision + CountryCombobox). Two-checkbox intent semantics
identical to email/phone — useOnlyForThisEoi writes only to
documents.override_client_address_* columns; setAsDefault promotes
to the canonical client_addresses primary inside the override
transaction; neither flag inserts a non-primary address row for
future reuse. eoi-context route now returns available.addresses so
the dialog can render the picker over existing rows.
- yachts.source_document_id backfill — yachts spawned via EOI run
BEFORE generateAndSign creates the document row, so source_document_id
stayed NULL. Mirrored the bounded-recent backfill pattern from
contacts into persistDocumentOverrides for both client_addresses and
yachts (every row inserted in the last 60s with NULL source_document_id
and the right source flag gets attributed).
- Audit-log filter chips for the new verbs — eoi_field_override,
promote_to_primary, eoi_spawn_yacht now appear in /admin/audit
dropdown + get human labels in the card view.
Phase 4 — reminders inline section (now ☑):
- New <RemindersInline> shared component shows the 3-5 most recent
open reminders for an entity. Mounted on Overview tab of yacht /
client / interest detail. Empty state hints at the header button
rather than duplicating it.
Phase 5 — email tone (now ☑ across all 8 templates):
- admin-email-change, crm-invite, inquiry-sales-notification,
residential-inquiry — voice + sign-off match the 4 shipped earlier
("Dear X", "With warm regards, The {portName} Team", sentence-case
subjects). Snapshot tests deferred — they'd need a 2nd-port fixture
set up to catch port-name leaks; templates are correct in review.
Phase 7 — PDF editor (now ☑):
- 7.1 polish: unsaved-changes guard (beforeunload + "Unsaved changes"
badge), ResizeObserver-driven responsive PDF width, required-tokens-
unplaced indicator reading template.mergeFields.
- 7.2 drag-to-move with on-page clamping.
- 7.2 four-corner resize handles with min-size enforcement.
- 7.2 right-click context delete via onContextMenu.
- 7.2 multi-page navigation + per-page marker filter.
- 7.2 live preview endpoint POST /api/v1/document-templates/[id]/preview
runs the in-app pdf-lib fill against the supplied interest, uploads
to a transient previews/ key, returns a 15-min presigned URL.
- 7.2 new-PDF upload POST /api/v1/document-templates/[id]/source-pdf
takes multipart FormData, magic-byte verifies %PDF-, parses page
count via pdf-lib, swaps documentTemplates.sourceFileId. Editor
warns when the new page count truncates the prior set.
Quality gates: 1374/1374 vitest, tsc clean, lint 0 errors.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 5 — luxury-port email tone (4 of 8 templates):
- portal-auth.tsx — activation + reset: "It's our pleasure to invite
you to the {portName} client portal — your private space to review
your berth, manage signed documents, and stay in touch with your
sales liaison", sign-off "With warm regards, The {portName} Team",
subjects "Welcome to {portName} — activate your client portal" /
"Reset your {portName} portal password".
- inquiry-client-confirmation.tsx — "We've noted your enquiry, and a
member of our team will be in touch shortly through your preferred
channel", "should anything come to mind in the meantime", sign-off
"With warm regards, The {portName} Sales Team".
- notification-digest.tsx — "Your {portName} update" header, "Here's
what's waiting for you", "With warm regards, The {portName} Team".
- document-signing.tsx — all 4 sign-offs ("Dear X, ... Thank you, The
{portName} team") rewritten to "With warm regards, The {portName} Team"
with capitalised Team for consistency.
- Voice captured from old-CRM Nuxt repo
(/Users/matt/Repos/Port Nimara/Port Nimara Client Portal/client-portal/
server/utils/signature-notifications.ts) which already used "Dear",
"Best regards", and collective sign-offs.
Remaining 4 templates (admin-email-change, crm-invite,
inquiry-sales-notification, residential-inquiry) + cross-port snapshot
tests queued as follow-up.
Phase 7.1 — PDF editor scaffold:
- New admin route /admin/templates/[id]/editor/page.tsx wired to a
client-side <TemplateEditor>.
- Renders page 1 via react-pdf (worker URL pattern mirrors
components/files/pdf-viewer.tsx); click-to-place markers in percent
coordinates so a future page-size swap doesn't shift placements.
- Token picker over VALID_MERGE_TOKENS (sorted).
- Save persists overlayPositions via PATCH against the existing
document_templates row; validator accepts the new field via
fieldMapSchema from lib/templates/field-map.ts (no migration needed
— overlay_positions JSONB column already exists).
- Outer/inner-body split + key-by-templateId remount avoids the
in-render setState antipattern when seeding from server data.
- Add + delete markers supported. Multi-page, drag, resize, preview,
new-PDF upload all defer to 7.2.
Per-entity polish:
- [+ Reminder] button on yacht / client / interest detail headers,
threading defaultYachtId / defaultClientId / defaultInterestId so the
ReminderForm opens with the entity pre-linked.
- [EOI] badge on yacht detail header when yacht.source === 'eoi-generated'
(mirrors the contacts-editor pattern shipped in eaab149).
Phase 6 hardening:
- imap-bounce-poller strips whitespace from IMAP_PASS so Google
Workspace App Passwords (16-char "abcd efgh ijkl mnop" format) work
whether pasted with or without spaces. Confirmed via Google docs that
the visual spaces are formatting only and must not reach the IMAP
server.
Quality gates: 1374/1374 vitest, tsc clean, lint 0 errors.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 3b — EOI dialog field overrides:
- New EoiOverridesInput shape (clientEmail / clientPhone / yachtName)
threaded through generate-and-sign validator + both pathways
(in-app pdf-lib fill, Documenso template generate).
- src/lib/services/eoi-overrides.service.ts applies side-effects in one
transaction: useOnlyForThisEoi writes documents.override_* and stops;
setAsDefault demotes the prior primary + promotes (existing contactId)
or inserts + promotes (fresh value); neither flag inserts a non-primary
client_contacts row for future dropdown reuse.
- Document override columns persisted post-insert, with a 1-minute
source_document_id backfill on freshly inserted contact rows.
- eoi-context route returns available.{emails, phones} so the dialog
can render combobox options.
- <OverridableContactField> in eoi-generate-dialog.tsx renders the
combobox + manual input + 2 checkboxes per field with mutually
exclusive intent semantics.
Phase 3c — yacht spawn from EOI dialog:
- YachtForm gains createExtras + onCreated callbacks; the EOI dialog
opens it as a nested Sheet pre-filled with the linked client as owner.
On save the new yacht is stamped source='eoi-generated' and the
interest is PATCHed with the new yachtId so the EOI context reflows.
Phase 3d — promote-to-primary + audit + [EOI] badge:
- POST /api/v1/clients/:id/contacts/:contactId/promote-to-primary
(transactional demote+promote via promoteContactToPrimary).
- src/lib/audit.ts AuditAction type adds eoi_field_override,
promote_to_primary, eoi_spawn_yacht (DB column is free-text).
- ContactsEditor surfaces an [EOI] badge on non-primary rows where
source='eoi-custom-input'.
Phase 4 — worker + TOD picker:
- processOverdueReminders refactored to UPDATE...RETURNING with a
fired_at IS NULL gate so parallel workers can't double-fire. Uses
the idx_reminders_due_unfired partial index from migration 0072.
- /settings gets a "Default reminder time" time-of-day picker; the
value lands in user_profiles.preferences.digestTimeOfDay (validated
HH:MM at the route). <ReminderForm> seeds its dueAt from this
preference via a React-Query me-prefs fetch.
Phase 6 hardening:
- IMAP bounce poller strips whitespace from IMAP_PASS so a copy-paste
of Google Workspace's 16-char App Password formatted as
"abcd efgh ijkl mnop" still authenticates. Workspace activation
procedure documented in MASTER-PLAN §Phase 6 (was previously written
to CLAUDE.md, which was bloat — moved to the plan).
Quality gates: 1374/1374 vitest, tsc clean, lint 0 errors.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three of the master plan's "suggested execution order" items shipped this
session; Phase 3b (EOI dialog overrides) deferred — estimate exceeded the
remaining session time.
- Phase 4 polish: yachtId field on <ReminderForm> via the existing
YachtPicker, Ship-icon subtitle on <ReminderCard>, listReminders filter
by yachtId, getReminder joins the yacht relation.
- Phase 2 risk-signal data wiring: getInterestById derives the 3 dates
(dateDocumentDeclined / dateReservationCancelled / dateBerthSoldToOther)
from document_events / berth_reservations / cross-interest interest_berths
in parallel — chosen over new schema columns to keep the master plan's
"no new tables" promise. Threaded through to DealPulseChip.
- Phase 6 cron + UI: src/jobs/processors/imap-bounce-poller.ts polls the
configured IMAP mailbox (IMAP_* env), matches NDRs to recent
document_sends rows via recipient + 7-day window, idempotent via
bounceDetectedAt, fires email_bounced notifications on hard/soft
(skips OOO). State persisted to system_settings.bounce_poller_state.
Wired into maintenance queue at */15 * * * *. Admin /admin/sends page
surfaces the bounce badge + reason inline.
- CLAUDE.md: trimmed 27KB → ~19.5KB (~28% smaller bytes). Prose-heavy
Documenso webhook / v1-v2 routing / Document folders sections rewritten
as scannable bullets. Added a new "Working in this repo — skills, MCPs,
agents" section promoting brainstorming/TDD/debugging/frontend-design
skills, Context7/Playwright/Serena MCPs, and the Explore/feature-dev
agents. Documented Phase 2 derivation choice in the data-model section.
Quality gates: 1374/1374 vitest pass, tsc --noEmit clean, lint 0 errors.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Single source of truth for all remaining audit + feature work:
Documenso completion, deal-pulse signals + admin config, EOI overrides,
Reminders, email-copy refactor, IMAP bounce linking, PDF editor.
Each phase carries goal, scope, schema, API/UI surfaces, acceptance
criteria, test plan, effort estimate, and a sub-task tracker that
fresh sessions tick through.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Design spec for moving tenant-configurable env vars into the per-port
admin UI via a settings registry. Covers scope decisions, registry
shape, resolver, encryption, admin UI generation, env catalog by
disposition, migration plan, and testing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
11 of 13 known issues (A1-A20) fixed and verified; legacy-stage rank
tables in clients.service.ts + berth-recommender.service.ts purged of
9-stage enum keys. 1373/1373 vitest pass.
Remaining catalog (300+ checks) listed by section so it's clear what's
covered vs. still on the to-do list.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Covers super-admin, sales-rep, viewer, portal, catch-up wizard, and the
single-tree responsive shell. 13 findings catalogued with reproduction +
effort estimates, plus a positive-findings section confirming what
shipped is working end-to-end:
- F22/F23/F25/F44 verified live
- #67 catch-up wizard runs full transaction (client+interest+clear-override)
- #26 single-tree shell verified at 390px and 1440px viewports
- permission gating holds for sales-agent and viewer
Critical issues found:
- A4 New Client form silently rejects submit when an empty contact row is present (F19 filter runs in mutationFn, too late)
- A16 file upload at documents-hub root fails: client sends nulls, validator wants strings or absent
- A17 /api/v1/admin/ports is super-admin-only but apiFetch uses it to bootstrap port-slug→port-id resolution for every user
See docs/audit-2026-05-15.md for the full list.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
24 fixes + 1 new feature, tiered by priority. T0 already shipped in the
previous commit; T1-T4 batches sequenced with effort estimates and file
pointers. Includes the manual-berth-status catch-up workflow design.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
NocoDB inspection (via MCP) confirms the legacy Interests table carries
only the current Sales Process Level value plus point-in-time event
timestamps as text fields — no dedicated stage-change history table.
That isn't enough resolution to replay stages-over-time through the
recommender's tier-ladder + heat-score weights. Simulator deferred
until ~10+ real wins accumulate under the new pipeline, then we can
simulate against actual CRM history.
The existing /admin/berth-recommender heat-weight tuning UI is
sufficient for v1 launch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wave 3 of the 2026-05-12 audit cleared all ~45 useEffect→fetch→
setState sites; eslint.config.mjs promoted the rule to error in the
same sweep. BACKLOG's "next pass" entry was stale from before that.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Five engineering refactors and six mechanical service splits the
AUDIT-2026-05-12 dossiers flagged. Assessed against today's reality
(no active webhook subscribers, small DB, low-frequency storage
paths) and explicitly deferred. Listed here so future-me doesn't
re-research them when triaging the audit.
Each entry carries its cost estimate and the trigger condition that
should bring it back onto the roadmap.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 5 is mostly coordination + verification rather than a code
build — the embedded signing pages live in a different repo. What
lands here:
1. transformSigningUrl hardening — routes through extractSigningToken
so a bare URL like `https://sig.example.com` no longer produces
the malformed `<host>/sign/<role>/sig.example.com`. The token
validator (≥8 URL-safe chars) rejects malformed tails so the
function falls back to returning the raw URL.
2. 10 unit tests pin the role-segment mapping so a future refactor
can't silently break the contract with the marketing website's
/sign/[type]/[token] page. Covers:
- all five SignerRole → URL segment mappings
- trailing-slash normalization on the host
- null host fallback (single-tenant / staging)
- rejection of non-token-shaped tails
3. docs/documenso-integration-audit.md updated with:
- Phase 2/3/4/7 landed-work summary (replacing the old
"deferred" list that was now stale)
- Phase 5 coordination tracker for the marketing-website side
(the four edits the website team needs to make — listed
here so the CRM stays the source of truth on the contract)
- Phase 6 polish backlog (auto-send delay, document expiration,
per-document message, reminder display, failed-webhook UI,
field metadata panel, zoom controls, recipient drag-reorder)
Tests: 21 new transformSigningUrl + signers tests across two files;
full suite 1340 → 1350 ✅; tsc clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Final pass over the unaddressed AUDIT-2026-05-12 dossiers, taking the
tractable Critical/High items from each:
error-ux-auditor (5 items)
- C2: 17 toast.error(err.message) sites swept to toastError(err, …) so
every user-visible failure carries a copy-paste Reference ID
- C3: apiFetch synthesizes a client-side correlation id when a 5xx
comes back with a non-JSON body (reverse-proxy HTML pages); message
becomes "The server is unreachable. Please try again." with code
UPSTREAM_UNREACHABLE
- C4: checkRateLimit fails OPEN when Redis is unavailable so an outage
no longer 500s login + portal sign-in; logged at warn so monitoring
catches it
- H2: StorageTimeoutError (name='TimeoutError') replaces the plain
Error throw in s3.ts withTimeout — error-classifier hints fire now
- H5: errorResponse() adopted across /api/storage/[token],
/api/public/website-inquiries, and the Documenso webhook body (drops
the "Invalid secret" reconnaissance string)
outbound-webhook-auditor (5 items)
- C1: signature is now HMAC(secret, `${ts}.${body}`) with the
timestamp surfaced as X-Webhook-Timestamp so receivers can reject
replays outside a freshness window
- C3: dead-letter with reason missing_signing_secret when secret is
null (defence-in-depth against DB tampering / future migration
mistakes)
- H2: webhooks queue bumped to maxAttempts=8 with 30 s base
exponential backoff so a 30 s receiver blip during a deploy no
longer dead-letters every in-flight event; per-queue
backoffDelayMs added to QUEUE_CONFIGS
- M1: SSRF denylist gains Oracle Cloud metadata 192.0.0.192
- M2: dispatch-time https:// assertion before fetch, so a bad DB edit
can't slip plaintext through
storage-pathing-auditor (2 items)
- H1: berth-PDF presigned-upload keys now `${portSlug}/berths/…/…`
with portSlug threaded into backend.presignUpload — engages the
filesystem-proxy port-binding `p` token verifier
- H2: presignDownloadUrl auto-derives portSlug from the key's first
segment when callers don't pass it, so all 8 download sites engage
the `p`-token guard without per-site plumbing
search-auditor (1 item)
- H3: removed dead void wantEmail; void wantPhone; pair plus the
unused looksLikeEmail helper — the bucket-reorder it was scaffolded
for was never wired
maintainability-auditor (1 item)
- M2: swept seven abandoned `void <symbol>` markers and their dead
imports across clients/bulk, interests/bulk, admin/email-templates,
admin/website-submissions, alert-rules, and notes.service
Deferred to future work (substantial refactors, schema migrations, or
multi-file UI work):
- error-ux M3-M8 (global-error.tsx, per-route loading.tsx coverage,
ErrorBanner component, /api/ready route, worker DLQ admin surface)
- maintainability C1-C4 (documents/search/notes service splits,
interest-tabs split — multi-hour refactors)
- currency C1-H5 (mixed-currency dashboard aggregation, FX history
table, rounding policy) — wait for second non-USD port
- outbound-webhook C2 (deliveries reaper job), H1 (DNS-rebind TOCTOU
with undici Agent), H3 (circuit-breaker), H5 (presigned-post-policy)
- storage-pathing C2 (orphan reaper), H3-H5 (streaming + content-type
binding)
Tests: 1315/1315 vitest ✅ ; tsc clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Build a shared <TemplateTokenPicker> that renders the canonical
MERGE_FIELDS catalog grouped by scope, plus a dynamically-fetched
"Custom (port-specific)" group surfaced from /api/v1/admin/custom-fields.
The custom group is filtered to entity types the resolver actually
expands at send time (client/interest/berth - see
mergeCustomFieldValues in document-sends.service).
Wire it into both consumers:
- admin/document-templates/template-form.tsx (replaces TEMPLATE_VARIABLES
list which had drifted from the canonical catalog)
- admin/sales-email-config-card.tsx (replaces flat alphabetical dump)
Closes custom-fields §B "UI surfacing of {{custom.…}} tokens".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Swap the one outlier (client-interests-tab.tsx) from Vaul Drawer to
Sheet side=right so every detail-preview surface uses the same
primitive. Document the doctrine: Sheet for side panels on both desktop
and mobile; Vaul Drawer reserved for mobile-only bottom-sheet UX
(currently just MoreSheet).
Closes ui/ux M11.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>