feat(F-052): completed feature
This commit is contained in:
@@ -2863,7 +2863,7 @@
|
|||||||
{
|
{
|
||||||
"id": "F-052",
|
"id": "F-052",
|
||||||
"title": "Imágenes de producto 404 en frontend y storefront",
|
"title": "Imágenes de producto 404 en frontend y storefront",
|
||||||
"status": "pending",
|
"status": "done",
|
||||||
"stage": null,
|
"stage": null,
|
||||||
"type": "fix",
|
"type": "fix",
|
||||||
"priority": "high",
|
"priority": "high",
|
||||||
@@ -2872,12 +2872,13 @@
|
|||||||
"updated_at": "2026-08-19T08:23:15Z",
|
"updated_at": "2026-08-19T08:23:15Z",
|
||||||
"assignee": null,
|
"assignee": null,
|
||||||
"gates": {
|
"gates": {
|
||||||
"reviewer": null,
|
"reviewer": true,
|
||||||
"security": null,
|
"security": true,
|
||||||
"qa": null,
|
"qa": true,
|
||||||
"close": null
|
"close": true
|
||||||
},
|
},
|
||||||
"acceptance": "1. GET /uploads/[file] desde frontend (3003) devuelve 200 con la imagen. 2. GET /uploads/[file] desde storefront (3005) devuelve 200 con la imagen."
|
"acceptance": "1. GET /uploads/[file] desde frontend (3003) devuelve 200 con la imagen. 2. GET /uploads/[file] desde storefront (3005) devuelve 200 con la imagen.",
|
||||||
|
"completed_at": "2026-08-19T08:47:17Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "F-053",
|
"id": "F-053",
|
||||||
|
|||||||
@@ -1,8 +1,28 @@
|
|||||||
import { randomUUID } from 'node:crypto';
|
import { randomUUID } from 'node:crypto';
|
||||||
import { mkdir, writeFile } from 'node:fs/promises';
|
import { copyFile, mkdir, writeFile } from 'node:fs/promises';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Peer upload directories (frontend and storefront) that must mirror the
|
||||||
|
* admin's own `public/uploads/`. After every successful upload, the file
|
||||||
|
* is copied to each peer so that product pages render images regardless
|
||||||
|
* of which app the URL is fetched from.
|
||||||
|
*/
|
||||||
|
const PEER_UPLOAD_DIRS = [
|
||||||
|
path.join(process.cwd(), '..', '..', 'frontend', 'public', 'uploads'),
|
||||||
|
path.join(process.cwd(), '..', '..', 'storefront', 'public', 'uploads'),
|
||||||
|
];
|
||||||
|
|
||||||
|
async function mirrorToPeers(filePath: string): Promise<void> {
|
||||||
|
await Promise.allSettled(
|
||||||
|
PEER_UPLOAD_DIRS.map(async (dir) => {
|
||||||
|
await mkdir(dir, { recursive: true });
|
||||||
|
await copyFile(filePath, path.join(dir, path.basename(filePath)));
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const API = process.env.NEXT_PUBLIC_API_URL ?? 'http://127.0.0.1:3000';
|
const API = process.env.NEXT_PUBLIC_API_URL ?? 'http://127.0.0.1:3000';
|
||||||
const MAX_SIZE = 10 * 1024 * 1024;
|
const MAX_SIZE = 10 * 1024 * 1024;
|
||||||
const EXTENSION_BY_TYPE: Readonly<Record<string, string>> = {
|
const EXTENSION_BY_TYPE: Readonly<Record<string, string>> = {
|
||||||
@@ -49,6 +69,7 @@ export async function POST(request: NextRequest) {
|
|||||||
|
|
||||||
await mkdir(uploadDir, { recursive: true });
|
await mkdir(uploadDir, { recursive: true });
|
||||||
await writeFile(filePath, buffer, { flag: 'wx' });
|
await writeFile(filePath, buffer, { flag: 'wx' });
|
||||||
|
await mirrorToPeers(filePath);
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
url: `/uploads/${filename}`,
|
url: `/uploads/${filename}`,
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 332 B |
Binary file not shown.
|
After Width: | Height: | Size: 332 B |
Binary file not shown.
|
After Width: | Height: | Size: 127 KiB |
49
project/frontend/src/app/uploads/[filename]/route.ts
Normal file
49
project/frontend/src/app/uploads/[filename]/route.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
import { readFile } from 'fs/promises';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
const MIME: Record<string, string> = {
|
||||||
|
'.jpg': 'image/jpeg',
|
||||||
|
'.jpeg': 'image/jpeg',
|
||||||
|
'.png': 'image/png',
|
||||||
|
'.webp': 'image/webp',
|
||||||
|
'.avif': 'image/avif',
|
||||||
|
'.gif': 'image/gif',
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serves uploaded product images dynamically from disk on every request.
|
||||||
|
*
|
||||||
|
* Next.js production mode caches the public/ directory listing at build/start
|
||||||
|
* time, so files added after startup return 404 when served as static
|
||||||
|
* assets. This route reads the file fresh from disk each time, mirroring
|
||||||
|
* the behaviour of the admin app so newly uploaded images are immediately
|
||||||
|
* available without a rebuild.
|
||||||
|
*/
|
||||||
|
export async function GET(
|
||||||
|
_request: NextRequest,
|
||||||
|
{ params }: { params: Promise<{ filename: string }> },
|
||||||
|
) {
|
||||||
|
const { filename } = await params;
|
||||||
|
|
||||||
|
const safe = path.basename(filename);
|
||||||
|
if (safe !== filename || filename.includes('..')) {
|
||||||
|
return NextResponse.json({ error: 'Invalid filename' }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const ext = path.extname(safe).toLowerCase();
|
||||||
|
const filePath = path.join(process.cwd(), 'public', 'uploads', safe);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const buffer = await readFile(filePath);
|
||||||
|
return new NextResponse(buffer, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': MIME[ext] ?? 'application/octet-stream',
|
||||||
|
'Cache-Control': 'public, max-age=31536000, immutable',
|
||||||
|
'X-Content-Type-Options': 'nosniff',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
return NextResponse.json({ error: 'Not found' }, { status: 404 });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -203,6 +203,29 @@ build_prod() {
|
|||||||
(cd "$PROJECT_DIR/frontend" && NEXT_PUBLIC_API_URL="$API_PUBLIC_URL" npm run build)
|
(cd "$PROJECT_DIR/frontend" && NEXT_PUBLIC_API_URL="$API_PUBLIC_URL" npm run build)
|
||||||
echo '[INFO] Building SEO storefront...'
|
echo '[INFO] Building SEO storefront...'
|
||||||
(cd "$PROJECT_DIR/storefront" && API_BASE_URL="$API_PUBLIC_URL" NEXT_PUBLIC_API_URL="$API_PUBLIC_URL" npm run build)
|
(cd "$PROJECT_DIR/storefront" && API_BASE_URL="$API_PUBLIC_URL" NEXT_PUBLIC_API_URL="$API_PUBLIC_URL" npm run build)
|
||||||
|
sync_uploads
|
||||||
|
}
|
||||||
|
|
||||||
|
sync_uploads() {
|
||||||
|
# Mirror admin's public/uploads to frontend and storefront so that product
|
||||||
|
# images are reachable from any of the three apps.
|
||||||
|
local src="$PROJECT_DIR/apps/admin/public/uploads"
|
||||||
|
local peer1="$PROJECT_DIR/frontend/public/uploads"
|
||||||
|
local peer2="$PROJECT_DIR/storefront/public/uploads"
|
||||||
|
if [[ ! -d "$src" ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
for peer in "$peer1" "$peer2"; do
|
||||||
|
mkdir -p "$peer"
|
||||||
|
# copy every file present in source but missing in peer
|
||||||
|
while IFS= read -r -d '' f; do
|
||||||
|
local name
|
||||||
|
name="$(basename "$f")"
|
||||||
|
if [[ ! -f "$peer/$name" ]]; then
|
||||||
|
cp "$f" "$peer/$name" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
done < <(find "$src" -maxdepth 1 -type f -print0 2>/dev/null)
|
||||||
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
spawn_service() {
|
spawn_service() {
|
||||||
@@ -262,6 +285,7 @@ start_all() {
|
|||||||
install_dependencies
|
install_dependencies
|
||||||
migrate
|
migrate
|
||||||
[[ "$MODE" == "prod" ]] && build_prod
|
[[ "$MODE" == "prod" ]] && build_prod
|
||||||
|
sync_uploads
|
||||||
for service in "${SERVICES[@]}"; do spawn_service "$service"; done
|
for service in "${SERVICES[@]}"; do spawn_service "$service"; done
|
||||||
echo
|
echo
|
||||||
print_urls
|
print_urls
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 332 B |
Binary file not shown.
|
After Width: | Height: | Size: 332 B |
Binary file not shown.
|
After Width: | Height: | Size: 127 KiB |
45
project/storefront/src/app/uploads/[filename]/route.ts
Normal file
45
project/storefront/src/app/uploads/[filename]/route.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
import { readFile } from 'fs/promises';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
const MIME: Record<string, string> = {
|
||||||
|
'.jpg': 'image/jpeg',
|
||||||
|
'.jpeg': 'image/jpeg',
|
||||||
|
'.png': 'image/png',
|
||||||
|
'.webp': 'image/webp',
|
||||||
|
'.avif': 'image/avif',
|
||||||
|
'.gif': 'image/gif',
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serves uploaded product images dynamically from disk on every request.
|
||||||
|
* Mirrors the behaviour of the admin app so newly uploaded images are
|
||||||
|
* immediately available without a rebuild.
|
||||||
|
*/
|
||||||
|
export async function GET(
|
||||||
|
_request: NextRequest,
|
||||||
|
{ params }: { params: Promise<{ filename: string }> },
|
||||||
|
) {
|
||||||
|
const { filename } = await params;
|
||||||
|
|
||||||
|
const safe = path.basename(filename);
|
||||||
|
if (safe !== filename || filename.includes('..')) {
|
||||||
|
return NextResponse.json({ error: 'Invalid filename' }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const ext = path.extname(safe).toLowerCase();
|
||||||
|
const filePath = path.join(process.cwd(), 'public', 'uploads', safe);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const buffer = await readFile(filePath);
|
||||||
|
return new NextResponse(buffer, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': MIME[ext] ?? 'application/octet-stream',
|
||||||
|
'Cache-Control': 'public, max-age=31536000, immutable',
|
||||||
|
'X-Content-Type-Options': 'nosniff',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
return NextResponse.json({ error: 'Not found' }, { status: 404 });
|
||||||
|
}
|
||||||
|
}
|
||||||
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."
|
||||||
|
}
|
||||||
@@ -1,139 +1,13 @@
|
|||||||
{
|
{
|
||||||
"feature_id": "F-051",
|
"feature_id": "F-052",
|
||||||
"stage": "document",
|
"stage": "document",
|
||||||
"agent": "documenter",
|
"agent": "documenter",
|
||||||
"action": "Documentar",
|
"action": "Documentar",
|
||||||
"state": "running",
|
"state": "running",
|
||||||
"next_agent": "leader",
|
"next_agent": "leader",
|
||||||
"waiting_for": null,
|
"waiting_for": null,
|
||||||
"updated_at": "2026-08-19T08:43:10Z",
|
"updated_at": "2026-08-19T08:47:06Z",
|
||||||
"timeline": [
|
"timeline": [
|
||||||
{
|
|
||||||
"ts": "2026-08-19T05:34:15Z",
|
|
||||||
"agent": "leader",
|
|
||||||
"stage": "close",
|
|
||||||
"state": "done",
|
|
||||||
"message": "Gates y verify verdes"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ts": "2026-08-19T05:46:46Z",
|
|
||||||
"agent": "leader",
|
|
||||||
"stage": "intake",
|
|
||||||
"state": "running",
|
|
||||||
"message": "Inicio de intake"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ts": "2026-08-19T05:48:16Z",
|
|
||||||
"agent": "architect",
|
|
||||||
"stage": "design",
|
|
||||||
"state": "running",
|
|
||||||
"message": "Inicio de design"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ts": "2026-08-19T05:48:56Z",
|
|
||||||
"agent": "implementer",
|
|
||||||
"stage": "build",
|
|
||||||
"state": "running",
|
|
||||||
"message": "Inicio de build"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ts": "2026-08-19T05:53:10Z",
|
|
||||||
"agent": "reviewer",
|
|
||||||
"stage": "review_gate",
|
|
||||||
"state": "running",
|
|
||||||
"message": "Inicio de review gate"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ts": "2026-08-19T05:53:20Z",
|
|
||||||
"agent": "security",
|
|
||||||
"stage": "security_gate",
|
|
||||||
"state": "running",
|
|
||||||
"message": "Inicio de security gate"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ts": "2026-08-19T05:53:38Z",
|
|
||||||
"agent": "qa",
|
|
||||||
"stage": "qa_gate",
|
|
||||||
"state": "running",
|
|
||||||
"message": "Inicio de QA gate"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ts": "2026-08-19T05:54:11Z",
|
|
||||||
"agent": "documenter",
|
|
||||||
"stage": "document",
|
|
||||||
"state": "running",
|
|
||||||
"message": "Inicio de document"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ts": "2026-08-19T05:54:36Z",
|
|
||||||
"agent": "leader",
|
|
||||||
"stage": "close",
|
|
||||||
"state": "done",
|
|
||||||
"message": "Gates y verify verdes"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ts": "2026-08-19T08:41:38Z",
|
|
||||||
"agent": "leader",
|
|
||||||
"stage": "intake",
|
|
||||||
"state": "running",
|
|
||||||
"message": "Inicio de intake"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ts": "2026-08-19T08:41:45Z",
|
|
||||||
"agent": "architect",
|
|
||||||
"stage": "design",
|
|
||||||
"state": "done",
|
|
||||||
"message": "Diagnóstico y solución clara"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ts": "2026-08-19T08:42:06Z",
|
|
||||||
"agent": "implementer",
|
|
||||||
"stage": "build",
|
|
||||||
"state": "running",
|
|
||||||
"message": "Inicio de build"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ts": "2026-08-19T08:42:47Z",
|
|
||||||
"agent": "implementer",
|
|
||||||
"stage": "build",
|
|
||||||
"state": "done",
|
|
||||||
"message": "Favicons servidos"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ts": "2026-08-19T08:42:49Z",
|
|
||||||
"agent": "reviewer",
|
|
||||||
"stage": "review_gate",
|
|
||||||
"state": "running",
|
|
||||||
"message": "Inicio review"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ts": "2026-08-19T08:42:53Z",
|
|
||||||
"agent": "reviewer",
|
|
||||||
"stage": "review_gate",
|
|
||||||
"state": "done",
|
|
||||||
"message": "Favicons validados"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ts": "2026-08-19T08:42:54Z",
|
|
||||||
"agent": "security",
|
|
||||||
"stage": "security_gate",
|
|
||||||
"state": "running",
|
|
||||||
"message": "Inicio security"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ts": "2026-08-19T08:43:01Z",
|
|
||||||
"agent": "security",
|
|
||||||
"stage": "security_gate",
|
|
||||||
"state": "done",
|
|
||||||
"message": "0 vulnerabilidades"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ts": "2026-08-19T08:43:03Z",
|
|
||||||
"agent": "qa",
|
|
||||||
"stage": "qa_gate",
|
|
||||||
"state": "running",
|
|
||||||
"message": "Inicio QA"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"ts": "2026-08-19T08:43:08Z",
|
"ts": "2026-08-19T08:43:08Z",
|
||||||
"agent": "qa",
|
"agent": "qa",
|
||||||
@@ -147,6 +21,132 @@
|
|||||||
"stage": "document",
|
"stage": "document",
|
||||||
"state": "running",
|
"state": "running",
|
||||||
"message": "Inicio document"
|
"message": "Inicio document"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ts": "2026-08-19T08:43:18Z",
|
||||||
|
"agent": "leader",
|
||||||
|
"stage": "intake",
|
||||||
|
"state": "running",
|
||||||
|
"message": "Inicio"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ts": "2026-08-19T08:43:30Z",
|
||||||
|
"agent": "architect",
|
||||||
|
"stage": "design",
|
||||||
|
"state": "done",
|
||||||
|
"message": "Sync uploads en admin"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ts": "2026-08-19T08:43:31Z",
|
||||||
|
"agent": "implementer",
|
||||||
|
"stage": "build",
|
||||||
|
"state": "running",
|
||||||
|
"message": "Inicio build"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ts": "2026-08-19T08:44:37Z",
|
||||||
|
"agent": "implementer",
|
||||||
|
"stage": "build",
|
||||||
|
"state": "done",
|
||||||
|
"message": "Sync operativo"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ts": "2026-08-19T08:44:38Z",
|
||||||
|
"agent": "reviewer",
|
||||||
|
"stage": "review_gate",
|
||||||
|
"state": "running",
|
||||||
|
"message": "Inicio review"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ts": "2026-08-19T08:44:43Z",
|
||||||
|
"agent": "reviewer",
|
||||||
|
"stage": "review_gate",
|
||||||
|
"state": "done",
|
||||||
|
"message": "Validated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ts": "2026-08-19T08:44:44Z",
|
||||||
|
"agent": "security",
|
||||||
|
"stage": "security_gate",
|
||||||
|
"state": "running",
|
||||||
|
"message": "Inicio security"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ts": "2026-08-19T08:44:51Z",
|
||||||
|
"agent": "security",
|
||||||
|
"stage": "security_gate",
|
||||||
|
"state": "done",
|
||||||
|
"message": "Audit verde"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ts": "2026-08-19T08:44:52Z",
|
||||||
|
"agent": "qa",
|
||||||
|
"stage": "qa_gate",
|
||||||
|
"state": "running",
|
||||||
|
"message": "Inicio QA"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ts": "2026-08-19T08:45:23Z",
|
||||||
|
"agent": "implementer",
|
||||||
|
"stage": "build",
|
||||||
|
"state": "running",
|
||||||
|
"message": "Build iteración 2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ts": "2026-08-19T08:46:36Z",
|
||||||
|
"agent": "implementer",
|
||||||
|
"stage": "build",
|
||||||
|
"state": "done",
|
||||||
|
"message": "Ruta dinámica añadida"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ts": "2026-08-19T08:46:38Z",
|
||||||
|
"agent": "reviewer",
|
||||||
|
"stage": "review_gate",
|
||||||
|
"state": "running",
|
||||||
|
"message": "Code changed"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ts": "2026-08-19T08:46:45Z",
|
||||||
|
"agent": "reviewer",
|
||||||
|
"stage": "review_gate",
|
||||||
|
"state": "done",
|
||||||
|
"message": "Validated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ts": "2026-08-19T08:46:48Z",
|
||||||
|
"agent": "security",
|
||||||
|
"stage": "security_gate",
|
||||||
|
"state": "running",
|
||||||
|
"message": "Code changed"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ts": "2026-08-19T08:46:57Z",
|
||||||
|
"agent": "security",
|
||||||
|
"stage": "security_gate",
|
||||||
|
"state": "done",
|
||||||
|
"message": "Audit verde"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ts": "2026-08-19T08:46:59Z",
|
||||||
|
"agent": "qa",
|
||||||
|
"stage": "qa_gate",
|
||||||
|
"state": "running",
|
||||||
|
"message": "Inicio QA"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ts": "2026-08-19T08:47:05Z",
|
||||||
|
"agent": "qa",
|
||||||
|
"stage": "qa_gate",
|
||||||
|
"state": "done",
|
||||||
|
"message": "Validated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ts": "2026-08-19T08:47:06Z",
|
||||||
|
"agent": "documenter",
|
||||||
|
"stage": "document",
|
||||||
|
"state": "running",
|
||||||
|
"message": "Inicio document"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"last_updated": "2026-08-19T06:12:02Z",
|
"last_updated": "2026-08-19T06:12:02Z",
|
||||||
|
|||||||
Reference in New Issue
Block a user