Adds `POST /api/doctor-report/pdf` that returns a fully rendered PDF (`Content-Type: application/pdf`) using the same jsPDF document the web client builds locally, just executed server-side. Locale resolves from explicit body param → `Accept-Language` → `de` fallback. The existing `POST /api/doctor-report` JSON endpoint is unchanged; both endpoints share `collectDoctorReportData()` so any future change to the aggregation lands in one place. The render core (`doctor-report-pdf-core`) is isomorphic — same code paths run in browser jsPDF and Node jsPDF. Rate-limit (10/h per user) is shared between the two endpoints so the quota cannot be doubled by mixing JSON and PDF requests. Tests: 6 core tests (PDF header bytes, size, determinism, DE+EN, empty data, jsPDF doc shape) + 5 route tests (401, 429, 200 happy-path with content-type/disposition, default `days=90`, Accept-Language fallback). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2.7 KiB
Doctor Report
The doctor report is a printable PDF summary of a user's tracked vitals, medication compliance, glucose context, and mood over a configurable window (default 90 days, max 365). It is intended as discussion material for a doctor's appointment, not a medical diagnosis.
Two endpoints
The same aggregation logic backs both endpoints — collectDoctorReportData
in src/lib/doctor-report-data.ts is the single source of truth — so the
JSON payload and the server-rendered PDF are guaranteed to be in sync.
POST /api/doctor-report — JSON payload
Returns the aggregated DoctorReportData envelope. Used by the PWA's
client-side renderer (src/lib/doctor-report-pdf.ts, jsPDF + autotable).
Best on desktop browsers and on Android, where local generation keeps the
PDF off the wire and works offline (cached page).
- Auth: session required.
- Rate-limit: 10/h per user.
- Body:
{ days?: number }(default 90). - Response:
200 application/jsonwith envelope{ data, error: null }. - Audit action:
doctor-report.generate.
POST /api/doctor-report/pdf — server-rendered PDF
Returns the ready-to-download PDF bytes. Used primarily on iOS/Safari, where the client-side jsPDF download UX is unreliable (Safari sometimes opens the PDF in a new tab without preserving the filename or fails the blob save).
- Auth: session required (
401otherwise). - Rate-limit: 10/h per user, shared with the JSON endpoint (
429on excess). - Body:
{ days?: number; locale?: "de" | "en" }. Body is optional — empty bodies are accepted. - Locale resolution: explicit
body.localewins, then theAccept-Languageheader (de*→de, anything else →en), then the hard fallbackde. - Response:
200 application/pdfwithContent-Disposition: attachment; filename="healthlog-report-YYYY-MM-DD.pdf"andCache-Control: no-store. Body is the raw PDF bytes. - Errors: JSON envelope
{ data: null, error: "..." }for401and429;500if rendering fails. - Audit action:
doctor-report.pdf.generate.
Implementation notes
src/lib/doctor-report-pdf-core.tscontains the isomorphic renderer. jsPDF runs unchanged in Node —doc.output("arraybuffer")returns a valid%PDF-byte stream in both environments, so we deliberately avoid headless Chromium (Puppeteer/Playwright) to keep the runtime dependency surface low.src/lib/doctor-report-pdf.tsis a thin client façade that returns the rawjsPDFinstance so the existing settings-page download flow (which callsdoc.save(...)) keeps working unchanged.- Server-side translations come from
messages/{de,en}.jsonviagetServerTranslator()(src/lib/i18n/server-translator.ts).