Reports
La herramienta de Reports (SIMON-T8) publica un catálogo de reports de negocio construidos sobre la capa semánticaanalytics (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 desdeanalytics.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()devuelvefalse, el catálogo marcadataAvailable:falsey el detalle responde un estado vacío conunavailableReason(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 esNOINHERITy solo MEMBER deanalytics_reader. Una conexión desnuda no tiene privilegios de tabla: cada query corre dentro de una transacción que haceSET LOCAL ROLE analytics_reader,SET LOCAL transaction_read_only = on, unstatement_timeoutde 15 s ySET 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.