feat(F-131): completed feature

This commit is contained in:
chattie
2026-08-21 17:50:31 +02:00
parent f37333bb56
commit e5126797a6
10 changed files with 229 additions and 15 deletions

View File

@@ -5348,13 +5348,15 @@
"description": "El buscador de /products (admin panel) no filtra los productos al escribir. Revisar el input de búsqueda en la lista, el endpoint backend y los query params enviados. Debe buscar por nombre y slug, idealmente con ILIKE case-insensitive.", "description": "El buscador de /products (admin panel) no filtra los productos al escribir. Revisar el input de búsqueda en la lista, el endpoint backend y los query params enviados. Debe buscar por nombre y slug, idealmente con ILIKE case-insensitive.",
"priority": "high", "priority": "high",
"risk": "low", "risk": "low",
"status": "pending", "status": "done",
"created_at": "2026-08-21", "created_at": "2026-08-21",
"gates": { "gates": {
"reviewer": false, "reviewer": true,
"security": false, "security": true,
"qa": false "qa": true,
} "close": true
},
"completed_at": "2026-08-21T15:50:31Z"
}, },
{ {
"id": "F-132", "id": "F-132",

View File

@@ -12,9 +12,10 @@ const API = process.env.NEXT_PUBLIC_API_URL ?? 'http://127.0.0.1:3000';
*/ */
export async function GET(req: NextRequest) { export async function GET(req: NextRequest) {
const path = req.nextUrl.pathname.replace('/api/', ''); const path = req.nextUrl.pathname.replace('/api/', '');
const search = req.nextUrl.search;
const cookies = req.headers.get('cookie') ?? ''; const cookies = req.headers.get('cookie') ?? '';
try { try {
const backendRes = await fetch(`${API}/${path}`, { const backendRes = await fetch(`${API}/${path}${search}`, {
headers: { Cookie: cookies }, headers: { Cookie: cookies },
}); });
if (path === 'admin/logs/stream') { if (path === 'admin/logs/stream') {
@@ -37,12 +38,13 @@ export async function GET(req: NextRequest) {
export async function POST(req: NextRequest) { export async function POST(req: NextRequest) {
const path = req.nextUrl.pathname.replace('/api/', ''); const path = req.nextUrl.pathname.replace('/api/', '');
const search = req.nextUrl.search;
const cookies = req.headers.get('cookie') ?? ''; const cookies = req.headers.get('cookie') ?? '';
const body = await req.text(); const body = await req.text();
try { try {
const headers: Record<string, string> = { Cookie: cookies }; const headers: Record<string, string> = { Cookie: cookies };
if (body) headers['Content-Type'] = 'application/json'; if (body) headers['Content-Type'] = 'application/json';
const backendRes = await fetch(`${API}/${path}`, { const backendRes = await fetch(`${API}/${path}${search}`, {
method: 'POST', method: 'POST',
headers, headers,
...(body ? { body } : {}), ...(body ? { body } : {}),
@@ -64,10 +66,11 @@ export async function POST(req: NextRequest) {
export async function PATCH(req: NextRequest) { export async function PATCH(req: NextRequest) {
const path = req.nextUrl.pathname.replace('/api/', ''); const path = req.nextUrl.pathname.replace('/api/', '');
const search = req.nextUrl.search;
const cookies = req.headers.get('cookie') ?? ''; const cookies = req.headers.get('cookie') ?? '';
const body = await req.text(); const body = await req.text();
try { try {
const backendRes = await fetch(`${API}/${path}`, { const backendRes = await fetch(`${API}/${path}${search}`, {
method: 'PATCH', method: 'PATCH',
headers: { 'Content-Type': 'application/json', Cookie: cookies }, headers: { 'Content-Type': 'application/json', Cookie: cookies },
body, body,
@@ -81,10 +84,11 @@ export async function PATCH(req: NextRequest) {
export async function PUT(req: NextRequest) { export async function PUT(req: NextRequest) {
const path = req.nextUrl.pathname.replace('/api/', ''); const path = req.nextUrl.pathname.replace('/api/', '');
const search = req.nextUrl.search;
const cookies = req.headers.get('cookie') ?? ''; const cookies = req.headers.get('cookie') ?? '';
const body = await req.text(); const body = await req.text();
try { try {
const backendRes = await fetch(`${API}/${path}`, { const backendRes = await fetch(`${API}/${path}${search}`, {
method: 'PUT', method: 'PUT',
headers: { 'Content-Type': 'application/json', Cookie: cookies }, headers: { 'Content-Type': 'application/json', Cookie: cookies },
body, body,
@@ -98,9 +102,10 @@ export async function PUT(req: NextRequest) {
export async function DELETE(req: NextRequest) { export async function DELETE(req: NextRequest) {
const path = req.nextUrl.pathname.replace('/api/', ''); const path = req.nextUrl.pathname.replace('/api/', '');
const search = req.nextUrl.search;
const cookies = req.headers.get('cookie') ?? ''; const cookies = req.headers.get('cookie') ?? '';
try { try {
const backendRes = await fetch(`${API}/${path}`, { const backendRes = await fetch(`${API}/${path}${search}`, {
method: 'DELETE', method: 'DELETE',
headers: { Cookie: cookies }, headers: { Cookie: cookies },
}); });

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,67 @@
# F-131 — Buscador /products (admin) no filtra
## Diagnóstico
Reproducción:
1. Login en `http://192.168.18.93:3004/`
2. Navegar a `/products`
3. Escribir "almendra" en el buscador
4. → La lista sigue mostrando los 13 productos (no filtra)
Investigación:
```
GET http://192.168.18.93:3000/catalog/products?q=almendra&limit=5
→ backend responde: total=1, items=1 ("Almendras Crudas Ecologicas") ✅ correcto
GET http://192.168.18.93:3004/api/catalog/products?q=almendra&limit=5
→ backend responde: total=13, items=13 (todos) ❌ filtro perdido
```
El **frontend y el backend funcionan bien aisladamente**. El problema está en el proxy del admin.
## Causa raíz
`apps/admin/src/app/api/[...path]/route.ts` (catch-all proxy que reenvía al backend) usa:
```ts
const path = req.nextUrl.pathname.replace('/api/', '');
const backendRes = await fetch(`${API}/${path}`, { ... });
```
**Nunca añade `req.nextUrl.search`**, por lo que cualquier query string (`?q=…`, `?limit=…`, `?offset=…`, etc.) se descarta antes de llegar al backend.
Esto afecta a **TODOS los métodos** (GET, POST, PATCH, PUT, DELETE) — todos usan `req.nextUrl.pathname` sin `req.nextUrl.search`.
Consecuencias visibles:
- `/products` no filtra (no llega `q`)
- `/inventory` no filtra (F-126, mismo síntoma)
- Paginación rota si la API espera `?limit=&offset=` (los `totalPages` se calculan con `data.items.length` en frontend, pero el `limit` real siempre cae al default del backend)
- Cualquier filtro futuro basado en query params falla silenciosamente
## Diseño
Extraer `const search = req.nextUrl.search` (ya viene con `?` o vacío) y concatenarlo en las 5 funciones handler.
```ts
const path = req.nextUrl.pathname.replace('/api/', '');
const search = req.nextUrl.search; // "" o "?q=…&limit=…"
const backendRes = await fetch(`${API}/${path}${search}`, { ... });
```
Aplicar a: GET, POST, PATCH, PUT, DELETE.
## Riesgos
- Bajo. Es un bug donde se omite información; el cambio es añadir lo que faltaba.
- Riesgo residual: si alguna ruta backend asume un orden fijo de query params y se envía en otro orden, podría romper tests, pero nuestro backend usa URLSearchParams en server (orden-independiente).
## Plan
1. Editar `apps/admin/src/app/api/[...path]/route.ts` — añadir `const search = req.nextUrl.search` en cada handler y concatenarlo en la URL.
2. `cd apps/admin && npx tsc --noEmit`.
3. `cd apps/admin && npm run build`.
4. Verificar con `curl`:
- `curl -b cookies /api/catalog/products?q=almendra` → 1 item
- `curl -b cookies /api/catalog/products?q=almendra&limit=5` → 1 item, no 13
5. Cerrar gates.

View File

@@ -0,0 +1,57 @@
# F-131 — Reenviar query string en proxy catch-all del admin
## Cambios
### `apps/admin/src/app/api/[...path]/route.ts`
- 5 handlers (GET, POST, PATCH, PUT, DELETE) actualizados para incluir `req.nextUrl.search` al construir la URL hacia el backend.
- Antes: `fetch(`${API}/${path}`, …)` — descartaba cualquier query string.
- Después: `fetch(`${API}/${path}${search}`, …)` — reenvía `?q=…&limit=…&offset=…` al backend.
```diff
export async function GET(req: NextRequest) {
const path = req.nextUrl.pathname.replace('/api/', '');
+ const search = req.nextUrl.search;
- const backendRes = await fetch(`${API}/${path}`, { … });
+ const backendRes = await fetch(`${API}/${path}${search}`, { … });
```
(Mismo patrón aplicado a POST, PATCH, PUT, DELETE.)
## Verificación
### Antes del fix
```
GET /catalog/products?q=almendra&limit=5 (directo al backend)
→ 200 { total: 1, items: ['Almendras Crudas Ecologicas'] } ✅
GET /api/catalog/products?q=almendra&limit=5 (vía proxy del admin)
→ 200 { total: 13, items: [los 13 productos] } ❌ (q se pierde)
```
### Después del fix
- `apps/admin/.next/server/chunks/[root-of-the-server]__0tr-qzw._.js.map` contiene:
```
fetch(`${API}/${path}${search}`, { headers: { Cookie: cookies } })
fetch(`${API}/${path}${search}`, { method: 'POST', … })
fetch(`${API}/${path}${search}`, { method: 'PATCH', … })
fetch(`${API}/${path}${search}`, { method: 'PUT', … })
fetch(`${API}/${path}${search}`, { method: 'DELETE', … })
```
- Los 5 handlers ahora reenvían `search`.
### Tests
- `cd apps/admin && npx tsc --noEmit` → exit 0.
- `cd apps/admin && NEXT_PUBLIC_API_URL=http://192.168.18.93:3000 npm run build` → exit 0.
## Impacto colateral
Este fix también repara:
- **F-126** (buscador /inventory): mismo síntoma, misma causa. Ahora `?q=…` llega al backend.
- Cualquier otro endpoint del admin que reciba query params (paginación `?limit=&offset=`, filtros de fecha, etc.).
- El frontend ya construye correctamente las URLs; el proxy era el cuello de botella.
## Notas
- El operador debe reiniciar el admin (`./scripts/monolith.sh prod restart`) para desplegar el bundle compilado.
- Antes del restart, el bundle live en `:3004` sigue con el bug (verificado: `?q=almendra` aún devuelve 13 productos a través del proxy).

View File

@@ -0,0 +1,17 @@
{
"verdict": "APPROVED",
"agent": "leader",
"feature_id": "F-131",
"summary": "F-131 listo para commit. Build del admin regenerado con el fix. Operador reinicia para desplegar.",
"checks": [
"reviewer.json APPROVED",
"security.json APPROVED",
"qa.json APPROVED",
"implementer.md completo",
"verify.sh verde",
"1 archivo modificado: apps/admin/src/app/api/[...path]/route.ts"
],
"commit_message": "feat(F-131): completed feature",
"next_step": "operador: ./scripts/monolith.sh prod restart",
"closed_at": "2026-08-21T15:50:00Z"
}

View File

@@ -0,0 +1,21 @@
{
"verdict": "APPROVED",
"reviewer": "qa",
"feature_id": "F-131",
"summary": "Verificación de build limpio y fix en bundle compilado.",
"checks": [
"Repro pre-fix: curl http://192.168.18.93:3004/api/catalog/products?q=almendra → 13 items (sin filtro)",
"Repro pre-fix: curl http://192.168.18.93:3000/catalog/products?q=almendra → 1 item (filtra OK)",
"Build post-fix: apps/admin/.next/server/chunks/...route_actions_*.js contiene fetch(${API}/${path}${search}) en los 5 métodos",
"5 métodos (GET, POST, PATCH, PUT, DELETE) ahora reenvían search",
"tsc --noEmit exit 0",
"npm run build exit 0"
],
"evidence_files": [
"apps/admin/src/app/api/[...path]/route.ts",
"apps/admin/.next/server/app/api/[...path]/route.js",
"apps/admin/.next/server/chunks/[root-of-the-server]__0tr-qzw._.js"
],
"notes": "Tras reinicio del monolith, /products y /inventory deberían filtrar correctamente por query params.",
"reviewed_at": "2026-08-21T15:50:00Z"
}

View File

@@ -0,0 +1,16 @@
{
"verdict": "APPROVED",
"reviewer": "reviewer",
"feature_id": "F-131",
"summary": "Fix mínimo y crítico en proxy catch-all: reenvío de query string al backend.",
"checks": [
"apps/admin/src/app/api/[...path]/route.ts — 5 handlers (GET, POST, PATCH, PUT, DELETE) actualizados",
"Cada handler extrae req.nextUrl.search y lo concatena a la URL del backend",
"tsc --noEmit exit 0",
"npm run build exit 0",
"Bundle compilado contiene fetch(`${API}/${path}${search}`) en los 5 handlers",
"No se modificaron rutas más específicas (/api/auth/*, /api/upload)"
],
"notes": "El bug afectaba también a /inventory (F-126) y a cualquier endpoint con query params. El fix es transversal.",
"reviewed_at": "2026-08-21T15:50:00Z"
}

View File

@@ -0,0 +1,15 @@
{
"verdict": "APPROVED",
"reviewer": "security",
"feature_id": "F-131",
"summary": "Fix de proxy: añade query string al reenviar. No introduce nuevas superficies de ataque.",
"checks": [
"Sin cambios en autenticación / autorización (el cookie se sigue reenviando)",
"Sin cambios en validación de inputs (las queries llegan al backend que ya las valida con zod)",
"No se exponen endpoints nuevos: solo se reenvía lo que ya se aceptaba",
"El proxy sigue siendo un catch-all controlado",
"Sin riesgo de SSRF: la URL base del backend es fija (NEXT_PUBLIC_API_URL o localhost:3000)"
],
"notes": "El cambio es estrictamente aditivo: añade info que antes se descartaba. Ningún vector de ataque nuevo.",
"reviewed_at": "2026-08-21T15:50:00Z"
}

View File

@@ -1,12 +1,12 @@
{ {
"feature_id": "F-123", "feature_id": "F-131",
"stage": "build", "stage": "build",
"agent": "implementer", "agent": "implementer",
"action": "Remove warnings in ProductEditor and adapt PriceStockSection/ImagesSection to optional productId", "action": "Forward query string in admin catch-all proxy",
"state": "running", "state": "running",
"next_agent": "reviewer", "next_agent": "reviewer",
"waiting_for": "build complete", "waiting_for": "build",
"updated_at": "2026-08-21T15:17:05Z", "updated_at": "2026-08-21T15:49:19Z",
"timeline": [ "timeline": [
{ {
"ts": "2026-08-21T15:16:30Z", "ts": "2026-08-21T15:16:30Z",
@@ -21,6 +21,20 @@
"stage": "build", "stage": "build",
"state": "running", "state": "running",
"message": "Remove warnings in ProductEditor and adapt PriceStockSection/ImagesSection to optional productId" "message": "Remove warnings in ProductEditor and adapt PriceStockSection/ImagesSection to optional productId"
},
{
"ts": "2026-08-21T15:45:42Z",
"agent": "architect",
"stage": "design",
"state": "running",
"message": "Diagnose search in /admin/products"
},
{
"ts": "2026-08-21T15:49:19Z",
"agent": "implementer",
"stage": "build",
"state": "running",
"message": "Forward query string in admin catch-all proxy"
} }
] ]
} }