Saltar al contenido principal

Reportes

Los reportes archivan las operaciones del sistema —el registro diario, el balance de masa, el inventario, los movimientos— y la auditoría como documentos PDF/xlsx descargables y verificables. Cada reporte se genera en background y se notifica en tiempo real cuando está listo; no hay vista previa dentro de la aplicación —el reporte es el PDF o el XLSX externo, no una pantalla—. Esta página está orientada al supervisor y al administrador: cómo generar, descargar y verificar un reporte paso a paso. El visor de auditoría (la pantalla que registra quién cambió qué) se documenta en la hermana auditoría.

Quién genera y quién exporta

RolPuede
operador · supervisorVer y generar reportes operacionales (reports:read, reports:generate)
administradorVer y generar reportes (reports:read, reports:generate)
administrador (único)Exportar el log de auditoría en CSV (admin:system) — el tipo AUDIT_CSV
El engineer NO ve Reportes

El ingeniero no posee reports:read, así que el ítem "Reportes" no aparece en su menú. El export del audit log en CSV es exclusivo del administrador (admin:system); el resto de los tipos los puede generar también el operador y el supervisor.

Los 5 tipos de reporte

TipoQué archivaQuién
DAILY_LOGRegistro diario de operaciones del tanqueoperador / supervisor / admin
MASS_BALANCEBalance de masa por períodooperador / supervisor / admin
INVENTORYInventario de productos por tanque o por sitiooperador / supervisor / admin
MOVEMENTMovimientos de entrada y salida por períodooperador / supervisor / admin
AUDIT_CSVExportación del log de auditoríaadmin (único) — ver auditoría

Los cuatro primeros son reportes operacionales: cualquier rol con reports:generate los puede crear. El quinto, AUDIT_CSV, es el export del registro de auditoría y es admin-only: corresponde al mismo artefacto que se documenta en la página de auditoría.

Generar un reporte — paso a paso

  1. Abrir la página de reportes. Ruta /reports. Es un Server Component (RSC) que pre-cachea la lista inicial de reportes archivados (ArchivedReport) con sus metadatos ya resueltos (scopeLabel, generatedByLabel).
  2. Pulsar "Generar reporte". El CTA abre el GenerateDrawer: un formulario validado contra GenerateReportRequestSchema (Zod). Allí se elige el tipo, el scope (tanque, producto, sitio o tipo de movimiento) y el rango de fechas (periodStart / periodEnd).
  3. Solicitar la generación. Al confirmar, el formulario hace POST /api/v1/reports/generate (autoridad reports:generate). La respuesta es 202 Accepted con el DTO del ArchivedReport y un flag idempotent (ver abajo) — el reporte se procesa después, no de forma síncrona.
  4. El job procesa en background. El reporte entra en una cola por tipo y un worker ejecuta la query, genera el PDF y el XLSX y los sube a almacenamiento (ver Procesamiento y estados).
  5. Notificación en tiempo real. Cuando el worker termina, el sistema avisa al usuario y la lista se refresca sola (ver Procesamiento y estados).
  6. Descargar y verificar. Una vez listo (READY), el reporte se descarga en PDF o XLSX y su integridad se puede verificar con SHA-256 (ver Descargar y verificar).
El período está acotado a 366 días — protección anti-DoS

GenerateReportRequestSchema aplica un clamp de 366 días sobre el rango (periodEnd − periodStart). Un rango más largo se rechaza en la validación: evita que una solicitud arrastre meses de telemetría y sature el worker. Si necesitas un horizonte mayor, divídelo en varios reportes consecutivos.

Captura pendiente

Screenshot pendiente del barrido diferido — ver 61-DEFERRED-SWEEP.md (/reports GenerateDrawer abierto (tipo/scope/rango)).

Idempotencia por scopeHash — no se duplica el trabajo

Mismo tipo + mismo scope + mismo período → te devuelve el reporte que ya existe

Cuando solicitas un reporte, el sistema calcula su scopeHash —el SHA-256 del scope— y busca si ya existe un reporte con la misma combinación (type, scopeHash, periodStart, periodEnd) que no haya sido reemplazado (supersededById nulo). Si lo encuentra, te devuelve el existente con el flag idempotent: true en lugar de re-ejecutar la query: dos personas que pidan "el balance de masa del tanque TK-101 de mayo" obtienen exactamente el mismo documento, sin trabajo duplicado.

