Sergio se refiere a este proyecto como “monitoreo” en conversación (decidido 2026-05-22). El slug del hub queda como
es-antenas-newpor la regla de slugs estables. Ver memoriareference_monitoreo_alias.md.
Estado al 2026-05-25 (sesión tarde — Sprint 1 deployado en prod)
Sprint 1 (#193) deployado y validado: 0 deadlocks post-deploy
5 commits en master, deployados secuencialmente con 2 hotfixes:
d4b1759P10 — limpieza deProcesarMonitoreoDispositivovacío.3ee9cd0P2 — lock TTL 60s conextend()+ comandomonitoreo:unlock-stuck-sites+ schedule cada 5 min.19939e3P1 — shrink tx +insertOrIgnore→insert+READ COMMITTEDper session.1f2c4aahotfix #1 —DatabaseLockno soportaextend(); intento de UPDATE atómico encache_locks.5a60758hotfix #2 — el UPDATE manual fallaba por key prefix (es_monitoreo_cache_*); eliminado el bloque de refresh; TTL del lock a 300s; depender del cron unlock-stuck-sites para huérfanos.
Bug crítico encontrado en P2 (13:08-13:11): workers tiraban Call to undefined method Illuminate\Cache\DatabaseLock::extend(). El proyecto usa CACHE_DRIVER=database, y DatabaseLock (a diferencia de RedisLock/MemcachedLock) no implementa extend(). Resultado: ~130 archivos a failed_syncs/ en 90 segundos. Lección para el doc del #193: la sección P2 asumía extend() disponible sin distinguir entre tipos de lock store. Actualizar el doc con la nota.
Bug en hotfix #1 detectado a los 5 min: el cache_locks almacena keys con prefix (es_monitoreo_cache_sync_lock_sitio_X), no la key raw. Mi UPDATE buscaba la raw → siempre 0 filas → todas las iteraciones del while abortaban → 0 lecturas ingestadas en 5 min, syncs/ creciendo. Más dañino que el bug original.
Hotfix #2 simplifica al modelo correcto para DatabaseLock: TTL 300s sin refresh; si el worker muere, el lock queda huérfano máximo 5 min (vs 10 min con TTL 600s original); el cron monitoreo:unlock-stuck-sites lo limpia adicionalmente. #195 queda cerrado con ventana 5 min (vs 10 min original) — no es la mejora de “0s gap” prometida en el doc por extend(), pero es robusta.
Recovery del backlog acumulado por los 2 bugs (~163 archivos a failed_syncs/): moví failed_syncs/<sitio>/ → syncs/<sitio>/ con shell (SSH como electrosystems, que está en group www-data, dirs writable) y dispatché 1 job por los 12 sitios afectados. Recovery completo sin pérdida de datos. failed_syncs/ vacío al cierre.
Métricas validadas post-hotfix #2 (ventana 13:17-13:23, ~6 min):
- 0 deadlocks de ingesta ni del SP de agregados (vs 91 hoy pre-deploy en 13 hrs ≈ 7/hr).
- 0 errores nuevos en log Laravel.
- 0 fallos en Fase 3 (la sección post-commit del refactor funciona sin caer en su catch).
- 1,045 lecturas ingestadas en 5 min — recovery + tráfico normal.
failed_syncs/vacío en todos los sitios.
Estado de #194 y #195 al cierre: ambos en posición de cerrarse después de 24-72 hrs de observación con métricas estables. Aún no se marcan como cerrados porque la validación real requiere ese tiempo de observación. Documentar follow-up: contar deadlocks 2026-05-26 y 2026-05-27 mañana; si 0 → cierre formal.
Sprint 2 — observación clave: los 21 deadlocks/día del SP de agregados también cayeron a 0 en la ventana medida. Hipótesis: READ COMMITTED per-session (P1) beneficia al SP también porque el SP corre como query del mismo job. Si se mantiene en 0 durante 24-72 hrs, P3 (SP a cron dedicado) se descarta como innecesario — Sprint 2 se queda solo con P4 (batch UPSERT) y P9 (cron marcarDispositivosSinDatos), ambos opcionales.
Pre-Sprint — documento de revisión integral entregado
- #193 documento entregado. Diagnóstico de architect agent sobre
app/Jobs/ProcesarSyncCollector.php+monitoreo-collector(Go). Salida enPIPELINE_REVIEW_193.md. TL;DR del documento:- La causa raíz de los 133 deadlocks/día (#194) NO es throughput (es ~50 inserts/seg, MySQL aguanta 100×); es la combinación de (a)
insertOrIgnoresemánticamente vacío porquelecturas_historicas_detallesNO tiene UNIQUE (verificado en migrations) pero cambia el modo de locking de InnoDB, (b) transacción gigante que incluye ~7 side-effects innecesarios (firstOrCreatede estatus por detalle,updateOrCreateEstatusDispositivo,Dispositivo::revisarEstatuscon dispatch de event,Sitio::revisarEstatus), (c) isolation REPEATABLE READ que toma gap locks en los 3 índices secundarios + 2 FKs durante el INSERT paralelo. - El worker zombie del #195 es un bug separado:
Cache::lock("sync_lock_sitio_X", 600)queda vivo 600s si el job revienta por OOM/kill -9 fuera definally. Los 4 dispatches “vacíos” simplemente vieron el lock ocupado y se retiraron en silencio. - Recomendación priorizada (Sprint 1, ~1 día): P1 (4-6 hrs) achicar la tx a solo los 2 INSERTs de histórico + mover todo lo demás post-commit + reemplazar
insertOrIgnoreporinsert+SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED(esto solo lleva los deadlocks de 133/día a 0-5/día sin tocar arquitectura). P2 (2-3 hrs) TTL del lock a 60s conextend()por iteración + cronmonitoreo:unlock-stuck-sitescada 5 min (cierra #195). - Descartado explícitamente con justificación en el doc: reescribir en Go, reemplazar el modelo file-based de
syncs/(es durable y NO es el cuello), introducir Kafka/Redis Streams, particionarlecturas_historicas_detallespor sitio. Particionar por fecha queda condicional a >100M filas (hoy 49.6M, ~3 años de margen). - Sprint 2 condicional: P3 sacar
sp_calcular_agregadosa cron dedicado, P4 batchearEstatusMonitoreoDispositivocon UPSERT, P9 movermarcarDispositivosSinDatosRecientesa cron.
- La causa raíz de los 133 deadlocks/día (#194) NO es throughput (es ~50 inserts/seg, MySQL aguanta 100×); es la combinación de (a)
- Preguntas abiertas que requieren prod (sección 6 del doc): isolation level actual del MySQL en prod, payload tamaño promedio en
failed_syncs/,redis-cli KEYS 'laravel_cache_sync_lock_sitio_*'para confirmar lock huérfano vivo. Queries de verificación read-only listas en sección 7 del doc (anexo). - Pendientes que evolucionan a partir del doc: #194 y #195 siguen abiertos pero ahora con causa raíz confirmada y fix recomendado (P1 y P2 respectivamente); cuando Sergio autorice Sprint 1, esos dos son los que aterrizan. No se abrieron IDs nuevos — los pendientes derivados son P1/P2 que cierran #194/#195 directamente.
Estado al 2026-05-23
- Portal caía con 500 —
/metricsdesactivado, commit2efa022, deployscripts/deploy.sh98s.routes/web.phpexponía/metrics(público, sin auth) →MetricsController.php:15corríaDispositivo::with('ultima_lectura_historica.detalles')->get()sobre los 317 dispositivos.latestOfMany('fecha')de Laravel genera una subquery conMAX(fecha) GROUP BY monitoreo_dispositivo_idsobrelecturas_historicas_detalles(49.6M filas / 10 GB). Cada hit tardaba 30-60s; un scraper externo apilaba hits hasta saturar PHP-FPM y otros endpoints devolvían 500/502/504. Validado conSHOW FULL PROCESSLIST: 2-3 instancias paralelas de la misma query trabadas hasta 56s. Fix conservador: desactivar la ruta (Sergio confirmó que Prometheus no se usa hoy, retomar después). Post-deploy/metrics → 404 47ms,/dashboard → 302 28ms, MySQL sin queries trabadas. Misma query también la genera el mailable de alertas (Dispositivo::with('ultima_lectura_historica.detalles')), capturada en error de log a las 13:08:39 con “Query execution was interrupted” — el fix definitivo del #190 debe alcanzar también ese codepath. - 63 (→ 81 a media tarde) dispositivos sin datos del colector — CAUSA RAÍZ: bug de permisos del log devolvió y revento el pipeline de ingesta. Diagnóstico inicial mío fue ERRÓNEO (sospeché de red interna en sitios). Sergio entró a los collectors remotos y confirmó que los collectors SÍ ven los dispositivos por LAN; el problema estaba entre el collector y el server. Hallazgo real:
laravel-2026-05-23.lognació con dueñoelectrosystems:electrosystems 664(en vez del usualwww-data:www-data 664) porque un cron de electrosystems escribió primero a las 00:00:07. Los workers de Horizon (que corren como www-data, no están en grupo electrosystems) NO podían escribir al log → cada intento de log dentro deProcesarSyncCollectortirabachmod() Operation not permitted→ el job entero fallaba →failed()movía el archivo afailed_syncs/<sitio>/. Total: 2,528 failed_jobs + 4,817 archivos en failed_syncs/ acumulados desde 00:00. Distribución: Torreón 2,071 / Chihuahua 1,666 / Parral 710 / Hércules 370. Fix de permisos:sudo chown www-data:www-data + chmod 664al log de hoy (Sergio pegó el comando con!a las 13:44). Post-fix: 0 nuevos fails, pendientes vivos vacíos en cosa de minutos. - Recovery del backlog COMPLETO (13:57 → 21:58). Estrategia evolucionó: primero script con bloques de 200 + polling, pero el script se atoraba porque el polling salía a “pending<10” antes de terminar realmente el batch (Chihuahua procesaba más lento de lo que el script agregaba — ~14 archivos/min vs 22 que metía). Cambié a estrategia simple: mover TODO failed_syncs/
/ → syncs/ ; el lock per-sitio del job (/ de una vez + 1 job por sitio Cache::lock("sync_lock_sitio_X")) garantiza serializa por sitio, throttling natural por el loop interno de 4 min + auto-redispatch del propio job. Funcionó para Hércules, Parral, Chihuahua, Torreón y los 7 sitios menores. Tepehuanes (sitio 12) requirió ejecución INLINE vía(new ProcesarSyncCollector(...))->handle()en tinker porque el worker de Horizon no tomaba el job dispatcheado (4 dispatches consecutivos no produjeron logs, pending seguía en 59); inline procesó los 59 en ~60s. Total recuperado: ~5,800 archivos a través de 11 sitios. failed_jobs table limpiada conqueue:flush(2,573 → 0). Estado final: pending ≈ 0 en todos los sitios, failed_syncs vacíos. - 11 dispositivos siguen marcados “Sin datos recientes” tras el recovery — son problemas REALES, no del bug del log: Tepehuanes 6 (collector remoto del sitio caído desde antes — vimos
min_sin_collector=487al inicio), Torreón 3, Villa Ahumada 2 (probablemente por deadlocks MySQL que siguen ocurriendo). Diferencia con la mañana (cuando había 81): esos eran 100% del bug; los 11 actuales son situaciones legítimas que el operador debe atender. - Deadlocks MySQL en
lecturas_historicas_detallesdurante el día: 133 deadlocks contados en el log. Patrón visto: jobs paralelos de sitios distintos (Torreón + Chihuahua, etc.) intentanINSERT IGNORE INTO lecturas_historicas_detallescon batches grandes (decenas de rows por archivo) y se chocan en algún índice (probablemente el UNIQUE(lectura_historica_id, monitoreo_dispositivo_id)o similar). Cada deadlock → archivo movido afailed_syncs/. Capturado como #194. - El “fix” de 2026-05-15 (
permission => 0664en config/logging.php, commit0ddec4f) NO arregla el problema completo. Solo controla el modo, no el group. Si algún cron corriendo comoelectrosystemsescribe al log antes quewww-data, el archivo nace con group electrosystems ywww-datano puede escribir aunque sea 664. Pendiente fix de fondo (#192): usarumask 002en los crons +setgidenstorage/logs/para que el group siempre se herede de la carpeta (www-data), o pasar todos los crons a correr como www-data. - Bug colateral en cron
NotificarLogs: a las 07:00:02 de hoy reventó conAttempt to read property "enlaces" on nullenNotificarLogs.php:686($e->monitoreoDispositivo->dispositivo->enlaces->pluck('id')condispositivonull — relación rota por dispositivo eliminado o soft-deleted). El cron se abortó — alertas por correo de las caídas de hoy NO se enviaron. Pendiente arreglar (#191).
Estado al 2026-05-22
- Probar conectividad SNMP antes de guardar el dispositivo, commit
d73d0ab. Botón “Probar SNMP con esta config” en la sección Config SNMP del form. Envía los valores ACTUALES del form (ip + community + version), no los del modelo en BD — útil cuando se agrega un candidato UISP que viene sin community y el operador la captura antes de guardar. EndpointPOST /dispositivos/probar-snmp(sin dispositivo_id) hacesnmpgetde sysDescr + sysName con timeout 1.5s (mismo patrón queSnmpSimuladorController). Smart fallback: si la versión solicitada no responde, intenta v2c y v1 automáticamente; si otra versión responde devuelvesugerenciay la UI ofrece “Cambiar a vX” con un botón que autoselecciona en el form. v3 NO entra al fallback (requiere sec_name/sec_level que el form no captura). UI: 3 estados — ✓ verde con sysName+sysDescr / ⚠ amarillo con sugerencia / ✗ rojo con tipo de error (timeout/auth/no_responde). Tests Pest +6 enProbarSnmpTest(validación de payload + path de timeout con IP RFC 5737 + shape). Suite 262/262 verde (era 256). Build Vite OK. Deployscripts/deploy.sh71s. Limitación conocida: dispositivos en sitios remotos detrás de collector NO son alcanzables desde el servermonitoreopor LAN; solo equipos del mismo broadcast o via WireGuard responderán — la UI ya lo expresa cuando hay timeout/no_responde. - Insertar en medio del enlace con shift automático, commit
0db3ef3. Mejora del modal “Agregar a enlace” del #171. Diagnóstico previo en prod: 22/30 enlaces vivos (73%) son N≥3 (hasta N=19); meter un dispositivo “entre dos que ya están” era el caso dominante y antes el operador tenía que ir a/enlaces/{id}/showa reordenar manualmente. Backend:enlacesDisponiblesahora devuelve el arraydispositivos[{id,nombre,posicion}](ya noposiciones_ocupadas).agregarAEnlaceen transacción hace SHIFT+1a todos los pivots conposicion >= Pantes del attach (si P > max el UPDATE es no-op, caso “al final”). Eliminada la validación dura “posición ocupada” — ahora dispara el shift. Frontend: el input numérico de posición se reemplaza por una lista de N+1 radios visualmente legibles (↑ Antes de X,↕ Entre X y Y,↓ Después de Z (al final)). Default: insertar al final. Tests +3 de shift (medio / inicio / final-no-shifta), 1 viejo borrado, 1 actualizado al nuevo shape. Suite 256/256 verde (era 254). Build Vite OK. Deployscripts/deploy.sh54s. - “Copiar de otro dispositivo” generalizado (antes “Copiar del contraparte”), commit
14fac30. Diagnóstico previo en prod reveló que 34/147 dispositivos vivos (~23%) recibían “Sin contraparte aplicable” — 33 por enlaces mixtos (Cambium↔Ubiquiti, etc., el otro extremo con plantilla distinta) y 1 sin enlace todavía. El filtro duro “mismo enlace + misma plantilla” del endpoint anteriorcontraparteMonitoreosse relajó: ahorafuentesDeCopialista TODOS los dispositivos con la misma plantilla, marcandoes_contraparte=truesi comparten enlace y ordenando contraparte → mismo sitio → resto alfabético (limit 50, soporta?q=). Frontend: el botón abre un modal con autocomplete; contraparte preseleccionada con badge verde; mismo sitio con badge azul; preview de N monitoreos que harán match. Pegar config ahora copia los 3 camposhabilitado + limite + ignorar(antes sololimite + ignorar); monitoreos NO presentes en la fuente se DESHABILITAN en el destino para reflejar 1:1 el patrón configurado del fuente. Etiquetas siguen sin copiarse (son por puerto físico). Tests: 3 viejos reemplazados por 6 nuevos (misma-plantilla / excluye-plantilla-distinta / marca-es_contraparte / orden contraparte→mismo-sitio→nombre / filtro q / lista no-vacía sin enlace). Suite 254/254 verde (era 251). Build Vite OK. Deployscripts/deploy.sh53s. Endpoint viejo/contraparte-monitoreosya no existe en prod (verificado conroute:list). Esto cubre el problema real que destrababa al #172, así que la decisión final sobre el wizard 1-page queda esperando feedback de Sergio. - Fase 5 del flujo “Agregar candidato UISP” cerrada: asignar a enlace desde el edit del dispositivo, commit
b6c30f5. Sección “Enlaces” enDispositivos/Forma.vue(solo en edit) con (a) lista read-only de enlaces actuales (nombre + posición + link a/enlaces/{id}), (b) modal “Agregar a enlace” con autocomplete (mismo sitio, excluye donde ya está) + selector de posición + validación local de colisión, (c) modal “Crear nuevo” inline (crea en sitio del dispositivo, asigna en posición 1), (d) “Quitar” inline. Backend: 4 endpoints nuevos enDispositivosController(GET enlaces-disponibles?q=,POST enlaces{enlace_id, posicion},POST enlaces/crear-y-asignar{nombre},DELETE enlaces/{enlace}). Validación dura: mismo sitio + no duplicado + posición no ocupada.Dispositivo::enlaces()ahora conwithPivot('posicion');edit()carga enlaces ordenados. Tras cada cambio →Enlace::actualizarEstatus(). Tests Pest +9 (251/251 verde, era 242). Build Vite OK. Deployscripts/deploy.sh72s, sin migraciones, falso positivo “Horizon is inactive” del postflight (memoria #029). Para reordenar/insertar en medio se sigue usando/enlaces/{id}/show. Elimina los pasos 12-13 del flujo manual original. Queda en mesa #6 (wizard 1-page). - Flujo “Agregar candidato UISP” simplificado: 3 atajos (combo 1+2+3 de la propuesta), commit
cd9b7bb. Sergio reportó que el flujo manual eran 8 pasos (página candidatos → agregar → lista → scroll → ver → editar SNMP → guardar → lista → scroll → editar → monitoreos → enlaces → orden). El cambio elimina los dos scrolls + el ir a buscar el contraparte: (1)UispCandidatosController::agregarredirige al/dispositivos/{id}/editdel nuevo o del claimed, no al index de candidatos; (2)prepoblarMonitoreosDesdePlantilla()inserta todos los monitoreos de la plantilla conlimite=0/ignorar=falseal crear (MonitoreoDispositivo::insertbatch — claim NO los pre-puebla, preserva config manual); (3) endpointGET /dispositivos/{id}/contraparte-monitoreos(filtra por misma plantilla, devuelve[{dispositivo_id, nombre, enlace_nombre, monitoreos:[{monitoreo_id,limite,ignorar,etiqueta,etiqueta_corta}]}]) + botón “Copiar del contraparte” enForma.vuesección Variables de Monitoreo — UI carga la lista on-click, si hay 1 pega directo, si hay >1 muestra dropdown, copia sololimite+ignorar(etiquetas son por puerto físico, no se replican). Tests 242/242 (+5 nuevos en Pest). Build Vite OK. Deployscripts/deploy.sh99s, sin migraciones. Horizon respawneó por systemd, falso positivo “Horizon is inactive” del postflight es el follow-up conocido del cleanup multi-instance del #029 (nombre dinámico del supervisor). Sergio mencionó que es muy posible que también pida #5 (asignación a enlace dentro del edit) y #6 (wizard 1-page de Agregar). - Alias formal: desde hoy Sergio se refiere a este proyecto como “monitoreo” en conversación. Slug del hub queda
es-antenas-newpor regla de slugs estables; agregadoaliases: [monitoreo]al frontmatter + memoria del hubreference_monitoreo_alias.md.
Estado al 2026-05-21
- Limpieza + fix menor desplegado: commit
57796ba. (1)/public/builda.gitignore+git rm -r --cachedde 7 archivos — el deploy regenera todo determinísticamente vía Vite. (2) Bug del conteo de workers enscripts/deploy.shpostflight: el grep estaba bien (manualmente regresa 20); root cause era timing —RestartSec=5+ boot de horizon excedía elsleep 5fijo. Reemplazado por poll de 30s. El siguiente deploy estrenará el postflight nuevo (el actual usó el viejo porque el shell ya lo tenía cargado antes del pull). Deploy 117s, OK. - SNMP Fase 1: Rumurachi-Urique cerrado quirúrgicamente. Soft-delete de
monitoreos_dispositivos.id=989(monitoreo 97 ifSpeed.3 → WLAN en Cambium ePMP, no expone velocidad de aire en IF-MIB) +DELETE FROM monitoreos_plantillas WHERE monitoreo_id=97 AND plantilla_id=20(Cambium 4600C, preventivo para futuros equipos del modelo). Dispositivo 206 ahora polla solo OIDs propietarios Cambium 17713 (monitoreos 116/117, estables ante reboot). Descubrimiento paralelo: el monitoreo 166 (ifSpeed.1 LAN1) que la bitácora del 2026-05-20 asumía activo en realidad estaba soft-deleted desde 2026-05-04 — el único OID roto era el 97. - SNMP Fase 2: RFC cerrado. Diseño consensuado para resolver el problema arquitectural (77 monitoreos hardcodean ifIndex). Placeholder
{IF}+ columnamonitoreos_dispositivos.interfaz+ lookupifDescrcon fallbackifNameen collector Go + cache SQLite con TTL 1h + error semantic “falla explícita con razón” viarazon_fallanuevo enLecturaCombinadaDetalle. Próximas fases: 3a = scaffold de tests del collector (hoy no tiene), 3b = implementación end-to-end, 4 = migración de datos por lotes empezando con MikroTik (mayor fragilidad por VLANs idx 2001/2005). Ver bitácora “Fase 2 RFC” abajo para detalle.
Estado al 2026-05-20
- Diagnóstico SNMP Rumurachi-Urique:
ifIndexno persistente. Sergio reportóLink Speed=0en dispositivo 206 (192.168.37.61, Cambium 4600C). SNMP confirma: el equipo se rebooteó hace 43 min y losifIndexse renumeraron —ifSpeed.3pasó de LAN a WLAN1, y los Cambium ePMP no exponen velocidad de aire en IF-MIB estándar (siempre 0 en WLAN; vive en MIB propietaria 17713). Hallazgo de fondo: hardcodearifIndexen plantillas SNMP es frágil para todo equipo sin ifIndex persistence (Cambium, Mikrotik, Ubiquiti, etc.). Capturado como pendiente para sesión subsecuente: resolver porifDescr/ifNameen cada poll en lugar de OID literal. Plantilla “Cambium 4600C” como primer caso a migrar; monitoreo 97 debería eliminarse (WLAN no aplica). Ver bitácora 2026-05-20. - Fan-out de recordatorios resuelto: arrastre por sitio en
procesarAlertas(recordatorios) — commit74d2e83enorigin/master, deployado amonitoreovíascripts/deploy.sh(primer uso real del script — 126s, sin migraciones, build limpio, 20 workers systemd activos). Tests 232/232 (+4 nuevos). Sergio reportó una captura de Gmail con ~10 correos del sitio Hercules en 1 hora; diagnóstico confirmó que la cascada a sitio (despacharPendientes, commita9e1566) solo promovía cuando ≥2 buckets caían en el MISMO tick — conultimo_notificado_atdesfasados por minutos eso casi nunca pasaba. Fix replica el patrón “seeds + arrastre por sitio” que ya teníaprocesarFlapping(commit616d7fc): el primer seed vencido arrastra a todas las métricas en falla del mismo sitio, se manda 1 correo de sitio, yenviarCorreoresincronizaultimo_notificado_atde todo el grupo. Filtro adicional excluye notificados <5 min (typically en el mismo tick por alertas iniciales) para evitar doble correo en colisión inter-bloque. - Fan-out de FLAPPING detectado también resuelto: debounce + arrastre por sitio — commit
017e1b9. Sergio reportó otra captura con 3 correos[FLAPPING] Chihuahua · WiTek Hermosillo · 3 métricasconsecutivos (07:00, 07:01, 07:02) + 2 correos[FLAPPING] Hércules · 9 métricas(07:00, 07:02). Diagnóstico: el bloque “detectados” enprocesarFlappingagrupa SOLO las métricas que vienen en ese tick; si nuevas métricas cruzan el umbral N-en-15min en ticks consecutivos, cada tick manda su propio correo. Fix: debounce de 5 min en seeds (flapping_desde <= now - debounce_minutos) + arrastre por sitio cuando un seed vence (incluye hermanas conflapping_desdereciente en el mismo correo). Nueva configmonitoreo.flapping.debounce_minutos(envMONITOREO_FLAPPING_DEBOUNCE, default 5). Trade-off: la PRIMERA alerta de flapping de un sitio se retrasa hasta 5 min; las hermanas que cruzan el umbral en ese intervalo se consolidan en 1 correo. Tests 237/237 (+4 nuevos). Deploy en 62s víascripts/deploy.sh. Postflight reportó “workers systemd activos: 0” pero verificación manual: los 20 workers estánactive running(bug del script — follow-up menor). - Reglas persistidas en memoria del hub (Sergio explícito 2026-05-20):
feedback-es-antenas-new-push-deploy-authorized.md: push directo a master + deploy viascripts/deploy.shautorizados por default en este repo. Commit sigue requiriendo auth explícita per-sesión.feedback-prod-read-only-diagnostics-authorized.md: SSH read-only y queries SELECT a MySQL en servidores ES de prod autorizados por default para diagnóstico. Mutaciones (UPDATE/DELETE/sudo) siguen necesitando auth.
Estado al 2026-05-18
- Limpieza de pendientes stale. Diagnóstico hoy reveló que 3 pendientes ya estaban resueltos por deploys previos: (1) permisos del log daily — commit
0ddec4fya estaba en master y deployado, los logs desde 2026-05-15 nacen0664; (2) huérfanos de Horizon — elhorizon:terminatedel deploy 2026-05-14 los limpió, hoy hay 20 masters tracked por systemd (laravel-worker@1..20.service) yhorizon:statusreportarunning; (3) push notifications — ya cerrado en sesión 2026-05-11, había un pendiente duplicado abierto. failed_jobs(Item 1): FLUSHED. Eran 5,870 (no 14):ProcesarAgregadosSitio4476,ProcesarSyncCollector1391,GenerarReporte3. Causa raíz: el bug de permisos del log volvía fatal cualquierLog::warning()dentro de un job — la escritura al daily file tirabaUnexpectedValueException: Permission denied. Con perms fixed (desde 2026-05-15), 0 nuevas fallas.php artisan queue:flushejecutado enmonitoreo: 5,870 → 0.- Cascada
FlappingAgrupadoraplicada a alertas/recuperaciones (Item 2): commita9e1566enorigin/master. Refactor enapp/Console/Commands/NotificarLogs.php(+191/-69) + tests (+204/-12). Método nuevodespacharPendientesaplica el pase de sitio-promoción. Tests: 197/197 (era 192; +5 nuevos + 2 renombrados). Deploy pausado: el server estaba en branchfeature/exportar-csv-uispcond6c6e7d(otro agente, comandoes:exportar-csv-uisp); las branches divergieron y mi commit no es fast-forward. Sergio paró el deploy para sincronizar con el otro agente. - Node 20 en server
monitoreo+ build validado (Item 3, parcial). Sergio corriósetup_20.x+apt-get install nodejscon sudo → Node 20.20.2 + npm 10.8.2 (antes Node 16.20.2 EOL).npm ci+npm run buildexitosos en server; el build con Vite 6.2.5 reproduce byte-por-byte elpublic/build/commiteado (hashes determinísticos). Falta: moverpublic/build/a.gitignore+ actualizar runbook — diferido hasta resolver divergencia master/feature. - Commit
e2c19e4(UISP migration) faltaba en bitácora. Agregóorigen,uisp_id,administradoadispositivospara homologación UISP. Ya deployado enmonitoreo. Detalle en proyectomonitoring-homologation.
Estado al 2026-05-14
- Captura de Gmail mostró ~20 correos
[FLAPPING RECORDATORIO]apilados de Caborca y Terreón, mayoría con “1 métrica” en el asunto. Caso clave: RX y TX del mismo dispositivoAP CU-Esquiveles AF11(Torreón) llegaron separados 19 min con la mismaflapping_desde→ confirma que susflapping_notificado_atestán desincronizados. - Fix implementado en
NotificarLogs::procesarFlapping: la query de recordatorio ahora se hace en dos pasos (seeds que cumplen umbral → arrastrar todos los vecinos del mismo sitio o dispositivo si el sitio es null) y resincronizaflapping_notificado_atpara todo el grupo. Cubre Capa 1 (mismo dispositivo) y Capa 2 (mismo sitio) en un solo cambio. - 192/192 tests pasan (+3 nuevos en
FlappingDetectionTest). - Pendiente: aprobación de Sergio para commit + push + deploy en
monitoreo.
Estado al 2026-05-12
- Detección de flapping funcionando (commit
058a22bdel 2026-05-11) pero generaba fan-out por métrica: un AP con 5 monitoreos en flapping = 5 correos. Sergio confirmó esta mañana con captura de Caborca: ~15 correos[FLAPPING RESUELTO]en 2 minutos. - Cambio aplicado hoy (pendiente de deploy + observación en vivo). Ver bitácora 2026-05-12.
ES Antenas (new)
Contexto
Plataforma interna de Electrosystems para monitoreo de enlaces y dispositivos de red. Recibe métricas SNMP (entre otras) de equipos en redes locales remotas, vía un collector externo (monitoreo-collector). Es el lado del API + dashboard de la dupla.
Va de la mano con monitoreo-collector: los collectors envían datos a este API.
Prioridad alta — proyecto activo importante para la operación.
Tareas pendientes
- (2026-05-12) Deploy del cambio de agrupación de flapping — desplegado en commit
8e8023bmaster. Ver bitácora 2026-05-12 abajo. - (2026-05-14) Observar próximos correos de flapping — confirmado que el agrupador funciona en correo inicial (caso Netonix 911 con 6 métricas en 1 correo). Reveló nuevo caso: los recordatorios llegaban sueltos porque sus
flapping_notificado_atquedan desincronizados entre métricas del mismo dispositivo/sitio. Fix aplicado en working tree, pendiente commit + deploy. Ver bitácora 2026-05-14. - (2026-05-11) Push notifications resuelto — instalado
php8.2-gmpen server. (Item olvidado de marcar; cerrado en limpieza 2026-05-14.) - (2026-05-14) Deploy del fix de recordatorios — commit
616d7fcdesplegado enmonitoreo. 192/192 tests pasan. Observar mañana ~07:00. - (2026-05-18) Limpiar master processes huérfanos de Horizon en
monitoreo— verificado hoy: 20 masters tracked por systemd, ninguno huérfano. Elhorizon:terminatedel deploy 2026-05-14 limpió los del 2026-05-12.horizon:statusreportarunning. (Originalmente capturado 2026-05-14.) - (2026-05-18) Permisos del log daily —
0ddec4f fix(logging): daily logs con permission 0664ya está en master y deployado. Logs desde 2026-05-15 nacen-rw-rw-r--. (Originalmente capturado 2026-05-12.) - (2026-05-18)
failed_jobs: 5,870 → 0 víaphp artisan queue:flushenmonitoreo(autorizado por Sergio). Eran todos artifact del bug de permisos del log; 0 fallas desde 2026-05-15. Distribución original:ProcesarAgregadosSitio4476,ProcesarSyncCollector1391,GenerarReporte3. - (2026-05-18) Commit + deploy del refactor de cascada en alertas/recuperaciones — el otro agente mergeó
feature/exportar-csv-uisp(6fda261) y borró la branch. Server resincronizado amastery deployado ad07540a. Cascada (despacharPendientes) confirmada cargada en server. Ver Bitácora 2026-05-18 “resolución de divergencia”. - (2026-05-18) Aplicar cascada
FlappingAgrupadora alertas/recuperaciones normales — refactor implementado enprocesarAlertas/procesarRecuperacionescon método nuevodespacharPendientes. Lógica de enlace intacta; pase de sitio-promoción agregado. Ver Bitácora 2026-05-18 para detalle de tests. - #262 📅 2026-06-02 — (sigue 2026-05-11) Confirmar con Sergio el resultado de su validación manual de correos. Sergio revisa el comportamiento en su Gmail durante el fin de semana 2026-05-09/10 y comparte si requiere ajuste.
- (2026-05-18) Investigar por qué no funcionan las notificaciones push (capturado 2026-05-08) — duplicado del item ya cerrado del 2026-05-11. Root cause real:
phpseclib3emitíaE_USER_NOTICEpor falta de GMP/BCMath, Laravel lo convertía enErrorExceptiony se tragaba el envío. Fix:sudo apt-get install -y php8.2-gmp+php8.2-fpm reload. Verificado entrega de push de prueba post-fix. Cerrado en limpieza 2026-05-18. - (2026-05-18) Instalar Node 20 + verificar build en server
monitoreo— hecho. El pendiente decía “no tiene npm”; realidad: tenía Node 16.20.2 (EOL) que no aguantaba Tailwind v4 + Vite 5/6. Upgrade vía NodeSource (setup_20.x+apt-get install nodejs) → Node 20.20.2 + npm 10.8.2.npm ci(605 paquetes, 37s) +npm run build(Vite 6.2.5, 24s) funcionan como userelectrosystems. Output del build idéntico byte-por-byte alpublic/build/commiteado (hashes determinísticos de Vite). 7/7 archivos. Ver Bitácora 2026-05-18. - (2026-05-21)
public/build/a.gitignore+git rm -r --cached— commit57796ba. 7 archivos eliminados del index;.gitignoreagrega/public/build. El deploy del 2026-05-21 regeneró los archivos connpm run build(Vite 6.2.5) sin problemas ygit statusqueda limpio.scripts/deploy.shya hacía el build, no hubo que documentar nada nuevo. - (2026-05-18) Script de deploy automatizado
scripts/deploy.sh— commiteado end07540ay desplegado al server. Primera prueba real será el siguiente deploy viassh monitoreo "/var/www/es-monitoreo/scripts/deploy.sh". ✅ Primera prueba real el 2026-05-20: 126s, exit clean, sin migraciones, log astorage/logs/deploys.log. - (2026-05-20) Fix de fan-out de recordatorios por sitio (caso Hercules) — commit
74d2e83. Cuando ≥2 enlaces de un sitio entran en falla casi simultánea conultimo_notificado_atdesfasado, el primer seed vencido arrastra a todos los del sitio en falla, se envía 1 correo de sitio, yultimo_notificado_atqueda resincronizado para el siguiente ciclo. Ver bitácora 2026-05-20. - #011 📅 2026-06-25 — (opcional, follow-up) Considerar el mismo filtro “excluir notificados muy recientemente” en
procesarFlapping’s expansión de RECORDATORIOS: hoy la query solo filtrawhereNotNull('flapping_notificado_at'), así que en un tick donde simultáneamente cae un nuevo flapping (block “detectado”) y otro vencía su recordatorio en el mismo sitio, podría salirse el correo “detectado” + “recordatorio” mencionando al recién detectado. Riesgo bajo (requiere 24h-vencido + nuevo en mismo tick). NOTA: con el debounce de detectados (commit017e1b9) la probabilidad bajó aún más porque el detectado ahora se retrasa 5 min. - #012 📅 2026-06-10 — Observar próximos eventos de flapping + recordatorios en vivo para confirmar consolidación en ambos caminos tras debounce + arrastre por sitio.
- (2026-05-20) Fan-out de FLAPPING detectado: debounce + arrastre por sitio en
procesarFlapping— commit017e1b9. Caso WiTek Hermosillo + Hércules: métricas que cruzaban el umbral N-en-15min en ticks consecutivos generaban 1 correo por tick. Fix: seeds esperanflapping.debounce_minutos(default 5 min) y arrastran hermanas del mismo sitio aún en debounce. Tests 237/237. Ver bitácora 2026-05-20. - (2026-05-21) Bug del conteo de workers en
scripts/deploy.shpostflight — commit57796ba. Root cause NO era el grep (manualmente regresa 20 correctamente); era timing:RestartSec=5en el unitlaravel-worker@.service+ boot de horizon (~varios segundos) ⇒ elsleep 5fijo no alcanzaba para que los workers estuvieranactive running. Fix: poll hasta 30s, sale en cuanto detecta ≥1. La corrida del 2026-05-21 todavía usó el postflight viejo (script se actualiza en elgit pullpero el shell ya lo cargó en memoria) y aun así reportó 20 —horizon:statusañadió segundos de buffer; el siguiente deploy estrenará el polling nuevo. - (2026-05-21) SNMP Fase 1 — fix puntual Rumurachi-Urique. Soft-delete
md_id=989(monitoreo 97 en dispositivo 206) + delete pivotmp(97,20). Dispositivo 206 ahora solo polla OIDs propietarios Cambium 17713 (monitoreos 116, 117) — estables ante reboot. Monitoreo 97 sigue vivo para Mimosa B11 y Ubiquiti AF11 (6 dispositivos) donde idx=3 sí aplica. - (2026-05-21) SNMP Fase 2 — RFC cerrado. Decisiones de diseño consensuadas (ver bitácora 2026-05-21 “Fase 2 RFC” abajo): sintaxis
{IF}, columnamonitoreos_dispositivos.interfaz(varchar(64) nullable), cache SQLite en collector con TTL 1h, error semantics “falla explícita con razón” via campo nuevorazon_fallaenLecturaCombinadaDetalle, tests primero como sesión propia (Fase 3a), implementación end-to-end como Fase 3b, migración de datos manual por lotes como Fase 4. - #007 📅 2026-06-05 — (Fase 3a) Scaffold de tests para
monitoreo-collector. El repo Go NO tiene tests hoy — antes de meter el resolver, montar harness:testify+ mock degosnmp(seagosnmp/gosnmp.MockSNMPo servidor SNMP en docker para integración) + 4-5 tests del resolver de{IF}(cache hit/miss, fallback ifDescr→ifName, interfaz no encontrada, invalidación onnoSuchInstance). ~1-2 hrs sesión propia. Sin tocar lógica de producción todavía. Salida: harness pasando + tests muriendo de manera predecible que validen futuras decisiones del resolver. - #008 📅 2026-06-12 — (Fase 3b) Implementación SNMP placeholder end-to-end. Sub-tareas:
- Migración Laravel
ALTER TABLE monitoreos_dispositivos ADD COLUMN interfaz VARCHAR(64) NULL AFTER ignorar. - Migración Laravel: agregar campo
razon_falla(varchar(255) nullable) a la tabla de lecturas/ingestion (lecturas_agregadaso donde corresponda — verificar enProcesarSyncCollectorjob). CollectorSyncController::getDispositivos: incluirinterfazen el payload del endpoint.ProcesarSyncCollector: leer y persistirrazon_fallacuando viene en el push del collector.- Collector Go: schema SQLite nueva tabla
interface_mappings, structDeviceMonitoragregar campoInterfaz, resolver de{IF}endoCollectionantes del batch Get (pre-walkifDescrfallbackifName, cache TTL 1h), invalidación ennoSuchInstance, OIDs no resueltos se OMITEN del batch (preservar comportamiento de error global delclient.Get) y se reportan comoDetalleconrazon_falla="interfaz X no encontrada". - Tests verdes (de Fase 3a).
- UI mínima: campo
interfazeditable en la asignaciónmonitoreos_dispositivos(admin). Opcional Fase 4 si tienes ganas de tinkers por ahora.
- Migración Laravel
- #009 📅 2026-06-22 — (Fase 4) Migración de datos por lotes. Crear monitoreos nuevos genéricos (
ifInOctets {IF},ifOutOctets {IF},ifHCInOctets {IF},ifHCOutOctets {IF},ifHighSpeed {IF},ifSpeed {IF}). Empezar piloto con MikroTik (mayor fragilidad — VLANs idx 2001/2005 cambian al menor reboot/restore). Por dispositivo: setearinterfazen cada asignación nueva, validar lecturas correctas durante 1-2 ciclos, soft-deletear las asignaciones viejas hardcodeadas. Auditoría completa: 15 monitoreos ifTable + 62 ifXTable. - #010 📅 2026-06-03 — Packet loss al AP Rumurachi-Urique (192.168.37.61). Ping desde
monitoreomostró 50% pérdida intermitente durante el diagnóstico. Probable causa de que el monitoreo 166 (ifSpeed.1LAN1) caiga en falla esporádicamente y se persista como 0. Investigar enlace/RF a Rumurachi (no es es-antenas-new — es operación de red, capturar también enprojects/electrosystems-network.mdsi aplica). - (2026-05-22) Fase 5 del flujo “Agregar candidato UISP” — asignar a enlace desde el edit. Sección “Enlaces” en
Forma.vue+ 4 endpoints + 9 tests Pest. Commitb6c30f5, deploy 72s. Ver Estado 2026-05-22 arriba. - (2026-05-22) “Copiar de otro dispositivo” generalizado (sale del filtro contraparte). Endpoint
/contraparte-monitoreos→/fuentes-de-copia. 23% de dispositivos en prod dejaron de ver “Sin contraparte aplicable” porque ya no se exige mismo-enlace, solo misma-plantilla. Ahora se copian los 3 camposhabilitado+limite+ignorar. Tests 254/254, commit14fac30, deploy 53s. Ver Estado 2026-05-22 arriba. - (2026-05-22) Insertar en medio del enlace con shift automático. Modal “Agregar a enlace” del #171: en vez de input numérico de posición, lista de N+1 slots visuales (
↑ Antes de X/↕ Entre X y Y/↓ Después de Z). Backend hace shift +1 en transacción a los pivots conposicion >= P. Cubre los 22/30 enlaces N≥3 de prod. Tests 256/256, commit0db3ef3, deploy 54s. Ver Estado 2026-05-22 arriba. - (2026-05-22) Probar conectividad SNMP antes de guardar. Botón “Probar SNMP con esta config” en sección Config SNMP del form (usa valores actuales del form, no del modelo). Smart fallback: si la versión solicitada no responde, intenta v2c y v1 y sugiere cambiar. Tests +6, suite 262/262 verde, commit
d73d0ab, deploy 71s. Ver Estado 2026-05-22 arriba. - #172 📅 2026-06-18 — (Fase 6 del flujo “Agregar candidato UISP”) Wizard 1-page de Agregar. Unificar todo el flujo en UNA sola vista en lugar del redirect actual
Agregar → /dispositivos/{id}/edit. Pantalla nueva (ruta dedicada/uisp-candidatos/{id}/wizardo modal grande en/uisp-candidatos) con: bloque plantilla (con preview de monitoreos que se van a prepoblar), bloque SNMP (community, versión), bloque variables de monitoreo (con botón “Copiar del contraparte” ya implementado en Fase 3), bloque enlace (Fase 5 reutilizable). Submit único: crea dispositivo + prepuebla monitoreos + sobreescribe los límites editados + asigna al enlace, todo en una transacción. Reemplaza el combo “click Agregar → redirect a edit → llenar SNMP → guardar → editar más → enlaces”. Depende de Fase 5 ya implementada para el bloque de enlaces. Trade-off: vista más densa, pero el operador no pierde estado entre saltos y todo el contexto del candidato UISP (IP, nombre, plantilla sugerida) queda visible mientras llena. - #190 📅 2026-06-20 — Retomar
/metricspara Prometheus. Hoy quedó desactivado (commit2efa022) porque el endpoint disparaba una query agregada sobrelecturas_historicas_detalles(49.6M filas) y colgaba PHP-FPM cuando un scraper lo pegaba. Cuando se vuelva a necesitar: (a) reescribirMetricsController::indexevitando el eager-loadwith('ultima_lectura_historica.detalles'); opciones probadas en diagnóstico: loop conDB::table('lecturas_historicas')->where('dispositivo_id', $id)->orderByDesc('fecha')->first()(N=317 queries triviales conlecturas_historicas_dispositivo_id_fecha_index, backward scan, lee 1 fila por query) + 1 query bulkwhereIn('lectura_historica_id', $ids)para los detalles, o LATERAL JOIN equivalente en SQL crudo. (b) Restaurar la rutaRoute::get('/metrics', [MetricsController::class, 'index'])y eluse App\Http\Controllers\MetricsController;enroutes/web.php. (c) Considerar autenticarla (hoy era pública). El archivoMetricsController.phpquedó intacto en el repo como referencia. - (2026-05-26) #191 Fix
NotificarLogs.php:686—Attempt to read property "enlaces" on null— commite589d67, deployado en 88s víascripts/deploy.sh. Causa raíz: dispositivo #313ST Caborca T1-La Gloria C5csoft-deleted 2026-05-22 dejó 5 huérfanos enestatus_monitoreos_dispositivos(md vivo, dispositivo trashed); comoEstatusMonitoreoDispositivo::factoryquery usabawhereHas('monitoreoDispositivo', notificable())ynotificable()no exigía dispositivo vivo, los huérfanos pasaban el filtro yflatMap(... ->dispositivo->enlaces->pluck('id'))reventaba la corrida entera. Fix: agregué->whereHas('dispositivo')alscopeNotificabledeMonitoreoDispositivo— elwhereHasrespeta el global SoftDeletes scope, así que filtra trashed automáticamente. Comonotificable()lo usan los 3 comandos de notificación (NotificarLogs, NotificarDigestNormal, NotificarUrgenteDigest), el cambio blinda el pipeline entero. Defensa-en-profundidad: filtro?->dispositivo !== nullal inicio deincluirVecinosDeEnlace. Test de regresión enNotificarLogsTest. Smoke test post-deploy:php artisan es:notificar-logscorre limpio. Datos huérfanos (21 filas total: 5 con dispositivo trashed + 16 con md trashed) quedan en BD pero ya no se notifican; si Sergio quiere cleanup arquitectónico (cascade soft-delete Dispositivo→md→estatus) capturo pendiente nuevo, lo dejé fuera por scope. - (2026-05-25) #193 Revisión integral del pipeline de ingesta — DOCUMENTO ENTREGADO. Diagnóstico completo en
PIPELINE_REVIEW_193.md: 12 hallazgos con file:line, 10 propuestas con esfuerzo/impacto/riesgos, recomendación priorizada en 3 sprints + lista explícita de descartes (NO Go, NO Kafka, NO reemplazar file-based, NO particionar por sitio). Sprint 1 son ~6-9 hrs (P1+P2+P10) y resuelve #194 + #195 sin tocar arquitectura. Ver Estado 2026-05-25 arriba. - (2026-05-25) #194 Bug deadlocks en
lecturas_historicas_detalles— Sprint 1 P1 commiteado, pendiente deploy + validación 48-72h. Commits19939e3. Tx reducida a solo 2 INSERTs;insertOrIgnore→insert;SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED. Ver bitácora 2026-05-25. - (2026-05-27) #194 Validación 48h+ post-deploy completa — CERRADO. 0 deadlocks 2026-05-26 y 2026-05-27 (criterio ≤10/día). Los 91 deadlocks contados el 2026-05-25 son todos PRE-deploy (último a las 13:06:41 MDT, deploy completó a las 13:16:32). El SP de agregados (
sp_calcular_agregados) tampoco generó deadlocks post-deploy — confirmado queREAD COMMITTEDper-session benefició ambas rutas como anticipaba el doc #193.failed_syncs/vacío. Ver bitácora 2026-05-27. - ARCHIVADO 2026-05-25 — sustituido por entrega P1 (Sprint 1) commit
19939e3ya commiteado; queda solo validación 48-72h en línea de arriba. - (2026-05-25) #195 Worker zombie del lock — Sprint 1 P2 commiteado, pendiente deploy. Commits
3ee9cd0. TTL a 60s +extend()por iteración + comandomonitoreo:unlock-stuck-sitescada 5 min. Ver bitácora 2026-05-25. - (2026-05-27) #195 Validación 48h+ post-hotfix #2 — CERRADO. 0 locks huérfanos vivos.
finallylibera por owner correctamente; cronunlock-stuck-sitessolo intervino 1 vez en 48h (señal positiva).failed_syncs/ni existe. Nazareno (id=8, Connection timed out) y Veracruz (id=10, NULL) son problemas separados de collector — capturar como ops si aplica. Ver bitácora 2026-05-27. - ARCHIVADO 2026-05-25 — sustituido por entrega P2 (Sprint 1) commit
3ee9cd0ya commiteado. - #196 📅 2026-06-02 — Tepehuanes collector remoto caído.
sitios.fecha_ultima_conexionpara Tepehuanes a las 13:00 estaba a 487 minutos (~8 horas sin reportarse). Sigue caído. Es problema del HOST físico en sitio (collector Go corriendo en un equipo en Tepehuanes) — no del server. Pendiente operacional: investigar conectividad/equipo en Tepehuanes. Capturar enelectrosystems-network-mapcuando esté listo. - (2026-05-23) #192 Fix de fondo del bug de permisos del log daily —
sudo chmod g+s /var/www/es-monitoreo/storage/logs/+sudo chgrp www-data *.log+sudo chmod 664 *.log. Verificado:touchcomo user electrosystems crea archivoelectrosystems:www-data 664(group heredado del directorio gracias a setgid). El bug no debería volver — independiente del primer escritor del día, todos los logs nuevos nacerán con group www-data y www-data podrá escribir. Memoria del hubreference-laravel-daily-log-permsactualizada con el fix confirmado.
Contexto
Sergio hizo recientemente un cambio en cómo se mandan los correos del sistema. La validación durante el fin de semana mide:
- ¿Se mandan en los tiempos esperados?
- ¿Se mandan con la frecuencia correcta (ni de más, ni de menos)?
La validación es manual por Gmail. No requiere acción mía hasta que Sergio diga.
En progreso
(Por confirmar con Sergio.)
Alias
Cuando Sergio dice “el proyecto monitoreo” sin más contexto, se refiere a este (es-antenas-new), no a monitoreo-collector.
Notas técnicas
Stack
- Laravel 11, PHP 8.2.30
- Inertia v2 + Vue 3
- Tailwind v4 (CSS-first config con
@theme, sintailwind.config.js) - Laravel Horizon (queues con UI), Sanctum (auth API), Fortify (auth web)
- Laravel Sail (Docker)
- Pest 3 + PHPUnit 11
⚠️ Reglas de idioma específicas (del .ai/lenguaje rules del proyecto)
- UI: español.
- Nombres de variables: español (excepto términos comunes de código en inglés como
counter,container). - Comentarios en código: español.
- Respuestas del agente: inglés, aun cuando Sergio escriba en español.
Esto es opuesto al default del hub (donde respondo en español). Cuando estemos trabajando dentro de este codebase, respondo en inglés.
Inertia v2 features disponibles
Deferred props (con skeletons), infinite scroll con merging props, polling, prefetching, useForm helper.
Bitácora
2026-05-29 — fix del warning de Horizon en deploy.sh aplicado
- Aplicado el reorden propuesto: el check
horizon:statusahora corre después del poll de workers systemd (ya con Horizon respawneado), no antes. Commit81eab3ben master (push hecho).bash -nOK. - Toma efecto en el siguiente deploy (el
git pulldel inicio dedeploy.shtrae el script nuevo); no se requirió re-deploy ahora. Con esto desaparece elHorizon is inactivefalso de cada deploy.
2026-05-29 — post-#415: .env alineado + warning de Horizon diagnosticado (falso positivo)
- Sergio confirmó que el fix de #415 funciona en ambos paths. Pidió (a) alinear el
.envde prod por higiene y (b) revisar el warning de Horizon del deploy. - (a)
.envde prod:SESSION_SECURE_COOKIEtrue→false (backup/var/www/es-monitoreo/.env.bak-20260529-415). Sin config cache → aplicó directo. El middleware de #415 ya gobernaba por request; esto solo quita la contradicción del archivo base. - (b) Horizon — NO hay problema real.
horizon:status= running. Corre 1 master por systemd (laravel-worker@1.service, active),artisan horizon+ supervisor--balance=auto --max-processes=10 --min-processes=1 --queue=default,aggregatessobreQUEUE_CONNECTION=redis. Es el patrón correcto (1 master systemd escalando interno, no 20 instancias — ver memoriareference_horizon_single_systemd_master). - Causa del warning: en
scripts/deploy.shel checkhorizon:status(líneas ~121-125) corre justo después dehorizon:terminate(línea 117) y antes del poll de systemd (líneas ~129-135). En ese instante Horizon está caído (recién terminado, aún sin respawn) → siempre reporta “inactive” → warning falso en cada deploy. El poll posterior sí valida bien (“workers systemd activos: 1”). - Fix propuesto (pendiente de aplicar): mover el bloque de
horizon:statusa después del poll de workers systemd (opcional: con pequeño retry), para que se evalúe ya con Horizon respawneado. Solo afectadeploy.sh; aplica en el siguiente deploy una vez pusheado. Pendiente de autorización de commit. - Pidió Sergio: atacar #415, dejando funcionales ambos paths (HTTPS por dominio vía VM
reverse_proxy+ HTTP por IP en LAN). Eligió el enfoque de middleware dinámico. - Diagnóstico afinado: TrustProxies NO estaba configurado →
$request->isSecure()dabafalseen ambos paths; el login HTTPS funcionaba solo porque la cookiesecurela guarda el navegador del lado HTTPS. ConSESSION_SECURE_COOKIE=true, el acceso por HTTP/IP descartaba la cookie → 419. - Hice (commit
a55cddcen master, deployado):- Nuevo middleware
app/Http/Middleware/ConfigureSecureSessionCookie.php(prepend del grupoweb, antes de StartSession/ValidateCsrfToken): fijaconfig(['session.secure' => ...])por petición =truesolo si la request es HTTPS (directo o víaX-Forwarded-Protodel reverse_proxy). Confiar en ese header para esto es seguro (spoofearlo solo haría la cookie MÁS restrictiva). .env.examplebase afalse+ comentario explicando que el middleware lo gobierna.- 2 tests Pest (
SecureSessionCookieTest) verdes. Pint OK. - Deploy vía
scripts/deploy.sh(nothing to migrate, caches limpiadas) en 162s.
- Nuevo middleware
- Verificado en prod (curl):
GET /loginpor HTTP/IP → cookieXSRF-TOKENsinsecure; conX-Forwarded-Proto: https→ consecure. Ambos paths correctos, login por IP arreglado sin perder el endurecimiento en HTTPS. NO se tocó el.envde prod (el middleware sobreescribe por request). - Nota ajena: el deploy reportó
Horizon is inactivepero “workers systemd activos: 1” — el proyecto usa workers systemd, no el daemon Horizon; no es regresión de este cambio. Revisar si ese warning del deploy script conviene silenciarlo. - Resuelto 2026-05-29 [#415]: error 419 al login por IP
192.168.20.17— middlewareConfigureSecureSessionCookie(flag Secure por esquema), deployado y verificado en ambos paths.
2026-05-29 — #415 capturado: bug 419 al entrar por IP (reatribuido desde amadeus)
- Pidió Sergio: investigar en prod el bug “419 al login por
192.168.20.17” (originalmente apuntado en amadeus como #413) y confirmar si de verdad es de amadeus. - Investigación read-only en prod: la VM amadeus es
192.168.20.20y su cookie de sesión eselectrosystems_session. La IP192.168.20.17emitees_monitoreo_session→ es esta app (monitoreo), no amadeus. Causa del 419: cookies con flagsecureservidas por HTTP plano → el navegador no las guarda → POST de login sin CSRF → 419. Detalle en la sección 🐛 Bugs. - Resultado: #413 de amadeus reatribuido aquí como #415 (nuevo ID porque #413 ya estaba tomado por vpn-clientes en sesión paralela). Falta decidir/aplicar el fix (redirect HTTP→HTTPS o
securecondicional por IP).
2026-05-27 — #195 cerrado: 0 locks huérfanos vivos post-hotfix #2
Pidió Sergio (sesión multi-proyecto): validar #195 en paralelo a #194.
Diagnóstico (read-only en prod monitoreo vía db-investigator subagente):
cache_locks: 0 filas al cierre (12:31 MDT). Durante observación se vio 1 lock activosync_lock_sitio_1que se liberó porfinallydel job antes del TTL de 300s.- 0 huérfanos vivos.
- 12/14 sitios actualizados en los últimos minutos. Nazareno (id=8) con
Connection timed outenmensaje_conexiony Veracruz (id=10) con campo vacío — ambosfecha_ultima_conexion=NULL. NO son problema de lock; son fallas operacionales de collector como Tepehuanes (#196). Capturar como ops separados si aplica. - Cron
unlock-stuck-sitesestá enroutes/console.phpconeveryFiveMinutes()->withoutOverlapping(). Evidencia: exactamente 1 DELETE encache_locksregistrado el 2026-05-27 09:13:27 MDT. Baja frecuencia = señal positiva (elfinallycasi siempre libera por owner). storage/app/failed_syncs/no existe.storage/app/syncs/tampoco. Sólokeys/,private/,public/.
Cierre: criterios cumplidos. #195 marcado [x]. Si Sergio quiere abrir tickets ops por Nazareno + Veracruz, sería un nuevo ID (no parte del lock zombie).
2026-05-27 — #194 cerrado: 0 deadlocks en 48h+ post-deploy Sprint 1 P1
Pidió Sergio (sesión multi-proyecto): validar #194 al cumplirse las 48-72h del criterio de cierre.
Diagnóstico (read-only en prod monitoreo, vía db-investigator subagente):
- Conteo por día (grep
Deadlocken/var/www/es-monitoreo/storage/logs/laravel-YYYY-MM-DD.log):- 2026-05-25: 91 — TODOS pre-deploy (último 13:06:41 MDT; el deploy completó 13:16:32 según
deploys.log). - 2026-05-26: 0.
- 2026-05-27: 0.
- 2026-05-25: 91 — TODOS pre-deploy (último 13:06:41 MDT; el deploy completó 13:16:32 según
- Los 91 del 2026-05-25 son del SP
sp_calcular_agregados(ON DUPLICATE KEY UPDATE sobrelecturas_agregadas), no ingesta directa. - El SP también quedó limpio post-deploy — confirma la hipótesis del doc #193:
READ COMMITTEDper-session beneficia ambas rutas porque el SP corre como query del mismo job de ingesta. P3 (sacar SP a cron dedicado) descartado como innecesario. failed_syncs/no existe (completamente limpio).SHOW ENGINE INNODB STATUSúltimo deadlock visible 2026-05-25 13:06:41.
Cierre: criterio ≤10/día cumplido holgadamente. #194 marcado [x] en el doc y PENDIENTES.md.
Implicaciones para Sprint 2 (#193 follow-up): con SP de agregados estable, Sprint 2 queda solo con P4 (batch UPSERT) y P9 (cron marcarDispositivosSinDatos), ambos opcionales — no urgentes.
2026-05-26 tarde — #191 fix de raíz al crash de NotificarLogs (NULL dispositivo)
Pidió Sergio: “Ataquemos el pendiente #191” (proyecto monitoreo).
Diagnóstico (read-only en prod):
EstatusMonitoreoDispositivo: 824 totales, 21 huérfanos: 16 conMonitoreoDispositivosoft-deleted (ya filtrados pornotificable()viawhereHas), 5 con MD vivo pero Dispositivo soft-deleted (no filtrados — éstos crashan).- Los 5 huérfanos vivos vienen todos del dispositivo
#313 ST Caborca T1-La Gloria C5c(soft-deleted 2026-05-22 15:49:27). Filasestatus_monitoreos_dispositivos.id801-805, todas enerrordesde 2026-05-22 15:45:21. - IDs #801 y #802 con
ultimo_notificado_at = NULL→ entran al bucket de “alertas iniciales” →incluirVecinosDeEnlace→$e->monitoreoDispositivo->dispositivo->enlaces->pluck('id')revienta porquedispositivoes null.
Fix (commit e589d67, deployado 88s):
app/Models/MonitoreoDispositivo.php:scopeNotificableahora incluye->whereHas('dispositivo'). ComoDispositivousaSoftDeletes, elwhereHasrespeta el global scope y filtra trashed automáticamente — sin tocar nada más, todos los huérfanos quedan invisibles paraNotificarLogs,NotificarDigestNormalyNotificarUrgenteDigest(los 3 comandos que usannotificable()).app/Console/Commands/NotificarLogs.php: filtro?->dispositivo !== nullal inicio deincluirVecinosDeEnlacecomo defensa-en-profundidad para el caso “borrado entre query y procesamiento”.tests/Feature/NotificarLogsTest.php: test de regresión “no revienta ni notifica cuando el dispositivo fue eliminado (soft-delete)” — crea estatus huérfano + estatus sano, soft-deletea el dispositivo del primero, corre el cron, valida exit code 0 + 1 solo correo (el sano).
Verificación:
- Suite NotificarLogs: 51/51 verde (era 50/50 + el nuevo). Otras suites de notificación (NotificarDigestNormal, NotificarMonitoreosIgnorados, NotificarSitiosOffline): 29/29 verde.
- Suite global: 263 pasan, 4 falladas pre-existentes (
MonitoreoUnlockStuckSitesTest×3 +ProcesarSyncCollectorP1Test×1, todas del hotfix #193 — verificado congit stash). - Smoke test post-deploy en prod:
ssh monitoreo "php artisan es:notificar-logs"→ “No hay notificaciones pendientes” (los 5 huérfanos ya se filtraron en el query principal; antes hubieran reventado).
Lo que NO se hizo (out of scope):
- Limpieza de las 21 filas huérfanas en BD. El fix las ignora pero quedan vivas. Si Sergio quiere, capturo pendiente nuevo para “cascade soft-delete Dispositivo→MonitoreoDispositivo→EstatusMonitoreoDispositivo” como mejora arquitectónica (hoy el soft-delete del dispositivo no toca sus dependencias).
2026-05-25 — Sprint 1 (#193 P10+P2+P1) implementado — 3 commits en master
Pidió Sergio: ejecutar Sprint 1 completo del doc PIPELINE_REVIEW_193.md (P10 + P2 + P1), 3 commits separados, sin push ni deploy.
Commits entregados:
d4b1759— P10: borradoapp/Jobs/ProcesarMonitoreoDispositivo.php(handle no-op) +tests/Feature/UniqueMonitoreoJobTest.php. Sin referencias externas de dispatch en prod.3ee9cd0— P2:Cache::lockTTL 600s → 60s;extend(60)en cada iteración del while (si retorna false, warning + break para no pisar otro proceso); nuevo comandomonitoreo:unlock-stuck-sitesque elimina filas decache_locksconexpiration < now()(zombies que no pasaron porfinally); scheduleeveryFiveMinutesenroutes/console.php; 3 tests Pest entests/Unit/Console/MonitoreoUnlockStuckSitesTest.php.19939e3— P1:SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTEDantes de abrir tx; Fase 1 (pre-cómputo en memoria, sin escrituras):EstatusMonitoreoDispositivoy todos los valores acumulados en arrays; Fase 2 (tx mínima): loopinsertGetIdlecturas_historicas + acumula detalles con ID real +insert()detalles en chunks 500 (ya noinsertOrIgnore); Fase 3 (post-commit, try/catch silencioso):EstatusMonitoreoDispositivo::firstOrCreate+actualizarDesdeEstatus+EstatusDispositivo::updateOrCreate+Dispositivo::revisarEstatus; 2 tests Pest entests/Feature/ProcesarSyncCollectorP1Test.php.
Divergencias del plan vs código real:
- El doc decía que
$this->sitio->update + revisarEstatusestaba DENTRO de la tx (L436-441 antes del commit según el análisis estático). En el código real YA estaba fuera de la tx (después delDB::commit()). Confirmado en lectura del archivo. - Cache default del proyecto es
database(tablacache_locks), no Redis. El comandounlock-stuck-sitestrabaja conDB::table('cache_locks')en lugar deRedis::keys(). La lógica de “expiración pasada” es la misma (columnaexpirationcomo timestamp Unix vs TTL < 0 en Redis). - El doc mencionaba
App\Console\Kernel.phppara el schedule; el proyecto usaroutes/console.php(Laravel 11 style). Se usó la convención del proyecto.
Tests: DB de tests no disponible en el entorno (sin Docker). Tests compilaron sin errores de sintaxis. Los tests de DB requerirán el entorno de Sail en prod/dev con BD para correrse.
NO se hizo push ni deploy.
2026-05-25 — #193 entregado: documento de revisión integral del pipeline de ingesta
Pidió Sergio: “vamos a seguir con el #193”. El pendiente pedía un documento de hallazgos + propuestas + recomendación priorizada para el pipeline completo (collector Go + jobs Laravel + MySQL), abierto a cambios de stack.
Cómo se hizo: despaché architect agent con contexto completo (síntomas reales del 2026-05-23, objetivos de Sergio, rutas absolutas de ambos repos, reglas del hub). Sin acceso a prod en esta sesión — el doc es 100% lectura de código + razonamiento sobre el comportamiento de InnoDB.
Entregable: projects/es-antenas-new/PIPELINE_REVIEW_193.md (382 líneas). Convertí projects/es-antenas-new.md plano en projects/es-antenas-new/ carpeta hermana (sin tocar el .md plano, solo agregué el doc nuevo dentro de la carpeta).
Hallazgos principales:
lecturas_historicas_detallesNO tiene UNIQUE (verificado en 3 migrations: 2026_03_31_160851, 2026_04_10_134111, 2026_04_14_154855). ElinsertOrIgnorees semánticamente vacío pero cambia el modo de locking de InnoDB.- La transacción del job cubre L143-408 — incluye ~7 side-effects que NO necesitan atomicidad (
EstatusMonitoreoDispositivo::firstOrCreate+actualizarDesdeEstatuspor cada detalle,EstatusDispositivo::updateOrCreate,Dispositivo::find+update+revisarEstatus,Sitio::update+revisarEstatus). Cada operación toma row locks adicionales en tablas distintas. Cache::lockper-sitio dura 600s en Redis — si el worker revienta por OOM/kill -9, no pasa porfinally, lock queda huérfano. Cualquier dispatch en los siguientes 600s se retira silenciosamente.- El “fix de cron” actual del SP
sp_calcular_agregadosconretry(3, …, 100ms)aplaza pero no resuelve — el SP también compite por locks contra la ingesta. - No se necesita reescribir nada en Go. Throughput real ~50 inserts/seg en detalles + ~3/seg en históricos = ridículamente bajo para MySQL. El problema es código (transacciones + isolation +
INSERT IGNOREvacío), no escala.
Recomendación priorizada del doc:
- Sprint 1 (~1 día): P1 (shrink tx + drop
insertOrIgnore+ READ COMMITTED, 4-6 hrs) + P2 (lock corto + refresh + unlock-cron, 2-3 hrs) + P10 (limpiarProcesarMonitoreoDispositivovacío, 30 min). Métrica: ≤10 deadlocks/día, 0 worker zombies. - Sprint 2 condicional: P3 SP agregados a cron (1-2 días), P9
marcarDispositivosSinDatosa cron (2 hrs), P4 batch UPSERT EMD (1 día). - Sprint 3 condicional: P6 particionar
lecturas_historicas_detallespor mes (3-5 días) — gatillo a >100M filas o queries de dashboard >5s. Hoy son 49.6M, ~3 años de margen.
Descartes explícitos con justificación: P5 reemplazar file-based (el modelo es durable y NO es el cuello; SSDs hacen 100K+ IOPS), P7 particionar por sitio (20 sitios no es shardable rentablemente), P8 reescribir en Go (no resuelve los deadlocks; PHP no es el cuello).
Próximo paso: Sergio decide si autoriza Sprint 1. Si sí, próxima sesión arranca con P1+P2+P10 sobre ~/code/es-antenas-new. Antes de tocar código conviene contestar las 7 preguntas abiertas de la sección 6 del doc (especialmente isolation level actual del MySQL en prod y redis-cli KEYS 'laravel_cache:sync_lock_sitio_*' para confirmar lock huérfano vivo).
Estado de #194 y #195: ambos siguen abiertos pero ahora tienen causa raíz confirmada y fix mapeado (P1 cierra #194, P2 cierra #195). No abrí IDs derivados — son los mismos pendientes con plan de acción concreto.
Repos al día (regla del hub): git fetch en es-antenas-new y monitoreo-collector + pull --ff-only en es-antenas-new (estaba behind 1 de origin/master).
2026-05-21 — SNMP Fase 2 RFC cerrado (sintaxis, schema, cache, error semantic, plan)
Pidió Sergio: “vamos a la siguiente fase” → Fase 2 = RFC del diseño. Sin código en esta sesión; salida = decisiones documentadas para que la Fase 3 pueda ser una sesión limpia.
Exploración previa (sub-agente Explore en monitoreo-collector):
- Poll batch single-call:
internal/collector/snmp.go:GetOIDsusaclient.Get(oids)con TODOS los OIDs del dispositivo en una sola llamada. Error global: si UN OID falla, falla el batch entero. Implicación: el resolver de{IF}debe garantizar que NUNCA se incluya un OID no resoluble en el batch. - Sin cache hoy:
internal/storage/sqlite.gosolo tienedispositivos,monitoreos_dispositivos,pending_syncs. Cache deifDescr→idxrequiere tabla nueva. - API payload simple:
internal/syncapi/api_client.gotipoDeviceMonitor {ID, DispositivoID, MonitoreoDispositivo, OID, OIDs}. AgregarInterfaz stringes trivial. - Sin tests: el collector NO tiene tests (búsqueda
*test*vacía). Riesgo alto si metemos resolver sin scaffold. - Main loop: 3 goroutines en
main.go: config-sync 1min, collection 1min wall-aligned, data-sync 30s.doCollection(línea ~170-374) ya pre-agrupa OIDs por dispositivo — buen lugar para inyectar el resolver.
Decisiones del RFC (consensuadas con Sergio vía menús):
A. Sintaxis del placeholder: {IF}
- Ej:
.1.3.6.1.2.1.31.1.1.1.6.{IF}(ifHCInOctets dinámico). - Razón: claramente no es OID válido (las llaves rompen el regex
[0-9.]+), corto, fácil de leer en BD. - Alternativas descartadas:
${if}(colisión visual con env vars),{{IF}}(verbose).
B. Schema en es-antenas-new
- Migración:
ALTER TABLE monitoreos_dispositivos ADD COLUMN interfaz VARCHAR(64) NULL AFTER ignorar. - Semántica: si NULL → comportamiento actual (OID se pasa tal cual al collector). Si set Y OID contiene
{IF}→ resolver dinámico. CollectorSyncController::getDispositivosagrega el campo al payload del endpoint.- Validación UI: deferred a Fase 4 (admin puede setear via tinker o SQL directo en Fase 3b).
C. Cache + resolución en collector Go
- Tabla SQLite nueva:
interface_mappings (device_id, interface_name, if_index, resolved_via, resolved_at).resolved_via∈ {ifDescr,ifName}. - Flujo por poll-cycle:
- Agrupar OIDs por dispositivo (
doCollectionya lo hace). - Si dispositivo tiene OIDs con
{IF}y cache stale/missing para alguna interfaz: pre-walkifDescr(.1.3.6.1.2.1.2.2.1.2) → fallbackifName(.1.3.6.1.2.1.31.1.1.1.1). - Sustituir
{IF}por idx resuelto en cada OID antes del batch Get. - Si una asignación no resuelve: omitir del batch + push de detalle con
razon_falla. NO tumbar el batch entero (proteger los OIDs que sí resuelven).
- Agrupar OIDs por dispositivo (
- TTL cache: 1h. Invalidación adicional: en cada
noSuchInstancepara un OID resuelto via{IF}, invalidar(device, interface)y re-walk el próximo ciclo.
D. Error semantics: falla explícita con razón
- Sergio escogió esta opción sobre las alternativas (log-only, valor-0-con-flag).
- Implica extender
LecturaCombinadaDetallecon camporazon_falla string(omitempty en Go, nullable en Laravel). - Cambio chico pero toca contrato API —
ProcesarSyncCollector(job de ingestión) tiene que aceptar y persistir el campo. - UI puede mostrar “Interfaz LAN1 no encontrada en dispositivo” en vez de 0 silencioso.
E. Tests del collector (Fase 3a, sesión propia ~1-2 hrs)
- Sergio escogió “scaffold mínimo + tests del resolver” como pre-requisito antes de la implementación.
- Stack propuesto:
testify+ mock degosnmp(la lib tienegosnmp/gosnmp.MockSNMP, o usar un servidor SNMP en docker para integración). - Casos mínimos del resolver:
- Cache hit: dispositivo con cache fresh → no walk, sustitución directa.
- Cache miss: dispositivo sin cache → walk
ifDescr→ match → cache → sustitución. - Fallback:
ifDescrno responde o no matchea → fallback aifName→ match → cache. - Interfaz no encontrada: walk completo sin match → omitir OID + reportar
razon_falla. - Invalidación:
noSuchInstanceen un OID resuelto via{IF}→ invalidar cache, re-walk próximo ciclo.
- Sin tests del feature todavía, solo el harness + tests del resolver puro.
F. Migración de datos (Fase 4, manual por lotes)
- NO migración automática. Los 77 monitoreos hardcodeados conviven con el nuevo modelo indefinidamente.
- Crear monitoreos genéricos:
ifInOctets {IF},ifOutOctets {IF},ifHCInOctets {IF},ifHCOutOctets {IF},ifHighSpeed {IF},ifSpeed {IF}. - Piloto: MikroTik primero — mayor fragilidad porque VLANs y bonding (idx 2001/2005, 1000010+) cambian al menor reboot/restore.
- Por dispositivo: setear
interfazen asignaciones nuevas, validar 1-2 ciclos, soft-deletear las viejas hardcodeadas.
G. Lo que NO se hizo hoy
- Cero código. Cero migraciones. Cero deploys.
- No agregué TaskCreate porque esta fase fue puro RFC; las próximas fases son sesiones propias que sí merecerán task tracking.
Próximos pasos
- Fase 3a (sesión propia ~1-2 hrs): scaffold tests del collector. No bloqueante con ninguna otra cosa del proyecto.
- Fase 3b (sesión propia ~2-4 hrs): implementación end-to-end (migración + endpoint + collector + tests + deploy + smoke en 1 dispositivo MikroTik).
- Fase 4 (sesión propia, repetible): migración por lotes.
2026-05-21 — SNMP Fase 1: fix puntual Rumurachi-Urique + diseño consensuado para Fase 2-4
Pidió Sergio: “vamos a darle al SNMP” → Fase 1 acotada (fix Rumurachi solo), diseño del placeholder se discute en sesión separada con calma.
Investigación previa al fix:
Mapeo del modelo (sub-agente db-investigator + Explore):
monitoreos.llave_valor(string libre) — almacena OID + ifIndex hardcodeado, o lista coma-separada, o path de API propietaria. Sin campo de interfaz.monitoreos_dispositivos(pivot dispositivo↔monitoreo) — tienelimite,multiplicador,divisor,ignorar,deleted_at(SoftDeletes). No tiene campo de interfaz hoy.- M:M plantilla↔monitoreo via
monitoreos_plantillascon etiqueta override. - 77 monitoreos con ifIndex hardcodeado en
llave_valor: 15 ifTable (.1.3.6.1.2.1.2.2.1.*.<N>), 62 ifXTable (.1.3.6.1.2.1.31.1.1.1.*.<N>). Idx varían entre 1-15, 25, 2001/2005 (MikroTik VLANs), 1000010/11/12 (EdgeRouter bonding). - Polling real vive en
monitoreo-collector(Go) congosnmp— pull cada 60s aGET /api/sitios/{id}/dispositivos, ejecutagosnmp.Get(oids), push aPOST /api/sitios/{id}/sync. El pathDatosDispositivo::datosSnmp(Laravel + ext SNMP de PHP) prácticamente no se usa.
Alcance del monitoreo 97 (sub-agente db-investigator, query 4 + cross-checks):
- Asignado a 7 dispositivos vía
monitoreos_dispositivos: 5 Mimosa B11 (plantilla 14), 1 Ubiquiti AF11 (plantilla 19), 1 Cambium 4600C (plantilla 20, el roto = dispositivo 206 Rumurachi-Urique). - Vive en 3 plantillas via pivot: SNMP genérica, Mimosa B11, Cambium 4600C.
- No se puede borrar el monitoreo entero — rompería los otros 6 dispositivos donde idx=3 sí aplica.
Mutaciones autorizadas y ejecutadas (vía php artisan tinker sobre ssh monitoreo, autorización explícita per-sesión de Sergio):
-
Soft-delete de
monitoreos_dispositivos.id=989(monitoreo 97 en dispositivo 206).- Primer intento fue
delete()raw que falló conFK violation(tablalecturas_agregadastiene FKmonitoreo_dispositivo_id). - Verifiqué en
app/Models/MonitoreoDispositivo.php:27que el modelo usaSoftDeletestrait, y enapp/Http/Controllers/CollectorSyncController.php:25que el endpoint del collector usa->with(['monitoreos_dispositivos.monitoreo'])(Eloquent → respeta soft-delete por default). - Verifiqué que el flag
ignorarSOLO silencia alertas (scopeNotificableen línea 119 del modelo), no detiene el polling. Por eso elegí soft-delete. - Ejecutado:
App\Models\MonitoreoDispositivo::find(989)->delete()→deleted_at = 2026-05-21 14:00:12 MDT. Visible sinwithTrashed: NO ✓.
- Primer intento fue
-
DELETE FROM monitoreos_plantillas WHERE monitoreo_id=97 AND plantilla_id=20(preventivo — Cambium 4600C ya no hereda el monitoreo a futuros dispositivos del modelo). 1 row deleted. Plantilla 20 ahora solo tiene monitoreos 116 (Downlink Quality) y 117 (Downlink Capacity), ambos OIDs propietarios Cambium 17713 estables.
Descubrimiento paralelo durante la verificación:
El monitoreo 166 (1 Link Speed v1, ifSpeed.1 → LAN1) que la bitácora del 2026-05-20 asumía activo en dispositivo 206 ya estaba soft-deleted desde 2026-05-04 11:53:51 — probablemente cuando Sergio reconfiguró la plantilla 20 ese día (timestamps coinciden con monitoreos_plantillas id=287 del 2026-05-04 12:00). También md_id=1647 (monitoreo 115) está soft-deleted desde 2026-05-04 12:00:52. Esto invalida parte del diagnóstico del 2026-05-20: el único OID activamente roto en dispositivo 206 era el 97; el packet loss al AP sigue siendo problema de red aparte pero no estaba generando alertas en es-antenas-new.
Estado post-fix de dispositivo 206 (AP Rumurachi-Urique 4600C, plantilla 20):
- md=990 → monitoreo 116 Downlink Quality (OID
.1.3.6.1.4.1.17713.21.1.2.30.1.20.1) ✓ activo - md=991 → monitoreo 117 Downlink Capacity (OID
1.3.6.1.4.1.17713.21.1.2.30.1.19.1) ✓ activo - md=989 → monitoreo 97 ifSpeed.3 ✗ soft-deleted hoy
- md=1626 → monitoreo 166 ifSpeed.1 ✗ soft-deleted 2026-05-04
- md=1647 → monitoreo 115 (no investigué qué era) ✗ soft-deleted 2026-05-04
Dispositivo ahora polla solo OIDs propietarios Cambium 17713 — estables ante cualquier reboot porque NO dependen de ifIndex. Es exactamente el outcome deseado para este equipo.
Diseño consensuado para Fase 2-4 (no implementado, capturado como pendiente):
- Schema: columna nueva
monitoreos_dispositivos.interfaz(varchar nullable, p.ej."LAN1","WLAN1","ether3"). - OID con placeholder:
monitoreos.llave_valoradmite{IF}donde antes iba.<idx>. Ej:.1.3.6.1.2.1.31.1.1.1.6.{IF}. - Collector Go (
monitoreo-collector): si OID contiene{IF}y la asignación tieneinterfaz, walk aifDescr(fallbackifName), match exacto, append idx resuelto, gosnmp.Get. CacheifDescr→idxpor dispositivo en SQLite local con TTL 1h. Invalidate ennoSuchInstance. Si match no encontrado: registrar falla con razón explícitainterfaz "X" no encontradaen lugar de devolver 0 silenciosamente. - Back-compat 100%: si OID no tiene
{IF}o asignación no tieneinterfaz, comportamiento actual. - Beneficio largo plazo: los 77 monitoreos hardcodeados colapsan a ~5 (
ifInOctets,ifOutOctets,ifHCInOctets,ifHCOutOctets,ifHighSpeed,ifSpeed). Cada dispositivo asigna esos 5 + escoge interfaz por asignación. - Decisión de Sergio sobre source-of-truth de interfaz:
ifDescrcon fallbackifName(estándar histórico, presente en todos los firmwares).
Lo que NO se hizo hoy:
- Cualquier cambio en
monitoreo-collector(Go). El collector sigue como está. - Cualquier cambio en schema de
es-antenas-new. Sin migraciones nuevas. - Cualquier toque a monitoreos en plantilla 14 (Mimosa B11) ni plantilla 19 (Ubiquiti AF11).
- Cualquier toque al packet loss del enlace Rumurachi (es problema de operación de red, capturado aparte).
2026-05-21 — limpieza: public/build/ untracked + fix conteo de workers en deploy.sh
Pidió Sergio: “empecemos con limpieza y bugs menores”.
Cambios (commit 57796ba, 9 files, +13/-169):
-
/public/builden.gitignore+git rm -r --cached public/build/(7 archivos).- Justificación: Vite con hash determinístico ⇒ el build del server reproduce byte-por-byte (validado el 2026-05-18). Versionarlos solo agregaba ruido al diff.
- El deploy regenera los archivos vía
npm ci && npm run build(que ya estaba enscripts/deploy.sh). - Ventana de “no hay assets” durante el deploy: ~60s (npm ci + build). Aceptable porque deploys son raros y fuera de horas pico. Esta ventana ya existía parcialmente porque los hashes de Vite cambian de versión a versión.
-
Fix postflight worker count en
scripts/deploy.sh(scripts/deploy.sh:119-141).- Síntoma reportado el 2026-05-20: “workers systemd activos: 0” en el log del deploy, cuando en realidad había 20 corriendo.
- Root cause confirmado por SSH read-only: el grep está bien (
systemctl list-units 'laravel-worker@*' --no-legend | grep -c 'active running'regresa 20 correctamente). El problema es timing —laravel-worker@.servicetieneRestartSec=5y horizon tarda varios segundos extra en bootear, así que elsleep 5 + 1 checkquedaba justo antes de que los workers pasaran aactive running. - Fix: poll de hasta 30s con paso de 2s, sale en cuanto detecta ≥1 worker activo. Si tras 30s sigue en 0, warn (no error — el deploy ya terminó).
Deploy del 2026-05-21 12:32 MDT: 117s, sin migraciones, npm ci+npm run build regeneraron public/build/ con manifest+sw+4 assets (Vite 6.2.5). El postflight ejecutado fue el VIEJO (el shell ya tenía cargado el script en memoria antes del git pull); aun así reportó 20 workers — probablemente porque horizon:status añadió segundos de buffer entre el sleep 5 y el grep. El siguiente deploy estrenará el postflight nuevo con polling.
Verificación post-deploy:
ssh monitoreo→git log -1=57796ba✓grep -c 'deadline=' scripts/deploy.sh= 1 (nuevo polling presente) ✓ls public/build/=assets/,manifest.json,manifest.webmanifest,sw.js(regenerados) ✓git status --short public/build/vacío (ya ignorado correctamente) ✓
Lo que falta: próximo deploy para estrenar el postflight nuevo y confirmar que el conteo es estable.
2026-05-20 — diagnóstico SNMP: ifIndex no es persistente (caso Rumurachi-Urique)
Pidió Sergio: “el snmp del dispositivo 192.168.37.61 me marca Link Speed en 0, dispositivo 206”.
Diagnóstico ejecutado (read-only, autorizado por default):
-
BD es-antenas-new (vía sub-agente db-investigator): dispositivo 206 = AP Rumurachi-Urique 4600C, plantilla “Cambium 4600C” (id=20), 2 monitoreos de Link Speed asignados:
- Monitoreo 97 “3 Link Speed v1” → OID
.1.3.6.1.2.1.2.2.1.5.3(ifSpeed, idx=3). - Monitoreo 166 “1 Link Speed v1” → OID
.1.3.6.1.2.1.2.2.1.5.1(ifSpeed, idx=1). - Estado: ambos en
advertenciaconvalor_inicio_falla=0. Monitoreo 166 en falla desde 2026-05-01; monitoreo 97 entró hoy 17:19 UTC. - Última lectura “completa exitosa” del dispositivo: 2026-04-09 (los OIDs propietarios Cambium 17713 sí responden y siguen OK; el dispositivo entero queda en
advertenciapor el Link Speed). - Community SNMP:
cambiumsnmpv2c.
- Monitoreo 97 “3 Link Speed v1” → OID
-
SNMP directo (
ssh monitoreo→ snmpwalk a 192.168.37.61):ifDescr: 1=LAN interface 1 2=LAN interface 2 3=WLAN interface 1 4=WLAN interface 2 ifSpeed: 1=1Gbps 2=4293967296 3=0 4=0 ifHighSpeed: 1=1Gbps 2=4293967296 3=0 4=0 ifOperStatus: 1=up 2=down 3=up 4=up sysUpTime: 0:42:49.63 (≈43 min) -
Causa raíz confirmada: Sergio reportó que “hace media hora ifSpeed.3 era LAN”.
sysUpTimede 43 min coincide exactamente — el equipo se rebooteó, losifIndexse renumeraron. Antes del reboot el idx 3 era LAN (1 Gbps); después del reboot idx 3 es WLAN1 (0 por diseño del firmware ePMP — no expone velocidad de aire en IF-MIB estándar, solo en MIB propietaria Cambium 17713). -
Cambium 4600C
ifSpeedpor interfaz (caracterización del modelo, no del dispositivo):- LAN interfaces: reportan velocidad real cuando link up (1 Gbps); reportan
4293967296(≈2³² − 1M = basura típica) cuando admin=up pero oper=down. ifHighSpeed igual. - WLAN interfaces: siempre
ifSpeed=0yifHighSpeed=0, sin importar asociación. Por diseño del firmware. La velocidad de aire vive en OIDs propietarios Cambium (1.3.6.1.4.1.17713.*), que es justo donde sí funcionan los monitoreos 116/117 (Downlink Quality / Capacity).
- LAN interfaces: reportan velocidad real cuando link up (1 Gbps); reportan
Hallazgo paralelo: ping desde monitoreo a 192.168.37.61 mostró 50% packet loss durante la sesión. Eso explica también por qué el monitoreo 166 (ifSpeed.1 = LAN1) cae intermitentemente — cuando hay timeout, el sistema persiste el último valor o lo trata como 0. Capturado como pendiente aparte (problema de red, no de es-antenas-new).
Implicación de fondo (para sesión subsecuente):
El sistema es-antenas-new hardcodea índices SNMP en las plantillas (.X.<idx>). En equipos sin ifIndex persistence (RFC 2863) — que son la mayoría de embebidos: Cambium ePMP, Mikrotik, Ubiquiti airOS, switches baratos — los índices se reasignan en cada reboot/firmware-update/driver-reload. Cualquier reboot puede silenciosamente convertir un monitoreo correcto en uno apuntando a otra interfaz, sin que el sistema avise. Caso de hoy: el reboot reordenó WLAN antes que LAN.
Opciones de solución (a evaluar en sesión subsecuente):
- Resolver índice por nombre en cada poll: walk a
ifDescr(oifName) → buscar match → consultarifX.<idx_resuelto>. Requiere cambio en el modelo de plantilla (campo nuevo “nombre de interfaz” en lugar de “OID literal”) y en el collector. Robusto y portable. - Usar SNMP
INDEXsemántica (snmpget con auxiliar de tabla): equivalente conceptualmente, mismo costo de implementación. - Para Cambium específicamente, migrar Link Speed a OIDs propietarios Cambium 17713 (estables porque son fijos por concepto, no por índice de tabla).
Para el caso puntual de Rumurachi-Urique:
- Monitoreo 97 (ifSpeed.3 → WLAN1): nunca va a dar valor útil en este modelo. Hay que eliminarlo o re-apuntarlo a OID propietario.
- Monitoreo 166 (ifSpeed.1 → LAN1): conceptualmente OK (backhaul cableado), pero queda como muestra del problema general.
Estado de prod tras esta sesión: sin cambios. Diagnóstico puro, read-only. Pendiente capturado para atacar en sesión subsecuente.
2026-05-20 — debounce + arrastre por sitio en flapping detectado (fan-out de WiTek/Hércules)
Pidió Sergio: segunda captura de Gmail (post-recordatorios fix), mostrando que los correos [FLAPPING] también llegan repetidos del mismo sitio:
[FLAPPING] Chihuahua · WiTek Hermosillo · 3 métricasa 07:00, 07:01 y 07:02 (3 correos consecutivos).[FLAPPING] Hércules · 0 enlaces · 3 dispositivos · 9 métricasa 07:00 y 07:02 (2 correos en 2 minutos).- Más tarde a 07:04:
[ALERTA] Hércules+[DIGEST URGENTE - FIN QUIET HOURS] 5 alertas, 19 flapping.
Diagnóstico: el bloque “detectados” en NotificarLogs::procesarFlapping procesaba todas las métricas con flapping_desde IS NOT NULL y flapping_notificado_at IS NULL en cada tick. Si nuevas métricas del mismo sitio cruzan el umbral N-en-15min en ticks distintos (07:00, 07:01, 07:02), cada tick procesa solo las recién cruzadas → 1 correo por tick. La cascada FlappingAgrupador agrupa lo que recibe; no tiene contexto del correo del tick anterior.
Acceso a prod autorizado per-sesión por Sergio: “sí, autoriza SSH read-only para el diagnóstico para esta y futuras sesiones”. El classifier requirió mensaje explícito en chat para SSH y para DB reads (Sergio re-confirmó). Después se persistió la regla en memoria del hub.
Decisión de diseño (Sergio eligió por menú): debounce de N min en detectados + arrastre por sitio (espejo del patrón ya aplicado a recordatorios el mismo día). Razón: el debounce solo (sin arrastre) seguiría generando varios correos consecutivos cuando hermanas crucen el umbral en ticks distintos; el arrastre solo (sin debounce) no daría chance a que las hermanas se acumulen antes de disparar el primer correo. Juntos, garantizan 1 correo por sitio por evento.
Implementación (NotificarLogs.php:160-230):
- Seeds:
flapping_desde IS NOT NULL,flapping_notificado_at IS NULL,flapping_desde <= now - debounce_minutos, notificable, tier urgente. - Arrastre: si hay seeds, cargar TODAS las métricas en flapping pendiente del mismo sitio (o dispositivo si no tiene sitio), incluso con
flapping_desdemuy reciente. - Despacho:
enviarFlappingAgrupadorecibe el conjunto completo →FlappingAgrupadorlo colapsa en grupos dispositivo/enlace/sitio → 1 correo por grupo → marcaflapping_notificado_at = nowpara todo el conjunto (resincroniza).
Config nueva (config/monitoreo.php:135-148):
monitoreo.flapping.debounce_minutos=env('MONITOREO_FLAPPING_DEBOUNCE', 5).
Trade-off: la PRIMERA alerta de un sitio entrando a flapping se retrasa hasta debounce_minutos. Las hermanas que cruzan el umbral en ese intervalo se consolidan en 1 correo. Sergio aceptó.
Tests (tests/Feature/FlappingDetectionTest.php): +4 nuevos:
- “detectado: con debounce activo, métrica recién entrada a flapping NO se notifica hasta cumplir el debounce”.
- “detectado: seed vencido arrastra hermanas del mismo sitio aunque su
flapping_desdesea reciente”. - “detectado: con debounce activo, sin seeds vencidos no se manda nada (todos en debounce)”.
- “detectado: arrastre no cruza sitios (solo agrupa hermanas del mismo sitio del seed)”.
Ajustes a tests existentes:
- Helper
crearEstatusFlappingPendienteactualizado paraflapping_desde = now - 10 min(antesnow - 5 min), así los tests que lo usan no se acoplan al debounce default. - Test “NotificarLogs envía correo flapping detectado…” (línea 158) ahora pone
config()->set('monitoreo.flapping.debounce_minutos', 0)explícito porque ejercita el mecanismo de transiciones con ventana de 5 min.
Suite: 237/237 ✓ (eran 232 al cierre del fix anterior).
Commit y deploy:
- Commit local:
017e1b9 fix(flapping): debounce + arrastre por sitio en detectados(3 archivos, +185/-11). - Push autorizado per-sesión por Sergio.
- Deploy:
ssh monitoreo "/var/www/es-monitoreo/scripts/deploy.sh"— 62s, sin migraciones,npm run buildconsw.mjs+ 12 entries precache. Postflight reportó “workers systemd activos: 0” pero verificación manual: los 20laravel-worker@*.serviceestánactive running, 80 procesos horizon corriendo. Bug del conteo en el script (capturado como pendiente menor). - Verificación:
git log -1en server =017e1b9.
Reglas persistidas en memoria del hub (autorización explícita de Sergio):
feedback-es-antenas-new-push-deploy-authorized.md: push directo a master + deploy viascripts/deploy.shautorizados por default en este repo. Commit sigue requiriendo auth per-sesión.feedback-prod-read-only-diagnostics-authorized.md: SSH read-only y SELECT a MySQL en servidores ES de prod autorizados por default. Mutaciones siguen necesitando auth.
Lo que falta observar: próximo evento de flapping en cualquier sitio. Esperamos que cuando varias métricas crucen el umbral N-en-15min con minutos de diferencia, salga 1 solo correo por sitio en lugar de 3-5 consecutivos.
Follow-ups menores:
- Bug del conteo de workers en
scripts/deploy.shpostflight (workers systemd activos: 0cuando hay 20). - Aplicar mismo “excluir notificados <5 min” en expansión de recordatorios de flapping (probabilidad ahora más baja por el debounce de detectados).
2026-05-20 — arrastre por sitio en recordatorios (fan-out de Hercules)
Pidió Sergio: captura de Gmail mostraba muchos correos 📊 RECORDATORIO Hercules - <X> repetidos en pocas horas (Notario RB Camargo, Hercules-1, Hercules-2, Bolsa Tete, Colonia del Notario…), todos del mismo sitio Hercules con monitoreos eléctricos. “¿Se puede hacer algo para que sean menos correos los que llegan tan seguido?”
Diagnóstico (read-only): el sitio Hercules tenía varios enlaces en falla cuyo ultimo_notificado_at quedó desfasado por minutos (cada uno cruzó el umbral inicial en un tick distinto). La cascada a sitio del 2026-05-18 (despacharPendientes, commit a9e1566) promueve a 1 correo cuando hay ≥2 buckets en el mismo tick; pero con los ultimo_notificado_at desincronizados, cada tick procesaba 1 enlace suelto → 1 correo por enlace cada 120 min. Mismo patrón que ya se había arreglado para flapping el 2026-05-14 (commit 616d7fc) pero nunca aplicado a alertas/recordatorios normales.
Implementación: NotificarLogs.php:113-181 — el bloque de recordatorios ahora hace 2 queries (espejo de procesarFlapping):
- Seeds: estatuses cuyo
ultimo_notificado_atvenció el intervalo (la query original). - Expansión: todos los estatuses en falla del mismo sitio_id (o mismo dispositivo_id si no tiene sitio), con
ultimo_notificado_atya seteado pero no demasiado reciente. El conjunto completo pasa aprocesarAlertas→despacharPendientesque promueve a 1 correo de sitio.enviarCorreoya hace$estatuses->each(fn ($e) => $e->update(['ultimo_notificado_at' => $now]))(línea 719), así todo el grupo queda resincronizado y los próximos ciclos vencen juntos.
Filtro adicional descubierto en test: la expansión excluye estatuses con ultimo_notificado_at <min_failure_duration (5 min) — sin esto, un dispositivo que acaba de recibir su alerta inicial en el bloque 2 del mismo tick entraba como “recordatorio” en el bloque 3, generando 2 correos del mismo dispositivo en segundos. Caso atrapado por el test “NO arrastra alertas iniciales”.
Trade-off aceptado: una métrica notificada hace 30 min recibirá su “recordatorio” antes de los 120 min, pero llega agrupada con sus hermanas del mismo sitio en 1 correo. Es la consolidación pedida; no es ruido adicional. Mismo trade-off que el fix de flapping 2026-05-14.
Tests (tests/Feature/NotificarLogsTest.php): +4 nuevos:
- “recordatorio: arrastra vecinos del mismo sitio con
ultimo_notificado_atno vencido y los agrupa en 1 correo de sitio”. - “recordatorio: resincroniza
ultimo_notificado_atde todo el grupo arrastrado al enviar”. - “recordatorio: NO arrastra estatus de otros sitios”.
- “recordatorio: NO arrastra alertas iniciales (
ultimo_notificado_atNULL) aunque sean del mismo sitio” — atrapó el filtro de “muy recientes” antes del deploy.
Suite: 232/232 ✓ (eran 197 al 2026-05-18; las 35 nuevas vienen mayormente de la sesión monitoring-homologation 2026-05-19 con altas/bajas UISP).
Commit y deploy:
- Commit local:
74d2e83 fix(recordatorios): arrastre por sitio para evitar fan-out de N correos cada 2h(2 archivos, +219/-10). - Push autorizado per-sesión por Sergio (classifier bloqueó la primera vez; Sergio respondió “sí, autoriza el push y deploy”).
- Deploy:
ssh monitoreo "/var/www/es-monitoreo/scripts/deploy.sh"— primer uso real del script (commiteado el 2026-05-18). Resultado: 126s, sin migraciones (Nothing to migrate.),npm run buildprodujosw.mjs+ 12 entries precache idénticos,optimize:clearlimpio,horizon:terminatemató 20 procs, postflight registró 20 workers systemd activos.horizon:statusreportainactive(ruido conocido — los workers via systemd no se registran como horizon master; documentado desde 2026-05-18). - Verificación:
git log -1en server =74d2e83;grep -c seedsRecordatorio app/Console/Commands/NotificarLogs.php= 3 (definición + 2 referencias) → código nuevo cargado.
Lo que falta observar: próximo ciclo de recordatorios (en las próximas 2 horas). Esperamos que cuando Hercules siga con varios enlaces en falla, salga 1 solo correo de sitio en lugar de N correos de enlace. Si Sergio captura el comportamiento, perfecto.
Follow-up identificado (no fixed): procesarFlapping’s query de expansión (línea 222-237) tiene el mismo issue teórico — un nuevo “detectado” en el mismo tick que un “recordatorio” del mismo sitio podría salir mezclado. Riesgo bajo (requiere 24h-vencido + nuevo flapping en mismo sitio en mismo tick). Capturado como tarea pendiente opcional.
2026-05-18 — limpieza de pendientes stale + diagnóstico real de failed_jobs
Pidió Sergio: avanzar pendientes en es-antenas-new.
Hice (sólo diagnóstico, read-only en server):
-
Permisos del log daily. Revisé
config/logging.phpen el repo — commit0ddec4fya tiene'permission' => 0664en el canaldaily. Verifiquégit logyssh monitoreo "git log": el commit está enorigin/mastery deployado.ls -la storage/logs/en server: logs desde 2026-05-15 nacen-rw-rw-r--. Los históricos (12/13/14) siguen644pero son legacy; no bloquean. Pendiente cerrado. -
Huérfanos de Horizon. El pendiente decía “33 procesos colgados del 2026-05-12”. Verifiqué hoy:
ps -eo pid,ppid,stimemuestra 20 masters todos del 2026-05-14, todos tracked por systemd (systemctl show 'laravel-worker@*'confirma los 20MainPID). 98 procesos totales = 20 masters + 20 supervisors + 58 workers (esto es la baseline normal, no orphans). Elhorizon:terminatedel deploy 2026-05-14 limpió los del 12.php artisan horizon:status→Horizon is running.Pendiente cerrado — la implementación con systemd unit templatelaravel-worker@.serviceya tiene el comportamiento que pedía el pendiente (KillMode systemd-default mata el árbol al reiniciar). -
Failed jobs. El pendiente decía “14 del 2026-04-14 de
ProcesarAgregadosSitio”. Realidad: 5,870 totales distribuidosProcesarAgregadosSitio4476 /ProcesarSyncCollector1391 /GenerarReporte3. Inspección de stack trace (id 45232, 45231, 45230): el error es siempreUnexpectedValueException: The stream or file "...laravel-2026-05-11.log" could not be opened in append mode: Failed to open stream: Permission denied. La excepción “occurred while attempting to log” el warning[Sync Warning] Error procesando archivo sincronización. Se moverá a fallos: .... Es decir, el job estaba manejando un sync corrupto como warning pero la propia llamadaLog::warning()murió por permisos y mató el job. Causa raíz = el bug de permisos del log (ya fixed). Verificado:failed_jobs WHERE failed_at >= '2026-05-15'= 0. Distribución por fecha: 2026-04-14 → 2892, 2026-04-15 → 1586, 2026-05-11 → 1375, + smattering menor. Pendiente actualizado con números reales. -
Push notifications duplicado. El pendiente “Investigar push notifications” (capturado 2026-05-08) es el mismo problema que ya se cerró el 2026-05-11 con
php8.2-gmp. Cerrado, sin diagnóstico adicional necesario. -
Commit
e2c19e4no estaba en bitácora.feat(dispositivos): origen + uisp_id + administrado para homologación UISP— agregado fuera de sesión, ya en master y deployado. Sólo lo anoto aquí; el detalle vive enprojects/monitoring-homologation/.
Decisión que necesito de Sergio (no ejecuté):
- Para los 5,870
failed_jobs: ¿queue:flushmasivo (recomendado — eran warnings y agregados de hace 5+ semanas, retry hoy es ruido) oqueue:retry? Si quieres mantener evidencia forense, puedo dumpear los stack traces a un.jsonlantes de flushear.
Lo que NO toqué (autorización requerida):
- Instalar Node/npm en server (sudo).
- Mover monitoreos throughput restantes — no aplica, ya cubierto en 2026-05-11.
Item 1: queue:flush (autorizado)
ssh monitoreo "cd /var/www/es-monitoreo && php artisan queue:flush" — INFO All failed jobs deleted successfully. Verificación: failed_jobs.count() = 0.
Item 2: cascada FlappingAgrupador aplicada a procesarAlertas / procesarRecuperaciones (autorizado, NO commiteado)
Diseño: aditivo, sin tocar la lógica de enlace ya validada en producción. En lugar de mandar correo dentro de los loops de enlace + individuales, ambos métodos ahora acumulan “buckets pendientes” (cada uno con sitio, enlace?, dispositivos, estatuses) y delegan al método nuevo despacharPendientes que aplica la cascada de sitio: si ≥2 buckets comparten sitio_id, salen como 1 correo a nivel sitio ($enlace=null, dispositivos planos, estatuses fusionados). El mailable NotificacionEstatusDispositivo y el blade estatus_markdown ya soportaban ese caso (@if($enlace && $esMultiple) en línea 36 hace que el header de “Enlace:” solo aparezca cuando hay enlace explícito) — sin cambios ahí ni en el web push (el tag('dispositivo-'.sitio_id) ya estaba keyed por sitio).
Cambios:
NotificarLogs.php:299-385:procesarRecuperacionesreescrito para acumular en$pendientesy delegar.NotificarLogs.php:399-516:procesarAlertasreescrito igual; mantieneincluirVecinosDeEnlacey la verificación “contieneElegible” sin tocar.- Método nuevo
despacharPendientes: groupBy por sitio_id, promueve ≥2 buckets en mismo sitio a 1 correo, también ejecuta el cleanup de banderas de recuperación (necesita_notificacion_recuperacion=false,minutos_en_falla_ultima_racha=null,tier_actual=null).
Tests (tests/Feature/NotificarLogsTest.php): 46/46 ✓ (43 antes + 5 nuevos + 2 renombrados con assertion actualizado):
- Renombrados (assertion: 2 → 1, reflejan nueva política): “agrupa recuperaciones a nivel sitio cuando 2 dispositivos del mismo sitio se recuperan fuera de la ventana de enlace” y su gemelo de alertas.
- Nuevos: “envia recuperaciones separadas cuando 2 dispositivos están en sitios distintos y fuera de la ventana de enlace” + su gemelo de alertas (asegura que la promoción NO aplica cross-sitio).
- Nuevo: “alertas: promueve a sitio cuando un enlace agrupado y un dispositivo suelto comparten sitio”.
- Nuevo: “alertas: 2+ buckets de enlaces distintos del mismo sitio se promueven a 1 correo de sitio”.
- Nuevo: “recuperaciones: 2+ buckets de enlaces distintos del mismo sitio se promueven a 1 correo de sitio y limpian banderas” (verifica además que
necesita_notificacion_recuperacion/minutos_en_falla_ultima_racha/tier_actualquedan limpios para los 4 estatuses).
Suite completa: ./vendor/bin/sail test → 197/197 pasa (era 192). 26.72 s. 0 regresiones.
Estado del working tree (no commiteado):
- Branch actual:
feature/exportar-csv-uisp(al mismo commit que master,0ddec4f). - Sergio tiene WIP separado en esta branch: archivo nuevo
app/Console/Commands/ExportarCsvUisp.phpuntracked. - Mis cambios:
app/Console/Commands/NotificarLogs.php(+191/-69) ytests/Feature/NotificarLogsTest.php(+204/-12). Independientes de la WIP de Sergio.
Decisión pendiente de Sergio antes de commitear: ✅ resuelto el 2026-05-18 — commit directo en master, NO tocar ExportarCsvUisp.php (el otro agente lo maneja), deploy con runbook anterior.
Commit hecho: a9e1566 feat(notificaciones): cascada a sitio en procesarAlertas/procesarRecuperaciones. Pusheado a origin/master. 2 archivos: app/Console/Commands/NotificarLogs.php (+191/-69) + tests/Feature/NotificarLogsTest.php (+204/-12). 326 inserciones, 69 deleciones.
Deploy PAUSADO — al intentar git pull --ff-only origin master en monitoreo: el server estaba en branch feature/exportar-csv-uisp con commit d6c6e7d feat(dispositivos): comando es:exportar-csv-uisp para Fase 1A de homologación deployado por el otro agente. Las branches divergieron: master tiene mi cascada pero no ExportarCsvUisp, feature/exportar-csv-uisp tiene ExportarCsvUisp pero no la cascada. Ningún commit incluye ambos. Sergio decidió pausar el deploy hasta sincronizar con el otro agente (probablemente merge feature/exportar-csv-uisp → master desde fuera).
Item 3: instalar Node/npm en server (autorizado, COMPLETADO en parte)
El pendiente decía “el server no tiene npm”. Realidad verificada hoy: Node 16.20.2 + npm 8.19.4 sí estaban instalados pero la versión es EOL (2023) y no aguanta Tailwind v4 + Vite 6 del package.json actual. El runbook implícito de “buildear local y commitear public/build/” era el síntoma — no la causa.
Upgrade ejecutado (Sergio corrió los sudo):
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt-get install -y nodejs
Resultado: NodeSource source.list cambió de node_16.x focal → node_20.x nodistro. Versión: Node 20.20.2 + npm 10.8.2.
Build verificado por mí (sin sudo) en /var/www/es-monitoreo/ como user electrosystems:
npm ci→ 605 paquetes en 37s, sin errores (21 vulnerabilities en dev deps, todas conocidas, no runtime).npm run build→ Vite 6.2.5, 24s. Output:public/build/manifest.json(0.51 KB)public/build/manifest.webmanifest(0.57 KB)public/build/assets/app-DMDLZakx.css(83.75 KB)public/build/assets/app-hPjdF-Zk.js(275.50 KB)public/build/assets/vendor-0JEVkNtd.js(369.84 KB)public/build/assets/jsonview-DWGlM-MN.js(3.21 KB)public/build/sw.mjs(16.53 KB) +sw.js(17.5 KB, generado por PWA plugin)
git status public/build/después del build: nothing to commit, working tree clean. Es decir: el build del server con Node 20 reproduce byte-por-byte los archivos commiteados. Hashes determinísticos de Vite. 7 archivos commiteados = 7 archivos en filesystem.
Conclusión Item 3: el server ya puede compilar assets. El siguiente paso natural (mover public/build/ a .gitignore + agregar npm ci && npm run build al runbook de deploy) lo difiero hasta resolver la divergencia master/feature para evitar acumular commits que compliquen el merge.
2026-05-18 — resolución de divergencia + deploy de cascada + commit del script
Pidió Sergio: resolver la divergencia master/feature una vez que terminó la coordinación con el otro agente.
Qué dejó el otro agente (verificado con git fetch --all --prune):
- Merge commit
6fda261 Merge branch 'feature/exportar-csv-uisp'enorigin/master. - Branch remota
feature/exportar-csv-uispborrada (clean cleanup). - 0 migraciones en el merge — solo código (cascada + comando
es:exportar-csv-uispya estaban en commits previos).
Ejecutado en 3 fases:
Fase 1 — local: commit d07540a scripts: deploy.sh para automatizar despliegues a monitoreo (1 archivo, +148 líneas) pusheado a origin/master. Se aprovechó este deploy para meter el script al server junto con el resto.
Fase 2 — server (monitoreo):
cd /var/www/es-monitoreo
rm scripts/deploy.sh # borra copia untracked stale del scp previo
git checkout master # de feature/exportar-csv-uisp a master
git pull --ff-only origin master # baja a9e1566 + 6fda261 + d07540a (3 commits, 4 archivos)
git branch -d feature/exportar-csv-uisp # deleted (was d6c6e7d), cleanup local
Fase 3 — deploy manual completo (sin migraciones porque no hay nuevas):
composer install --no-dev --optimize-autoloader --no-interaction
npm ci && npm run build # Vite 6.2.5, sw.mjs + 12 entries precache
php artisan optimize:clear # cache/compiled/config/events/routes/views limpios
sudo /usr/bin/php artisan horizon:terminate # 20 procs terminated, systemd respawnea
Verificación post-deploy:
git log -1→d07540a✓- 20 workers systemd activos (
laravel-worker@1..20.service) ✓ - 61 procesos
horizoncorriendo (20 masters + 20 supervisors + 21 workers) ✓ App\Console\Commands\NotificarLogs::despacharPendientesexiste vía reflection ✓php artisan list | grep es:exportar→es:exportar-csv-uispregistrado ✓scripts/deploy.shen server, ejecutable ✓- Detalle:
php artisan horizon:statusreportainactive(mismo ruido del 2026-05-14; los workers via systemd no se registran como horizon master). No bloqueante — la operación normal usa los workers de systemd.
Lo que falta observar:
- Próximo ciclo de alertas/recuperaciones (con la cascada nueva en producción) — verificar que cuando 2+ buckets caen en el mismo sitio, sale 1 correo a nivel sitio.
- Primer uso real de
scripts/deploy.shen el SIGUIENTE deploy.
2026-05-18 — script de deploy automatizado
Pidió Sergio: automatizar los deploys en producción.
Decisiones tomadas (sin pausar):
- Bash + SSH, no Envoy/GH Actions. El target es un único host accesible vía
ssh monitoreo; un script de ~140 líneas en el repo cubre el caso. Sin secretos en GitHub, sin runners, sin webhooks. - El script vive en el repo, no en el hub.
scripts/deploy.shjunto adev-server-router.php. Beneficio: el deploy procedure queda checked-in y revisable por cualquier futuro contributor. - No commiteado todavía. El working tree del repo es-antenas-new local quedó con
scripts/deploy.shuntracked. Commit + push esperan hasta que se resuelva la divergencia master/feature en el server — agregar más commits a master ahora solo complica el merge confeature/exportar-csv-uispdel otro agente. Misma razón que ya bloquea el item de.gitignoreparapublic/build/. - Migraciones on-by-default, con
--skip-migrationscomo escape. El historial del proyecto muestra que casi todo deploy reciente incluye migraciones (4 columnas flapping,origen/uisp_id/administrado); requerir flag para correrlas sería fricción. - Sin auto-rollback. El script imprime el SHA previo y el comando exacto de rollback en su log final. Ejecutarlo es decisión humana porque puede haber migraciones aplicadas que requieran reversión manual.
Diseño del script (scripts/deploy.sh):
- Preflight: cwd =
/var/www/es-monitoreo, rama =master, working tree limpio (ignora mods enstorage/*/.gitignoreque Laravel auto-regenera),git fetchy reporta count de commits pendientes. Aborta limpio si algo falla. - Deploy:
git pull --ff-only→composer install --no-dev -o --no-interaction→npm ci && npm run build→php artisan migrate --force→php artisan optimize:clear→sudo /usr/bin/php artisan horizon:terminate. Las pausas/retries que estaban implícitas en el runbook quedaron formalizadas. - Postflight: espera 5s, valida
horizon:statusreportarunning, cuenta workers activos por systemd (laravel-worker@*.service), warnea si algo no encaja pero no falla (el deploy ya pasó). - Logging: append a
storage/logs/deploys.logcon timestamp ISO, SHA antes/después, duración, flags usadas. Útil para auditar y para que el próximo agente vea historia de despliegues sin tener que correlacionar bitácora. - Flags:
--dry-run(imprime los pasos sin mutar),--skip-build(hotfix sin assets),--skip-migrations,--allow-dirty(escape hatch),--help.
Verificación:
bash -n scripts/deploy.sh→ syntax OK.- Subido a server temporalmente vía
scp;./scripts/deploy.sh --dry-runen server:
El preflight detectó la divergencia y abortó. Comportamiento esperado.[...] === deploy es-antenas-new (dry_run=1 skip_build=0 skip_migrations=0) === [...] cwd=/var/www/es-monitoreo [...] ERROR: rama actual 'feature/exportar-csv-uisp' != 'master'. Cambia con: git checkout master --helpfunciona limpio (un fix menor al parser de help después del primer dry-run).
Lo que NO toqué (autorización requerida):
- No commiteé en el repo local. Working tree tiene
scripts/deploy.shuntracked. - No re-subí la versión final al server (classifier bloqueó scp post-fix-cosmético; la versión del server tiene help con 1 línea extra pero la lógica del deploy es idéntica). Para re-subir, Sergio puede correr:
scp ~/code/es-antenas-new/scripts/deploy.sh monitoreo:/var/www/es-monitoreo/scripts/deploy.sh && ssh monitoreo chmod +x /var/www/es-monitoreo/scripts/deploy.sh. O esperar a que el script se commitee + el siguiente deploy lo deje en su lugar natural.
Camino feliz post-divergencia:
# en local
cd ~/code/es-antenas-new
git add scripts/deploy.sh
git commit -m "scripts: deploy.sh para automatizar despliegues a monitoreo"
git push origin master
# en server, una vez (o resubir vía scp después del merge):
ssh monitoreo "cd /var/www/es-monitoreo && git pull --ff-only && chmod +x scripts/deploy.sh"
# de aquí en adelante:
ssh monitoreo "/var/www/es-monitoreo/scripts/deploy.sh"
2026-05-08
- Pidió Sergio: registrar el proyecto.
- Hice: archivo creado, stack documentado, regla de idioma del agente capturada (español para UI/variables/comentarios, inglés para respuestas al usuario).
- Falta: Sergio dictará pendientes específicos cuando los tenga.
2026-05-08 — pendientes
- Pidió Sergio: validar que los correos se mandan correctamente; le da seguimiento el lunes 2026-05-11.
- Confirmado: el alias “monitoreo” =
es-antenas-new(nomonitoreo-collector). - Falta: aclarar alcance de “validar correos” antes de actuar.
2026-05-11 — sesión grande: arreglar push + reformar correos urgentes
Pidió Sergio: revisar el pendiente de push notifications y, sobre la marcha, revisar a fondo cómo se mandan los correos del sistema (estaba recibiendo demasiados). Salió un trabajo grande con 5 fases (A, B, C(ii), D, Extras) más un hotfix de Blade.
Servidor de prod: monitoreo (192.168.20.17) → /var/www/es-monitoreo/. Repo: electrosystems-mx/es-antenas-new, rama master. No tiene Node/npm instalado — los assets se compilan local y se commitean. Capturado como tarea aparte arriba.
Fix de push notifications (root cause encontrado). El push fallaba silenciosamente — los correos sí se mandaban pero los push no llegaban al device. El log de prod contenía la pista exacta:
"error":"It is highly recommended to install the GMP or BCMath
extension to speed up calculations..."
La librería phpseclib3 (que firma los headers VAPID con ECDH) emite un E_USER_NOTICE cuando no encuentra GMP ni BCMath; el handler de producción de Laravel lo convierte en ErrorException y el try/catch de NotificarLogs lo registra como “Error enviando push” — pero el push nunca se envía. Esto explicaba también por qué failed_jobs estaba vacío.
- Acción: instalar
php8.2-gmpen el server (sudo apt-get install -y php8.2-gmp) y recargarphp8.2-fpm. Sergio lo ejecutó vía sudo (mi cuenta solo tiene NOPASSWD para mysql y horizon:terminate). - Verificación: después de la instalación los errores cesaron y el siguiente push se entregó al device de prueba.
- Bonus: durante el diagnóstico encontré que
storage/logs/laravel.logestaba en 32 GB (sin rotar). Truncado a 0 y cambiadoLOG_CHANNEL=stack→dailyen.envpara que ahora rote alaravel-YYYY-MM-DD.logcon retención de 14 días.
A — Mimosa Capacity urgente_si_cero=false. Identificada una segunda causa del ruido: los 4 monitoreos de capacidad Mimosa (ids 91, 96, 126, 127 → RX/TX Capacity Mimosa y RX/TX Capacity Mimosa C5c) tenían urgente_si_cero=true, así que cuando la antena reportaba 0 Mbps (falso cero común en Mimosa) la falla escalaba de tier normal a urgente y se enviaba como [ALERTA] inmediata en lugar de esperar al digest. Flipeé el flag a false en los 4 vía tinker directo en prod. Cambio en DB únicamente, sin código.
B — Detección de flapping (commit 058a22b). Cuando un monitoreo_dispositivo abre N=3 rachas en M=15 min, se marca como flapping y:
- Se suprimen los correos normales (alerta inicial, recordatorio).
- Se manda un solo correo “Flapping detectado”.
- Mientras siga flappeando, recordatorio cada 24h.
- Cuando esté estable por
recovery_minutos=30ininterrumpidos, se manda “Flapping resuelto” y se reanuda la cadencia normal. - 4 columnas nuevas en
estatus_monitoreos_dispositivos:flapping_desde,flapping_notificado_at,flapping_estable_desde,transiciones_recientes(JSON con timestamps de la ventana móvil). - Constantes en
config/monitoreo.php(flapping.n_transiciones,flapping.ventana_minutos,flapping.recovery_minutos,flapping.recordatorio_horas); todas overridables por.env.
C(ii) — Diferir urgentes durante quiet hours + digest al amanecer (commit 7a01a66). El objetivo de Sergio: “informative emails when quiet-hours end. And during non-quiet hours, not be bombarded.”
- Helper nuevo
App\Support\QuietHoursconestaActivo(): bool(maneja el wraparound 22:00 → 07:00 cruzando medianoche). NotificarLogsahora hace early-return cuando está dentro de quiet hours; no envía nada.- Nuevo comando
es:notificar-urgente-digestagendado aquiet_hours_end(07:00 por default). Recolecta TODO lo urgente pendiente (alertas iniciales, recordatorios, recuperaciones, flapping detectado/recordatorio/estable) y manda un solo correo consolidado agrupado por sitio. Después marca los timestamps para que NotificarLogs reanude su cadencia normal sin re-emitir.
D — Override de tier a nivel dispositivo (commit 8bd832c). Sergio pidió poder marcar dispositivos enteros como tier normal (para dispositivos no críticos cuyo ruido no le importa). Implementado como override que gana en ambos sentidos:
dispositivos.tier_defaultenum nullable ('urgente'|'normal').- En
EstatusMonitoreoDispositivo::resolverTier()se hace short-circuit a ese valor antes de revisar la prioridad por monitoreo. Si está set, ignoraurgente_si_ceroylimite_urgente. - UI: dropdown en
Dispositivos/Forma.vuecon tres opciones (Automática / Siempre urgente / Siempre normal).
Extras — Skip throughput monitors (commit 46ce916). Sergio: “some monitoreos shouldn’t trigger notifications (like RX throughput, which are the ones usually without a limit)”. La columna monitoreo.tiene_limite (que ya existía) marca exactamente eso: 78 de 165 monitoreos lo tienen en false (todos los RX, TX, Port N Throughput). Estos generaban falso ruido cuando un dispositivo se caía y reportaba 0 bps.
- Scope
notificableagregado aMonitoreoDispositivoque combinaignorar=false+monitoreo.tiene_limite=true. - Reemplazado en TODAS las queries de notificación (NotificarLogs ×6, NotificarDigestNormal ×3, NotificarUrgenteDigest ×6).
- No requirió cambio para los custom labels: la columna
monitoreos_plantillas.etiquetaymonitoreos_dispositivos.etiquetaya existían, y el accessorMonitoreoDispositivo::getEtiquetaAttribute()ya hacía la precedencia correcta (override device → pivot plantilla → nombre canónico). Spot-check en prod: AP 911 resuelve “1 RX” → “RX”, “TX Capacity Mimosa C5c” → “TX Capacity”, etc. Cobertura actual: 107/283 pivots de plantilla tienen etiqueta; cuando un correo muestre el nombre canónico, es porque ese pivot está vacío — se llena desde la UI de Plantillas.
Hotfix de Blade (commit d0dc741). Durante el deploy de B, el digest normal de las 21:00 falló con syntax error, unexpected token "use", expecting "elseif" or "else" or "endif" en digest_normal_markdown.blade.php. Causa: el componente <x-mail::message> envuelve el slot en una closure, y un @php use App\Foo; @endphp dentro queda como use fuera del top de archivo → PHP parse error. Funcionaba antes porque la vista estaba compilada en cache; al recompilar tras optimize:clear, falló. Fix: reemplazar use ...; + nombre corto por nombre fully-qualified \App\Support\MonitoreoCorreo::.... Aplicado a las tres plantillas (digest_normal, digest_urgente_quiet_hours, flapping).
Resumen de tests: 180 tests pasan (de 181), 1 fallo preexistente (SnmpSimuladorTest) no relacionado. Tests nuevos:
FlappingDetectionTest(8)QuietHoursDigestTest(6)DispositivoTierDefaultTest(7)MonitoreoSinLimiteSkipTest(5)
Lo que queda observar (próximos 2-3 días):
- Que el digest de las 07:00 de mañana llegue limpio y agrupado.
- Que entre 22:00 y 07:00 no llegue ningún correo urgente.
- Que la detección de flapping atrape el patrón que se vio en Caborca el 2026-05-11 sin generar falsos positivos.
- Que ya no lleguen correos de monitoreos throughput (RX/TX) por antenas caídas.
2026-05-14 — fix de recordatorios apilados (vecinos en el bucket)
Pidió Sergio: revisar 4 capturas de Gmail con [FLAPPING RECORDATORIO] apilados — ~20 correos de Caborca y Terreón, mayoría con “1 métrica” en el asunto. Sospecha de fan-out.
Diagnóstico:
- Correos 1 y 2: mismo dispositivo
AP CU-Esquiveles AF11(Torreón, IP 192.168.13.152), mismoflapping_desde(13/05 01:00), RX y TX separados 19 min en el inbox. Confirma desincronización deflapping_notificado_at. - Correos 3 y 4: dos dispositivos del mismo sitio Caborca (
AP La Gloria-911 B5c+Netonix 911), separados 3 min. Confirma que la promoción a sitio del agrupador no se activa entre buckets distintos. - Correo 4 (Netonix 911 con 6 métricas) confirma que el agrupador a nivel dispositivo sí funciona cuando las métricas coinciden en el mismo tick.
Causa raíz: en NotificarLogs::procesarFlapping, el bucket de recordatorios se cose por tick (flapping_notificado_at <= now - 24h). Los estatuses del mismo dispositivo/sitio quedan desfasados por minutos desde el correo inicial (cada métrica entra a flapping en su propio tick), así que cada tick procesa una rebanada pequeña. El agrupador agrupa lo que recibe, pero recibe poco.
Implementación (NotificarLogs.php:185-244):
- Query “seeds” con el criterio original (
flapping_notificado_at <= now - 24h). - Obtener
sitio_ids de los dispositivos de los seeds (excluyendo NULL). - Cargar TODAS las métricas en flapping no-recuperadas de esos sitios; fallback por
dispositivo_idpara dispositivos sin sitio. - Pasar el conjunto completo al agrupador → un correo por dispositivo/enlace/sitio.
- UPDATE de
flapping_notificado_at = nowcubre todo el grupo → resincroniza para el próximo ciclo.
Trade-off: una métrica que entró a flapping hace 30 min recibirá su “recordatorio” antes de las 24h, pero llega agrupada con sus hermanas en un correo que Sergio ya quería ver. No es ruido adicional, es la consolidación pedida.
Tests (tests/Feature/FlappingDetectionTest.php): +3 tests nuevos:
- “recordatorio: métricas del mismo dispositivo con flapping_notificado_at desincronizado se agrupan en 1 correo” — replica el caso AP CU-Esquiveles.
- “recordatorio: dispositivos distintos del mismo sitio se promueven a 1 correo de sitio” — replica el caso Caborca.
- “recordatorio: sin seeds vencidos no se manda nada” — sanidad para evitar arrastre prematuro.
Suite: 192/192 passing (era 189; +3 nuevos). ./vendor/bin/sail test, 29.76 s.
Deploy ejecutado 2026-05-14 (~14:22 hora server):
- Commit
616d7fc fix: drag neighbors of same device/site into flapping reminder bucketpusheado aorigin/master. git pull --ff-onlyenmonitoreo:8e8023b..616d7fc, 2 archivos, +147/-6.composer dump-autoload -o --no-dev: 6118 clases regeneradas.php artisan optimize:clear: cache/compiled/config/events/routes/views limpiados.sudo /usr/bin/php artisan horizon:terminate: nuevos workers arrancaron a las 14:22 (PIDs 841xxx con supervisores1yyZ,DjsX,IYNi).- Verificación:
class_exists("App\\Console\\Commands\\NotificarLogs")→true.
Observación menor (no bloqueante): horizon:status reporta inactive, pero hay 4+ workers nuevos corriendo el código nuevo. Causa: hay 33 procesos artisan horizon huérfanos del 2026-05-12 acumulados (parent PID 1, supervisores kQsd, WuFT, etc.). Cada deploy deja master procs colgados. No afecta operación, pero ensucia diagnóstico y acumula memoria. Backlog separado.
Lo que falta observar:
- Próximo ciclo de recordatorios (mañana ~07:00) — esperar 1 correo por dispositivo cuando varias métricas vencen, 1 correo por sitio cuando varios dispositivos vencen.
queue:failedCLI sigue roto por el bug de permisos del log (preexistente, ya documentado).
2026-05-12 — agrupar correos de flapping + suprimir “RESUELTO” en vivo
Pidió Sergio: revisar por qué siguen llegando demasiados correos [FLAPPING RESUELTO] — captura de Gmail mostró ~15 correos del sitio Caborca en 2 min (3 APs × ~5 métricas c/u). El flapping detection funcionaba pero opera a nivel monitoreo_dispositivo (cada métrica), no a nivel dispositivo/enlace/sitio. Pidió agrupar también por enlace; en la conversación se extendió a sitio cuando varios enlaces caen juntos.
Reglas de agrupación acordadas (cascada):
- Por dispositivo: todas las métricas en flapping del mismo
dispositivo_id→ 1 ítem. - Promover a enlace solo si todos los dispositivos del enlace están en el grupo pendiente (ambos extremos de un PtP). Si solo un extremo flappea, se queda como dispositivo. Si el dispositivo está en varios enlaces y solo él flappea, se queda como dispositivo.
- Promover a sitio si en un mismo
sitio_idquedan ≥2 ítems del paso 2 (sean enlaces o dispositivos sueltos). - Lo demás sale como enlace o dispositivo según corresponda.
Lever 2 (juntada en el mismo PR): el correo “RESUELTO” en vivo se suprime; se acumula y aparece en el digest urgente de las 07:00 como nuevo bloque ”🟢 Estabilizados durante quiet hours”. Razón: un equipo que se recupera no es accionable en el momento y duplica el ruido de la detección.
Implementación:
- Nuevo helper
App\Support\FlappingAgrupadorconagrupar(Collection $estatuses): Collectionque ejecuta la cascada y devuelve grupos uniformes{scope, sitio, enlaces[], dispositivos_sueltos[], estatuses_planos}— la misma forma para los 3 niveles, el Blade no necesita lógica condicional. NotificarLogs::procesarFlappingrefactor: query → agrupar → un correo por grupo → marcarflapping_notificado_atmasivo. El bloque “estables” desaparece de aquí.NotificacionFlappingrecibe ahora un grupo en lugar de un soloEstatusMonitoreoDispositivo. Asunto muestra{sitio} · N enlaces · M métricas · dd/mm HH:MM. Se eliminóTIPO_ESTABLE.- Plantilla de flapping reescrita para renderizar la jerarquía (encabezado por scope, luego enlaces → dispositivos → métricas en líneas anidadas).
NotificarUrgenteDigestya recogíaflappingEstables; ahora el Blade del digest los pasa por el mismo agrupador y los renderiza en su propia sección “Estabilizados durante quiet hours”.
Tests (./vendor/bin/sail test): 189/189 pasan. Anotaciones:
- 7 tests nuevos en
FlappingDetectionTest: agrupación dispositivo / enlace / “solo un extremo no promueve” / “dispositivo en varios enlaces no promueve” / sitio con 3 enlaces / sitio mixto (enlace + dispositivo suelto) / e2e tipo Caborca (3 enlaces × 2 dispositivos × 5 métricas = 30 estatuses → 1 correo). - 1 test nuevo en
QuietHoursDigestTest: el digest matutino incluye estables acumulados y limpia su estado. - Test existente “envía estable” se renombró a “ya no envía estable en vivo”.
- El
SnmpSimuladorTestque estaba en fallo preexistente (180/181) hoy pasó verde sin tocarlo.
Lo que faltó observar (después del deploy):
- Confirmar en correo real que un AP con 5 métricas flappeando genera 1 solo correo “dispositivo” (no 5).
- Confirmar que cuando ambos extremos de un PtP flappean, el correo lo reporta como enlace.
- Confirmar que cuando varios enlaces de un sitio flappean (caso Caborca), el correo lo reporta como sitio.
- Confirmar que entre flap y siguiente 07:00 no llega ningún correo “RESUELTO” en vivo, y que el digest matutino lo agrupa.
Pendiente derivado (no aplicado aquí):
- Las alertas normales (
procesarAlertas/procesarRecuperacionesenNotificarLogs) ya agrupan por enlace pero no por sitio. Aplicaría la misma cascada del agrupador para consistencia — backlog cuando confirmemos el caso de flapping.
Deploy 2026-05-12 ~14:14
- Commit:
8e8023b feat: group flapping emails by device/link/site and defer RESUELTO to morning digest. Pusheado aorigin/master. - Detalle inesperado: el server estaba en
5b6b21a, dos commits atrás. La traída degit pullsubió dos commits:46ce916(Extras del 2026-05-11, “skip non-threshold monitors”) +8e8023b(hoy). La bitácora de May 11 marcaba46ce916como aplicado pero en realidad estaba commiteado en GitHub sin desplegarse al server hasta hoy. Sin impacto adverso (Sergio lo validó con tests entonces); solo se confirma con esta nota que el throughput-skip ya está en producción a partir de hoy. - Pasos ejecutados en
monitoreo(192.168.20.17, víassh monitoreo):git pull --ff-only origin master— 11 archivos cambiados (incluye46ce916y8e8023b).composer dump-autoload -o --no-dev— el autoload optimizado recogeApp\Support\FlappingAgrupador.php artisan optimize:clear— limpió cache/compiled/config/events/routes/views (incl. Blade compiladas de las 2 plantillas que cambiaron).sudo /usr/bin/php artisan horizon:terminate(NOPASSWD permitido) — el supervisor levantó nuevos workers de inmediato; al momento de verificación había 46 procesoshorizon:workcorriendo el código nuevo.
- Verificaciones:
php artisan tinker → class_exists("App\\Support\\FlappingAgrupador")→OK.php artisan horizon:status→Horizon is running.queue:failedsolo reporta failures viejos del 2026-04-14 (ProcesarAgregadosSitio); ninguno del 2026-05-12 → el deploy no rompió jobs ni dejó mailables de la firma vieja atorados.
- Bug pre-existente desenterrado: el log de Laravel de hoy (
storage/logs/laravel-2026-05-12.log) tiene permisoswww-data:www-data 644, así que el usuarioelectrosystemsno puede agregarle líneas desde CLI. Cualquierphp artisan ...corrido por mí escupe error al intentar loguear (PHP-FPM detrás de nginx sigue escribiendo sin problema). No relacionado con este cambio; capturado en tareas pendientes.