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>
This commit is contained in:
2026-07-05 11:17:05 +02:00
parent 44eb2d2186
commit 5a610c10f0
31 changed files with 5017 additions and 1834 deletions

View File

@@ -0,0 +1,90 @@
# 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.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<string, Blob>`)
- `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
- [ ] 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.