fix(documents): consume the ?doc= deep link during render, not in an effect

The one-shot deep-link consumption moves to the render-phase derived-
state pattern the view already uses for its search draft (the effect
variant trips the set-state-in-effect rule), gated on resolved auth so
a deep link never burns before the module flag is known.

Refs: document-vault plan W3-T4
This commit is contained in:
Marc-André Bombeck committed 2026-07-07 13:46:02 +02:00
1 parent 8bff4d0c86
commit 23e371965b
2 files changed
+25 -24

No files matched your search

@@ -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([
[
+10 -6
View File
@@ -464,15 +464,19 @@ export function DocumentsView() {
// `?doc=<id>` 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<string | null>(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) => {