# Registro de decisiones

Por qué la aplicación es como es. Una entrada por decisión, de la más reciente
a la más antigua. Solo entran decisiones con fuente; si el motivo no consta, se
dice. Antes de «arreglar» algo que parezca raro, búscalo aquí.

Las fuentes son commits (`git show <hash>` tiene el detalle). Lo que está fuera
del repo se indica como tal.

---

### 2026-10-24 · Sin registro público, tampoco en la API

- **Decisión.** Las cuentas solo las crea el admin (`POST /api/users`). En la
  web, `/register` redirige al login desde el 1-ago. En la API,
  `POST /api/auth/register` no está montado desde el 24-oct. El controlador se
  conserva para poder reactivarlo, pero ya no acepta un rol: siempre crea
  alumnos.
- **Motivo.** Al quitar el registro solo de la web, la API siguió abierta, y
  aceptaba el rol que mandara quien llamaba, `ADMIN` incluido.
- **Fuente.** `20ebf15`, `076e8b9`.

### 2026-10-24 · La cuota del alumno no caduca por meses

- **Decisión.** La cuota vigente son todas las cuotas (base y extra) no
  caducadas del último periodo en el que el alumno tiene alguna. El consumo se
  cuenta desde el inicio de ese periodo, sin fecha de fin. Al editar la cuota se
  escribe en ese mismo periodo, en una transacción.
- **Motivo.** `QuotaGrant` iba atado al mes natural y nada lo renovaba: el 1-nov
  todos los alumnos con cuota se habrían quedado a 0. EWC usa el extra (el único
  campo con caducidad) como su «paquete de horas hasta tal fecha» y deja la base
  a 0, así que una regla basada en «el último BASE» también los habría dejado a
  0. Esto último lo detectó la revisión de Codex del 19-oct.
- **Aplazado.** Un sistema nuevo de cuotas (paquetes, pausa, extras con fecha
  propia), pendiente de EWC: ver [status.md](status.md).
