diff --git a/CHANGELOG.md b/CHANGELOG.md
index 90e9a3921..05b515660 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,16 @@
## [Unreleased]
+## [1.28.7] — 2026-07-09 — Scanned-PDF reading actually works
+
+The prior fix made the PDF image library loadable, but the page renderer was
+still a bundled copy that failed on a real document with a bare error, so reading
+a scanned PDF on a non-Anthropic provider still fell back to "PDF scanning needs
+a Claude vision provider". The renderer now runs the real, un-bundled module —
+the same one that renders these documents correctly in isolation — so scanned
+PDFs are read on a subscription or local vision provider. Also drops a redundant
+per-view note from the document summary panel.
+
## [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
diff --git a/Dockerfile b/Dockerfile
index 3e2d2c45f..aad0d0b4c 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -106,6 +106,18 @@ RUN set -e; \
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
+# pdfjs-dist is `serverExternalPackages` (NOT bundled — the bundled copy's render
+# path breaks in the standalone image), so the server does a runtime bare
+# `import('pdfjs-dist/legacy/build/pdf.mjs')`. Node resolves that up to
+# /app/node_modules, so hoist the real pnpm-store copy to the top level too.
+RUN set -e; \
+ PDFJS_DIR="$(find /app/node_modules/.pnpm -maxdepth 4 -type d -path '*pdfjs-dist@*/node_modules/pdfjs-dist' 2>/dev/null | head -1)"; \
+ if [ -n "$PDFJS_DIR" ]; then \
+ ln -sfn "$PDFJS_DIR" /app/node_modules/pdfjs-dist; \
+ chown -h nextjs:nodejs /app/node_modules/pdfjs-dist; \
+ echo "pdfjs-dist hoisted: $PDFJS_DIR"; \
+ else echo "WARN: pdfjs-dist 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
COPY --from=builder /app/prisma.config.ts ./prisma.config.ts
diff --git a/docs/api/openapi.yaml b/docs/api/openapi.yaml
index e38b7d504..904c2db1f 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.6
+ version: 1.28.7
description: >-
Self-hosted personal-health-tracking PWA — public API surface for the iOS native client and external ingest.
diff --git a/messages/de.json b/messages/de.json
index 77e50b4eb..9702f9964 100644
--- a/messages/de.json
+++ b/messages/de.json
@@ -8257,7 +8257,6 @@
"showText": "Erkannten Text anzeigen",
"summaryTitle": "Zusammenfassung",
"textTitle": "Erkannter Text",
- "notSaved": "Nicht gespeichert · keine Diagnose. Nur einmalig für diese Ansicht angezeigt.",
"close": "Ausblenden"
},
"ai": {
diff --git a/messages/en.json b/messages/en.json
index ab2ead205..4744fc5c8 100644
--- a/messages/en.json
+++ b/messages/en.json
@@ -8257,7 +8257,6 @@
"showText": "Show extracted text",
"summaryTitle": "Summary",
"textTitle": "Extracted text",
- "notSaved": "Not saved · not a diagnosis. Shown once for this view only.",
"close": "Hide"
},
"ai": {
diff --git a/messages/es.json b/messages/es.json
index 349d0c9ed..e2056fac8 100644
--- a/messages/es.json
+++ b/messages/es.json
@@ -8257,7 +8257,6 @@
"showText": "Mostrar texto extraído",
"summaryTitle": "Resumen",
"textTitle": "Texto extraído",
- "notSaved": "No se guarda · no es un diagnóstico. Se muestra solo una vez en esta vista.",
"close": "Ocultar"
},
"ai": {
diff --git a/messages/fr.json b/messages/fr.json
index b6e907177..6501d839a 100644
--- a/messages/fr.json
+++ b/messages/fr.json
@@ -8257,7 +8257,6 @@
"showText": "Afficher le texte extrait",
"summaryTitle": "Résumé",
"textTitle": "Texte extrait",
- "notSaved": "Non enregistré · pas un diagnostic. Affiché une seule fois pour cette vue.",
"close": "Masquer"
},
"ai": {
diff --git a/messages/it.json b/messages/it.json
index b3e75070d..821af3453 100644
--- a/messages/it.json
+++ b/messages/it.json
@@ -8257,7 +8257,6 @@
"showText": "Mostra testo estratto",
"summaryTitle": "Riepilogo",
"textTitle": "Testo estratto",
- "notSaved": "Non salvato · non è una diagnosi. Mostrato una sola volta per questa vista.",
"close": "Nascondi"
},
"ai": {
diff --git a/messages/pl.json b/messages/pl.json
index d08590f4e..ebdeee94a 100644
--- a/messages/pl.json
+++ b/messages/pl.json
@@ -8257,7 +8257,6 @@
"showText": "Pokaż wyodrębniony tekst",
"summaryTitle": "Podsumowanie",
"textTitle": "Wyodrębniony tekst",
- "notSaved": "Nie zapisano · to nie diagnoza. Wyświetlane tylko raz w tym widoku.",
"close": "Ukryj"
},
"ai": {
diff --git a/next.config.ts b/next.config.ts
index fc2280969..9613aa34b 100644
--- a/next.config.ts
+++ b/next.config.ts
@@ -233,6 +233,18 @@ const nextConfig: NextConfig = {
"pg-boss",
"@prisma/adapter-pg",
"pg",
+ // Document PDF handling MUST run the real, un-bundled modules. When
+ // Turbopack bundles pdfjs-dist into the server chunks, its runtime
+ // `require('@napi-rs/canvas')` and its NodeCanvasFactory render path break
+ // in the standalone image (a scanned PDF fails to rasterize with a bare
+ // `Error`, so the read falls back to "PDF scanning needs a Claude vision
+ // provider"), while the identical un-bundled module rasterizes the same
+ // document fine. Externalise both so the server loads the real files from
+ // node_modules; the Dockerfile hoists them to the top level so the runtime
+ // `import()` resolves in the standalone tree, and outputFileTracingIncludes
+ // still ships pdfjs's wasm decoders.
+ "pdfjs-dist",
+ "@napi-rs/canvas",
],
// v1.4.25 Fix-G — `src/lib/ai/prompts/safety-contracts.ts` reads its
// sibling YAML files at runtime via `__dirname + readFileSync`. The
diff --git a/package.json b/package.json
index 6baebe067..6db93a609 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "healthlog",
- "version": "1.28.6",
+ "version": "1.28.7",
"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 48d81e877..3b91811a7 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.6";
+ /* @sw-version-fallback */ "v1.28.7";
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/*`
diff --git a/src/components/documents/__tests__/document-ai-panels.test.tsx b/src/components/documents/__tests__/document-ai-panels.test.tsx
index 3ee93d715..6e6874adf 100644
--- a/src/components/documents/__tests__/document-ai-panels.test.tsx
+++ b/src/components/documents/__tests__/document-ai-panels.test.tsx
@@ -126,11 +126,9 @@ describe("
- {t("documents.summary.notSaved")} -
); } diff --git a/src/lib/documents/rasterize-pdf.ts b/src/lib/documents/rasterize-pdf.ts index b880441c9..b1ae43765 100644 --- a/src/lib/documents/rasterize-pdf.ts +++ b/src/lib/documents/rasterize-pdf.ts @@ -176,7 +176,13 @@ export async function rasterizePdf(buffer: Buffer): Promise