diff --git a/backlog/features.json b/backlog/features.json index 24f2781..2b9fc1e 100644 --- a/backlog/features.json +++ b/backlog/features.json @@ -2863,7 +2863,7 @@ { "id": "F-052", "title": "Imágenes de producto 404 en frontend y storefront", - "status": "pending", + "status": "done", "stage": null, "type": "fix", "priority": "high", @@ -2872,12 +2872,13 @@ "updated_at": "2026-08-19T08:23:15Z", "assignee": null, "gates": { - "reviewer": null, - "security": null, - "qa": null, - "close": null + "reviewer": true, + "security": true, + "qa": true, + "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", diff --git a/project/apps/admin/src/app/api/upload/route.ts b/project/apps/admin/src/app/api/upload/route.ts index 818e454..818a780 100644 --- a/project/apps/admin/src/app/api/upload/route.ts +++ b/project/apps/admin/src/app/api/upload/route.ts @@ -1,8 +1,28 @@ 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 { 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 { + 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 MAX_SIZE = 10 * 1024 * 1024; const EXTENSION_BY_TYPE: Readonly> = { @@ -49,6 +69,7 @@ export async function POST(request: NextRequest) { await mkdir(uploadDir, { recursive: true }); await writeFile(filePath, buffer, { flag: 'wx' }); + await mirrorToPeers(filePath); return NextResponse.json({ url: `/uploads/${filename}`, diff --git a/project/frontend/public/uploads/a7e3aa30-facd-4638-a078-b36dbb78b3c6.jpg b/project/frontend/public/uploads/a7e3aa30-facd-4638-a078-b36dbb78b3c6.jpg new file mode 100644 index 0000000..d429f54 Binary files /dev/null and b/project/frontend/public/uploads/a7e3aa30-facd-4638-a078-b36dbb78b3c6.jpg differ diff --git a/project/frontend/public/uploads/d04dc789-4075-44e0-8437-69db363f1b70.jpg b/project/frontend/public/uploads/d04dc789-4075-44e0-8437-69db363f1b70.jpg new file mode 100644 index 0000000..d429f54 Binary files /dev/null and b/project/frontend/public/uploads/d04dc789-4075-44e0-8437-69db363f1b70.jpg differ diff --git a/project/frontend/public/uploads/fa7a31db-bab6-4c21-8e84-de444d6f1668.jpg b/project/frontend/public/uploads/fa7a31db-bab6-4c21-8e84-de444d6f1668.jpg new file mode 100644 index 0000000..2f14a21 Binary files /dev/null and b/project/frontend/public/uploads/fa7a31db-bab6-4c21-8e84-de444d6f1668.jpg differ diff --git a/project/frontend/src/app/uploads/[filename]/route.ts b/project/frontend/src/app/uploads/[filename]/route.ts new file mode 100644 index 0000000..3226f4c --- /dev/null +++ b/project/frontend/src/app/uploads/[filename]/route.ts @@ -0,0 +1,49 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { readFile } from 'fs/promises'; +import path from 'path'; + +const MIME: Record = { + '.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 }); + } +} diff --git a/project/scripts/monolith.sh b/project/scripts/monolith.sh index 91be407..d4a9ed3 100755 --- a/project/scripts/monolith.sh +++ b/project/scripts/monolith.sh @@ -203,6 +203,29 @@ build_prod() { (cd "$PROJECT_DIR/frontend" && NEXT_PUBLIC_API_URL="$API_PUBLIC_URL" npm run build) 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) + 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() { @@ -262,6 +285,7 @@ start_all() { install_dependencies migrate [[ "$MODE" == "prod" ]] && build_prod + sync_uploads for service in "${SERVICES[@]}"; do spawn_service "$service"; done echo print_urls diff --git a/project/storefront/public/uploads/a7e3aa30-facd-4638-a078-b36dbb78b3c6.jpg b/project/storefront/public/uploads/a7e3aa30-facd-4638-a078-b36dbb78b3c6.jpg new file mode 100644 index 0000000..d429f54 Binary files /dev/null and b/project/storefront/public/uploads/a7e3aa30-facd-4638-a078-b36dbb78b3c6.jpg differ diff --git a/project/storefront/public/uploads/d04dc789-4075-44e0-8437-69db363f1b70.jpg b/project/storefront/public/uploads/d04dc789-4075-44e0-8437-69db363f1b70.jpg new file mode 100644 index 0000000..d429f54 Binary files /dev/null and b/project/storefront/public/uploads/d04dc789-4075-44e0-8437-69db363f1b70.jpg differ diff --git a/project/storefront/public/uploads/fa7a31db-bab6-4c21-8e84-de444d6f1668.jpg b/project/storefront/public/uploads/fa7a31db-bab6-4c21-8e84-de444d6f1668.jpg new file mode 100644 index 0000000..2f14a21 Binary files /dev/null and b/project/storefront/public/uploads/fa7a31db-bab6-4c21-8e84-de444d6f1668.jpg differ diff --git a/project/storefront/src/app/uploads/[filename]/route.ts b/project/storefront/src/app/uploads/[filename]/route.ts new file mode 100644 index 0000000..dd9de22 --- /dev/null +++ b/project/storefront/src/app/uploads/[filename]/route.ts @@ -0,0 +1,45 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { readFile } from 'fs/promises'; +import path from 'path'; + +const MIME: Record = { + '.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 }); + } +} diff --git a/work/artifacts/F-052/architect.md b/work/artifacts/F-052/architect.md new file mode 100644 index 0000000..597f4a7 --- /dev/null +++ b/work/artifacts/F-052/architect.md @@ -0,0 +1,32 @@ +# Architect — F-052 + +## Diagnóstico +- Las URLs de imagen de producto se almacenan como `/uploads/.` 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: `` 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/` + - `GET http://192.168.18.93:3005/uploads/` +- Tras un restart del monolito, los uploads ya existentes se sincronizan. +- `next build` no falla por assets faltantes. diff --git a/work/artifacts/F-052/documenter.md b/work/artifacts/F-052/documenter.md new file mode 100644 index 0000000..af0c955 --- /dev/null +++ b/work/artifacts/F-052/documenter.md @@ -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/` +- Storefront: `http://192.168.18.93:3005/products/` + +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. diff --git a/work/artifacts/F-052/implementer.md b/work/artifacts/F-052/implementer.md new file mode 100644 index 0000000..df98cae --- /dev/null +++ b/work/artifacts/F-052/implementer.md @@ -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/` 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. diff --git a/work/artifacts/F-052/leader-close.json b/work/artifacts/F-052/leader-close.json new file mode 100644 index 0000000..b504ec4 --- /dev/null +++ b/work/artifacts/F-052/leader-close.json @@ -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" +} diff --git a/work/artifacts/F-052/qa.json b/work/artifacts/F-052/qa.json new file mode 100644 index 0000000..84e036e --- /dev/null +++ b/work/artifacts/F-052/qa.json @@ -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." +} diff --git a/work/artifacts/F-052/reviewer.json b/work/artifacts/F-052/reviewer.json new file mode 100644 index 0000000..c655d47 --- /dev/null +++ b/work/artifacts/F-052/reviewer.json @@ -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." +} diff --git a/work/artifacts/F-052/security.json b/work/artifacts/F-052/security.json new file mode 100644 index 0000000..14f6f79 --- /dev/null +++ b/work/artifacts/F-052/security.json @@ -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." +} diff --git a/work/runtime-status.json b/work/runtime-status.json index ed61250..f391772 100644 --- a/work/runtime-status.json +++ b/work/runtime-status.json @@ -1,139 +1,13 @@ { - "feature_id": "F-051", + "feature_id": "F-052", "stage": "document", "agent": "documenter", "action": "Documentar", "state": "running", "next_agent": "leader", "waiting_for": null, - "updated_at": "2026-08-19T08:43:10Z", + "updated_at": "2026-08-19T08:47:06Z", "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", "agent": "qa", @@ -147,6 +21,132 @@ "stage": "document", "state": "running", "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",