# 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/<file>` and `/uploads/200/<file>` 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/<file>.jpg` — generated inline by upload route.
- `project/frontend/public/uploads/40/<file>.jpg` — generated inline by upload route.
- `project/storefront/public/uploads/40/<file>.jpg` — generated inline by upload route.

## Status

Build completed. Ready for reviewer/security/qa gates.