mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 04:50:59 +00:00
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:
17 files changed
+48
-30
No files matched your search
@@ -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
@@ -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,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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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
@@ -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
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user