Saltar a contenido

Jobs de fondo y alertas

WorkDone Sanitarios no es un sistema puramente reactivo: no todo nace de un tap sobre una placa NFC. Hay un conjunto de procesos periódicos (@Scheduled) que corren en el backend cada cierto tiempo, sin que nadie los dispare, y que se encargan de generar alertas, cerrar estado colgado y limpiar datos.

Esta página explica esos jobs, el catálogo real de alertas, cómo viven y mueren las alertas (incluida la deduplicación y la auto-atención), y los estados de conexión que el backend deriva de cada dispositivo.

Fuente de verdad

Todo lo que sigue está verificado contra el código del backend (ar.com.lubeca.workdone). Las rutas de archivo y líneas citadas son las reales al momento de escribir esta página.

1. Schedulers @Scheduled

Cada job es un método @Scheduled + @Transactional independiente: si uno falla, su transacción hace rollback sola y no afecta a los demás. La cadencia es configurable por properties (se muestra el default).

Job Clase Cadencia (default) Qué hace
revisarSlaVencido SlaSchedulerService cada 5 min (app.scheduler.sla-check-fixed-rate-min) Recorre los SLA activos. Si el último EGRESO válido de un sanitario excede frecuencia_min, genera una alerta SLA_VENCIDO (con dedup).
cerrarTrabajosColgados SlaSchedulerService cada 1 hora (fijo) Cierra trabajos abiertos hace demasiado tiempo, trunca la duración y los marca como no contables para SLA. Ver detalle abajo.
limpiarRefreshTokensExpirados SlaSchedulerService cron 3 AM (app.scheduler.refresh-token-cleanup-cron) Borra físicamente los refresh tokens cuyo expires_at quedó fuera de la ventana de retención.
verificarTerminalesSinReportar DispositivoMonitorService cada 5 min (app.monitor.monitor-check-fixed-rate-min) Recorre las terminales Smiley activas; genera o auto-atiende DISPOSITIVO_SIN_REPORTAR.
evaluarTendencias SmileyTendenciaService cada 5 min (app.scheduler.tendencias-check-fixed-rate-min) Evalúa cada terminal Smiley contra su regla de tendencia; genera o auto-atiende TENDENCIA_NEGATIVA.
generarEjecucionesDelDia RutaEjecucionGeneratorService cron 4 AM (app.scheduler.rutas-generar-ejecuciones-cron) Cierra como INCOMPLETA las ejecuciones de ruta de días anteriores y genera las del día (idempotente).
removeNotActivatedUsers UserService cron 1 AM (fijo) Limpieza JHipster estándar: borra usuarios no activados.

Single-instance asumido

El scheduler de Spring corre single-thread, así que un mismo job nunca se solapa consigo mismo. El deployment hoy es una sola instancia EC2 — por eso el check-then-insert anti-duplicado basta. Con escalado horizontal el check sería race-prone (TOCTOU); el backstop a nivel datos es el índice único filtrado UX_alerta_sla_vencido_activa (migration 029, ADR-035).

cerrarTrabajosColgados en detalle

Busca trabajos abiertos cuyo inicio_ts es anterior a now - app.scheduler.trabajos-colgados-max-hours y los cierra con estado EGRESO_CIERRE_AUTOMATICO. Sobre el cierre:

  • Trunca el fin_ts al máximo de app.tap.cierre-automatico-max-minutes (no inventa una duración gigante).
  • Setea cuenta_para_sla = false: un trabajo colgado durante horas no es una limpieza real medida, así que no debe contar para el SLA.
  • Agrega el warning scheduler_cierre al trabajo.

No confundir con el cierre automático por tap

Hay dos cierres automáticos distintos:

  • Por scheduler (este job): el trabajo quedó colgado y el sistema lo cierra de oficio → cuenta_para_sla = false.
  • Por siguiente ingreso (tap): cuando llega un nuevo INGRESO en el mismo sanitario y había uno abierto, se cierra el anterior. Ese cuenta para el SLA (ADR-024) y NO toca cuenta_para_sla.

El detalle de ambos vive en Reglas operativas.

