# WP-25 — Server-rendered letter preview (HTML; PDF seam deferred) Status: done Phase: 6 — Brief v2 (edit-on-the-letter, org templates, server-rendered preview) ## Why "What you compose is what is sent" needs a server-side rendering of the letter — placeholders resolved, org template applied — from the same CSS contract the canvas uses (PRD §2b: one rendering, used twice). The preview is the artifact: at send, the same composition is archived with the brief, making sent letters immutable. ## Read first - PRD Brief v2 §2b, §8; `docs/backlog/WP-24-letter-canvas.md` (the `letter.css` contract) - `backend/src/BigRegister.Api/Program.cs` — upload `content` endpoint (binary house pattern: `.ExcludeFromDescription()` + hand-written FE fetch) - `src/app/shared/upload/upload.adapter.ts` (hand-written transport precedent) ## Decisions (pre-made, don't relitigate) - **HTML, not PDF** (user decision at plan review): no Microsoft.Playwright/Chromium dependency in the POC. `GET /api/v1/brief/preview` returns `text/html` — the fully composed, print-ready letter (`@page` CSS; browser print-to-PDF is the manual affordance). The endpoint is the seam where a headless-Chromium PDF render slots in later; mark it `// ponytail: HTML today, Chromium PDF behind this same route if the POC ever needs real PDF bytes`. - **`LetterHtml.Render(brief, orgTemplate)`** is a pure static composer: mirrors the canvas class vocabulary exactly, inlines `public/letter.css` from disk, inlines the logo bytes as a data-URI. Placeholders: auto-resolvable keys resolve from seed/case data; unresolved manual keys render as `[NOG IN TE VULLEN: label]` (PRD §8) — preview is allowed with errors, only send blocks on them. - **Parity is tested, not hoped for**: a golden-file test snapshots the composed HTML; a second test asserts every `letter`-prefixed class in the golden HTML exists in `letter.css`. `dotnet test` never launches a browser. - **Archive at send**: `Send` stores the composed HTML in `BriefEntity.ArchivedHtml` (SQLite text column) alongside the WP-23 version pin; the preview endpoint serves the archive when status is `sent`, so a republish never changes a sent letter. - **Two endpoints, both excluded from OpenAPI** (JSON-only generated client stays clean): `GET /brief/preview` and `GET /admin/org-template/{subOrgId}/preview` (proefbrief: draft template + a fixture brief). FE consumes them via a small hand-written fetch (needs the `X-Role` header) → blob → object URL in a new tab. - Watermark: previews of unsent letters carry a `VOORBEELD` watermark (CSS), the archived/sent rendering never does — the PRD's open question resolved the simple way. ## Files - `backend/src/BigRegister.Api/Domain/Letters/LetterHtml.cs` (new) - `backend/src/BigRegister.Api/Data/BriefStore.cs` — `ArchivedHtml` + archive at send (+migration) - `backend/src/BigRegister.Api/Program.cs` — 2 preview endpoints - `backend/tests/BigRegister.Tests/LetterHtmlTests.cs` (new) + `LetterHtml.golden.html` - `src/app/brief/infrastructure/letter-preview.adapter.ts` (new, fetch → `Result`) - `src/app/brief/application/brief.store.ts` — `previewLetter()` command - `src/app/brief/ui/letter-composer/*` — "Voorbeeld" button ## Steps 1. `LetterHtml.Render` + placeholder resolution + data-URI logo + watermark flag. 2. Golden-file + class-parity tests. 3. Endpoints (serve archive when sent; proefbrief renders the draft template). 4. Archive-at-send in `BriefStore.Send` (+ migration for `ArchivedHtml`). 5. FE adapter + store command + button (explicit action — no live re-render; PRD §8). ## Acceptance criteria - [x] Preview opens the composed print-ready letter in a new tab; browser print shows correct margins via `@page`. - [x] Unresolved manual placeholders render `[NOG IN TE VULLEN: …]`; preview works despite lint errors (only send blocks). - [x] A sent brief serves its archived HTML unchanged after an org-template republish. - [x] Golden + parity tests green without any browser installed. - [x] `swagger.json` unchanged by the two endpoints (drift check green). ## Verification `cd backend && dotnet test`; GREEN one-liner; manual: compose → preview → print dialog; send → republish template → preview still the archived rendering. ## Out of scope Real PDF bytes / headless Chromium (the deliberate deferral — the endpoint is the seam). Pixel-parity testing (the shared CSS + class-parity test is the fence). Pagination fidelity beyond the browser's own print engine. ## Risks `LetterHtml` reads `public/letter.css` from disk — path must resolve for `dotnet run`, tests, and docker (bind mount/copy); fail loudly with a clear error if missing. The golden file will churn whenever the letter structure changes — that is its job; update it deliberately, never blindly.