feat(F-066): completed feature

This commit is contained in:
chattie
2026-08-19 17:22:39 +02:00
parent e60e22ca30
commit 9f1858f6d7
10 changed files with 256 additions and 30 deletions

View File

@@ -0,0 +1,122 @@
# F-066 — Implementer evidence
## Scope delivered
Three separate defects surfaced together; all are addressed in this
ticket.
1. **Image invisible on `/products/[slug]`.** The container in
`frontend/src/app/products/[slug]/page.tsx` was `relative w-full
max-h-[500px]` with a `next/image` `fill` child. `next/image` with
`fill` requires a non-zero-height parent; `max-h` is only a ceiling,
so the parent collapsed to zero and the absolutely positioned image
filled zero pixels. F-061 had removed the `aspect-[5/7]` constraint
that used to give the box a real height (it caused a different
centering problem). This fix restores the `aspect-[5/7]` inside a
flex centering wrapper, so the box has a height **and** is centred
horizontally inside its grid cell.
2. **`PATCH /api/products/:id` returned 500 when the payload included
`attributes`.** The `attributes` column is JSONB, but
`PgProductRepository.update` was binding the JS array directly via
`pg-node`. `pg-node` serialises JS arrays as PG array literals
(`{bio,keto}`); the JSONB parser rejects that with
`invalid input syntax for type json`. The fix `JSON.stringify`s the
array and casts the parameter as `::jsonb`.
3. **`serializeProduct` omitted `channels`, `featured`, and
`attributes`.** The admin editor saved these values successfully but
the next GET round-tripped an object without them, so the UI never
reflected the change. Added the three fields to `serializeProduct`.
## Changes
`project/frontend/src/app/products/[slug]/page.tsx`
```diff
- <div className="relative w-full max-h-[500px] bg-gray-50 rounded-2xl border border-gray-100 overflow-hidden">
- <Image fill className="object-contain" ... />
- </div>
+ <div className="flex justify-center">
+ <div className="relative w-full max-w-md aspect-[5/7] max-h-[500px] bg-gray-50 rounded-2xl border border-gray-100 overflow-hidden">
+ <Image fill className="object-contain" ... />
+ </div>
+ </div>
```
`project/src/modules/catalog/infrastructure/pg-product-repository.ts`
```diff
for (const [key, column] of UPDATABLE) {
if (key in patch) {
+ if (key === 'attributes' && Array.isArray(patch[key])) {
+ values.push(JSON.stringify(patch[key]));
+ setClauses.push(`${column} = $${values.length}::jsonb`);
+ } else {
values.push(patch[key]);
+ setClauses.push(`${column} = $${values.length}`);
+ }
}
}
```
`project/src/modules/catalog/api/catalog.routes.ts`
```diff
function serializeProduct(product, images = []) {
return {
…,
state: product.state,
+ channels: product.channels,
+ featured: product.featured,
+ attributes: product.attributes,
seoTitle: product.seoTitle,
};
}
```
## Acceptance traceability
| Acceptance criterion | How it is met |
| -------------------- | ------------- |
| `GET /products/[slug]` shows the product image (no zero-height container) | `aspect-[5/7] max-h-[500px]` gives the wrapper a real height; curl on the rendered page shows `<img alt="Proteina Guisante Ecologica" … style="position:absolute;height:100%;width:100%;…">` inside `<div class="relative w-full max-w-md aspect-[5/7] max-h-[500px] …">`. |
| `PATCH /api/products/:id` with `attributes` succeeds and round-trips | `JSON.stringify` + `::jsonb` cast; verified with `PATCH {"attributes":["bio","keto"],"featured":true,"channels":"online"}` → 200 + the same shape echoed back. |
| `GET /api/products/:id` returns `channels`, `featured`, `attributes` | `serializeProduct` now includes them; verified the field is present in the JSON. |
| `verify.sh` is green | Exit 0. |
## Manual verification
```
$ curl -X PATCH http://192.168.18.93:3004/api/products/13a65dc0-… \
-H 'Content-Type: application/json' -b /tmp/admin_cookies.txt \
-d '{"attributes":["bio","keto"],"featured":true,"channels":"online"}'
{ … "channels":"online", "featured":true, "attributes":["bio","keto"], … }
HTTP 200
$ curl http://192.168.18.93:3004/api/products/13a65dc0-… -b /tmp/admin_cookies.txt
{ … "channels":"online", "featured":true, "attributes":["bio","keto"], … }
$ curl http://192.168.18.93:3003/products/proteina-guisante-ecologica | grep -oE 'aspect-\[5/7\][^"]*'
aspect-[5/7] max-h-[500px] …
```
The 500 line is gone from `project/.runtime/prod/backend.log`; the
next PATCH with `attributes` is 200.
## Build verification
- `npm run typecheck` (backend) — exit 0
- `npm test` (backend) — 124 passed, 56 skipped
- `npm run build` (backend) — exit 0
- `npx tsc --noEmit` (frontend) — exit 0
- `monolith.sh prod restart backend frontend` → backend / frontend 200
- `./scripts/verify.sh` — exit 0
## Files touched
```
project/frontend/src/app/products/[slug]/page.tsx (image container height)
project/src/modules/catalog/infrastructure/pg-product-repository.ts (JSONB cast for attributes)
project/src/modules/catalog/api/catalog.routes.ts (serializeProduct fields)
```

