Skip to content

Latest commit

 

History

42 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Tatimer

Tatimer — Temporizador para shows en vivo

Temporizador para el monitor del ponente en eventos, conferencias y shows en vivo.

Timer standalone con gestión de sesiones (ponentes, tiempos y títulos de ponencia) y vistas independientes para escenario y sala. Sin dependencias, sin servidor: funciona online o abriendo index.html en cualquier navegador.

Abrir en el navegador Invítame a un café

plataforma offline show%20control Licencia: GPL v3


📑 Índice


¿Qué es Tatimer?

Tatimer es un temporizador pensado para el monitor del ponente en eventos, conferencias y shows en vivo. Permite organizar un congreso entero con sus ponentes y tiempos, mostrar una cuenta atrás a pantalla completa en el escenario y proyectar en la sala quién interviene a continuación, todo controlado por un operador desde una única interfaz.

Está disponible online sin instalación y también funciona descargando index.html y abriéndolo localmente en cualquier navegador: sin dependencias y sin servidor.

Funciones principales

  • ⏱️ Cuatro modos — cuenta atrás, cuenta atrás hasta una hora fija, cuenta adelante y reloj en tiempo real.
  • 👥 Gestión de sesiones — panel lateral con ponentes, títulos y tiempos individuales.
  • 🖥️ Vista Escenario — timer a pantalla completa sincronizado para el monitor del ponente.
  • 📢 Vista Audience — pantalla de sala con nombre y título del ponente activo, publicada manualmente por el operador.
  • 🟡🔴 Alertas visuales configurables en directo (amarillo y rojo), activables o desactivables globalmente.
  • 📊 Barra de progreso segmentada en tres zonas de color con marcador triangular de posición.
  • ⏳ Continúa en negativo al terminar el tiempo, con aviso de tiempo consumido.
  • 💬 Mensajes al ponente en pantalla en tiempo real, visibles también en la vista Escenario.
  • 🕐 Reloj de hora actual en la vista Escenario, activable desde el operador.
  • 🧹 Modo limpio — solo el contador y la barra de progreso a tamaño máximo.
  • ⬛ Blackout — pantalla negra completa; el contador sigue corriendo por debajo.
  • 🌙 Tema oscuro / claro y selector de idioma ES / EN.
  • 🔗 Control por URL para integración con QLab u otras herramientas de show control.
  • 📴 Funciona offline e instalable como app (PWA) — service worker propio, sin depender de la caché del navegador.
  • 🔆 Pantalla siempre encendida durante el show (Wake Lock) — no se apaga ni activa el salvapantallas mientras el timer está en pantalla.
  • ↩️ Deshacer al eliminar un ponente o una sesión — 5 segundos para recuperarlo antes de que se pierda.
  • 🔄 Recuperación de estado tras recarga o cierre accidental — si el operador recarga la pestaña o el navegador se cierra a media cuenta, el timer retoma corrigiendo el tiempo transcurrido durante el corte (running o en pausa), sin perder el conteo.
  • ✏️ Edición en vivo del tiempo — clic sobre el número grande (o tecla T) para escribir directamente el tiempo restante (countdown/hasta hora) o transcurrido (cuenta arriba), corriendo o en pausa. Enter aplica al instante y se retransmite a Escenario/Audience sin esperar al siguiente tick; Escape o clic fuera cancela sin tocar nada.
  • 🟢 Indicador de ventanas conectadas — el operador ve de un vistazo si las vistas Escenario y Audience siguen abiertas y recibiendo datos.
  • ❓ Panel de ayuda in-app — referencia rápida de modos, sesiones y atajos, sin salir de la app (tecla H).

🌐 Uso

Online

Abre javitatay.github.io/Tatimer en Safari o Chrome y arrastra la ventana al monitor del ponente. No necesita instalación ni conexión posterior.

Local (offline)

  1. Descarga todo el repositorio (index.html, manifest.json, icon.svg, sw.js).
  2. Ábrelo en Safari o Chrome.
  3. Arrastra la ventana al monitor del ponente.

Un service worker propio (sw.js) cachea la aplicación en la primera carga, así que funciona completamente offline desde la segunda vez que se abre, tanto en local como servida. También se puede instalar como aplicación (PWA) desde el navegador para tenerla como icono independiente. Las fuentes de Google Fonts se cargan con fuentes del sistema como alternativa si no hay conexión.


❓ Ayuda

Pulsa H o el botón ? de la esquina superior derecha para abrir un panel de ayuda dentro de la propia app: repasa modos, sesiones, vistas, atajos de teclado y control remoto sin salir de Tatimer ni depender de tener este README a mano en mitad de un show. Se cierra con Esc, con el botón ✕, o haciendo clic fuera del panel, y sigue el idioma (ES/EN) y el tema activos.


