Resumen técnico
unDEAenunaCaja es un simulador de Desfibrilador Externo Automático (DEA) y Semiautomático (DESA) para capacitación en reanimación. Una persona instructora conduce la simulación con botones; la aplicación reproduce las indicaciones de voz del equipo, muestra el texto en pantalla y marca los 2 minutos de RCP entre análisis de ritmo.
| Característica | Detalle |
|---|---|
| Tipo | Aplicación web estática, instalable como PWA y utilizable sin conexión |
| Stack | HTML5, CSS3 y JavaScript (ES6+) sin frameworks, sin dependencias y sin paso de compilación |
| APIs web | HTMLAudioElement, requestAnimationFrame, Service Worker, Cache API, Web App Manifest |
| Modelo de ejecución | Máquina de estados dirigida por datos: 17 estados definidos en el objeto historia, interpretados por un único motor |
| Salida | Audio MP3 pregrabado (13 archivos, unos 1,3 MB; metronomo.mp3 es el 35 %) y texto en pantalla |
| Persistencia | Ninguna de la simulación (el estado vive en memoria). El service worker guarda los recursos en caché |
| Compatibilidad | Chrome, Edge, Firefox y Opera en Windows, Linux y Android; Safari en iOS (limitado) |
| Licencia | GPL v3 |
| Repositorio | github.com/Ggaita/undeaenunacaja |
- Tocá Comenzar y elegí el modo: Automatico o Semiautomatico.
- El equipo brinda indicaciones por voz. Quien instruye responde con los botones según lo que hacen los participantes:
verde cuando lo hacen bien,
rojo cuando no, iniciando un bucle. - Al tener que iniciar maniobras de RCP aparece la cuenta regresiva de 2 minutos. Al terminar, el equipo vuelve a pedir el análisis de ritmo.
- Reiniciar vuelve a la pantalla de inicio en cualquier momento, sin recargar la página.
Flujos de simulación
Cada pantalla tiene una indicación de voz, un texto y hasta dos botones.
El identificador de cada pantalla es la clave que se usa en index.js.
Pantallas comunes
inicio · audio inicio.mp3 (4,8 s)inicioOpcionesinicioOpciones · audio opcionesdesimulacion.mp3 (1,8 s)automatico Semiautomatico →
semiautomaticoModo automático
El equipo analiza el ritmo y, si corresponde, entrega la descarga por sí solo. Quien instruye solo indica el ritmo y confirma que los participantes siguen las indicaciones.
automatico · modoAutomatico.mp3etapaParchesautomaticoetapaParchesautomatico · comienzo.mp3 (5,1 s)etapaParches2Automatico Sí →
analisisRitmoAutomaticoetapaParches2Automatico · colocarParches2.mp3 (4,6 s)analisisRitmoAutomaticoanalisisRitmoAutomatico · analizandoRitmo.mp3 (3,7 s)ritmoNoDesfibrilableAutomatico Desfibrilable → ritmoDesfibrilableAutomaicoritmoDesfibrilableAutomaico · ritmoDesfibrilableAutomatico.mp3 (7,8 s)
metronomoAutomaticoritmoNoDesfibrilableAutomatico · ritmoNoDesfibrilable.mp3 (7,9 s)
metronomoAutomaticometronomoAutomatico · metronomo.mp3 (2:00)analisisRitmoAutomatico.Modo semiautomático
Igual que el automático, pero con un paso más: cuando el ritmo es desfibrilable, el equipo pide indicar la descarga, y recién después se pasa a la RCP.
semiautomatico · modoSemiautomatico.mp3etapaParchesetapaParches y etapaParches2 · comienzo.mp3 / colocarParches2.mp3etapaParches2 (se repite) Sí → analisisRitmoanalisisRitmo · analizandoRitmo.mp3ritmoNoDesfibrilable Desfibrilable → ritmoDesfibrilableritmoDesfibrilable · descargaNecesaria.mp3 (5,0 s)continuaRcpcontinuaRcp · descargaRealizada.mp3 (7,5 s)metronomoritmoNoDesfibrilable · ritmoNoDesfibrilable.mp3metronomometronomo · metronomo.mp3 (2:00)analisisRitmo.Metrónomo y temporizador
En las pantallas metronomo y metronomoAutomatico se reproduce metronomo.mp3, que dura 2 minutos y
marca el ritmo de las compresiones. Mientras suena aparece la barra de progreso y el tiempo restante (MM:SS).
La cuenta regresiva sale del propio audio: restante = duración − tiempo actual.
Si el navegador todavía no conoce la duración, usa 120 segundos. El color de la barra cambia según el tiempo que queda:
| Tiempo restante | Color |
|---|---|
| Más del 50 % | Verde #00c853 |
| Entre 50 % y 20 % | Amarillo #ffd600 |
| Menos del 20 % | Rojo #d50000 |
Cuando el audio termina (evento audio.onended) se oculta el temporizador y la aplicación pasa sola a la pantalla de análisis de ritmo del modo en curso.
Arquitectura
El diseño separa los datos (qué dice y a dónde va cada pantalla) del motor (cómo se reproduce y se dibuja). Agregar o cambiar pantallas es editar un objeto, sin tocar la lógica; la excepción son los dos metrónomos, que están escritos en el motor (ver Extensión).
| Capa | Dónde | Responsabilidad |
|---|---|---|
| Presentación | index.html, documentacion.html, styles-documentacion.css, styles.css, reset.css |
DOM fijo; los botones de respuesta se crean dinámicamente en cada estado |
| Datos | Objeto historia (index.js, líneas 13 a 173) |
Grafo dirigido de 17 estados: audio, texto y opciones con su destino |
| Motor | cambiarEstado, reproducir, temporizador |
Interpreta un estado: reproduce el audio, escribe el texto, crea los botones y controla el temporizador |
| PWA | service-worker.js, manifest.json, registro en index.js |
Instalación, caché sin conexión y aviso de actualización |
| Recursos | src/, icons/ |
Audios, logos, bandera e íconos |
Ciclo de vida de la página
- Carga: se muestra el botón Iniciar. Todavía no suena nada porque los navegadores exigen un gesto del usuario antes de reproducir audio.
- Se registra el service worker (
service-worker.js) y se instala la caché. - Iniciar ejecuta
empezar(): oculta ese botón, muestra#opcionesy#btnReiniciar, y entra en el estadoinicio. - Cada botón de respuesta llama a
cambiarEstado(siguiente). Las únicas transiciones sin botón son las de fin de metrónomo, que dispara el eventoendeddel audio.
Qué hace cambiarEstado(clave)
- Guarda
estadoActual = clavey obtienehistoria[clave]. Si la clave no existe, el nodo esundefinedy la función lanza un error: la aplicación deja de responder. - Reproduce
nodo.audiodesde el segundo 0. - Vacía
#opcionesy escribenodo.textocontextContent(cadena vacía si el nodo no tiene texto). - Si la clave es
metronomoometronomoAutomatico: muestra el temporizador y defineaudio.onendedpara volver al análisis de ritmo de su modo. En cualquier otro estado oculta el temporizador y poneaudio.onended = null. - Crea un
<button>por cada opción, con su clase CSS, y le asignaonclick = () => cambiarEstado(op.siguiente).
Referencia del código (index.js)
Los números de línea corresponden a la revisión 3e1b788. El script se carga como archivo clásico (sin módulos)
para que los atributos onclick del HTML encuentren las funciones en el ámbito global.
Funciones
| Función | Líneas | Descripción |
|---|---|---|
reproducir(src) |
182–186 | Asigna audio.src, pone currentTime = 0 y llama a audio.play(). La promesa que devuelve play() no se captura. |
iniciarTemporizador() |
188–228 | Muestra la barra y el tiempo, cancela el cuadro de animación anterior y ejecuta actualizar(), que se reprograma con requestAnimationFrame mientras el audio suene y quede tiempo. |
ocultarTemporizador() |
230–233 | Pone display: none en la barra y en el tiempo restante. |
cambiarEstado(estado) |
238–295 | Motor de la máquina de estados (ver Arquitectura). |
empezar() |
298–304 | Primera entrada: oculta #btnInicio, muestra #opciones y #btnReiniciar, entra en inicio. |
reiniciar() |
308–310 | Vuelve al estado inicio. No recarga la página ni vuelve a mostrar el botón Iniciar. |
| Registro del service worker | 313–339 | Registra service-worker.js, escucha updatefound y muestra #updateBanner si hay una versión nueva instalada y ya había un worker activo. En controllerchange recarga la página. |
actualizarApp() |
342–346 | Si hay un worker nuevo en espera, le envía el mensaje "SKIP_WAITING" code>. |
Variables globales
undeaenunacaja/
├── index.html Página única (43 líneas)
├── documentacion.html Documentación (100 líneas)
├── index.js Datos (historia), motor, temporizador, registro del service worker
├── styles-documentacion.css Estilos de la documentación
├── styles.css Estilos del simulador
├── reset.css Reset y ancho máximo de 1024 px
├── service-worker.js Precaché, estrategia cache-first, actualización
├── manifest.json Datos de instalación de la PWA
├── icons/ icono-192.png, icono-512.png
├── src/ 13 audios .mp3, logos y bandera (src/images/)
├── LICENSE GPL v3
└── README.md
- No hay carpeta de compilación ni
package.json: el repositorio es lo que se publica. - Para publicar basta un servidor de archivos estáticos con HTTPS (por ejemplo GitHub Pages). Los service workers solo funcionan en HTTPS.
Modelo de datos y extensión
Cada estado es una entrada del objeto historia. La clave es el identificador del estado.
nombreDelEstado: {
audio: "./src/archivo.mp3",
texto: "Texto que se muestra",
opciones: [
{ texto: "Sí", siguiente: "otroEstado", clase: "btn-verde" },
{ texto: "No", siguiente: "nombreDelEstado", clase: "btn-rojo" }
]
}| Campo | Tipo | Obligatorio | Regla |
|---|---|---|---|
audio |
string | sí | Ruta relativa a un .mp3. Si el archivo no existe, el estado igual avanza pero no suena. |
texto |
string | no | Se muestra tal cual (sin HTML). Si falta, se limpia el texto anterior. |
opciones |
array | sí | Puede ser vacío. Solo los estados de metrónomo terminan por el fin del audio; un estado vacío cualquiera dejaría la simulación sin salida, salvo Reiniciar. |
opciones[].texto |
string | sí | Rótulo del botón |
opciones[].siguiente |
string | sí | Clave de un estado existente. Si apunta al mismo estado, la indicación se repite. |
opciones[].clase |
string | no | btn-opciones, btn-verde o btn-rojo. Sin clase, el botón es azul. |
Agregar o modificar una pantalla
- Grabar el audio en MP3 y guardarlo en
src/. - Agregar el estado en
historiay apuntarle un botón desde otro estado. - Agregar la ruta del audio a
urlsToCacheenservice-worker.js. - Subir
CACHE_NAME(por ejemplo adea-simulador-v2) para que los dispositivos descarguen la versión nueva. - Validar el grafo con el script de la sección Verificación.
Agregar un modo nuevo
Hoy exige tres cambios: los estados del modo, un botón eninicioOpciones y un caso en cambiarEstado para
el metrónomo. Para no tocar el motor, se podría agregar un campo alTerminar a cada estado de metrónomo, con la clave del estado que sigue.
metronomo: {
audio: "./src/metronomo.mp3",
texto: "Espere los 2 minutos para analizar el ritmo nuevamente",
opciones: [],
temporizador: true,
alTerminar: "analisisRitmo"
}
// En cambiarEstado: reemplaza las dos ramas por nombre
if (nodo.temporizador) {
iniciarTemporizador();
audio.onended = () => {
ocultarTemporizador();
cambiarEstado(nodo.alTerminar);
};
} else {
ocultarTemporizador();
audio.onended = null;
}
PWA y funcionamiento sin conexión
manifest.json
| Campo | Valor | Observación |
|---|---|---|
name / short_name |
unDEAenunaCAJA | |
start_url |
. |
Relativa a la ubicación del manifest |
display |
standalone |
Se abre sin barra del navegador |
background_color / theme_color |
#000000 |
El meta theme-color de index.html es #d32f2f |
icons |
icono-192.png (192×192) e icono-512.png (512×512) | Sin purpose: no declara variante maskable |
service-worker.js
| Evento | Comportamiento |
|---|---|
install |
Abre la caché CACHE_NAME y ejecuta cache.addAll(urlsToCache). Es atómico: si un solo archivo falla (404, red),
la instalación entera falla y no queda nada guardado. No llama a skipWaiting(): una versión nueva espera hasta que la
persona acepte la actualización. |
activate |
Borra toda caché cuyo nombre sea distinto de CACHE_NAME. No llama a clients.claim(): en la primera instalación,
la pestaña abierta no queda controlada hasta recargar. |
fetch |
Cache-first para todo: caches.match(request).then(r => r || fetch(request)). No hay caché dinámica: lo que no está en
el precaché se pide a la red y no se guarda (por ejemplo /favicon.ico). |
message |
Si el dato es "SKIP_WAITING", ejecuta self.skipWaiting(). |
Flujo de actualización
- Se publica una versión con
CACHE_NAMEdistinto. El navegador detecta queservice-worker.jscambió y lo instala en segundo plano (nueva caché). - Cuando el worker nuevo queda
installedy ya hay uno controlando la página,index.jsmuestra el cartel «Nueva versión disponible». - Actualizar envía
SKIP_WAITING; el worker nuevo se activa y borra la caché vieja. - El evento
controllerchangerecarga la página con los recursos nuevos.
Precaché (28 entradas)
| Grupo | Archivos |
|---|---|
| Aplicación | /, /index.html, /styles.css,
/reset.css, /index.js, /manifest.json, /LICENSE, /README.md |
| Audios (13) | Todos los src/*.mp3, incluido bucleComienzo.mp3 que la aplicación no usa |
| Imágenes | cajaWeb.webp, codesLogo.webp, titulounDEAenunacaja.webp, images/Flag_of_Argentina.svg.webp |
| Íconos | icons/icono-192.png, icons/icono-512.png,src/cajaWebIcon.ico (256 KB) |
- Verificado en esta revisión: las 28 rutas corresponden a archivos existentes.
service-worker.jsno figura en la lista, lo cual es correcto: el navegador lo gestiona aparte. - 26 rutas empiezan con
/y 2 no (src/titulounDEAenunacaja.webpysrc/cajaWebIcon.ico). Con el sitio en la raíz del dominio todas resuelven bien. En una subcarpeta (por ejemplo GitHub Pages en/undeaenunacaja/) las 26 absolutas apuntan a la raíz del dominio, devuelven 404 y la instalación falla; habría que pasarlas a rutas relativas.
Procedimiento para publicar una versión
- Modificar los archivos. Si se agregan, renombran o quitan recursos, actualizar
urlsToCache. - Subir
CACHE_NAME. - Publicar y abrir la aplicación con la versión anterior ya instalada: debe aparecer el cartel de actualización.
- Revisar en las herramientas del navegador:Application → Service Workers(estado del worker) yCache Storage (28 entradas en la caché nueva, y sin la vieja).
- Probar sin conexión: activar modo avión, recargar y recorrer un modo completo.
Estilos, diseño y accesibilidad
Interfaz oscura de un solo fondo, con botones grandes. Los valores están escritos de forma literal en los CSS (no hay variables). No se declara tipografía: se usa la del sistema, así la aplicación carga sin recursos externos.
Paleta
Colores del temporizador (asignados desde JavaScript): #00c853 verde, #ffd600 amarillo, #d50000 rojo.
Componentes
| Componente | Especificación |
|---|---|
| Botón | Fondo #2c7ae0, texto rgb(221,214,214), borde 1px #6c7d8f, radio 5 px, relleno 10×20 px, margen 20 px, 1.2rem, transición de fondo de 0,3 s |
| Texto de estado | #textoEstado: 1.2rem, centrado, relleno lateral del 10 %, margen inferior 30 px |
| Barra de tiempo | Pista de 80 % del ancho (máximo 400 px), alto 22 px, radio 30 px; el relleno cambia de ancho con transición de 0,1 s |
| Tiempo restante | 3rem, negrita, blanco, centrado |
| Cartel de actualización | Fijo al pie, fondo #222, texto blanco, relleno 10 px. Estilos en línea en el HTML |
| Pie | Flex centrado, fondo #333, relleno 20 px; bandera de 70 px con margen izquierdo de 10 px |
Disposición y adaptación
htmlybodycon ancho 100 %, máximo1024pxy centrados;bodyes una columna flex.maincentra su contenido.- No hay media queries: la adaptación depende de unidades relativas y de
max-width: 100%en las imágenes.
Glosario
| Término | Significado |
|---|---|
| DEA / DESA | Desfibrilador Externo Automático / Semiautomático. En el automático el equipo entrega la descarga por sí solo; en el semiautomático la persona debe accionarla. |
| Ritmo desfibrilable / no desfibrilable | Ritmo cardíaco en el que la descarga está indicada / no está indicada. |
| RCP | Reanimación cardiopulmonar. El simulador marca ciclos de 2 minutos entre análisis de ritmo. |
| PWA | Aplicación web progresiva: se instala desde el navegador y puede funcionar sin conexión. |
| Service worker | Script del navegador que intercepta las solicitudes de la página y gestiona la caché. |
| Precaché / cache-first | Guardar archivos en la instalación / responder desde la caché antes de consultar la red. |