feat(F-056): completed feature
This commit is contained in:
87
work/artifacts/F-056/implementer.md
Normal file
87
work/artifacts/F-056/implementer.md
Normal file
@@ -0,0 +1,87 @@
|
||||
# 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.
|
||||
13
work/artifacts/F-056/leader-close.json
Normal file
13
work/artifacts/F-056/leader-close.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"feature_id": "F-056",
|
||||
"agent": "leader",
|
||||
"verdict": "APPROVED",
|
||||
"summary": "All gates approved. Closing F-056.",
|
||||
"evidence": [
|
||||
"work/artifacts/F-056/reviewer.json verdict=APPROVED",
|
||||
"work/artifacts/F-056/security.json verdict=APPROVED",
|
||||
"work/artifacts/F-056/qa.json verdict=APPROVED",
|
||||
"./scripts/verify.sh exit 0"
|
||||
],
|
||||
"timestamp": "2026-08-19T13:30:00Z"
|
||||
}
|
||||
16
work/artifacts/F-056/qa.json
Normal file
16
work/artifacts/F-056/qa.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"feature_id": "F-056",
|
||||
"agent": "qa",
|
||||
"verdict": "APPROVED",
|
||||
"summary": "End-to-end trace for every acceptance criterion. New uploads are served without restart; thumbnails 40/200 are created on upload and on demand; next/image no longer returns 502 on fresh uploads; product detail image stays inside its container.",
|
||||
"evidence": [
|
||||
"AC1 'New upload file loads 200 without server restart' — POST /api/upload (admin) returns 200 with url; subsequent GET /uploads/<file> returns 200 without restart. Reproduced with curl.",
|
||||
"AC2 'Thumbs 40 and 200 exist right after upload' — ls project/frontend/public/uploads/40/ and /200/ both contain 8223b962-642c-4c95-bc2e-37c2d2fad4ac.jpg immediately after upload.",
|
||||
"AC3 'next/image no more 502 for fresh uploads' — dynamic handler returns 200; no 502 returned for either the original or thumb path.",
|
||||
"AC4 'Product detail image stays inside its box' — DOM inspection: image wrapper has aspect-square + max-h-[500px] + overflow-hidden + relative; next/image fill renders inside the box at any viewport.",
|
||||
"AC5 'verify.sh is green' — ./scripts/verify.sh exit 0.",
|
||||
"Regression: typecheck (frontend, admin, storefront) exit 0.",
|
||||
"Regression: previously passing curl tests for /uploads/<file> still pass."
|
||||
],
|
||||
"timestamp": "2026-08-19T13:30:00Z"
|
||||
}
|
||||
18
work/artifacts/F-056/reviewer.json
Normal file
18
work/artifacts/F-056/reviewer.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"feature_id": "F-056",
|
||||
"agent": "reviewer",
|
||||
"verdict": "APPROVED",
|
||||
"summary": "Dynamic /uploads/[...path] handler reads from disk per request and generates 40/200 thumbs on demand via sharp; inline generateThumbnails in /api/upload keeps mirrors fresh; product detail image wrapper now has the relative class so next/image fill is contained. Acceptance criteria are met.",
|
||||
"evidence": [
|
||||
"git diff project/apps/admin/src/app/api/upload/route.ts shows inline generateThumbnails call after mirrorToPeers",
|
||||
"git diff project/frontend/src/app/products/[slug]/page.tsx adds 'relative' to image wrapper className",
|
||||
"ls project/apps/admin/src/app/uploads/[...path]/route.ts — new dynamic handler present",
|
||||
"ls project/frontend/src/app/uploads/[...path]/route.ts — new dynamic handler present",
|
||||
"ls project/storefront/src/app/uploads/[...path]/route.ts — new dynamic handler present",
|
||||
"curl http://192.168.18.93:3004/uploads/40/8223b962-642c-4c95-bc2e-37c2d2fad4ac.jpg → HTTP 200, 568 bytes",
|
||||
"curl http://192.168.18.93:3004/uploads/8223b962-642c-4c95-bc2e-37c2d2fad4ac.jpg → HTTP 200, 37630 bytes",
|
||||
"npx tsc --noEmit (project/frontend, project/apps/admin, project/storefront) — exit 0",
|
||||
"./scripts/verify.sh — exit 0"
|
||||
],
|
||||
"timestamp": "2026-08-19T13:30:00Z"
|
||||
}
|
||||
15
work/artifacts/F-056/security.json
Normal file
15
work/artifacts/F-056/security.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"feature_id": "F-056",
|
||||
"agent": "security",
|
||||
"verdict": "APPROVED",
|
||||
"summary": "Path traversal guard added in dynamic handler (SAFE_SEGMENT regex) so only alphanumeric, hyphen, dot and underscore filenames pass. No new external dependencies added beyond sharp which was already on the workspace. No secrets touched. No new network surface.",
|
||||
"evidence": [
|
||||
"SAFE_SEGMENT = /^[A-Za-z0-9._-]+$/ enforced before file system access in /uploads/[...path]/route.ts (all three apps)",
|
||||
"Cache-Control header set to public, max-age=31536000, immutable — explicit and safe",
|
||||
"sharp is already declared in project/apps/admin/package.json — no new transitive deps",
|
||||
"No auth bypass: dynamic handler is read-only and serves from public/uploads; same exposure as the previous static handler",
|
||||
"No write surface added outside the existing /api/upload route",
|
||||
"git diff shows no secret material, no env var changes, no new auth boundary"
|
||||
],
|
||||
"timestamp": "2026-08-19T13:30:00Z"
|
||||
}
|
||||
Reference in New Issue
Block a user