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

@@ -1,7 +1,9 @@
import type { Request, Response } from 'express'
import { prisma } from '../services/prisma.js'
import { mondayService } from '../services/monday.service.js'
import { resumenMiDia } from '../services/mi-dia.service.js'
import { resumenMiDia, resumenMiDiaLocal, unirPendientesLocales } from '../services/mi-dia.service.js'
import { saludMonday } from '../services/salud-monday.service.js'
import { esFalloMonday } from '../services/sync-monday.service.js'
import { config } from '../config/index.js'
import { AppError } from '../utils/errors.js'
@@ -62,16 +64,30 @@ export async function resumenMiDiaEndpoint(req: Request, res: Response): Promise
orderBy: { createdAt: 'desc' },
})
res.json(
await resumenMiDia(
const usuarioMiDia = { nombre: usuario.nombre, rol: usuario.rol, mondayUserId: usuario.mondayUserId }
// Monday caído (circuit breaker / MODO_LOCAL_FORZADO): todo desde local
if (!saludMonday.estaDisponible()) {
res.json(resumenMiDiaLocal(usuarioMiDia, capturas, clientes))
return
}
try {
const resumen = await resumenMiDia(
mondayService,
{ nombre: usuario.nombre, rol: usuario.rol, mondayUserId: usuario.mondayUserId },
usuarioMiDia,
nombresPorMondayId,
capturas,
clientes,
new Date(),
config.automatizaciones.vendedorCompartidoId > 0 ? config.automatizaciones.vendedorCompartidoId : null,
capturadosIds,
),
)
)
// Unión: capturas locales aún no sincronizadas se agregan con badge ⏳
res.json(unirPendientesLocales(resumen, capturas.filter((c) => c.pendienteSync), usuario.nombre))
} catch (err) {
// Monday se cayó a media lectura: mismo resumen, pero todo local
if (!esFalloMonday(err)) throw err
res.json(resumenMiDiaLocal(usuarioMiDia, capturas, clientes))
}
}