🖥️ Vistas adicionales

El operador puede abrir dos vistas independientes desde los botones de la esquina inferior derecha o con los atajos de teclado. Cada vista se abre en una ventana nueva que se puede arrastrar a otra pantalla y poner en pantalla completa (F11). La comunicación entre ventanas usa localStorage y funciona tanto en local (file://) como publicado en cualquier servidor.

Los botones Escenario y Audience de la interfaz del operador llevan un punto de estado: se pone verde en cuanto la ventana correspondiente está abierta y recibiendo datos, y vuelve a apagarse a los pocos segundos si se cierra o pierde la conexión. Es independiente de cómo se abrió la ventana (botón, atajo o URL directa) y sobrevive a recargar la página del operador.

Vista Escenario (E)

Pensada para el monitor del ponente en el escenario. Muestra únicamente el timer a pantalla completa con fondo negro, la barra de progreso con los mismos umbrales de color configurados en el operador, el banner de tiempo consumido al pasar de cero, y los mensajes que el operador envía desde la interfaz principal. Se actualiza en tiempo real, tick a tick.

El operador puede activar un reloj de hora actual que aparece en la esquina inferior derecha de la vista Escenario, en gris muy tenue para no distraer al ponente. Se activa y desactiva desde el botón con icono de reloj situado junto al botón Escenario en la interfaz del operador.

Si la ventana pierde la señal más de 6 segundos, muestra un indicador de conexión perdida (el operador emite un heartbeat cada 2s incluso parado o en pausa, así que esto solo salta si la ventana realmente se queda sin datos).

Para integrarla como ventana superpuesta en un solo monitor (arrastrada y redimensionada a mano sobre tu vista de moderador, sin mesa de mezclas), ver los parámetros display=numbers/display=bar/display=both, size y opacity en Control por URL.

Vista Audience (A)

Pensada para proyectar en la sala de espera o hall mientras se hace el cambio entre ponentes. Muestra el nombre del ponente en grande, el título de su ponencia, la duración asignada y el nombre de la sesión. El contenido no cambia automáticamente — el operador decide el momento exacto pulsando el botón Publicar en el panel de sesiones, lo que permite preparar el cambio sin que la sala lo vea antes de tiempo.

Al abrir la ventana carga el último ponente publicado, si lo hay.

Flujo de trabajo con vistas

  1. Pulsa E — se abre la vista Escenario; arrástrala al monitor del escenario y pon pantalla completa.
  2. Pulsa A — se abre la vista Audience; arrástrala al proyector de sala y pon pantalla completa.
  3. Trabaja desde la ventana principal del operador con normalidad.
  4. Cuando llegue el turno de un ponente, cárgalo desde el panel de sesiones, pulsa Publicar para que aparezca en la sala, y arranca el timer.

📋 Panel de sesiones

El panel lateral de sesiones permite organizar un congreso o evento con varias ponencias antes del show. Se abre desde el botón ☰ del borde izquierdo o con la tecla P.

Estructura

Cada sesión agrupa una lista de ponentes, cada uno con nombre del ponente, título de la ponencia y duración asignada (minutos : segundos).

Se pueden crear y gestionar múltiples sesiones — una por bloque del evento, por ejemplo. Las sesiones se guardan automáticamente en el navegador (localStorage) y persisten entre recargas. En cuanto hay dos o más, aparece un selector desplegable sobre el nombre para saltar entre ellas.

Flujo de trabajo habitual

  1. Abre el panel (P o el botón ☰ lateral).
  2. Crea una sesión nueva y ponle nombre.
  3. Añade los ponentes con sus tiempos desde el formulario inferior.
  4. Reordena las tarjetas arrastrando si es necesario.
  5. Durante el evento, haz clic en la tarjeta del ponente activo: el tiempo se carga automáticamente en el timer y hace reset.
  6. Pulsa Publicar para enviar ese ponente a la vista Audience.
  7. Pulsa Space para arrancar.

Opciones del panel

Elemento Función
Selector de sesión Cambia entre sesiones ya creadas (visible con 2 o más)
Campo de nombre Nombre de la sesión activa. Enter o Guardar para confirmar
Duplicar Copia la sesión activa entera (ponentes incluidos) como plantilla, p. ej. para el día 2 de un congreso
Nueva Crea una sesión vacía
Eliminar sesión Borra la sesión activa con confirmación (deshacer disponible 5 seg)
Tarjeta de ponente Click para cargar su tiempo en el timer
⠿ (asa de arrastre) Reordena ponentes por drag & drop
✕ Elimina el ponente de la lista (deshacer disponible 5 seg)
Total Duración acumulada de todos los ponentes de la sesión
Publicar Envía el ponente actualmente cargado a la vista Audience
↓ JSON Exporta solo la sesión activa como archivo .json
↑ JSON Importa uno o varios archivos — detecta automáticamente el formato (ver abajo)
⬇ Backup Exporta todas las sesiones de golpe, con fecha en el nombre del archivo

Solo se puede deshacer la última eliminación: si borras dos elementos seguidos, la posibilidad de recuperar el primero desaparece.

Formato de exportación / importación

Sesión individual (lo que genera ↓ JSON):

{
  "id": "abc123",
  "name": "Mañana — Bloque A",
  "speakers": [
    { "id": "x1", "name": "Ana García", "title": "Inteligencia artificial aplicada", "min": 20, "sec": 0 },
    { "id": "x2", "name": "Marc Puig",  "title": "Diseño de experiencia sonora",    "min": 15, "sec": 0 }
  ]
}

Backup completo (lo que genera ⬇ Backup), con varias sesiones dentro:

{
  "type": "tatimer_backup",
  "version": 1,
  "exportedAt": "2026-07-02T10:00:00.000Z",
  "sessions": [ { "name": "Bloque A", "speakers": [...] }, { "name": "Bloque B", "speakers": [...] } ]
}

↑ JSON acepta cualquiera de los dos formatos anteriores, un array de sesiones sin envoltura de backup, o un array plano de ponentes sin envoltura de sesión (formato heredado). La importación siempre añade sesiones nuevas — nunca sobrescribe lo que ya tienes.


🎛️ Controles en pantalla

Elemento Ubicación Función
☰ / ✕ Borde izquierdo Abre / cierra el panel de sesiones
Minutos / Segundos Panel izquierdo Configura el tiempo inicial (modo cuenta atrás). Se actualiza en tiempo real al editar.
Hora objetivo Panel izquierdo Configura la hora de llegada (modo Hasta hora). Se actualiza en tiempo real al editar.
Cuenta atrás / Hasta hora / Cuenta adelante / Reloj Panel derecho Cambia el modo del temporizador
Preaviso (min) Panel derecho Minutos restantes para activar el primer aviso. El círculo de color junto a la etiqueta personaliza el tono.
Alerta final (min) Panel derecho Minutos restantes para activar el segundo aviso, más cerca del final. El círculo de color junto a la etiqueta personaliza el tono.
Alertas ON / OFF Panel derecho Activa o desactiva todas las alertas globalmente
ES / EN Panel derecho Cambia el idioma de la interfaz
◐ Panel derecho Alterna entre tema oscuro y tema claro
Iniciar / Reanudar Centro inferior Arranca o reanuda el contador. Verde mientras no ha arrancado nunca (Iniciar); verde azulado/teal, un tono distinto, en cuanto hay algo que reanudar (Reanudar) — así se distinguen de un vistazo aunque ocupen el mismo botón.
Reset Centro inferior Vuelve al estado inicial. En rojo.
Pausar Centro inferior Pausa sin perder el tiempo. Solo visible mientras corre, en ámbar.
−30 seg Centro inferior Resta 30 segundos al tiempo
+1 min Centro inferior Añade 1 minuto al tiempo
Blackout Centro inferior Cubre la pantalla con negro. El contador sigue corriendo.
🕐 Esquina inferior derecha Activa / desactiva el reloj de hora en la vista Escenario
Escenario Esquina inferior derecha Abre / enfoca la vista Escenario
⚙ Esquina inferior derecha Opciones de la vista Escenario — overlay/picture-in-picture (ver Vistas adicionales)
Audience Esquina inferior derecha Abre / enfoca la vista Audience
⊞ Esquina inferior derecha Activa / desactiva el modo limpio

⌨️ Atajos de teclado

Tecla Acción
Space Iniciar / Pausar
R Reset
M +1 minuto
S −30 segundos
T Editar tiempo directamente (también con clic en el número)
B Blackout
C Modo limpio / vista completa
P Abrir / cerrar panel de sesiones
Esc Cerrar panel de sesiones (si está abierto)
E Abrir / enfocar vista Escenario
A Abrir / enfocar vista Audience
H Abrir / cerrar el panel de ayuda

⏲️ Modos

Cuenta atrás — introduce minutos y segundos antes de empezar. Al llegar a cero el contador continúa en negativo para no interrumpir, y aparece un aviso de tiempo consumido en la parte superior.

Hasta hora — introduce una hora fija (HH:MM) y el timer cuenta atrás hasta llegar a ella. Si la hora indicada ya pasó hoy, se asume automáticamente para mañana. Útil para "el show empieza a las 10:00" o "volvemos del descanso a las 17:30". Comparte el resto del comportamiento con cuenta atrás: alertas, barra de progreso, tiempo consumido en negativo.

Cuenta adelante — arranca desde cero y cuenta el tiempo transcurrido.

Reloj — muestra la hora actual en formato HH:MM:SS. No requiere iniciar ni pausar.


📊 Barra de progreso

La barra muestra tres zonas de color — verde, amarillo y rojo — cuyo tamaño proporcional se calcula a partir de los umbrales de alerta configurados. Un marcador triangular blanco indica la posición actual del tiempo. A medida que avanza el cronómetro, la zona ya consumida se oscurece y el marcador viaja hacia la izquierda.

Los umbrales se pueden ajustar en directo durante el show sin reiniciar el contador; la barra se recalcula al instante. Si las alertas están desactivadas, la barra se muestra completamente en verde. La vista Escenario refleja los mismos umbrales y colores.


🚦 Alertas de tiempo

Dos umbrales independientes configurables en el panel derecho:

  • Preaviso — minutos restantes para activar el primer cambio de color.
  • Alerta final — minutos restantes para activar el segundo cambio de color, más cerca del final.

Junto a cada etiqueta hay un pequeño círculo de color (<input type="color">, sin el cuadrado nativo del navegador) para personalizar el tono de cada alerta a gusto del operador — el nombre de cada una ya no va ligado a ningún color por defecto. Un solo color por alerta, igual en ambos temas una vez elegido — mientras no se toque, cada tema sigue usando su propio valor por defecto de siempre. Se aplica al momento a: el dígito del cronómetro, la barra de progreso y el banner de "tiempo consumido" (overtime) — tanto en la ventana principal como en la vista Escenario, incluida si ya está abierta (se actualiza en vivo, sin recargarla). Se guarda en localStorage y se recuerda entre sesiones.

El toggle Alertas ON / OFF debajo de la alerta final desactiva ambas alertas globalmente. Con alertas OFF el timer permanece en blanco independientemente del tiempo restante, la barra de progreso no muestra segmentos de color, y la vista Escenario no cambia de color. Útil para ponentes que prefieren no ver ningún aviso visual.


💬 Mensajes al ponente

El operador escribe en la caja de texto inferior y pulsa Enter. El texto aparece en pantalla grande en amarillo tanto en la ventana principal como en la vista Escenario, visible desde el escenario. Esc o el botón Borrar (en azul, igual que Enviar) lo eliminan en ambas pantallas simultáneamente.


⬛ Blackout

El botón Blackout (o la tecla B) cubre toda la pantalla con negro. El contador sigue corriendo por debajo. Pulsar de nuevo el botón, la tecla B o hacer clic en cualquier parte de la pantalla negra desactiva el blackout y muestra el tiempo real transcurrido.


🧹 Modo limpio

El botón de maximizar (esquina inferior derecha) oculta todos los controles y muestra únicamente el contador y la barra de progreso a tamaño máximo. El panel de sesiones y los botones de vistas también se ocultan en este modo. Útil cuando el monitor del ponente está cerca del público. La tecla C hace lo mismo.


🔗 Control por URL

El temporizador acepta parámetros en la URL para integrarse con herramientas de show control como QLab:

https://javitatay.github.io/Tatimer/?action=start
https://javitatay.github.io/Tatimer/?action=pause
https://javitatay.github.io/Tatimer/?action=reset
https://javitatay.github.io/Tatimer/?action=addminute
https://javitatay.github.io/Tatimer/?action=subtractthirty
https://javitatay.github.io/Tatimer/?action=start&minutes=10&seconds=0
https://javitatay.github.io/Tatimer/?action=start&mode=countup
https://javitatay.github.io/Tatimer/?action=start&mode=clock
https://javitatay.github.io/Tatimer/?lang=en

Las vistas también se pueden abrir directamente por URL:

https://javitatay.github.io/Tatimer/?view=escenario
https://javitatay.github.io/Tatimer/?view=audience

La vista Escenario admite además opciones de visualización, pensadas para usarla como ventana superpuesta sobre tu vista de moderador (Keynote, PowerPoint, Slides) cuando solo hay un monitor de referencia — sin depender de una mesa de mezclas de vídeo: se arrastra y redimensiona como cualquier ventana, con opacidad de fondo ajustable y un borde sutil para distinguirla. Se activan con el botón de engranaje junto a Escenario en la esquina inferior derecha (se quedan recordadas para la próxima vez), o directamente por URL:

https://javitatay.github.io/Tatimer/?view=escenario&display=numbers
https://javitatay.github.io/Tatimer/?view=escenario&display=bar
https://javitatay.github.io/Tatimer/?view=escenario&display=both
https://javitatay.github.io/Tatimer/?view=escenario&display=numbers&size=150&opacity=40
  • display=numbers — solo el número, sin barra de progreso ni reloj ni banner de tiempo consumido (el número sigue cambiando de color en warning/danger/overtime).
  • display=bar — lo mismo pero solo con la barra de progreso, sin el número.
  • display=both — número y barra juntos, sin reloj ni banner de overtime.
  • size=<50-400> — escala el número y la barra (por defecto 100%). Hasta 400% para que el número solo pueda llenar toda la vista en modo "solo números". Útil también para ajustar el tamaño al espacio libre en la vista de moderador antes de redimensionar la ventana en sí.
  • opacity=<0-100> — opacidad del fondo (por defecto 100%, sólido). Solo tiene efecto junto con display=numbers, display=bar o display=both — en la vista completa (sin display) el fondo siempre es sólido. En una ventana normal sin compositor (OBS/vMix), una opacidad baja se ve rellena por lo que el propio navegador pinte detrás, no por lo que haya en la aplicación de abajo.
  • Combinables entre sí y con el resto de parámetros de esta sección.

🛠️ Para desarrolladores

Tatimer es una aplicación web autocontenida en index.html, sin dependencias externas ni framework. La persistencia y la comunicación entre ventanas se hacen con localStorage, por lo que funciona igual servido (GitHub Pages) o en local (file://).

Incluye un service worker (sw.js, estrategia stale-while-revalidate) y un manifest.json para funcionamiento offline real e instalación como PWA. Ambos son opcionales: si el navegador no los soporta o el archivo se abre desde un contexto no seguro (file://, content://), la aplicación funciona igual, simplemente sin esa capa de caché adicional.

Cuida accesibilidad de teclado (:focus-visible, aria-pressed/aria-expanded en los toggles, focus trap en el panel de sesiones) y contraste WCAG AA en ambos temas.

Colores de estado de los botones de control (state-start/state-resume/state-yellow/state-red/state-blue). Fondo muy tenue (8-12% de alfa) + borde y texto en color sobre la base de botón de siempre — nada de bloques sólidos, para no romper el tono discreto del resto de la interfaz. Reutilizan la misma paleta semántica que ya usa el cronómetro (verde = en marcha, ámbar = aviso, rojo = peligro) en vez de inventar colores nuevos. Iniciar y Reanudar comparten el mismo botón (#btn-startstop) y por tanto la misma forma de "pill" (.primary, solo mientras no está corriendo) pero llevan colores distintos — verde para Iniciar, verde azulado/teal para Reanudar — centralizados en setStartButtonLabel(isResume), una función a la que llaman toggleTimer(), pauseTimer(), resetTimer(), restoreTimerState() y setLang() en vez de tocar textContent/classList cada una por su cuenta, para que el texto y el color siempre cambien juntos sin depender de acordarse de actualizar los dos en cada sitio.

Colores de alerta personalizables (--alert-warning/--alert-danger, color-warning/color-danger). Dos custom properties CSS que sustituyen a los hex sueltos (#e8a000, #FF6363, #c8a800, #d42b00...) que antes estaban repartidos por #display.warning/.danger, #progress-fill.warning/.danger, #overtime-banner y sus equivalentes en la vista Escenario — de paso corrigen una inconsistencia real que ya existía: en tema oscuro, el aviso amarillo del dígito del operador usaba var(--accent), que en dark theme vale literalmente lo mismo que var(--danger) (#FF6363), así que salía en rojo en vez de en ámbar. applyAlertColors(colors) fija (o quita, si no hay personalización) --alert-warning/--alert-danger como inline style en <html> — un solo sitio que cascada a todos los selectores de golpe. Sin personalizar, cada tema sigue usando su propio valor por defecto (definido en :root/body.light, como siempre); en cuanto se elige un color desde el <input type="color">, ese inline style gana la cascada y aplica igual en ambos temas hasta que se personalice de nuevo. Persistido en localStorage (tatimer_alert_colors) y propagado a la vista Escenario — que es un documento aparte (window.open), sin el <html> del operador — con el mismo patrón de lectura-al-abrir + listener de storage para actualización en vivo que ya usan las opciones de la propia vista Escenario; el listener parsea e.newValue directamente (no relee localStorage), igual que el resto de listeners de esa misma función, en vez de depender de un side-channel adicional.

El estado del timer (mode, elapsed, totalSeconds, running, ts) se emite cada tick a localStorage (tatimer_bc_timer), el mismo canal que sincroniza las vistas Escenario/Audience. Al cargar la página, restoreTimerState() lee ese último estado y corrige elapsed con el drift real (Date.now() - ts) antes de retomar — así una recarga o un crash del operador a media cuenta no pierde el tiempo transcurrido. Cede siempre ante un estado explícito ya en marcha (p. ej. ?action=start por URL). Valida los campos numéricos antes de usarlos (un LS_TIMER de una versión anterior sin ts, o corrupto, se ignora en vez de propagar NaN al display) y descarta estados de más de 2h de antigüedad, tratándolos como sesión anterior olvidada en vez de crash reciente.

El conteo en marcha (toggleTimer()) no incrementa elapsed a ciegas en cada disparo del setInterval — lo recalcula siempre desde un ancla de reloj real (tickAnchorWallClock/tickAnchorElapsed + Date.now()). Esto importa para directos largos: un tab en segundo plano, una pestaña que el navegador throttlea, o simplemente una GC pause pueden hacer que el setInterval de 1000ms no se dispare puntualmente durante minutos — con un contador ciego el timer se queda retrasado sin avisar; con el ancla, en cuanto el tick vuelve a disparar se autocorrige al tiempo real. Verificado con @sinonjs/fake-timers simulando una pestaña en background varios minutos.

restoreTimerState() y handleURLParams() se ejecutan envueltos en try/catch en el arranque: si cualquiera de las dos falla (por ejemplo, un id de elemento que cambia en un futuro retoque de maquetado), el error se registra en consola pero el resto del arranque — listeners de los inputs, Wake Lock, etc. — se completa igual. Antes, una excepción ahí habría cortado el resto del <script> sin avisar.

broadcastTimerState() ya no depende solo de los ticks del timer corriendo: hay un setInterval(broadcastTimerState, 2000) incondicional en el arranque, así que la vista Escenario recibe estado fresco cada 2s pase lo que pase (parado, pausado, corriendo). Antes, en pausa o antes de arrancar la cuenta, no salía ningún broadcast y a los 4s Escenario se caía a "esperando señal" aunque todo funcionara bien; su margen de espera (connTimeout) subió de 4s a 6s para dar más colchón sobre el heartbeat de 2s.

Las clases .warning/.danger del número grande (operador y Escenario) ya solo cambian de color — se quitó la animación de parpadeo (pulse-warning/pulse-danger, pw/pd) a petición expresa: el cambio de color es suficiente aviso, sin distraer en pantalla.

Tipografía autohospedada, sin fonts.googleapis.com. La página del operador declaraba font-family: 'Bebas Neue' en #display y .mode-label pero nunca la importaba (solo importaba Inter) — el número grande caía siempre a la tipografía por defecto del sistema, con o sin internet, en local o en https. Ahora las tres vistas (operador, Escenario, Audience) llevan Inter y Bebas Neue embebidas como @font-face en base64 dentro del propio HTML (subset latin, cubre español/catalán: á é í ó ú ñ ü ç ¿ ¡). Cero peticiones de red para tipografías, en ninguna vista, nunca — coherente con que Tatimer sea "sin dependencias, sin servidor". Esto añade ~180KB al archivo (fuentes en base64 pesan ~33% más que el binario original), asumible para un archivo local. Verificado que las reglas @font-face se parsean sin errores y que los datos en base64 decodifican exactamente al tamaño original de cada fuente (sin truncar).

Edición en vivo del tiempo (startEditTime()/commitEditTime()). El #display se vuelve un <input> al hacer clic (o pulsar T); parseTimeEdit() acepta MM:SS, segundos sueltos sin :, y un signo -/− inicial para fijar overtime directamente. Al confirmar con Enter: en countdown/hasta-hora ajusta totalSeconds = elapsed + valorEscrito — deja elapsed intacto a propósito, porque es el ancla de reloj real de la cuenta (ver más abajo), así una edición en pleno directo no descuadra el tick; en cuenta-arriba ajusta elapsed directamente y resincroniza el ancla si está corriendo, igual que hacen addMinute()/subtractThirty(). Llama a broadcastTimerState() justo después de aplicar, así Escenario/Audience reflejan el cambio al instante en vez de esperar al heartbeat de 2s. Escape o perder el foco cancela sin aplicar nada. Verificado con pruebas para countdown corriendo, pausado, cuenta-arriba corriendo (confirmando que sigue contando bien tras la edición), entrada de overtime directa, cancelación, y entradas inválidas.

escHtml() (usado al renderizar nombre/título de cada ponente en el panel de sesiones) ahora castea con String(str ?? '') antes de escapar. Antes, un valor no-string en speaker.name/speaker.title — por ejemplo tras importar un JSON con un campo numérico en vez de texto — hacía renderSessionsPanel() sin capturar la excepción en toda regla, dejando el panel de sesiones a medio pintar (sin las tarjetas de ponentes) para el resto del directo, sin ningún aviso visible. Verificado forzando ese caso: antes rompía, ahora renderiza con normalidad.

Opciones de visualización de Escenario (overlayNumbersOnly/overlayBarOnly/overlayBothOnly/escScalePct/escOpacityPct, applyEscOverlayVisuals()). Leídas de ?display=numbers, ?display=bar, ?display=both, ?size=<50-400> y ?opacity=<0-100> al arrancar la vista — pero son let, no const: se recalculan en vivo si el popover del operador cambia mientras esta ventana sigue abierta (mismo origen), vía un listener de storage sobre la key tatimer_esc_options que también usa el popover para persistir su estado. La URL solo manda en el arranque, o de forma permanente cuando no hay un operador en el mismo origen escribiendo esa key (p.ej. la URL pegada directamente como fuente en OBS, en su propio Chromium embebido y aislado — ahí no llega ningún evento storage y el comportamiento es el de siempre, fijo por URL).

display=numbers añade la clase overlay-mode (oculta barra/reloj/banner de overtime); display=bar añade overlay-bar-mode (al revés, oculta el número); display=both añade overlay-both-mode (muestra número y barra juntos, oculta solo reloj/banner de overtime). Las tres ocultan con !important porque esos elementos también los toca applyTimer() vía style.display inline — una regla !important en la hoja de estilos sí gana a un inline style normal, así que no hace falta tocar esa lógica.

Fondo y borde con opacidad ajustable (100% por defecto = sólido), ambos aplicados como inline style dentro de applyEscOverlayVisuals() — una función aparte que tanto applyTimer() (en cada tick, con el tema en directo del operador) como el listener de opciones en vivo (con el último tema conocido, guardado en currentTheme) pueden llamar sin duplicar la lógica. document.body.style.backgroundColor en vez de CSS porque el color base (negro u claro) depende de d.theme, que solo se conoce al recibir cada tick de estado; se probó primero con una variable CSS (background:rgba(0,0,0,var(--x))) pero el motor de CSS usado en las pruebas automatizadas (jsdom) no resuelve var() anidado dentro de rgba() — un estándar bien soportado en navegadores reales, pero para poder verificarlo con las pruebas del proyecto se cambió al cálculo directo en JS.

El borde se desvanece a la vez que el fondo (0.15 * opacity), a propósito, para servir los dos usos reales sin que se estorben: a opacidad alta (ventana de navegador normal arrastrada a mano sobre la vista de moderador, sin mesa de mezclas — el caso original de esta función) ayuda a ver el borde de la ventana; a opacidad 0 (incrustada con canal alfa real vía OBS/vMix Browser Source, NDI con alfa, etc.) desaparece del todo para no dejar un marco visible sobre la señal compuesta. El número y la barra llevan además una sombra fija — text-shadow en el número, box-shadow en la barra — que NO depende de la opacidad, porque con canal alfa real el fondo puede llegar a ser transparente de verdad y la sombra es lo único que mantiene el número legible sobre cualquier imagen de fondo, sea cual sea.

Bug real de transparencia, ya corregido: el fondo iba solo en body, pero <html> seguía con background:#000 fijo en su propia regla CSS (html,body{...background:#000...}). Como <html> se pinta detrás de <body>, aunque applyEscOverlayVisuals() pusiera el rgba() correcto en body, <html> seguía aportando un negro sólido debajo — con lo que ningún valor del tirador de opacidad, ni siquiera 0%, llegaba a dar transparencia real para un compositor con canal alfa (seguía viéndose opaco de todas formas). Arreglado separando la regla: html,body{...} ya no lleva background, y body{background:#000} va aparte, solo en body. <html> se queda sin fondo propio (transparente por defecto) en los tres temas/estados, así que ahora si el tirador está a 0% el resultado final es transparente de verdad en toda la pila, no solo en apariencia.

size se traduce a la variable CSS --esc-scale (clamp 50-400, por defecto 100) aplicada como transform: scale() a #esc-timer y #esc-bar-wrap — vale tanto en overlay como en la vista completa; a partir de ~250-300% en modo "solo números", el número desborda el viewport original (el body mantiene overflow:hidden) y queda recortado limpiamente en los bordes, que es justo el efecto buscado para llenar toda la pantalla solo con dígitos. opacity solo tiene efecto con display=numbers, display=bar o display=both activos — la vista completa (sin display) se queda deliberadamente fuera de este tratamiento, para no sorprender a quien la use como monitor de escenario dedicado de toda la vida. Sin estos parámetros el comportamiento no cambia — verificado explícitamente con un caso de regresión.

Estas opciones también se pueden activar desde un botón de engranaje (#btn-escenario-options) junto al botón Escenario: un popover con cuatro radios (vista completa / solo números / solo barra / número + barra), un slider de tamaño y un slider de opacidad de fondo (este último solo con efecto fuera de "vista completa"), sin tener que escribir la URL a mano. openView('escenario') traduce la selección a los parámetros correspondientes al abrir la ventana. El estado se guarda en localStorage (tatimer_esc_options) y se restaura al recargar. Si la ventana Escenario ya está abierta, los cambios en el popover se aplican ahí en vivo, sin cerrarla ni recargarla — no hace falta reabrirla para ver el efecto. Solo afecta a openView('escenario') — openView('audience') nunca lleva estos parámetros.

Tatimer/
│
├── README.md
├── LICENSE
├── manifest.json
├── icon.svg
├── sw.js
└── index.html

⚠️ Si solo distribuyes/descargas index.html suelto (como al bajarlo directo del repo o de un enlace, sin el resto de archivos), el <link rel="manifest"> y navigator.serviceWorker.register('sw.js') fallan en silencio (van con .catch(()=>{}), no rompen nada) — pero pierdes la instalación como PWA y el caché real de service worker. Para uso local vía file:// esto no importa (la app funciona igual, ya es autocontenida en JS/CSS/fuentes), pero conviene saber que esas dos capas de robustez extra solo están activas si sw.js/manifest.json viajan junto al index.html, o si se sirve desde el repo (GitHub Pages).


🧭 Arquitectura y mejoras estructurales a valorar

Notas de una auditoría completa del archivo (3.360 líneas, ~310KB con fuentes embebidas). Nada de esto es urgente para el próximo directo — son mejoras de mantenibilidad a medio plazo, no bugs activos:

  • Triplicación entre las 3 vistas. Operador, Escenario y Audience son documentos independientes (Escenario/Audience se generan como HTML+CSS+JS embebidos en strings dentro de un IIFE, inyectados en una ventana nueva). Esto significa que CSS compartida (fuentes, colores de alerta, etc.) vive copiada 2-3 veces. Ya nos ha mordido dos veces en esta sesión: el fix de las fuentes y el de quitar el flash de alertas hubo que aplicarlos por triplicado. A medio plazo, un pequeño script de build (sin necesidad de webpack/bundlers — un node build.js con plantillas y fs.readFileSync bastaría) que genere el index.html final a partir de piezas compartidas (una hoja de fuentes, un bloque de estilos de alerta) eliminaría esta clase de bug por diseño, manteniendo igualmente un único archivo final para distribuir.
  • Sin suite de tests persistida. Todo lo verificado hoy (drift de reloj, recuperación tras crash, heartbeat de Escenario, edición en vivo, escapado de HTML) vive en scripts sueltos de una sesión de trabajo, no en el repo. Merece la pena guardar una carpeta tests/ con Node + jsdom (dependencias de desarrollo, no afectan al index.html final) y un npm test que cargue la página y verifique estos escenarios — así una futura modificación no reintroduce silenciosamente alguno de estos bugs ya cazados.
  • Sin número de versión ni changelog en el propio archivo. Con la cantidad de parches de esta sesión, sería fácil dentro de unos meses no saber qué build exacta se está usando en un venue. Un comentario simple al principio del <script> (// Tatimer build: 2026-09-12) o un CHANGELOG.md en el repo ayudaría a rastrearlo.
  • Tamaño del archivo. ~310KB es perfectamente manejable para una herramienta local de un solo operador — no es una web pública con presupuesto de carga. Si en el futuro crece mucho más (más idiomas, más vistas), sí empezaría a tener sentido separar en módulos con el build mencionado arriba.

🔗 Más herramientas

Otras herramientas para producción de eventos en directo:

  • 🎪 Tarimeo — Diseña y organiza la distribución de tarimas y escenarios. Ver proyecto · Demo
  • 📡 RFTDT — Consulta de frecuencias TDT libres para microfonía inalámbrica. Ver proyecto · Demo
  • 🔷 VectorSlice — Convierte vectores de Illustrator en Slices de Resolume Arena. Ver proyecto · Demo
  • 🎬 QLab Live Scripts — Controla PowerPoint, Chrome, Zoom, PDF y Spotify desde QLab 5. Ver proyecto · Demo
  • 🎓 AVV Lab — Plataforma educativa interactiva de Animación Visual en Vivo. Ver proyecto · Demo

📄 Licencia

Tatimer se distribuye bajo la licencia GNU General Public License v3.0.

Eres libre de usar, estudiar, modificar y compartir este software. La única condición importante es que, si distribuyes una versión modificada, debe mantenerse también como código abierto bajo esta misma licencia, para que las mejoras sigan estando disponibles para todos.

Licencia: GPL v3


✉️ Contacto

Javier Tatay Rubio 📧 j.tatayrubio@edu.gva.es · javitatay@gmail.com


Probado en macOS Sonoma 14 · Safari · Google Chrome 124+ · Tatimer · 2026

About

Temporizador para el monitor del ponente en eventos en directo. PWA offline, sin dependencias.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages