Skip to content

Receipt detail drawer opens, but cropped + original images render only as a placeholder picture icon

Symptom

After the NAS rebuild that brought the T-087 fix live (so the I-004 regression is gone β€” the drawer now opens on double-click), the receipt images inside the drawer don't render. Both the cropped and original variants show only AntD's fallback picture icon, never the actual receipt photo.

Owner, 2026-06-22 (verbatim): "I can now open the Receipt Detail modal, but both processed and original receipt images are unable to be shown and it shows only a picture icon"

Likely causes (priority order)

  1. /api/receipts/:id/image?variant=… is failing with 401/404/500 β€” the AntD <Image> only shows the fallback icon when its src doesn't resolve to image bytes. Confirmed structure: the drawer fetches at components/records/ReceiptDetailDrawer.tsx:914-915
    src={`/api/receipts/${receiptId}/image?variant=${variant}&v=${cacheKey}`}
    
    and that endpoint (pages/api/receipts/[id]/image.ts) streams from Drive via streamReceiptFile(fileId) using getDriveClient('receipts').
  2. Drive SA env vars dropped in the new container. The receipts Drive client reads DRIVE_RECEIPTS_CLIENT_EMAIL / DRIVE_RECEIPTS_PRIVATE_KEY (with GOOGLE_* fallback) per lib/drive/client.ts:56-72. If the NAS rebuild started with a different / pruned .env.production, the SA auth would fail closed and the endpoint would return 500.
  3. metadata.drive.{croppedFileId, originalFileId} is missing or null on the receipts. If the field name drifted (or the receipt records are older and only carry *Link, not *Id), streamReceiptFile(undefined) throws. Unlikely if uploads worked recently.
  4. Browser cached the old bundle β€” but the modal-open behaviour confirms the new bundle IS running, so this can be ruled out.

Diagnostic the owner can run in <30s

Open DevTools β†’ Network tab β†’ double-click a receipt β†’ click on the request named /api/receipts/<id>/image?variant=cropped&v=… β†’ look at the Status column.

  • 500 β†’ the proxy is throwing; likely cause #2 (env vars) or #3 (missing fileId).
  • Open the response body β€” it'll have {"error":"Failed to fetch image"} or similar. The container logs will have the real stack.
  • 401 β†’ session not detected by getServerSession server-side (less likely, since the drawer fetch is on the same session that loaded the page).
  • 404 β†’ receipt id mismatch; the unified feed's recordId is wrong for this row.
  • 200 but no image renders β†’ response Content-Type is wrong, OR the stream is empty.

Next

Per the diagnostic outcome: - 500 β†’ I'll need to either reach the NAS container logs (via the Claude↔NAS channel) or get a copy of the response body. Most likely a structural fix β†’ escalates to a T-NNN. - 404 / 401 β†’ app-level bug, ticket-only fix.

Decision log

2026-06-22 β€” opened (investigating)

  • βœ… Attestation (Records (Infrastructure)): read AGENTS.md; scope-scanned tickets + tasks β€” no existing ticket / task covers the post-T-087 image-render symptom (I-004 was double-clickβ†’external window, now resolved; this is a sibling symptom inside the now-opening drawer).
  • Source: Records (Infrastructure) Β· https://claude.ai/code/session_018RDB37kCqfouHdygVXTAtD
  • What: opened this ticket; will escalate to a T-NNN task if the diagnostic points at a structural cause (env-var drop in the rebuilt container, drive-client misconfig, or field-shape drift on receipts).
  • Why: the symptom showed up specifically after the NAS rebuild that landed T-087, so the surface area is narrow β€” most likely a side-effect of the rebuild rather than a pre-existing latent bug. Diagnostic gates the next move.

