diff --git a/src/app/api/documents/inbound/[id]/original/__tests__/route.test.ts b/src/app/api/documents/inbound/[id]/original/__tests__/route.test.ts index 9ab2d097c..c759ae581 100644 --- a/src/app/api/documents/inbound/[id]/original/__tests__/route.test.ts +++ b/src/app/api/documents/inbound/[id]/original/__tests__/route.test.ts @@ -211,24 +211,21 @@ describe("serving-posture header matrix (Class A inline vs Class B attachment)", ["image/png", "scan.png"], ["image/webp", "scan.webp"], ["image/gif", "anim.gif"], - ])( - "Class A %s → inline, true type, nosniff", - async (mime, name) => { - seed(mime, name); - const res = await callGet(makeReq("doc-m"), ctx("doc-m")); - expect(res.status).toBe(200); - expect(res.headers.get("Content-Type")).toBe(mime); - expect(res.headers.get("Content-Disposition")).toContain("inline"); - expect(res.headers.get("X-Content-Type-Options")).toBe("nosniff"); - // The response CSP is owned by the proxy carve-out (`default-src - // 'none'; frame-ancestors 'self'`, deliberately no sandbox — - // Chromium force-downloads sandboxed PDFs). A route-level value - // would be overwritten there, so the route must not set one; see - // src/__tests__/proxy-document-serve-framing.test.ts for the pin. - expect(res.headers.get("Content-Security-Policy")).toBeNull(); - expect(res.headers.get("Cache-Control")).toBe("private, no-store"); - }, - ); + ])("Class A %s → inline, true type, nosniff", async (mime, name) => { + seed(mime, name); + const res = await callGet(makeReq("doc-m"), ctx("doc-m")); + expect(res.status).toBe(200); + expect(res.headers.get("Content-Type")).toBe(mime); + expect(res.headers.get("Content-Disposition")).toContain("inline"); + expect(res.headers.get("X-Content-Type-Options")).toBe("nosniff"); + // The response CSP is owned by the proxy carve-out (`default-src + // 'none'; frame-ancestors 'self'`, deliberately no sandbox — + // Chromium force-downloads sandboxed PDFs). A route-level value + // would be overwritten there, so the route must not set one; see + // src/__tests__/proxy-document-serve-framing.test.ts for the pin. + expect(res.headers.get("Content-Security-Policy")).toBeNull(); + expect(res.headers.get("Cache-Control")).toBe("private, no-store"); + }); it.each([ [ diff --git a/src/components/documents/documents-view.tsx b/src/components/documents/documents-view.tsx index 6a34e00ca..2075025d7 100644 --- a/src/components/documents/documents-view.tsx +++ b/src/components/documents/documents-view.tsx @@ -464,15 +464,19 @@ export function DocumentsView() { // `?doc=` deep link (used by the illness page's document rows): open // the detail sheet once per value; closing the sheet strips the param so - // back/forward and refresh behave. The id is shape-checked before it is - // interpolated into an API path. + // back/forward and refresh behave. Render-phase derived-state adjustment + // (the file's established pattern), consumed only once auth has resolved + // so a deep link never burns before the module flag is known. The id is + // shape-checked before it is interpolated into an API path. const docParam = searchParams.get("doc"); const [consumedDocParam, setConsumedDocParam] = useState(null); - useEffect(() => { - if (!moduleEnabled || !docParam || docParam === consumedDocParam) return; + if (!authLoading && moduleEnabled && docParam !== consumedDocParam) { setConsumedDocParam(docParam); - if (/^[a-zA-Z0-9_-]{1,40}$/.test(docParam)) openDetail(docParam); - }, [docParam, consumedDocParam, moduleEnabled, openDetail]); + if (docParam !== null && /^[a-zA-Z0-9_-]{1,40}$/.test(docParam)) { + setDetailId(docParam); + setDetailOpen(true); + } + } const handleDetailOpenChange = useCallback( (open: boolean) => {