2026-05-19
electro-ia — Fase 2.8: feedback loop v1 cerrado
Sergio pidió avanzar pendientes en electro-ia. Tras menú de opciones eligió feedback loop (memoria del agente) — la pieza más conceptualmente alineada con el goal del proyecto (mejora continua persistida).
Diseño (4 decisiones rápidas)
- Sin confirmación al guardar —
/olvida Nsiempre revierte; más fluido que la danza depending_action. - Solo admin puede crear scopes
global/role:*/user:<ajeno>. Engineer/viewer solo su propio user. Previene envenenamiento horizontal. - Sin embeddings v1 — con <50 memorias todas caben en system prompt; cheap y debuggable.
- Scope v1: comandos + inyección + política. Sin endpoint admin ni auto-detección de correcciones implícitas.
Implementación
- Migration:
sql/0002_agent_memory.sqlaplicada en DBelectroia. Tablaagent_memory(scope global|role|user,scope_value,body 1..2000,source command|admin, soft-delete condeactivated_at/by, FKs aidentity). CHECK constraints que enforcement los scope_value vs scope. - Nuevo módulo:
src/memory.js— 4 funciones públicas (createMemory,loadMemoriesFor,listVisibleMemories,forgetMemory) +renderMemoriesForPrompt. La diferencia entreloadMemoriesFor(lo que se inyecta al prompt) ylistVisibleMemories(lo que el usuario ve con/memorias) es que admin ve TODO conlistVisibleMemoriesperoloadMemoriesForse limita a global ∪ role(propio) ∪ user(propio). - Agent loop:
src/agent.jscarga memorias en paralelo con el history (Promise.all); el bloque se append al final del system prompt como markdown plano. Para Antigravity API esto se cachea automáticamente con el system prompt estable. - Slash commands:
src/index.jshandlers deterministas (sin LLM en medio). Parseo de scope en/recuerda: admin puede usar prefixglobal:/role:engineer:/user:<id>:; sin prefix → siempreuser:<propio>. Aliases:/recordar,/remember,/memories,/recuerdos,/olvidar,/forget.
Validación
- Smoke unitario (node directo contra
src/memory.js): 13/13 aserciones. Validado aislamiento — otro admin no veuser:<sergio>, engineer hipotético no veuser:<sergio>pero sírole:engineer, forget cross-user denegado a engineer. - Smoke E2E (Sergio, Telegram, 5 pasos):
/recuerda <regla telcel>→ guardado #1 [user:sergio];/memorias→ listó;/api hablame del estado de Telcel→ Sonnet 4.6 respetó la regla, pidió el enlace específico;/olvida 1→ confirmó;/memorias→ vacío. PASA.
Commit
b61c505enlaptop-ia:/home/electroia/electro-iamain — sin push (el repo no tiene remote).- Archivos:
sql/0002_agent_memory.sql,src/memory.js,src/agent.js,src/index.js. 4 files, 253 insertions, 6 deletions.
Detalles operacionales notables
- NOPASSWD asimetría: el user
electroia(que corre el bot) NO tiene sudo NOPASSWD parasystemctl restart electro-ia. El privilegio aplica al usersergio/gchavira. Consecuencia: mid-sesión, cuando hicekill -TERMesperando que systemd resurrectara el servicio (Restart=on-failure), no pasó nada porque el handler deindex.jshaceprocess.exit(0)que cuenta como clean exit. Sergio tuvo quesudo systemctl start electro-iadesde su cuenta. Pendiente operativo: o (a) agregar NOPASSWD paraelectroiauser restringido al unit, o (b) cambiar el SIGTERM handler aprocess.exit(1)para que systemd cuente como failure. Decisión deferida. - Cambios ajenos en working tree:
policy/hosts.yaml(+host monitoreo 192.168.20.17) ypolicy/paths.yaml(+write_allowed policy/). No los toqué — quedaron en working tree para que Sergio los committee aparte.
Memoria para futuras sesiones
Nada nuevo que requiera memoria persistente — el patrón de "feedback loop con comandos /recuerda" queda documentado en el README de electro-ia. Pero recordatorio: a partir de hoy electro-ia tiene su propia memoria operacional persistida en DB, independiente del sistema de memoria de este hub (~/.claude/projects/.../memory/). No confundir: la mía guía mis respuestas aquí, la de electro-ia guía las suyas allá.
holbox — 4 entregas de la tarde + fix de descuadre de reportes
Sergio pidió avanzar pendientes acumulados de holbox; tres tareas grandes + un fix reactivo del cliente.
1. Monto facturado de optometría en reporte Avanzado (Desempeño Equipo)
Pendiente del 2026-05-18. La celda Optometría por asociada ahora muestra conteo (badge ámbar) + monto facturado debajo en gris, oculto si es $0. Backend agrega SUM(CASE WHEN tipo_optometria IS NOT NULL THEN precio_optometria * cantidad ELSE 0 END) as monto_optometrias al select por empleada en AvanzadosController. 2/2 tests Pest verdes. Commit ea9be52.
2. Resumen de optometría también en el reporte de Ventas clásico
Sergio amplió el scope a media implementación: "que en el reporte normal de ventas también haya manera de consultar cantidades y totales de optometría". Agregué 4ª KPI card "Optometría" en Reportes/Ventas (grid pasó a 4 columnas, icono MagnifyingGlassPlusIcon por su asociación visual con "aumento/graduación" de lentes). Backend en VentasController agrega cantidad_optometrias + monto_optometrias al resumen vía subquery whereIn que respeta TODOS los filtros activos. 3/3 tests verdes. Detour: primer intento descubrió MySQL error 1235 (LIMIT en IN subquery) porque clonaba $query después del paginate(50); muevo el cálculo del resumen ANTES del paginate. Commit 5938ba2.
3. Vista "mis ventas del día" para asociadas en el POS
Pendiente del 2026-05-18. Endpoint nuevo GET /pos/mis-ventas-hoy (PosController::misVentasHoy) que devuelve ventas, unidades_por_categoria (GROUP BY categoría, orden DESC) y totales. Filtra por auth()->id() + estado completada + día operativo en TZ Cd. Juárez. UI: botón verde esmeralda "Mis ventas" en cabecera del POS junto al de Corte, abre modal con 3 cards (ventas/total/unidades) + lista compacta + chips "Categoría · N" al pie. 2/2 tests (aislamiento por user_id, exclusión de canceladas/ayer/otra-asociada, orden de chips). Commit 79232f5.
4. Fix de descuadre de reportes — el admin reportó
Aarón preguntó por qué el total del lunes 18-may daba $10,169 en una pantalla y $10,091.50 en otra. Diagnóstico (SSH read-only a prod): una sola venta descontada ese día — VTA4-000237 Jocelyn Cordero subtotal $1,550 / desc $77.50 / total $1,472.50. Tres reportes sumaban cosas distintas: semanalCategorias y Avanzados/empleadas usaban SUM(venta_detalles.subtotal) (ignora descuento), mientras Comisiones y semanalEmpleadas usaban $ventas->sum('total').
Decisión de Sergio: opción 2 — agregar columnas Subtotal + Descuentos + Total Recaudado en ambos reportes problemáticos (transparencia financiera + cuadre exacto con el resto del sistema). El descuento de cada venta se prorratea entre sus líneas según peso del subtotal: descuento_linea = subtotal_linea × venta.descuento / venta.subtotal. Resultado en prod (semana 18-24 sucursal 4): subtotal $18,249, descuento prorrateado $77.50 (47.50 a Premium + 30.00 a Clásico), neto $18,171.50. 3/3 tests verdes incluyendo replicación exacta del escenario reportado. Commit 8e706a9.
Push autorizado en esta sesión
Sergio dio "Haz push" — 4 commits a origin/main, GHA deploys ambos verdes. Convención de holbox sigue siendo per-sesión: NO marco esto como autorización permanente.
Decisión memorable
Frente al descuadre, ofrecí 4 opciones (prorrateo silencioso, prorrateo silencioso ambos, dos columnas, solo explicar). Sergio escogió opción 2 — dos columnas. Patrón válido para reportes financieros del cliente: ante divergencias entre cifras visibles para el admin, mostrar las cifras intermedias (subtotal + descuento + neto) en lugar de "esconder" la diferencia con un solo número corregido. Capturado como memoria de feedback.
Detalle ajeno notable
El suite pest --filter="Reporte|Avanzados" muestra 1 falla pre-existente en CajaCorteTest > gerente puede ver reporte de cortes — reproducible en baseline, no causado por mis cambios.
Memoria para futuras sesiones
- Guardé feedback nuevo sobre transparencia financiera en reportes (opción "dos columnas" preferida a prorrateo silencioso) — aplicable a holbox y posiblemente a otros POS/contables.
- El bug del paginate + whereIn subquery con MySQL 1235 vale la pena recordar como gotcha de Laravel; si vuelvo a hacer un resumen filtrado en una query paginada, calcular ANTES de llamar
paginate().
electro-ia — Fase 2.9: deuda técnica de audio cerrada (endpoint binario)
Tras cerrar Fase 2.8 (feedback loop), Sergio eligió continuar con el siguiente pendiente: limpiar la deuda técnica de audio — el kludge filesystem-sharing que había entre electroia (bot) y gchavira (whisper server).
Lo que se atacó
electroia guardaba el .ogg en /tmp/electro-ia/audio/ con mode 0o644 y le mandaba el path a whisper :18081. Eso forzó dos kludges (commit f3e6f08 original):
PrivateTmp=falseen el unit de electro-ia (compartir /tmp entre namespaces).- Mode 0o644 en el writeFile (voice notes legibles a cualquier user local).
Solución (Opción B del plan original)
Cambiar el contrato del endpoint de path a body binario.
Server (/home/gchavira/projects-hub/src/transcribe/server.py):
do_POSTdetecta por Content-Type.application/json→ modo legacy (lee{path}, abre por filesystem). Cualquier otro (audio/*,application/octet-stream) → spool del body atempfile.NamedTemporaryFileprivado al user gchavira, transcribe, unlink enfinally.- Query param
?language=esopcional. - Cap de body 30 MB.
- Backwards compat deliberada para deploy atómico — al reiniciar whisper, el cliente viejo sigue funcionando hasta que el nuevo cliente también esté arriba.
Cliente (electro-ia/src/transcribe.js):
fetchahora mandabody: bufferconContent-Type: <mimetype>(defaultaudio/ogg).- Removido
{ mode: 0o644 }del writeFile local — el audit trail vuelve a 0o600 efectivo (default umask).
Deploy (atómico, sin downtime cliente)
- Sergio mata whisper viejo (PID 73572, proceso huérfano de tmux antiguo) y relanza con el server.py nuevo.
- Validación curl: los 3 modos (legacy JSON, body sin language, body
?language=es) devuelven el mismo transcript del mismo audio existente. - Sergio restart electro-ia → cliente nuevo activo.
- Smoke E2E: 3 audios desde Telegram (0.6-0.7 s c/u). OK.
- Sergio borra override
PrivateTmp=false,daemon-reload, restart.systemctl show -p PrivateTmp=yes. - Smoke E2E final con
PrivateTmp=yes: audio OK en 0.66 s. Prueba definitiva — sin /tmp compartido, el flujo solo puede pasar si el cliente manda bytes.
Commits
- electro-ia main:
189d99b"refactor(transcribe): endpoint binario en lugar de path compartido" (1 archivo, 13/14). - projects-hub main: dirty. Sergio prefirió commitearlo como gchavira aparte; server.py sigue corriendo en memoria desde su restart.
Notas operacionales
electroiatiene write a/home/gchavira/projects-hub/src/transcribe/vía pertenencia al grupo gchavira yg+wen el archivo — por eso pude editar server.py directamente. Al guardarlo el archivo quedó ownerelectroiapero grupogchavira(lo cual no afecta read/write para nadie).- Whisper sigue siendo un proceso huérfano sin systemd unit propio. La salida limpia "Opción A" del plan original (whisper como su propio user con systemd) sigue siendo deuda técnica pendiente, pero ya no es urgente — el filesystem-sharing kludge desapareció.
- El modo legacy en server.py se puede borrar en una próxima limpieza (~10 LOC). Recordatorio: si pasa ~1 semana sin incidentes, cortar.
Memoria para futuras sesiones
Nada nuevo a memorizar — el patrón "endpoint binario en vez de path compartido" es estándar y queda documentado en los comentarios del propio código y en este log. La memoria del NOPASSWD asimétrico (reference_electroia_systemctl_nopasswd) sigue siendo el único hint operacional que vale la pena tener cached.
backups-infra — arranque del proyecto
Sergio pidió empezar con backups-infra. Primera sesión real de este proyecto desde su captura el 2026-05-08.
Decisiones marco cerradas
- #0 Scope — solo Electrosystems-internal. Excluidos: proyectos
type: personal(medicinas, aprende-ingles, hub-portability); servidores web de clientes externos en internet (holbox, deportescampeon, grecocell, jmeza prod); delaptop-iasolo entra DBelectroia. - #1 Destino — solo NAS
es-nas(192.168.20.26). Off-site para fase futura.
Decisiones #2-#6 (herramienta, retención, cifrado, monitoreo, restore drill) quedaron abiertas con recomendaciones; la próxima sesión arranca por ahí.
Lo que se hizo
Inventario inicial de DBs vía script
db_inventory.shread-only en 9 hosts candidatos del config: holbox, docs, asterisk-adfsa-new, laptop-ia, netbox, amadeus, jmeza, deportescampeon, grecocell. Hallazgos:adfsa-voip(MariaDB 10.11, asteriskcdrdb 62 MB) — socket OK.deportescampeon(MySQL 8, DB 5 GB) — socket OK pero quedó OUT por ser cliente externo web.grecocell(MySQL 8, DB 183 MB) — socket OK pero OUT.laptop-ia(Postgres 16, projhub 9.7 MB / electroia 9 MB) — peer auth OK; solo electroia entra al backup NAS.- Resto requiere creds.
docsno tiene DB visible — bandera roja paradocs-platform, no para este proyecto.
Escaneo TCP probe
10.11.0.0/16desde laptop (vía VPN del host Windows). bash +nccon paralelismo 400, 5 min 8 seg. 413 IPs vivas. 34 servidores Linux ES en puerto 58695, 31 inventariados, 4 con DB Asterisk medible:miscelec-chih4.8 GB asteriskcdrdb ⭐arias-ep1.1 GBvitalpbx1.1 GBlineas-americanas628 MB- Total ~7.6 GB en los 4 hosts con socket auth + ~20 PBX más con MariaDB activa pero "needs creds".
- 354 hits en
10.11.30-37.0/24solo en puerto 22 son equipos de red — territorio debackup-system.
Escaneo
192.168.20.0/24desde la VMwireguard(que vive en esa red). 29 hits.- Hallazgo clave: doble-stack confirmado — los hostnames duplicados entre 10.11.x y 192.168.20.x son el mismo host (peer WG + LAN). Memoria guardada como
reference_es_wg_lan_dual_stack. - 3 CentOS 6 EOL en datacenter: orion (.2), otrs (.21),
clientes.electrosystemsnet.com(.60). Mismo riesgo queorion-decommission. - 2 hosts con llave id_rsa_es rotada: reverse-proxy (.14), monitoreo (.17). Necesitan re-auth antes de backup.
- viaticos (.19) caído,
indelek-chihuahua(10.11.1.206) caído. val-softreporta hostnamepaginas-web— alias mal nombrado o server multi-rol.- Bloque .103-.106 sin auth (probable workstations) + .13/.16/.24/.57/.254 sin identificar.
- Hallazgo clave: doble-stack confirmado — los hostnames duplicados entre 10.11.x y 192.168.20.x son el mismo host (peer WG + LAN). Memoria guardada como
Documentación
projects/backups-infra.mdactualizado: 2 secciones nuevas en Notas técnicas (Inventario DBs, Escaneo 10.11/16, Escaneo 192.168.20/24), tabla de decisiones marco con #0 y #1 cerradas, sección "Tareas pendientes" completamente reescrita en 6 bloques (A reanudar / B completar inventario / C duplicidades / D derivados / E implementación / F fuera-scope), bitácora del día con cierre de sesión.PENDIENTES.mdactualizado: encabezado del día + sección de backups-infra reescrita con los nuevos pendientes accionables.
Memoria para futuras sesiones
reference_es_wg_lan_dual_stack— la regla del doble-stack 10.11.x / 192.168.20.x. Aplica a CUALQUIER análisis futuro de infra ES, no solo a este proyecto.project_backups_infra_progreso— punto de retomar (decisiones cerradas + dónde está el detalle).
Para la siguiente sesión
Arranca por la sección "Tareas pendientes → A. Reanudar próxima sesión" del proyecto. Primer movimiento: cerrar decisiones marco #2-#6 (especialmente la específica de si asteriskcdrdb entra al backup, porque es el 95% del volumen medido).
holbox — Venta de error VTA4-000247 + dos arreglos de reportes
Sesión nocturna, 3 movimientos secuenciales sobre prod.
1. Cancelación + limpieza física de venta errónea
- Sergio dio de alta
VTA4-000247en sucursal 4 por error (1 producto, $2,100, tarjeta, sin cliente). Pidió "borrar y actualizar inventarios". - Le surface el flujo correcto: el endpoint
VentaController::cancelarya hace todo (estado=cancelada, repone stock, decrementa stats del cliente, libera cupón) en una transacción. Sergio lo cliqueó en la UI admin. - Validé via SSH read-only: cancelación atómica OK, inventario repuesto (+1 al producto_id=3 en sucursal 4).
- Después pidió borrado físico ("simular que nunca existió"). Validaciones previas: 0 ventas posteriores en suc 4 (por id ni por fecha), 0 cambios, 0 garantías, único corte de caja del día fue 6h antes de la venta — sin riesgo de descuadre. DELETE en transacción con
lockForUpdatey guard idempotente.cascadeOnDeleteen FK deventa_detalles/cambios/garantiassimplificó la limpieza. Post-deleteMAX(folio)suc 4 =VTA4-000246→ la próxima venta tomaVTA4-000247otra vez.
2. Reporte de ventas oculta canceladas por default
Mientras investigaba, noté que el listado mostraba la cancelada junto a las completadas y totalVentas la sumaba (mientras cantidad_completadas no). Inconsistencia heredada.
Fix: VentasController aplica where('estado','!=','cancelada') cuando no hay filtro explícito. Dropdown del frontend cambia de "Todos los Estados" → "Activas (sin canceladas)". El admin que quiera ver canceladas las selecciona explícito. 2 tests Pest nuevos cubriendo default + filtro explícito.
Commit f67bcab, deployado GHA 26131224699.
3. Audit USD/MXN — directorio de clientes
Sergio reportó que en el listado de Clientes se mostraba $X.XX USD cuando el sistema opera en pesos. Audit completo del codebase:
- Único bug:
Clientes/Index.vue:178usabatoLocaleString('en-US')con label" USD"hardcoded sobremonto_acumulado(que está en MXN). - POS preserva "USD" intencionalmente (líneas 795/1360/1383/1399/1408 — conversiones de referencia y label del input de cobro en dólares).
- Configuracion preserva el label "Tipo de Cambio (USD a MXN)" (es el setting del FX rate).
- Backend cero referencias a USD.
Fix: helper formatCurrency local con patrón estándar es-MX/MXN → $1,234.56 sin label. Commit 6bb4fb0, deployado GHA 26132413697.
Memoria capturada
reference_holbox_borrar_venta_huerfana— flujo seguro para borrar venta de error en holbox (cancelar primero por UI → SSH valida que sea última en la sucursal y sin relaciones → DELETE con cascade → MAX(folio) cae a la anterior y se libera el folio).
electro-ia — Fase 2.10: bug anafórico + tool send_email (EN PAUSA — deployados pendiente restart)
Sergio reportó que el bot le contestó al jefe sobre un archivo equivocado. Diagnostiqué, fixeé, y como siguiente pendiente arrancamos send_email. Ambos están deployados en disco pero el restart + credenciales SMTP quedan pendientes para la próxima sesión.
Bug diagnosticado
El modelo local (qwen2.5:14b) alucinó completo sobre el contenido del Excel factibilidades_seleccionadas2.xls (file_id=10) cuando el jefe preguntó "dame el total de las rentas mensuales de este archivo". Cero tool_calls en ese turn — el modelo inventó un bloque sourceMapping: \n - sheet_name: 'Sheet1' header: ['Fecha', 'Descripción', 'Débito', 'Crédito'] con items reciclados del PDF de WellsFargo que había analizado 24 minutos antes ("SAMS CLUB", "Depósito de Gustavo C Valle", "Renta $794.86").
Causa: "este archivo" sin file_id explícito no gatillaba la regla inviolable existente del prompt. El history del local (max_messages=6) tenía respuestas previas con la síntesis del PDF, y el modelo las recicló. Modo de falla nuevo no cubierto antes: imitar el output crudo de la tool (no el footer técnico, ese ya estaba prohibido en Fase 2.6).
Fixes deployados
src/db.js—fetchRecentUploads({ identity_id, window_minutes: 120, limit: 5 }). Lee directo de la tablauploaded_file.src/agent.js—respondToahora carga uploads en paralelo con history+memorias (Promise.all). Nueva funciónrenderUploadsBlock(uploads)arma un bloque markdown que se concatena al system prompt, con el más reciente marcado[MÁS RECIENTE].prompts/system.md— regla inviolable de "archivos" expandida a 4 puntos:file_idexplícito → tool obligatoria (igual que antes).- Referencia anafórica → resolver al MÁS RECIENTE de la lista de uploads del prompt.
- NUNCA mezcles archivos diferentes del history.
- NUNCA imites formato de tool output (footer, sourceMapping, tablas markdown con datos fabricados).
Tool send_email deployada
Decisiones de diseño cerradas con Sergio (4 preguntas):
- Proveedor: Google Workspace Electrosystems (smtp.gmail.com:587 con App Password).
- Allowlist: solo
@electrosystems.com— pero interpretado como "allowlist suave" (externos disparan confirmación, no bloqueo). OJO: si Sergio prefería el bloqueo estricto, mañana lo confirma; está documentado en bitácora del proyecto. - Confirmación: si destinatario externo OR to+cc > 3.
- Roles: admin + engineer.
Decisiones implícitas: plain text only v1, sin adjuntos, rate limit 30/24h via tool_call_log.
Archivos (todos en disco, pendientes de restart):
src/email.js(nuevo, ~50 LOC) — wrapper sobre nodemailer con lazy transporter.src/tools/send_email.js(nuevo, ~180 LOC) — schema + run + executeConfirmed, valida emails, gestiona rate limit y pending_action.src/tools/index.js— registra send_email + export executeSendEmail.src/index.js— agrega send_email a PENDING_EXECUTORS + nueva funciónformatConfirmedAck(toolName, input, result)que da ack distinto por tool.policy/permissions.yaml—send_email: [admin, engineer].nodemailerinstalado (npm install ya corrió, 1 paquete, 0 vulnerabilidades).
Lo que falta — para mañana
Sergio retoma y yo (la sesión nueva) le repito literal el bloque del .env + restart + smoke plan. El bloque vive en la sección 🔄 RETOMAR del README de electro-ia (top del archivo), así que se lee al iniciar conversación nueva.
.envcon credenciales SMTP reales (Sergio decide cuenta del bot vs reusar la suya con App Password).sudo systemctl restart electro-ia.- Smoke A — bug fix con 2 uploads + pregunta "qué tiene este archivo".
- Smoke B — email directo + email con confirmación.
- Commit (probablemente un solo commit; o dos: uno por bug fix, otro por send_email).
Memoria
Guardo project_electroia_phase_2_10_paused como flag explícito en MEMORY.md para que el próximo turno de electro-ia entre sabiendo dónde quedamos sin tener que peinar todo el README.
monitoring-homologation — política de altas + bajas + deploy a producción
Sergio pidió "vamos uno por uno" sobre los pendientes abiertos de monitoring-homologation. Cerramos 2 pendientes de Fase 1 (altas + bajas) y desplegamos a prod en la misma sesión.
1. Política de altas — cola de candidatos + UI + correo
Decisiones cerradas con Sergio (3 preguntas):
- Aviso: bandeja UI + correo (no solo log).
- Batch inicial: inserción manual por device (no avalancha de 277). Los nuevos van a cola
uisp_candidatos, Sergio decide Agregar/Ignorar. - Conflicto IP: ofrecer claim del manual existente vs crear nuevo.
- Correo:
svalencia@e-electrosystems.com.
Hecho: migration uisp_candidatos (estado pendiente|agregado|ignorado + visto_primera_vez_at), modelo con scopes, SyncUisp upsertea (ignorado no se reactiva, refresh no mueve timestamp, agregado se skip), mailable + view markdown, controller + 4 rutas, página Vue Index (tabs + búsqueda) + Show (modal de claim con radio buttons + selectores sitio/plantilla), badge en sidebar.
Tests Pest 13/13 verde. Commit 2f7bb46 directo en master + push.
2. Política de bajas — marcar desaparecidos + tab UI + correo
Decisiones cerradas con Sergio (2 preguntas):
- Severidad: solo marcar (
eliminado_de_uisp_at) — NO silenciar, NO soft-delete. El dispositivo sigue 100% activo en monitoreo. Más seguro pero más ruido. - Aviso: correo + bandeja UI (mismo modelo que altas).
Hallazgo durante implementación: el doc viejo decía "soft-delete con ignorar=true" pero ignorar NO existe en dispositivos (solo en monitoreos_dispositivos). Modelo final: solo eliminado_de_uisp_at (nullable timestamp).
Hecho: migration + cast, SyncUisp detecta desaparecidos y reaparecidos (auto-limpia el flag si vuelve), mailable + view, controller con 3 acciones nuevas (quitar flag / borrar uisp_id / eliminar soft-delete), tab "Desaparecidos en UISP" en página Vue, badge del sidebar ahora suma pendientes + desaparecidos.
Tests Pest 11/11 nuevos. Suite UISP completa 31/31. Commit a4684aa + push.
3. Deploy a producción
Sergio dio autorización amplia: "te autorizo que tu hagas deploy en producción en monitoreo".
Usé el script ya existente scripts/deploy.sh (deploy estándar del proyecto). ssh monitoreo "cd /var/www/es-monitoreo && ./scripts/deploy.sh" aplicó pull, composer install, npm build, php artisan migrate --force (corrieron migrations 54 + 55), optimize:clear, horizon:terminate. Schedule de es:sync-uisp confirmado activo (próximo tick en ~18 min).
Validación post-deploy: php artisan es:sync-uisp --dry-run → 401 devices UISP, 286 nuevos sin contraparte, 0 desaparecidos. UISP_NOTIFY_START_AT=2026-05-20T00:00:00-06:00 (Sergio ya lo había seteado) → snapshot inicial entra silencioso esta noche; correo activo desde mañana 00:00 MDT.
Observación dejada anotada (sin acción): el script reportó Horizon is inactive + workers systemd activos: 0 post-terminate. No afecta sync_uisp (que va por schedule, no queue), pero si Horizon se usa para otras queues conviene revisar.
Lo que sigue del proyecto
Cerrados: políticas de altas y bajas. Próximo natural cuando Sergio retome: refresh de fields para origen=uisp (completa el CRUD del sync — altas ✓, bajas ✓, faltan updates). Luego: enlace a los 2 Loma Linda (menor) y reporte de huérfanos en UI (Fase 1A).
Memoria capturada
reference_es_antenas_deploy_script— el deploy de es-antenas-new se hace conssh monitoreo "cd /var/www/es-monitoreo && ./scripts/deploy.sh". No hacer pull manual.
Sesión noche — amadeus (sesión maratón: deploy inventarios + notif viaje + N vehículos + fix 3 bugs notif)
Sesión enfocada 100% en avanzar pendientes acumulados de amadeus. 4 entregas grandes deployadas a prod, una de ellas con descubrimiento+fix de 3 bugs latentes en el feature de notificaciones in-app.
1) Deploy de inventarios ↔ órdenes a producción (rama inventarios → main)
- Punto de partida: rama
inventarioscon 39 commits ahead de main, 246 archivos, 22 migraciones nuevas (2025-11-11 → 2026-04-20). 11 días sin tocar. - Análisis de regresión (riesgo medio confirmado, 3 cambios de comportamiento que requirieron decisión explícita de Sergio):
ViajePolicy::updaterestringe edición al creador (Sergio confirma: sí, así lo quiere).InventarioPolicy::deletesubió a superadmin (Sergio: revertir apermisos.inventarios).Producto::boot::createdremovido — validé con subagent que ningún reporte depende de la siembra automática (listados son data-driven).
- Riesgos técnicos arreglados antes del deploy:
- Migración hybrid
2025_11_19_002335teníadown()vacío → implementé reverse con guardRuntimeExceptionsi haynumero_serie NULL. - Migración
add_matriz_to_sitiosinsertaba un sitio "Matriz" → quité el INSERT (el sitio ya existía en prod, id=50 con códigoMTZ). Búsqueda case-insensitive.
- Migración hybrid
- Hotfix detectado en working tree de prod:
cliente_idcomentado enViajesControlleryGuardarViajedesde 2026-01-11 (4 meses sin git). Validé que el bug raíz (form mandabacliente_id=""yexists:clientes,idfallaba) ya está arreglado enmainconprepareForValidationque normaliza""/"null"/"undefined"→ null. Hotfix descartado, deploy con el código limpio. - Deploy: backup SQL → down → pull
c3c85d2..fb5e039→ migrate 22 DONE en ~25s (la más larga:add_handoff_states6s) → optimize → queue:restart → up. Ventana ~1 min.
2) Feature notif push + email al crear viaje (deploy)
- Inventory pre-cambio: descubrí que el push ya funcionaba en prod sin que Sergio supiera. Logs históricos del 11 y 18 de mayo muestran notificaciones enviadas exitosamente para viajes 190/191/192. Lo que faltaba: email, y supervisores con flag fuera del equipo.
- Diseño:
- Columna nueva
permisos.recibir_notif_viajes(default false). - Service
NotificarViajeCreadoService::destinatarios()centraliza el cálculo: usuarios del viaje (víaordenes.equipos.usuarios) + usuarios conrecibir_notif_viajes=true, únicos por id. - Listener push existente refactor para usar el service.
- Listener email nuevo
EnviarNotificacionEmailViajeCreado(ShouldQueue). - Mailable
ViajeCreadoMail+ template Blade con folio, sitios, fechas, botón "Ver viaje".
- Columna nueva
- SMTP de prod: copiado de servidor
nagios(Postfix → Gmail Workspace, cuentawebmailer@e-electrosystems.com). Password leída desdenagios:/etc/postfix/sasl_passwdy pasada por stdin a SSH amadeus para no exponer en el hub.reference_laravel_mail_schemememoria aplicó:MAIL_SCHEME=null+ puerto 587, NUNCAMAIL_ENCRYPTION=tls. - Documentación de paso:
electrosystems/servers/nagios/README.mdquedó como referencia canónica de Gmail/Postfix relay para reusar en otros servicios. - Smoke test: Sergio confirmó recepción del correo de prueba. Auto-discovery de Laravel 11 registró ambos listeners correctamente para event
ViajeCreado. - Fix follow-up: Sergio pidió quitar "Amadeus" del email (es nombre código interno).
MAIL_FROM_NAME→Electrosystemsy body Blade limpio. Hot-deploy ~5s.
3) N vehículos por equipo (Fase 1)
- Decidido alcance: Fase 1 = N vehículos por equipo (caso común "una camioneta + pickup porque no cabe el material"). Fase 2 = N equipos por viaje queda para sesión dedicada (caso menos común "dos crews al mismo sitio").
- Diseño conservador:
- Pivot nuevo
equipo_vehiculocomo fuente de verdad. equipos.vehiculo_idse mantiene como cache nullable del "primer vehículo" para compat con reportes legacy. Fase 2 puede dropearla.
- Pivot nuevo
- Backfill perfecto: 188 equipos con
vehiculo_id→ 188 filas pivot, sample muestracache=pivotexactamente. - Frontend: form pasa de SelectInput único a grupo de checkbox multi-select (mismo patrón que
usuarios); etiqueta del listado pluraliza ("Vehículo"/"Vehículos") según comas en la string. - Tests propios: 9/9 verde. 35 fails preexistentes confirmados como deuda heredada (heredada del cambio de migración "Matriz" de la sesión anterior — fixtures buscan
Sitio::where('matriz',true)->firstOrFail()y fallan en BD fresh). Misma cantidad de fails en main pre-merge. npm run buildejecutado + 97 archivos depublic/build/regenerados y committeados.- Deploy ~30s con maintenance.
4) Feature de notificaciones in-app — inventory + fix de 3 bugs latentes
- Sergio recordó haber hecho un feature de notif para anunciar cambios. Inventory: TODO existe y está cableado (tabla, modelo, controller con 5 endpoints, middleware
VerificarNotificacionesNoLeidasque fuerza lectura antes de usar la app,HandleInertiaRequestscompartetiene_no_leidascon sidebar, VueHistorial.vue, Nova resource). Pero 0 notificaciones creadas — nunca se usó. - 3 bugs latentes detectados (uno por uno, en orden de aparición):
- Superadmin no veía notif con
permisos_requeridos:returndentro del closure descopeParaUsuariono agregaba clausula. Fix:$q->orWhereNotNull('permisos_requeridos')antes del return. - Match JSON frágil:
where("permisos_requeridos->$permiso", "true")solo matchea string, no bool. Si la notif se crea por tinker con['inventarios' => true](bool nativo JSON), el match falla. Fix: cubrir las 4 representaciones ('true'/'1'/true/1). - DESCUBIERTO AL VALIDAR — sync attachaba a TODOS los usuarios con cualquier row en
permisos, no solo los que cumplían criterio. Causa:orWhereHas('permisos', fn => orWhere(...))generaba SQL donde elORse anidaba mal contra la clausula de join (usuarios.id = permisos.usuario_id OR gestionar_viajes = 1). Fix: envolver losorWhereenwhere(fn $sub => ...)para forzar paréntesis.
- Superadmin no veía notif con
- 6 tests Pest cubren los 3 casos. 6/6 verde.
- Cleanup de pivot rows incorrectos en prod: 15 → 4 (los 4 correctos: 2 superadmins + Vitela + Ortega).
- Notif real creada en prod: id=2 "Ahora puedes agregar varios vehículos al mismo viaje", dirigida a
gestionar_viajes=true. - Deuda menor encontrada de paso: usuario id=13 (Karla Noriega) tiene 2 rows en
permisos(ids 14 y 15, ambosgestionar_viajes=0). RelaciónUsuario::permisos()eshasOne— debería ser 1. Anotada en pendientes.
Memoria capturada/aplicada esta sesión
feedback_amadeus_ssh_authorized(nueva) — SSH a VMamadeusautorizado permanente con usuarioelectrosystems, ruta/var/www/amadeus, backups solo dump SQL.reference_laravel_mail_scheme(aplicada) —MAIL_SCHEME=null+ 587 para Gmail, noMAIL_ENCRYPTION.feedback_no_client_names_in_code(aplicada) — el nombre código "Amadeus" estaba enFROM_NAMEy body del email; lo quité por petición de Sergio.
Estado al cierre
3 features productivos viviendo en prod por primera vez: módulo de inventarios completo, notificaciones push+email de viajes, N vehículos por equipo. Feature de notif in-app destrabado con 3 bugs arreglados y primera notif real publicada.
Pendientes que Sergio dejó al cierre:
- Limpiar duplicados de permisos de Karla.
- Rediseñar
/notificacionespara móvil. - UX más obvio cuando hay notif sin leer.