--- asset_id: MIN-EL-Jesus-20260702-v01 version: v01 tipo: MIN — Minuta del día owner: Jesús sherpa: Jess ratificador: Jesús fecha: 2026-07-02 intellibank: IB-EL-EmpowerLabs subbank: EQ-EL-Equipo proyecto: api-nodejs-mongo · diagnóstico de memoria · infraestructura · Rebelocity Club Librería --- # Minuta del día — Jesús · 2026-07-02 ## Qué se hizo - **Diagnóstico de memoria — workers Apache/mod_php en producción (`api-nodejs-mongo`):** - Investigado el reporte de workers Apache (prefork, mod_php) que hacen proxy hacia la API Node.js en el puerto 3000 inflándose hasta ~1GB de RSS sin liberarlo, ni siquiera minutos después de quedar inactivos (Keep-Alive) - Endpoint disparador observado: `GET /api/points/balance?userId=X&domId=Y&progress=...` - Revisado a fondo el flujo completo del endpoint (`PointsController.getBalance` → `PointsService.getBalance` + `TierService.getTierProgress` + `PointsService.getActionBreakdown` + `resolveOrganizationId`): todas las queries están acotadas por `user_id + organization_id` o son catálogos pequeños — sin `SELECT *` sin filtro, sin N+1, sin acumulación de arrays sin límite - Revisado el pool de conexiones MySQL (`config/dbMySql.js`, `mysql2`, `connectionLimit: 10`) en todo el módulo `points`: todos los `getConnection()` liberan correctamente en bloque `finally` — descartado como causa - Aclaración clave documentada: el worker Apache y el proceso Node (PM2 `api-nodejs`) son procesos de SO separados — un leak en el código Node no puede inflar directamente la RSS de un worker Apache, salvo que ese worker buferee la respuesta completa vía `mod_proxy`. RSS que no baja tras Keep-Alive inactivo es comportamiento normal del allocator (glibc/PHP), no necesariamente evidencia de leak - Encontrados 3 hallazgos secundarios de robustez (no explican el pico puntual, pero son mejoras reales): cache `Map()` sin TTL/límite en `controllers/ai-sherpa/vectorstore.js:15`; `body-parser` con límite de 50mb aplicado global a todas las rutas (`index.js:117-118`); `uncaughtException` que nunca termina el proceso, lo que enmascararía un leak real futuro (`index.js:12-17`) - Entregado diagnóstico completo con hallazgos priorizados y fixes propuestos (código sugerido) en `DIAGNOSTICO-MEMORIA-2026-07-02.md`, en la raíz del repo - **Fix aplicado en producción:** `MaxConnectionsPerChild = 500` en `mpm_prefork.conf` (antes en `0`/ilimitado) — con `0` los workers nunca se reciclaban, así que cualquier pico de memoria puntual quedaba permanentemente en ese worker hasta un restart manual de Apache. Con `500`, cada worker se recicla tras atender 500 conexiones, liberando ese "high-water mark" de RSS periódicamente. Queda en monitoreo para confirmar impacto real en el consumo de memoria observado - Sigue pendiente confirmar si la RSS del proceso Node (`api-nodejs`) también sube durante el incidente — el fix de `MaxConnectionsPerChild` ataca el síntoma (worker que nunca se recicla) pero no confirma si además hay un leak real del lado Node - **Infraestructura:** - Revisados los logs del sistema para identificar los procesos que podrían estar provocando la caída de algunos servicios. Se detectaron posibles puntos de mejora y se compartieron con el equipo de programación para su análisis - Aplicados ajustes en el servidor para limitar y liberar procesos, con el objetivo de mejorar la estabilidad y el consumo de recursos - Contacto con Alex para revisar el comportamiento de los procesos de IntelliBanks y analizar posibles causas relacionadas con el rendimiento del sistema - **Rebelocity Club Librería:** - Iterado un bug en el menú superior en la vista del playbook — causa raíz: colisión entre la imagen del logo y el título del playbook - **Socket.IO Editor:** - Auditoría completa del servidor Socket.IO: identificadas y corregidas fugas de memoria, configuraciones de seguridad pendientes y vulnerabilidades que afectaban la estabilidad del servicio - Eliminados archivos legacy que ya no se utilizaban y corregidas importaciones de código muerto que se cargaban sin ejecutarse - Seguridad del servidor fortalecida: ajuste de configuración CORS, rate limiting por IP, HTTPS forzado en producción y timeouts agregados para prevenir conexiones abiertas indefinidamente - Manejo de memoria optimizado en las capas de edición de documentos, locks de IA y eventos de cursor, incorporando TTL y limpieza automática de recursos al desconectarse cada cliente - Corregidos errores críticos en la comunicación en tiempo real: eventos emitidos a canales incorrectos y handlers asíncronos sin manejo de errores - Mejoras documentadas en la guía interna del proyecto, el README y un changelog para dar trazabilidad a los cambios - **Módulo de Métricas — smartplaybooks Frontend:** - Aplicados fixes F3 (columna Tendencia ocultada mientras era `Math.random()`, re-habilitada tras B1), F4 (`formatTimeSpent` — muestra "X seg"/"X.X min"/"—"), F6 (`timeSpent` en `content_completed` corregido de timestamp a delta en `reader-view.component.ts` y `reader-view-custom.component.ts`) - Rediseño completo de la vista **App**: KPI strip horizontal (Usuarios hoy / Prom. diario / Sesión promedio), retention bars con gradiente y estado vacío explícito, activity timeline compacto con chips de color (azul Sesiones / verde Playbooks), orden descendente (más reciente primero), scroll fijo a 320px, limitado a 30 días - Vista **Overview**: KPI strip homologado con App (reemplazó las 3 tarjetas con emoji) - Integración de fixes de backend: `getSessionMetrics` acepta `period` (B4), `getAppRetention` tipo ampliado a `string` (acepta `'year'`), columna Tendencia en tabla de playbooks re-habilitada con trend real (B1) - F8 aplicado — `retention-chart.component.ts`: removido `indexAxis: 'y'` (sintaxis ng2-charts v3, ignorado en v2), movido `min: 0 / max: 100 / callback '%'` de xAxes a yAxes — escala pasa de -1/1 a 0–100% - `tl-total` (número de totalEngagement flotante sin etiqueta) removido del timeline - `avgSessionDuration / 1000`: aplicado y revertido en la misma sesión — backend confirmó que ya viene en segundos (`normalizeMillisecondsToSeconds` en `metrics.service.js:641`); el valor alto visible en dev es por datos acumulados en el ambiente, no un bug de unidades - Auditoría visual de `/stats` en browser: confirmados F3/F4/diseño App, detectados y resueltos bugs adicionales (tl-total, escala retención) - **Módulo de Métricas — smartplaybooks / `api-nodejs-mongo`:** - Respondido `preguntas-backend-metricas.md` del frontend (`respuestas-backend-metricas.md`, en `smartplaybooks/data/`), con lectura completa de `routes/metrics`, `controllers/metrics`, `services/metrics/*.js` y `sql/metrics/p_metrics_tables.sql`. Bugs raíz encontrados: `period=monthly` en `app-retention` no devuelve D30/D90/D180/D365 (devuelve D1/D7/D15/D30); el campo real es `avgTimeSpent` (ya en minutos), no `avgTime`; `trend` de playbooks era `Math.random()`; `/playbooks/:id/retention` usaba ratios hardcodeados, no cohortes reales - A partir de `plan-fixes-metricas.md` (armado por el frontend con los hallazgos), implementados los 5 fixes de backend (B1-B5): - B1: `trend` real en `/playbooks/activity-period` (antes `Math.random()`) — compara vistas del período actual vs. el período anterior de igual duración - B2: `/playbooks/:id/retention` con datos reales (`created_at`/`last_access_date` de `pb_activity_playbook`) en vez de ratios fijos (0.75/0.5) y trend hardcodeado - B3: eventos `bookmarked`/`downloaded`/`shared`/`search` revisados en `processEvent` (`analytics.service.js`) - B4: `GET /api/metrics/session-metrics` acepta `period`/`startDate`/`endDate` (antes siempre las últimas 1000 filas sin filtro, sesgado a días recientes) - B5: `buildMetricsScope` conecta `req.query.domain` al filtro de canal (ya lo soportaba el service, faltaba el controller) - Hallazgo relevante durante B3: `bookmarked`/`downloaded` **no** se conectaron a la tabla nueva de métricas — el usuario señaló que ya tienen fuente real en tablas legadas distintas. Confirmado contra `data/resources/php/pb-stats.php` y `/var/www/html/api/controllers/general/news.php`: bookmarks reales viven en `marcador (id, noticia_id, usuario)` (misma DB que `pool1`, `genniux_board`, ya leída por `PlaybookModel.js`), y downloads reales en `log_file(id_user, id_item, app, file, downDate)`. Se corrigió `getEngagementMetrics` para leer de esas tablas reales en vez de las columnas nuevas desconectadas (`bookmarkRate`/`downloadRate` daban 0 siempre) - `shared` se implementó primero (columna nueva `shares_count` + migración) y **se descartó después**: como la migración nunca se corrió en ningún entorno, ese código quedaba muerto (fallaría contra una columna inexistente). Se eliminó la migración, se quitó la columna del DDL canónico y el `case "shared"` quedó sin aggregate — igual que `bookmarked`/`downloaded`. `shareRate` de `getEngagementMetrics` vuelve a `0` hardcodeado, como estaba originalmente - Aclarado el alcance de B3: no es parte de "Permanencia" (lo que la UI de Métricas usa hoy) — es la sección §5.1 del audit ("Engagement, ya trackeado, no visualizado"), o sea una función a futuro. `bookmarkRate`/`downloadRate` quedan corregidos y funcionando ya (no dependen de ninguna migración); `shareRate` queda pendiente para cuando se priorice - Pregunta aparte del usuario: si vale la pena portar `pb-stats.php` (2548 líneas) completo a Node — se determinó que `PbStatsModel.js` ya cubre ~70% (1839 líneas, 19 de 28 funciones), y se recomendó portar incremental (solo cuando el frontend pida un stat puntual que siga solo en PHP), no una migración completa de una vez. Quedó como criterio, sin acción pendiente - Pregunta del usuario sobre el orden de `GET /api/metrics/activity-period`: hoy devuelve ascendente (más antiguo primero — `ORDER BY ... ASC` en SQL y `sort()` default en JS, `metrics.service.js:343-396`). Se decidió no tocarlo del lado backend por ahora — el usuario prefiere revisarlo y, si hace falta invertir el orden, que lo resuelva el frontend - **Módulo de Métricas — datos de usuarios (`preguntas-backend-usuarios.md` → `respuestas-backend-usuarios.md`):** - Investigado si conviene una vista de "Usuarios" en Métricas (top usuarios, inactivos, nuevos). No existe ningún endpoint de usuarios hoy en `routes/metrics` - Encontrada función canónica reusable `profileUser({ puesto: userId })` (`models/UserModel.js`), ya usada por Points/Cards y login — resuelve nombre, avatar, email, canal, rol, fecha de registro (`perfil.registro`) y último acceso (`perfil.ultimo_acceso`) en una sola query. No hace falta construir esto de cero - Confirmado que "usuarios nuevos" ya tiene precedente exacto en el sistema legado (`PbStatsModel.getGeneralStats`, `perfil.registro BETWEEN`) - Hallazgo importante de alcance más amplio: **ningún endpoint de esta API Node tiene autenticación ni verificación de rol** — no es algo específico de métricas, es así en todo el backend. Si se expone actividad individual de usuarios (nombre+avatar+actividad), hoy sería público sin control - El usuario del frontend confirmó la duda abierta más importante: `userId` en `heartbeat`/`analytics/events` **es `puesto.id`** (campo `puesto` de la cookie `usuario2`, ID numérico como string). Esto desbloquea el cruce directo `pb_activity_*.user_id` → `puesto.id` → `perfil`/`colaborador` sin mapeo adicional — actualizado en `respuestas-backend-usuarios.md` - Recomendación de orden de implementación: `users/new` y `users/inactive` primero (queries simples de una tabla), `users/top` después (junta agregación + enriquecimiento de perfil) ## Decisiones - El diagnóstico de memoria se entrega como documento separado (`DIAGNOSTICO-MEMORIA-2026-07-02.md` en el repo) antes de tocar código en producción — la prioridad es confirmar si el leak está en Node o es 100% infraestructura Apache/PHP antes de aplicar fixes - `MaxConnectionsPerChild` se fija en `500` (no `0`/ilimitado, tampoco un valor muy bajo que fuerce recreación excesiva de workers) — balance entre liberar memoria acumulada periódicamente y no generar overhead de fork constante - B3 (engagement): `bookmarkRate`/`downloadRate` se quedan corregidos (datos reales, sin dependencias pendientes). `shared`/`shares_count` se descarta completo por no haberse ejecutado la migración en ningún entorno — mejor no dejar código dependiendo de un cambio de schema que nadie corrió - No se porta `pb-stats.php` completo a Node de una vez — se prefiere migración incremental, función por función, solo cuando se necesite - Orden de `activity-period` (ascendente vs. descendente) se deja como está en backend — si se necesita cambiar, lo resuelve el frontend ## NEXTs - [ ] Evaluar aplicar los fixes de robustez propuestos en el diagnóstico (cache con TTL en `vectorstore.js`, límite de `body-parser` por ruta, `uncaughtException` que sí termine el proceso) - [ ] Frontend: aplicar F7 (DAU/MAU con endpoints correctos) — queda pendiente en `plan-fixes-metricas.md` - [x] F8 (escala retención) — aplicado en esta sesión - [x] Frontend: `activity-period` en orden descendente — resuelto con getter `reversedData` en `activity-timeline.component.ts` - [x] Responder `preguntas-backend-usuarios.md` — hecho, `respuestas-backend-usuarios.md` con viabilidad de los 3 endpoints propuestos (`/top`, `/inactive`, `/new`) y confirmación de identidad de usuario - [ ] Backend: construir `GET /api/metrics/users/new` y `/users/inactive` (queries simples contra `perfil.registro`/`perfil.ultimo_acceso`, patrón ya probado en `PbStatsModel`) — sin bloqueantes, listos para implementar cuando se priorice - [ ] Backend: construir `GET /api/metrics/users/top` — requiere agregación en `pb_activity_playbook`/`pb_activity_user` + enriquecer con `profileUser()` - [ ] Producto + Backend: decidir y construir capa de auth/rol antes de exponer actividad individual de usuarios en la UI (no existe ninguna hoy en todo el backend Node) - [ ] Retomar `shared`/`shares_count` en `getEngagementMetrics` si en algún momento se prioriza esa métrica (requiere nueva migración, se descartó la anterior por no haberse corrido) **Cierre del día — de este lado (`api-nodejs-mongo`) terminamos por hoy.**