The api now depends on @dorfteich/plugin-sdk, so the api Dockerfile must
copy and build it alongside shared — the CD "Build and push images" (and
CI "Build container images") step failed without it. Also pre-create
/data/plugins so the plugins named volume is writable by the non-root user,
mirroring /data/uploads.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EwZ4jR4KFAPvpjWevfUGX1
Server-side PDF export for reading/sharing (ADR 0009), rendered by a new
internal Gotenberg (headless Chromium) sidecar.
- Sidecar: `gotenberg/gotenberg:8` in the compose stack (internal, pinned,
healthcheck); api `GOTENBERG_URL` env; a `renderer` readyz check at
warning-level (mirrors the converter) so PDF export degrades gracefully when
Gotenberg is down without failing readyz.
- Export HTML: `buildPdfHtml` renders a self-contained document (no app chrome)
— the page's content with images inlined as data URIs, the pond's fonts
inlined as base64 `@font-face` + applied via CSS variables (ADR 0016), print
CSS (A4, page-break rules, a title header), and page numbers from Gotenberg's
footer. Plugin-block fallbacks are a marked TODO(#79) for M7.
- Fonts in the api image: the api Dockerfile now bakes the font catalog in
(`build-fonts.mjs` with FONTS_OUT) so the exporter can read a pond's chosen
WOFF2 and inline them; a missing file falls back to the system stack.
- Job flow: `POST /pages/:id/export {format: pdf}` builds the HTML (read
permission checked by the guard) and enqueues an `export_pdf` job on the #62
queue with the HTML as input; the worker branches `to === 'pdf'` to the
`GotenbergRenderer` (html → pdf) instead of pandoc, retrying an unreachable
sidecar and failing a refused render (`renderer_unavailable`/`render_failed`,
de+en). The client polls and downloads `GET /jobs/:id/result`.
- Frontend: the page-menu PDF button is now a real export (PDF added to
EXPORT_FORMATS; the disabled placeholder removed).
- Tests: export.service.db PDF cases (HTML has title/font-variable/inlined
image; renderer-down fails with `render_failed`); e2e PDF export self-skips
without a Gotenberg sidecar (like the .docx case). Verified locally against
real Gotenberg — a valid PDF with the pond font embedded.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EwZ4jR4KFAPvpjWevfUGX1
Implements the FileStorage abstraction (uploads/<pondId>/<fileId> on the
mounted volume), the attachments model, and POST /ponds/:id/files, GET
/media/:fileId, DELETE /files/:id. Uploads are validated by sniffing
magic bytes rather than trusting the client's Content-Type/filename
(catches a renamed .html-as-.png), checked against the max_file_bytes
and storage_bytes quotas, and served with nosniff + immutable caching.
Closes#27
Multi-stage images: web (workspace build baked into unprivileged
nginx with SPA fallback, asset caching, /healthz) and api (pnpm deploy
bundle with the prisma CLI for migrate-on-start, non-root, node-based
healthcheck). deploy/compose/docker-compose.yml defines the stage
stack (web, api, db) with frontend/internal networks, localhost-only
published ports for the host reverse proxy, log rotation, and named
volumes; .env.example documents every variable. compose.dev.yml layers
hot-reloading dev containers (or database-only usage) over the same
definition. Verified locally: full stack healthy, SPA fallback, readyz
green after automatic migration, db not reachable from outside.
Closes#6
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>