El scopeHash es una columna GENERATED en Postgres (encode(digest(scope::text, 'sha256'), 'hex')): la base de datos lo calcula sola a partir del scopenunca se almacena ni se compone a mano—, de modo que dos scopes idénticos siempre colisionan al mismo hash y dos distintos nunca.

¿Y si necesitas una versión fresca (porque cambiaron los datos de origen)? Entonces regeneras: el sistema crea un reporte nuevo y le pone supersededById apuntando al anterior. Se forma así una cadena de versiones —el original nunca se pisa ni se borra—; la idempotencia siempre devuelve el último vigente (el que aún no tiene supersededById).

Procesamiento y estados

El reporte recorre una pequeña máquina de estados mientras se genera:

EstadoSignificado
PENDINGEncolado, esperando worker
RUNNINGEl worker está ejecutando la query y renderizando el PDF/XLSX
READYListo y disponible para descargar
FAILEDFalló; se reintenta hasta un máximo (retryCount ≤ MAX)
EXPIREDAgotó los reintentos sin éxito

Detrás del telón, cada tipo tiene su cola (q:reports:daily-log, q:reports:mass-balance, …) y un worker dedicado: ejecuta la query SQL, renderiza el PDF y el XLSX, los sube al almacenamiento de objetos y persiste sus huellas pdfSha256 y xlsxSha256. Un cron de recuperación reintenta los reportes que se quedaran atascados en RUNNING, y otro detecta los que perdieron su archivo en el almacenamiento.

La notificación en tiempo real la entrega ReportsSocket: suscribe al usuario a su sala privada (reports:user:{userId}) y, al terminar el worker, el backend emite report.ready o report.failed; la lista se invalida sola. Mientras tanto, el InFlightTray —el panel persistente de la derecha— muestra los jobs en vuelo con un navy pulse dot que late hasta que el reporte queda listo.

Descargar y verificar la integridad

La descarga usa una URL presignada efímera — y nunca se persiste

La descarga (GET /api/v1/reports/:id/pdf o .../xlsx) responde con un 302 hacia una URL presignada del almacenamiento de objetos, con TTL corto (por defecto ~15 minutos). Esa URL nunca se loguea ni se guarda: solo se audita la emisión del enlace con @Audit({action:'VIEW', debounce:'1h'}) —como máximo un registro de auditoría por usuario y artefacto cada hora—, no la URL en sí. Pasado el TTL, el enlace caduca y hay que volver a solicitar la descarga.

Para confirmar que el archivo no se alteró, usa la verificación de integridad: GET /api/v1/reports/:id/verify recalcula el SHA-256 sobre los bytes reales del almacenamiento y lo compara con el persistido:

  • 200 { match: true } — íntegro: el archivo es bit a bit el que se generó.
  • 409 { match: false, computedSha256, persistedSha256 }drift: el archivo no coincide; se reportan ambos hashes para diagnóstico.
El reporte se lee en SI o en imperial, sin perder precisión

TankOS almacena en SI y convierte solo al renderizar. El mismo reporte se puede leer en SI o en imperial según la preferencia de cada usuario —volúmenes en m³ o en barriles, temperaturas en °C o °F—, y el valor custody almacenado es reproducible bit a bit con independencia de la unidad mostrada. Ver el sistema de unidades SI ⇄ Imperial y la hermana preferencias de unidades.

La lista de reportes

La página /reports muestra una tabla virtualizada con los reportes generados y permite filtrar por tipo y estado desde la URL (?type=&state=). Cada fila ofrece sus acciones —descargar PDF o XLSX; eliminar solo el administrador— y, a la derecha, el tray de jobs en vuelo late mientras hay reportes generándose.

Lista de reportes con jobs en vuelo

Páginas relacionadas

  • La auditoría documenta el visor de audit_log y el export AUDIT_CSV (admin-only) que se referencia arriba.
  • Los tickets de custodia también se firman y se verifican con SHA-256 (GET /:id/verify), igual que los reportes.
  • El sistema de unidades SI ⇄ Imperial y las preferencias de unidades explican cómo leer el mismo reporte en SI o en imperial sin perder precisión.
  • Las guías por rol (qué reportes genera cada perfil en su día a día) se publicarán próximamente.