feat(F-131): completed feature
This commit is contained in:
@@ -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.",
|
||||
"priority": "high",
|
||||
"risk": "low",
|
||||
"status": "pending",
|
||||
"status": "done",
|
||||
"created_at": "2026-08-21",
|
||||
"gates": {
|
||||
"reviewer": false,
|
||||
"security": false,
|
||||
"qa": false
|
||||
}
|
||||
"reviewer": true,
|
||||
"security": true,
|
||||
"qa": true,
|
||||
"close": true
|
||||
},
|
||||
"completed_at": "2026-08-21T15:50:31Z"
|
||||
},
|
||||
{
|
||||
"id": "F-132",
|
||||
|
||||
@@ -12,9 +12,10 @@ const API = process.env.NEXT_PUBLIC_API_URL ?? 'http://127.0.0.1:3000';
|
||||
*/
|
||||
export async function GET(req: NextRequest) {
|
||||
const path = req.nextUrl.pathname.replace('/api/', '');
|
||||
const search = req.nextUrl.search;
|
||||
const cookies = req.headers.get('cookie') ?? '';
|
||||
try {
|
||||
const backendRes = await fetch(`${API}/${path}`, {
|
||||
const backendRes = await fetch(`${API}/${path}${search}`, {
|
||||
headers: { Cookie: cookies },
|
||||
});
|
||||
if (path === 'admin/logs/stream') {
|
||||
@@ -37,12 +38,13 @@ export async function GET(req: NextRequest) {
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const path = req.nextUrl.pathname.replace('/api/', '');
|
||||
const search = req.nextUrl.search;
|
||||
const cookies = req.headers.get('cookie') ?? '';
|
||||
const body = await req.text();
|
||||
try {
|
||||
const headers: Record<string, string> = { Cookie: cookies };
|
||||
if (body) headers['Content-Type'] = 'application/json';
|
||||
const backendRes = await fetch(`${API}/${path}`, {
|
||||
const backendRes = await fetch(`${API}/${path}${search}`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
...(body ? { body } : {}),
|
||||
@@ -64,10 +66,11 @@ export async function POST(req: NextRequest) {
|
||||
|
||||
export async function PATCH(req: NextRequest) {
|
||||
const path = req.nextUrl.pathname.replace('/api/', '');
|
||||
const search = req.nextUrl.search;
|
||||
const cookies = req.headers.get('cookie') ?? '';
|
||||
const body = await req.text();
|
||||
try {
|
||||
const backendRes = await fetch(`${API}/${path}`, {
|
||||
const backendRes = await fetch(`${API}/${path}${search}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json', Cookie: cookies },
|
||||
body,
|
||||
@@ -81,10 +84,11 @@ export async function PATCH(req: NextRequest) {
|
||||
|
||||
export async function PUT(req: NextRequest) {
|
||||
const path = req.nextUrl.pathname.replace('/api/', '');
|
||||
const search = req.nextUrl.search;
|
||||
const cookies = req.headers.get('cookie') ?? '';
|
||||
const body = await req.text();
|
||||
try {
|
||||
const backendRes = await fetch(`${API}/${path}`, {
|
||||
const backendRes = await fetch(`${API}/${path}${search}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json', Cookie: cookies },
|
||||
body,
|
||||
@@ -98,9 +102,10 @@ export async function PUT(req: NextRequest) {
|
||||
|
||||
export async function DELETE(req: NextRequest) {
|
||||
const path = req.nextUrl.pathname.replace('/api/', '');
|
||||
const search = req.nextUrl.search;
|
||||
const cookies = req.headers.get('cookie') ?? '';
|
||||
try {
|
||||
const backendRes = await fetch(`${API}/${path}`, {
|
||||
const backendRes = await fetch(`${API}/${path}${search}`, {
|
||||
method: 'DELETE',
|
||||
headers: { Cookie: cookies },
|
||||
});
|
||||
|
||||
File diff suppressed because one or more lines are too long
67
work/artifacts/F-131/architect.md
Normal file
67
work/artifacts/F-131/architect.md
Normal 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.
|
||||
57
work/artifacts/F-131/implementer.md
Normal file
57
work/artifacts/F-131/implementer.md
Normal 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).
|
||||
17
work/artifacts/F-131/leader-close.json
Normal file
17
work/artifacts/F-131/leader-close.json
Normal 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"
|
||||
}
|
||||
21
work/artifacts/F-131/qa.json
Normal file
21
work/artifacts/F-131/qa.json
Normal 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"
|
||||
}
|
||||
16
work/artifacts/F-131/reviewer.json
Normal file
16
work/artifacts/F-131/reviewer.json
Normal 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"
|
||||
}
|
||||
15
work/artifacts/F-131/security.json
Normal file
15
work/artifacts/F-131/security.json
Normal 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"
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"feature_id": "F-123",
|
||||
"feature_id": "F-131",
|
||||
"stage": "build",
|
||||
"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",
|
||||
"next_agent": "reviewer",
|
||||
"waiting_for": "build complete",
|
||||
"updated_at": "2026-08-21T15:17:05Z",
|
||||
"waiting_for": "build",
|
||||
"updated_at": "2026-08-21T15:49:19Z",
|
||||
"timeline": [
|
||||
{
|
||||
"ts": "2026-08-21T15:16:30Z",
|
||||
@@ -21,6 +21,20 @@
|
||||
"stage": "build",
|
||||
"state": "running",
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user