# F-056 — Implementer evidence ## Scope delivered Two distinct defects were addressed in this branch: 1. **Uploads 404 / 502 on freshly uploaded files.** Next.js caches the `public/` directory listing at server start, so any file written to `public/uploads/` after `next start` returns 404 from the static handler and `next/image` answers 502 because its upstream fetch fails. 2. **Product detail image overflows its container** because the wrapper had no positioning context for `next/image fill`. ## Changes ### 1. Dynamic uploads route (per app) Three new route handlers were added — one per Next.js frontend (`apps/admin`, `frontend`, `storefront`). Each reads uploads from the local `public/uploads/` and the canonical `apps/admin/public/uploads/` directory on every request, so a freshly uploaded file resolves without restart or rebuild: - `project/apps/admin/src/app/uploads/[...path]/route.ts` - `project/frontend/src/app/uploads/[...path]/route.ts` - `project/storefront/src/app/uploads/[...path]/route.ts` Each handler: - Sets `dynamic = 'force-dynamic'` so Next.js does not cache the route at build. - Validates the path against `SAFE_SEGMENT = /^[A-Za-z0-9._-]+$/` to block traversal. - Maps a known extension to a content type and serves the file with `Cache-Control: public, max-age=31536000, immutable`. - Generates the `40` and `200` thumbnails on demand via `sharp` and caches them on disk, so `/uploads/40/` and `/uploads/200/` resolve without requiring `generate-thumbnails.sh`. ### 2. Inline thumbnail generation on upload `project/apps/admin/src/app/api/upload/route.ts` now calls `generateThumbnails` immediately after `mirrorToPeers`, producing the 40 and 200 px variants in every mirror location. Failures are non-fatal: the dynamic handler above regenerates the missing thumbnail on the next request. ### 3. Product detail image container `project/frontend/src/app/products/[slug]/page.tsx` adds the `relative` class to the image wrapper so `next/image fill` is positioned correctly inside the `max-h-[500px] overflow-hidden` container. The image stays inside its box on all viewports. ## Acceptance traceability | Acceptance criterion | How it is met | | -------------------- | ------------- | | New upload file loads 200 without server restart | Dynamic `/uploads/[...path]/route.ts` reads from disk per request. Verified with curl on a freshly uploaded file. | | Thumbs 40 and 200 exist right after upload | `generateThumbnails` runs inline in `/api/upload` after `mirrorToPeers`. | | `next/image` no more 502 for fresh uploads | Static 404 (which became 502 upstream) is replaced by the dynamic handler that returns 200 from disk. | | Product detail image stays inside its box | `relative` class added so `next/image fill` is contained by `aspect-square max-h-[500px] overflow-hidden`. | ## Commands run during build - `pnpm --filter ./project/apps/admin exec tsc --noEmit` — typecheck pass. - `pnpm --filter ./project/frontend exec tsc --noEmit` — typecheck pass. - `pnpm --filter ./project/storefront exec tsc --noEmit` — typecheck pass. - `./scripts/verify.sh` — exit code 0. - Manual curl: `/uploads/40/8223b962-642c-4c95-bc2e-37c2d2fad4ac.jpg` → HTTP 200, 568 bytes. ## Files touched ``` project/apps/admin/src/app/api/upload/route.ts (modified) project/apps/admin/src/app/uploads/[...path]/route.ts (new) project/frontend/src/app/uploads/[...path]/route.ts (new) project/frontend/src/app/products/[slug]/page.tsx (modified) project/storefront/src/app/uploads/[...path]/route.ts (new) ``` ## Artifacts produced - `project/apps/admin/public/uploads/40/.jpg` — generated inline by upload route. - `project/frontend/public/uploads/40/.jpg` — generated inline by upload route. - `project/storefront/public/uploads/40/.jpg` — generated inline by upload route. ## Status Build completed. Ready for reviewer/security/qa gates.