Scoresheet pipeline¶
CSV in, PDF out: CSV → Handlebars → Chromium → per-chunk PDFs → merged
PDF, in sage-tools-api/src/scoresheets/. Entirely independent of the
sync pipeline — no shared state, no shared endpoints.
Composition¶
ScoresheetConfig.mjsis the type registry — each registered type maps to atemplates/<type>.html+<type>.csspair. Four types ship today: Standard (with a referee column), No Referee, No Referee Wide, and Best of 3. Adding a new type is a new template pair and aScoresheetConfig.mjsentry — nothing else needs touching.index.mjscomposes the pipeline: parse the uploaded CSV, render each match through the selected type's Handlebars template, render the populated HTML through headless Chromium (Puppeteer) into per-chunk PDFs, merge into one final PDF.
The pipeline is wired lazily, not eagerly, in index.mjs's
composition root. Puppeteer is only imported on the first scoresheet
request — the sync pipeline is wired eagerly, so a sync-only cold start
(the common case for this service) never pays Puppeteer's import cost at
all. Don't move that import to the top level.
Where the CSV comes from¶
tools/scoresheet-generator.html can source the csv part of the request
from either an uploaded file, or — for a registered event — the matchesCsv
string in that day's published event-data snapshot
(<event-key>/data/<day-key>.json), fetched client-side and handed to the
same FormData path as a Blob. That snapshot's CSV already has the exact
header this pipeline's CsvService expects (matched by header name, not
position), so no transformation happens on either side — this pipeline has
no awareness of where its input came from. See
sage-docs/docs/specs/implemented/scoresheet-event-picker-spec.md for the picker's own
design.
Endpoints¶
POST /scoresheets/generate— multipart (csvfile +evt,type,out,blanks), returns a PDF directly.POST /scoresheets/generate/stream— same inputs, but returns NDJSON progress lines as each chunk renders, then a final{"phase":"done","pdfBase64":...}line.POST /v3/scoresheets— the/v3twin of both: the PDF, or the NDJSON stream when the request saysAccept: application/x-ndjson. This is whattools/scoresheet-generator.htmlcalls, and its progress bar reads the stream; the two routes above stay served, frozen.
Once streaming has started, the HTTP status is locked at 200 — a failure
midstream arrives as a {"phase":"error"} line in the body, not an HTTP
error status, since headers are already sent.
Deployment¶
Runs on node:22-slim (Puppeteer requires Node ≥22.12), with system
Chromium installed via apt rather than Puppeteer's own bundled download —
PUPPETEER_SKIP_DOWNLOAD + PUPPETEER_EXECUTABLE_PATH in the Dockerfile.
See Deployment for the full Cloud Run setup.
Features: Scoresheet Generator usage