feat(F-052): completed feature
This commit is contained in:
32
work/artifacts/F-052/architect.md
Normal file
32
work/artifacts/F-052/architect.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# Architect — F-052
|
||||
|
||||
## Diagnóstico
|
||||
- Las URLs de imagen de producto se almacenan como `/uploads/<uuid>.<ext>` en `catalog_product_images.url`.
|
||||
- Solo el admin (`apps/admin/public/uploads/`) tiene esos archivos.
|
||||
- El frontend (3003) y el storefront (3005) sirven estáticos desde sus propios `public/`, que no contienen `uploads/`.
|
||||
- El backend (3000) no tiene una ruta `/uploads/` y por tanto tampoco puede servirlos.
|
||||
- Resultado: `<img src="/uploads/...">` desde frontend/storefront → 404.
|
||||
|
||||
## Solución elegida
|
||||
**Sincronizar `public/uploads/` del admin al frontend y al storefront en cada upload, y al arranque.**
|
||||
|
||||
Pasos:
|
||||
1. Crear un helper que, ante una subida nueva en el admin (`/api/upload`), copie el archivo a `frontend/public/uploads/` y `storefront/public/uploads/` después de escribirlo.
|
||||
2. Al arranque de `monolith.sh`, sincronizar los archivos existentes con `rsync` para cubrir el caso "arranque en frío sin uploads aún".
|
||||
3. El backend sigue siendo solo API; no se añade ruta `/uploads/`.
|
||||
|
||||
Razón de no añadir ruta backend:
|
||||
- Mantener el backend libre de filesystem compartido.
|
||||
- Evita acoplarse a `process.cwd()` del backend, que es distinto al del admin.
|
||||
- El admin es el único origen de uploads.
|
||||
|
||||
## Alternativas descartadas
|
||||
- **Ruta `/uploads/` en el backend**: requiere cambiar `cwd` o aceptar una ruta absoluta; complica el deployment.
|
||||
- **Proxy desde frontend a admin**: añade un round-trip y un puerto extra en la URL.
|
||||
|
||||
## Acceptance
|
||||
- Tras subir una imagen desde el admin, esa imagen aparece como 200 en:
|
||||
- `GET http://192.168.18.93:3003/uploads/<file>`
|
||||
- `GET http://192.168.18.93:3005/uploads/<file>`
|
||||
- Tras un restart del monolito, los uploads ya existentes se sincronizan.
|
||||
- `next build` no falla por assets faltantes.
|
||||
17
work/artifacts/F-052/documenter.md
Normal file
17
work/artifacts/F-052/documenter.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# Documenter — F-052
|
||||
|
||||
## Cambio visible
|
||||
Las imágenes de producto ahora cargan correctamente en:
|
||||
- Frontend: `http://192.168.18.93:3003/products/<slug>`
|
||||
- Storefront: `http://192.168.18.93:3005/products/<slug>`
|
||||
|
||||
Las nuevas subidas desde el backoffice aparecen **sin reiniciar** ningún servicio.
|
||||
|
||||
## Cómo funciona
|
||||
1. El admin sube una imagen vía `/api/upload` → escribe en `apps/admin/public/uploads/`.
|
||||
2. Inmediatamente, el mismo handler copia el archivo a `frontend/public/uploads/` y `storefront/public/uploads/`.
|
||||
3. Los tres apps sirven la imagen con un handler dinámico `/uploads/[filename]/route.ts` que lee del disco en cada request.
|
||||
4. Al hacer `monolith.sh start|restart`, `sync_uploads()` reconcilia los archivos que estuvieran desincronizados.
|
||||
|
||||
## Detalle técnico
|
||||
Next.js en producción cachea el listado de `public/` al hacer `next build`. Sin el handler dinámico, los archivos subidos después devolverían 404 hasta el próximo rebuild. El handler dinámico (`readFile` en cada request) evita ese problema y replica el patrón del admin.
|
||||
42
work/artifacts/F-052/implementer.md
Normal file
42
work/artifacts/F-052/implementer.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# Implementer — F-052 (iteración 2)
|
||||
|
||||
## Cambios (completos)
|
||||
|
||||
### Código
|
||||
- `apps/admin/src/app/api/upload/route.ts`: tras `writeFile`, copia a `frontend/public/uploads/` y `storefront/public/uploads/` con `Promise.allSettled`. La copia no bloquea el upload principal.
|
||||
- `project/scripts/monolith.sh`: nueva función `sync_uploads()` que se invoca al final de `build_prod()` y antes de `spawn_service` en `start_all()` para cubrir el caso "arranque en frío sin uploads aún".
|
||||
- **`frontend/src/app/uploads/[filename]/route.ts`** y **`storefront/src/app/uploads/[filename]/route.ts`**: handlers dinámicos (idénticos al del admin) que leen cada petición de `public/uploads/<filename>` directamente del disco. Imprescindible: Next.js sirve los estáticos de `public/` cacheados al build time, por lo que cualquier archivo subido después devolvería 404 sin esta ruta.
|
||||
|
||||
### Assets
|
||||
- `frontend/public/uploads/` y `storefront/public/uploads/` creados.
|
||||
- Archivos sincronizados manualmente para evitar esperar al próximo restart.
|
||||
|
||||
### Servicios
|
||||
- `next build` re-ejecutado para admin, frontend y storefront.
|
||||
- Servicios reiniciados.
|
||||
|
||||
## Verificación end-to-end
|
||||
|
||||
### Imágenes existentes (en build)
|
||||
```
|
||||
GET 3004 /uploads/fa7a31db-...jpg → 200 image/jpeg 129711
|
||||
GET 3003 /uploads/fa7a31db-...jpg → 200 image/jpeg 129711
|
||||
GET 3005 /uploads/fa7a31db-...jpg → 200 image/jpeg 129711
|
||||
```
|
||||
|
||||
### Imagen subida en caliente (sin rebuild)
|
||||
```
|
||||
POST 3004 /api/upload (admin) → 201 {url: /uploads/a7e3aa30-...jpg}
|
||||
GET 3004 /uploads/a7e3aa30-...jpg → 200 image/jpeg
|
||||
GET 3003 /uploads/a7e3aa30-...jpg → 200 image/jpeg
|
||||
GET 3005 /uploads/a7e3aa30-...jpg → 200 image/jpeg
|
||||
```
|
||||
|
||||
### Favicons siguen 200
|
||||
```
|
||||
GET 3004 /favicon.ico → 200
|
||||
GET 3005 /favicon.ico → 200
|
||||
```
|
||||
|
||||
## Por qué fue necesaria la 2ª iteración
|
||||
El `mirrorToPeers` del admin copia el archivo al disco de frontend/storefront, pero los servicios en producción cachean el listado de `public/` al build. Sin una ruta dinámica `/uploads/[filename]/`, los archivos nuevos solo se sirven si existían al hacer `next build`. La ruta dinámica los lee frescos del disco en cada request, igual que hace el admin desde F-050.
|
||||
10
work/artifacts/F-052/leader-close.json
Normal file
10
work/artifacts/F-052/leader-close.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"feature_id": "F-052",
|
||||
"verdict": "APPROVED",
|
||||
"agent": "leader",
|
||||
"timestamp": "2026-08-19T08:47:30Z",
|
||||
"gates_approved": {"reviewer": true, "security": true, "qa": true},
|
||||
"verify_sh": "green",
|
||||
"summary": "Imágenes de producto servidas en los 3 apps; nuevos uploads aparecen sin rebuild vía mirrorToPeers + rutas dinámicas /uploads/[filename]/.",
|
||||
"push": "No origin remote"
|
||||
}
|
||||
17
work/artifacts/F-052/qa.json
Normal file
17
work/artifacts/F-052/qa.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"feature_id": "F-052",
|
||||
"verdict": "APPROVED",
|
||||
"agent": "qa",
|
||||
"timestamp": "2026-08-19T08:47:30Z",
|
||||
"checks": {
|
||||
"acceptance_existing_frontend": {"pass": true, "evidence": "GET /uploads/fa7a31db-...jpg en 3003 → 200 image/jpeg 129711"},
|
||||
"acceptance_existing_storefront": {"pass": true, "evidence": "GET /uploads/fa7a31db-...jpg en 3005 → 200 image/jpeg 129711"},
|
||||
"acceptance_hot_upload_admin": {"pass": true, "evidence": "Subida nueva a7e3aa30-...jpg sin rebuild → 200 en 3004"},
|
||||
"acceptance_hot_upload_frontend": {"pass": true, "evidence": "GET /uploads/a7e3aa30-...jpg en 3003 → 200 image/jpeg"},
|
||||
"acceptance_hot_upload_storefront": {"pass": true, "evidence": "GET /uploads/a7e3aa30-...jpg en 3005 → 200 image/jpeg"},
|
||||
"regression_favicon": {"pass": true, "evidence": "Favicons siguen 200 (F-051 OK)"},
|
||||
"regression_lan_smoke": {"pass": true, "evidence": "5/5 servicios en LAN 200"},
|
||||
"hygiene": {"pass": true, "evidence": "git diff --check verde"}
|
||||
},
|
||||
"notes": "QA aprobado. Las imágenes se sirven correctamente desde cualquier app y los nuevos uploads aparecen sin rebuild gracias a la ruta dinámica."
|
||||
}
|
||||
16
work/artifacts/F-052/reviewer.json
Normal file
16
work/artifacts/F-052/reviewer.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"feature_id": "F-052",
|
||||
"verdict": "APPROVED",
|
||||
"agent": "reviewer",
|
||||
"timestamp": "2026-08-19T08:47:00Z",
|
||||
"checks": {
|
||||
"acceptance_frontend": {"pass": true, "notes": "GET /uploads/fa7a31db-...jpg en 3003 → 200 image/jpeg 129711"},
|
||||
"acceptance_storefront": {"pass": true, "notes": "GET /uploads/fa7a31db-...jpg en 3005 → 200 image/jpeg 129711"},
|
||||
"acceptance_hot_upload": {"pass": true, "notes": "Subida nueva en admin sin restart → 200 image/jpeg en los 3 puertos"},
|
||||
"code_review_route": {"pass": true, "notes": "frontend y storefront tienen /uploads/[filename]/route.ts idénticos al admin (path.basename + path traversal guard + mime map + Cache-Control immutable)"},
|
||||
"mirror_review": {"pass": true, "notes": "admin upload route.ts mirrorToPeers con Promise.allSettled"},
|
||||
"monolith_review": {"pass": true, "notes": "sync_uploads() idempotente con find -maxdepth 1"},
|
||||
"build_ok": {"pass": true, "notes": "admin, frontend, storefront builds sin errores"}
|
||||
},
|
||||
"notes": "Aprobado. La segunda iteración añadió la ruta dinámica que faltaba para que Next.js sirva archivos subidos tras el build."
|
||||
}
|
||||
15
work/artifacts/F-052/security.json
Normal file
15
work/artifacts/F-052/security.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"feature_id": "F-052",
|
||||
"verdict": "APPROVED",
|
||||
"agent": "security",
|
||||
"timestamp": "2026-08-19T08:47:30Z",
|
||||
"checks": {
|
||||
"audit": {"pass": true, "notes": "npm audit --omit=dev --audit-level=high: 0 vulnerabilidades"},
|
||||
"upload_route_security": {"pass": true, "notes": "El handler sigue exigiendo isAuthenticatedBackofficeRequest y validación magic-bytes + tamaño; la copia a peers se hace después del writeFile atómico"},
|
||||
"new_routes_security": {"pass": true, "notes": "Los handlers dinámicos /uploads/[filename]/route.ts de frontend y storefront validan filename con path.basename y rechazan '..' antes de leer; mime map fijo; X-Content-Type-Options: nosniff"},
|
||||
"path_safety": {"pass": true, "notes": "Sin path traversal. Los peer dirs se construyen con path.join relativo a process.cwd() y filename siempre es UUID generado"},
|
||||
"secret_scan": {"pass": true, "notes": "Sin credenciales ni cambios en auth"},
|
||||
"hygiene": {"pass": true, "notes": "git diff --check verde"}
|
||||
},
|
||||
"notes": "Security aprobado tras segunda iteración."
|
||||
}
|
||||
Reference in New Issue
Block a user