@medicus.ai/medicus-report-pdf-generator 1.3.13 → 1.3.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,16 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(awk 'NR==55{print substr\\($0,1,300\\)}' lib/sanusx_report_generator.js)",
5
+ "Bash(awk 'NR==44{print substr\\($0,1,300\\)}' lib/sanusx_report_generator.js)",
6
+ "Bash(awk 'NR==46{print substr\\($0,1,300\\)}' lib/sanusx_report_generator.js)",
7
+ "Bash(node -e \"console.log\\(JSON.stringify\\(require\\('./config/default.json'\\).corporate_report, null, 2\\)\\)\")",
8
+ "Bash(node -e \"console.log\\(JSON.stringify\\(require\\('./config/sanusx.json'\\), null, 2\\)\\)\")",
9
+ "Bash(node -e ' *)",
10
+ "Bash(ls -la docs)",
11
+ "Bash(ls *.md)",
12
+ "Bash(head -c 500 \"data converter.js\")",
13
+ "Bash(head -c 500 example.txt)"
14
+ ]
15
+ }
16
+ }
package/HANDOVER.md ADDED
@@ -0,0 +1,146 @@
1
+ # Handover: Medicus PDF Generator
2
+
3
+ **Audience:** software engineers taking over this project who are not frontend specialists. You don't need deep CSS/HTML expertise to maintain this — you need to understand it as a **data-in, PDF-out rendering pipeline** with some unusual, homegrown templating. This document is the fast path to being productive. Full details, with file:line-level citations, are in [`docs/`](docs/README.md) — this file tells you what matters most and when to go read which doc.
4
+
5
+ ---
6
+
7
+ ## 1. What this thing actually is
8
+
9
+ `@medicus.ai/medicus-report-pdf-generator` is an **npm library**, not a service. There is no HTTP server, no listener, no routes anywhere in this repo. A host application (per the README, originally a Meteor app) `require()`s it and calls its exported functions directly — or, optionally, through `run.js`, which forks a child process for isolation.
10
+
11
+ It has:
12
+ - **No authentication or authorization.** No JWT, no sessions, no API keys — check `package.json`, there's nothing auth-related in the dependency list. Whatever calls this library is trusted completely; access control is 100% the host application's job.
13
+ - **No database.** Every call is stateless: JSON in, PDF/HTML/base64 out. The only persistent side effects are files written to `output/` or the OS temp directory (see §6).
14
+ - **Four distinct report "products"**, each with its own generator file and its own quirks (see §4). They are not built on a shared rendering engine — each independently reimplements the same "read HTML fragment from disk, do string replacement, build a DOM with jsdom+jQuery, hand to Puppeteer" pattern.
15
+
16
+ If you remember one mental model: **this package is a fancy `data.json + template.html → report.pdf` compiler, called once per report, with no state carried between calls** (aside from a few shared-singleton gotchas noted below).
17
+
18
+ ---
19
+
20
+ ## 2. The one fact that will save you the most time
21
+
22
+ **`index.js` requires `lib/pdf_generator.min.js` — not `lib/pdf_generator.js`.**
23
+
24
+ These look like a source/minified-build pair. They are not. There's no build script anywhere in this repo that produces one from the other (no webpack config exists, despite `webpack` sitting in `devDependencies`). Git history proves it: `pdf_generator.js` was last edited in **March 2021**; `pdf_generator.min.js` was last edited in **July 2025** — completely independent edit histories, over four years apart.
25
+
26
+ **If you're asked to fix a bug in the core "Medicus" lab report and you edit `lib/pdf_generator.js`, your fix will silently not ship.** Edit `lib/pdf_generator.min.js` instead. This is the single highest-value thing to internalize before touching this codebase. Full detail: [`docs/01-architecture-overview.md §1.4`](docs/01-architecture-overview.md#14-critical-fact-the-core-report-renderer-that-actually-runs-is-pdf_generatorminjs-not-pdf_generatorjs).
27
+
28
+ (Longer-term: either delete `pdf_generator.js` and rename `.min.js` to something honest, or wire up a real build step. Either fixes this permanently.)
29
+
30
+ ---
31
+
32
+ ## 3. How data flows in
33
+
34
+ Every report-generating export in `index.js` follows the same shape: the caller passes a JSON *string* (or, for the core report, a raw base64 string) whose real content — patient data, biomarkers, scores, questions, whatever — is **base64-encoded** inside a `data` field, alongside plain-text sibling fields like `client`, `language`, and (if emailing) SMTP config.
35
+
36
+ ```js
37
+ {
38
+ "data": "<base64 of the actual report JSON>",
39
+ "client": "Mediclinic",
40
+ "language": "en",
41
+ "host": "smtp...", "port": 587, "authUser": "...", "authPass": "...", // only if emailing
42
+ "pdfPassword": "..." // generateFullPdf only — output gets encrypted
43
+ }
44
+ ```
45
+
46
+ **Base64 is encoding, not security.** Anyone who can see a request to this library (including anyone reading a debug log) can trivially recover everything inside it, including SMTP credentials riding alongside the report data. See §7.
47
+
48
+ ---
49
+
50
+ ## 4. The four report pipelines — know which one you're in before debugging
51
+
52
+ - **Core "Medicus"** — module `lib/pdf_generator.min.js`. Clinical lab report: biomarkers, panels, reference ranges, doctor notes, insights. No questionnaires. Entry point: `generateMedicusPDF`.
53
+ - **Wellbeing** — module `lib/wellbeing_report_generator.js` (+ `lib/template.js`, `lib/big_integral_questionnaire.js`). Patient wellbeing/lifestyle report — questionnaire scores and Q&A, optionally merged with a full lab report ("SmartReport"). Entry points: `generateNascoPDF` (plain), `generateFullPdf` (branded + SmartReport + PDF encryption).
54
+ - **Corporate** — module `lib/corporate_report_generator.js`. Population-level analytics report for an employer/corporate client. Entry point: `generateCorporateReportPDF`.
55
+ - **SanusX** — module `lib/sanusx_report_generator.js`. Consumer "Health Hero" wellbeing report with an avatar/scoring gimmick. Entry point: `generateSanuxPDF`.
56
+
57
+ They genuinely don't share code — a fix in one does not apply to the others. Before debugging "the report looks wrong," identify which of these four you're actually looking at. Full detail per pipeline, including known bugs specific to each: [`docs/04-report-pipelines.md`](docs/04-report-pipelines.md).
58
+
59
+ ---
60
+
61
+ ## 5. Whitelabelling in one paragraph (and its landmines)
62
+
63
+ Branding is chosen by a plain string (`client`/`clientName`) in the request payload, which each generator maps — independently, and **inconsistently** — to a `config/{Brand}.json` file (colors, logos) and sometimes a `templates/{Brand}/` folder (full HTML layout). There is no environment variable and no central "client registry." Five different pieces of code (the wellbeing generator, the corporate generator, the email module, the sanusx generator, and the fact that the core report doesn't do this at all) each have their own rule for turning a client string into a filename, and they disagree on casing and on which strings get special-cased (only `"pha"` → `"Pha"` is handled specially, and only in some of them). One of these (`corporate_report_generator.js`) lower-cases the client string before building a path to a capitalized filename — this only works today because of case-insensitive filesystems; it will break on Linux. `sanusx_report_generator.js` ignores the client string for branding entirely. **Before onboarding a new brand, read [`docs/02-configuration-and-whitelabelling.md`](docs/02-configuration-and-whitelabelling.md) in full** — guessing "just add a config file" will not be enough.
64
+
65
+ ---
66
+
67
+ ## 6. Where do the questions come from? (the thing you specifically asked about)
68
+
69
+ **This library does not own or store questionnaire content.** There is no question bank, no question-ID lookup, no survey schema in this repository. Every question's text and its answer arrive **together, pre-rendered**, as flat `{title, value}` pairs inside the JSON payload — the host application (whatever system runs the actual questionnaire/survey with the patient) is responsible for assembling that array before calling this library.
70
+
71
+ Concretely, in the wellbeing pipeline:
72
+ ```js
73
+ "Profile": [
74
+ { "title": "What is your gender?", "value": "Male" },
75
+ { "title": "Over the last 2 weeks, how often have you been bothered by feeling down, depressed, or hopeless?",
76
+ "value": "More than half the days" }
77
+ // ...
78
+ ]
79
+ ```
80
+
81
+ This gets laid out as a two-column "Quiz Answers" table by `renderDoctorDetails()` in `lib/wellbeing_report_generator.js` — plain layout code, no interpretation of what the question means. Numeric scores (PHQ‑9 totals, etc.) are a *separate* field (`data.wellbeing.calculation`) that's also pre-computed upstream — this library doesn't score anything either. The core lab-report pipeline has no question concept at all (its domain is biomarkers, not questionnaires).
82
+
83
+ **One trap:** `lib/big_integral_questionnaire.js` looks like a real questionnaire-rendering feature but currently renders **100% hardcoded dummy data** — it is a design/preview scaffold, not wired to real patient answers. If someone asks why it doesn't reflect real data, that's expected, not a bug to "fix" quickly — it needs real wiring designed first.
84
+
85
+ Also worth flagging to whoever owns data quality upstream: the answer *values* rendered into this table are **not HTML-escaped**. If free-text ever flows into `Profile[].value` from a less-trusted source, that's an injection risk into the rendered page.
86
+
87
+ Full detail, including exactly which field goes where in which pipeline: [`docs/05-questions-and-data-model.md`](docs/05-questions-and-data-model.md).
88
+
89
+ ---
90
+
91
+ ## 7. Security posture — what to tell people who ask
92
+
93
+ - **No auth in this library** (see §1). Access control lives entirely in the host application.
94
+ - **SMTP credentials travel as plaintext JSON fields**, inside a payload that's only base64-encoded, not encrypted. This is the most concrete, fixable security gap: see [`docs/07-email-notifications.md §7.4`](docs/07-email-notifications.md#74-security-considerations). If asked to harden this, moving SMTP credentials to host-app-side environment configuration (rather than per-call payload fields) is the right direction.
95
+ - The `secure` flag callers pass for SMTP is silently ignored (dead code) — the transport actually always forces STARTTLS via `requireTLS: true`, so it's not literally insecure, just misleading.
96
+ - Unescaped answer text in the wellbeing Q&A table (§6) is a latent XSS-into-rendered-PDF risk if upstream data isn't sanitized.
97
+ - No secrets are hardcoded in the codebase itself (verified during this review).
98
+
99
+ ---
100
+
101
+ ## 8. Internationalization and RTL — the short version
102
+
103
+ Two separate, independently configured instances of the `i18n` npm package exist: one for the core report (`locales/`), one for **everything else** — wellbeing, corporate, *and* SanusX — (`locales/wellbeing/`, despite the folder name). If you're hunting for a translation string and the generator you're editing isn't the core lab report, look in `locales/wellbeing/`, not `locales/`.
104
+
105
+ RTL (Arabic) support works via three overlapping mechanisms (whole alternate template files, alternate block partials, inline CSS direction switches) and — important gotcha — three of the four generators forcibly rewrite any Arabic-ish language code to `ar-SA`, meaning `ar-AE` is effectively unreachable. SanusX's RTL is outright broken (never triggers). Locale state also lives on a shared, process-global singleton, so concurrent report-generation calls in the same process can theoretically leak one request's language into another's — the codebase already fixed an analogous bug for temp file paths but not for this. Full detail: [`docs/06-internationalization.md`](docs/06-internationalization.md).
106
+
107
+ ---
108
+
109
+ ## 9. Running/testing this locally
110
+
111
+ There's no formal test framework (no Jest/Mocha in `devDependencies`, `package.json`'s only script is `"start": "node index.js"`, which just requires the library — it does nothing on its own). What exists instead:
112
+
113
+ - **`test.js`** (root) — an ad hoc script that calls `generateHTMLWellbeingReportWithSmartReport`/`generatePDFReport` directly against fixture data from `testing-reports/` and writes to `output/nasco-sample.pdf` (and an encrypted copy via `node-qpdf2`). Swap which fixture variable is assigned to `data`/`client` at the top of the file to test a different brand/scenario.
114
+ - **`testing-reports/{mediclinic,pha}/*.js`** — example payloads (`doctor-data.js` has `IsDoctor: true` with a full PHQ‑9/GAD‑7-style `Profile` array — the best reference for the Q&A shape described in §6).
115
+ - **`preview-big-integral.js`** — standalone harness for the `big_integral_questionnaire.js` dummy-data table.
116
+ - **`tests/test.js`, `tests/test-qr-code.js`** — more ad hoc scripts; `test-qr-code.js` is the reference for how `generatePatientQR` expects a caller-pre-rendered HTML/SVG string.
117
+ - Puppeteer needs a compatible Chromium available; `downloadfile.js` is a one-off helper that fetches a pinned Chromium build for Windows if you need it locally.
118
+
119
+ There is no CI test suite validating rendered PDF output — verification today is manual (run a script, open the resulting PDF in `output/`).
120
+
121
+ ---
122
+
123
+ ## 10. If you're prioritizing what to fix first
124
+
125
+ In order of leverage for time invested:
126
+
127
+ 1. Resolve the `pdf_generator.js` vs `.min.js` divergence (§2) — either delete the stale file or add a real build step. Highest risk of silent, confusing bugs.
128
+ 2. Unify client-name → config/template resolution into one shared helper, used by all five places that currently reimplement it differently (§5).
129
+ 3. Fix the SMTP-credentials-in-plaintext-payload pattern (§7) — at minimum, stop it from ever landing in debug logs; ideally move it out of the payload entirely.
130
+ 4. Scope locale state per-request instead of a shared singleton (§8) — currently a latent concurrency bug.
131
+ 5. Everything else is cataloged, ranked, and cross-referenced in [`docs/08-known-issues-and-technical-debt.md`](docs/08-known-issues-and-technical-debt.md) — treat that file as your punch list.
132
+
133
+ ---
134
+
135
+ ## 11. Full documentation index
136
+
137
+ For anything beyond this summary, go to [`docs/README.md`](docs/README.md), which indexes:
138
+
139
+ 1. [Architecture Overview](docs/01-architecture-overview.md)
140
+ 2. [Configuration & Whitelabelling](docs/02-configuration-and-whitelabelling.md)
141
+ 3. [Templating & Rendering Pipeline](docs/03-templating-and-rendering.md)
142
+ 4. [Report Pipelines](docs/04-report-pipelines.md)
143
+ 5. [Questions, Answers & the Data Model](docs/05-questions-and-data-model.md)
144
+ 6. [Internationalization (i18n) & RTL](docs/06-internationalization.md)
145
+ 7. [Email Notifications](docs/07-email-notifications.md)
146
+ 8. [Known Issues & Technical Debt](docs/08-known-issues-and-technical-debt.md)
@@ -0,0 +1,149 @@
1
+ # 1. Architecture Overview
2
+
3
+ ## 1.1 What this package does
4
+
5
+ `@medicus.ai/medicus-report-pdf-generator` (`package.json:2`) converts JSON health-report data into PDF documents for several distinct products:
6
+
7
+ - **Core "Medicus" lab report** (biomarkers/panels/insights) — module `lib/pdf_generator.min.js`, called via `generateMedicusPDF`
8
+ - **Wellbeing report** (questionnaire scores, PHQ‑9/GAD‑7 style) + optional merged lab "SmartReport" — module `lib/wellbeing_report_generator.js`, called via `generateNascoPDF` and `generateFullPdf`
9
+ - **Corporate/aggregate analytics report** (population-level, for employers) — module `lib/corporate_report_generator.js`, called via `generateCorporateReportPDF`
10
+ - **SanusX consumer wellbeing report** ("Health Hero" avatar report) — module `lib/sanusx_report_generator.js`, called via `generateSanuxPDF`
11
+
12
+ All four are independent implementations. They do **not** share a common rendering module — each has its own copy of the "read HTML fragment from disk → string-replace `{{tokens}}` → load into jsdom → manipulate with jQuery → serialize → hand to Puppeteer" pattern. See [03-templating-and-rendering.md](03-templating-and-rendering.md).
13
+
14
+ ## 1.2 Directory map
15
+
16
+ ```
17
+ index.js Public API — the only file a host app requires
18
+ run.js Optional child-process wrapper around generateMedicusPDF
19
+ lib/
20
+ pdf_generator.js Core report renderer — SOURCE, but NOT what actually runs (see 1.4)
21
+ pdf_generator.min.js Core report renderer — the file index.js actually requires
22
+ wellbeing_report_generator.js Wellbeing + SmartReport renderer, plus PDF conversion for it
23
+ big_integral_questionnaire.js Renders one static "Big Integral Questionnaire" table (Maison Sante only)
24
+ corporate_report_generator.js Corporate/analytics report renderer
25
+ sanusx_report_generator.js SanusX report renderer
26
+ template.js Shared rendering helpers used ONLY by wellbeing_report_generator.js
27
+ (biomarker/panel/insight HTML builders, V3-suffixed functions)
28
+ sendEmail.js Nodemailer wrapper, used by the wellbeing/corporate/sanusx flows
29
+ app/
30
+ i18n.config.js i18n config for the core "Medicus" report (locales/)
31
+ i18n_wellbeing.config.js i18n config for wellbeing/corporate/sanusx (locales/wellbeing/)
32
+ services/localeService.js Thin wrapper exposing .t(key) / .setLocale(lang)
33
+ config/ One JSON per brand (whitelabelling) + default.json
34
+ templates/ HTML fragments — shared (templates/blocks) + per-brand folders
35
+ locales/ Translation JSON — main + locales/wellbeing subfolder
36
+ assets/ CSS/JS/fonts/images served into the rendered HTML, some per-brand
37
+ output/ Default write location for generated PDFs/HTML/logs (not cleaned up)
38
+ testing-reports/ Example payloads used by test.js / preview scripts
39
+ tests/ Ad hoc test scripts (not a real test runner/framework)
40
+ ```
41
+
42
+ ## 1.3 Entry points (`index.js`)
43
+
44
+ `index.js` exports a flat object of async functions. There is no class, no server, no routing — just functions a host app calls directly.
45
+
46
+ - **`generateHTMLStaging`** — pipeline: Core. Build HTML only (re-exported straight from `pdf_generator.min.js`).
47
+ - **`generateMedicusPDF(base64Object, isDebugging, isDownloadable, onlyHTML)`** — pipeline: Core. Full core lab report → PDF.
48
+ - **`generateNascoPDF(data, isDebugging, isDownloadable, shouldSendEmail)`** — pipeline: Wellbeing. Plain wellbeing report, optionally emailed.
49
+ - **`generateFullPdf(data, isDebugging, isDownloadable, shouldSendEmail, selectedLabs, showHeaderLogo)`** — pipeline: Wellbeing (extended). Branded wellbeing report + merged SmartReport + **encryption**.
50
+ - **`generateSanuxPDF(data, isDebugging, isDownloadable, shouldSendEmail)`** — pipeline: SanusX. SanusX consumer report, optionally emailed.
51
+ - **`generateCorporateReportPDF(json, isDebugging, isDownloadable)`** — pipeline: Corporate. Corporate/analytics report.
52
+ - **`sendEmail(json)`** — fire a generic notification email (no PDF pipeline).
53
+ - **`generateQrCode(json)`** — pipeline: Core (`generatePatientQR`). Rasterizes a caller-supplied HTML string (e.g. an inline SVG QR code) to PDF — despite the name, does not generate a QR code itself.
54
+
55
+ **Payload convention:** every PDF-producing function expects the "real" data wrapped one level up: the caller passes a JSON *string* whose parsed object has a `data` field containing **base64-encoded JSON** (the actual report content), plus sibling fields like `client`, `language`, `host`/`port`/`authUser`/`authPass`/`sendFromEmail`/`secure` (SMTP config, only used if emailing) and, for `generateFullPdf`, `pdfPassword`. `generateMedicusPDF` is the odd one out — it takes the base64 payload directly as its first argument rather than nested inside a JSON envelope.
56
+
57
+ ```js
58
+ // Illustrative shape of the outer envelope for generateNascoPDF / generateFullPdf / generateSanuxPDF
59
+ {
60
+ "data": "<base64-encoded JSON string — the actual report payload>",
61
+ "client": "Mediclinic", // whitelabel selector, see doc 2
62
+ "language": "en", // or "ar", "ar-AE", "de", ...
63
+ "host": "smtp.example.com", // only read if shouldSendEmail is true
64
+ "port": 587,
65
+ "authUser": "...",
66
+ "authPass": "...",
67
+ "sendFromEmail": "noreply@...",
68
+ "secure": true, // NOTE: currently ignored, see doc 7
69
+ "pdfPassword": "..." // generateFullPdf only — PDF is encrypted with this
70
+ }
71
+ ```
72
+
73
+ Every function decodes with `Buffer.from(base64Object, 'base64').toString('utf8')` then `JSON.parse` — this is **encoding, not encryption**; the payload (including any SMTP credentials riding alongside it) is trivially recoverable by anyone who can see the request. See [07-email-notifications.md](07-email-notifications.md) for the security implication.
74
+
75
+ `run.js` is a thin optional wrapper that runs `generateMedicusPDF` in a forked child process (`process.on('message', ...)` / `process.send(...)`), for host apps that want report generation isolated from their main event loop. It is not required — most callers use `index.js` directly.
76
+
77
+ ## 1.4 Critical fact: the core report renderer that actually runs is `pdf_generator.min.js`, not `pdf_generator.js`
78
+
79
+ `index.js:1` requires `./lib/pdf_generator.min` — **not** `./lib/pdf_generator`. These are commonly assumed to be a source-file/build-artifact pair (the kind that a bundler regenerates), but they are not:
80
+
81
+ - There is **no build script** anywhere in the repo that produces `pdf_generator.min.js` from `pdf_generator.js` (no `webpack.config.js` exists, despite `webpack` being a devDependency).
82
+ - Git history shows the two files are edited **completely independently**: `lib/pdf_generator.js` was last touched by commit `f31298e` ("fix range") on **2021-03-25**. `lib/pdf_generator.min.js` was last touched by commit `8bb73da` ("Reformat Code") on **2025-07-30** — over four years later — and that commit only reformatted/beautified the minified file (it went from a single packed line to ~525 readable lines); it did not re-minify from `pdf_generator.js`, which was untouched.
83
+ - Today the two files happen to be logically equivalent in most places (verified by spot comparison), but this is coincidental, not enforced by any tooling. **A change made only to `lib/pdf_generator.js` will never ship** — `index.js` never loads it.
84
+
85
+ **Practical rule: when working on the core "Medicus" lab-report pipeline, edit `lib/pdf_generator.min.js`.** Treat `lib/pdf_generator.js` as a legacy reference copy at best, and flag it for removal or for wiring up an actual build step. All other generators (`wellbeing_report_generator.js`, `corporate_report_generator.js`, `sanusx_report_generator.js`) do not have this problem — each is required directly from its single, non-minified source file.
86
+
87
+ ## 1.5 Data flow, at a glance
88
+
89
+ ```
90
+ Host app This package Output
91
+ ───────── ──────────── ──────
92
+ JSON payload ──base64──▶ index.js export
93
+
94
+
95
+ decode base64 → JSON.parse
96
+
97
+
98
+ generateHTML*(data, isDebugging, client, language)
99
+ - loads config/{client}.json (branding) ┐
100
+ - loads templates/{brand or shared}/*.html │ see doc 2 & 3
101
+ - loads locales/{...}/{language}.json │
102
+ - fs.readFileSync HTML fragments, │
103
+ string .replace("{{token}}", value) │
104
+ - loads shell HTML into jsdom + jQuery │
105
+ - DOM-injects rendered fragments ┘
106
+
107
+ ▼ (HTML string, plus metadata: header/footer HTML, output file path)
108
+ generatePDF*(html) — Puppeteer
109
+ - page.setContent / page.goto(file://...)
110
+ - page.pdf({ headerTemplate, footerTemplate, margin, ... })
111
+ - (core report only) two page.pdf() calls merged via hummus,
112
+ to give page 1 a different header than the rest
113
+
114
+ ▼ PDF Buffer
115
+ (generateFullPdf only) node-qpdf2 encrypts the buffer with reportData.pdfPassword
116
+
117
+
118
+ return Buffer (isDownloadable) | base64 string | sendNascoEmail(...) result
119
+ ```
120
+
121
+ ## 1.6 Key third-party dependencies and why they're there
122
+
123
+ - **`puppeteer`** (pinned `^1.15.0`, very old) — headless Chrome → PDF rendering, in all four generators. Each generator launches its own Puppeteer instance independently — no shared "renderPdf" helper.
124
+ - **`hummus`** (`1.0.111`) — merging two separately-rendered PDF buffers (core report only, to give page 1 a distinct header). Effectively unmaintained upstream.
125
+ - **`node-qpdf2`** — encrypting the final PDF with a password (`generateFullPdf` only). Dynamically `import()`-ed (ESM) inside a CommonJS file.
126
+ - **`jsdom` + `jquery`** — server-side DOM construction and manipulation before handing HTML to Puppeteer. Used by every generator except the newer `template.js` V3 functions, which build HTML via plain string concatenation instead.
127
+ - **`i18n`** (`^0.8.3`, resolves to `0.8.6`) — translation string lookup. Two independently configured instances — see doc 6.
128
+ - **`nodemailer`** — sending report emails with the PDF attached. Transport config comes entirely from the caller's payload, not env vars.
129
+ - **`chart.js`** (npm) — declared but **not actually used server-side** — dead import in `pdf_generator.js`. The real chart rendering uses a separately bundled copy, `assets/charts.min.js`, executed **inside the headless Chrome page** before the PDF snapshot is taken. Two independent copies of Chart.js that can drift out of sync.
130
+ - **`qr-image` / `qrcode`** (npm) — also effectively unused server-side for the in-report PIN/QR box — that box is rendered client-side via the bundled `assets/qrcode.min.js`. `generatePatientQR` in `index.js`/`pdf_generator.min.js` is a generic "rasterize this HTML string" utility, not a QR generator itself.
131
+ - **`canvas`, `get-canvas-context`, `self-adapt-fontsize`, `textfit`, `big-text.js`** — legacy font-fitting/canvas helpers. Present in `package.json` but not central to the current rendering path — treat as legacy.
132
+
133
+ ## 1.7 File/temp-file handling — an evolving pattern worth knowing
134
+
135
+ Older code paths (`generateMedicusPDF`, `generateNascoPDF`, `generateSanuxPDF`, the internals of `pdf_generator.js`/`.min.js`) write intermediate HTML and the final PDF to **fixed, shared filenames** inside the package's own `output/` directory (e.g. `output/sample.pdf`, `output/nasco-sample.pdf`, `output/LOGS.txt`) and never delete them. Two concurrent requests through the same process can clobber each other's files.
136
+
137
+ `generateFullPdf` (`index.js:194-314`) is the one flow that was hardened against this: it generates a `crypto.randomUUID()` per call and builds all temp paths (`wb-{callId}-in.pdf`, `wb-{callId}-enc.pdf`, `wb-{callId}-mail.pdf`) inside `os.tmpdir()`, then cleans them up in a `finally` block via a best-effort `safeUnlink`. **If you add a new PDF-producing flow, follow this newer pattern, not the older fixed-filename one.**
138
+
139
+ ## 1.8 Appendix: other root-level files (accounted for, not part of the runtime pipeline)
140
+
141
+ A handful of files at the repo root are not required by `index.js` and are not wired into any of the four report pipelines. Listed here so nothing is silently unexplained:
142
+
143
+ - **`test.js`**, **`tests/test.js`**, **`tests/test-qr-code.js`**, **`preview-big-integral.js`** — ad hoc developer scripts, not a test framework. See [`HANDOVER.md` §9](../HANDOVER.md#9-runningtesting-this-locally) for how to use them.
144
+ - **`downloadfile.js`** — a one-off helper that fetches a pinned Chromium build for Puppeteer on Windows. Not required if Puppeteer's own Chromium download already succeeded during `npm install`.
145
+ - **`data converter.js`** (53 lines) — a standalone script that parses an ad hoc "Antimicrobial Agent / Sensitivity" text format into structured rows. Not `require`d by anything in `lib/`, `index.js`, or `run.js` — an orphaned, one-off data-conversion utility from some prior integration, not part of the active rendering pipeline.
146
+ - **`base64/cairo-font.js`** — a 2-line file containing a single, large base64-encoded font data URI. Not `require`d anywhere — orphaned/unused; the fonts actually used at render time are the files under `assets/fonts/`.
147
+ - **`example.txt`** — a large (1000+ line) saved fragment of previously-rendered report HTML, kept as a manual reference/comparison sample, not consumed by any code.
148
+
149
+ None of these affect report output; they're safe to leave alone or clean up opportunistically.
@@ -0,0 +1,111 @@
1
+ # 2. Configuration & Whitelabelling
2
+
3
+ Whitelabelling ("which brand does this report look like") is driven entirely by a **runtime string** — a `client` (or, for the corporate report, `clientName`) field in the request payload. There is no environment variable, no CLI flag, and no central "client registry" module. Each generator independently maps that string to a config JSON file and a template folder, and — this is the single most important thing to understand about this system — **the four generators do it four slightly different, inconsistent ways.**
4
+
5
+ ## 2.1 The `config/` directory
6
+
7
+ One JSON file per brand, named after the client string, plus `config/default.json` as the fallback:
8
+
9
+ ```
10
+ config/
11
+ default.json
12
+ Mediclinic.json
13
+ Pha.json
14
+ Najeeb.ai.json
15
+ bionext.json
16
+ diagnostikare.json
17
+ maisonsante.json
18
+ nasco.json (referenced by filename convention; see 2.2 for how "nasco" resolves)
19
+ sanitas.json
20
+ sanusx.json
21
+ ```
22
+
23
+ ### Common schema (present in most/all files)
24
+
25
+ - **`logo`** — Logo image, URL or base64 data-URI
26
+ - **`body_icon`, `lifestyle_icon`, `mind_icon`, `bulb_icon`** — Section icons, URL or base64 data-URI
27
+ - **`first-level-color` … `fifth-level-color`** — 5-step color ramp used for score/chart coloring
28
+ - **`general-font-color`, `disclaimer-text-color`, `top-box-background`, `footer-background`** — Theme colors
29
+ - **`body_font_color`, `list_icons_font_color`, `insights_font_color`** — More theme colors
30
+ - **`signature`** — Free text signed off in emails/footers, e.g. `"Team Medicus"`, `"Team Sanitas"`
31
+ - **`corporate_report`** — Nested object, see 2.1.1
32
+
33
+ ### Brand-specific keys
34
+
35
+ - `title` — only `Mediclinic.json`, `Pha.json`, `maisonsante.json` (`"HealthRiskAssessment"`).
36
+ - `score-header-color`, `score-description-color`, `svg-icon-color`, `low-score-text-color`, `section-title-border-color`, `first-section-scores-border-color`, `footer-border-color` — only `Mediclinic.json`, `Pha.json`, `maisonsante.json`.
37
+ - `labs-logo` / `labs-logo-aliases` — **only `Pha.json`**. `labs-logo` maps a canonical lab key (e.g. `"labcorp"`, `"wp"`) to a base64 logo image. `labs-logo-aliases` maps the same keys to arrays of alternate spellings (e.g. `["lab corp", "lab_corp", ...]`) used to match whatever spelling the caller sends in `selectedLabs` (see doc 4, wellbeing SmartReport header logos).
38
+ - `big_integral_questionnaire` — only `Pha.json` and `maisonsante.json`: `{ title, showHormonalFemale, showHormonalMale, colors: { headerBg, subheaderBg, headerText, rowBorder, elevatedText } }`. Consumed by `lib/big_integral_questionnaire.js` (see doc 5 — this module currently renders 100% mock data).
39
+
40
+ #### 2.1.1 The nested `corporate_report` object
41
+
42
+ ```json
43
+ "corporate_report": {
44
+ "logo": "...",
45
+ "main_color": "rgb(0, 0, 153)",
46
+ "report_date_color": "rgb(139, 139, 167)",
47
+ "section_title_color": "rgb(0, 0, 153)",
48
+ "section_title_border_color": "rgb(0, 158, 226)",
49
+ "chart_title": "rgb(139, 139, 167)",
50
+ "summary_background": "#e5f5fc",
51
+ "relation_content_border_color": "rgba(0, 0, 153, 0.3)",
52
+ "relation_content_bg": "#f2f2ff",
53
+ "table_tr_even_bg": "rgb(237, 237, 248)",
54
+ "recommends_bg": "rgba(0, 0, 153, 0.05)",
55
+ "recommends_title": "Moxie",
56
+ "show_medicus_logo": true,
57
+ "show_second_footer_logo": true
58
+ }
59
+ ```
60
+
61
+ `show_second_footer_logo` is missing from `sanusx.json` and `diagnostikare.json`. `bionext.json`'s `corporate_report` block additionally has its own `body_font_color`, unique to that one file.
62
+
63
+ `Najeeb.ai.json` is essentially a duplicate of `default.json` (same colors, `signature: "Team Medicus"`) — a placeholder brand with no real customization yet. `sanitas.json` overrides `signature` ("Team Sanitas"), the color ramp, and `corporate_report.recommends_title` ("Sanitas") but otherwise mirrors the default.
64
+
65
+ ## 2.2 How `client`/`clientName` resolves to a config file — and why it's inconsistent
66
+
67
+ There is no shared config-loader. Each generator reimplements the lookup:
68
+
69
+ - **`wellbeing_report_generator.js`** (`loadClientConfig`) — Resolution logic: `'pha'` (any case) → `'Pha'`; otherwise uses the string as-is to build `config/{name}.json`; falls back to `config/default.json` via `fs.existsSync`. *Gotcha:* only `pha` gets special-cased; every other brand must be passed with exact on-disk casing.
70
+ - **`corporate_report_generator.js`** — Resolution logic: `client = data.clientName.toLowerCase()`, then `config/{client}.json`. *Gotcha:* **lower-cases before lookup**, but the actual files are capitalized (`Mediclinic.json`, `Pha.json`, `Najeeb.ai.json`). This only works on case-insensitive filesystems (Windows/macOS default). On a case-sensitive Linux filesystem, `clientName: "Mediclinic"` silently falls back to `default.json`.
71
+ - **`lib/sendEmail.js`** (`sendNascoEmail`) — Resolution logic: uses the **raw, unmodified** client string, no lower-casing, no `pha`→`Pha` mapping. *Gotcha:* caller must pass the exact on-disk filename casing, a different rule than the wellbeing generator it's normally called alongside.
72
+ - **`sanusx_report_generator.js`** — Resolution logic: **ignores `clientName` for config entirely** — unconditionally `require('../config/sanusx.json')`. *Gotcha:* the `client` parameter this generator accepts is only used later to add a CSS class (`$(".score-main").addClass(client)`), which currently matches no CSS rule — effectively a no-op. SanusX is single-brand in practice.
73
+ - **`lib/pdf_generator.js` / `.min.js`** (core report) — Resolution logic: does not touch `config/` at all. *Gotcha:* branding for the core report must arrive pre-baked inside the data payload from the host app — there is no per-client config file in this pipeline.
74
+
75
+ **Practical consequence:** if you're onboarding a new brand, check *which* generator you're using before assuming "just add `config/NewBrand.json`" is enough — confirm the exact casing rule for that specific generator, and remember `sanusx_report_generator.js` won't pick it up at all without a code change.
76
+
77
+ One more special case: `wellbeing_report_generator.js:1341-1353` hard-codes a feature gate — when `client.toLowerCase() === 'maisonsante' && data.IsDoctor`, it re-reads `config/maisonsante.json` a second time (bypassing the already-resolved config object) specifically to feed `big_integral_questionnaire.js`.
78
+
79
+ ## 2.3 Config → colors/branding, not template choice, in most flows
80
+
81
+ Loading `config/{client}.json` gives you colors/logos/icons that get injected as inline `<style>` overrides or `{{token}}` substitutions into whatever HTML template was already chosen (see 2.4) — it does **not**, by itself, choose which template folder is used. Two exceptions: the corporate report and SanusX generators always use one fixed template folder regardless of config; only the wellbeing "extended" (SmartReport) flow ties template folder selection directly to the client string.
82
+
83
+ ## 2.4 Template folder selection — no blocks-level fallback
84
+
85
+ `templates/` has full per-brand subfolders for **`Mediclinic`**, **`Pha`**, and **`maisonsante`** (each with `ltr_no_pages.html`, `rtl_no_pages.html`, `wellbeing_template.html` and its own `blocks/`), a generic **`wellbeing`** folder used as the default, a **`sanusx`** folder, plus non-brand folders `corporate_report/`, `imc/`, `popup/`, and the shared top-level `templates/blocks/`.
86
+
87
+ - **Core report** (`pdf_generator.min.js`) — always `templates/blocks/*` (+ `templates/imc/first-header-template.html` for one specific lab type). No per-brand folder is ever consulted; all differentiation happens through the data payload, not through swapped templates.
88
+ - **Wellbeing, plain** (`generateHTMLWellbeingReport` → `loadWellbeingTemplates`) — always `templates/wellbeing/` regardless of client. Brands going through this flow are differentiated purely by injected config colors/logo.
89
+ - **Wellbeing, extended/SmartReport** (`generateHTMLWellbeingReportWithSmartReport` → `loadExtendedTemplates`) — derives the folder name **directly from the client string** (`'pha'→'Pha'`, `'mediclinic'→'Mediclinic'`, else used as-is) → `templates/{ClientName}/`. Reads every block with `fs.readFileSync` and **no existence check** — if the folder or a block inside it is missing, it throws `ENOENT`. Only works today for clients with a dedicated folder: `Mediclinic`, `Pha`, `maisonsante`.
90
+ - **Corporate report** — always `templates/corporate_report/*`, regardless of client.
91
+ - **SanusX** — always `templates/sanusx/*`, regardless of client.
92
+
93
+ There is **no fallback from a brand folder to the shared `templates/blocks/`** for an individual missing block — brand folders are complete, parallel copies, not overrides layered on a shared base. If you add a fourth brand to the "extended" wellbeing flow, you must create a full `templates/{Brand}/` folder with every block file the existing three have, or the render will crash.
94
+
95
+ ## 2.5 Brand asset directories
96
+
97
+ `assets/Mediclinic/`, `assets/pha/labcrop-logo.png`, `assets/sanusx/`, `assets/imc/`, `assets/corporate_report/`, `assets/wellbeing/`, `assets/medicus_pdf/*.css`.
98
+
99
+ Notable: **`templates/Pha/*.html` and `templates/maisonsante/*.html` both reference `../assets/Mediclinic/css/...` and `../assets/Mediclinic/js/js.js`** — Pha and Maison Sante do not have their own CSS/JS bundle; they reuse the Mediclinic bundle wholesale and rely entirely on the `config/{Client}.json` color values for visual differentiation. `templates/corporate_report/cover_page.html` also hard-codes a Nasco logo image path regardless of client, unless overridden by config.
100
+
101
+ ## 2.6 Environment variables
102
+
103
+ There is exactly **one** environment variable read anywhere in the codebase: `SHOW_QR_BOX` (`lib/template.js`), which toggles whether the in-report PIN/QR box renders. It has nothing to do with whitelabelling. **No environment variable selects a client, config, or template.**
104
+
105
+ ## 2.7 Step-by-step trace: "given `client = X`, what actually loads?"
106
+
107
+ 1. Host app calls an `index.js` export with `client: X` (or `clientName: X` for the corporate report) inside the payload.
108
+ 2. The generator resolves `X` to `config/{mapped X}.json` if it exists, else `config/default.json` — using whichever mapping rule from §2.2 applies to that generator.
109
+ 3. Config values (colors, logo, icons, `corporate_report`/`big_integral_questionnaire` sub-objects) get inlined into the HTML as `<style>` overrides / `{{token}}` substitutions.
110
+ 4. The template folder is chosen per §2.4 — for three of the four generators this is *independent* of `X`; only the "extended" wellbeing flow ties folder choice to `X`.
111
+ 5. Assets are pulled in via relative `<link>`/`<script>` tags baked into whichever template was chosen — mostly `assets/Mediclinic`, `assets/wellbeing`, `assets/sanusx`, `assets/corporate_report`, or `assets/imc` — independent of `X` except for the config-driven inline color overrides.
@@ -0,0 +1,71 @@
1
+ # 3. Templating & Rendering Pipeline
2
+
3
+ ## 3.1 There is no template engine
4
+
5
+ Despite the `templates/` folder full of `.html` files, this project does **not** use Handlebars, EJS, Mustache, or any templating library. Every generator (core, wellbeing, corporate, sanusx) hand-rolls the same two techniques:
6
+
7
+ **(a) Plain string `{{token}}` replacement**, for fragments Puppeteer needs as raw strings (Chrome's `headerTemplate`/`footerTemplate` print options only accept static HTML strings, so these can't be manipulated after the fact):
8
+ ```js
9
+ headerTemplate = headerTemplate
10
+ .replace("{{logo}}", img)
11
+ .replace("{{patient_name}}", patientName)
12
+ .replace("{{report_ref}}", fitNumber(data.reportNumber))
13
+ // ...
14
+ ```
15
+
16
+ Block files contain literal `{{...}}` tokens, e.g. `templates/blocks/header.html` (`{{patient_name}}`, `{{report_ref}}`, `{{division_name}}`, `{{report_date}}`), `templates/blocks/doctor-note.html` (`{{note-id}}`), `templates/blocks/panel-details.html` (`{{panel-id}}`, `{{panel-title}}`).
17
+
18
+ Note: `lib/sendEmail.js` uses a **different** placeholder convention for its own hand-built HTML — single-brace `{token}` (e.g. `{logo}`, `{header_text}`) — not `{{double-brace}}`. If you're hunting for a placeholder and it's an email string, look for single braces.
19
+
20
+ **(b) jQuery-over-jsdom DOM manipulation**, for the main body of each report. The pattern:
21
+
22
+ 1. Load a page-skeleton HTML file (e.g. `templates/ltr_no_pages.html`) with `fs.readFileSync`.
23
+ 2. `let dom = new JSDOM(html); $ = require('jquery')(dom.window);` — construct a real DOM in Node and bind jQuery to it.
24
+ 3. Load each block fragment, do its `{{token}}` replacements, then inject it into the DOM by selector: `$('#content').append(newPanelHtml)`, `$("#reference").text(data.reportNumber)`, `$('.patient-table').html(...)`, `$(selector).remove()`, etc.
25
+ 4. Serialize the whole document back to a string with `dom.serialize()` and write it to a temp/output HTML file.
26
+ 5. Puppeteer loads that file via a `file://` URL and rasterizes it to PDF (see 3.3).
27
+
28
+ This exact five-step pattern is duplicated **independently** in `lib/pdf_generator.min.js`, `lib/wellbeing_report_generator.js`, `lib/corporate_report_generator.js`, and `lib/sanusx_report_generator.js` — there is no shared "render engine" module between them, only convention.
29
+
30
+ `lib/template.js` is a partial exception: it contains a newer, parallel set of rendering functions (suffixed `V3` — `renderBiomarkerV3`, `renderPanelV3`, `renderNoteV3`, etc.) that build HTML purely via JS template-literal string concatenation, with no `fs.readFileSync` of block `.html` files and no jsdom/jQuery for those specific pieces. `template.js` is used **only** by `wellbeing_report_generator.js` (for rendering the merged "SmartReport" biomarker section) — it is not shared with the core report's own (older, still jsdom/jQuery-based) biomarker rendering in `pdf_generator.min.js`, even though the two do very similar things. This suggests the codebase is mid-migration from "external HTML blocks + jQuery/jsdom" toward "HTML generated inline in JS," but the migration has only reached the wellbeing pipeline so far.
31
+
32
+ ## 3.2 Top-level page templates — live vs. dead
33
+
34
+ Only two root-level templates are actually loaded at runtime by the core report: **`templates/ltr_no_pages.html`** and **`templates/rtl_no_pages.html`**, chosen by a single `isRtl` boolean. Both are minimal skeletons — a `<head>` pulling in `charts.min.js`/`qrcode.min.js`, empty containers (`#patient-container`, `#more-link`, `#pin`, `<canvas id="canvas">`), and one empty `<div id="content">` that everything else gets appended into. The RTL variant additionally sets `dir="rtl" lang="ar"` and loads `arabic.min.css`/`translation.min.js`. Neither contains manual page-break markup — pagination is left entirely to the print engine, which is why they're named `*_no_pages` (as opposed to the older, page-per-`<div class="paper">` style below).
35
+
36
+ `templates/imc/first-header-template.html` is swapped in only for one specific lab type (`labType === 9`, a wide custom header).
37
+
38
+ The remaining root-level files — `templates/base.html`, `templates/template.html`, `templates/ltr.html`, `templates/no_pages.html`, `templates/empty.html`, `templates/first_page_head.html` — are **not referenced by any code path** (confirmed by grep across all of `lib/`). They are static mockups/prototypes from earlier iterations of the pagination model (manual `<div class="paper">` per-page blocks with repeated headers/footers) or trivial smoke-test fixtures. Don't assume editing them affects output — they're dead weight, safe candidates for removal but currently harmless if left alone.
39
+
40
+ ## 3.3 PDF conversion (Puppeteer)
41
+
42
+ Each generator launches its own Puppeteer instance and calls `page.pdf(...)` with its own options — there is no shared "renderPdf" helper across the four pipelines.
43
+
44
+ **Core report** (`pdf_generator.min.js`) calls `page.pdf()` **twice**, because Chrome's print header/footer can't vary within a single call:
45
+ - Once for `pageRanges: '1'` with the "first page" header template and different top margin (page 1 needs the patient-info/QR header).
46
+ - Once for `pageRanges: '2-'` with the regular header template.
47
+
48
+ The two resulting PDF buffers are then merged into one file using `hummus` (`createWriterToModify(...).appendPDFPagesFromPDF(...)`).
49
+
50
+ **Wellbeing / Corporate / SanusX** each call `page.pdf()` once, with their own `headerTemplate`/`footerTemplate`/margins tuned per report type. The SanusX generator additionally passes `pageRanges: '1'`, meaning **any content overflowing page 1 is silently dropped** — a fragile single-page assumption worth remembering if SanusX report content ever grows (e.g. longer translated strings pushing content past one page).
51
+
52
+ The corporate report generator sets page content **twice** — once via `page.setContent(...)` and again by navigating to the serialized temp HTML file via `page.goto("file://...")` — and separately injects jQuery from a public CDN (`cdn.jsdelivr.net`) mid-render via `page.addScriptTag`. This means corporate-report rendering has a live external-network dependency at PDF-generation time, unlike the other three pipelines which bundle their own JS/CSS as local `assets/` files.
53
+
54
+ ## 3.4 Charts
55
+
56
+ There is no server-side chart *image* generation. The npm `chart.js` package is `require`d at the top of `pdf_generator.js`/`.min.js` but never actually called — a dead import. Instead:
57
+
58
+ 1. Server-side, biomarker history is serialized into a plain JS array (`{id, dataset, dataColor, dataLabel}` per biomarker) and injected into a `<script>` tag as `var chartsData = [...]`.
59
+ 2. The page template loads a **separately bundled, minified browser copy of Chart.js 2.x** — `assets/charts.min.js` (not the npm package).
60
+ 3. `assets/js.js`'s `drowBioCharts()` function runs **inside the headless Chrome page itself**, before `page.pdf()` is called, and instantiates real `Chart(ctx, {type:'line', ...})` objects against `<canvas>` elements.
61
+
62
+ So charts are rendered client-side, in-browser, immediately before the PDF snapshot — a legitimate technique, but it means the npm `chart.js` dependency and `assets/charts.min.js` are two independent copies of the same library that can silently drift out of version sync.
63
+
64
+ The corporate report's bar charts don't use a charting library at all: `renderBarChart()` emits plain `<div class="bar-chart" data-value=".." data-total="..">` elements, and `assets/corporate_report/js/js.js` computes their pixel width client-side via `$(this).css("width", "calc(" + percent + "% + 60px)")`. SanusX's score "gauges" are similarly library-free — static PNG segment-circle images overlaid with a CSS-rotated needle (`transform: rotateZ(...)`), not a canvas or SVG chart.
65
+
66
+ ## 3.5 QR codes — two unrelated mechanisms
67
+
68
+ Don't confuse these:
69
+
70
+ 1. **The in-report patient PIN/QR box** (page 1 "more info" panel): the server builds a plain concatenated string (`'v_' + patientName + '_!_' + pin`, misleadingly assembled near variables named `base64text`/`base64name`/`base64pin` that are computed but never actually used), injects it as a script variable, and `assets/js.js`'s `drawQRCode()` calls `QRCode.toCanvas(...)` **client-side, in-browser**, using the bundled `assets/qrcode.min.js`, targeting a `<canvas id="canvas">` in the page template.
71
+ 2. **`generatePatientQR(data)`** (exported from `index.js` as `generateQrCode`) — despite its name, this function does **not generate a QR code**. It takes an **already-fully-rendered HTML string** from the caller (e.g. containing an inline `<svg>` QR code the caller produced elsewhere), wraps it in jsdom, writes it to a temp file, and rasterizes it to a `letter`-format PDF with no margins/headers via Puppeteer. It is a generic "HTML string → PDF" utility that happens to be used for QR codes by convention, not a QR-code generator itself. See `tests/test-qr-code.js` for the expected caller-side pattern (pre-render the QR as inline SVG, then pass the whole HTML string in).
@@ -0,0 +1,128 @@
1
+ # 4. Report Pipelines
2
+
3
+ Four independent report types. Each has its own generator module, its own template folder(s), and its own quirks.
4
+
5
+ ---
6
+
7
+ ## 4.A Core "Medicus" Lab Report
8
+
9
+ **Module:** `lib/pdf_generator.min.js` (the file that actually runs — see [doc 1.4](01-architecture-overview.md#14-critical-fact-the-core-report-renderer-that-actually-runs-is-pdf_generatorminjs-not-pdf_generatorjs)). `lib/pdf_generator.js` is a stale reference copy.
10
+
11
+ **Purpose:** a clinical lab report — biomarkers grouped into panels, each with reference ranges, history charts, doctor notes, and AI/clinical "insights." No questionnaire concept at all.
12
+
13
+ **Entry points:** `generateHTMLStaging(data, isDebugging)` → HTML; `generatePDF(html)` → PDF buffer via Puppeteer; both wired together by `index.js`'s `generateMedicusPDF`. `generatePatientQR(data)` is a separate, generically-named "HTML string → PDF" utility (see [doc 3.5](03-templating-and-rendering.md#35-qr-codes--two-unrelated-mechanisms)).
14
+
15
+ **Data shape (top-level fields observed in the code and `assets/data/data2.json`):**
16
+ ```
17
+ language, labType (9 = "IMC lab", triggers customHeader), labLogo, labName, labAddress,
18
+ labPhoneNumber, patientName, patientPin, showPIN, timeZoneOffset, reportDate (unix seconds),
19
+ reportNumber, reportDivision, reportTitle, copyright, moreLink, showCompactView, showDetailsView,
20
+ profileItems: [{title, value}],
21
+ panels: [{ panelId, name, panelDetails, notes,
22
+ biomarkers: [{ id, name, fullName, unit, value, formattedValue, color, isNormal,
23
+ showInCompactView, showInDetailsView, ranges[], history[],
24
+ childrenBiomarkers[], doctorNotes[], relatedInsights[],
25
+ status, insightsCount }],
26
+ insights: [...] }],
27
+ summary: [{ ...stat insights, stackedInsight, relatedBiomarkers, isClinicalReading }],
28
+ doctorNote: [{ id, createdByName, content }],
29
+ reportSummary, reportProperties,
30
+ signatures: [{ name, signatureURL }], labStamp, approvalDate
31
+ ```
32
+
33
+ **Templates:** always `templates/blocks/*.html` (shared, non-brand) plus `templates/imc/first-header-template.html` for `labType === 9`. No per-brand template folder is ever consulted in this pipeline — whitelabelling here happens entirely through data values the host app bakes into the payload (logo URL, colors would need to be pre-applied since there's no `config/` lookup at all in this file).
34
+
35
+ **Notable quirks:**
36
+ - Module-level mutable state (`$`, `debug`, `OUT_FILE`, `Pdf_file`, `shouldRenderCustomHeader`) is reassigned per call with no isolation — concurrent calls in the same process can interfere with each other (contrast with `generateFullPdf`'s UUID-based temp files).
37
+ - Every render leaves a timestamped HTML file behind in the package's own `output/` directory; nothing is cleaned up.
38
+ - `labType === 9` ("IMC") special-casing runs throughout what's otherwise framed as the generic/core pipeline.
39
+ - Pinned to very old `puppeteer@^1.15.0` and `hummus@1.0.111`.
40
+
41
+ ---
42
+
43
+ ## 4.B Wellbeing Report (+ optional merged "SmartReport")
44
+
45
+ **Module:** `lib/wellbeing_report_generator.js` (2000+ lines), using shared rendering helpers from `lib/template.js` for the SmartReport section, and `lib/big_integral_questionnaire.js` for one Maison-Sante-only static table.
46
+
47
+ **Purpose:** a patient-facing wellbeing/lifestyle report — physical/psychological/lifestyle scores, PHQ‑9/GAD‑7-style questionnaire answers (when a doctor reviewed it), and tips. Optionally merged with a full lab biomarker report ("SmartReport").
48
+
49
+ There are **two distinct entry functions** with materially different behavior:
50
+
51
+ **`generateHTMLWellbeingReport`** (the plain flow):
52
+ - Signature: `(data, isDebugging, clientName, language)`
53
+ - Template folder: always `templates/wellbeing/` (generic), regardless of client
54
+ - Data source: top-level `data.calculation` / `data.partsScore` / `data.insights.tips`
55
+ - Called from: `generateNascoPDF` (`index.js`)
56
+ - PDF conversion: `generatePDFWellbeingReport(html)`
57
+
58
+ **`generateHTMLWellbeingReportWithSmartReport`** (the branded/extended flow):
59
+ - Signature: `(data, isDebugging, clientName, language, selectedLabs=[], showHeaderLogo=true, hideWellbeingUI=false)`
60
+ - Template folder: `templates/{Mediclinic, Pha, or maisonsante}/` — client-specific, throws if the client has no dedicated folder
61
+ - Data source: `data.wellbeing` (scores), `data.profileInfo` (metadata), `data.Profile` + `data.IsDoctor` (Q&A), `data.SR` (merged SmartReport lab data)
62
+ - Called from: `generateFullPdf` (`index.js`) — the only flow that also encrypts the output
63
+ - PDF conversion: `generatePDFReport(data, hideWellbeingUI)` — adds running header/footer, `data.footer` support
64
+
65
+ **Brand/theme support:** `loadColorTheme` explicitly supports **mediclinic, pha, nasco, maisonsante**. Any other client string (`bionext`, `diagnostikare`, `sanitas`, `sanusx`, `Najeeb.ai`) silently falls back to the mediclinic color theme.
66
+
67
+ **SmartReport merge (`data.SR`):** a full lab/biomarker report payload (same general shape as the corporate report's item list: `items[]` of `biomarker`, `panel`, `insight`, `biomarkerNote`, `allergyItems`, etc.), parsed as string or object, run through `generateReportHtml()` (reusing `renderReportItems`/`renderSummary`/`renderHTMLTemplate` from `lib/template.js`), and injected into `#smart-report`. If `hideWellbeingUI` is true or `data.wellbeing` is empty, the quiz-UI sections are removed from the DOM so only the lab report shows.
68
+
69
+ **`selectedLabs` / `showHeaderLogo`:** used to decide which lab logos appear in the header, normalized against `config.labs_logo`/`labs_logo_aliases` (Pha only — see [doc 2.1](02-configuration-and-whitelabelling.md#211-the-nested-corporate_report-object)) so different spellings of a lab name (`"lab corp"`, `"LabCorp"`) resolve to one config entry.
70
+
71
+ **Known bug:** `index.js` passes `showHeaderLogo || true` into the generator. Since `false || true === true`, **the header logo can never actually be suppressed**, contradicting both the parameter name and its JSDoc in `index.js`.
72
+
73
+ **Localization:** uses `app/i18n_wellbeing.config.js` / `locales/wellbeing/*.json` (not the main `locales/` set — see [doc 6](06-internationalization.md)).
74
+
75
+ **Encryption:** only `generateFullPdf` in `index.js` encrypts the final buffer, using `node-qpdf2` with `reportData.pdfPassword`. `wellbeing_report_generator.js` itself has no encryption logic — it only ever returns a plaintext PDF buffer.
76
+
77
+ **Other quirks worth knowing:**
78
+ - Per-client special-casing is scattered as inline conditionals rather than being config-driven: PHA-only CSS overrides, PHA-only bottom margin, hardcoded per-client addresses/phone numbers in the footer builder, tips category ordering swapped for `pha`, header-logo sizing branched by client name. Onboarding a new client for this flow means hunting through many such `if (client === ...)` branches, not editing one config file.
79
+ - `generateHeaderInfo()`'s output is computed but discarded — its target selector `.report-info` is commented out in all three brand `blocks/header.html` files. The live equivalent is `renderPatientTable()`, which injects into `.patient-table`.
80
+ - `templates/{wellbeing,Mediclinic,Pha,maisonsante}/wellbeing_template.html` files are not referenced by any `.js` file — orphaned, superseded by `ltr_no_pages.html`/`rtl_no_pages.html`.
81
+ - Shared mutable module state (`config`, the singleton `localeService`/`i18n`, a module-level `debug` flag) is reassigned per call; file paths were hardened against concurrent-request collisions (per an in-code comment referencing a real past bug) but config/locale/debug state was not similarly isolated — concurrent requests for different clients/languages can still race.
82
+
83
+ ### 4.B.1 `lib/big_integral_questionnaire.js` — read this before assuming it's a real feature
84
+
85
+ This module is **not** a scoring engine and does **not** read real questionnaire answers from the input payload. It is a presentational renderer for one static demo table ("Key to the Big Integral Questionnaire" — a functional-medicine systems review, unrelated to the PHQ‑9/GAD‑7 wellbeing scores) built entirely from `getDummyData()`, which returns 11 hardcoded rows with `result: 0` for every row and a fixed fake patient (`"Final Test 3"`, age 37). The single export, `renderBigIntegralQuestionnaire({ clientConfig })`, merges `clientConfig.big_integral_questionnaire` colors/toggles (from `config/maisonsante.json`) and renders the table. It is invoked only when `client.toLowerCase() === 'maisonsante' && data.IsDoctor`, targeting `#big-integral-section` (present only in `templates/maisonsante/ltr_no_pages.html`). `preview-big-integral.js` is a standalone dev harness confirming this is a design/preview scaffold, not a wired, data-driven feature. **If a stakeholder asks "why doesn't the Big Integral Questionnaire reflect the patient's real answers" — that's not a bug, it was never wired to real data.**
86
+
87
+ ---
88
+
89
+ ## 4.C Corporate Report
90
+
91
+ **Module:** `lib/corporate_report_generator.js`.
92
+
93
+ **Purpose:** an aggregate/analytics PDF summarizing results across a *population* of participants (e.g. an employer's workforce), not a single patient — cover page, "Participant Analytics" section (per-metric bar charts), then one page per health "element" with a summary, bar charts, textual "relations," a gender-breakdown table, recommendations, and sources. Built for HR/benefits stakeholders.
94
+
95
+ **Entry points:** `generateHTMLCorporateReport(data, isDebugging)` → HTML; `generatePDFCorporateReport(html)` → PDF; wired together by `index.js`'s `generateCorporateReportPDF(json, isDebugging, isDownloadable)`.
96
+
97
+ **Data shape:** `data.clientName` (branding selector), `data.language`, `data.participantAnalytics: [...]` (drives the Participant Analytics bar charts — the `participant_analytics.html` template itself is just an empty `<div class="container">`; all its content is generated in JS), `data.elements: [{ summary, chartValues, secondChartValues, relations, dataByGender, recommends, sources }]`.
98
+
99
+ **Templates:** always `templates/corporate_report/*` (`cover_page.html`, `participant_analytics.html`, `page.html`, `footer.html`), regardless of client — only colors/logo vary by `config/{client}.corporate_report`.
100
+
101
+ **RTL:** this generator correctly branches to `templates/corporate_report/rtl_no_pages.html` vs `ltr_no_pages.html` based on `data.language` containing `"ar"` — unlike SanusX (4.D), RTL is not broken here.
102
+
103
+ **Notable quirks:**
104
+ - Sets page content twice (`page.setContent` then `page.goto(file://...)`) and injects jQuery from a public CDN mid-render (`page.addScriptTag({ url: 'https://cdn.jsdelivr.net/...' })`) — a live external-network dependency at render time.
105
+ - Contains a copy-pasted, entirely dead `combinePDFBuffers` function (and the `hummus`/`memory-streams` requires that exist only to support it) — never actually invoked in this file.
106
+ - Onboarding a new client's custom cover-page background image requires editing shared CSS by client-name string (`.cover-overlay-container.bionext { background-image: url(...) }` in `assets/corporate_report/css/report.css`) — bypasses the otherwise config-JSON-driven theming model.
107
+ - Corporate-report translation strings live inside the `locales/wellbeing/` bucket (via `app/i18n_wellbeing.config.js`), not a dedicated namespace — a naming trap when hunting for a string to translate.
108
+
109
+ ---
110
+
111
+ ## 4.D SanusX Report
112
+
113
+ **Module:** `lib/sanusx_report_generator.js`.
114
+
115
+ **Purpose:** a branded, single-consumer wellbeing product ("Your Health Hero") — an individual is scored on physical/psychological axes and assigned an avatar archetype (monkey/tiger/owl) with a "superpower," a strength/weakness, three predicted-vs-actual insight cards, and three tips cards. The SanusX-brand analogue of the wellbeing report.
116
+
117
+ **Entry points:** `generateHTMLSanusXReport(data, isDebugging, clientName, language)` → HTML; `generateSanusXReport(html)` → PDF; wired together by `index.js`'s `generateSanuxPDF`.
118
+
119
+ **Data shape:** `data.highestAvatarScorePartId` (1/2/3, maps to monkey/tiger/owl), score fields feeding a rounded physical/psychological score bucketed into low/med/high (drives a static PNG "speedometer" image + CSS-rotated needle, not a real chart), `data.PartScoresHighestValues.{body,mind,lifeStyle}[0].isPredictiveElement` (drives "predicted right/wrong" copy), `data.insights.tips` (3 items, split into lifestyle/body/mind cards).
120
+
121
+ **Templates:** always `templates/sanusx/*`, regardless of `clientName`.
122
+
123
+ **Notable quirks — more fragile than the other three pipelines:**
124
+ - **Ignores `clientName` for branding entirely.** Config is hardcoded to `require('../config/sanusx.json')`. The `client` parameter is only used to add a CSS class (`$(".score-main").addClass(client)`) that currently matches no rule in `assets/sanusx/css/sanusx_report.css` — a no-op.
125
+ - **RTL is broken/incomplete.** There's a comment (`/*check if the language is an RTL language*/`) with no logic after it — `templates/sanusx/ltr_no_pages.html` is loaded unconditionally regardless of `language`. `templates/sanusx/rtl_no_pages.html` exists on disk but references asset paths from the *wellbeing* report (`assets/wellbeing/css/nasco_report.css`) that don't exist under `assets/sanusx/` — it is dead, unreachable code, not a working alternative.
126
+ - **`page.pdf({ pageRanges: '1', ... })` hard-limits output to one page** — any overflow (e.g. from longer translated strings) is silently dropped rather than flowing to a second page.
127
+ - `templates/sanusx/blocks/tips.html` exists on disk but is never read by this generator (dead file) — the actual tips rendering is done by an in-file `renderInsightTips()` function building HTML via string concatenation.
128
+ - Reuses `sendNascoEmail` (named after a different client) for its email flow, same as the wellbeing and corporate pipelines — a shared helper with a client-specific name baked into shared infrastructure.
@@ -0,0 +1,67 @@
1
+ # 5. Questions, Answers & the Data Model
2
+
3
+ This section directly answers the question: **"How do we render a question, and where does the question text come from?"**
4
+
5
+ ## 5.1 The headline fact: there is no question bank in this codebase
6
+
7
+ This package does not store, own, or look up questionnaire questions. There is no database, no question-ID → question-text mapping, no survey-definition file anywhere in the repository. **Every question's title text and its answer value arrive together, pre-rendered, as flat `{title, value}` pairs inside the JSON payload the host application sends in.** This package's job is purely to lay that text out on the page — not to know what the questions mean, score them, or validate them.
8
+
9
+ This matters operationally: if a question's wording is wrong in a PDF, the fix is almost never in this repository — it's in whatever upstream system assembled the `Profile`/`profileInfo` array before base64-encoding it and calling this library.
10
+
11
+ ## 5.2 Where question/answer data lives, per pipeline
12
+
13
+ ### Core "Medicus" lab report — no questions at all
14
+
15
+ This pipeline's domain model is biomarkers/panels/insights/doctor-notes/signatures (see [doc 4.A](04-report-pipelines.md#4a-core-medicus-lab-report)). Grepping the entire core renderer for "question" or "answer" returns nothing. If you need to document or debug question rendering, **you are in the wrong pipeline** — go to the wellbeing report instead.
16
+
17
+ ### Wellbeing report — `data.Profile` (the actual Q&A) and `data.profileInfo` (metadata)
18
+
19
+ Two separate arrays, both flat `{title, value}` lists, both only meaningfully populated when `data.IsDoctor === true`:
20
+
21
+ ```js
22
+ // testing-reports/mediclinic/doctor-data.js — a real fixture used by test.js
23
+ {
24
+ "IsDoctor": true,
25
+ "Profile": [
26
+ { "title": "What is your gender?", "value": "Male" },
27
+ { "title": "When is your birthday?", "value": "51 years" },
28
+ { "title": "What is your height?", "value": "178 cm" },
29
+ { "title": "Do you have any of the following conditions?", "value": "Asthma" },
30
+ { "title": "Do you have a family history of any condition?",
31
+ "value": "Diabetes: No one | High blood pressure: No one" },
32
+ { "title": "Over the last 2 weeks, how often have you been bothered by feeling down, depressed, or hopeless?",
33
+ "value": "More than half the days" },
34
+ // ... a full PHQ-9 / GAD-7 style clinical questionnaire, plus lifestyle questions
35
+ ]
36
+ }
37
+ ```
38
+
39
+ - **`data.Profile`** — the real questionnaire content (PHQ‑9/GAD‑7-style clinical questions plus lifestyle questions). Rendered by `renderDoctorDetails(doctorData)` in `lib/wellbeing_report_generator.js`: each item becomes a two-column row — `.question-title` for `item.title`, `.details-value` for `item.value` — under a "Quiz Answers" header, injected into `.doctor-details` in the page DOM. **Neither the question title nor the answer value is HTML-escaped here** — see the security note in 5.4.
40
+ - **`data.profileInfo`** — patient/report metadata (LAB, Reference, Patient name, Report date, Date of birth, Weight, Height, Gender, BMI, Blood Pressure, Waist Circumference), *not* questionnaire answers. Split by array index: the first 4 entries go through `generateHeaderInfo()` (whose output is actually **dead** — its target `.report-info` is commented out of every brand's `header.html`), the rest go through `renderPatientTable()`, which *does* escape the label (not the value) and is injected live into `.patient-table`.
41
+ - Numeric wellbeing **scores** (as opposed to raw Q&A text) come from a different part of the payload — `data.wellbeing.calculation` / `data.wellbeing.partsScore` — pre-computed upstream; this package does not calculate PHQ‑9/GAD‑7 scores itself, it only displays whatever score value it's handed.
42
+
43
+ ### `lib/big_integral_questionnaire.js` — a cautionary example, not a real data source
44
+
45
+ As covered in [doc 4.B.1](04-report-pipelines.md#4b1-libbig_integral_questionnairejs--read-this-before-assuming-its-a-real-feature), this module currently renders **100% hardcoded dummy data** (`getDummyData()` — 11 fixed rows, `result: 0` for all of them, a fake patient name). It is not wired to any real answer data in the payload today. Do not use it as a reference for "how question rendering should work" — use `renderDoctorDetails`/`data.Profile` instead.
46
+
47
+ ### Corporate report and SanusX — no free-text Q&A
48
+
49
+ Neither of these two pipelines renders question/answer pairs in the `data.Profile` sense. The corporate report works with pre-aggregated `data.elements[]` (population-level summaries, chart values, gender breakdowns — see [doc 4.C](04-report-pipelines.md#4c-corporate-report)). SanusX works with pre-computed scores and flags (`highestAvatarScorePartId`, `isPredictiveElement` — see [doc 4.D](04-report-pipelines.md#4d-sanusx-report)). Both assume all interpretation/scoring already happened upstream.
50
+
51
+ ### "SmartReport" biomarker notes — a different kind of "note," not a question
52
+
53
+ When a wellbeing report is merged with lab data (`data.SR`, see [doc 4.B](04-report-pipelines.md#4b-wellbeing-report--optional-merged-smartreport)), individual biomarkers can carry `doctorNote`/`biomarkerNote`/`insight` entries. These are clinician-authored free-text notes attached to a specific lab value — a different concept from a questionnaire answer — rendered via `renderReportItems`/`renderNoteV3` in `lib/template.js`, not via `renderDoctorDetails`.
54
+
55
+ ## 5.3 Summary
56
+
57
+ - **`data.Profile`** (Wellbeing) — real questionnaire Q&A (title + value pairs). Rendered by `renderDoctorDetails` → `.doctor-details`. **Not** HTML-escaped.
58
+ - **`data.profileInfo`** (Wellbeing) — patient/report metadata, not Q&A. Rendered by `renderPatientTable` (live) / `generateHeaderInfo` (dead code). Label is escaped; value is not.
59
+ - **`data.wellbeing.calculation` / `.partsScore`** (Wellbeing) — pre-computed numeric scores. Rendered as score bars/gauges. N/A for escaping (numeric).
60
+ - **`big_integral_questionnaire.js` internal `getDummyData()`** (Wellbeing, Maison Sante only) — hardcoded demo rows, **not real answers**. Rendered by `renderTable`.
61
+ - **`data.SR.items[].biomarkerNote` / `.doctorNote` / `.insight`** (Wellbeing, SmartReport merge) — clinician notes on a lab value. Rendered by `renderReportItems`/`renderNoteV3` (`lib/template.js`). Escaping varies.
62
+ - **`data.elements[]`** (Corporate) — pre-aggregated population analytics. Rendered by `renderBarChart` and table builders.
63
+ - **`data.PartScoresHighestValues`, `data.insights.tips`** (SanusX) — pre-computed scores/flags. Rendered by the avatar/predictions/tips sections.
64
+
65
+ ## 5.4 Security note: unescaped answer text
66
+
67
+ `renderDoctorDetails` (wellbeing `data.Profile`) injects `item.value` — and, in one further case, `item.title` — as raw HTML with no escaping. If any upstream system ever allows free-text answers containing HTML/script-like content to flow into this field unsanitized, that content will be injected verbatim into the rendered page before Puppeteer snapshots it to PDF. Because report generation happens server-side inside a headless Chrome instance (not a browser the end user controls), this is not a classic reflected-XSS-in-the-browser risk to the *end user*, but it is a real risk if payload data can come from a less-trusted source than the doctor/clinician — e.g. if patient-entered free text were ever passed straight through into `Profile[].value` without sanitization upstream. If you're auditing security boundaries, treat "who is allowed to populate `data.Profile`, and is it sanitized before it reaches this library" as an open question to raise with the host application team — this library does not sanitize it.
@@ -0,0 +1,59 @@
1
+ # 6. Internationalization (i18n) & RTL
2
+
3
+ ## 6.1 Library and the two independent configurations
4
+
5
+ The project uses the **`i18n` npm package** (`"i18n": "^0.8.3"` in `package.json`, resolving to `0.8.6` installed) — not `i18n-nodejs`, not a custom framework. There are **two independently configured instances** of this same package, each a process-wide singleton:
6
+
7
+ - **`app/i18n.config.js`** — `locales`: `en`, `de`, `fr`, `ar-AE`, `ar-SA`, `pt`, `zh-CN`. Directory: `locales/`. Used by: `lib/pdf_generator.js` / `.min.js` (core report) only.
8
+ - **`app/i18n_wellbeing.config.js`** — `locales`: `en`, `de`, `fr`, `ar-AE`, `ar-SA`, `pt`, `zh-CN`, `tr`, `es`. Directory: `locales/wellbeing/`. Used by: `wellbeing_report_generator.js`, `template.js`, `corporate_report_generator.js`, `sanusx_report_generator.js`.
9
+
10
+ **Every generator except the core lab report pulls its strings from `locales/wellbeing/`, not `locales/`** — including the corporate report and SanusX report, despite neither being "wellbeing" products. This is a non-obvious naming trap when you're hunting for a string to translate: check which config the generator you're editing actually requires before assuming which locale folder to edit.
11
+
12
+ Both configs also set `cookie: 'currentLang'`, `queryParameter: 'lang'`, and alias the translate API to `translate`/`translateN` instead of the package default `__`/`__n`. Neither sets `objectNotation`, `fallbacks`, or `updateFiles`, so package defaults apply (see 6.5).
13
+
14
+ Because Node caches `require()`d modules, every file that requires either config gets the **same shared mutable object** — locale state is process-global, not per-request. See 6.6.
15
+
16
+ ## 6.2 `app/services/localeService.js`
17
+
18
+ A thin wrapper class, instantiated separately by each generator with its own `i18n` singleton:
19
+
20
+ - `t(key, args)` → `i18nProvider.__(key, args).message` — the actual string getter. In practice, `args` is never passed at any call site in this codebase — the package's built-in `%s`/vsprintf-style interpolation is wired up but unused.
21
+ - `setLocale(locale)` → **only** calls through to the underlying `i18n.setLocale(locale)` if `locale` is already in that instance's configured `locales` array; otherwise it silently no-ops (see 6.5 for why this matters).
22
+ - `getLocales()`, `getCurrentLocale()`, `translatePlurals()` — thin proxies.
23
+
24
+ Instead of the package's own `%s` interpolation, this codebase uses ad hoc single-token placeholders baked directly into translation strings and manually substituted at the call site, e.g. `localeService.t('predictionsDesc').replace("{$val}", finalScore)`.
25
+
26
+ `lib/template.js` additionally has a second, parallel string-getter, `getLocale(key)`, used at a couple of call sites instead of `localeService.t(key)` — worth confirming both resolve against the same underlying data before refactoring either.
27
+
28
+ ## 6.3 Locale JSON shape
29
+
30
+ Flat (no nesting — `objectNotation` is off), but each value is normally an **object**, not a plain string:
31
+
32
+ ```json
33
+ // locales/en.json
34
+ "APPROVED BY": { "message": "Approved", "description": "before the doctor signature..." }
35
+ ```
36
+
37
+ `LocaleService.t()` must append `.message` itself because of this shape. `locales/en.json` covers header/report labels, section titles, footer/pagination text, biomarker table headers, month names, and a `bigIntegral*` block. `locales/wellbeing/en.json` (424 lines) covers patient-header labels, section labels (`lifeStyle`/`body`/`mind`/`psychological`/`physical`), disclaimer text, corporate-report strings (`Copyright`, `Participant Analytics`), email body strings, and SanusX avatar/prediction content (including the `{$val}`/`{$value}` tokens mentioned above).
38
+
39
+ ## 6.4 RTL handling — three overlapping mechanisms
40
+
41
+ 1. **Whole alternate template files.** Every report family ships a matched pair, `rtl_no_pages.html` / `ltr_no_pages.html` (also under each brand subfolder). The RTL variant hardcodes `<html dir="rtl" lang="ar">` and loads a dedicated `assets/arabic.min.css` alongside the normal stylesheet.
42
+ 2. **Alternate block partials for Arabic.** The core report swaps in `templates/blocks/ar-first-page-header.html`, `ar-header.html`, `ar-footer.html`, `biomarker-compact-ar.html` when RTL.
43
+ 3. **Inline CSS direction/attribute switching** for pieces that aren't full templates — dozens of `isRtl ? ... : ...` inline-style branches (float/padding/text-align) scattered through `lib/template.js` and the report generators.
44
+
45
+ **RTL detection is a substring test:** `language.indexOf('ar') !== -1`, applied independently in the core report, wellbeing generator, and corporate report generator. In **all three** of those, once `isRtl` is true, **the language is forcibly overwritten to `'ar-SA'`**, regardless of whether the caller actually asked for `ar`, `ar-AE`, or `ar-SA`. Practical effect: **`locales/ar-AE.json` / `locales/wellbeing/ar-AE.json` are functionally unreachable** through those three pipelines — only `sanusx_report_generator.js` skips this coercion (though SanusX has its own, separate RTL problem — see [doc 4.D](04-report-pipelines.md#4d-sanusx-report): it never computes an `isRtl` flag at all and always loads the LTR template).
46
+
47
+ ## 6.5 Fallback logic — two layers that disagree
48
+
49
+ - **Package layer:** if a requested locale isn't in the configured `locales` list and there's no `fallbacks` entry (there never is, here), `i18n`'s own `setLocale`/`translate` internals force the locale to `defaultLocale` (`'en'`) — the behavior you'd naively expect.
50
+ - **`LocaleService` layer:** `setLocale()` only forwards to the package **if** the requested locale is already in `getLocales()`. If it isn't, the package's `setLocale` is **never called at all**, so its own fallback-to-`en` logic never runs — **the previously active locale on the shared singleton simply stays in effect.** Net effect: requesting an unsupported locale does not deterministically fall back to English; it silently reuses whatever locale a prior call last set on that same process-global singleton.
51
+ - Separately, the package's `updateFiles: true` default means a **missing translation key** (not a missing locale) causes the literal key string to be auto-persisted back into that locale's JSON file on disk the first time it's looked up, rather than falling back to another locale's text. This is visible today in `locales/wellbeing/en.json`, where a few keys near the bottom (e.g. `bigIntegralSystem`) are stored as bare strings rather than the usual `{message, description}` object — almost certainly auto-written by a prior lookup from the (currently unused) `big_integral_questionnaire.js` locale hook. **If that hook is ever wired up for real, `.message` access on these bare-string entries will return `undefined`, producing blank text in the PDF** — worth fixing (wrap them as proper `{message, description}` objects) before relying on them.
52
+
53
+ ## 6.6 Gotchas for a new engineer
54
+
55
+ - **Global, non-request-scoped locale state.** `i18n.setLocale(...)` mutates a shared, cached singleton. If two report-generation calls run concurrently in the same Node process (this package does not enforce single-threaded/sequential use), one request's language can leak into another's mid-render. There is no per-request locale scoping in use, though the `i18n` package does support it (passing a `req`/`res`-like context) — it's simply not adopted here. This is the same class of bug the codebase already had to specifically work around for temp file paths in `generateFullPdf` (see [doc 1.7](01-architecture-overview.md#17-filetemp-file-handling--an-evolving-pattern-worth-knowing)) — locale state was not given the same treatment.
56
+ - **Orphaned locale files.** `locales/ar.json` exists on disk but is **not** in `app/i18n.config.js`'s `locales` array, and — telling sign — its key set matches the *wellbeing* schema, not the main lab-report schema, suggesting it was dropped in the wrong directory. `locales/wellbeing/it-IT.json` similarly exists but `'it-IT'` is absent from `app/i18n_wellbeing.config.js`'s `locales` array. Neither file is ever loaded.
57
+ - **`ar-AE` is effectively dead** in three of the four generators (see 6.4).
58
+ - **Dead RTL template:** `templates/sanusx/rtl_no_pages.html` is never read by any code path.
59
+ - If you add a new locale, you must add it to **both** the correct `locales` array *and* the `LocaleService`'s implicit gate (it derives from the same array, so this is really one step) — but remember to check whether the RTL-coercion-to-`ar-SA` logic needs updating too if it's a new Arabic variant.
@@ -0,0 +1,59 @@
1
+ # 7. Email Notifications
2
+
3
+ ## 7.1 Module and exports
4
+
5
+ `lib/sendEmail.js` uses **nodemailer** and exports two functions:
6
+
7
+ - **`sendNascoEmail(data, pdfAttachmentPath, mailConfig, clientName)`** — sends a report email with the generated PDF attached. Used by `generateNascoPDF`, `generateFullPdf`, and `generateSanuxPDF` in `index.js` (i.e. shared across the wellbeing, extended-wellbeing, and SanusX flows, despite the "Nasco" name).
8
+ - **`sendEmailNotification(data)`** — a generic notification email, no PDF pipeline attached. Wired to `index.js`'s exported `sendEmail(json)`.
9
+
10
+ ## 7.2 SMTP transport configuration — entirely caller-supplied
11
+
12
+ There are **no environment variables and no hardcoded SMTP credentials** anywhere in this module. The transport is built directly from a `mailConfig` object that the caller constructs and passes in:
13
+
14
+ ```js
15
+ // index.js — constructed identically in generateNascoPDF, generateFullPdf, generateSanuxPDF
16
+ let mailConfig = {
17
+ host: reportData.host,
18
+ port: reportData.port,
19
+ authUser: reportData.authUser,
20
+ authPass: reportData.authPass,
21
+ sendFromEmail: reportData.sendFromEmail,
22
+ secure: reportData.secure
23
+ }
24
+ ```
25
+
26
+ ```js
27
+ // lib/sendEmail.js
28
+ var transporter = nodemailer.createTransport({
29
+ host: mailConfig.host,
30
+ port: mailConfig.port,
31
+ secure: false, // NOTE: hardcoded, see below
32
+ pool: true,
33
+ requireTLS: true,
34
+ maxConnections: 20,
35
+ maxMessages: 1,
36
+ auth: { user: mailConfig.authUser, pass: mailConfig.authPass }
37
+ });
38
+ ```
39
+
40
+ **Known bug:** `mailConfig.secure` is collected from the caller in all three call sites but **never actually read** by `sendMail` — the transporter hardcodes `secure: false` and instead forces a STARTTLS upgrade via `requireTLS: true`. So the connection isn't unencrypted (an unsupportable-TLS server will fail closed), but a caller who explicitly sets `secure: true` expecting implicit TLS (port-465 style) gets no such thing — the flag is dead code. If you ever need genuinely implicit-TLS support, this is where to add it.
41
+
42
+ `maxMessages: 1` combined with `pool: true` means a new SMTP connection/handshake is opened per message despite pooling being enabled — a minor inefficiency, not a security issue.
43
+
44
+ `sendEmailNotification` builds its own, differently-shaped `mailConfig` inline from the same style of caller-supplied fields.
45
+
46
+ ## 7.3 Email content
47
+
48
+ - **Attachment:** yes — the generated PDF, attached by **file path** (nodemailer reads it from disk, not from an in-memory buffer): `attachments: [{ path: pdfAttach, filename: data.name + " WellbeingReport.pdf", contentType: "application/pdf" }]`. In the `generateFullPdf` flow, this path is a per-call, UUID-suffixed temp file that gets unlinked after send (see [doc 1.7](01-architecture-overview.md#17-filetemp-file-handling--an-evolving-pattern-worth-knowing)); in the older `generateNascoPDF`/`generateSanuxPDF` flows it's a fixed shared filename under `output/`.
49
+ - **Subject/body (`sendNascoEmail`):** subject is a fixed, localized string (`localeService.t('wellbeingReport')`); the body is a hardcoded, i18n-translated multi-paragraph template — genuinely translated, but not caller-customizable beyond the patient's name.
50
+ - **Subject/body (`sendEmailNotification`):** fully caller-driven — `data.emailSubject`, plus `customHeaderHTML`/`customBodyHTML`/`customFooterHTML` override fields.
51
+ - **Branding:** `sendNascoEmail` pulls `logo`, `signature`, and a color from `config/{clientName}.json` (falling back to `config/default.json`) — using the client string **as-is, with no case-normalization and no `pha`→`Pha` mapping** (a third, different resolution rule from the ones in [doc 2.2](02-configuration-and-whitelabelling.md#22-how-clientclientname-resolves-to-a-config-file--and-why-its-inconsistent) — the caller must pass the exact on-disk filename casing here).
52
+ - **RTL:** email HTML uses a `lang === "ar"` check to flip `float`/`text-align`/`direction` in its hand-built inline styles — a simpler mechanism than the report templates', and using a different placeholder syntax: single-brace `{token}` (e.g. `{logo}`, `{header_text}`), not the `{{double-brace}}` convention used in the PDF templates. Don't apply PDF-template assumptions when editing email strings.
53
+
54
+ ## 7.4 Security considerations
55
+
56
+ - **SMTP credentials travel in plaintext inside the request payload.** `authUser`/`authPass` are plain JSON fields on the same object that's only base64-*encoded* (not encrypted) end to end — see [doc 1.3](01-architecture-overview.md#13-entry-points-indexjs). Anyone who can observe the call to this library (or a log of it) can trivially recover the SMTP password. If `isDebugging` logging is ever extended to dump the full `reportData`/`mailConfig` object, credentials would land in `output/LOGS-*.txt` in plaintext.
57
+ - **No enforcement of the caller's `secure` intent** — see 7.2. This is a config-drift risk more than an active vulnerability (the connection still requires TLS via `requireTLS`), but it means the `secure` field is misleading and should either be honored or removed from the API surface.
58
+ - **No hardcoded credentials found** in this module. The only hardcoded value is the fallback sender address `noreply@medicus.ai`, which is not a secret.
59
+ - **Recommendation for anyone hardening this:** if the host application controls both ends, consider moving SMTP credentials to a per-deployment secret/environment variable inside the *host* app rather than passing them through this library's payload on every call — this library has no mechanism for that today, so it would be a host-application-level change, not a change to this package's public API shape necessarily, but worth raising with whoever owns the calling system.
@@ -0,0 +1,41 @@
1
+ # 8. Known Issues & Technical Debt
2
+
3
+ Consolidated from all the research behind this documentation set, ranked roughly by how much time/pain each one can cost a new engineer. Each links to the doc section with full detail.
4
+
5
+ ## Critical — will actively bite you
6
+
7
+ 1. **`index.js` requires `lib/pdf_generator.min.js`, not `lib/pdf_generator.js`.** The two files have diverged independently since 2021 with no build step connecting them. Editing the readable source file silently does nothing. → [doc 1.4](01-architecture-overview.md#14-critical-fact-the-core-report-renderer-that-actually-runs-is-pdf_generatorminjs-not-pdf_generatorjs)
8
+ 2. **SMTP credentials travel as plaintext JSON fields** inside a payload that's only base64-*encoded*, not encrypted. A logged request or an intercepted call leaks the SMTP password outright. → [doc 7.4](07-email-notifications.md#74-security-considerations)
9
+ 3. **Client-name → config-file resolution is inconsistent across all five places that do it** (wellbeing generator, corporate generator, sendEmail, sanusx generator, core report which doesn't do it at all). Onboarding a new brand or debugging "why isn't my branding showing" requires checking the exact rule for the specific generator in use. → [doc 2.2](02-configuration-and-whitelabelling.md#22-how-clientclientname-resolves-to-a-config-file--and-why-its-inconsistent)
10
+ 4. **`corporate_report_generator.js` lower-cases the client string before building a filename path**, but the actual `config/*.json` files are capitalized. Works today only because of case-insensitive filesystems (Windows/macOS); will silently fall back to `default.json` on Linux. → [doc 2.2](02-configuration-and-whitelabelling.md#22-how-clientclientname-resolves-to-a-config-file--and-why-its-inconsistent)
11
+ 5. **`generateFullPdf`'s `showHeaderLogo || true` bug** — the header logo can never be suppressed regardless of what the caller passes. → [doc 4.B](04-report-pipelines.md#4b-wellbeing-report--optional-merged-smartreport)
12
+
13
+ ## High — will cost real debugging time
14
+
15
+ 6. **`sanusx_report_generator.js` ignores its `clientName` parameter for branding entirely** — always loads `config/sanusx.json`. Effectively single-brand despite accepting a client argument. → [doc 4.D](04-report-pipelines.md#4d-sanusx-report)
16
+ 7. **SanusX RTL support is broken** — no `isRtl` flag is ever computed, the LTR template loads unconditionally, and the RTL template that does exist on disk references asset paths that don't exist under `assets/sanusx/`. → [doc 4.D](04-report-pipelines.md#4d-sanusx-report)
17
+ 8. **SanusX's PDF is hard-limited to one page** (`pageRanges: '1'`) — any overflow is silently dropped, not pushed to page 2. → [doc 4.D](04-report-pipelines.md#4d-sanusx-report)
18
+ 9. **`lib/big_integral_questionnaire.js` renders 100% hardcoded dummy data**, not real patient answers. Easy to mistake for a real, working feature. → [doc 4.B.1](04-report-pipelines.md#4b1-libbig_integral_questionnairejs--read-this-before-assuming-its-a-real-feature)
19
+ 10. **Locale state is a process-global mutable singleton** (`i18n.setLocale(...)` mutates a shared, cached module). Concurrent report-generation calls in the same process can leak one request's language into another's render. The codebase already had to specifically patch around an analogous bug for temp-file paths (`generateFullPdf`'s UUID scheme) but never applied the same fix to locale state. → [doc 6.6](06-internationalization.md#66-gotchas-for-a-new-engineer)
20
+ 11. **Older flows (`generateMedicusPDF`, `generateNascoPDF`, `generateSanuxPDF`) write to fixed, shared filenames under `output/`** with no cleanup — concurrent requests can clobber each other's files, and disk usage grows unbounded over time. Only `generateFullPdf` was hardened with per-call UUID temp paths + cleanup. → [doc 1.7](01-architecture-overview.md#17-filetemp-file-handling--an-evolving-pattern-worth-knowing)
21
+ 12. **`LocaleService.setLocale()` silently no-ops on an unsupported locale** instead of falling back to English like the underlying package would — the previously active locale on the shared singleton just stays in effect. → [doc 6.5](06-internationalization.md#65-fallback-logic--two-layers-that-disagree)
22
+ 13. **`renderDoctorDetails` (wellbeing `data.Profile`) does not HTML-escape answer values** — a real risk if any upstream source of Q&A text isn't already sanitized. → [doc 5.4](05-questions-and-data-model.md#54-security-note-unescaped-answer-text)
23
+
24
+ ## Medium — worth fixing, lower urgency
25
+
26
+ 14. **No fallback from a brand template folder to shared `templates/blocks/`** for individual missing blocks — the "extended" wellbeing flow throws `ENOENT` if a client folder is incomplete or missing entirely; only `Mediclinic`, `Pha`, `maisonsante` are supported today. → [doc 2.4](02-configuration-and-whitelabelling.md#24-template-folder-selection--no-blocks-level-fallback)
27
+ 15. **Two independent copies of Chart.js** (npm `chart.js`, unused/dead server-side import, vs. bundled `assets/charts.min.js`, actually used client-side inside headless Chrome) can silently drift out of version sync. → [doc 3.4](03-templating-and-rendering.md#34-charts)
28
+ 16. **`generatePatientQR`/`generateQrCode` doesn't generate QR codes** — it's a generic "rasterize this HTML string" utility; the caller must pre-render the QR (e.g. as inline SVG) before calling it. Misleading name. → [doc 3.5](03-templating-and-rendering.md#35-qr-codes--two-unrelated-mechanisms)
29
+ 17. **Orphaned/dead locale files**: `locales/ar.json` (wrong schema for its directory, not in the configured locales list) and `locales/wellbeing/it-IT.json` (not in the configured locales list). → [doc 6.6](06-internationalization.md#66-gotchas-for-a-new-engineer)
30
+ 18. **`ar-AE` is functionally unreachable** in three of the four generators — RTL detection coerces any `"ar"`-containing language straight to `ar-SA`. → [doc 6.4](06-internationalization.md#64-rtl-handling--three-overlapping-mechanisms)
31
+ 19. **Auto-write-on-missing-key behavior** (`i18n`'s `updateFiles: true` default) has already silently written malformed entries into `locales/wellbeing/en.json` (bare strings instead of `{message, description}` objects) from a currently-unused lookup path — a landmine if that lookup is ever wired up for real. → [doc 6.5](06-internationalization.md#65-fallback-logic--two-layers-that-disagree)
32
+ 20. **Corporate report has a live external CDN dependency at render time** (`page.addScriptTag({ url: 'https://cdn.jsdelivr.net/...' })`) — unlike the other three pipelines, which bundle their own JS/CSS locally. A network outage or CDN change could break corporate-report generation specifically. → [doc 4.C](04-report-pipelines.md#4c-corporate-report)
33
+ 21. **Onboarding a new brand's corporate-report cover background requires editing shared CSS by client-name selector** (`.cover-overlay-container.bionext {...}`), bypassing the config-JSON theming model otherwise used everywhere else. → [doc 4.C](04-report-pipelines.md#4c-corporate-report)
34
+ 22. **Corporate/SanusX translation strings live in the `locales/wellbeing/` bucket**, not a dedicated namespace, despite neither product being "wellbeing." → [doc 6.1](06-internationalization.md#61-library-and-the-two-independent-configurations)
35
+ 23. **`sendNascoEmail` is reused across wellbeing, extended-wellbeing, and SanusX flows** despite its Nasco-specific name — a shared helper with a misleading, client-specific name baked into common infrastructure. → [doc 7.1](07-email-notifications.md#71-module-and-exports)
36
+ 24. **Dead code accumulation**: unused `templates/base.html`/`template.html`/`ltr.html`/`no_pages.html`/`empty.html`/`first_page_head.html`; `templates/popup/popup-template.html` (unreferenced anywhere); `templates/sanusx/blocks/tips.html` (unreferenced — a different `tips.html` under `templates/wellbeing/blocks/` is the one actually read); `combinePDFBuffers`/`isEmpty` copy-pasted but unused in `corporate_report_generator.js`; `generateHeaderInfo()`'s output computed but discarded (target selector commented out); `historyData` always an empty array, returned but never populated, across all three non-core generators; dead `chart.js`/`qr-image`/`qrcode` npm imports server-side. None of these are actively harmful, but they add noise when searching the codebase and should be pruned opportunistically.
37
+ 25. **`mailConfig.secure` is collected from every caller but never read** by the actual transport — dead parameter, misleading API surface. → [doc 7.2](07-email-notifications.md#72-smtp-transport-configuration--entirely-caller-supplied)
38
+
39
+ ## Suggested first fixes if you're picking one place to start
40
+
41
+ If asked to spend a day improving this codebase's reliability rather than adding features, the highest-leverage fixes are (1) wiring an actual build step (or simply deleting `pdf_generator.js` and renaming `.min.js`) to remove the source/runtime divergence risk, (3)/(4) unifying client-name resolution into one shared helper function used by all five call sites, and (10) scoping locale state per-request instead of relying on a shared singleton.
package/docs/README.md ADDED
@@ -0,0 +1,22 @@
1
+ # Medicus PDF Generator — Documentation
2
+
3
+ This is the full technical documentation for `@medicus.ai/medicus-report-pdf-generator`, a Node.js library that turns JSON health-report data into branded PDF documents (and, for one flow, emails them out).
4
+
5
+ **Read this first:** [`08-known-issues-and-technical-debt.md`](08-known-issues-and-technical-debt.md) and the project-root [`HANDOVER.md`](../HANDOVER.md) contain the handful of facts that will save you the most debugging time (in particular: `index.js` runs `lib/pdf_generator.min.js`, not the readable `lib/pdf_generator.js`).
6
+
7
+ ## Contents
8
+
9
+ 1. [Architecture Overview](01-architecture-overview.md) — what this package is, module map, entry points, dependencies, data flow.
10
+ 2. [Configuration & Whitelabelling](02-configuration-and-whitelabelling.md) — the `config/` system, how a `client` string selects branding/templates, per-generator inconsistencies.
11
+ 3. [Templating & Rendering Pipeline](03-templating-and-rendering.md) — how HTML is built from data (no template engine — string replace + jsdom/jQuery), how it becomes a PDF (Puppeteer), charts, QR codes.
12
+ 4. [Report Pipelines](04-report-pipelines.md) — the four distinct report generators (Core "Medicus", Wellbeing, Corporate, SanusX): purpose, entry points, data shape, brand support.
13
+ 5. [Questions, Answers & the Data Model](05-questions-and-data-model.md) — directly answers "where do questions come from and how are they rendered."
14
+ 6. [Internationalization (i18n) & RTL](06-internationalization.md) — locale system, Arabic/RTL handling, gotchas.
15
+ 7. [Email Notifications](07-email-notifications.md) — `lib/sendEmail.js`, SMTP config, security notes.
16
+ 8. [Known Issues & Technical Debt](08-known-issues-and-technical-debt.md) — consolidated list of bugs, dead code, and risks found while writing this documentation.
17
+
18
+ ## What this project is NOT
19
+
20
+ - It is **not a web server**. There is no HTTP listener, no routes, no session handling anywhere in this repository. It is a plain npm library — `index.js` exports async functions that a **host application** (e.g. a Meteor app, per the README) calls directly in-process, or via the `run.js` child-process wrapper.
21
+ - It has **no authentication or authorization layer**. There are no auth-related dependencies in `package.json` (no JWT, no passport, no session store). Any access control, user identity, or permission checking is entirely the host application's responsibility — this package trusts whatever JSON payload it is handed.
22
+ - It has **no database**. Every function call is stateless: it receives a JSON payload (usually base64-encoded), renders it, and returns a PDF/HTML/base64 string. Nothing is persisted except transient files under `output/` or the OS temp directory.
package/index.js CHANGED
@@ -313,6 +313,7 @@ module.exports = {
313
313
  }
314
314
  },
315
315
 
316
+ //not used
316
317
  generateSanuxPDF: async (data, isDebugging, isDownloadable, shouldSendEmail) => {
317
318
  let reportData = JSON.parse(data)
318
319
  let base64Object = reportData.data
@@ -396,19 +397,19 @@ module.exports = {
396
397
  return base64data;
397
398
  }
398
399
  },
399
-
400
+ //not used
400
401
  sendEmail: async (json) => {
401
402
  const decodedJSON = JSON.parse(json);
402
403
  const email = await sendEmailNotification(decodedJSON)
403
404
  return email
404
405
  },
405
-
406
+ //not used
406
407
  generateQrCode: async (json) => {
407
408
  let fileBuffer = await generatePatientQR(json)
408
409
  const base64data = Buffer.from(fileBuffer, 'utf8').toString('base64');
409
410
  return base64data
410
411
  },
411
-
412
+ //not used
412
413
  generateCorporateReportPDF: async (json, isDebugging, isDownloadable) => {
413
414
 
414
415
  let LOGS = '';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@medicus.ai/medicus-report-pdf-generator",
3
- "version": "1.3.13",
3
+ "version": "1.3.14",
4
4
  "description": "Nasco corporate report - latest update in 12/10/2023 - Fix HRC for bionext",
5
5
  "main": "index.js",
6
6
  "scripts": {