Files
atomic-design-poc/docs/backlog/WP-25-letter-preview-html.md
Edwin van den Houdt 5a610c10f0 feat(fp): WP-23 — org-template backend + admin role
Second template axis (org identity: letterhead, footer, signature,
margins) server-side: OrgTemplateStore with JSON version history,
publish/rollback, sent-brief version pinning, admin role + capability,
5 admin endpoints, org-logo upload category. FE seam widened only
(Role/Capability unions, interceptor); WP-24/26 consume it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 11:17:05 +02:00

4.8 KiB

WP-25 — Server-rendered letter preview (HTML; PDF seam deferred)

Status: todo 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.csArchivedHtml + 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<string, Blob>)
  • src/app/brief/application/brief.store.tspreviewLetter() 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

  • Preview opens the composed print-ready letter in a new tab; browser print shows correct margins via @page.
  • Unresolved manual placeholders render [NOG IN TE VULLEN: …]; preview works despite lint errors (only send blocks).
  • A sent brief serves its archived HTML unchanged after an org-template republish.
  • Golden + parity tests green without any browser installed.
  • 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.