View File

@@ -0,0 +1,13 @@
{
"feature_id": "F-066",
"agent": "leader",
"verdict": "APPROVED",
"summary": "All gates approved. Closing F-066.",
"evidence": [
"work/artifacts/F-066/reviewer.json verdict=APPROVED",
"work/artifacts/F-066/security.json verdict=APPROVED",
"work/artifacts/F-066/qa.json verdict=APPROVED",
"./scripts/verify.sh exit 0"
],
"timestamp": "2026-08-19T15:25:00Z"
}

View File

@@ -0,0 +1,15 @@
{
"feature_id": "F-066",
"agent": "qa",
"verdict": "APPROVED",
"summary": "End-to-end trace. The image now renders on /products/[slug]. PATCH with attributes no longer 500s and round-trips. GET returns channels, featured, attributes. Backend tests and verify.sh pass.",
"evidence": [
"AC1 'GET /products/[slug] shows the product image (no zero-height container)' — rendered HTML has aspect-[5/7] max-h-[500px] container with the image inside; the next/image srcSet returns 200",
"AC2 'PATCH /api/products/:id with attributes succeeds and round-trips' — curl verified",
"AC3 'GET /api/products/:id returns channels featured and attributes' — curl verified; previously missing",
"AC4 'verify.sh is green' — exit 0",
"Regression: backend tests 124/180 green; storefront still uses plain <img> tag with object-contain and 200 response",
"Regression: typecheck + build green across backend, frontend, admin"
],
"timestamp": "2026-08-19T15:25:00Z"
}

View File

@@ -0,0 +1,18 @@
{
"feature_id": "F-066",
"agent": "reviewer",
"verdict": "APPROVED",
"summary": "Three surgical fixes. The product image container now has aspect-[5/7] inside a flex centering wrapper so it both has a real height and is centred horizontally. The PATCH 500 is gone (JSON.stringify + ::jsonb cast). serializeProduct includes channels, featured, attributes so admin edits round-trip through the API.",
"evidence": [
"git diff project/frontend/src/app/products/[slug]/page.tsx — restored aspect-[5/7] max-h-[500px] inside a flex justify-center wrapper",
"git diff project/src/modules/catalog/infrastructure/pg-product-repository.ts — JSON.stringify(patch[key]) + ::jsonb cast for attributes",
"git diff project/src/modules/catalog/api/catalog.routes.ts — channels, featured, attributes added to serializeProduct",
"curl PATCH /api/products/13a65dc0-… with attributes → 200 with attributes, featured, channels in the response body",
"curl GET /api/products/13a65dc0-… → same fields present (round-trip)",
"curl /products/proteina-guisante-ecologica → image element rendered inside aspect-[5/7] max-h-[500px] container",
"backend log no longer shows 'invalid input syntax for type json' for PATCH",
"npm test (backend) — 124 passed, 56 skipped",
"./scripts/verify.sh — exit 0"
],
"timestamp": "2026-08-19T15:25:00Z"
}

View File

@@ -0,0 +1,13 @@
{
"feature_id": "F-066",
"agent": "security",
"verdict": "APPROVED",
"summary": "No security boundary change. The PATCH endpoint was already gated by admin role; we just stopped it from 500-ing. The image container change is a CSS-only fix. Adding fields to serializeProduct expands the response shape but does not introduce a new write surface.",
"evidence": [
"PATCH /api/products/:id still requires admin role (unchanged)",
"JSON.stringify on attributes is the same encoding the JSONB parser expected; no new code path",
"serializeProduct expansion: read-only — the write path is unchanged",
"No new env vars, no new dependencies"
],
"timestamp": "2026-08-19T15:25:00Z"
}