Merge pull request #451 from MBombeck/release/v1.28.7

release/v1.28.7 — scanned-PDF reading works (un-bundled pdfjs)
This commit is contained in:
Marc-André Bombeck authored and GitHub committed 2026-07-09 16:55:41 +02:00
commit b8d0f140fd
17 files changed
+48 -30

No files matched your search

+10
View File
@@ -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
+12
View File
@@ -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
+1 -1
View File
@@ -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.
+1 -8
View File
@@ -196,9 +196,7 @@ test.describe("document vault — AI assist + content search", () => {
// ── (b) Session-only summary: transient, note present, closing discards ──
test("summary panel is transient and labelled not-saved / not-a-diagnosis", async ({
page,
}) => {
test("summary panel is transient and discards on close", async ({ page }) => {
await mockAiEnabled(page, { mode: "vision" });
const summaryText = "A chest imaging report issued by a radiology clinic.";
let summaryCalls = 0;
@@ -221,11 +219,6 @@ test.describe("document vault — AI assist + content search", () => {
const panel = sheet.locator('[data-slot="document-summary-panel"]');
await expect(panel).toBeVisible();
await expect(panel.getByText(summaryText)).toBeVisible();
await expect(
panel.getByText(
"Not saved · not a diagnosis. Shown once for this view only.",
),
).toBeVisible();
expect(summaryCalls).toBe(1);
// Closing discards — the panel is gone and nothing lingers.
-1
View File
@@ -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": {
-1
View File
@@ -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": {
-1
View File
@@ -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": {
-1
View File
@@ -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": {
-1
View File
@@ -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": {
-1
View File
@@ -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": {
+12
View File
@@ -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
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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/*`
@@ -126,11 +126,9 @@ describe("<DocumentSummaryPanel>", () => {
);
expect(html).toContain('data-slot="document-summary-panel"');
expect(html).toContain('data-slot="document-summary-loading"');
expect(html).toContain("Not saved");
expect(html).toContain("not a diagnosis");
});
it("renders the summary body and the persistent note", () => {
it("renders the summary body", () => {
const html = render(
<DocumentSummaryPanel
output="summary"
@@ -141,7 +139,6 @@ describe("<DocumentSummaryPanel>", () => {
/>,
);
expect(html).toContain("A lipid panel dated 1 March 2026.");
expect(html).toContain("Not saved");
});
it("renders extracted text in a monospace block", () => {
@@ -159,7 +156,7 @@ describe("<DocumentSummaryPanel>", () => {
expect(html).toContain("font-mono");
});
it("surfaces an error via role=alert but keeps the note", () => {
it("surfaces an error via role=alert", () => {
const html = render(
<DocumentSummaryPanel
output="summary"
@@ -171,7 +168,6 @@ describe("<DocumentSummaryPanel>", () => {
);
expect(html).toContain('role="alert"');
expect(html).toContain("read the document");
expect(html).toContain("Not saved");
});
});
@@ -130,7 +130,7 @@ describe("<DocumentAiSection>", () => {
expect(html).toContain("review before saving");
});
it("renders the transient summary panel with the session-only note", () => {
it("renders the transient summary panel", () => {
const html = render(
base({
summaryOutput: "summary",
@@ -139,7 +139,6 @@ describe("<DocumentAiSection>", () => {
);
expect(html).toContain('data-slot="document-summary-panel"');
expect(html).toContain("An MRI report of the left knee.");
expect(html).toContain("Not saved");
});
it("renders no per-document egress notice — the settings confirm covers it", () => {
@@ -281,10 +281,6 @@ export function DocumentSummaryPanel({
{body}
</p>
) : null}
<p className="text-muted-foreground border-border/60 border-t pt-2 text-xs">
{t("documents.summary.notSaved")}
</p>
</div>
);
}
+7 -1
View File
@@ -176,7 +176,13 @@ export async function rasterizePdf(buffer: Buffer): Promise<RasterResult> {
} catch (err) {
annotate({
action: { name: "documents.rasterize.failed" },
meta: { reason: err instanceof Error ? err.name : "unknown" },
meta: {
reason: err instanceof Error ? err.name : "unknown",
// The message pins WHY a render failed (bundled-module mismatch, an
// encrypted/odd PDF, a font issue) — the name alone is opaque. Bounded
// so a pathological message can't bloat the wide event.
message: err instanceof Error ? err.message.slice(0, 300) : String(err),
},
});
return { ok: false };
} finally {