2026-06-22 β€” investigation: creds/SA/Drive RULED OUT; cause is stale code/env in the live container

  • βœ… Attestation (Records (Infrastructure)): read AGENTS.md; kept this ticket current per the "keep the board current" rule.
  • Source: Records (Infrastructure) Β· https://claude.ai/code/session_018RDB37kCqfouHdygVXTAtD
  • Confirmed status: owner's DevTools showed 500 on both /api/receipts/d01e1d96-…/image-meta?variant=cropped|original AND …/image?….
  • Reached the NAS via the Claude↔NAS Cloudflare-tunnel + DSM Web API (creds were in env as DSM_account / DSM_password, separate vars β€” not embedded in the DSM URL string as the runbook implies; runbook should be updated). Findings, all proven, not inferred:
  • Receipt record is healthy β€” tebs-erl … /receipts/entries/d01e1d96-… has metadata.drive.croppedFileId + originalFileId populated. (Rules out "missing fileId".)
  • The NAS's own Drive creds work. Read /docker/eop/.env.production via FileStation: DRIVE_RECEIPTS_* + DRIVE_ACCOUNTING_* are absent, so the receipts client falls back to GOOGLE_CLIENT_EMAIL=service@aote-pms + GOOGLE_PRIVATE_KEY (len 1732, literal \n). Minted a token from that exact key (via openssl, key never echoed) and performed the exact two ops streamReceiptFile does β€” files.get metadata + alt=media download β€” on both variants: HTTP 200, cropped 175124 B, original 395784 B. So creds are present, valid, active, and the SA has Drive access to the files. β†’ SA allocation / Drive access is NOT the cause. (The drafted enquiry to the Service-Account (Local) agent is premised on a cause that's now disproven β€” do not send it as written.)
  • The GOOGLE_* fallback in lib/drive/client.ts landed 2026-06-21 04:37 (PR #772), before the T-087 fix (2026-06-22 03:02). So any image new enough to have the drawer fix (which the owner confirmed works) is also new enough to have the fallback β†’ at repo HEAD this path works.
  • T-084 only added ENABLE_IN_APP_CRON=true to .env.production (diff vs the …backup-pre-T084-20260622023953 copy) β€” it did not touch Drive creds.
  • The app is UP (live from sandbox: /β†’307, /auth/signinβ†’200, x-powered-by: Next.js). The image endpoint returns 401 for an unauthenticated request β€” so the 500 only happens for an authenticated session, after the auth gate, in the Drive path. (Can't repro 500 without the owner's session.)
  • Therefore: every static + credential check says the path should work, yet authenticated calls 500 β†’ the live serving container is almost certainly running stale code and/or a stale env (a container/image instantiated before the fallback, or one not carrying the current :main image / current .env.production).
  • Why I can't get the stack trace (blockers):
  • Root-exec via DSM Task Scheduler id=4 is DOWN. method=run (v1) returns success:true but the dispatcher never executes (_cmd is never consumed, _cmd.out never written, polled 60s). Task config is intact (Run: …/run-gcp-statements.sh, owner=root, enabled) β€” so it's a DSM execution-layer break, likely T-084 collateral. Needs its own fix; tracked-worthy.
  • The live container isn't introspectable via the read API. SYNO.Docker.Container.list shows only a stale/leftover eop-app (image :main, Exited (1) at 2026-06-22T07:45:07Z, started 06-21 18:08, not OOM) and no compose file exists in /docker/eop. The container actually serving prod is started outside Container Manager's tracking, so its image digest / live env / docker logs are not reachable without root-exec (down) or shell.
  • Next action (needs owner / the deploy-build agent who has shell):
  • The rebuild the owner is already running β€” IF it recreates the serving container from the current :main image and the current /docker/eop/.env.production β€” should fix this (proven: those creds work; :main code has the fallback). Re-test the receipt images after a clean recreate.
  • If it still 500s after a clean recreate, it's a genuine runtime bug β†’ capture: docker ps (find the live app container) β†’ docker logs <app> --since 2h | grep -iE 'image-meta|receipts|drive|Error'. That stack trace escalates this to a T-NNN.
  • Housekeeping: clean up the stale exited eop-app container; restore root-exec (task id=4); fix the runbook's DSM cred-format note.
  • Status β†’ blocked on a clean container recreate + (if still failing) docker logs.