From 0e7514f7b06793d788339b50137c5219bcae090f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 9 Jul 2026 14:40:12 +0200 Subject: [PATCH] fix(documents): hoist @napi-rs/canvas so pdfjs can load it in the standalone image MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PDF rasterization failed at runtime — pdfjs-dist does a bare require('@napi-rs/canvas') from its own directory, and the standalone image had no top-level @napi-rs/canvas for Node's upward resolution to find, so pdfjs could not polyfill DOMMatrix and every scanned-PDF read fell back to 'PDF scanning needs a Claude vision provider'. Hoist the canvas package and its musl binary to /app/node_modules/@napi-rs via symlink into the pnpm store (the prior per-package .node copy only fixed canvas's own loader, not pdfjs's require). Verified in the container: resolves from pdfjs and renders. --- CHANGELOG.md | 8 ++++++++ Dockerfile | 35 +++++++++++++++++++++++------------ docs/api/openapi.yaml | 2 +- package.json | 2 +- public/sw.js | 2 +- 5 files changed, 34 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e7f0bd0b..90e9a3921 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ ## [Unreleased] +## [1.28.6] — 2026-07-09 — Fix scanned-PDF reading in the container + +Reading a scanned PDF with a non-Anthropic AI provider (a signed-in subscription +model or a local vision model) failed with "PDF scanning needs a Claude vision +provider" because the PDF page renderer could not load its image library inside +the standalone container image. The library is now resolvable where the renderer +looks for it, so a scanned PDF is rendered to images and read as intended. + ## [1.28.5] — 2026-07-09 — Read documents with a subscription AI, cleaner surfaces Documents can now be read by a signed-in (subscription) AI provider — the diff --git a/Dockerfile b/Dockerfile index 7d3bcd895..3e2d2c45f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -82,18 +82,29 @@ COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static # @napi-rs/canvas (document PDF rasterization): Next's file tracer copies the -# native binary's package into the standalone tree but NOT the pnpm symlink that -# @napi-rs/canvas's loader resolves it through, so at runtime it fails with -# "Failed to load native binding" and rasterization silently degrades. Copy the -# prebuilt .node into the canvas package dir itself so the loader's built-in -# local-file fallback (`require('./skia..node')`) resolves it. Per-arch -# buildx installs only the matching musl triple, so the find is arch-correct. -RUN CANVAS_DIR="$(find ./node_modules/.pnpm -maxdepth 4 -type d -path '*@napi-rs+canvas@*/node_modules/@napi-rs/canvas' 2>/dev/null | head -1)"; \ - NODE_BIN="$(find ./node_modules/.pnpm -maxdepth 5 -name 'skia.linux-*-musl.node' 2>/dev/null | head -1)"; \ - if [ -n "$CANVAS_DIR" ] && [ -n "$NODE_BIN" ]; then \ - cp "$NODE_BIN" "$CANVAS_DIR/" && chown nextjs:nodejs "$CANVAS_DIR/$(basename "$NODE_BIN")" && \ - echo "canvas native binding staged: $(basename "$NODE_BIN") -> $CANVAS_DIR"; \ - else echo "WARN: canvas native binding not found; PDF rasterization will degrade to local text"; fi +# native binary's package into the standalone tree but NOT the pnpm symlinks that +# resolve it, so at runtime nothing can `require('@napi-rs/canvas')`. TWO +# resolutions must work: (1) pdfjs-dist does a bare `require('@napi-rs/canvas')` +# from its OWN dir — Node walks up to /app/node_modules, so the package must be +# HOISTED to the top level there (verified: without it pdfjs logs "Cannot load +# @napi-rs/canvas" → "Cannot polyfill DOMMatrix" → rasterize ReferenceError); +# (2) @napi-rs/canvas's own loader then resolves its platform binary. Hoist BOTH +# the canvas package and the musl binary package to /app/node_modules/@napi-rs +# via symlink into the .pnpm store, and also drop the prebuilt .node into the +# canvas dir so the loader's local-file fallback (`require('./skia..node')`) +# is belt-and-suspenders. Per-arch buildx installs only the matching musl triple. +RUN set -e; \ + CANVAS_DIR="$(find /app/node_modules/.pnpm -maxdepth 4 -type d -path '*@napi-rs+canvas@*/node_modules/@napi-rs/canvas' 2>/dev/null | head -1)"; \ + MUSL_DIR="$(find /app/node_modules/.pnpm -maxdepth 4 -type d -path '*@napi-rs+canvas-linux-*-musl@*/node_modules/@napi-rs/canvas-linux-*-musl' 2>/dev/null | head -1)"; \ + NODE_BIN="$(find /app/node_modules/.pnpm -maxdepth 5 -name 'skia.linux-*-musl.node' 2>/dev/null | head -1)"; \ + if [ -n "$CANVAS_DIR" ]; then \ + mkdir -p /app/node_modules/@napi-rs; \ + ln -sfn "$CANVAS_DIR" /app/node_modules/@napi-rs/canvas; \ + [ -n "$MUSL_DIR" ] && ln -sfn "$MUSL_DIR" "/app/node_modules/@napi-rs/$(basename "$MUSL_DIR")"; \ + [ -n "$NODE_BIN" ] && cp "$NODE_BIN" "$CANVAS_DIR/"; \ + chown -R nextjs:nodejs /app/node_modules/@napi-rs; \ + echo "canvas hoisted for pdfjs: $CANVAS_DIR (musl=$MUSL_DIR)"; \ + else echo "WARN: @napi-rs/canvas not found; PDF rasterization will degrade to local text"; fi # Copy Prisma for migrations (schema, migration SQL, config, engines) COPY --from=builder /app/prisma ./prisma diff --git a/docs/api/openapi.yaml b/docs/api/openapi.yaml index d45566003..e38b7d504 100644 --- a/docs/api/openapi.yaml +++ b/docs/api/openapi.yaml @@ -1,7 +1,7 @@ openapi: 3.1.0 info: title: HealthLog API - version: 1.28.5 + version: 1.28.6 description: >- Self-hosted personal-health-tracking PWA — public API surface for the iOS native client and external ingest. diff --git a/package.json b/package.json index a02003788..6baebe067 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "healthlog", - "version": "1.28.5", + "version": "1.28.6", "description": "Self-hosted personal-health-tracking PWA with Withings integration, AI insights, and doctor-report PDF export.", "license": "PolyForm-Noncommercial-1.0.0", "homepage": "https://healthlog.dev", diff --git a/public/sw.js b/public/sw.js index e6b3b6c29..48d81e877 100644 --- a/public/sw.js +++ b/public/sw.js @@ -36,7 +36,7 @@ try { // v1.4.38.4 → v1.4.42. Do not hand-edit; bump `package.json` and rebuild. const CACHE_VERSION = (typeof self !== "undefined" && self.__APP_VERSION__) || - /* @sw-version-fallback */ "v1.28.5"; + /* @sw-version-fallback */ "v1.28.6"; const STATIC_CACHE = `healthlog-static-${CACHE_VERSION}`; const PAGE_CACHE = `healthlog-pages-${CACHE_VERSION}`; // v1.18.6 — read-only data cache for a curated allowlist of safe GET `/api/*`