# Informe QA — Módulo Reacciones Químicas (4.1) (formato SAQI)

**Fecha:** 2026-09-10 · **Ciclo:** Actualización · **Estado final:** ✅ 19/19 funnel (producción) · 32/32 unit+int (local)

## 1. MVP entregado
- Tarjeta **"Reacciones Químicas (4.1)"** insertada en `templates/index.html`, con enlace a `/reacciones-quimicas`.
  Verificado en producción: `https://adoniz-acer.tail60dd6a.ts.net/quimica/` muestra la tarjeta.
- Blueprint Flask `reacciones_cdt.py` (registrado en `app.py`) con token, incidentes y guardado de **una sola fila por estudiante con nota unificada**. Dominio puro `reacciones_grader.py` + config central `reacciones_config.py`.
- Página autocontenida `templates/reacciones-quimicas.html`: teoría arriba, evaluación abajo, actividad a libro abierto, **sin pantalla completa obligatoria**.
- Anti-trampa: `−5 pts` por pestaña/ventana/copiar-pegar/atajos/clic-derecho/DevTools, registrado en `reacciones_incidencias.csv`.
- **Notificación visible por incidencia** (paridad RA3 CDT1): contador vivo en el
  encabezado (`#n-incidentes` / `#descuento`) y banner `⚠️ Incidente registrado: <tipo>
  (-5 pts). Total: -N pts` que se muestra por 4 s tras cada reporte exitoso. La llamada
  es `async`/`await`; si el servidor no responde no se notifica y el descuento se
  recalcula al enviar.
- **Nota unificada**: El módulo ahora calcula una nota final única (sobre 100) sumando los puntos obtenidos en todos los temas y dividiendo entre los puntos totales posibles, aplicando el descuento por incidentes.

## 2. Batería ejecutada

### L4 Functional (menú y navegación)
- `/quimica/` responde 200 y contiene enlace a `reacciones-quimicas` → PASS.
- Enlaces de módulos del menú devuelven 200 vía funnel → PASS.

### L3 Functional (flujo)
- `GET /reacciones-quimicas` → 200, HTML responsive, sin `requestFullscreen` → PASS.
- `POST /iniciar-reacciones` entrega token de 32 hex → PASS.
- Carrera no permitida → 400; identificación incompleta → 400 → PASS.
- `POST /incidente-reacciones` válido acepta y descuenta 5 → PASS.
- `POST /guardar-reacciones`: envío sin respuestas → nota 0 sin caer; envío perfecto → nota 100; reenvío con mismo token → HTTP 409; una sola fila por estudiante con nota unificada (No correlativo por estudiante: 1, 2, …) → PASS.

### ATK-SEC Adversarial
- JSON malformado / Content-Type incorrecto / payload de 1 MB → 400 (sin 500) → PASS.
- Inyección CSV (`=…`), XSS (`<script>`), salto de línea en apellidos → sin 500, celda escapada con `'` → PASS.
- Token desconocido → 400; tipo de incidente fuera de allow-list → 400 → PASS.
- La página no expone la clave de respuestas; no fuerza pantalla completa → PASS.

## 3. Problemas vistos en uso y soluciones aplicadas

| ID | Severidad | Descripción | Solución |
|---|---|---|---|
| QA-R1 | Alta | "A veces con muchos usuarios no deja entrar a algunos, pero al finalizar algunos, otros podían entrar". | `threaded=True` en `app.run` (Flask dev server servía peticiones de forma secuencial → cola; ahora hilos paralelos). Reiniciado. |
| QA-R2 | Media | Responsividad pobre en celulares (tabla ancha, padding grande, texto que no se ajusta). | CSS responsive con `@media (max-width:600px)`, tabla de 4.1.4 envuelta en `.tabla-scroll`, botones full-width, `-webkit-text-size-adjust:100%`, `word-break`. |
| QA-R3 | Baja (cliente) | El docente no entraba desde su celular aunque la URL era correcta. | Diagnóstico SAQI: MagicDNS de Tailscale resuelve `*.ts.net` a la IP interna de la tailnet; el funnel público funciona para dispositivos sin Tailscale activo. Se recomienda al docente probar con Tailscale apagado en el propio móvil. |

## 4. Resultados de la corrida (producción)
- CSV de producción reseteado a solo encabezado tras correr el funnel (obligatorio para que el primer estudiante real arranque limpio):
  - `reacciones_resultados.csv` → `No,Apellidos,Nombres,Carrera,Puntos,Posibles,Nota`
  - `reacciones_incidencias.csv` → `fecha,token,estudiante,tipo`
- Servicio reiniciado y funcionando, puerto 8081, `threaded=True`, 200 en LAN y público.