- **Consecuencias.** Limitaciones aceptadas en
  [features/quotas.md](features/quotas.md#limitaciones-conocidas).
- **Fuente.** `2b9e5ce`.

### 2026-10-24 · El corte por cuota lo hace el servidor con un reloj propio

- **Decisión.** Durante la llamada, el WebSocket lee el saldo una vez al
  empezar y lo descuenta con un reloj local, independiente del tiempo del bot.
  Corta el primero de los dos que se agote.
- **Motivo.** El consumo solo se anota al terminar la llamada, así que consultar
  la BD cada segundo devolvía siempre el saldo inicial y la llamada no se
  cortaba. Además, el control solo arrancaba si el bot tenía temporizador
  propio.
- **Consecuencias.** Una recarga o una caducidad durante la llamada valen desde
  la siguiente, y dos llamadas a la vez del mismo alumno pueden pasarse del
  saldo.
- **Fuente.** `fbf3b1a`.

### 2026-09-24 · «Auxiliares» en la interfaz

- **Decisión.** La interfaz llama «auxiliares de conversación» (en inglés,
  *assistants*) a los bots. El código sigue diciendo `Bot`.
- **Motivo.** No consta.
- **Fuente.** `7a7107a`.

### 2026-09-21 · Solo el admin gestiona las cuotas

- **Decisión.** Ver y cambiar la cuota de un alumno es solo de ADMIN
  (`authorize('ADMIN')` en `routes/usage.ts`).
- **Motivo.** Según el propio código, los profesores no reparten tiempo de
  conversación («teachers do not hand out talk time»).
- **Fuente.** `aea496b`.

### 2026-09-21 · Admin y profesor no consumen cuota

- **Decisión.** La cuota solo se aplica a STUDENT. A ADMIN y TEACHER no se les
  bloquea ni se les anota consumo.
- **Motivo.** Tienen que poder probar los auxiliares, y la interfaz solo permite
  asignar cuota a alumnos: cualquier consumo suyo sería una deuda imposible de
  saldar.
- **Fuente.** `4ef3aed`; comentarios en `quotaService.ts` y
  `conversationService.ts`.

### 2026-09-21 · El WebSocket identifica al usuario por su cookie

- **Decisión.** La identidad y el rol de quien abre `/ws` salen de la cookie
  `accessToken`, nunca del mensaje del cliente.
- **Motivo.** Si no, cualquiera podía mandar el id de otro usuario y heredar su
  rol, sus auxiliares y su exención de cuota.
- **Fuente.** `7bccf54`.

### 2026-08-16 · `AppSelect` en lugar del `<select>` nativo

- **Decisión.** Todos los desplegables usan `components/AppSelect.vue`.
- **Motivo.** El desplegable abierto del `<select>` nativo lo pinta el sistema
  operativo. En Chrome y Safari de Mac sale el hover azul del sistema y no se
  puede cambiar con CSS.
- **Consecuencias.** Tiene trampas de uso, descritas en
  [AGENTS.md](../AGENTS.md#desplegables-appselect-nunca-select-nativo).
- **Fuente.** `bc278ad`, `f0f128f`, `d101950`, `0ca668c`.

### 2026-08-15 · La traducción de la transcripción va dentro de cada turno

- **Decisión.** Cada entrada de `aiAnalysis.transcript` lleva su `translation`.
  También se traduce lo que dice el auxiliar. Si la conversación ya tiene
  traducciones, se reutilizan.
- **Motivo.** No necesita migración, queda alineada turno a turno y ya la sirve
  `GET /api/conversations/:id`. Reutilizar evita pagar dos veces la misma
  traducción.
- **Fuente.** `60159a6`, `8361396`.

### 2026-08-15 · Idioma de transcripción por bot y subtítulos en vivo

- **Decisión.** Cada bot tiene `transcriptionLanguage` (ES, EN o NONE; ES por
  defecto), que fija a qué idioma se traducen sus subtítulos y su
  transcripción. Con NONE no se traduce nada.
- **Evolución.** Al principio los subtítulos no se podían ocultar (decisión de
  Carla). El 27-ago se añadió un botón para mostrarlos u ocultarlos, con los
  subtítulos visibles por defecto (`2608c73`).
- **Fuente.** `931bbae`, `eda1cad`, `1aa4765`, `2608c73`.

### 2026-08-02 · El feedback se guarda, no se pide cada vez

- **Decisión.** El modal de feedback y la tabla de feedbacks del admin usan
  `POST /api/elevenlabs/conversation/:conversationId/ensure-feedback`. Ese
  endpoint mira primero la BD y solo pide los datos a ElevenLabs si faltan;
  entonces los traduce y los guarda.
- **Motivo.** Antes, cada vez que se abría el feedback se repetían las llamadas
  a ElevenLabs (y, con F4, también a OpenAI) sin guardar nada.
- **Fuente.** `0667817`.

### 2026-08-02 · Solo se traduce el resumen del feedback

- **Decisión.** F4 traduce al español el resumen de la transcripción y su
  respaldo en BD: sobrescribe `transcript_summary` y guarda el original en
  `transcript_summary_original`. El análisis y los criterios de evaluación no
  se traducen automáticamente.
- **Motivo.** Decisión explícita de Carla. Más tarde se añadió un botón para
  traducir la evaluación cuando se pida (`c047c34`).
- **Fuente.** `0667817`.

### 2026-08-02 · El auxiliar se despide antes del corte

- **Decisión.** El servidor manda `time_warning` cuando faltan 40 s (por el
  tiempo del bot o por la cuota). El navegador se lo pasa al agente con
  `sendContextualUpdate`, y el corte real llega 10 s después de llegar a 0. Se
  quitaron los cortes locales del navegador.
- **Motivo.** La duración máxima de ElevenLabs corta en seco y el agente no
  sabe cuánto tiempo lleva hablando, así que las instrucciones en el prompt no
  bastan.
- **Consecuencias.**
  - Cada agente necesita un párrafo en su system prompt, y su duración máxima
    en ElevenLabs debe ser el tiempo del bot más 90 s (ver
    [features/conversations.md](features/conversations.md#configuración-de-cada-agente-en-elevenlabs)).
  - Mientras se ve una conversación antigua no se puede empezar una llamada,
    así que el alumno no puede reanudar conversaciones desde la pantalla.
- **Fuente.** `4e359a8`; propuesta aceptada en julio de 2026 (fuera del repo).

### 2026-08-01 · Permisos de profesor por flags

- **Decisión.** Cada usuario tiene `canCreateBots` y `canEditBots` (este último
  cubre editar, desactivar y borrar), y la API los comprueba con
  `requireBotPermission`. Crear alumnos y asignar auxiliares es solo del admin.
  Se elimina `canCreateStudents`. Los profesores que ya existían empezaron con
  `canEditBots = false`.
- **Motivo.** Se acordó con EWC en la reunión del 8-jul-2026 que no todos los
  profesores tengan los mismos permisos. Hasta entonces los flags solo
  ocultaban botones: ningún endpoint los comprobaba.
- **Fuente.** `20ebf15`; propuesta aceptada en julio de 2026 (fuera del repo).

### 2026-08-01 · La nota de asignación no se edita después

- **Decisión.** La nota opcional de una asignación, visible para el alumno,
  solo se escribe al asignar. No se puede editar después.
- **Motivo.** Decisión de Carla al implementar R3.
- **Fuente.** `20ebf15`.

### 2026-08-01 · Nombres de los criterios de evaluación, leídos del agente

- **Decisión.** Los nombres de los criterios se leen en vivo de la
  configuración del agente en ElevenLabs (`getEvaluationCriteriaNames`, con
  caché de 10 min), en lugar de un diccionario manual.
- **Motivo.** ElevenLabs genera el `id` de cada criterio a partir de su nombre y
  le quita las tildes («comprensin»). El nombre correcto sigue estando en la
  configuración del agente.
- **Fuente.** `20ebf15`.

### 2026-08-01 · Idioma del mensaje de fin, leído del agente

- **Decisión.** El idioma del agente se lee de ElevenLabs en cada sesión
  (`getAgentLanguage`, sin migración) y viaja en `elevenlabs_ready`. El mensaje
  de fin sale de `END_SESSION_MESSAGES`: es, en, fr y de, con el español si el
  idioma no está.
- **Fuente.** `20ebf15`.

### 2026-08-01 · Toda la app en español e inglés, con selector

- **Decisión.** vue-i18n con selector ES/EN en toda la app (alumno, profesor y
  admin), con el español por defecto.
- **Motivo.** El plan inicial era solo el entorno del alumno en español, sin
  selector y con un módulo de constantes. Se cambió sobre la marcha con Carla
  durante las pruebas.
- **Fuente.** `20ebf15`.

### 2026-07-09 · Solo scripts `.sh`

- **Decisión.** Se eliminaron los scripts `.ps1` de Windows, porque el entorno
  de trabajo es macOS.
- **Fuente.** `940f5d2`.

### 2026-02 · Apache en el servidor y nginx dentro del contenedor

- **Decisión.** En producción, Apache (en el servidor) hace de proxy inverso y
  gestiona el TLS; el frontend lo sirve un nginx dentro de `frontend-prod`.
- **Motivo.** No consta. El cambio de nginx a Apache en los scripts de
  despliegue es de febrero de 2026.
- **Fuente.** `5345cdd`; `scripts/apache-ewc-config.inc.sh`.
