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
externalNamerpt_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
| Proyecto | Rol | Cambios para este reporte | Archivos clave | Rama / commit |
|---|---|---|---|---|
plataforma/cron-job-executer | Dispara el job: crea una Cloud Task por transportista | Job runDailyStatusVehiclesReport envía diaAlertas; cambio de horario a 00:45 y dia = ayer; fix del filtro transportistaId | index.js, src/taskjobs.js (dailyStatusVehiclesReport), src/tasks.js | a0dd5db "Fixes para poder cambiar la hora en reporte general" (rama status-vehicles-0045, mergeada a staging, no en master) |
plataforma/job-report-status-vehicles | Calcula y persiste estado de conexión y alertas GPS | Se agrega processingAlertasGps y la tabla reporteAlertasGps | index.js, src/reportStatusVehicles.js, src/alertasGps.js, src/sql.js | 3fc745b "Agrego reporte de alertas" (en staging y master) |
plataforma/apis (LoopBack) | Define el esquema de las tablas | Modelo nuevo reporteAlertasGps | common/models/reporte-alertas-gps.json, common/models/reporte-estado-vehiculos.json, server/model-config.json | 7438aa8f "Agrego modelo para alertas gps" (en staging y master) |
plataforma/go-query-layer | API de lectura del reporte | Rutas nuevas /api/reporting/* | pkg/router/reporting.go, pkg/http/reporting.go, pkg/dao/mysql/reporting-dto.go, pkg/dao/models/reporting.go | fe85304 "Subo reportes", 1bd27e4 "Agrego kms al response del drilldown", 856be0e "Agrego reporte gps" (en staging y master) |
visualizacion/dashboard-k2 | Vistas del reporte | Vistas, componentes, API client, ruta y card | src/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.js | a904a782d "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:
- Lee los transportistas de Redis (
transportistas), filtrados portransportistaIdsi vino en el mensaje. - 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 traigadayy/odiaAlertas.- 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 acepta5o"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íadiaAlertas, así que el job calcula las alertas dedia - 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_deletedy excluyendo lostipodvr = APP. - Última conexión:
ultimaConexionde Redis (posicionVehiculos); si no está, el últimofechaGPSenincidentesde los últimos 2 meses. - Clasificación contra el momento en que corre el job:
status | Nombre | Regla |
|---|---|---|
| 1 | CHEQUEADO | última conexión ≤ 24 hs |
| 2 | REVISION | entre 24 y 72 hs |
| 0 | DESCONECTADO | > 72 hs, o sin ninguna conexión |
| 3 | NOTIFICADO | era 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
resetDVRenplacasCmd, siempre que la unidad tenga placa y no haya ya unresetDVRpendiente sin ack.
2. processingAlertasGps(diaAlertas): alertas de eventos GPS
- Ventana = día calendario en
America/Argentina/Cordobaconvertido a UTC. Los eventos se cuentan por fecha de recepción (incidentes.fecha) dentro del día.fechaGPSse usa para acotar la búsqueda (índice) y para el contexto de losLAG. - 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
fechaGPSatrasada (hasta 60 días). - Reglas (gana la severidad más alta y se acumulan todos los textos):
| Severidad | Condición |
|---|---|
| CRITICA | ≥ 20 reportes por tiempo, ninguno con velocidad > 0 y > 5 km por coordenadas |
| CRITICA | 0 reportes por tiempo pero con eventos de contacto |
| VALIDAR | Contacto ON u OFF faltante |
| VALIDAR | Hueco > 30 min sin reportes con desplazamiento > 2 km |
| VALIDAR | Eventos 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:
DELETEde(dia, transportistaId)y despuésINSERT ... 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 elresetDVRsi 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
}
| Campo | Efecto |
|---|---|
day | Día del estado (dia). Con el código nuevo, si se omite se usa ayer ART. |
diaAlertas | Dí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. |
transportistaId | Opcional. Limita a una empresa (el filtro funciona recién con el código nuevo). |
dryRun | Opcional. El job loguea las queries pero no escribe nada (tampoco crea resetDVR). |
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
| Columna | Tipo | Nota |
|---|---|---|
dia | DATE | día reportado |
status | TINYINT | 0 DESCONECTADO, 1 CHEQUEADO, 2 REVISION, 3 NOTIFICADO |
fechaUltimaConexion | DATETIME(3) | UTC, puede ser NULL |
transportistaId, vehiculoId | FK |
Clave única: index_dia_transp_vehiculo (dia, transportistaId, vehiculoId).
reporteAlertasGps
| Columna | Tipo | Nota |
|---|---|---|
dia | DATE | día de recepción de los eventos (ART) |
severidad | VARCHAR(10) | CRITICA, VALIDAR, INFO |
alertas | JSON | array de textos legibles |
contactoOn, contactoOff, inicioDetenido, finDetenido | INT | conteos del día |
faltaContactoOn, faltaContactoOff | INT | |
reportesPorTiempo, reportesVelocidadPositiva | INT | |
kmCoordenadas | DOUBLE | |
kmOdometro | INT | puede ser NULL |
maxHuecoMovimientoMin | INT | |
atrasados, atrasadosDesde, atrasadosHasta | INT / DATETIME(3) | eventos atrasados y rango de su fechaGPS |
fechaCalculo | DATETIME(3) | UTC |
transportistaId, vehiculoId | FK |
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.
| Ruta | Body | Tabla(s) origen | Devuelve |
|---|---|---|---|
/kpi | fechaFrom, fechaTo, transportistaIds | vehiculos, choferes, viajes | unidades, choferes, recorridos (viajes por fechaCarga en el rango) |
/estadoVehiculos | ídem | reporteEstadoVehiculos | fechas (DD/MM) y por vehículo dias[] (color por día), diasConProblema, estadoGeneral |
/rankingIsp | ídem + limit (default 10, máx. 50) | icmDiario, kilometrosChofer | top choferes por ISP con nivelRiesgo y detalle |
/rankingIcm | ídem + limit (default 10, máx. 50) | icmDiarioPorUnidad, kilometrosUnidad | top unidades por ICM con nivelRiesgo, kmPorDia y detalle |
/jornada | ídem + limit (máx. 500) | icmDiario | horas de manejo por chofer; primero los que superaron 12 hs en algún día. total = cantidad que superó el umbral |
/drilldown | vehiculoId, fecha | incidentes, kilometrosUnidad | conteos 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 |
/alertasGps | fechaFrom, fechaTo, transportistaIds | reporteAlertasGps | resumen (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 headerX-Session-Info(base64) del que sale elUserFilter(rol, grupo, transportistas del grupo). transportistaIdsdel 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
/drilldownexcluyen empresas y unidades con "baja", "prueba" o "test" en el nombre./estadoVehiculosy/rankingIcmademás excluyen las unidadesAPP.
Validaciones
- El decoder usa
DisallowUnknownFields: cualquier clave de más en el body da 400. Por esosrc/api/reporting.jsarma cada body a mano con solo las claves documentadas. fechaFromyfechaToen formatoYYYY-MM-DD, inclusivas, confechaTo >= fechaFromy 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, comparafechaUltimaConexioncon el fin del día consultado: < 24 hsY, 24–48 hsO, > 48 hsR, reconectó después del díaG. Sin dato →X. Los días sin fila también sonX.estadoGeneral:Rsolo si los últimos 2 o más días seguidos sonR; si no, gana el peor color de la fila, y unRaislado cuenta comoO.diasConProblemacuenta todo lo que no esG, incluidoX.nivelRiesgo: ICM ≥ 80 CRÍTICO, ≥ 60 ALTO, ≥ 30 MODERADO. ISP > 25 ALTO, > 7 MODERADO (misma escala queISP.vue).anomaliasdel drill-down: Contacto ON ≠ OFF, Inicio ≠ Fin detenido, sin reportes por tiempo con eventos de contacto.resumende alertas GPS: conteo de filas por severidad.
Front (dashboard-k2)
| Elemento | Detalle |
|---|---|
| Card | src/views/Reports.vue, "Estado General", externalName: rpt_estado_general |
| Rutas | src/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 client | src/api/reporting.js (http_query, un método por endpoint) |
| Componentes | src/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,icmyjornadason 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 →
DrilldownModalcon/reporting/drilldownpara ese vehículo y día. - Botón "Alertas GPS" → navega a la sub-vista pasando
desde,hastaytransportistapor 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) condominioTractory el día completo, visible solo con permisorpt_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.
colorSemaforoarma 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 daG, 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
fechaGPSdel 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.
/drilldowntoma los eventos confechaGPSen[fecha 00:00, fecha+1 00:00)UTC, que no es exactamente el día ART que muestra el semáforo. kpi.unidadesykpi.choferesno dependen de las fechas. Son el total actual de unidades y choferes en alcance. Solorecorridosusa 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,
alertasGpsresponde 500 pero con un body válido (resumenen 0 yalertas: []). Lo mismo pasa enestadoVehiculos, los rankings yjornada. 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.
estadoVehiculosmuestraXyalertasGpsdevuelve vacío para los días en que el job no corrió o falló.