Files
Marc-André Bombeck ee76672798 feat(documents): preview thumbnails in the vault overview
Render a small JPEG preview for each stored document and show it at the
card's leading edge, falling back to the kind icon when none exists.

- Generate previews in the background on upload (new document-thumbnail
  pg-boss queue) plus a boot-time corpus backfill for existing documents;
  upload never blocks on or fails because of it. Images are downscaled via
  @napi-rs/canvas, a PDF's first page is rasterised then downscaled, every
  other type is skipped. Generation never throws.
- Store the preview encrypted at rest in a 1:1 DocumentThumbnail side table
  (migration 0236) so the list/detail queries that omit the original blob
  never drag the preview either. Same AES-256-GCM string codec + fail-closed
  decrypt as the content index; wired into the key-rotation walk.
- Serve it through an owner-scoped GET /api/documents/inbound/[id]/thumbnail
  (auth + module gate + userId-scoped query + fail-closed decrypt), always
  image/jpeg + nosniff + private, no-store; 404 on miss. The card renders an
  authed lazy <img>, onError falls back to the icon.
- Add hasThumbnail to the document DTO so the card only fetches a preview
  that exists.

Also drop the "search looks inside your documents" caption from the overview
(the index-all prompt stays) and the per-card searchable marker; both were
noise. Remove the now-unused i18n keys across all six locales.
2026-07-10 01:15:32 +02:00

51 lines
2.0 KiB
SQL

-- Document vault: encrypted preview thumbnails.
--
-- `document_thumbnails` — a 1:1 sibling of `inbound_documents` holding a small
-- JPEG preview (~320px long edge, q70) rendered in the background from the
-- original: images are decoded + downscaled, a PDF's first page is rasterised.
-- The blob rides `thumbnail_encrypted` in the `encrypt()`-string-as-UTF-8
-- BYTEA shape (base64 of the JPEG) `document_content_index.text_encrypted`
-- uses — a scanned medical preview is PHI, never stored in the clear or
-- logged. The canvas re-encode strips EXIF/GPS from the source, so the
-- preview is metadata-free by construction.
--
-- A SIDE TABLE, not a column on `inbound_documents`, so the list/detail
-- queries that `omit` the original blob never drag this blob into the SELECT.
--
-- Both foreign keys cascade: a deleted document (or user) takes its thumbnail
-- with it. Additive; applies clean on a 0234 database and on a fresh database.
-- CreateTable
CREATE TABLE "document_thumbnails" (
"id" TEXT NOT NULL,
"document_id" TEXT NOT NULL,
"user_id" TEXT NOT NULL,
"thumbnail_encrypted" BYTEA NOT NULL,
"width" INTEGER NOT NULL,
"height" INTEGER NOT NULL,
"byte_size" INTEGER NOT NULL,
"source_mime" TEXT NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "document_thumbnails_pkey" PRIMARY KEY ("id")
);
-- 1:1 with the document.
CREATE UNIQUE INDEX "document_thumbnails_document_id_key"
ON "document_thumbnails" ("document_id");
-- Owner-scoped queries (the backfill discovery + list-page existence probe).
CREATE INDEX "document_thumbnails_user_id_idx"
ON "document_thumbnails" ("user_id");
ALTER TABLE "document_thumbnails"
ADD CONSTRAINT "document_thumbnails_document_id_fkey"
FOREIGN KEY ("document_id") REFERENCES "inbound_documents" ("id")
ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "document_thumbnails"
ADD CONSTRAINT "document_thumbnails_user_id_fkey"
FOREIGN KEY ("user_id") REFERENCES "users" ("id")
ON DELETE CASCADE ON UPDATE CASCADE;