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)¶
/api/receipts/:id/image?variant=β¦is failing with 401/404/500 β the AntD<Image>only shows the fallback icon when itssrcdoesn't resolve to image bytes. Confirmed structure: the drawer fetches atcomponents/records/ReceiptDetailDrawer.tsx:914-915and that endpoint (pages/api/receipts/[id]/image.ts) streams from Drive viastreamReceiptFile(fileId)usinggetDriveClient('receipts').- Drive SA env vars dropped in the new container. The receipts Drive client reads
DRIVE_RECEIPTS_CLIENT_EMAIL/DRIVE_RECEIPTS_PRIVATE_KEY(withGOOGLE_*fallback) perlib/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. 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.- 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
getServerSessionserver-side (less likely, since the drawer fetch is on the same session that loaded the page). - 404 β receipt id mismatch; the unified feed's
recordIdis 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-NNNtask 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|originalANDβ¦/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 theDSMURL string as the runbook implies; runbook should be updated). Findings, all proven, not inferred: - Receipt record is healthy β
tebs-erl β¦ /receipts/entries/d01e1d96-β¦hasmetadata.drive.croppedFileId+originalFileIdpopulated. (Rules out "missing fileId".) - The NAS's own Drive creds work. Read
/docker/eop/.env.productionvia FileStation:DRIVE_RECEIPTS_*+DRIVE_ACCOUNTING_*are absent, so the receipts client falls back toGOOGLE_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 opsstreamReceiptFiledoes βfiles.getmetadata +alt=mediadownload β 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 inlib/drive/client.tslanded2026-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=trueto.env.production(diff vs theβ¦backup-pre-T084-20260622023953copy) β 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
:mainimage / 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) returnssuccess:truebut the dispatcher never executes (_cmdis never consumed,_cmd.outnever 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.listshows only a stale/leftovereop-app(image:main, Exited (1) at2026-06-22T07:45:07Z, started06-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 logsare 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
:mainimage and the current/docker/eop/.env.productionβ should fix this (proven: those creds work;:maincode 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 aT-NNN. - Housekeeping: clean up the stale exited
eop-appcontainer; restore root-exec (task id=4); fix the runbook'sDSMcred-format note. - Status β blocked on a clean container recreate + (if still failing)
docker logs.