2. Generación de alertas

El catálogo real es el enum TipoAlerta (domain/enumeration/TipoAlerta.java). Estas son las 9 clases de alerta y quién las dispara:

TipoAlerta Condición que la dispara Quién la genera
SLA_VENCIDO El último EGRESO válido del sanitario excede frecuencia_min (y hoy es día con exigencia). SlaSchedulerService.revisarSlaVencido (job 5 min)
DURACION_ANOMALA Un EGRESO con duración menor a duracion-minima-seg — posible doble tap. TrabajoTapService (al procesar el tap)
OPERARIO_INACTIVO_OFFLINE Se acepta un trabajo sincronizado de un operario marcado como inactivo. TrabajoTapService (al procesar el tap)
TAG_DESCONOCIDO Tap sobre un tag NFC no habilitado; el trabajo queda sin sanitario, pendiente de resolución manual. TrabajoTapService (al procesar el tap)
EVENTO_REPORTADO El operario reporta un evento al cerrar un trabajo (ej. falta de insumos). TrabajoTapService (al procesar el tap)
RELOJ_DESVIADO El device_now del dispositivo difiere del server más de drift-max-seg. SyncService.procesarDriftReloj (durante el sync)
TAG_DEFECTUOSO Se registra un trabajo en modo MANUAL porque la placa NFC del sanitario no funciona. TrabajoTapService (al procesar el tap)
TENDENCIA_NEGATIVA El conteo/proporción de opiniones NEGATIVA supera el umbral de la regla en la ventana. SmileyTendenciaService.evaluarTendencias (job 5 min)
DISPOSITIVO_SIN_REPORTAR Una terminal Smiley activa pasa al estado SIN_REPORTAR. DispositivoMonitorService.verificarTerminalesSinReportar (job 5 min)

Reactivas vs. periódicas

Las que genera TrabajoTapService / SyncService nacen reactivamente mientras se procesa lo que mandó un dispositivo. Las tres restantes (SLA_VENCIDO, TENDENCIA_NEGATIVA, DISPOSITIVO_SIN_REPORTAR) nacen periódicamente desde un job, sin tap de por medio.

3. Ciclo de vida de una Alerta

Una alerta tiene dos timestamps clave:

  • generada_en — cuándo se creó.
  • atendida_en — cuándo se cerró. null significa que está abierta.
stateDiagram-v2
    [*] --> Abierta: generada_en = now\natendida_en = null
    Abierta --> Atendida: alguien la atiende\n(operario/admin)
    Abierta --> AutoAtendida: el sistema detecta\nque la condición se normalizó
    Atendida --> [*]
    AutoAtendida --> [*]
    note right of Abierta
        Mientras hay una abierta del mismo
        (tipo, sanitario/dispositivo) NO se
        crea otra: dedup.
    end note

Deduplicación

Antes de crear una alerta, el backend chequea si ya existe una abierta equivalente y, si la hay, no crea una nueva. La clave de equivalencia es (tipo, sujeto, atendida_en IS NULL), donde el sujeto es el sanitario o el dispositivo según el tipo:

// SlaSchedulerService.java:112
if (alertaRepository.existsByTipoAndSanitario_IdAndAtendidaEnIsNull(TipoAlerta.SLA_VENCIDO, s.getId())) continue;

Este patrón se repite igual en TENDENCIA_NEGATIVA, DISPOSITIVO_SIN_REPORTAR, RELOJ_DESVIADO y TAG_DEFECTUOSO. Resultado: una sola alerta abierta por sujeto y tipo.

Auto-atención

Algunas alertas las cierra el propio sistema (atendida_por_usuario = "sistema") cuando detecta que la condición que las generó se normalizó. No requieren intervención humana.

TipoAlerta Se auto-atiende cuando… Evidencia
RELOJ_DESVIADO el drift del reloj del dispositivo vuelve por debajo del umbral. SyncService.java:228-244
DISPOSITIVO_SIN_REPORTAR la terminal vuelve a reportar (estado ≠ SIN_REPORTAR). DispositivoMonitorService.java:163-183
TENDENCIA_NEGATIVA hay una limpieza válida posterior a generada_en de la alerta. SmileyTendenciaService.java:129-148
TAG_DEFECTUOSO llega un trabajo NFC posterior en ese sanitario (la placa volvió a leerse). TrabajoTapService.java:602-613

