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_tsal máximo deapp.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_cierreal 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 sí 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ó.nullsignifica 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 null → NUNCA_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:
- el
fin_tsde la última limpieza válida del sanitario, y 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¶
- Reglas operativas — cierres automáticos, SLA y semántica de los taps.
- Modelo de datos — entidad
Alerta, enums y relaciones. - Kiosk Smiley — terminales, opiniones y orígenes.
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).