Address the pdf-auditor findings that survived the 2026-05-12 PDF stack
overhaul (pdfme → react-pdf). Items C-2/C-3 (tiptap-to-pdfme bugs) were
resolved when that 571-LOC bridge was deleted; remaining items:
- **M-7 wrong-port brand fallback** — replace `'Port Nimara'` defaults
in PDF-rendering services. `reports.service` and `expense-export`
throw when the port row is missing (the job is FK-keyed on a real
port, so absence = broken state, must not stamp a competitor brand).
`record-export` uses `'(port)'` as the visible placeholder.
- **M-2 silent field drift in fill-eoi-form** — promote the
always-silent catch in `setText` / `setCheckbox` to log a structured
warning per missing field (mirroring the existing `setBerthRange`
pattern). A re-cut template with drifted AcroForm field names now
surfaces in ops logs instead of shipping with empty values.
- **M-3 form not flattened** — `fillEoiFormFields` now flattens the
AcroForm before save. Documenso pathway flattens server-side; this
brings the in-app pathway to parity, so the signer can't edit
pre-filled yacht dimensions / address / berth number after the fact.
- **M-1 PDF metadata** — set Title / Author / Subject / Lang / Producer
/ Creator on the generated EOI PDF for downstream readers and a11y
tooling.
- **M-4 noisy berth-range warnings** — downgrade per-mooring warn to
debug; emit a single summary warn per call when any passthrough
occurred. Multi-berth EOIs with archived/legacy moorings no longer
spam the log on every render.
- **M-6 source PDF sha pinning** — pin
`assets/eoi-template.pdf` sha256 via `EXPECTED_EOI_SHA256` (exported
for tests); `loadEoiTemplatePdf` warns once per process when the
bytes drift without an explicit hash bump. Documented the
intentional-update workflow in `assets/README.md`.
Tests updated in `tests/unit/pdf/fill-eoi-form.test.ts` to reflect
flatten + metadata (form fields are gone after flatten; pdf-lib has no
getLanguage so we assert the other setters round-trip).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 1 / commits 7-9 of 14 — bundled because all three record exports
share the same conversion pattern and call sites.
Templates:
client-summary.tsx header + KV grid for client, contacts table
with primary badge, yacht table, interests
table with stage/category, recent activity
table
berth-spec.tsx header + status badge, overview KV grid,
dimensions KV grid (with min markers), pricing
& tenure KV grid, infrastructure KV grid,
waiting list table with priority badges,
maintenance log table
interest-summary.tsx header + stage badge, status KV grid, client
KV, optional yacht/berth sections, milestones
KV grid, recent timeline table
record-export.tsx (renamed .ts -> .tsx for JSX):
- swap generatePdf(...) calls for renderPdf(<…Pdf … />) calls
- inject port logo via resolvePortLogo()
- shape data into typed template props (Drizzle returns are passed
through deliberately so the template controls its own type surface)
Drops two latent bugs the old templates carried:
- client.nationality was read as a property but the schema field is
nationalityIso — old PDFs always showed "—" for nationality
- interest.notes was read but the interests table doesn't have a
notes column (interest_berths does) — old PDFs always showed "No
notes"
Both fields are now sourced correctly (or omitted) in the new templates.
Old pdfme files deleted (3 templates). API routes that import
exportClientPdf/exportBerthPdf/exportInterestPdf unchanged.
Tests:
tests/unit/record-export-templates.test.tsx (4 tests): each template
renders to valid PDF bytes with representative data, plus a minimal-
input path for the berth spec.
1317/1317 vitest green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>