You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
As a homeowner sending a report to my bank, I want the cover letter to look like a proper business letter — formatted body text, a real signature block, and my own name and address as sender so that the document I hand over reads as professional correspondence rather than a dump of plain text.
Priority: Should Have Parent Epic: none — part of the parent-less Bank Report Wizard cluster (#1876–#1879, #1898–#1901, #1923–#1925). Found during: user review of generated report PDFs (2026-08-01).
Problem
The cover letter is the first page a bank employee reads. Today it is five unstyled text blocks stacked with 20pt gaps, and it looks unfinished.
Actual
1. The body is a single unformatted text run.
client/src/lib/reportPdf/coverLetterPdf.ts L~57 emits the whole body as one pdfmake text node:
and client/src/components/reports/ReportContentEditor.tsx L~117–130 edits it as a bare <textarea>. There is no way to produce a bold phrase, a paragraph break that survives rendering, or a bulleted list — which is exactly what a letter enumerating a claim needs. No markdown or rich-text library exists in client/package.json today (dependencies: @cornerstone/shared, @floating-ui/react, i18next, konva, pdf-lib, pdfmake, react, react-dom, react-i18next, react-konva, react-router-dom).
2. There is no signature field.
client/src/lib/reportContent/buildReportContent.ts L~262 derives the signature implicitly:
constsignature=sender.split('\n')[0]?.trim()??'';
— i.e. the first line of the sender block, which is the household name. It is not exposed as an editable field in ReportContentEditor (sender, recipient, reference, subject and body are; signature is not). The PDF then prints that string 40pt below the body with no closing salutation and no signature space (coverLetterPdf.ts L~63–69).
This was accepted as a documented deviation during the #1909 review ("signature derived from sender"). The user has now seen the output and rejected it. That earlier acceptance is superseded.
Only HouseholdSettings.householdName + householdAddress (shared/src/types/settings.ts L8–9). The logged-in user's own name is never used, even though useAuth().user.displayName (UserResponse, via client/src/contexts/AuthContext.tsx) is available client-side. A bank letter is signed by a person.
4. Overall layout is not a letter.
Sender, recipient, date, reference, subject and body are six equally-weighted blocks separated by identical 20pt margins. There is no visual hierarchy: the subject does not read as a subject, the date does not sit where a date sits, and nothing distinguishes the letter head from the letter body.
5. The reset "X" is an oversized visual glitch.
client/src/components/EditableField/EditableField.module.css: .container is display: flex; flex-direction: column, and the reset button is a direct child of .container (outside .fieldWrapper), so it stretches to full container width. Combined with:
the SVG scales to fill the entire 44px-plus box, producing a huge "X" beneath the field the moment any field is edited. The 44px hit area is correct and must be kept; the glyph should not be 44px.
Acceptance Criteria
Body formatting — plain text with preserved line breaks
Scope ruling (user, 2026-08-02): "no full wysiwyg necessary - just a simple text body with line breaks". Plain text only. No markdown, no rich-text editor, no new client dependency. Bold phrases and bulleted lists are out of scope despite being cited as motivation in the Problem section above. See the scope-decision comment on this issue.
Premise correction: pdfmake already honours \n as a required line end (node_modules/pdfmake/js/TextBreaker.js L30–34, L53–58) — the existing sender block depends on it. The body's line-break round trip therefore already works and is merely unpinned. ACs 1.1–1.3 are regression guards, not a feature build; AC 1.6 is the only new behaviour in this section.
1.1 Given a cover-letter body containing several paragraphs separated by blank lines, When the step-5 editor renders it, Then the textarea shows the paragraph structure the user typed, unaltered — no collapsing, trimming, or normalisation of internal whitespace on the round trip through the override store.
1.2 Given the same multi-paragraph body, When the PDF is exported, Then every line break and blank line the user typed is present in the rendered PDF, in the same order and at the same positions — asserted against a real render (pdfmake's resolved layout), not merely against the string content of the emitted text node.
1.3 Given a body containing characters that look like markup (**, - , #, <b>), When preview and PDF render, Then those characters appear literally and unchanged — the body is emitted as text and is never parsed as markup or HTML, and no markdown or rich-text dependency is added to client/package.json.
1.4Given the user is editing the body, When they look at the editor, Then the supported formatting is discoverable — STRUCK AS VACUOUS. There is no markup to discover. Replaced by: the body editor must not advertise, hint at, or imply formatting support, and the textarea must be tall enough to show a multi-paragraph letter's structure without the user scrolling to find it (currently rows={6}).
1.5VACUOUS BY CONSTRUCTION — retained as a negative constraint. No script execution or HTML injection is possible because the body is only ever rendered into a <textarea> value and a pdfmake text node. The criterion is therefore: no dangerouslySetInnerHTML, no HTML parsing, and no markup interpretation is introduced anywhere on the cover-letter body path.
1.6 Given the AI enhancement path, When it returns a cover-letter body, Then that body contains no markup — the prompt (server/src/services/budgetExtraction/prompts.ts, the "Letter body" instruction, L~142, which today says nothing about output format) explicitly requires plain prose with paragraphs separated by blank lines and forbids markdown, bullet characters, and HTML — so no literal **, - , or tag characters reach the exported PDF. This is the load-bearing criterion in this section.
Signature
2.1 Given step 5 with a cover letter enabled, When the editor renders, Then there is an explicit, editable signature field, presented like the other editable letter fields (visible label, edited indicator, per-field reset).
2.2 Given the user has not edited the signature, When the letter is generated, Then the signature is pre-populated with a sensible default derived from the user's identity (see AC 3.1), not silently taken from the first line of the sender block.
2.3 Given the user edits the signature, When the PDF is exported, Then the edited value appears in the PDF, and resetting the field restores the generated default.
2.4 Given any cover letter, When the PDF is exported, Then the signature renders as a signature block — a closing element, blank space for a handwritten signature, and the signatory name — not as a bare line of text floating below the body.
2.6 Given the user has explicitly edited the signature, When they then also edit the sender, Then the sender edit does not silently overwrite the signature. applyOverrides.ts L66–68 currently recomputes signature from an overridden sender (result.coverLetter.sender.split('\n')[0]), and types.ts L44 documents signature as DERIVED; both must be reconciled with signature becoming a first-class editable field, and the realRender assertion that pins the old recompute ("overriding coverLetter.sender changes the rendered signature too", realRender.test.ts L997) must be updated to the new intended behaviour, not deleted. (Added during scope reconciliation: this coupling between §2 and §3 exists in the code today and would otherwise surface as a surprise at review.)
Sender
3.1 Given a logged-in user with a display name and a household with a configured address, When a cover letter is generated, Then the sender block contains the user's display name and the household address.
3.2 Given a user with no display name, or a household with no address, or both missing, When a cover letter is generated, Then the sender block degrades gracefully — it contains whatever identity information is available, with no blank lines, no placeholder text, and no crash.
3.3 Given the generated sender, When the user edits the sender field, Then the edit is respected end-to-end into the PDF and can be reset (existing override behaviour preserved).
3.4 Given the cover-letter availability rule, When a source has neither a contact address nor a reference, Then the cover letter remains unavailable with its existing disabled hint — this issue does not change when the cover letter is offered.
Letter layout
4.1 Given a generated cover letter, When the PDF is exported, Then it is laid out as a business letter: a sender block, a recipient block, a date, an optional reference, a subject that is visually identifiable as the subject, the body, and the signature block — in a conventional order with deliberate (not uniform) spacing between them. Amended for the plain-text ruling: this AC now also owns paragraph separation within the body. With no formatting markup to carry paragraph semantics, a blank line in the body currently renders as an empty line at full body line-height; whether that is acceptable or whether the letter layout applies typographic paragraph spacing instead is a §4 layout decision (ux-designer, per AC 4.4) — it is no longer a §1 concern.
4.2 Given the same letter, When the step-5 preview renders, Then the preview conveys the same structure and reading order as the PDF, so the user can predict the exported result.
4.3 Given a cover letter of any length, When the PDF is exported, Then no letter element is clipped or overlaps another, and the existing page break between the cover letter and the report table is preserved.
4.4 The letter layout is specified by ux-designer before implementation, and all CSS values use design tokens (stylelint clean).
4.5 Given the report language differs from the interface language, When the letter renders in preview and PDF, Then every string that is part of the letter artifact (labels, closing, subject caption, date) is in the report language, and only edit affordances remain in the interface language.
Reset button
5.1 Given any edited EditableField anywhere in the application, When the reset control renders, Then its glyph is proportionate to the surrounding text and does not visually dominate the field.
5.2 Given the same control, When measured, Then its interactive target still meets the 44×44px minimum touch-target requirement.
5.3 Given the same control, When it renders, Then it does not stretch to the full width of its container and is positioned as a per-field affordance adjacent to its field.
5.4 Given keyboard navigation, When the reset control receives focus, Then a visible focus indicator meeting WCAG AA is present, in both light and dark mode.
5.5 Given the existing accessible name and title (resetFieldAriaLabel with the field name interpolated) and the edited-state dot and screen-reader hint, When the control is changed, Then all of those behaviours are preserved unchanged.
5.6 The fix applies to the shared EditableField component, so every consumer benefits — no per-call-site override.
Unaffected by the plain-text ruling.#1925 is about the date caption in the letter head (interface-language label beside a report-language value) and about caption chrome styling. Neither depends on the body-formatting model. All four ACs carry forward verbatim. 6.1's "per the resolution chosen in the new letter layout" resolves under §4 exactly as before.
6.1 Given interface language de and report language en (and the mirror case), When the step-5 preview renders the cover letter, Then no single rendered line mixes the two languages — the date caption and the date value are consistent per the resolution chosen in the new letter layout.
6.2 Given any interface/report language combination, When the cover letter preview renders, Then any non-editable caption is visually distinguishable as editor chrome, consistent with the other captions in the same panel.
6.3content.coverLetter.dateLine remains report-language-formatted via reportFormatters.formatDate — this must not regress.
6.4 Locale parity holds for every cover-letter key touched: present in both en and de, or removed from both.
Tests
7.1 Unit tests cover the line-break round trip for a multi-paragraph body (editor → override store → pdfmake node), the signature field's default / edit / reset cycle including the AC 2.6 sender-edit interaction, sender composition including every degraded case in AC 3.2, and the reset-button sizing/target constraints.
7.2 A real-render assertion in client/src/lib/reportPdf/realRender.test.ts pins the exported letter's structure, its report-language strings, and — per AC 1.2 — that a multi-paragraph body's line breaks and blank lines survive into the rendered output. This pin is the guard against a future change reflowing the body into per-token inline runs (the technique used elsewhere in reportPdf/ for pdfmake's all-or-nothing wordBreak), which would silently destroy \n handling.
7.3 E2E coverage exercises editing the signature and a multi-paragraph body in step 5 and exporting, at desktop and mobile viewports, in both light and dark mode.
7.4 A test asserts the AC 1.6 prompt constraint — the cover-letter body instruction in prompts.ts requires plain prose and forbids markup — in the same style as the existing prompts.test.ts regression guards.
Needs a design decision before implementation.RESOLVED by user scope decision, 2026-08-02 — see the scope-decision comment on this issue. The body-formatting mechanism is not an architect or UX call any more: the user ruled "no full wysiwyg necessary - just a simple text body with line breaks". Plain text, no markdown, no rich-text editor, no new client dependency, bold/lists out of scope. ux-designer still owns the letter layout (AC 4.4), including the paragraph-spacing question §4.1 now absorbs.
PDF_STYLES is defined in pageGeometry.ts, not merge.ts. merge.ts imports and re-exports it, so existing import { PDF_STYLES } from './merge.js' call sites still work — but any new letter styles this issue adds must be added in pageGeometry.ts, and pageGeometry.ts must never import from merge.ts (circular import; a directional comment in the file header states this — keep it accurate).
HEADER_ROW_HEIGHT is renamed HEADER_ROW_HEIGHT_MAX and is a deliberately conservative ceiling (68pt exported vs. 45.81pt measured, a 48% gap). Do not use it as an estimate for any page-fill arithmetic. In practice the cover letter ends with a hard pageBreak: 'after' (coverLetterPdf.ts) so it never shares a page with the table, and this issue should need the constant at all — if the implementation reaches for it, that is a signal the letter-layout approach has drifted.
WORST_CASE_CHAR_ADVANCE_EM stays at 1.04 with a scoped comment. Do not re-derive or adjust it.
Files: client/src/lib/reportContent/buildReportContent.ts (~L252–276), client/src/lib/reportPdf/coverLetterPdf.ts, client/src/components/reports/ReportContentEditor.tsx, client/src/components/EditableField/EditableField.tsx + .module.css, client/src/i18n/{en,de}/budget.json, plus the ReportContent cover-letter type and its override keys.
Supersedes the feat(reports): editable HTML report preview with on-demand PDF export #1909 "signature derived from sender" acceptance. That deviation was accepted at review time on the reasoning that the sender's first line was an adequate signatory; user feedback has overturned it. Record the reversal rather than re-litigating it.
Interacts with the PDF-layout issue in this batch (page margins, running header, page breaks). If both are in flight, the letter-layout work must not reintroduce clipping and must be re-verified against that issue's header/margin ACs.
Scope guard: this issue does not add letterhead/logo upload, does not add stored letter templates, does not change which report types offer a cover letter, and does not change the report table.
[product-owner]
As a homeowner sending a report to my bank, I want the cover letter to look like a proper business letter — formatted body text, a real signature block, and my own name and address as sender so that the document I hand over reads as professional correspondence rather than a dump of plain text.
Priority: Should Have
Parent Epic: none — part of the parent-less Bank Report Wizard cluster (#1876–#1879, #1898–#1901, #1923–#1925).
Found during: user review of generated report PDFs (2026-08-01).
Problem
The cover letter is the first page a bank employee reads. Today it is five unstyled text blocks stacked with 20pt gaps, and it looks unfinished.
Actual
1. The body is a single unformatted text run.
client/src/lib/reportPdf/coverLetterPdf.tsL~57 emits the whole body as one pdfmake text node:and
client/src/components/reports/ReportContentEditor.tsxL~117–130 edits it as a bare<textarea>. There is no way to produce a bold phrase, a paragraph break that survives rendering, or a bulleted list — which is exactly what a letter enumerating a claim needs. No markdown or rich-text library exists inclient/package.jsontoday (dependencies:@cornerstone/shared,@floating-ui/react,i18next,konva,pdf-lib,pdfmake,react,react-dom,react-i18next,react-konva,react-router-dom).2. There is no signature field.
client/src/lib/reportContent/buildReportContent.tsL~262 derives the signature implicitly:— i.e. the first line of the sender block, which is the household name. It is not exposed as an editable field in
ReportContentEditor(sender, recipient, reference, subject and body are; signature is not). The PDF then prints that string 40pt below the body with no closing salutation and no signature space (coverLetterPdf.tsL~63–69).This was accepted as a documented deviation during the #1909 review ("signature derived from sender"). The user has now seen the output and rejected it. That earlier acceptance is superseded.
3. The sender is the household, not the person.
buildReportContent.tsL~257–260:Only
HouseholdSettings.householdName+householdAddress(shared/src/types/settings.tsL8–9). The logged-in user's own name is never used, even thoughuseAuth().user.displayName(UserResponse, viaclient/src/contexts/AuthContext.tsx) is available client-side. A bank letter is signed by a person.4. Overall layout is not a letter.
Sender, recipient, date, reference, subject and body are six equally-weighted blocks separated by identical 20pt margins. There is no visual hierarchy: the subject does not read as a subject, the date does not sit where a date sits, and nothing distinguishes the letter head from the letter body.
5. The reset "X" is an oversized visual glitch.
client/src/components/EditableField/EditableField.module.css:.containerisdisplay: flex; flex-direction: column, and the reset button is a direct child of.container(outside.fieldWrapper), so it stretches to full container width. Combined with:the SVG scales to fill the entire 44px-plus box, producing a huge "X" beneath the field the moment any field is edited. The 44px hit area is correct and must be kept; the glyph should not be 44px.
Acceptance Criteria
Body formatting — plain text with preserved line breaks
**,-,#,<b>), When preview and PDF render, Then those characters appear literally and unchanged — the body is emitted as text and is never parsed as markup or HTML, and no markdown or rich-text dependency is added toclient/package.json.Given the user is editing the body, When they look at the editor, Then the supported formatting is discoverable— STRUCK AS VACUOUS. There is no markup to discover. Replaced by: the body editor must not advertise, hint at, or imply formatting support, and the textarea must be tall enough to show a multi-paragraph letter's structure without the user scrolling to find it (currentlyrows={6}).<textarea>value and a pdfmake text node. The criterion is therefore: nodangerouslySetInnerHTML, no HTML parsing, and no markup interpretation is introduced anywhere on the cover-letter body path.server/src/services/budgetExtraction/prompts.ts, the "Letter body" instruction, L~142, which today says nothing about output format) explicitly requires plain prose with paragraphs separated by blank lines and forbids markdown, bullet characters, and HTML — so no literal**,-, or tag characters reach the exported PDF. This is the load-bearing criterion in this section.Signature
reportT), never through the editor's interfacet— per the artifact-content-vs-edit-affordance rule established in feat(reports): editable HTML report preview with on-demand PDF export #1909 and reinforced in feat(reports): report table cleanup — shared footnotes, deposit labels, claim metadata, total-only summary, usage area (#1923) #1924.applyOverrides.tsL66–68 currently recomputessignaturefrom an overridden sender (result.coverLetter.sender.split('\n')[0]), andtypes.tsL44 documentssignatureasDERIVED; both must be reconciled with signature becoming a first-class editable field, and the realRender assertion that pins the old recompute ("overriding coverLetter.sender changes the rendered signature too",realRender.test.tsL997) must be updated to the new intended behaviour, not deleted. (Added during scope reconciliation: this coupling between §2 and §3 exists in the code today and would otherwise surface as a surprise at review.)Sender
Letter layout
ux-designer, per AC 4.4) — it is no longer a §1 concern.ux-designerbefore implementation, and all CSS values use design tokens (stylelint clean).Reset button
EditableFieldanywhere in the application, When the reset control renders, Then its glyph is proportionate to the surrounding text and does not visually dominate the field.resetFieldAriaLabelwith the field name interpolated) and the edited-state dot and screen-reader hint, When the control is changed, Then all of those behaviours are preserved unchanged.EditableFieldcomponent, so every consumer benefits — no per-call-site override.Folded-in duplicate: #1925
deand report languageen(and the mirror case), When the step-5 preview renders the cover letter, Then no single rendered line mixes the two languages — the date caption and the date value are consistent per the resolution chosen in the new letter layout.content.coverLetter.dateLineremains report-language-formatted viareportFormatters.formatDate— this must not regress.enandde, or removed from both.Tests
client/src/lib/reportPdf/realRender.test.tspins the exported letter's structure, its report-language strings, and — per AC 1.2 — that a multi-paragraph body's line breaks and blank lines survive into the rendered output. This pin is the guard against a future change reflowing the body into per-token inline runs (the technique used elsewhere inreportPdf/for pdfmake's all-or-nothingwordBreak), which would silently destroy\nhandling.prompts.tsrequires plain prose and forbids markup — in the same style as the existingprompts.test.tsregression guards.Notes
Needs a design decision before implementation.RESOLVED by user scope decision, 2026-08-02 — see the scope-decision comment on this issue. The body-formatting mechanism is not an architect or UX call any more: the user ruled "no full wysiwyg necessary - just a simple text body with line breaks". Plain text, no markdown, no rich-text editor, no new client dependency, bold/lists out of scope.ux-designerstill owns the letter layout (AC 4.4), including the paragraph-spacing question §4.1 now absorbs.client/src/lib/reportPdf/— read before starting. This issue wasblocked-byreportPdf geometry hygiene: bounds that name their own scope (HEADER_ROW_HEIGHT_MAX, char-advance scope, channel enumeration, PDF_STYLES relocation) #1939 (PR refactor(reports): make reportPdf geometry bounds name their own scope (#1939) #1948), which lands first specifically so Cover letter overhaul: formatted body, editable signature block, personal sender, professional layout #1932 cannot misread the geometry layer. After it merges:PDF_STYLESis defined inpageGeometry.ts, notmerge.ts.merge.tsimports and re-exports it, so existingimport { PDF_STYLES } from './merge.js'call sites still work — but any new letter styles this issue adds must be added inpageGeometry.ts, andpageGeometry.tsmust never import frommerge.ts(circular import; a directional comment in the file header states this — keep it accurate).HEADER_ROW_HEIGHTis renamedHEADER_ROW_HEIGHT_MAXand is a deliberately conservative ceiling (68pt exported vs. 45.81pt measured, a 48% gap). Do not use it as an estimate for any page-fill arithmetic. In practice the cover letter ends with a hardpageBreak: 'after'(coverLetterPdf.ts) so it never shares a page with the table, and this issue should need the constant at all — if the implementation reaches for it, that is a signal the letter-layout approach has drifted.WORST_CASE_CHAR_ADVANCE_EMstays at 1.04 with a scoped comment. Do not re-derive or adjust it.overviewPdf.tsand theHEADER_ROW_HEIGHT_MAXbasis. Cover letter overhaul: formatted body, editable signature block, personal sender, professional layout #1932 should not touchoverviewPdf.tsat all; if it does, reconcile with whichever of the two lands second.client/src/lib/reportContent/buildReportContent.ts(~L252–276),client/src/lib/reportPdf/coverLetterPdf.ts,client/src/components/reports/ReportContentEditor.tsx,client/src/components/EditableField/EditableField.tsx+.module.css,client/src/i18n/{en,de}/budget.json, plus theReportContentcover-letter type and its override keys.