feat(modo-local): fallback completo sin Monday + cola de sincronización

- Circuit breaker saludMonday (3 fallos → modo local; ping cada 60s)
- Endpoint GET /api/sistema/estado {mondayOk, pendientesSync}
- LeadCaptura extendida (board, etapa, origen, motivo, pendienteSync)
- Tablas Unidad (espejo inventario, 288 seed) + SyncPendiente
- crmLocal: crearLead, afluencia, cambio etapa, descarte, conversión, apartar
- Ruteo en controllers: fallback local ante 502 o Monday caído
- Mi día local: resumen completo desde SQLite + unión con Monday
- Fallbacks: unidades, contactos, reportes, prefill, buscador
- Sync worker cada 2min + handlers 15 tipos (con DependenciaPendienteError)
- Gate automatizaciones: skip si !mondayOk
- Banner 'Modo local' en 5 apps + badge pendiente en mi-dia
- 597 API tests + 263 front tests verdes

Env MODO_LOCAL_FORZADO=true para testing sin Monday.
This commit is contained in:
2026-09-06 08:08:56 +00:00
parent af2fb10956
commit 60649890eb
70 changed files with 8763 additions and 240 deletions

View File

@@ -2,10 +2,44 @@ import cron from 'node-cron'
import { crearApp } from './app.js'
import { config } from './config/index.js'
import { automatizacionesService } from './services/automatizaciones.service.js'
import { mondayService } from './services/monday.service.js'
import { recordatoriosCobranzaService } from './services/recordatorios-cobranza.service.js'
import { saludMonday } from './services/salud-monday.service.js'
import { syncMondayService } from './services/sync-monday.service.js'
// Registra los handlers de cada tipo de SyncPendiente sobre el worker (efecto lateral)
import './services/sync-handlers.js'
const app = crearApp()
// Wiring del circuit breaker: el ping vive en monday.service; se inyecta aquí
// para que salud-monday.service no lo importe (ciclo de imports).
saludMonday.registrarPing(() => mondayService.ping())
// Sondeo de salud de Monday (cada 60 s): tras FALLOS_PARA_CAER fallos GraphQL
// seguidos el flag cae y la app opera en modo local; cuando el ping vuelve a
// responder se recupera el flag y se drena la cola de sincronización.
// Corre siempre (aunque MONDAY_AUTOMATIONS_ENABLED=false): es la infra del modo local.
cron.schedule('* * * * *', () => {
void (async () => {
if (process.env.MODO_LOCAL_FORZADO === 'true') return // modo local forzado: no sondear
if (saludMonday.estaDisponible()) return // Monday sano: nada que hacer
const recuperado = await saludMonday.sondear()
if (recuperado && saludMonday.estaDisponible()) {
// eslint-disable-next-line no-console
console.log('[salud] Monday recuperado; drenando cola de sincronización')
void syncMondayService.procesarPendientes() // fire-and-forget (mutex interno)
}
})().catch((err: unknown) => console.error('[cron salud] error en sondeo:', err))
})
// Sync worker del modo local (cada 2 min): drena SyncPendiente FIFO hacia
// Monday. No-op mientras Monday esté caído. Secuencial por diseño (SQLite).
cron.schedule('*/2 * * * *', () => {
syncMondayService
.procesarPendientes()
.catch((err: unknown) => console.error('[cron sync] error en la pasada:', err))
})
// Crons de respaldo: los webhooks de Monday (registrados sobre
// https://frasa.consultoria-as.com/api/webhooks/monday) disparan estas mismas
// reglas en tiempo real; estos barridos quedan como red de seguridad por si un
@@ -18,6 +52,12 @@ if (config.automatizaciones.enabled) {
// escaneos en paralelo provoca timeouts P1008 en AutomatizacionState.
cron.schedule(config.automatizaciones.pollCron, () => {
void (async () => {
// Modo local: Monday caído → saltar las reglas sin ruido (fallan seguro de todos modos)
if (!saludMonday.estaDisponible()) {
// eslint-disable-next-line no-console
console.debug('[cron poll] Monday no disponible (modo local); pasada omitida')
return
}
try {
const r = await automatizacionesService.runRoundRobin()
if (r.asignados > 0) {
@@ -55,6 +95,11 @@ if (config.automatizaciones.enabled) {
// Escaneo diario de leads inactivos (recordatorio vía update en el item)
cron.schedule(config.automatizaciones.recordatorioCron, () => {
if (!saludMonday.estaDisponible()) {
// eslint-disable-next-line no-console
console.debug('[cron inactividad] Monday no disponible (modo local); escaneo omitido')
return
}
automatizacionesService
.escaneoInactividad({ preview: false })
.then((r) => {