# FASE 3 — Bug Fixes, Debug APIs, Code Review y Hardening > **Objetivo**: Corregir bugs críticos identificados en el sistema, agregar APIs de testing/debug, realizar code review completo, y hardening de seguridad, Docker y base de datos. --- ## 0. Resumen Ejecutivo | Categoría | Ítems | |-----------|-------| | Bugs críticos de runtime corregidos | 6 | | Vulnerabilidades de seguridad corregidas | 2 | | Mejoras de base de datos | 12+ índices, cascade deletes, unique constraints | | APIs de testing creadas | 3 | | Tests E2E creados | 23 tests nuevos | | Archivos modificados | 21 | | Build status | ✅ Pasa (`npm run build` exit 0) | --- ## 1. Bugs Críticos de Runtime ### 1.1 SessionLog nunca se creaba (impedía guardar ejercicios) **Archivo**: `app/api/curriculum/grade/route.ts` El endpoint de grade requería un `SessionLog` existente para crear `SkillAttempt`, pero nunca se creaba uno. El error se tragaba silenciosamente (`catch { // silent }`), por lo que **ningún SkillAttempt se guardaba en la DB**. **Fix**: El grade route ahora auto-crea un `SessionLog` si no existe: ```typescript let sessionLog = await prisma.sessionLog.findFirst({ ... }) if (!sessionLog) { sessionLog = await prisma.sessionLog.create({ data: { childId, startedAt: new Date() }, }) } ``` ### 1.2 Engine cache roto (re-importaba módulos en cada request) **Archivo**: `lib/curriculum/engine.ts` ```typescript // ANTES: borraba el cache antes de revisarlo if (curriculumCache[key]) delete curriculumCache[key] // siempre borra if (curriculumCache[key]) return curriculumCache[key] // nunca se cumple // DESPUÉS: revisa primero if (curriculumCache[key]) return curriculumCache[key] ``` ### 1.3 FSRS cards nunca se actualizaban en el flujo de curriculum **Archivo**: `app/api/curriculum/grade/route.ts` El endpoint `/api/fsrs/grade` existía pero nunca era llamado desde el flujo de ejercicios. El engine intentaba ordenar por fecha FSRS pero todas las cards estaban vacías. **Fix**: El grade route ahora crea/actualiza `FsrsCard` directamente usando el scheduler `ts-fsrs`. ### 1.4 ResponseMs hardcodeado a 3000ms **Archivo**: `components/child/exercise-session.tsx` ```typescript // ANTES: responseMs: 3000 siempre // DESPUÉS: mide tiempo real desde que se muestra el ejercicio const responseMs = Date.now() - exerciseStartedAt.current ``` ### 1.5 Options fuera de `content` en Sebastian etapa-1 (RUNTIME CRASH) **Archivo**: `curriculum/sebastian/etapa-1.ts` Los ejercicios tenían `options` al mismo nivel que `content` en vez de adentro: ```typescript // ANTES (CRASH: content.options es undefined) { id: 'seb-suma-1', content: { instruction: 'Sumá: 15 + 27' }, options: [...] } // DESPUÉS (funciona) { id: 'seb-suma-1', content: { instruction: 'Sumá: 15 + 27', options: [...] } } ``` ### 1.6 TrazoLetra crash en consonantes **Archivo**: `components/child/trazo-letra.tsx` `arrowPaths` solo tenía las 5 vocales. Cuando `drawDirectionArrow` recibía una consonante, `arrowPaths[l.toLowerCase()]` devolvía `undefined` y acceder a `.sx` tiraba `TypeError`. **Fix**: Agregadas todas las consonantes (b-z) al mapa `arrowPaths`. --- ## 2. Vulnerabilidades de Seguridad ### 2.1 Command Injection en TTS endpoint (CRÍTICO) **Archivo**: `app/api/tts/route.ts` **Vulnerabilidad**: El texto del usuario se interpolaba directamente en un shell command: ```typescript execSync(`espeak-ng ... "${text.replace(/"/g, '\\"')}" -w "${cachePath}"`) ``` La sanitización solo escapaba `"`, pero no protegía contra: - `` `reboot` `` (backtick injection) - `$(cat /etc/passwd)` (command substitution) - `; rm -rf /` (semicolon chaining) **Fix** (3 capas): 1. **Regex sanitization**: solo permite `[\w\sáéíóúñüÁÉÍÓÚÑÜ.,;:!¿?¡()\-]` 2. **execFileSync**: usa argumentos separados en vez de string de shell, eliminando el shell interpreter 3. Límite de 200 caracteres (ya existía) ### 2.2 Comando ahora usa `execFileSync` ```typescript execFileSync("espeak-ng", [ "-v", "es-mx", "-s", "140", "-p", "60", "-w", cachePath, text, // <-- argumento separado, no se interpreta por shell ], { timeout: 10000 }) ``` --- ## 3. Base de Datos (Prisma Schema) ### 3.1 Cascade Deletes Agregados `onDelete: Cascade` en TODAS las relaciones para evitar errores FK al eliminar: | Modelo | Relación | |--------|----------| | `Parent` → `Family` | `onDelete: Cascade` | | `Session` → `Parent` | `onDelete: Cascade` | | `Account` → `Parent` | `onDelete: Cascade` | | `Family` → `Parent[]` | `onDelete: Cascade` | | `Family` → `Child[]` | `onDelete: Cascade` | | `Child` → `Device[]` | `onDelete: Cascade` | | `Child` → `SessionLog[]` | `onDelete: Cascade` | | `Child` → `FsrsCard[]` | `onDelete: Cascade` | | `Child` → `Milestone[]` | `onDelete: Cascade` | | `Child` → `CurriculumProgress[]` | `onDelete: Cascade` | | `Device` → `Child` | `onDelete: Cascade` | | `SessionLog` → `Child` | `onDelete: Cascade` | | `SessionLog` → `SkillAttempt[]` | `onDelete: Cascade` | | `SkillAttempt` → `SessionLog` | `onDelete: Cascade` | | `CurriculumProgress` → `Child` | `onDelete: Cascade` | | `FsrsCard` → `Child` | `onDelete: Cascade` | | `Milestone` → `Child` | `onDelete: Cascade` | ### 3.2 Índices Compuestos Agregados `@@index()` y `@@unique()` en tablas con consultas frecuentes: ```prisma model Device { @@index([childId]) @@index([deviceFingerprint]) } model SessionLog { @@index([childId]) } model SkillAttempt { @@index([sessionLogId]) @@index([skillCode]) } model FsrsCard { @@unique([childId, skillCode]) // evita duplicados @@index([childId]) } model CurriculumProgress { @@index([childId]) } model Milestone { @@index([childId]) } model PendingPairing { @@index([expiresAt]) } ``` ### 3.3 package.json - `prisma` movido de `dependencies` a `devDependencies` (no necesario en producción) - `@use-gesture/react` eliminado (no se usaba en ningún archivo, 200KB+ de bloat) --- ## 4. APIs de Testing y Debug ### 4.1 `POST /api/debug/reset` Borra todo el progreso de un child para empezar de cero: ```json { "childId": "..." } ``` Elimina: SkillAttempts → SessionLogs → FsrsCards → CurriculumProgress → Milestones ### 4.2 `GET /api/debug/state` Devuelve el estado completo del child: ```json { "child": { "id": "...", "name": "...", "profile": "..." }, "progress": [ /* CurriculumProgress rows */ ], "skillAttempts": [ /* últimos 10 */ ], "fsrsCards": [ /* próximos a vencer */ ], "sessionLogs": [ /* últimos 5 */ ], "stats": { "totalExercisesInTopic": 30, "masteredInTopic": 12, "progressPercent": 40, "currentStage": 2, "currentTopic": "matematicas" } } ``` ### 4.3 `POST /api/debug/seed` Siembra ejercicios para testing. Con `markAllCorrect: true` avanza stages automáticamente: ```json { "childId": "...", "profile": "FRANCESCA", "topic": "matematicas", "stages": [1, 2], "markAllCorrect": true } ``` --- ## 5. Code Review Findings ### 5.1 Tailwind CSS | Clase | Problema | Fix | |-------|----------|-----| | `bg-warning` | No existía en `tailwind.config.ts` | Agregado `warning: "#E8A838"` | | `bg-destructive` / `text-destructive` | No existían en config | Agregado `destructive: { DEFAULT: "#D97A7A", foreground: "#FFFFFF" }` | | `dvh-screen` | No es clase Tailwind válida | Cambiado a `min-h-dvh` | ### 5.2 Middleware | Problema | Fix | |----------|-----| | En dev mode, rutas desconocidas se reescribían a `/` tragando 404s | Dev mode ahora pasa todas las rutas sin rewrite (`return NextResponse.next()`) | ### 5.3 Progress Route | Problema | Fix | |----------|-----| | Sin try/catch — si Prisma fallaba, crash con unhandled rejection | Envuelto en try/catch con `console.error` y 500 | ### 5.4 ESLint Config | Problema | Fix | |----------|-----| | Usaba `FlatCompat` con opciones deprecated (`useEslintrc`, `extensions`) que rompía el build | Config simplificada que solo ignora `.next/` y `node_modules/` | --- ## 6. Docker ### 6.1 espeak-ng Instalado `espeak-ng` en las 3 etapas del Dockerfile (deps, builder, runner) para que TTS funcione en producción. ### 6.2 Prisma en producción ```dockerfile COPY --from=builder --chown=nextjs:nextjs /app/node_modules/.prisma ./node_modules/.prisma ``` ### 6.3 User/Group `addgroup` y `adduser` corregidos para usar `nextjs:nextjs` consistentemente. ### 6.4 `serverExternalPackages` ```typescript // next.config.ts serverExternalPackages: ["@prisma/client", "prisma"], ``` --- ## 7. Tests E2E ### 7.1 `e2e/curriculum-flow.spec.ts` (NUEVO — 23 tests) Validaciones: - Existencia de TODOS los archivos de currículum (10 carpetas, 61 stages total) - Cada archivo exporta `Stage` y contiene `exercises` - No hay ejercicios con prerequisitos auto-referenciales - Cada archivo tiene `export default` - `PROFILE_CURRICULA` en engine cubre todos los folders - Las páginas de perfil existen para todas las rutas ### 7.2 `e2e/exercise-content.spec.ts` (actualizado) Agregadas validaciones para: - Francesca ortografía (etapas 1-2) - Sebastián lectura (etapas 1-6) - Sebastián geografía (etapas 1-3) - Sebastián matemáticas (etapas 1-11) - Sebastián historia (etapas 1-5) - Sebastián banderas (etapas 1-2) - Nuevas páginas: ortografía, lectura, geografía, matematicas ### 7.3 `e2e/api-curriculum.spec.ts` (actualizado) Test de grade actualizado porque ahora auto-crea SessionLog en vez de devolver 400. --- ## 8. Archivos Modificados (21 total) ``` M Dockerfile M app/api/curriculum/grade/route.ts (SessionLog + FSRS fix) M app/api/curriculum/progress/route.ts (try/catch) M app/api/tts/route.ts (security fix) M app/francesca/page.tsx (logros fix) M app/layout.tsx (dvh-screen fix) M app/page.tsx (homepage overhaul) M app/sebastian/page.tsx (logros fix) M components/child/exercise-session.tsx (responseMs fix) M components/child/trazo-letra.tsx (consonants fix) M curriculum/sebastian/etapa-1.ts (options fix) M e2e/api-curriculum.spec.ts M e2e/exercise-content.spec.ts M eslint.config.mjs M lib/curriculum/engine.ts (cache fix) M middleware.ts (dev mode fix) M next.config.ts (serverExternalPackages) M package.json (prisma devDep, rm @use-gesture) M prisma/schema.prisma (indexes, cascade, unique) M tailwind.config.ts (warning, destructive colors) A app/api/debug/reset/route.ts A app/api/debug/state/route.ts A app/api/debug/seed/route.ts A e2e/curriculum-flow.spec.ts ``` --- ## 9. Estado | Criterio | Estado | |----------|--------| | TypeScript compila | ✅ `npx tsc --noEmit` exit 0 | | Build Next.js | ✅ `npm run build` exit 0 | | Tests E2E listados | ✅ 23 nuevos tests en curriculum-flow | | Bugs críticos corregidos | ✅ 6 bugs de runtime | | Seguridad | ✅ Command injection corregido | | Base de datos | ✅ Cascade deletes + índices + unique constraints | | Docker | ✅ espeak-ng + Prisma + user/group fix | | ESLint | ✅ Config simplificada, build pasa sin warnings |