Skip to main content

Reports

La herramienta de Reports (SIMON-T8) publica un catálogo de reports de negocio construidos sobre la capa semántica analytics (esquema analytics, migraciones 166/167). Cada report es una lista declarativa de secciones tipadas: el backend corre SQL curado de solo lectura y devuelve el payload; el frontend renderiza cada sección de forma genérica según section.type. Nunca se recalculan métricas en el cliente — toda cifra viene del backend. Superficie in-app: la ruta /app/reports muestra la parrilla del catálogo y /app/reports/:reportId el detalle. Es también la superficie donde vive Simón, el asistente de IA sobre analytics.
Añadir un report = un builder registrado en el módulo + su entrada de catálogo. Cero cambios de frontend: el renderer ya conoce todos los tipos de sección.

Endpoints (/api/analytics/reports)

El prefijo es analytics/reports porque reports ya lo ocupa el módulo daily-health. Auth: Auth0OrApiKeyGuard global + ToolAccessGuard; cada ruta exige el tool tool-reports con rol viewer.

Catálogo de reports

Las categorías posibles son negocio | clientes | soporte | operacion (ReportCategory). Cada report declara además un icon (nombre de icono Lucide que el frontend mapea a un componente).

Payload declarativo

ReportPayload (en packages/common/src/types/analytics-reports.ts) trae la metadata del report, su freshness, dataAvailable y un array de sections. Cada sección es una unión discriminada por type: Las unidades (ReportUnit) cubren count | nights | pct | days | minutes | hours | ratio | nps | eur.

Freshness

Cada report reporta la frescura de sus datos desde analytics.v_data_freshness: AnalyticsReadService.getFreshness(jobType) traduce el last_success_at del job de sync relevante (sync_bookings, sync_tickets, …) a una etiqueta humana + horas transcurridas.

Acceso a datos — AnalyticsReadService

Lectura de solo lectura sobre el esquema analytics mediante una conexión directa de Postgres (pg.Pool), no supabase-js, porque PostgREST no expone el esquema analytics.
  • Configuración por ANALYTICS_DATABASE_URL. Si falta, el servicio degrada con gracia: isConfigured() devuelve false, el catálogo marca dataAvailable:false y el detalle responde un estado vacío con unavailableReason (nunca lanza).
  • Modelo de roles fail-closed (alineado con las tools SQL de Simón). El DSN apunta al rol LOGIN dedicado simon_analytics, que es NOINHERIT y solo MEMBER de analytics_reader. Una conexión desnuda no tiene privilegios de tabla: cada query corre dentro de una transacción que hace SET LOCAL ROLE analytics_reader, SET LOCAL transaction_read_only = on, un statement_timeout de 15 s y SET LOCAL search_path = analytics. Sin el SET ROLE la SQL curada recibiría “permission denied”, así que el camino de lectura queda incapaz de escribir.
Sin ANALYTICS_DATABASE_URL la herramienta no se rompe: todos los reports aparecen con dataAvailable:false y un estado vacío explicativo.

Estructura de código