# 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 -
- -
+
+
+ +
+
``` `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 `Proteina Guisante Ecologica` inside `
`. | | `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) ```