diff --git a/next.config.ts b/next.config.ts
index 262feb75..032188aa 100644
--- a/next.config.ts
+++ b/next.config.ts
@@ -52,9 +52,15 @@ const nextConfig: NextConfig = {
// Prefer AVIF (~20-30% smaller than WebP), falling back to WebP, for any
// next/image output.
formats: ["image/avif", "image/webp"],
- // Permit optimizing Supabase Storage signed URLs (private document/image
- // previews) through next/image. Scoped to this app's exact production and
- // (when configured) staging project hostnames, not the wildcard *.supabase.co.
+ // Private signed document/image previews opt out of the optimizer at the
+ // component level (`SignedImage` sets `unoptimized`). Do not rely on
+ // `minimumCacheTTL` as an expiry cap for bearer URLs: it is a lower bound,
+ // and stale-while-revalidate can keep serving private bytes past the
+ // signed-URL lifetime without re-entering the authenticated signed-URL route.
+ // Permit optimizing other Supabase Storage URLs through next/image when a
+ // caller intentionally uses the optimizer. Scoped to this app's exact
+ // production and (when configured) staging project hostnames, not the
+ // wildcard *.supabase.co.
remotePatterns: (() => {
const allowedHostnames = [expectedSupabaseProject.ref + ".supabase.co"];
const stagingRef = process.env.SUPABASE_STAGING_PROJECT_REF?.trim();
diff --git a/src/components/clinical-dashboard/signed-image.tsx b/src/components/clinical-dashboard/signed-image.tsx
index 0463f480..66e691a2 100644
--- a/src/components/clinical-dashboard/signed-image.tsx
+++ b/src/components/clinical-dashboard/signed-image.tsx
@@ -1,7 +1,6 @@
"use client";
-/* eslint-disable @next/next/no-img-element */
-
+import Image from "next/image";
import { memo, useEffect, useRef, useState } from "react";
import { CircleAlert, Loader2, Maximize2 } from "lucide-react";
@@ -129,15 +128,20 @@ export const SignedImage = memo(function SignedImage({
)}
>
{url ? (
-
setLoaded(true)}
onError={handleImageError}
className={cn(
- "absolute inset-0 h-full w-full rounded-lg object-contain transition-opacity duration-300 motion-reduce:transition-none",
+ "rounded-lg object-contain transition-opacity duration-300 motion-reduce:transition-none",
loaded ? "opacity-100" : "opacity-0",
)}
/>
diff --git a/tests/sheet.dom.test.tsx b/tests/sheet.dom.test.tsx
index 74e48065..d3812541 100644
--- a/tests/sheet.dom.test.tsx
+++ b/tests/sheet.dom.test.tsx
@@ -15,7 +15,11 @@ beforeEach(() => {
});
afterEach(() => {
- document.body.style.overflow = "";
+ // Guard teardown: unhandled rAF/setTimeout from Sheet focus scheduling can
+ // fire after Vitest tears down the jsdom environment under coverage workers.
+ if (typeof document !== "undefined" && document.body) {
+ document.body.style.overflow = "";
+ }
});
function Stacked({
diff --git a/tests/signed-image.dom.test.tsx b/tests/signed-image.dom.test.tsx
index c882abd0..77291283 100644
--- a/tests/signed-image.dom.test.tsx
+++ b/tests/signed-image.dom.test.tsx
@@ -52,7 +52,11 @@ describe("SignedImage failure/retry (jsdom)", () => {
await user.click(retry);
const img = await screen.findByRole("img", { name: "Airway diagram" });
- expect(img).toHaveAttribute("src", "/demo/airway.png");
+ // Private previews use unoptimized next/image, so src stays the direct URL
+ // (jsdom may absolutize it) and must not be rewritten through `/_next/image`.
+ const src = img.getAttribute("src") ?? "";
+ expect(src.endsWith("/demo/airway.png")).toBe(true);
+ expect(src).not.toContain("/_next/image");
expect(screen.queryByText("Image preview failed.")).not.toBeInTheDocument();
expect(fetchMock).toHaveBeenCalledTimes(2);
});
diff --git a/tests/signed-image.test.ts b/tests/signed-image.test.ts
index 449282d4..707a262a 100644
--- a/tests/signed-image.test.ts
+++ b/tests/signed-image.test.ts
@@ -47,7 +47,10 @@ describe("SignedImage", () => {
const markup = renderToStaticMarkup(createElement(SignedImage, { endpoint: ENDPOINT, alt: "Airway diagram" }));
expect(markup).toContain("![]()