La auto-atención corre ANTES de evaluar el umbral

En evaluarTendencias la auto-atención va deliberadamente antes del chequeo de umbral. Si hubo una limpieza correctiva pero siguen llegando negativas en la nueva ventana, en la misma ejecución se cierra la alerta vieja y se abre una nueva. Es decir: la limpieza no silencia indefinidamente.

4. Estados derivados de conexión del dispositivo

El backend no persiste el estado de conexión: lo deriva en memoria a partir de ultimo_contacto_ts y now, sin un hit extra a la base. La lógica vive en DispositivoMonitorService.derivarEstado (DispositivoMonitorService.java:90-108).

Si ultimo_contacto_ts es nullNUNCA_CONECTADO. En el resto, los umbrales dependen del tipo de dispositivo:

Tipo EN_LINEA CON_RETRASO SIN_REPORTAR
APP_OPERARIO (celular) < 30 min < 120 min ≥ 120 min
TERMINAL_SMILEY (tablet) < 15 min < 30 min ≥ 30 min

Los valores son los defaults de MonitorDispositivoProperties y son configurables (app.monitor.*).

Solo las terminales Smiley generan alerta por estar caídas

El job verificarTerminalesSinReportar solo itera TERMINAL_SMILEY. Un celular APP_OPERARIO en SIN_REPORTAR no genera DISPOSITIVO_SIN_REPORTAR: el operario puede estar offline a propósito entre turnos, y eso se gestiona por inactividad de SLA, no por conexión (decisión G-D4). Más sobre las terminales en Kiosk Smiley.

5. Tendencia negativa (Smiley)

La detección de tendencias vive en SmileyTendenciaService. Lo no obvio es cómo se delimita la ventana de análisis y qué opiniones cuentan.

Ventana de análisis

La ventana arranca en el más reciente entre:

  1. el fin_ts de la última limpieza válida del sanitario, y
  2. now - ventanaMin (el inicio "natural" de la ventana configurada).
// SmileyTendenciaService.java:191-195
Instant calcularVentanaInicio(Instant now, int ventanaMin, Instant ultimaLimpieza) {
    Instant porVentana = now.minusSeconds(ventanaMin * 60L);
    if (ultimaLimpieza == null) return porVentana;
    return ultimaLimpieza.isAfter(porVentana) ? ultimaLimpieza : porVentana;
}

La consecuencia: una limpieza reciente reinicia el conteo. Las negativas previas a esa limpieza dejan de pesar, porque ya fueron "respondidas" por la limpieza correctiva.

Qué opiniones cuentan

Las opiniones con origen = QR_PUBLICO se EXCLUYEN de las tendencias. Solo cuentan las de origen TERMINAL_SMILEY. Las consultas de OpinionRepository filtran explícitamente:

// OpinionRepository.java
"  AND o.origen = ar.com.lubeca.workdone.domain.enumeration.OrigenOpinion.TERMINAL_SMILEY "

ADR-041

Las opiniones por QR público son "de segunda clase" y nunca alimentan el motor de tendencias (regla de oro V22_QR_PUBLICO). Sí se contabilizan aparte para estadística, pero jamás disparan una TENDENCIA_NEGATIVA.

Para seguir


Resumen. WorkDone corre 7 jobs @Scheduled independientes que generan alertas y mantienen el estado sin depender de taps. El catálogo real son los 9 valores de TipoAlerta; cada alerta vive con generada_en/atendida_en (null = abierta), se deduplica por (tipo, sujeto, atendida_en IS NULL) y algunas el sistema las auto-atiende al normalizarse la condición. El estado de conexión se deriva de ultimo_contacto_ts con umbrales distintos para celular (30/120) y terminal (15/30). Para tendencias, la ventana arranca en la última limpieza válida y solo cuentan opiniones TERMINAL_SMILEY (ADR-041).