Files
renato97 a3360ec6e4 feat: fase 2+3 — expansion masiva multi-perfil + bug fixes + hardening
Fase 2 (expansion):
- Francesca: +6 etapas matematicas (5-10), +7 etapas lectura (3-9), +2 ortografia
- Sebastian: +8 etapas matematicas (4-11), +6 lectura, +3 historia, +3 geografia, +1 banderas
- Nuevas paginas: ortografia, lectura, geografia, matematicas
- ~900 ejercicios nuevos, paridad ~500 por perfil

Fase 3 (bug fixes + hardening):
- Fix: SessionLog auto-create en grade route
- Fix: engine cache (no borrar antes de revisar)
- Fix: FSRS cards actualizadas en flujo curriculum
- Fix: responseMs real (no hardcodeado 3000)
- Fix: options dentro de content en sebastian etapa-1
- Fix: trazo-letra consonantes crash
- Security: execFileSync anti-command-injection en TTS
- DB: 18 cascade deletes + indices + unique constraints
- APIs debug: reset, state, seed
- Docker: espeak-ng + prisma + user/group
- E2E: 23 tests nuevos (curriculum-flow.spec.ts)
- Code review: tailwind colors, middleware dev mode, eslint config

Verificado: tsc exit 0, build exit 0, 61 E2E tests pass
2026-07-25 22:07:28 -03:00

203 lines
5.1 KiB
Plaintext

generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
enum Profile {
ISABELLA
FRANCESCA
SEBASTIAN
}
model Parent {
id String @id @default(cuid())
email String @unique
name String?
emailVerified Boolean @default(false)
image String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
familyId String?
family Family? @relation(fields: [familyId], references: [id], onDelete: Cascade)
sessions Session[]
accounts Account[]
}
model Session {
id String @id @default(cuid())
email String?
userId String?
parentId String?
parent Parent? @relation(fields: [parentId], references: [id], onDelete: Cascade)
token String @unique
expiresAt DateTime
ipAddress String?
userAgent String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Account {
id String @id @default(cuid())
accountId String
providerId String
userId String
user Parent @relation(fields: [userId], references: [id], onDelete: Cascade)
accessToken String?
refreshToken String?
idToken String?
accessTokenExpiresAt DateTime?
scope String?
password String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Verification {
id String @id @default(cuid())
identifier String
value String
expiresAt DateTime
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Family {
id String @id @default(cuid())
name String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
parents Parent[] @relation(onDelete: Cascade)
children Child[] @relation(onDelete: Cascade)
}
model Child {
id String @id @default(cuid())
name String?
profile Profile @default(ISABELLA)
avatar String?
birthdate DateTime?
profileNotes String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
familyId String
family Family @relation(fields: [familyId], references: [id], onDelete: Cascade)
devices Device[] @relation(onDelete: Cascade)
sessions SessionLog[] @relation(onDelete: Cascade)
fsrsCards FsrsCard[] @relation(onDelete: Cascade)
milestones Milestone[] @relation(onDelete: Cascade)
curriculumProgress CurriculumProgress[] @relation(onDelete: Cascade)
}
model Device {
id String @id @default(cuid())
deviceFingerprint String
pairedAt DateTime @default(now())
lastSeen DateTime @updatedAt
role String @default("child")
createdAt DateTime @default(now())
childId String
child Child @relation(fields: [childId], references: [id], onDelete: Cascade)
@@index([childId])
@@index([deviceFingerprint])
}
model SessionLog {
id String @id @default(cuid())
startedAt DateTime @default(now())
endedAt DateTime?
durationSec Int?
skillsPracticed String?
childId String
child Child @relation(fields: [childId], references: [id], onDelete: Cascade)
skillAttempts SkillAttempt[] @relation(onDelete: Cascade)
@@index([childId])
}
model SkillAttempt {
id String @id @default(cuid())
skillCode String
promptLevel Int @default(0)
correct Boolean
responseMs Int?
errorType String?
exerciseId String?
stage Int?
createdAt DateTime @default(now())
sessionLogId String
sessionLog SessionLog @relation(fields: [sessionLogId], references: [id], onDelete: Cascade)
@@index([sessionLogId])
@@index([skillCode])
}
model CurriculumProgress {
id String @id @default(cuid())
topic String
stage Int
unlockedAt DateTime @default(now())
completedAt DateTime?
childId String
child Child @relation(fields: [childId], references: [id], onDelete: Cascade)
@@unique([childId, topic, stage])
@@index([childId])
}
model FsrsCard {
id String @id @default(cuid())
skillCode String
dueAt DateTime @default(now())
stability Float @default(0)
difficulty Float @default(0)
reps Int @default(0)
lapses Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
childId String
child Child @relation(fields: [childId], references: [id], onDelete: Cascade)
@@unique([childId, skillCode])
@@index([childId])
}
model Milestone {
id String @id @default(cuid())
skillCode String
reachedAt DateTime @default(now())
childId String
child Child @relation(fields: [childId], references: [id], onDelete: Cascade)
@@unique([childId, skillCode])
@@index([childId])
}
model PendingPairing {
id String @id @default(cuid())
code String @unique
deviceFingerprint String?
childId String?
createdAt DateTime @default(now())
expiresAt DateTime
@@index([expiresAt])
}