Saltar al contenido principal

Reporte Estado General (arquitectura)

El reporte Estado General muestra, para un rango de fechas y una empresa (o todas), el semáforo de conectividad de las unidades, KPIs del período, rankings de riesgo (ISP por chofer, ICM por unidad) y la jornada laboral de los choferes. Tiene una sub-vista Alertas GPS con las inconsistencias de eventos GPS por vehículo y día.

  • Vistas: /reportes/generalStatus (Estado General) y /reportes/generalStatus/alertasGps (Alertas GPS).
  • Permiso: las dos rutas usan el externalName rpt_estado_general.
  • Origen: es un port a la plataforma de los reportes que generaba silstech-reporte (PDF/HTML diario). Las reglas de semáforo, riesgo y alertas replican las de esos scripts.

Los datos del semáforo y de las alertas los precalcula un job nocturno y se guardan en MySQL. El resto (KPIs, rankings, jornada, drill-down) se calcula on-line contra tablas que ya existían.


Diagrama de flujo​

PROCESAMIENTO NOCTURNO (1 vez por día)
─────────────────────────────────────

┌──────────────────┐ Pub/Sub ┌─────────────────────────────┐
│ Cloud Scheduler │ ───────────▶ │ cron-job-executer │
│ (00:45 ART *) │ {job: ...} │ cloudEvent cronJobsExecuter │
└──────────────────┘ │ job runDailyStatusVehicles- │
│ Report │
└──────────────┬──────────────┘
│ 1 Cloud Task por transportista
│ (lista desde Redis `transportistas`)
│ body {transportistaId, dia, diaAlertas}
▼
┌─────────────────────────────┐
│ Cloud Tasks │
└──────────────┬──────────────┘
│ HTTP POST
▼
┌─────────────────────────────┐
Redis: vehiculos, ─────────▶ │ job-report-status-vehicles │
posicionVehiculos │ (Cloud Run function) │
MySQL: incidentes, ─────────▶ │ 1. processingStatus │
notificaciones, avisos, │ 2. processingAlertasGps │
turnos └──────────────┬──────────────┘
│ escribe
▼
┌────────────────────────────────────────────────┐
│ MySQL │
│ reporteEstadoVehiculos (upsert) │
│ reporteAlertasGps (DELETE + INSERT) │
│ placasCmd (resetDVR si REVISION)│
└────────────────────────────────────────────────┘
▲
CONSULTA ON-LINE (cada vez que se abre el reporte)│ lee (+ icmDiario, icmDiarioPorUnidad,
──────────────────────────────────────────────────│ kilometrosChofer/Unidad, viajes,
│ incidentes, vehiculos, choferes)
┌──────────────┴──────────────┐
│ go-query-layer │
│ POST /api/reporting/* │
│ (Firebase + X-Session-Info) │
└──────────────▲──────────────┘
│ http_query (axios)
┌──────────────┴──────────────┐
│ dashboard-k2 │
│ GeneralStatus.vue │
│ GpsAlerts.vue │
└─────────────────────────────┘

(*) 00:45 ART es el horario nuevo, pendiente de deploy a producción
(ver "Procesamiento nocturno"). Antes: 23:00 ART.

Request desde el navegador​

Navegador (GeneralStatus.vue) go-query-layer MySQL
───────────────────────────── ────────────── ─────
click "Buscar"
├─ POST /reporting/kpi ───────────▶ auth Firebase (Bearer)
├─ POST /reporting/estadoVehiculos ▶ X-Session-Info → UserFilter
├─ POST /reporting/rankingIsp ────▶ valida body (fechas, rango)
├─ POST /reporting/rankingIcm ────▶ intersección de transportistas
└─ POST /reporting/jornada ───────▶ query ─────────────────────────────▶ SELECT
(5 requests en paralelo, ◀──── deriva semáforo / riesgo ◀──── filas
cada bloque se pinta solo)
click en una celda del semáforo
└─ POST /reporting/drilldown ─────▶ ... ───────────────────────────────▶ incidentes
botón "Alertas GPS" → GpsAlerts.vue
└─ POST /reporting/alertasGps ────▶ ... ───────────────────────────────▶ reporteAlertasGps

Proyectos involucrados​

ProyectoRolCambios para este reporteArchivos claveRama / commit
plataforma/cron-job-executerDispara el job: crea una Cloud Task por transportistaJob runDailyStatusVehiclesReport envía diaAlertas; cambio de horario a 00:45 y dia = ayer; fix del filtro transportistaIdindex.js, src/taskjobs.js (dailyStatusVehiclesReport), src/tasks.jsa0dd5db "Fixes para poder cambiar la hora en reporte general" (rama status-vehicles-0045, mergeada a staging, no en master)
plataforma/job-report-status-vehiclesCalcula y persiste estado de conexión y alertas GPSSe agrega processingAlertasGps y la tabla reporteAlertasGpsindex.js, src/reportStatusVehicles.js, src/alertasGps.js, src/sql.js3fc745b "Agrego reporte de alertas" (en staging y master)
plataforma/apis (LoopBack)Define el esquema de las tablasModelo nuevo reporteAlertasGpscommon/models/reporte-alertas-gps.json, common/models/reporte-estado-vehiculos.json, server/model-config.json7438aa8f "Agrego modelo para alertas gps" (en staging y master)
plataforma/go-query-layerAPI de lectura del reporteRutas nuevas /api/reporting/*pkg/router/reporting.go, pkg/http/reporting.go, pkg/dao/mysql/reporting-dto.go, pkg/dao/models/reporting.gofe85304 "Subo reportes", 1bd27e4 "Agrego kms al response del drilldown", 856be0e "Agrego reporte gps" (en staging y master)
visualizacion/dashboard-k2Vistas del reporteVistas, componentes, API client, ruta y cardsrc/api/reporting.js, src/views/Reports/GeneralStatus.vue, src/views/Reports/GpsAlerts.vue, src/components/reports/general-status/, src/views/Reports.vue, src/router/router-config.jsa904a782d "Agrego reporte parte 1", 18bb8fe9d "Agrego kms al drilldown", f4263db69 "Agrego alertas" y ajustes posteriores (rama reporte-estado-general, mergeada a staging)

El estado de las ramas es el de los repos locales al momento de escribir este doc; puede haber cambiado.


Procesamiento nocturno​

Disparo (cron-job-executer)​

El Cloud Scheduler publica en el topic del cron-job-executer un mensaje con job: "runDailyStatusVehiclesReport". La función (cronJobsExecuter, cloudEvent de Pub/Sub) decodifica el body y llama a dailyStatusVehiclesReport, que:

  1. Lee los transportistas de Redis (transportistas), filtrados por transportistaId si vino en el mensaje.
  2. Crea una Cloud Task por transportista contra el job (JOB_STATUS_VEHICLES_URL) con body {transportistaId, dia, diaAlertas, ...extra}.

La configuración del Cloud Scheduler (horario y payload) no está versionada en el repo; se configura en GCP.

Comportamiento nuevo (pendiente de deploy, rama status-vehicles-0045)​

  • El scheduler pasa a correr a las 00:45 ART y procesa el día que acaba de terminar.
  • dia = diaAlertas = ayer en ART (UTC-3), salvo que el mensaje traiga day y/o diaAlertas.
  • Se corrige el filtro por transportistaId: antes no se pasaba a la función y se creaban tareas para todas las empresas; además la comparación ahora es por string, así que acepta 5 o "5".

Está mergeado en staging pero no en master, y el cambio de horario del Cloud Scheduler hay que aplicarlo aparte.

Comportamiento anterior (el que sigue en producción hasta el deploy)​

  • El scheduler corre a las 23:00 ART.
  • dia = hoy (fecha ART) y no se envía diaAlertas, así que el job calcula las alertas de dia - 1 (su default).

Qué calcula el job (job-report-status-vehicles)​

Cloud Run function HTTP jobReportStatusVehicles. Recibe {transportistaId, dia, diaAlertas, dryRun}; transportistaId es obligatorio (si falta, 400). Si no viene dia usa la fecha UTC actual; si no viene diaAlertas usa dia - 1.

1. processingStatus(dia): estado de conexión por vehículo

  • Vehículos del transportista desde Redis (vehiculos), sin _deleted y excluyendo los tipodvr = APP.
  • Última conexión: ultimaConexion de Redis (posicionVehiculos); si no está, el último fechaGPS en incidentes de los últimos 2 meses.
  • Clasificación contra el momento en que corre el job:
statusNombreRegla
1CHEQUEADOúltima conexión ≤ 24 hs
2REVISIONentre 24 y 72 hs
0DESCONECTADO> 72 hs, o sin ninguna conexión
3NOTIFICADOera REVISION o DESCONECTADO pero tiene notificación de módulo técnico NOTIFICADO, aviso SIN_CONEXION programado vigente o turno abierto con falla SIN_CONEXION
  • Upsert en reporteEstadoVehiculos (INSERT ... ON DUPLICATE KEY UPDATE status, fechaUltimaConexion).
  • Si queda en REVISION, inserta un comando resetDVR en placasCmd, siempre que la unidad tenga placa y no haya ya un resetDVR pendiente sin ack.

2. processingAlertasGps(diaAlertas): alertas de eventos GPS

  • Ventana = día calendario en America/Argentina/Cordoba convertido a UTC. Los eventos se cuentan por fecha de recepción (incidentes.fecha) dentro del día. fechaGPS se usa para acotar la búsqueda (índice) y para el contexto de los LAG.
  • Tipos de evento: Contacto ON (51), Contacto OFF (57), Inicio detenido (99), Fin detenido (153), Reporte por tiempo (107).
  • Métricas por vehículo: cantidad de reportes por tiempo y cuántos con velocidad > 0, km por coordenadas y por odómetro, máximo hueco sin reportes con la unidad en movimiento, contactos ON/OFF faltantes (dos OFF o dos ON seguidos con > 1 km entre ellos) y eventos recibidos en el día con fechaGPS atrasada (hasta 60 días).
  • Reglas (gana la severidad más alta y se acumulan todos los textos):
SeveridadCondición
CRITICA≥ 20 reportes por tiempo, ninguno con velocidad > 0 y > 5 km por coordenadas
CRITICA0 reportes por tiempo pero con eventos de contacto
VALIDARContacto ON u OFF faltante
VALIDARHueco > 30 min sin reportes con desplazamiento > 2 km
VALIDAREventos recibidos con atraso
INFO≥ 3 Fin de detenido sin ningún Inicio
  • Solo se guardan los vehículos con alguna alerta. Se excluyen unidades o empresas con "baja" en el nombre.
  • Escritura: DELETE de (dia, transportistaId) y después INSERT ... ON DUPLICATE KEY UPDATE.
  • Si falla, se loguea y no se corta el job: el estado de vehículos ya quedó grabado.

Idempotencia​

  • reporteEstadoVehiculos: upsert por (dia, transportistaId, vehiculoId). Reprocesar sobreescribe el status.
  • reporteAlertasGps: delete + insert por (dia, transportistaId). Un reproceso no deja alertas viejas de vehículos que ya no las tienen.
  • placasCmd: no duplica el resetDVR si ya hay uno pendiente.

Reprocesos manuales​

Publicar en el topic del cron-job-executer:

{
"job": "runDailyStatusVehiclesReport",
"day": "2026-10-12",
"diaAlertas": "2026-10-12",
"transportistaId": 5,
"dryRun": true
}
CampoEfecto
dayDía del estado (dia). Con el código nuevo, si se omite se usa ayer ART.
diaAlertasDía de las alertas. Con el código nuevo, si se omite se usa day (o ayer). Con el código anterior, el job usaba dia - 1.
transportistaIdOpcional. Limita a una empresa (el filtro funciona recién con el código nuevo).
dryRunOpcional. El job loguea las queries pero no escribe nada (tampoco crea resetDVR).
precaución

El status de reporteEstadoVehiculos se calcula contra la hora actual, no contra el día pedido. Si se reprocesa un día viejo, el estado que queda grabado para ese día es el de la conectividad de hoy. Las alertas GPS sí se recalculan correctamente para el día pedido.


Tablas​

Ambas se definen como modelos LoopBack en apis (datasource administracion, sin endpoints REST expuestos) y se crean o actualizan con el endpoint de automigrate de apis (server/utils/automigrate.js, que hace dataSource.autoupdate del modelo).

reporteEstadoVehiculos​

ColumnaTipoNota
diaDATEdía reportado
statusTINYINT0 DESCONECTADO, 1 CHEQUEADO, 2 REVISION, 3 NOTIFICADO
fechaUltimaConexionDATETIME(3)UTC, puede ser NULL
transportistaId, vehiculoIdFK

Clave única: index_dia_transp_vehiculo (dia, transportistaId, vehiculoId).

reporteAlertasGps​

ColumnaTipoNota
diaDATEdía de recepción de los eventos (ART)
severidadVARCHAR(10)CRITICA, VALIDAR, INFO
alertasJSONarray de textos legibles
contactoOn, contactoOff, inicioDetenido, finDetenidoINTconteos del día
faltaContactoOn, faltaContactoOffINT
reportesPorTiempo, reportesVelocidadPositivaINT
kmCoordenadasDOUBLE
kmOdometroINTpuede ser NULL
maxHuecoMovimientoMinINT
atrasados, atrasadosDesde, atrasadosHastaINT / DATETIME(3)eventos atrasados y rango de su fechaGPS
fechaCalculoDATETIME(3)UTC
transportistaId, vehiculoIdFK

Clave única: index_dia_transp_vehiculo (dia, transportistaId, vehiculoId).


API de reporting (go-query-layer)​

Todas las rutas son POST bajo /api/reporting. El front las llama con la instancia http_query.

RutaBodyTabla(s) origenDevuelve
/kpifechaFrom, fechaTo, transportistaIdsvehiculos, choferes, viajesunidades, choferes, recorridos (viajes por fechaCarga en el rango)
/estadoVehiculosídemreporteEstadoVehiculosfechas (DD/MM) y por vehículo dias[] (color por día), diasConProblema, estadoGeneral
/rankingIspídem + limit (default 10, máx. 50)icmDiario, kilometrosChofertop choferes por ISP con nivelRiesgo y detalle
/rankingIcmídem + limit (default 10, máx. 50)icmDiarioPorUnidad, kilometrosUnidadtop unidades por ICM con nivelRiesgo, kmPorDia y detalle
/jornadaídem + limit (máx. 500)icmDiariohoras de manejo por chofer; primero los que superaron 12 hs en algún día. total = cantidad que superó el umbral
/drilldownvehiculoId, fechaincidentes, kilometrosUnidadconteos de eventos GPS del día, viajes, km, choferes, primer y último evento, anomalias[]. 404 si el vehículo no existe o está fuera de alcance
/alertasGpsfechaFrom, fechaTo, transportistaIdsreporteAlertasGpsresumen (CRITICA/VALIDAR/INFO, siempre con las 3 claves) y alertas[] ordenadas por día desc, severidad y empresa/dominio

Autenticación y alcance​

  • Middlewares globales: token de Firebase en Authorization: Bearer ... y header X-Session-Info (base64) del que sale el UserFilter (rol, grupo, transportistas del grupo).
  • transportistaIds del body se intersecta con los transportistas del grupo del usuario. Lista vacía o "0" significa todas las del alcance. Si la intersección queda vacía, el endpoint responde 200 con un resultado vacío.
  • Todos los endpoints salvo /drilldown excluyen empresas y unidades con "baja", "prueba" o "test" en el nombre. /estadoVehiculos y /rankingIcm además excluyen las unidades APP.

Validaciones​

  • El decoder usa DisallowUnknownFields: cualquier clave de más en el body da 400. Por eso src/api/reporting.js arma cada body a mano con solo las claves documentadas.
  • fechaFrom y fechaTo en formato YYYY-MM-DD, inclusivas, con fechaTo >= fechaFrom y un rango menor a 93 días. El front limita a 92 días de diferencia.

Derivaciones del lado servidor (pkg/http/reporting.go)​

  • colorSemaforo: status = 1 → G. Si no, compara fechaUltimaConexion con el fin del día consultado: < 24 hs Y, 24–48 hs O, > 48 hs R, reconectó después del día G. Sin dato → X. Los días sin fila también son X.
  • estadoGeneral: R solo si los últimos 2 o más días seguidos son R; si no, gana el peor color de la fila, y un R aislado cuenta como O. diasConProblema cuenta todo lo que no es G, incluido X.
  • nivelRiesgo: ICM ≥ 80 CRÍTICO, ≥ 60 ALTO, ≥ 30 MODERADO. ISP > 25 ALTO, > 7 MODERADO (misma escala que ISP.vue).
  • anomalias del drill-down: Contacto ON ≠ OFF, Inicio ≠ Fin detenido, sin reportes por tiempo con eventos de contacto.
  • resumen de alertas GPS: conteo de filas por severidad.

Front (dashboard-k2)​

ElementoDetalle
Cardsrc/views/Reports.vue, "Estado General", externalName: rpt_estado_general
Rutassrc/router/router-config.js: reportes/generalStatus → GeneralStatus.vue y reportes/generalStatus/alertasGps → GpsAlerts.vue. Las dos con el mismo permiso; la de alertas va después para que getFirstAvailablePath resuelva la principal
API clientsrc/api/reporting.js (http_query, un método por endpoint)
Componentessrc/components/reports/general-status/: KpiPanel, AlertCards, VehicleStatusGrid, RankingCard, WorkdayTable, DrilldownModal, GpsAlertCounters, GpsAlertsTable, GpsPlatformPatternTable, StatCard, constants.js

GeneralStatus.vue

  • Filtros: rango de fechas (default últimos 7 días hasta ayer, máximo ayer) y empresa ("Todas las empresas" para usuarios con varias). Se aplican recién con "Buscar".
  • Carga independiente por bloque: kpi, estado, isp, icm y jornada son 5 requests en paralelo, cada uno con su propio loading, error y reintento. Si uno falla, el resto de la página funciona igual. Las respuestas viejas se descartan por un contador de request por bloque.
  • Click en una celda del semáforo → DrilldownModal con /reporting/drilldown para ese vehículo y día.
  • Botón "Alertas GPS" → navega a la sub-vista pasando desde, hasta y transportista por query string (y al volver, los mismos).

GpsAlerts.vue

  • Un solo request (/reporting/alertasGps). Default: solo ayer.
  • Secciones: contadores por severidad, tabla Críticas, tabla A validar y Patrón de plataforma (INFO, Fin de detenido sin Inicio, agrupado). Exporta CSV.
  • Cada fila abre en otra pestaña ReportActions (/reportes/acciones) con dominioTractor y el día completo, visible solo con permiso rpt_hist_busqueda.

Otros lectores de reporteEstadoVehiculos​

La tabla ya existía antes de este reporte y la sigue leyendo el reporte Estado de vehículos (src/views/Reports/VehicleStatus.vue, permiso rpt_estado_vehiculos) vía src/api/technicalMonitoring.js → go-query-layer POST /api/technical/statusVehicle y /api/technical/counterStatusVehicle (pkg/dao/mysql/technicaldto.go). Cualquier cambio en el job o en la tabla impacta en los dos reportes.


Consideraciones conocidas​

  • El semáforo usa fin de día UTC. colorSemaforo arma el fin del día con la fecha en UTC (23:59:59 UTC = 20:59:59 ART). Una conexión entre las 21:00 y las 23:59 ART se considera "después del día" y da G, y las horas sin señal se miden hasta las 21:00 ART, no hasta medianoche.
  • Las alertas cuentan por fecha de recepción. Un evento con fechaGPS del día D que llega después de las 00:00 ART de D+1 no cuenta para D; cuenta como "atrasado" en D+1. Por eso el job corre después de medianoche y el front no deja elegir el día de hoy.
  • El drill-down filtra por día UTC. /drilldown toma los eventos con fechaGPS en [fecha 00:00, fecha+1 00:00) UTC, que no es exactamente el día ART que muestra el semáforo.
  • kpi.unidades y kpi.choferes no dependen de las fechas. Son el total actual de unidades y choferes en alcance. Solo recorridos usa el rango.
  • El status es "a la hora en que corrió el job". Con el horario nuevo, el estado del día D refleja la conectividad a las 00:45 de D+1. En reprocesos, refleja la de la hora del reproceso.
  • Error de DB → 500 con JSON vacío. Ante un error de base, alertasGps responde 500 pero con un body válido (resumen en 0 y alertas: []). Lo mismo pasa en estadoVehiculos, los rankings y jornada. El front lo trata como error porque axios rechaza el 500, pero un cliente que solo mire el body vería "sin datos".
  • Sin datos para días no procesados. estadoVehiculos muestra X y alertasGps devuelve vacío para los días en que el job no corrió o falló.