diff --git a/docs/adr/adr-001-autenticacion-propia-sesion-familia.md b/docs/adr/adr-001-autenticacion-propia-sesion-familia.md new file mode 100644 index 0000000..21b6bb7 --- /dev/null +++ b/docs/adr/adr-001-autenticacion-propia-sesion-familia.md @@ -0,0 +1,105 @@ +# ADR-001: Autenticación propia por sesión de familia (sin Keycloak) + +| Metadata | Valor | +|----------|-------| +| **Estado** | Aceptado (implementado) | +| **Fecha** | 2026-07-12 | +| **Autor(es)** | Jaume Garriga Maestre | +| **Supersede** | — | + +## Contexto + +recordaLexia es una app **doméstica/homelab** para niños con TDAH que corre en una +tablet en modo kiosko. Fuera del stack corporativo ECA por diseño: no hay Gravitee, +ni Kubernetes, ni infraestructura gestionada donde apoyar un IdP. + +Requisitos que condicionan la autenticación: + +- **Fricción cero para el niño**: abre la tablet y ve su día. Un niño de 7 años con + TDAH no puede depender de contraseñas ni de flujos de login. +- **Multi-tenant real**: varias familias en la misma instancia, con aislamiento + estricto de datos (un niño de la familia A no existe para la familia B). +- **Zona de configuración protegida**: el panel de padres debe quedar fuera del + alcance del niño sin romper el kiosko. +- **Operación doméstica**: un contenedor más en docker-compose es coste asumible; + operar un Keycloak (memoria, actualizaciones, backups del realm) no lo es. + +## Opciones Consideradas + +### Opción A: Keycloak OAuth2 (estándar corporativo) + +| Pros | Contras | +|------|---------| +| Estándar Asepeyo, flujos probados | Un servicio pesado más que operar en un homelab | +| SSO, federación, MFA disponibles | Fricción de login incompatible con el kiosko infantil | +| Separación IdP/aplicación | Sobredimensionado: no hay federación ni múltiples clientes | + +### Opción B: Auth propia por sesión de familia + PIN (elegida) + +| Pros | Contras | +|------|---------| +| Cero fricción para el niño (la sesión vive en el dispositivo) | Implementación propia: superficie de error en código nuestro | +| Dos niveles con un mecanismo (rol FAMILY / rol PARENT vía PIN) | Sin SSO ni MFA (irrelevantes en el contexto doméstico) | +| Nada extra que operar; BCrypt + tabla de sesiones | Rotación/expiración de sesión hay que diseñarla a mano | + +## Decisión + +**Opción B.** El adulto se registra/loguea una vez; el dispositivo conserva una +sesión de familia (`FamilySession`, handle opaco + expiración) que da el rol +`FAMILY` a toda la API del kiosko. El panel de padres (`/api/parents/**`) exige el +rol `PARENT`, que se obtiene desbloqueando con PIN (`POST /api/parents/unlock`) y +caduca solo. Contraseña y PIN con hash BCrypt. Todo scopeado a +`FamilyContext.currentFamilyId()`. + +La clase `AuthService` queda **encapsulada** precisamente para poder sustituirla por +un IdP externo sin tocar el resto del código, si el contexto cambia. + +```mermaid +sequenceDiagram + participant A as Adulto + participant K as Tablet kiosko + participant API as Backend + A->>API: register/login (una vez) + API-->>K: sesión de familia (rol FAMILY) + Note over K: el niño usa la app sin loguearse + A->>API: PIN → unlock (rol PARENT, temporal) + API-->>A: panel de padres +``` + +## Trade-offs + +### Optimizamos para: +- **Usabilidad del niño**: el requisito nº 1 del producto; cualquier fricción de + auth rompe el caso de uso. +- **Operabilidad doméstica**: menos piezas que mantener en un homelab. + +### Sacrificamos: +- **Robustez de un IdP maduro**: aceptable porque la superficie expuesta es la red + local doméstica y los datos, aunque personales, no salen de casa. +- **Estándares corporativos (Keycloak)**: desviación consciente y documentada; + el proyecto está explícitamente fuera del ECA. + +### Asunciones que podrían invalidar esta decisión: +- Si la app se expusiera a Internet o se ofreciera como SaaS multi-hogar, + reconsiderar: haría falta IdP real, MFA y hardening de sesiones. +- Si Asepeyo integrara este producto en su cartera, aplicaría el stack ECA + (Keycloak + Gravitee) y este ADR quedaría supersedido. + +## Consecuencias + +- (+) Kiosko sin fricción; multi-tenant con una sola tabla extra. +- (+) CORS/CSRF simplificados (API stateless con cabecera de sesión propia). +- (−) El equipo asume el mantenimiento del código de auth → mitigado con tests de + integración (`AuthIT`: registro, login fallido, gate por PIN, aislamiento entre + familias). + +## Criterios de Éxito + +- [x] El niño usa la app sin credenciales tras el primer login del adulto. +- [x] `/api/parents/**` inaccesible sin PIN (test verde). +- [x] Una familia no puede ver datos de otra (test verde, 404 para no filtrar existencia). + +## Referencias + +- `backend/src/main/java/es/asepeyo/recordalexia/security/` (SecurityConfig, AuthService, SessionAuthFilter, FamilyContext) +- `backend/src/test/java/es/asepeyo/recordalexia/web/AuthIT.java` diff --git a/docs/adr/adr-002-catalogo-rutinas-reutilizable.md b/docs/adr/adr-002-catalogo-rutinas-reutilizable.md new file mode 100644 index 0000000..db8aade --- /dev/null +++ b/docs/adr/adr-002-catalogo-rutinas-reutilizable.md @@ -0,0 +1,98 @@ +# ADR-002: Normalización de las rutinas de tarde (catálogo + asignación) + +| Metadata | Valor | +|----------|-------| +| **Estado** | Aceptado (implementado) | +| **Fecha** | 2026-07-12 | +| **Autor(es)** | Jaume Garriga Maestre | +| **Supersede** | — | + +## Contexto + +El modelo original de `AfternoonRoutine` mezclaba en una fila la **definición** de +la rutina (labelEs/labelCa, emoji, color, monedas) y su **asignación** (niño, día, +orden). Para tener "Merendar" de lunes a viernes había que crear la misma rutina +cinco veces, y editar el emoji exigía tocar cinco filas. El usuario lo detectó como +fricción real de uso. + +Las **mañanas ya estaban bien normalizadas**: `Activity` (catálogo por familia) + +`WeeklyTemplateEntry` (asignación niño+día+orden+override de monedas). Las tardes +eran la anomalía. + +## Opciones Consideradas + +### Opción A: Mantener el modelo y duplicar en la UI ("crear en varios días") + +| Pros | Contras | +|------|---------| +| Sin migración de datos | La duplicación persiste en BD: editar sigue costando N filas | +| Cambio solo de frontend | La asimetría mañana/tarde se consolida | + +### Opción B: Reutilizar `Activity` también para las tardes + +| Pros | Contras | +|------|---------| +| Una sola entidad de catálogo | Semántica forzada: Activity arrastra material del cole (N:M) que la rutina no necesita | +| Menos código | Acopla dos conceptos de dominio distintos | + +### Opción C: Nueva entidad `RoutineTask` gemela de `Activity` (elegida) + +| Pros | Contras | +|------|---------| +| Simetría total mañana/tarde: mismo patrón mental y de código | Migración de datos con deduplicación | +| Definir una vez, asignar a N días y N niños | Una entidad y un CRUD más | +| Monedas con cascada: override por día → catálogo → niño | | + +## Decisión + +**Opción C.** `RoutineTask` = catálogo por familia (labels, emoji, color, monedas +por defecto). `AfternoonRoutine` queda como pura asignación (`child + dayOfWeek + +routineTask + orderIndex + coinsReward` como override). La migración Liquibase +(changeset 003) consolida los duplicados existentes por clave natural +(familia + labels + emoji + color) sin pérdida; el changeset 004 añade +`ON DELETE CASCADE` para que borrar del catálogo arrastre las asignaciones +(el modelo mental del padre). + +```mermaid +erDiagram + FAMILY ||--o{ ROUTINE_TASK : "define una vez" + ROUTINE_TASK ||--o{ AFTERNOON_ROUTINE : "se asigna a N dias/niños" + CHILD ||--o{ AFTERNOON_ROUTINE : "" + FAMILY ||--o{ ACTIVITY : "(patrón gemelo de mañanas)" + ACTIVITY ||--o{ WEEKLY_TEMPLATE_ENTRY : "" + CHILD ||--o{ WEEKLY_TEMPLATE_ENTRY : "" +``` + +## Trade-offs + +### Optimizamos para: +- **Coste de edición del padre**: cambiar una rutina se hace UNA vez y se propaga. +- **Consistencia del dominio**: un solo patrón (catálogo+asignación) para todo el día. + +### Sacrificamos: +- **Simplicidad de una tabla única**: aceptable; el patrón ya existía en mañanas. +- **Riesgo puntual de migración**: mitigado con rollback explícito por changeset y + verificación contra Postgres real (Testcontainers, `MigrationIT`). + +### Asunciones que podrían invalidar esta decisión: +- Si apareciera la necesidad de rutinas con material asociado (como Activity), + reconsiderar la unificación con `Activity` (Opción B) en lugar de duplicar N:M. + +## Consecuencias + +- (+) Alta multi-día en una llamada (`POST /routines` con lista de días). +- (+) Cascada de monedas expresiva: día > catálogo > `coinsPerTask` del niño. +- (−) Lección aprendida en la migración: H2 no detecta el tipado estricto de + Postgres (`CAST(NULL AS INTEGER)`); por eso existe `MigrationIT` con Testcontainers. + +## Criterios de Éxito + +- [x] Una rutina definida una vez y asignada a varios días/niños (tests verdes). +- [x] Migración sin pérdida verificada contra Postgres 16 real. +- [x] Borrar del catálogo arrastra asignaciones sin error 500 (test de cascade). + +## Referencias + +- `backend/.../domain/RoutineTask.java`, `domain/AfternoonRoutine.java` +- `backend/src/main/resources/db/changelog/changes/003-routine-catalog.yaml`, `004-routine-task-cascade.yaml` +- Artefactos SDD en engram: `sdd/rutinas-reutilizables/{proposal,spec,design,tasks,verify-report}` diff --git a/docs/adr/adr-003-provision-inicial-dataset.md b/docs/adr/adr-003-provision-inicial-dataset.md new file mode 100644 index 0000000..9cdbab1 --- /dev/null +++ b/docs/adr/adr-003-provision-inicial-dataset.md @@ -0,0 +1,90 @@ +# ADR-003: Provisión inicial de datos al alta (familia y niño) + +| Metadata | Valor | +|----------|-------| +| **Estado** | Aceptado (implementado) | +| **Fecha** | 2026-07-12 | +| **Autor(es)** | Jaume Garriga Maestre | +| **Supersede** | — | + +## Contexto + +Una familia recién registrada empezaba con la app vacía: había que crear a mano +todos los materiales, actividades y rutinas. El objetivo es que cualquier familia +arranque con un **dataset inicial útil**: material escolar típico y rutinas de +tarde apropiadas a la edad de cada niño (tramos 8-9 / 10-11 / 12-14, tomados de la +literatura de desarrollo de autonomía en TDAH), sin impedir personalizarlo después. + +Restricción estructural clave: los datos cuelgan de `family_id` y `child_id`, que +son **datos de runtime** (se crean al registrarse), no de esquema. + +## Opciones Consideradas + +### Opción A: INSERTs en un changeset Liquibase + +| Pros | Contras | +|------|---------| +| Versionado junto al esquema | **Inviable**: Liquibase corre ANTES que cualquier ApplicationRunner; en la migración no existe aún ninguna familia a la que apuntar (FK NOT NULL) | + +### Opción B: Ampliar el DataSeeder + +| Pros | Contras | +|------|---------| +| Sencillo | Solo actúa con BD vacía (familia demo); las familias reales que se registren después no reciben nada | + +### Opción C: Servicio de provisión invocado en el alta (elegida) + +| Pros | Contras | +|------|---------| +| Se ejecuta exactamente cuando existen los datos padre (familia/niño) | Lógica de negocio adicional que testear | +| La edad del niño está disponible en su alta → tramo correcto | El catálogo posterior al alta no se "resincroniza" (decisión consciente) | +| Transaccional con el registro: o todo o nada | | + +## Decisión + +**Opción C.** `InitialDatasetService` con dos operaciones: + +- `provisionFamily(family)` — en `AuthService.register()`: catálogo de materiales + escolares típicos (9) + catálogo completo de rutinas (14). +- `provisionChildRoutines(child)` — en `ChildService.create()`: asigna a L-V las + rutinas del catálogo cuyo `minAge` ≤ edad del niño (acumulativo: base 0 / + organización 10 / corresponsabilidad 12). + +El `DataSeeder` demo **delega en el mismo servicio** (una sola fuente de verdad) y +solo añade sus extras (actividades del horario, premios, eventos). + +## Trade-offs + +### Optimizamos para: +- **Time-to-value de la familia**: la app es útil desde el minuto uno. +- **Adecuación evolutiva**: las responsabilidades crecen con la edad del niño. + +### Sacrificamos: +- **Sincronización posterior**: si la familia borra rutinas del catálogo, los niños + creados después reciben solo las que sigan existiendo (se busca por etiqueta ES). + Aceptable: respeta la personalización de la familia. +- **Contenido opinado**: el set inicial es una propuesta pedagógica, no neutra; + mitigado porque todo es editable desde el panel. + +### Asunciones que podrían invalidar esta decisión: +- Si el dataset inicial necesitara variar por idioma/región o ser configurable por + el admin, reconsiderar moverlo a datos (tabla de plantillas) en vez de código. + +## Consecuencias + +- (+) El registro deja a la familia lista para usar; el alta de un niño de 13 años + le pone 14 rutinas x 5 días sin tocar nada. +- (−) El seed vive en código Java (constantes): cambiarlo requiere release. Asumido + para v1 doméstica. + +## Criterios de Éxito + +- [x] Registro → 9 materiales + 14 rutinas de catálogo (test verde). +- [x] Niño de 8 → 25 asignaciones; niño de 13 → 70 (test verde). +- [x] Demo y familias reales comparten la misma provisión (DataSeederIT verde). + +## Referencias + +- `backend/.../service/InitialDatasetService.java` (+ test) +- `backend/.../security/AuthService.java`, `service/ChildService.java`, `bootstrap/DataSeeder.java` +- ADR-002 (el catálogo reutilizable es prerrequisito de esta provisión)