Hub

electrosystems

amadeus

active medium work
Creado
2026-05-08
Actualizado
2026-05-29
Directorios
  • /home/sergio/code/amadeus
Aliases
viaticos

Pendientes abiertos (38)

Ver todos →

🎯 Top de ataque (37)

  • sin fecha B.1 Migración auditorias_viaje + modelo + relaciones.
  • #392 ⏱ 2h 🔥 Quick-win nativo (ya viable). El Traccar de ares es 6.6 con emailEnabled:true → se puede definir el geofence "Cd. Juárez" + una notificación por email a la contadora directo en la UI de Traccar, sin tocar Amadeus. Requiere login admin de Traccar (lo tiene Sergio) — yo lo guío o lo hace él. Limitación conocida: dispara con cualquier entrada a Juárez (ruido) y sin contexto de viaje; cobertura provisional hasta el bridge.
  • #249 📅 2026-06-01 🔥 Prender ANALISIS_HOOK_CONSUMOS_ENABLED=true (validación automática al subir). Desbloqueado — workers ya listos (#371 cerrado). (Bloque 0)
  • sin fecha B.2 AuditorViajeService con flags determinísticos.
  • #393 cond:post-milestone ⏱ 1d ⚠️ Cimientos del bridge. Columna traccar_device_id en vehiculos + mapeo unidad↔device (consultando GET http://201.218.172.10:8082/api/devices); permiso nuevo notificar_retorno en permisos (Nova + modelo); crear token/usuario read-only en Traccar 6.6 (login admin de Sergio); montar scheduler (cron de php artisan schedule:run, hoy inexistente).
  • #369 📅 2026-06-04 ⏱ 1d 🔥 PR 1-4 de políticas/lineamientos editables (amadeus-politicas-viaticos.md). D1-D5 cerradas. ✅ PR 1, PR 2, PR 3 y PR 4 COMPLETADOS (commits 7e53af6/449a2cb/2506bb2/c60fc91 + PR 4 listo en local). Pendiente: (1) validación en staging/prod (correr analisis:auditar-viaje <viaje-cerrado> y comparar violaciones_politica vs juicio humano), (2) deploy de PR 4 a producción. (Bloque 0b)
  • sin fecha B.3 Llamada extra a Antigravity para resumen ejecutivo (helper en mismo service o ResumidorAuditoriaService).
  • #394 cond:post-milestone ⏱ 2d ⚠️ Bridge Amadeus↔Traccar. Comando programado que consulta la posición (GET http://201.218.172.10:8082/api/positions?deviceId= en Traccar 6.6) de los vehículos en viaje activo (fecha_cierre IS NULL y destino fuera de Juárez); detecta transición fuera→dentro del bbox de Juárez; dispara evento RetornoDetectadoEvent → listener push+email a quienes tengan notificar_retorno. Mensaje con contexto: "Vehículo U09 (viaje folio X, técnico Y) regresó a Cd. Juárez a las HH:MM." Idempotente (no re-avisar). Alternativa más limpia: notificación web (webhook) de Traccar 6.6 al geofence → endpoint en Amadeus (evita scheduler).
  • #380 📅 2026-06-06 ⏱ 1d ⚠️ Pantalla mapa Leaflet+OSM de consumos con popup/hover de detalle. (Bloque C — victoria visual)
  • sin fecha B.4 AuditoriaViajeController + ruta + policy.
  • #378 📅 2026-06-09 ⏱ 0.5d ⚠️ Coords oficina/matriz (config) + medir cobertura GPS de sitios. (Hospedaje omitido.) (Bloque B)
  • sin fecha B.5 Page Vue Admin/AuditoriaViaje.vue con secciones del diseño.
  • #379 📅 2026-06-10 ⏱ 1d ⚠️ Validación de ubicación por corredor (sitio/oficina/segmento), solo bandera; pasar anclas a la IA. (Bloque B)
  • sin fecha B.6 Botón "Auditar" en /viajes/{id} show (visible si admin).
  • #382 📅 2026-06-16 ⏱ 2d ⚠️ Subida offline = borrador local (IndexedDB + sync al reabrir con señal, preservar ubicación, notificar si queda observado). (Bloque D — el más caro)
  • sin fecha B.7 Comando artisan analisis:auditar-viaje.
  • #387 cond:post-milestone ⏱ 3-4d ⚠️ Reloj checador en sitio (geolocalizado) para técnicos de viaje. Checada con captura de coordenada + cotejo contra el GPS del sitio asignado (geofence por radio) para validar que estén físicamente en el lugar de trabajo. Validación suave (bandera, no bloqueo), coherente con la filosofía del milestone. Diseño/ideas en Notas técnicas → Checador geolocalizado (#387). Depende de coords de sitios pobladas (#378).
  • sin fecha B.8 Tests Pest end-to-end.
  • #388 cond:post-milestone ⏱ 4-6d ⚠️ Ingesta de facturas desde la bandeja de contabilidad + ligado automático a consumos. Leer los correos donde llegan las facturas (CFDI) de los viáticos, parsear el XML, y ligarlas automáticamente al consumo del técnico que corresponde (por monto + fecha + RFC/comercio) para que la contadora ya no cotije a mano al validar/imprimir. Diseño/ideas en Notas técnicas → Ingesta de facturas CFDI (#388).
  • sin fecha B.9 Smoke test contra viaje 191 / Manuel para validar que la IA captura los patrones que ya vemos en metadatos.
  • #392 ⏱ 2h 🔥 Quick-win nativo (ya viable): geofence Juárez + email a contadora directo en la UI de Traccar 6.6. Requiere login admin de Sergio.
  • #393 cond:post-milestone ⏱ 1d ⚠️ Cimientos del bridge: traccar_device_id en vehículos + permiso notificar_retorno + scheduler.
  • #394 cond:post-milestone ⏱ 2d ⚠️ Bridge Amadeus↔Traccar: re-entrada a Juárez de vehículos en viaje activo → push+email con contexto.
  • #395 cond:fase-2-lista ⏱ 2-3d Futuro: detección por celular del técnico (PWA) para viajes en avión.
  • #048 📅 2026-06-02 #048 Asignar permisos inventarios_preparar/_entregar/_revisar al resto (18 usuarios en 0) cuando Selene valide el piloto. Capa 1 ON parcial para Selene Ortega (user_id=14, los 3 permisos en 1). Capa 2 (INVENTARIO_VIAJE_LINK) sigue OFF — solo Sergio la activa editando .env.
  • #047 📅 2026-06-01 #047 Smoke piloto Selene — flujo manual end-to-end de inventarios desde /movimientos-inventario (preparar → entregar → revisar). Probar también MisEntregas y ViajeCompras. Crear OT, asociar inventario, verificar descuento.
  • #050 📅 2026-06-03 #050 Smoke N vehículos fase 1 — crear viaje con 2+ vehículos, verificar guardado, edit (vuelve a marcar los 2), listado pluralizado, reporte.
  • #211 📅 2026-06-04 #211a Smoke notificaciones in-app — crear notif en Nova con permisos_requeridos={"inventarios":true} y validar filtrado por superadmin + usuario con permiso + usuario sin permiso. Badge y campana móvil deben encenderse.
  • #175 📅 2026-06-05 #175 Smoke E2E bot electro-ia ↔ amadeus — desde Telegram (documentado en electro-ia con plan completo).
  • 📅 2026-06-05 Decidir branch a desplegar (recomendado: merge a main primero por trazabilidad).
  • 📅 2026-06-06 Backup de la BD de producción antes de migrar.
  • 📅 2026-06-06 Revisar las 22 migraciones por orden (especialmente modify_* y remove_* — son destructivas si fallan a mitad).
  • 📅 2026-06-07 Plan de rollback: snapshot de la VM en Poseidon + dump de BD. Reversión de migraciones no es trivial con tantos cambios encadenados.
  • 📅 2026-06-07 Ventana de despliegue acordada (¿fuera de horario operativo?).
  • 📅 2026-06-08 Composer install + npm build + php artisan migrate --force + php artisan optimize.
  • 📅 2026-06-08 Verificar permisos nuevos en add_inventario_granular_permisos (puede que necesite seed/sync de roles).
  • 📅 2026-06-08 Smoke test: crear OT, crear movimiento de inventario (rápido y normal), revisión de traslado, viaje_compras.

📦 Backlog (1)

  • #395 cond:fase-2-lista ⏱ 2-3d Fase futura: detección por celular del técnico. Para las contadas ocasiones en que el técnico viaja en avión (el vehículo no se mueve, así que Traccar no lo detecta). Geofence ligero vía la PWA de Amadeus (navigator.geolocation), reusando el patrón de captura GPS de #381 (consumos) y del checador #387. Más invasivo (requiere permiso de ubicación / app abierta) — por eso es fase 2, solo el caso avión.

Actividad en bitácora 10 días

jun
jul
ago
sep
oct
nov
dic
ene
feb
mar
abr
may
L
X
V
Menos
Más

Amadeus

Alias: viáticos. “Amadeus” es solo el nombre interno del código. En conversación con el equipo se llama “plataforma de viáticos”.

Contexto

Plataforma interna de Electrosystems para captura de viáticos y órdenes de trabajo, además de control de inventarios y sitios (ubicaciones físicas donde se realiza el trabajo). Uso interno por personal de la empresa.

Backlog Jira: 51 tickets analizados en amadeus-backlog.md (importado 2026-05-20). Sprints 15-21 + backlog diferido documentados en amadeus-sprints.md (en lugar de Jira, por decisión 2026-05-25). Sergio decide ítem por ítem antes de mover a “En progreso”.

Tareas pendientes

🔥🔥 MILESTONE URGENTE — Consumos bien capturados + IA por lineamientos (2026-05-28)

Design doc completo en amadeus-consumos-milestone.md. El sistema de auditoría IA ya está en prod; este milestone lo vuelve apelable (rechazo no terminal), agrega validación de ubicación, mapa de consumos y captura offline, todo bajo lineamientos editables (#369). Decisiones tomadas 2026-05-28 (observado / 3 re-subidas / sin hospedaje / borrador local). Deadline 2-junio NO cabe completo → cut = Bloque 0 + Bloque A.

🎯 Cut 2-junio (apelación end-to-end):

  • #374 ✅ DEPLOYADO 2026-05-28 — IA escribe observado (no rechazado); NoRechazadosScope ajustado (+bug fix). Mergeado a main + deploy a prod, migración reseteó 59 IA-rechazados históricos a NULL. (Bloque A — ver bitácora)
  • #375 ✅ DEPLOYADO 2026-05-28 — Botón “Apelar” del técnico → cola de revisión humana (sin re-subir foto) + notifica al admin. Commit 85e0d16 en main, prod migrado (columnas apelado_at/apelacion_motivo + ruta OK). (Bloque A — ver bitácora)
  • #376 ✅ DEPLOYADO 2026-05-28 — Cola de revisión/apelaciones del admin (/autorizaciones evolucionada: apeladas primero, rechazo-con-motivo, notifica al técnico). Commit 8015dd5 en main, prod sin migración. (Bloque A — ver bitácora)
  • #377 ✅ DEPLOYADO 2026-05-28 — Tope de 3 re-subidas → ancla a revisión humana. Commit 83b1964 en main, migración corrida en prod (intentos_fallidos OK, max_intentos_revision=3). Cierra el Bloque A completo (#374→#377). (Bloque A — ver bitácora)
  • #249 📅 2026-06-01 · 🔥 — Prender ANALISIS_HOOK_CONSUMOS_ENABLED=true (validación automática al subir). Desbloqueado — workers ya listos (#371 cerrado). (Bloque 0)

Post 2-junio:

  • #369 📅 2026-06-04 · ⏱ 1d · 🔥 — PR 1-4 de políticas/lineamientos editables (amadeus-politicas-viaticos.md). D1-D5 cerradas. ✅ PR 1, PR 2, PR 3 y PR 4 COMPLETADOS (commits 7e53af6/449a2cb/2506bb2/c60fc91 + PR 4 listo en local). Pendiente: (1) validación en staging/prod (correr analisis:auditar-viaje <viaje-cerrado> y comparar violaciones_politica vs juicio humano), (2) deploy de PR 4 a producción. (Bloque 0b)
  • #380 📅 2026-06-06 · ⏱ 1d · ⚠️ — Pantalla mapa Leaflet+OSM de consumos con popup/hover de detalle. (Bloque C — victoria visual)
  • #378 📅 2026-06-09 · ⏱ 0.5d · ⚠️ — Coords oficina/matriz (config) + medir cobertura GPS de sitios. (Hospedaje omitido.) (Bloque B)
  • #379 📅 2026-06-10 · ⏱ 1d · ⚠️ — Validación de ubicación por corredor (sitio/oficina/segmento), solo bandera; pasar anclas a la IA. (Bloque B)
  • #381 ✅ 2026-05-29 — Capturar GPS al momento de tomar la foto (no al submit) + persistir ubicación original. (Bloque D)
  • #382 📅 2026-06-16 · ⏱ 2d · ⚠️ — Subida offline = borrador local (IndexedDB + sync al reabrir con señal, preservar ubicación, notificar si queda observado). (Bloque D — el más caro)

📋 Post-milestone — features nuevas (capturadas 2026-05-28)

Disparo condicional: arrancan después del milestone de consumos en curso. Ambas tienen fuerte sinergia con piezas de ubicación que ese milestone deja a medio camino (#378 coords de sitios, #381 captura GPS).

  • #387 📅 cond:post-milestone · ⏱ 3-4d · ⚠️ — Reloj checador en sitio (geolocalizado) para técnicos de viaje. Checada con captura de coordenada + cotejo contra el GPS del sitio asignado (geofence por radio) para validar que estén físicamente en el lugar de trabajo. Validación suave (bandera, no bloqueo), coherente con la filosofía del milestone. Diseño/ideas en Notas técnicas → Checador geolocalizado (#387). Depende de coords de sitios pobladas (#378).
  • #388 📅 cond:post-milestone · ⏱ 4-6d · ⚠️ — Ingesta de facturas desde la bandeja de contabilidad + ligado automático a consumos. Leer los correos donde llegan las facturas (CFDI) de los viáticos, parsear el XML, y ligarlas automáticamente al consumo del técnico que corresponde (por monto + fecha + RFC/comercio) para que la contadora ya no cotije a mano al validar/imprimir. Diseño/ideas en Notas técnicas → Ingesta de facturas CFDI (#388).

🚗 Control de retorno de técnicos a Juárez (capturado 2026-05-28) → amadeus-control-retorno.md

Avisar automáticamente a contabilidad cuando un técnico regresa a Juárez tras un viaje, sin fricción para el técnico. ✅ DESBLOQUEADO 2026-05-28: el Traccar real y vivo corre en el host ares (no en el orion muerto del #391). Premisa confirmada. Roadmap y diseño en el design doc.

  • #391 ✅ 2026-05-28 — Validado read-only: el traccar2025 de orion NO sirve (red muerta = leftover de migración). Cierra #138.
  • #396 ✅ 2026-05-28 — Fuente GPS confirmada: Traccar vivo en host ares (192.168.3.2, ~20.5M check-ins). ares documentado en electrosystems/servers/ares/.
  • #398 ✅ 2026-05-28 — Internals del Traccar capturados: Traccar 6.6 en 201.218.172.10:8082 (API REST alcanzable desde la LAN, emailEnabled:true), puerto dispositivos 5046. Detalle en electrosystems/servers/ares/.
  • #392 📅 esta-semana · ⏱ 2h · 🔥 — Quick-win nativo (ya viable): geofence Juárez + email a contadora directo en la UI de Traccar 6.6. Requiere login admin de Sergio.
  • #393 📅 cond:post-milestone · ⏱ 1d · ⚠️ — Cimientos del bridge: traccar_device_id en vehículos + permiso notificar_retorno + scheduler.
  • #394 📅 cond:post-milestone · ⏱ 2d · ⚠️ — Bridge Amadeus↔Traccar: re-entrada a Juárez de vehículos en viaje activo → push+email con contexto.
  • #395 📅 cond:fase-2-lista · ⏱ 2-3d — Futuro: detección por celular del técnico (PWA) para viajes en avión.

🔥 Prioridad MAX — antes de cualquier sprint

  • (2026-05-29) #239 Validación inteligente de fotos en consumos.VERIFICADO HECHO (las 3 fases del scope + extras, ya en main). Ver bitácora 2026-05-29 y amadeus-validacion-fotos.md. Solo falta prender el flag en prod = #249.

Operación

  • 📅 2026-06-02 — #048 Asignar permisos inventarios_preparar/_entregar/_revisar al resto (18 usuarios en 0) cuando Selene valide el piloto. Capa 1 ON parcial para Selene Ortega (user_id=14, los 3 permisos en 1). Capa 2 (INVENTARIO_VIAJE_LINK) sigue OFF — solo Sergio la activa editando .env.

Smoke tests pendientes (acumulados de los deploys 2026-05-19/20/22)

  • 📅 2026-06-01 — #047 Smoke piloto Selene — flujo manual end-to-end de inventarios desde /movimientos-inventario (preparar → entregar → revisar). Probar también MisEntregas y ViajeCompras. Crear OT, asociar inventario, verificar descuento.
  • 📅 2026-06-03 — #050 Smoke N vehículos fase 1 — crear viaje con 2+ vehículos, verificar guardado, edit (vuelve a marcar los 2), listado pluralizado, reporte.
  • 📅 2026-06-04 — #211a Smoke notificaciones in-app — crear notif en Nova con permisos_requeridos={"inventarios":true} y validar filtrado por superadmin + usuario con permiso + usuario sin permiso. Badge y campana móvil deben encenderse.
  • 📅 2026-06-05 — #175 Smoke E2E bot electro-ia ↔ amadeus — desde Telegram (documentado en electro-ia con plan completo).

Sprints 15-21 (documentados en amadeus-sprints.md)

29 tickets distribuidos en 7 sprints + 6 diferidos. IDs #209-#238. No se trabaja hasta que Sergio diga qué sprint arrancar.

  • (2026-05-20) Notificaciones — UX del indicador: badge ”!” → contador numérico con color por urgencia (rojo+pulse / azul) + nueva campana en topbar móvil. Toast/modal al primer load NO se hizo (el middleware ya redirige para urgentes; ModalNotificaciones.vue sigue como código muerto). “Color del item del menú” tampoco — el badge ya es suficiente.
  • (2026-05-20) Notificaciones — diseño móvil de /notificaciones (Pages/Notificaciones/Historial.vue): header flex-col en móvil, card de notif con botón “Marcar como leída” full-width abajo, header de card con flex-wrap, padding p-4 sm:p-6, fechas text-xs sm:text-sm, filtros con flex-wrap. Desktop conserva layout horizontal.
  • (2026-05-20) Limpiar código muerto ModalNotificaciones.vue: eliminado (carpeta Notificaciones/ quedó vacía y también removida).
  • (2026-05-20) Endpoint sin uso /notificaciones/tiene-no-leidas: eliminado método del controller + su ruta.
  • (2026-05-20) Limpiar duplicado de Karla en permisos: borrado row id=15 (fósil 2024-08-19 con recibir_notif_viajes=0). Preservado id=14 (updated_at=2026-05-19 23:30:42, recibir_notif_viajes=1) — es el row que la relación Usuario::permisos() (hasOne, default order asc) ya estaba devolviendo. Sin duplicados restantes en la tabla.
  • (2026-05-20) Limpiar public/build/* huérfanos en prod: 28 archivos removidos con git clean -f. Working tree de prod ahora limpio salvo .env.backup-20260519-2234.
  • (2026-05-20) Commitear deploy.sh: ahora tracked en main (commit 008118c). Permisos +x preservados en prod.

Estado del repo (revisado 2026-05-19)

  • Rama actual local: inventarios (limpia, en sync con origin/inventarios).
  • Rama inventarios está 39 commits ahead de origin/main.
  • 246 archivos cambiados vs main.
  • 22 migraciones nuevas (rango: 2025_11_11 → 2026_04_20). Tocan: productos, inventarios, movimiento_inventarios + productos + revisiones, estatus_inventario, viaje_compras, sitios (matriz), equipos (nullable + lider), permisos granulares de inventario.
  • No hay script de deploy ni GitHub Actions en el repo (.github/workflows/ no existe).
  • Host de prod: VM amadeus en Poseidon (2c/2G), expuesta vía reverse-proxy.

Checklist de deploy (pendiente de ejecutar)

  • 📅 2026-06-05 — Decidir branch a desplegar (recomendado: merge a main primero por trazabilidad).
  • 📅 2026-06-06 — Backup de la BD de producción antes de migrar.
  • 📅 2026-06-06 — Revisar las 22 migraciones por orden (especialmente modify_* y remove_* — son destructivas si fallan a mitad).
  • 📅 2026-06-07 — Plan de rollback: snapshot de la VM en Poseidon + dump de BD. Reversión de migraciones no es trivial con tantos cambios encadenados.
  • 📅 2026-06-07 — Ventana de despliegue acordada (¿fuera de horario operativo?).
  • 📅 2026-06-08 — Composer install + npm build + php artisan migrate --force + php artisan optimize.
  • 📅 2026-06-08 — Verificar permisos nuevos en add_inventario_granular_permisos (puede que necesite seed/sync de roles).
  • 📅 2026-06-08 — Smoke test: crear OT, crear movimiento de inventario (rápido y normal), revisión de traslado, viaje_compras.

En progreso

  • Sin trabajo activo. El deploy de inventarios se ejecutó el 2026-05-19 (ver bitácora). Quedan smoke tests funcionales arriba.

Notas técnicas

Stack

  • Laravel 11, PHP 8.3.29
  • Inertia v1 + Vue 3
  • Tailwind 3
  • Laravel Nova v4 — panel admin
  • Laravel Sail (Docker)
  • Pest 2 + PHPUnit 10
  • Telescope, Socialite, Prompts

Convenciones (de Laravel Boost)

  • Comandos vía vendor/bin/sail.
  • Eloquent sobre DB::. Form Requests para validación. Casts en método casts().
  • Pint para formato (vendor/bin/sail bin pint --dirty).
  • Tests Pest, no eliminar tests sin permiso.

Checador geolocalizado (#387) — ideas de implementación

Objetivo: que el técnico de viaje cheque en sitio y que la coordenada de la checada se coteje contra el sitio de trabajo para validar presencia física.

Cómo cotejar coordenada vs sitio (de más simple/robusto a más sofisticado):

  1. Geofence por radio (haversine) — la base recomendada. Cada sitio tiene lat/lng (ya nullable; #378 los va a poblar). Al checar, se calcula la distancia haversine entre la coord de la checada y la del sitio asignado; si cae dentro de un radio configurable (sugerido 150–500 m, ajustable por sitio porque hay sitios de campo abierto vs urbanos) → “en sitio”. Cálculo en PHP/MySQL, sin servicios externos.
  2. Match al sitio más cercano del viaje activo. Como el técnico trae un viaje con sitios asignados, se busca el sitio más cercano de ese viaje y se mide contra su radio — así se distingue “llegó al sitio A” vs “anda fuera”.
  3. Reverse geocoding / ciudad (más débil) como respaldo cuando el sitio no tiene GPS: comparar ciudad de la coord vs ciudad del sitio. Ya hay precedente con el OCR de ciudad en tickets.

Filosofía (alineada al milestone): validación suave — la checada siempre se registra con su coord; si cae fuera del radio se marca fuera_de_sitio (bandera para revisión humana), nunca se bloquea la checada (el técnico puede estar en un punto legítimo cercano: hospedaje, comercio, acceso al predio). Mismo criterio que #379 (ubicación solo como bandera).

Anti-spoofing (GPS falso) — sin sobre-ingeniería en v1:

  • Capturar accuracy del navegador y marcar/descartar lecturas con accuracy > ~100 m (GPS pobre o WiFi-only).
  • En Android, registrar si la lectura viene de mock location (isFromMockProvider) cuando se pueda; marcar como sospechosa.
  • Guardar timestamp del dispositivo y del servidor; un desfase grande es señal.
  • Opcional v2: foto/selfie en la checada (reusa el pipeline de fotos que ya existe).

Modelo de datos sugerido: tabla checadas (o asistencias_sitio): usuario_id, viaje_id (nullable), sitio_id matcheado, tipo (entrada/salida), lat, lng, accuracy, distancia_m, dentro_de_sitio (bool), source_mock (bool), capturado_at_dispositivo, created_at. Captura GPS reusando el mismo patrón navigator.geolocation de consumos (idealmente compartir helper con #381).

Sinergias / dependencias: depende de que los sitios tengan coords pobladas (#378). Reusa la captura GPS de #381. Nota: ya hay experiencia con relojes checador en otro proyecto (jm-checador), pero ese es de oficina con descansos fijos — aquí el caso es geofence en campo, arquitectura distinta.

Ingesta de facturas CFDI (#388) — ideas de implementación

Objetivo: leer los correos de la bandeja de contabilidad donde llegan las facturas de viáticos y ligarlas solas a los consumos, para que la contadora no cotije a mano al validar/imprimir.

Parseo — el XML del CFDI es la pieza de oro: en México las facturas llegan como PDF + XML adjunto (CFDI 4.0). El XML trae estructurado: RFC emisor (comercio), RFC receptor (Electrosystems), total, UUID/folio fiscal, fecha de emisión y conceptos. Parsear el XML es mucho más confiable que OCR del PDF. Dedup por UUID.

Ingesta de correo (elegir según proveedor de la bandeja):

  • IMAP polling con webklex/laravel-imap + comando Laravel scheduled cada X min → descarga adjuntos XML+PDF. Es lo más simple y portable; recomendado para v1.
  • Si la bandeja es Google Workspace → Gmail API + push (Pub/Sub); si es Microsoft 365 → Graph API webhook. Más robusto pero más setup.
  • Credenciales IMAP/API solo en .env, nunca en repo. Debe ser una cuenta dedicada de contabilidad, no la personal de la contadora.

Modelo: tabla facturas (uuid CFDI único, rfc_emisor, rfc_receptor, total, fecha, xml_path, pdf_path, consumo_id nullable, estado: sin-ligar / sugerida / confirmada / sin-match).

Matching factura → consumo: por monto exacto + ventana de fecha (±N días) + RFC/comercio. El consumo ya tiene monto y fecha; con la IA de visión (#239) ya se extrae monto/concepto del ticket, lo que refuerza el match. No auto-ligar a ciegas: dejar el match como sugerencia y que la contadora confirme con un clic — le quita el cotejo manual pero conserva el control. En la vista donde valida/imprime, mostrar la factura ligada junto al ticket.

Bonus anti-fraude (sinergia con el espíritu de auditoría del proyecto): validar el UUID contra el servicio de verificación de CFDI del SAT para confirmar que la factura es real y vigente.

UI: pantalla “Facturas recibidas” con su estado + filtro de “sin match”; en la vista de validación del consumo, el bloque de factura ligada.

Bitácora

2026-05-29 (noche) — #381 implementado: Coordenadas GPS originales protegidas y UX mejorada

  • Pidió Sergio: Evitar que un usuario fraude al sistema creando un registro (apartando GPS) y editándolo luego en otro lado. Y también pulir la UI/UX móvil.
  • Hice: Verifiqué que el backend (ConsumosController::actualizar) ya protege las coordenadas iniciales ignorando la lat/lng enviada. Removí el requisito estricto de GPS en Editar.vue para facilitar correcciones. Deploy exitoso con assets compilados (Vite).
  • Estado: #381 ✅ cerrado.

2026-05-29 (tarde) — #369 PR 3 DEPLOY CONFIRMADO en prod: Analizador per-ticket evalúa contra la política

  • Pidió Sergio: confirmar deploy del PR 3 en prod. El commit c60fc91 ya estaba en origin/main pero no se había corrido deploy.sh.
  • Deploy: ssh amadeus 'cd /var/www/amadeus && ./deploy.sh' — pull Already up to date (commit ya en remoto), Nothing to migrate (migración aditiva a consumos_analisis ya corrida), queue:restart OK. Prod ahora corre el código de PR 3: ContextoTicketBuilder + AnalizadorTicketService con inyección de política + tool extendido con violaciones_politica/comentario_politica/requiere_factura_segun_politica + prompt caching (D4). Campos de política son solo informativos (D5), no tocan las puertas de rechazo.
  • Estado del #369: PR 1 ✅ + PR 2 ✅ + PR 3 ✅ en prod. Hook sigue OFF (#249). Falta: (1) validación en staging/prod — correr analisis:auditar-viaje <viaje-cerrado> y comparar violaciones_politica vs juicio humano; (2) PR 4 (hash en flags + UI del reporte).

2026-05-29 — #369 PR 1 implementado: schema + Nova + seeder de políticas de viáticos (sin tocar IA)

  • Pidió Sergio seguir con el milestone de consumos. Bloque A ya cerrado/deployado → siguiente en orden = #369 (lineamientos editables). Confirmó arrancar por #369. Implementé el PR 1 (el de bajo riesgo del plan de 4 PRs en amadeus-politicas-viaticos.md).
  • Hice (working tree en main, SIN commitear — espera auth de Sergio):
    • 2 migraciones: 2026_05_29_000001_create_politicas_viaticos_table (tablas politicas_viaticos + politicas_viaticos_historial append-only) y 2026_05_29_000002_add_politica_hash_to_auditorias_viaje (columna politica_hash CHAR(40) NULL + index). Aplicadas en BD dev.
    • Modelos PoliticaViaticos (deriva hash_sha1 del texto vía mutator de atributo — no evento, para que funcione bajo WithoutModelEvents del seeder; observer updating archiva el estado anterior en _historial; helper estático vigente()) y PoliticaViaticosHistorial (append-only, sin timestamps).
    • PoliticaViaticosPolicy (auto-discovery): superadmin bypass + permiso existente gestionar_viaticos; delete/restore/forceDelete = false (la política se edita, no se borra). Reusé el flag que ya existía en Permiso, no inventé gate nuevo.
    • Nova PoliticaViaticos (Textarea texto + Boolean vigente + Hidden editado_por_id=usuario actual vía fillUsing + Hash 7-char readonly + HasMany historial) y Nova PoliticaViaticosHistorial (read-only, fuera del menú, create/update/delete denegados). Nova auto-descubre resources (no hay registro explícito).
    • PoliticasViaticosSeeder (idempotente: no duplica si ya hay vigente) con texto BORRADOR derivado del resumen de 6 puntos ($150/comida, $100 libres/día, agua/sueros aparte, facturación, 2 personas, compras con previo aviso). Registrado en DatabaseSeeder. Factory PoliticaViaticosFactory para tests.
  • Verificado: 7 tests unit verdes (hash al crear, recálculo al editar, snapshot del estado anterior, no-historial-al-crear, acumulación, vigente() con/sin vigente). Migraciones + seed corridos en dev: política id=1 vigente, hash match=1. Pint limpio. Tests de consumos/auditoría/apelación/revisión verdes (97 passed); 3 fallos en ViajeCompraTest/MovimientoInventarioHandoffTest son los preexistentes de inventario (Sitio not found en su setup), ajenos a este cambio aditivo.
  • Falta: (1) texto oficial de la política — el seeder trae un borrador; el texto real lo confirma el área y se edita en vivo en Nova (ese es el punto de la feature). (2) Autorización de Sergio para commit+push+deploy del PR 1. (3) Que Sergio pruebe en /nova que un usuario con gestionar_viaticos puede editar. (4) Seguir con PR 2 (Resumidor inyecta política) → PR 3 (Analizador + contexto, riesgo alto) → PR 4 (persistir hash + UI).

2026-05-29 (más tarde) — #369 PR 1 DEPLOYADO + texto oficial refinado

  • Sergio autorizó commit+push+deploy y pasó el texto oficial de la política (6 puntos de la dirección). Lo refiné para que sea entendible, justo y auditable por la IA: estructura por secciones, montos explícitos ($150/comida antes de impuestos, $100 libres en jornada completa), reglas de facturación/hidratación-separada/2-personas/foto-al-teléfono-de-facturación/compras-con-previo-aviso. Quité el nombre propio (“por indicación de Gustavo” → “la dirección”) por la regla del hub de no nombres de personas en código/seed. Corregí un typo.
  • Deployado: commit 7e53af6git push origin mainssh amadeus ./deploy.sh (2 migraciones corridas en prod + queue:restart) → db:seed --class=PoliticasViaticosSeeder --force en prod. Verificado read-only: política id=1 vigente, hash match=1, historial=0. deploy.sh NO corre seeders (solo migrate), por eso el seed fue paso aparte.
  • Decisión de negocio abierta (no la asumo): el punto 2 (consumo libre en salidas parciales) — el texto original solo se quejaba sin fijar regla; lo dejé como “no se reconoce el monto completo”. Falta que la dirección decida si quiere criterio concreto (proporcional / medio día = $50) o se queda como observación (encaja con D5 solo-bandera). Editable en Nova.
  • Falta: que Sergio pruebe edición en /nova (usuario con gestionar_viaticos); luego PR 2 (Resumidor inyecta política).

2026-05-29 (más tarde) — #369 PR 2 DEPLOYADO: el Resumidor inyecta la política

  • Sergio: “avancemos con el milestone”. Implementé PR 2 (riesgo bajo).
  • Cambios: ResumidorAuditoriaService::generar() carga PoliticaViaticos::vigente(), construye el system prompt dinámico (base + lineamiento inyectado + instrucción “evalúa flags contra política, NO inventes incumplimientos, solo señalar nunca sancionar” — decisión D5) y devuelve politica_hash en todas las rutas (éxito, fallback timeout/non-2xx, deshabilitado, sin api key). AuditorViajeService::auditar() persiste el hash en auditorias_viaje.politica_hash (con ?? null, así el mock viejo del test no rompe). @property en el modelo. Pint removió de paso 2 imports muertos preexistentes.
  • Tests: 5 nuevos en ResumidorPoliticaTest (Http::fake: inyección de la política en system, hash devuelto, solo usa la vigente, hash aun con análisis deshabilitado, y persistencia en auditorias_viaje vía AuditorViajeService). 22 verdes con AuditorViajeServiceTest (sin regresión). Suite amplio análisis: 106 passed; 3 fallos ViajeCompraTest preexistentes (Sitio not found), ajenos.
  • Deploy: commit 2506bb2 → push → deploy.sh (sin migración, queue:restart). Validación end-to-end pagada (analisis:auditar-viaje <id>, ~$0.35) queda disponible a pedido — la inyección+persistencia ya están cubiertas por Http::fake.
  • Falta: PR 3 (Analizador per-ticket + ContextoTicketBuilder + tool schema, riesgo alto, requiere validación en staging contra viaje cerrado) → PR 4 (hash en flags + UI del reporte).
  • El sidebar de Nova es un menú explícito (Nova::mainMenu() en NovaServiceProvider), por eso el resource nuevo no salía pese al auto-discovery. Agregué MenuItem::resource(PoliticaViaticos::class) a la sección Viáticos (el item respeta la policy: solo visible con gestionar_viaticos/superadmin). Historial NO va al menú (read-only, se llega por el HasMany). Commit 449a2cb → push → deploy (sin migración, optimize reconstruyó cache de rutas/config). Pint+lint OK.

2026-05-29 — #413 reatribuido: NO era de amadeus (es de monitoreo)

  • Investigación en prod (read-only): la VM amadeus es 192.168.20.20, cookie electrosystems_session, SESSION_DOMAIN=null. La IP 192.168.20.17 del reporte emite es_monitoreo_session → es la app monitoreo (es-antenas-new), no amadeus. El 419 sale de esa app (cookies secure sobre HTTP plano por IP).
  • Resultado: el bug se sacó de amadeus y se recreó en es-antenas-new como #415 (no #413 porque ese ID ya lo tenía vpn-clientes por colisión de sesión paralela). Ver es-antenas-new.md §🐛 Bugs.

2026-05-29 — #051 cerrado

  • Resuelto 2026-05-29 [#051]: Smoke notif viaje creado — Sergio marcó como terminada la validación de push + email al crear un viaje real en prod.

2026-05-29 — capturado #413 (bug login 419 por IP local)

  • Pidió Sergio: anotar que al entrar a la app por la IP local 192.168.20.17 sale error 419 al hacer login.
  • Hice: lo registré como bug #413 (nueva subsección ”🐛 Bugs”), 📅 hoy. 419 = CSRF/page-expired; hipótesis = SESSION_DOMAIN/APP_URL atados al dominio público y la cookie de sesión no aplica bajo la IP. Pendiente de diagnóstico/fix.
  • Falta: revisar config de sesión/cookie en prod y reproducir.

2026-05-29 — #239 verificado HECHO (no requería trabajo)

  • Pidió Sergio: revisar si #239 (validación inteligente de fotos en consumos) ya está hecho.
  • Hice: investigación read-only en ~/code/amadeus (clone al día con origin/main) vía subagente. Veredicto: HECHO — las 3 fases del scope ya están en main, y rebasado. El doc amadeus-validacion-fotos.md quedó desactualizado (seguía en ready-to-build); el feature completo se construyó entre el 25-may y el milestone de consumos.
    • Modelo + tabla: app/Models/ConsumoAnalisis.php + migración base 2026_05_25_211027_create_consumos_analisis_table + 5 incrementales. Columnas completas (estado, razón, es_ticket_valido, coincide_monto, monto/concepto detectado, texto_extraido OCR, comercio/ciudad/folio, confianza, override admin, apelación, auditoría de costo tokens/usd, intentos_fallidos).
    • Job async: app/Jobs/AnalizarFotoConsumoJob.php (tries=3, backoff, timeout). Dispatchado en store + re-subida vía dispatchAnalisisIA() detrás del doble flag analisis.enabled + analisis.hook_consumos_enabled.
    • Servicio Antigravity vision: app/Services/Vision/AnalizadorTicketService.php/v1/messages con imagen base64 + tool use dictaminar_ticket (tool_choice forzado), JSON estructurado, cálculo de costo, cap diario, retry. Modelo default claude-sonnet-4-6.
    • Fase 1 (foto inválida): rechaza si es_ticket_valido=false. Fase 2 (OCR + monto): coincideMonto con tolerancia 10%, rechaza si no coincide. “Fase 3” (cobertura por categoría): implementada como heurística IA libre (parece_viatico_legitimo), NO la tabla de palabras clave del plan original — fue decisión D2-B de Sergio (25-may) eliminar la tabla a favor de heurística. Cubierto funcionalmente con otro mecanismo.
    • Extras más allá del plan: auditoría forense de viajes (#247), dashboard admin de análisis, batch histórico, UI completa (PanelAnalisisIA, badges), flujo override+apelación+auto-anclaje (#377), notificaciones, 8 tests Feature.
  • Estado real: todo el código está en main pero apagado por flags en prod (ANALISIS_ENABLED/ANALISIS_HOOK_CONSUMOS_ENABLED default false). Prenderlo = #249 (lunes 2026-06-01). #239 se marca cerrado.
  • Falta: nada de código. Actualizado el design doc amadeus-validacion-fotos.md a done.

2026-05-28 — #377 implementado: tope de 3 re-subidas → ancla a revisión humana (cierra Bloque A)

  • Pidió Sergio: seguir con #377 tras deployar #376. Tras 3 re-análisis IA fallidos, el consumo se ancla a revisión humana (ya no resetea) con mensaje al técnico. Decisión D2 del milestone (tope=3).
  • Diseño: intentos (ya existente) cuenta ejecuciones del job incluyendo retries transitorios → inservible como contador de re-subidas. Agregué intentos_fallidos que sube SOLO cuando la IA emite observación (rama de rechazo del Job) y NUNCA se resetea al re-subir → acumula re-subidas fallidas. Tope configurable analisis.max_intentos_revision (default 3). Al llegar al tope, auto-ancla reusando el mecanismo de #375: set apelado_at + motivo automático → entra a la cola admin de #376. Notificación técnico+admin reusa ConsumoRechazadoPorIA (su listener ya copia al admin) — sin nuevo stack de notificación.
  • Hice (rama feature/377-tope-reintentos, NO commiteado aún):
    • Migración 2026_05_28_200000: columna intentos_fallidos (unsignedTinyInteger, default 0) en consumos_analisis. Config max_intentos_revision.
    • Job AnalizarFotoConsumoJob: en la rama de rechazo incrementa intentos_fallidos; al alcanzar el tope (y si no estaba ya apelado) setea apelado_at + motivo “Límite de N intentos alcanzado”. Respeta una apelación previa del técnico (no la pisa).
    • Policy ConsumoPolicy::update: bloquea re-subir cuando intentos_fallidos >= max (anclado). El link de editar desaparece del dashboard, consistente con rechazado/autorizado.
    • Frontend: flag anclado en camposConsumo; badge “En revisión humana” (azul) vs “En revisión” (ámbar) en ListaViajesItemConsumos.vue; tipo intentos_fallidos/anclado en Consumo.ts.
    • Tests: TopeReintentosConsumoTest con 5 casos (incrementa contador / al tope ancla auto-apelando / no re-ancla si ya apeló / puede re-subir bajo el tope / no puede re-subir anclado). 52 verdes en el filtro del módulo. Pint PASS, build OK.
  • DEPLOYADO 2026-05-28: commit 83b1964 en main + deploy.sh corrió la migración en prod. Verificado: Schema::hasColumn(consumos_analisis, intentos_fallidos) OK, config(analisis.max_intentos_revision)=3. Con esto cierra el Bloque A completo (apelación end-to-end #374→#377). Queda #249 (prender ANALISIS_HOOK_CONSUMOS_ENABLED=true, solo Sergio) para que el flujo empiece a recibir consumos reales.

2026-05-28 — #376 implementado: cola de revisión/apelaciones del admin (Bloque A)

  • Pidió Sergio: seguir con #376 tras deployar #375. Vista admin “Cola de revisión/apelaciones” — aprobar/rechazar-definitivo con motivo (reusar override_admin_*) + notificar al técnico el resultado.
  • Diseño: evolucioné /autorizaciones (que ya recibe las apelaciones desde #374/#375) en vez de crear página nueva — evita duplicar lógica. (a) Las apelaciones (apelado_at) suben al tope de la cola con badge “⚖️ Apelado” + se muestra el motivo del técnico; (b) rechazo-con-motivo (antes rechazar no capturaba nada): reusa override_admin_id/motivo/at como rastro de auditoría — la dirección la da estatus (autorizado vs rechazado); (c) al resolver (aprobar o rechazar) se notifica al técnico por push+email.
  • Hice (rama feature/376-cola-revision, NO commiteado aún):
    • Backend: ConsumosController::rechazar ahora acepta override_motivo opcional, setea estatus=rechazado + override_admin_* (si hay análisis) y dispara ConsumoResueltoPorAdmin('rechazado'); autorizar dispara ConsumoResueltoPorAdmin('autorizado'). Evento + listener EnviarNotificacionConsumoResuelto (auto-discovery, ShouldQueue) → push + email al técnico (ConsumoResueltoMail + blade). Canales aislados.
    • Frontend: Autorizaciones.vue → título “Cola de revisión y apelaciones”, contador de apelaciones, orden apeladas-primero, badge “⚖️ Apelado”, fila resaltada, modal dual aprobar/rechazar con motivo (muestra razón IA + motivo de apelación). AnalisisConsumos.vue → indicador inline ahora distingue ”✓ Aprobado por admin” / ”✕ Rechazado por admin” / “⚖️ Apelado” (antes decía siempre “Override admin”, que con rechazo-con-override quedaría mal etiquetado).
    • Tests: RevisionAdminConsumoTest con 6 casos (aprueba registra override+notifica / rechaza-con-motivo registra override+notifica / rechazar saca de la cola / rechazar pendiente sin análisis no notifica / técnico recibe email / la cola incluye apelaciones con motivo). 35 verdes en el filtro del módulo. Pint PASS, build OK.
  • Nota: los 35 rojos de inventario del suite completo siguen siendo preexistentes (verificado en main). No los toqué.
  • DEPLOYADO 2026-05-28 (Sergio autorizó commit+merge+deploy): commit 8015dd5 → main → deploy.sh (“Nothing to migrate”, solo código + queue:restart). Verificado: prod en 8015dd5, clases evento/listener cargadas. 35 rojos de inventario preexistentes (ajenos).
  • Sigue: #377 (tope de 3 re-subidas → ancla a revisión humana) cierra el Bloque A.

2026-05-28 — capturados #387 (checador geolocalizado) y #388 (ingesta facturas CFDI) para post-milestone

  • Pidió Sergio: apuntar 2 pendientes para después del milestone en curso: (1) reloj checador para que los técnicos chequen en sitio cuando están de viaje, guardando coordenada y cotejándola con los sitios para validar presencia física — pidió ideas de cómo implementarlo; (2) leer los correos de la bandeja de contabilidad (donde llegan las facturas de los consumos de viáticos) para ligarlas automáticamente a los consumos de los técnicos y quitarle a la contadora el cotejo manual al validar/imprimir.
  • Hice: los capturé en la nueva sección Post-milestone — features nuevas con disparo cond:post-milestone (#387 ⏱3-4d, #388 ⏱4-6d). Escribí las ideas de implementación en Notas técnicas:
    • #387 checador: geofence por radio haversine contra el GPS del sitio (radio configurable), match al sitio más cercano del viaje, validación suave (bandera, no bloqueo — alineado a #379), anti-spoofing ligero (accuracy, mock location, timestamp dispositivo vs servidor), tabla checadas, reusar captura GPS de #381. Depende de coords de sitios (#378). Distinto a jm-checador (oficina/descansos), aquí es geofence en campo.
    • #388 facturas: parsear el XML del CFDI (no OCR del PDF) — trae UUID/total/RFC/fecha; ingesta vía IMAP polling (webklex/laravel-imap) o Gmail/Graph API según proveedor; tabla facturas con dedup por UUID; match a consumo por monto+fecha+RFC dejado como sugerencia que la contadora confirma (no auto-ligado ciego); bonus validación SAT del UUID.
  • Falta: no arrancan hasta cerrar el milestone de consumos. Cuando se retomen, ambas conviene hacerlas después de #378/#381 (coords de sitios + captura GPS ya resueltas).

2026-05-28 — #375 implementado: botón “Apelar” del técnico → cola de revisión humana (Bloque A)

  • Pidió Sergio: arrancar #375 tras el deploy de #374. Botón para que el técnico escale una observación de la IA a revisión humana sin re-subir foto (cuando cree que la IA se equivocó o no puede mejorar la foto), con mensaje “no es definitivo”, y notifica al admin.
  • Diseño: la apelación NO cambia consumo.estatus (sigue observado) ni el veredicto interno; solo registra la apelación en consumos_analisis (apelado_at + apelacion_motivo). El consumo ya aparece en /autorizaciones desde #374; la marca de apelación lo distingue/prioriza ahí (cola completa = #376). Re-subir foto (re-análisis) limpia la apelación previa.
  • Hice (rama feature/375-boton-apelar, NO commiteado aún):
    • Migración 2026_05_28_190000: 2 columnas nullable en consumos_analisis (apelado_at timestamp, apelacion_motivo text). Reversible.
    • Backend: ConsumoPolicy::apelar (mismo dueño/permiso/viaje-no-entregado que update, + exige NO ya-apelado y NO overrideado); ruta POST /viajes/{viaje}/consumos/{consumo}/apelar; ConsumosController::apelar (valida motivo opcional ≤1000, registra apelación, dispara evento, redirige con flash “en revisión, no es definitivo”); reset de apelado_at/apelacion_motivo en el re-análisis.
    • Notificación al admin: evento ConsumoApelado + listener EnviarNotificacionConsumoApelado (auto-discovery L11, ShouldQueue) → email a analisis.admin_email (ConsumoApeladoMail + blade) + push best-effort a admins (superadmin/gestionar_viaticos). Canales aislados.
    • Frontend: botón “Apelar a revisión humana” + textarea de motivo opcional en Editar.vue (gated client-side, server es la fuente de verdad); indicador ”⏳ En revisión humana — apelado” en PanelAnalisisIA.vue (visible para técnico y admin); campos en Consumo.ts.
    • Tests: ApelacionConsumoTest con 8 casos (apela OK / motivo opcional / no doble apelación / no si override / no si no-observado / no si ajeno / notifica admin por email / vista expone campos). 41 verdes en el filtro del módulo consumos. Pint + build OK.
  • Nota — fallos preexistentes: el suite completo de amadeus tiene 35 tests rojos en inventario/compras (MovimientoInventario*, ViajeCompra, LiderChecklist*, DashboardInventario) con ModelNotFoundException — verifiqué que fallan igual en main limpio (fixture/seeder faltante), son previos y ajenos a consumos. No los toqué.
  • DEPLOYADO 2026-05-28 (Sergio autorizó commit+merge+deploy): commit 85e0d16 → main → deploy.sh (migración + queue:restart). Verificación read-only en prod: columnas apelado_at/apelacion_motivo OK, ruta consumos.apelar registrada, 0 apeladas (nadie ha usado el botón aún — hook #249 sigue OFF). 35 rojos de inventario confirmados preexistentes en main (ajenos).
  • Sigue: #376 (cola admin dedicada de apelaciones) — hoy las apelaciones ya caen en /autorizaciones con el indicador “En revisión humana”; #376 le da vista/priorización propia.

2026-05-28 — #374 implementado: IA escribe observado, no rechazado (Bloque A base)

  • Pidió Sergio: seguir con #374 — base arquitectónica de la apelación. La IA nunca debe rechazar en definitivo; emite observado (visible y apelable). rechazado/autorizado quedan a decisión humana.
  • Diseño (clave): hay dos campos de estadoconsumos.estatus (lo que ve el técnico; lo filtra NoRechazadosScope) y consumos_analisis.estado (veredicto interno de la IA: ok|rechazado|error). Decisión: solo cambia consumo.estatus de rechazadoobservado; analisis.estado se queda en rechazado (es el veredicto interno, lo usan dashboard/KPIs/reportes/policy). Así el blast radius es mínimo y dashboard/reportes siguen intactos.
  • Hice (rama feature/ia-observado-no-rechazado, NO commiteado aún):
    • Migración schema 2026_05_28_180000: ALTER del enum consumos.estatuspendiente|observado|rechazado|autorizado (guardado con DB::getDriverName()==='mysql', patrón del repo). down() reversible (observado→null antes de angostar).
    • Migración datos 2026_05_28_180100: decisión de Sergio — los 43 consumos con estatus='rechazado' puestos por la IA en el batch forense 191/192 (sin override humano) se devuelven a estatus=null (“dejar esos viáticos como antes de la IA”). Preserva los consumos_analisis (artefacto del reporte). Solo toca rechazos de IA sin override; respeta eventuales rechazos humanos. down() no-op por diseño.
    • AnalizarFotoConsumoJob: $consumo->estatus = 'observado' (antes 'rechazado').
    • NoRechazadosScope: sigue ocultando solo rechazado (humano-terminal); observado queda visible. Bug latente arreglado: envolví el where()->orWhereNull() en closure (sin agrupar se fugaba de otros where, p. ej. viaje_id).
    • ConsumosController: re-subida resetea desde observado; query de /autorizaciones busca observado.
    • ConsumoPolicy::update: apelación (re-subir) habilitada cuando estatus='observado'.
    • Copy user-facing (espíritu del cambio, bajar fricción): badge “En revisión” para observado (ListaViajesItemConsumos.vue); label “Observado por IA” + “no es un rechazo definitivo” (PanelAnalisisIA.vue); push “Consumo marcado para revisión” (type consumo_observado_ia); mail subject/body “marcado para revisión / no es definitivo”; tipo Consumo.ts.
    • Tests: actualizados los que asumían consumo.estatus='rechazado' del flujo IA → observado (Job, E2E, integración, UI, notificación). Las aserciones sobre analisis.estado='rechazado' se mantienen (veredicto interno). 83/83 verdes (Job + E2E + integración + UI + notif + dashboard + forense + comandos). pint --dirty OK. npm run build OK (sin errores TS).
  • Decisión NO tomada (fuera de scope #374, anotada): el auditor forense AuditorViajeService::marcarPendientes (#247, herramienta manual) sigue saltando solo estatus='rechazado'; podría querer saltar también observado. No lo toqué — es otro path. Evaluar al retomar #247.
  • DEPLOYADO 2026-05-28 (Sergio autorizó commit+push+merge+deploy): commit 9ce3b6b, fast-forward a main, push a origin, deploy.sh en prod corrió las 2 migraciones + queue:restart. Verificación read-only en prod: enum ahora (pendiente,observado,rechazado,autorizado); estatus='rechazado' total = 0; la migración de datos reseteó 59 IA-rechazados (todos sin override humano — la estimación previa de 43 se quedó corta) a estatus=null, preservando sus consumos_analisis. estatus='observado' = 0 (correcto: hook #249 sigue OFF, no hay corridas nuevas).
  • Falta: (1) smoke visual del badge “En revisión” + panel + mail cuando entre el primer observado real (depende de prender #249); (2) #375 (botón Apelar) se construye encima de esto.

2026-05-28 — #371 cerrado: worker de cola YA existe en prod (premise desactualizado)

  • Pidió Sergio: arrancar el milestone con #371 (montar worker systemd queue:work --queue=analizar-foto).
  • Hallazgo (investigación read-only en repo + VM prod): el premise de #371 estaba desactualizado. Prod ya tiene worker de cola robusto:
    • 8 procesos queue:work gestionados por supervisor (/etc/supervisor/conf.d/laravel-worker.conf: numprocs=8, autostart=true, autorestart=true, user www-data, --sleep=3 --tries=3 --timeout=3600). Up desde 26-may, estables (~1d22h, sin crash-loop). deploy.sh ya hace php artisan queue:restart.
    • El comando no lleva --queue → procesan la cola default.
    • En el código, el hook ConsumosController::dispatchAnalisisIA() (línea 264, gated por analisis.enabled && analisis.hook_consumos_enabled) hace AnalizarFotoConsumoJob::dispatch($consumo->id) sin onQueue → el job cae en default. El Job tampoco define $queue.
    • El literal analizar-foto NO existe en el código. Vino de cómo se corrió el batch manual (analisis:batch-historico --cola=analizar-foto), una cola que ningún worker escucha — por eso esos jobs “esperaron en vano” en la sesión del 27-may. El hook automático, en cambio, usa default.
    • Prod: QUEUE_CONNECTION=database, ANALISIS_ENABLED=true, ANALISIS_HOOK_CONSUMOS_ENABLED=false (=#249). Tabla jobs vacía (nada atorado), failed_jobs=2 (históricos VAPID 2025-11, descartables).
    • worker.log confirma que los workers procesan default con éxito — incluyendo EnviarNotificacionConsumoRechazado (listener del flujo de visión) y jobs de Telescope, todos DONE.
  • Decisión de Sergio (entre 3 opciones que presenté): cerrar #371 — ya satisfecho por la infra supervisor existente. NO construir cola dedicada ni cambiar comando. (Se evaluó y descartó por ahora aislar los jobs de visión —lentos, ~60s, llaman a Antigravity— en su propia cola analizar-foto para no competir con push/email; queda como mejora futura si las notificaciones se ven retrasadas por ráfagas de fotos.)
  • Resultado: #371 cerrado. #249 desbloqueado — prender ANALISIS_HOOK_CONSUMOS_ENABLED=true en /var/www/amadeus/.env haría funcionar la validación automática al subir, con los 8 workers actuales. Solo Sergio prende ese flag (no lo toqué).
  • Falta: prender #249 cuando Sergio dé luz verde (un flag + php artisan config:cache); luego arrancar Bloque A (#374).

2026-05-28 — análisis del milestone “Consumos bien capturados + IA por lineamientos”

  • Pidió Sergio: modificar la auditoría automática de viáticos — (1) rechazos de IA apelables / no definitivos aunque re-suban foto y siga inválida; (2) IA valida ubicación de los consumos vs ubicación del viaje, incluyendo carretera rumbo al sitio desde oficina o hospedaje; (3) pantalla con mapa de ubicaciones de consumos + detalle al hover; (4) subir consumos offline y sincronizar al recuperar señal, preservando ubicación original + notificar si queda inválido. Todo conforme a lineamientos editables. Analizar estado, fricciones, pushback y separar en pendientes chicos.
  • Hice: exploración a fondo del módulo consumos (subagente Explore) + lectura de los design docs (validación-fotos, políticas). Hallazgo central: el sistema de auditoría IA ya está en prod (tablas consumos_analisis/auditorias_viaje, jobs, servicios Antigravity, dashboard, override admin, notificaciones, GPS dual). Verifiqué en código 3 fricciones clave: NoRechazadosScope oculta los rechazados (incompatible con “apelable”), sitios con GPS nullable, y no existen coords de oficina ni captura de hospedaje. Creé el design doc amadeus-consumos-milestone.md — 8 fricciones, 6 puntos de pushback, breakdown en #374-382 (9 nuevos) + #369/#371/#249 existentes, y 6 decisiones pendientes.
  • Pushback principal: que la IA nunca escriba rechazado terminal (usar observado; humano decide) para limpiar el NoRechazadosScope de raíz; ubicación solo como bandera nunca auto-rechazo; offline como fase final (sugerir “borrador local” en vez de Background Sync real); el #369 de políticas ES la pieza de “lineamientos modificables”, no duplicar.
  • Falta: respuestas a las 6 decisiones (semántica rechazo, tope re-subidas, ubicación hospedaje, offline real vs borrador, D1-D5 políticas, orden/fechas). Luego arrancar Bloque 0 (#371 worker + #369 políticas).

2026-05-27 — sesión multi-proyecto: auditoría IA 191/192 (4 pares nuevos) + design doc políticas

Pidió Sergio (sesión multi-proyecto en paralelo con 5+ proyectos):

  1. Inyectar texto-política de Gustavo en la IA de validación.
  2. Generar reporte de auditoría para todos los usuarios de viajes 191 + 192.

Sub-tarea A — Diseño políticas (architect subagente):

  • Design doc completo en amadeus-politicas-viaticos.md. Tabla politicas_viaticos + historial (single-row + observer) + politica_hash en auditorias_viaje + ContextoTicketBuilder + inyección en AnalizadorTicketService (con BD context — viaje + consumos previos + totales del día) + Resumidor + 4 PRs incrementales + tests + estimación 9.5-14.5h.
  • 5 decisiones pendientes (en sección final del doc) antes de codear: single-row vs rangos, JSON estructurado, cap USD, prompt caching beta, bloqueo vs flag por violación.
  • Sergio ya confirmó alto-nivel: ambos prompts + tabla BD versionada + correr IA en los 4 pares.

Sub-tarea B — Reporte 191/192 con IA en 5 pares (general-purpose + continuación):

  • 80/80 consumos con análisis IA persistido en consumos_analisis. Costo real: $1.49 USD ≈ $26 MXN (vs budget $0.60 — 2.5× más; trivial absoluto pero anotado).
  • Conteo por par:
    • v191/u9 Manuel: 31 (ya tenía IA del 2026-05-26).
    • v191/u21 Onan: 17 (1 pendiente c=5442 resuelto al re-disparar).
    • v192/u5 Adrián: 25.
    • v192/u10 Juan Luis: 20 — 5 OCR mismatches detectados (declarado vs ticket entre 18% y 62% arriba), hallazgo NUEVO no visible en versión EXIF-only.
    • v192/u22 Fernando: 18 — 62% del monto rechazado.
  • Hallazgos cross-par: 3 vouchers BBVA “Servicio y Comunicaciones” (Manuel/Onan/Fernando) identificados como TPV sin desglose, no factura de hospedaje. 5 comisiones ATM Banorte $34.80 detectadas con OCR del monto retirado real.
  • Hallazgo de infra: queue analizar-foto sin worker systemd en prod (capturado como #371). Por eso el agente original “esperó fotos” en vano — los jobs encolados nunca se procesaron. Fix: comando síncrono analisis:auditar-viaje ejecutado en su lugar.

Output: ~/code/amadeus/storage/auditorias/viaje-191-192-todos-usuarios.md (280 líneas, 20 KB).

Falta:

  • Smoke visual Sergio: 11 fotos OCR’d como ciudad “Monterrey” (Adrián × 5, Onan × 4, Fernando × 2) — ¿alucinación IA o reales? Capturado como #370.
  • Decisiones D1-D5 del design doc para arrancar PR 1 del módulo políticas.

2026-05-26 tarde — rediseño UI de Auditoría de Viaje deployado

  • Pidió Sergio: revisar cambios que aplicó con otra IA en ~/code/amadeus y commit + push + deploy.
  • Cambios revisados: resources/js/Pages/Admin/AuditoriaViaje.vue reescrito (701 ins / 288 del). Lógica intacta — mismos handlers generar()/abrirFoto()/rowClase()/banderasConsumo(), misma interfaz ConsumoFlag / FlagsAgregados, mismas props, mismo modal. Lo nuevo es la presentación: KPI cards arriba (consumos, ratio diario, fuera de horario, alertas críticas), grid 2-col de hallazgos (ráfagas, ciudades, comercios, duplicados por monto, duplicados por folio con marco rojo), tabla compacta en desktop + cards apiladas con border-l de color (rosa/ámbar/esmeralda según severidad) en móvil, modal con backdrop-blur-sm y barra superior con consumo_id + abrir-original. Dark mode pareado en todo. Eliminadas dependencias Table/TableTh/TableTd.
  • Fix sobre lo que vino: la otra IA usó 17 clases con pasos de color inexistentes en Tailwind 3 (slate-250/350/450/650/850, red-750, amber-850, rose-450) que no renderizan y degradan al color heredado. Mapeadas al paso válido más cercano. truncate-3-linesline-clamp-3. animate-spin-slow se dejó (no-op pero inofensivo; cambiarlo a animate-spin haría girar molesto el ícono “Re-generar”).
  • Flujo de deploy: npm run build (sail) regeneró todos los assets de public/build/. Branch feature/rediseno-auditoria-viaje push → merge --no-ff a main (e720e40) → push main → ssh amadeus ./deploy.sh ejecutado (composer install no cambios, optimize, sin migrate, queue:restart). HTTP 302 0.21s post-deploy.
  • Falta: smoke funcional con un viaje real auditado en prod (puede ser el mismo viaje 191) para confirmar que la tabla desktop, las cards móvil, los modals de foto y todos los enlaces a Maps abren bien en ambos modos.

2026-05-26 — reporte de auditoría del viaje 191 (user 9) para contabilidad

  • Pidió Sergio: reporte escrito de la primera (y única) auditoría existente — viaje 191 / user 9 / Manuel Pérez Morales / folio BJZ20260519 / 19-22 may / Benito Juárez — con foco en: cuántas fotos son inválidas, cuántas fechas no caen en el viaje (criterio ±1 día: si la fecha del ticket cae dentro del viaje se da por buena), y horarios de compra de víveres (sospecha de jornada que empieza tarde).
  • Hice: generé viaje-191.md (movido a deliverables/ el 2026-05-28) leyendo auditorias_viaje.id=1 directo de la BD de prod. Datos finales tras dos rondas de correcciones del propio Sergio:
    • 14 fotos inválidas (45.2 %, $3,039.60) — todas imágenes completamente negras. Incluye un Hospedaje por $850.
    • 5 fechas no coincidentes (16.1 %, $983.00) — todos los tickets con fecha entre 7-may y 11-may (una a dos semanas antes del viaje). Se corrigió #5465 a 2026-05-07 (lectura invertida) y se removió #5475 que era 2026-05-20 (dentro del viaje).
    • Horario: promedio de víveres 11:02 a.m., mediana 11:25; 50 % de víveres entre 9:45 y 9:58; cero tickets antes de las 9:00 a.m. en todo el viaje.
    • GPS: las 14 fotos negras se subieron con coordenadas casi idénticas en Cd. Juárez (31.6139, -106.3551, variación < 5 m) cuando el sitio asignado está ≈130 km al sur en Villa Ahumada. Punto fuerte del reporte.
  • Descartados explícitamente en el reporte (Sergio los reclasificó):
    • Ráfaga de 24 registros en 31 min el 21-may: práctica común que se atacará con otro candado en futuro; no se incluye como problema “por la ráfaga”, solo se aprovecha el dato de GPS de esas capturas.
    • Comisiones bancarias por retiro (#5489, #5490 — $34.80 ×2): la empresa las cubre hoy (posible cambio futuro).
    • Fecha de captura del 25-may (#5502, #5503, #5504): el técnico ya estaba en oficina; capturar tarde es normal. Lo que sí cuenta es que esos 3 son fotos negras.
    • Tickets con “Monterrey” como ciudad inferida (11): es dirección fiscal del comercio, Sergio ya los revisó y son válidos.
  • Falta: nada del reporte; queda como entregable a contabilidad. Para futuro, este caso ilustra exactamente el patrón que motiva #239 (validación inteligente de fotos en consumos) — vale la pena referenciarlo cuando se aborden las 5 decisiones D1-D5 de ese pendiente.

2026-05-25 noche — smoke E2E #175 completado + 2 bugs del bot fixeados + feature retry repropose

  • Pidió Sergio: “Vamos a seguir con #175” — smoke E2E desde Telegram de los endpoints /api/viajes + /api/sitios consumidos por el bot electro-ia (Fase 3.0).
  • Pre-flight check (todo verde):
    • Bot electro-ia activo en laptop-ia desde 2026-05-22 17:39.
    • viaticos.electrosystemsnet.com/api/{whoami,viajes,sitios} → 401 sin token con Accept: application/json (con browser default cae a 302 a login porque entra al middleware web; el bot usa Accept JSON, OK).
    • INVENTARIO_VIAJE_LINK no está en el .env → flag false → bot pide material + herramientas (no productos del catálogo). Confirma el piloto.
    • Baseline MAX(viajes.id) = 192.
  • Sub-flujo (c) sitio inexistente — viaje 193 a Dallas: Sergio mandó mensaje completo de un viaje al sitio “Dallas” (no existía). El bot detectó el missing site, ofreció crearlo, doble confirmación (sitio → viaje), creó:
    • Sitio 51 codigo=DAL, nombre=Dallas.
    • Viaje 193 folio=DAL20260525, fechas 25-28 mayo, tipo_viaje_id=2, urgencia=2, creado_por_id=2.
    • Orden 191 (sitio 51): descripcion="Descripcion de prueba, es solo una prueba de viaje por bot", requiere_formatos_firmados=true.
    • Equipo 190: lider_id=2, vehiculo_id=9 (U09 Nissan Frontier), material="switch Netonix 6 puertos, laptop", herramientas="N/A", integrante Sergio.
    • Evento ViajeCreado disparado — paridad total con flujo web: push 4/4 OK, mail 4/4 OK a los 4 usuarios con gestionar_viajes=true (Sergio/Gustavo/Vitela/Ortega). Logs en storage/logs/laravel.log: Push viaje enviado {"viaje_id":193,...,"results":{"total":4,"successful":4,"failed":0}} + Email viaje enviado {"viaje_id":193,...,"enviados":4,"fallidos":0}.
  • (b) mensaje parcial — viaje 194 a Cebollín: Sergio empezó con “Necesito crear un viaje a Cd Juárez”, el LLM preguntó campos faltantes, durante la conversación Sergio cambió a Cebollín, completó iterativamente. Resultado:
    • Viaje 194 folio=CEB20260526, tipo_viaje_id=1, urgencia=1, 26-27 mayo, creado_por_id=2.
    • Orden 192 (sitio Cebollín): descripción “Se esta creando otro viaje de prueba por bot.”.
    • Equipo 191: lider_id=3 (Vitela), vehiculo_id=6 (U06), material/herramientas N/A, integrantes Sergio + Vitela. Validó que el bot soporta separación creador/líder.
    • ViajeCreado disparado: push 4/4 + mail 4/4 OK.
  • (a) mensaje completo a sitio existente: implícitamente validado en (c) — el bot solo pidió confirmación, sin preguntas redundantes cuando recibió payload completo.
  • (d) edge cases: TTL del pending viaje (90s) probado en vivo con mensaje a Matriz. Sergio dejó expirar, dijo “sí” tarde, el bot inicialmente re-propuso silencio (bug 2, ver abajo). Pending #12 en BD quedó status='expired', MAX(viajes.id)=194 (NO se creó viaje fantasma). Sub-casos “sin vincular” y “sin permiso” quedan para tests unitarios — no se probaron en vivo para no involucrar a otros operadores.
  • 🐛 Bug 1 del bot — lookupByTelegramUserId no cargaba amadeus_usuario_id:
    • Síntoma: al intentar crear el sitio Dallas, el bot respondió “Tu usuario del bot no está vinculado a un usuario de amadeus. Pídele a un admin que ejecute UPDATE identity SET amadeus_usuario_id=…”, aunque en BD identity.amadeus_usuario_id para sergio era 2 y para gustavo_chavira_mx era 12 (vinculados desde 2026-05-22).
    • Causa raíz: la función lookupByTelegramUserId en src/identity.js:8-11 cargaba la identidad con un SELECT que no incluía la columna nueva: SELECT id, display_name, role, language_pref, telegram_user_id FROM identity WHERE telegram_user_id=$1 AND active=true. Por eso ctx.identity.amadeus_usuario_id llegaba undefined a las tools crear_sitio (línea 41) y crear_viaje (línea 82), que verifican explícitamente esa propiedad antes del dry-run.
    • Fix (1 palabra): añadir amadeus_usuario_id al SELECT. Aplicado vía sed -i en laptop-ia. Bot reiniciado, sub-flujo (c) inmediatamente exitoso.
    • Patrón reusable: al añadir columna a tabla cuya lectura va a más de un sitio, grep por SELECT.*FROM <tabla> antes de declarar la columna disponible. Aplica a cualquier proyecto.
  • 🐛 Bug 2 del bot — re-propose silencioso al timeout (UX):
    • Síntoma: Sergio mandó mensaje completo a Matriz, el bot creó pending #12 (TTL 90s), esperó >90s, dijo “sí”. El bot, en vez de avisar “expiró”, volvió a proponer el viaje en silencio creando un pending nuevo. Confuso porque pierde la señal de que el pending original murió.
    • Causa: el handler CONFIRM_PATTERN en src/index.js:404 llamaba resolveLastPending({status:'confirmed'}) — esa función filtra por expires_at > now() así que devolvió null. El control caía al LLM, que con todo el contexto en memoria conversacional reinterpretaba el “sí” como nueva intención y armaba otro dry-run.
    • Decisión de Sergio: prefiere comportamiento explícito con confirmación previa. Pidió re-propose con confirmación en 2 pasos: el primer “sí” tardío dispara “el pending expiró, ¿quieres reintentarlo?”; el segundo “sí” reinvoca la tool con el mismo input.
    • Fix determinista en código (no se tocó el prompts/system.md):
      • Migration Postgres: ALTER TABLE pending_action ADD COLUMN retry_state TEXT NULL (valores NULL/offered/consumed/rejected). Sin CHECK adicional para mantener simple; se validó en código. La columna status mantiene su status_check original (pending|confirmed|denied|expired).
      • 3 helpers en src/db.js:
        • offerRetryForRecentExpired({identity_id, max_age_seconds=300}) — busca pending con status='pending' AND retry_state IS NULL AND expires_at <= now() AND expires_at > now() - 300s, lo marca status='expired', retry_state='offered', resolved_at=now() en una transacción y devuelve la fila.
        • findOfferedRetry({identity_id, max_age_seconds=120}) — devuelve pending con retry_state='offered' reciente (no muta).
        • consumeOfferedRetry({id, state}) — marca retry_state='consumed' o 'rejected'.
      • Handler CONFIRM_PATTERN ampliado con 2 fallbacks tras resolveLastPending null:
        1. Si hay findOfferedRetry → marca consumed y re-ejecuta el proposer vía runTool({name, input, ctx: { identity, message_id }}) → nuevo dry-run + nuevo pending (TTL fresco 90s para viaje, 60s para sitio) + manda instruction_for_user del resultado al chat.
        2. Si hay offerRetryForRecentExpired → manda al chat: “El pending de crear_viaje expiró hace Xs. ¿Quieres que lo reintente con los mismos datos? Responde ‘sí’ para volver a proponerlo (TTL nuevo), o ‘no’ para cancelar.”
      • Handler DENY_PATTERN ampliado simétricamente: si hay offered retry, marca rejected y responde “OK, no reintento crear_viaje.”
    • Cobertura transversal: aplica a las 4 tools con pending (write_file/send_email/crear_sitio/crear_viaje) sin tocar cada una — el handler vive en index.js y opera sobre la tabla pending_action, agnóstica de tool.
    • Validación en vivo (rama DENY): Sergio mandó viaje completo a Matriz → pending #12 a las 18:07:41 → expiró 18:09:11 → Sergio dijo “sí” → bot ofreció reintento → Sergio dijo “no” → BD final id=12, status='expired', retry_state='rejected', resolved_at=18:10:20. MAX(viajes.id) sigue en 194 — TTL técnicamente intacto. Rama “sí” al retry (consume → re-propose → confirma) no probada en vivo; lógica simétrica a DENY que sí pasó.
  • Estado del repo electro-ia (laptop-ia, sin remote): changes uncommitted en src/db.js (+44 líneas), src/identity.js (+1 palabra), src/index.js (+47 líneas), policy/inventory.yaml (cambios ajenos a esta sesión, son las notas de connectable_dbs del 22-may que quedaron sin commitear). Backups en .bak.20260525. Sergio decide cuándo commitear localmente.
  • Decisión cerrada — sin vincular / sin permiso (sub-casos de d): cubrirlos con tests unitarios en próxima iteración del bot en vez de pedir colaboración a operadores. Lógica ya está en crear_viaje.js:82-89 y crear_sitio.js:41-48; con stubs de ctx.identity se cubre directo.
  • Falta: (1) commitear los 3 archivos del bot (db.js/identity.js/index.js) cuando Sergio decida; (2) tests unitarios cubriendo sin-vincular + sin-permiso + rama “sí” al retry; (3) cuando se prenda INVENTARIO_VIAJE_LINK=true en ~/code/amadeus/.env, repetir smoke completo para verificar que el bot deja de pedir material/herramientas y empieza a pedir productos del catálogo (sin redeploy del bot — el comportamiento lo decide amadeus en la respuesta de validación).

2026-05-08

  • Pidió Sergio: registrar el proyecto en el hub.
  • Hice: creé este archivo con stack y pendientes. Leí el ANTIGRAVITY.md del codebase — es boilerplate de Laravel Boost, no aporta contexto de negocio.
  • Falta: deploy de la nueva funcionalidad de inventarios ↔ órdenes de trabajo.

2026-05-08 — arranque de deploy

  • Pidió Sergio: arrancar con el deploy de inventarios.
  • Hice: moví la tarea principal a “En progreso” y dejé el checklist de deploy listo para usar.
  • Falta: ejecutar el checklist (backup, branch a desplegar, ventana, smoke test post-deploy).

2026-05-19 — diagnóstico de estado para retomar deploy

  • Pidió Sergio: avanzar con amadeus (11 días sin tocar).
  • Hice: revisé el repo local. Rama inventarios limpia, 39 commits ahead de origin/main, 246 archivos cambiados, 22 migraciones nuevas (2025-11-11 → 2026-04-20). No hay script de deploy ni Actions; el deploy es manual sobre la VM amadeus en Poseidon. Intenté SSH a la VM para inspeccionar estado actual pero el classifier lo bloqueó (requiere autorización explícita de Sergio para esta sesión).

2026-05-19 — análisis de regresión (¿rompe flujos existentes?)

  • Pidió Sergio: validar que subir inventarios no afecta viajes, viáticos, OTs.
  • Veredicto: RIESGO MEDIO. Deploy viable pero hay 3 cambios de comportamiento silenciosos que Sergio debe aprobar explícitamente antes de mergear. Ninguno es bloqueador técnico, son decisiones de producto.

Cambios de comportamiento que Sergio debe confirmar

  1. ViajePolicy::update restringe edición al creador del viaje. Hoy: cualquier usuario con gestionar_viajes edita cualquier viaje. Después del deploy: solo el creador (creado_por_id) puede editar; viajes legacy con creado_por_id null siguen abiertos a gestores. → ¿Es lo deseado?
  2. InventarioPolicy::delete ahora exige superadmin. Usuarios con permiso inventarios=true pierden capacidad de borrar inventarios. → ¿Aceptable?
  3. Producto::boot::created removido — al crear un producto ya NO se siembran automáticamente filas en inventarios por cada sitio. Si algún reporte asumía esa siembra, mostrará vacíos. → ¿Hay algo que dependa de esto?

Riesgos técnicos identificados

  1. Migración hybrid 2025_11_19_002335 tiene down() vacío — rollback no es reversible para esa migración. Backup completo antes es obligatorio.
  2. APP_LOCALE default cambia de en a es en config/app.php. Si el .env de prod no fuerza APP_LOCALE=en, mensajes Laravel pasarán a español (probablemente OK, confirmar).
  3. Ruta /dev/login-as registrada en prod — protegida por app()->environment('local') con abort 404. Segura si APP_ENV=production (verificar).
  4. ProductosController@index ahora retorna paginator en vez de Collection (15/pág). Vistas legacy/Nova que esperaban array pueden romperse.
  5. Migración add_matriz_to_sitios inserta sitio “Matriz” con código MTZ sin chequeo de colisión por código (solo busca por nombre). Si ya existe un sitio con código MTZ o nombre distinto, puede colisionar.
  6. Permisos nuevos inventarios_preparar/_entregar/_revisar se crean en false — requieren asignación manual post-deploy a los usuarios correspondientes.
  7. Flag INVENTARIO_VIAJE_LINK controla si al guardar un viaje se crea/sincroniza un MovimientoInventario borrador. Default false — verificar que NO esté en true en prod hasta que se quiera activar.

Tablas existentes tocadas por migraciones (todas reversibles excepto la nota 4)

  • productos: +marca, +modelo, +tiene_numero_serie, +deleted_at (additivo).
  • inventarios: +numero_serie, +equipo_id, +estatus_inventario_id, drop estatus enum, reintroduce cantidad. UPDATEs masivos en 11_14 y 11_19.
  • sitios: +matriz boolean, inserta “Matriz”.
  • equipos: material/herramientas → nullable, +lider_id.
  • viajes: +creado_por_id con backfill desde movimiento_inventarios.usuario_id.

Modelos/controllers existentes con cambios visibles

  • Viaje (additivo), Equipo (additivo), Producto (perdió hook boot::created).
  • ViajesController::guardarViaje (asigna creado_por_id, persiste lider_id; flag INVENTARIO_VIAJE_LINK).
  • ProductosController ahora paginado.
  • routes/web.php solo agrega; nada renombrado ni eliminado.

Acciones recomendadas antes del deploy

  • 📅 2026-06-08 — Confirmar con Sergio los 3 cambios de comportamiento (puntos 1, 2, 3).

  • 📅 2026-06-08 — Verificar en prod: APP_ENV=production, INVENTARIO_VIAJE_LINK no definido, APP_LOCALE (forzar en si se quiere mantener).

  • 📅 2026-06-08 — Confirmar que no exista sitio con código MTZ previo.

  • 📅 2026-06-08 — Backup completo de BD (en particular inventarios, productos, viajes, equipos, sitios, permisos) + snapshot de la VM en Poseidon.

  • 📅 2026-06-08 — Plan para asignar permisos nuevos inventarios_* a usuarios correspondientes post-migrate.

  • 📅 2026-06-08 — Correr en staging (si existe) o local los tests ViajesTest, ViajeCreadorEditTest, LiderChecklistAccessTest.

  • Falta: decisión de Sergio sobre los 3 cambios de comportamiento + ventana de deploy.

2026-05-19 — pre-deploy cleanup

  • Pidió Sergio:

    1. Edición de viajes: confirmado, solo el creador puede editar (sin cambio).
    2. Borrar inventarios: revertir a comportamiento previo (usuarios con permisos.inventarios pueden borrar).
    3. Producto::boot::created removido: validar que no afecte reportes.
    4. Arreglar las migraciones para rollback parcial.
    5. NO insertar sitio “Matriz” en la migración — ya existe uno en prod.
  • Hice (en ~/code/amadeus, rama inventarios, sin commit):

    a. Validación de Producto::boot::created (subagent Explore):

    • Conclusión: NINGÚN reporte/listado depende de la siembra automática.
    • InventariosController agrupa con groupBy('producto_id')->groupBy('sitio_id') solo sobre datos existentes; no asume matriz producto×sitio completa.
    • InventarioService::aplicarEntrada() (líneas 137-153) usa firstOrCreate() bajo demanda al procesar movimientos.
    • ProductoImporter, DatabaseSeeder, Nova Inventario.php y tests no asumen la siembra automática.
    • Conclusión: post-deploy, nuevos productos no tendrán rows en inventarios hasta que haya un movimiento — coherente con la arquitectura actual.

    b. app/Policies/InventarioPolicy.php línea 36: delete() regresa a $usuario->permisos->inventarios (era superadmin en la rama). restore() y forceDelete() siguen siendo superadmin (igual que en main).

    c. database/migrations/2025_11_19_002335_adjust_inventarios_for_hybrid_tracking.php: down() ahora implementado correctamente:

    • Cuenta filas con numero_serie IS NULL. Si hay >0, lanza RuntimeException con mensaje claro (no rollback silencioso ni destructivo).
    • Si no hay nulls: vuelve numero_serie a NOT NULL y dropea cantidad. Rollback parcial reversible bajo esa condición.

    d. database/migrations/2026_01_17_002211_add_matriz_to_sitios_table.php: eliminado el INSERT del sitio “Matriz”.

    • Sigue agregando la columna matriz boolean default false.
    • Marca como matriz un sitio existente con nombre case-insensitive “matriz”. Si no hay match exacto en prod, el flag queda false y debe asignarse manual con UPDATE sitios SET matriz=1 WHERE id=<id>.

    e. Validé sintaxis con php -l en los 3 archivos modificados → sin errores.

2026-05-19 — commit + merge + push

  • Pidió Sergio: “Dale al commit y merge/deploy”.
  • Hice:
    • Commit a10d7f6 en inventarios con los 3 fixes pre-deploy.
    • Push de inventarios al remote.
    • Merge inventariosmain con --no-ff (commit fb5e039). Auto-merge resolvió un conflicto trivial en app/Http/Controllers/ConsumosController.php.
    • Push de main al remote (c3c85d2..fb5e039).
  • Estado: repo en remote listo. origin/main ya tiene el módulo de inventarios + fixes.
  • Falta: ejecutar el deploy en la VM amadeus. Bloqueado por classifier (SSH a prod requiere autorización explícita de Sergio para esta sesión).

2026-05-19 — deploy ejecutado en prod (amadeus VM en Poseidon)

  • Pidió Sergio: “Procede tú con todo”. SSH autorizado permanente con usuario electrosystems, dump SQL únicamente (sin snapshot VM).
  • Inspección pre-deploy: working tree de prod estaba sucio con un hotfix de 4 meses (2026-01-11) que comentaba cliente_id en ViajesController.php y GuardarViaje.php. El hotfix se aplicó porque no se podía actualizar viajes sin cliente (form mandaba cliente_id="" y la regla exists:clientes,id fallaba).
  • Validación del hotfix: revisé el código de main. El problema YA ESTÁ ARREGLADO de forma definitiva en GuardarViaje::prepareForValidation() (líneas 58-64), que normaliza ""/"null"/"undefined"null antes de validar. Columna viajes.cliente_id ya es nullable; relación Viaje::cliente() usa withDefault(). Hotfix descartable sin riesgo.
  • Pasos ejecutados (todos OK):
    1. git restore de los 2 archivos PHP del hotfix + restauración de permisos cosméticos en artisan y 10 .gitignore.
    2. Dump SQL: ~/amadeus-pre-inventarios-20260519-2004.sql.gz (1.2 MB, usuario electrosystems en VM).
    3. php artisan down --refresh=15 → maintenance ON.
    4. git fetch && git pull --ff-only origin mainc3c85d2..fb5e039 fast-forward limpio.
    5. composer install --no-dev --prefer-dist --optimize-autoloader → OK.
    6. php artisan migrate --force22 migraciones DONE en ~25s (la más larga: add_handoff_states_to_movimiento_inventarios_table 6s; create_viaje_compras_table 3s).
    7. php artisan optimize → config/events/routes/views cached.
    8. php artisan queue:restart → signal broadcast.
    9. php artisan up → maintenance OFF.
  • Verificación post-deploy:
    • Sitio matriz: id=50, nombre="Matriz", codigo="MTZ" marcado correctamente con matriz=1 (la migración con búsqueda case-insensitive encontró el sitio existente; NO se duplicó).
    • Permisos nuevos: tabla permisos tiene 20 filas, 0 con inventarios_preparar/_entregar/_revisar=1 (defaults en false, asignación manual pendiente).
    • Última migración registrada: 2026_04_20_150100_add_lider_id_to_equipos_table en batch 13.
    • HTTP smoke test: curl -I https://viaticos.electrosystemsnet.com/HTTP/2 302 (redirect a login normal, sin 500).
  • Hallazgos / decisiones de la sesión guardados en memoria:
    • feedback_amadeus_ssh_authorized.md: SSH a amadeus autorizado permanente, usuario electrosystems, ruta /var/www/amadeus, backups solo dump SQL.
  • Falta (post-deploy):
    • Asignar permisos inventarios_preparar/_entregar/_revisar a usuarios correspondientes (vía Nova o SQL).
    • Smoke test funcional real con usuario.
    • Considerar limpieza de public/build/* huérfanos en prod (~28 archivos untracked, builds anteriores).
    • Considerar commitear deploy.sh que vive untracked en la VM.

2026-05-19 — feature notif push + email al crear viaje (deploy en prod)

  • Pidió Sergio: correo + push al crear viaje. Destinatarios: usuarios del viaje + supervisores con flag nuevo (opción D). SMTP: copiar config Google Workspace del servidor nagios.
  • Estado previo encontrado: push casi-completo (event, listener push, service, modelo PushSubscription, controller, service worker, composable Vue, auto-subscribe en layout, queue worker corriendo, 5 usuarios ya suscritos en prod). Probablemente ya funcionaba. NO había email ni flag para supervisores. Los 2 failed_jobs históricos eran de antes de configurar VAPID (2025-11), descartables.
  • Hice (commit 1c65c33, merge b744ab6):
    • Migración add_recibir_notif_viajes_to_permisos (boolean default false, after inventarios_revisar).
    • Modelo Permiso: +@property bool $recibir_notif_viajes.
    • Nova Permiso: +Boolean field con help text claro.
    • Service nuevo NotificarViajeCreadoService::destinatarios(Viaje $viaje): Collection — junta usuarios asignados (via ordenes.equipos.usuarios) + usuarios con permisos.recibir_notif_viajes=true, únicos por id. Reutilizable.
    • Listener push existente refactor: ahora usa el service, URL del payload corregida a /viajes/{id}.
    • Listener email nuevo EnviarNotificacionEmailViajeCreado (ShouldQueue): un correo por destinatario con email, log de enviados/fallidos.
    • Mailable ViajeCreadoMail: subject "Nuevo viaje asignado: {folio}", view Blade con folio + sitios + fechas + botón “Ver viaje”.
    • .env.example: documenta config Gmail Workspace (sin password) y usa MAIL_SCHEME (Laravel ≥9), no MAIL_ENCRYPTION.
  • SMTP config de Workspace documentado de paso en electrosystems/servers/nagios/README.md (cuenta webmailer@e-electrosystems.com, host smtp.gmail.com:587, app password vive en nagios:/etc/postfix/sasl_passwd). Nagios además quedó documentado en general (Nagios Core 4.4.10 con muy poca config — usado principalmente como SMTP relay).
  • Deploy ejecutado (prod):
    • Push feature/notif-viaje-creado + merge --no-ffmain → push (fb5e039..b744ab6).
    • Backup pre-deploy: ~/amadeus-pre-notif-20260519-2234.sql.gz (1.2M) + .env.backup-20260519-2234.
    • .env actualizado con credenciales Gmail (password leída desde nagios:/etc/postfix/sasl_passwd y pasada por stdin a SSH amadeus, nunca por el hub).
    • Down → fetch → pull → composer install → migrate (1 DONE) → optimize → queue:restart → up.
    • Verificado: columna agregada, ambos listeners visibles en event:list, mail.default=smtp con host/port/username/from correctos, HTTP 302 normal.
  • Falta:
    • Smoke test end-to-end real: Sergio crea un viaje en prod y confirma que:
      • Los usuarios asignados al viaje reciben push (5 usuarios ya estaban suscritos previo al deploy).
      • Los usuarios asignados con email reciben el correo.
      • Si Sergio marca un usuario con recibir_notif_viajes=true en Nova, ese usuario también recibe push+email.
    • Decidir QUIÉN debe tener el flag recibir_notif_viajes (probablemente: gerentes, supervisor de operaciones). Asignar en Nova post-deploy.
    • Si Sergio quiere test inmediato sin crear viaje real, puede pedir un test directo a un email específico (yo no inferí destinatario por seguridad; classifier lo bloqueó).

2026-05-19 — smoke test SMTP confirmado + fix “Amadeus” del email

  • Smoke test SMTP: enviado a svalencia@e-electrosystems.com a las 23:02 (server time), llegó OK confirmado por Sergio. SMTP Gmail Workspace funciona end-to-end desde prod.
  • Hallazgo en log: el push notification YA funcionaba antes de este deploy. Logs históricos muestran envíos exitosos: viaje 190 (2026-05-11), 191 y 192 (2026-05-18). Este deploy agregó email + flag supervisores; el push solo se refactor para usar el service compartido.
  • Pidió Sergio: “Amadeus es nombre código interno, no debe aparecer en el correo. Que diga solo Electrosystems.”
  • Hice (commit 8a2b266, hot-deploy sin maintenance):
    • Template viaje-creado.blade.php: "Se generó un nuevo viaje en Amadeus.""Se generó un nuevo viaje."
    • .env.example comentario: "Amadeus - Electrosystems""Electrosystems".
    • .env de prod: MAIL_FROM_NAME="Amadeus - Electrosystems"MAIL_FROM_NAME=Electrosystems.
    • Push a main → pull → php artisan optimize. Sin migrate ni queue:restart (no aplica).
    • Re-smoke email reenviado a las 23:23, confirmado por Sergio.
  • Nota: el subject "Nuevo viaje asignado: {folio}" del Mailable ya NO contenía “Amadeus” (revisado antes del fix); el problema vivía solo en FROM_NAME (encabezado) y en el body Blade.

2026-05-19 — N vehículos por equipo (fase 1) deployado

  • Pidió Sergio: permitir 2+ vehículos por viaje. Caso común: 1 equipo lleva 2 vehículos porque no cabe el material. Caso menos común (no incluido en fase 1): varios equipos al mismo viaje (Fase 2).
  • Decisión de arquitectura: mantener equipos.vehiculo_id como cache nullable del “primer vehículo” para compat con reportes legacy. Pivot equipo_vehiculo pasa a ser la fuente de verdad. Fase 2 puede dropear la columna.
  • Hice (commits e0ed6ad feat + 973cdbf build assets):
    • Migración create_equipo_vehiculo_table: pivot (equipo_id, vehiculo_id, timestamps) con unique compuesto + backfill desde equipos.vehiculo_id existente + vehiculo_id a nullable.
    • Modelo Equipo: agrega vehiculos() BelongsToMany. vehiculo() BelongsTo se documenta como cache del primero del pivot.
    • GuardarViaje request: vehiculo_id (singular required) → vehiculos[] array required min 1 + exists validation.
    • ViajesController::guardarViaje: sync del pivot + mantiene vehiculo_id = $validated['vehiculos'][0] como cache.
    • ViajesController::edit: carga equipos.vehiculos.
    • Viaje::relacionesDashboard: agrega ordenes.equipos.vehiculos.
    • transformarParaDashboard: emite los vehículos concatenados (pluck('nombre_combinado')->join(', ')), fallback al cache legacy si pivot vacío.
    • Nova/Equipo: agrega BelongsToMany('Vehiculos'); etiqueta el BelongsTo viejo como “Vehículo principal (cache)”.
    • Types Equipo.ts: marca vehiculo_id/vehiculo como @deprecated, agrega vehiculos: Vehiculo[].
    • Types Viaje.ts DatosFormaViaje: vehiculo_id: stringvehiculos: number[].
    • FormaViaje.vue: SelectInput único → grupo de checkbox multi-select (mismo patrón que usuarios). Populate de edición lee equipo.vehiculos con fallback a equipo.vehiculo_id.
    • ListaViajesItemOrden.vue: pluraliza etiqueta (“Vehículo”/“Vehículos”) según si la string trae coma.
    • Tests: ajustados payloads de POST/PUT (vehiculo_idvehiculos: [id]) en ViajesTest y ViajeCreadorEditTest. Fixtures de Equipo siguen con vehiculo_id directo (columna nullable los respeta).
    • npm run build ejecutado, 97 archivos de public/build/ regenerados.
  • Tests: 9/9 propios pasan en sail. Los 35 fails preexistentes de la suite (heredados del cambio de migración “Matriz” en sesión anterior — Sitio::where('matriz',true)->firstOrFail() falla en BD fresh) NO son regresión de este PR: misma cantidad de fails en main pre-merge.
  • Deploy ejecutado (prod):
    • Backup: ~/amadeus-pre-nvehiculos-20260520-0037.sql.gz (1.2M).
    • Down → fetch → pull (8a2b266..973cdbf) → migrate (1 DONE en 2s) → optimize → queue:restart → up. RC=0.
    • Backfill verificado: 188 equipos con vehiculo_id → 188 filas en equipo_vehiculo, 188 equipos distintos en el pivot. Sample muestra cache=pivot exactamente.
    • HTTP 302 normal.
  • Falta:
    • Smoke test funcional con un viaje real (crear viaje con 2 vehículos y verificar que se guardan, aparecen en edit, aparecen en el listado/reporte).
    • Fase 2 (N equipos por viaje) — separada, queda como pendiente.
    • Deuda de los 35 fails heredados del cambio “Matriz” — para sesión dedicada.

2026-05-19 — fix bugs en feature de notificaciones (in-app news)

  • Contexto: Sergio preguntó si el feature seguía vivo. Inventory completo: tabla notificaciones + pivot usuario_notificacion + modelo + middleware VerificarNotificacionesNoLeidas (registrado) + NotificacionesController (5 endpoints) + HandleInertiaRequests comparte notificaciones.tiene_no_leidas + sidebar con badge rojo ”!” + Vue Historial.vue + Nova resource — TODO existe y está cableado. 0 notificaciones creadas hasta ahora en prod.
  • 2 bugs latentes detectados:
    • Bug 1 — superadmin no ve notificaciones con permisos requeridos: scopeParaUsuario tenía if ($usuario->superadmin) return; dentro del closure where, sin agregar condición. El WHERE quedaba en permisos_requeridos IS NULL OR JSON_LENGTH=0 — superadmin nunca veía notificaciones dirigidas a permisos específicos.
    • Bug 2 — match JSON frágil: where("permisos_requeridos->$permiso", "true") solo matchea si Nova KeyValue serializa el valor como string. Si la notif se crea vía tinker/factory con ['inventarios' => true] (bool nativo JSON), el match falla.
  • Fix (commits 5f2461d + merge a0a07fc):
    • Bug 1: $q->orWhereNotNull('permisos_requeridos') antes del return en branch superadmin.
    • Bug 2: ampliado a match las 4 representaciones (string 'true'/'1' + JSON nativo true/1).
    • Tests Pest nuevos NotificacionParaUsuarioTest con 5 casos: superadmin con permisos, usuario con permiso (bool), usuario con permiso (string), usuario sin permiso (negativo), notif sin permisos para todos. 5/5 pasan en sail. Suite total: 56 passed (+5 nuevos), 35 failed (heredados, sin cambio).
  • Deploy ejecutado (hot, sin maintenance):
    • Push + merge --no-ff → main 973cdbf..a0a07fc.
    • SSH amadeus: git pull + artisan optimize. Sin migración, sin queue:restart, sin build.
    • Ventana efectiva: ~5 segundos.
  • Falta: smoke test funcional — Sergio crea una notificación de prueba en Nova (/nova/resources/notificaciones/new), confirma que aparece en /notificaciones y la badge del sidebar se enciende. Probar también con permisos_requeridos (clave inventarios, valor true) para validar que el filtro funciona end-to-end.

2026-05-20 — arquitectura del switch de inventarios + piloto Selene

  • Pidió Sergio: validar el flujo de activación completo del sistema de inventarios antes de asignar permisos. Recordaba que “el sistema solo debe activarse cuando él lo decida”.

  • Auditoría: el módulo tiene dos capas independientes de activación, lo cual es la arquitectura correcta:

    Capa 1 — Acceso operativo (permisos por usuario):

    • Permisos inventarios (legacy admin) + inventarios_preparar/_entregar/_revisar (granulares).
    • Controlan acceso a UI: Sidebar.vue:26-29 (puedeUsarMovimientos) + policies MovimientoInventarioPolicy, ViajeCompraPolicy.
    • Sin permisos → módulo invisible y bloqueado per-acción.

    Capa 2 — Integración automática viaje ↔ inventario (feature flag):

    • config/inventario.php'viaje_link_enabled' => env('INVENTARIO_VIAJE_LINK', false).
    • Consumidores: ViajesController::guardarViaje:205 (crea/sincroniza MovimientoInventario borrador), ViajesController::create/edit:358-360 (expone el flag al view), GuardarViaje::rules:25 (valida productos_data condicionalmente), FormaViaje.vue:96 (usarSelectorInventario para el selector inline).
    • Default false + variable NO definida en prod .env. La “activación total” requiere que Sergio agregue INVENTARIO_VIAJE_LINK=true al .env y haga php artisan config:cache.
  • Estado en prod (2026-05-20):

    • Capa 1: piloto ON parcial — Selene Ortega (user_id=14, fila permisos.id=16) con inventarios_preparar=1, inventarios_entregar=1, inventarios_revisar=1. UPDATE ejecutado con autorización explícita citando el query. Resto: 18 usuarios en 0.
    • Capa 2: OFF, sin cambios.
  • Camino recomendado:

    1. Selene prueba flujo manual end-to-end (preparar → entregar → revisar) desde /movimientos-inventario sin integración a viajes.
    2. Cuando Sergio dé luz verde, asignar permisos al resto de usuarios.
    3. Cuando todo el equipo esté listo, Sergio agrega INVENTARIO_VIAJE_LINK=true al .env de prod (capa 2 ON) y los viajes nuevos empiezan a generar movimientos borrador automáticos.

2026-05-20 — limpieza varia post-deploys de notificaciones

  • Pidió Sergio: abordar la limpieza acumulada después de los 2 deploys de UX de notificaciones.

  • Hecho en 4 pasos (cada uno con autorización per-paso del classifier):

    #1 — Borrar código muerto de notificaciones (commit 638b708, merge c4f6e32):

    • resources/js/Components/Notificaciones/ModalNotificaciones.vue eliminado (0 imports en cualquier layout).
    • NotificacionesController::tieneNoLeidas método eliminado.
    • Ruta /notificaciones/tiene-no-leidas (line 98 de routes/web.php) eliminada.
    • Deploy hot a prod: backup ~/amadeus-pre-cleanup-20260520-1945.sql.gz (1.2M) + pull + optimize.

    #2 — Limpiar duplicado de Karla en permisos (SQL directo en prod):

    • Hallazgo: el doc original era impreciso. Los 2 rows NO eran idénticos:
      • id=14 (updated_at=2026-05-19 23:30:42): recibir_notif_viajes=1 — row VIVO (Sergio lo activó ayer para el feature de email de viajes).
      • id=15 (updated_at=2024-08-19 20:33:41): recibir_notif_viajes=0 — fósil sin tocar desde la creación.
    • Como Usuario::permisos() es hasOne sin orderBy → MySQL devuelve por PK asc → id=14 ya era el row que la app usaba.
    • Aplicado: DELETE FROM permisos WHERE id = 15 (NO el plan original de “preservar id mayor”).
    • Verificación post-DELETE: SELECT … HAVING COUNT(*)>1 vacío; Karla solo tiene id=14 con recibir_notif_viajes=1.

    #3 — Commitear deploy.sh (commit 6c2c65d, merge 008118c):

    • Bajado desde la VM (creado 2025-10-28). Contenido: pull → composer → optimize → migrate —force → queue:restart. Sin backup ni maintenance ni npm build (que está commiteado).
    • Escrito idéntico en repo local con permisos +x, MD5 verificado (ebfff1901fbe721a9877931019514fc3).
    • Deploy: backup ~/amadeus-pre-deploysh-20260520-2008.sql.gz (1.2M); en prod rm deploy.sh && git pull (para evitar “untracked working tree files would be overwritten”); archivo quedó tracked con permisos +x.

    #4 — git clean -f public/build/ en prod:

    • Dry-run mostró 28 archivos huérfanos (*.js/*.css de builds anteriores).
    • Ejecutado real: 28 removidos. public/build/assets/ quedó con 67 archivos (los tracked alineados con HEAD).
    • HTTP 302 normal post-clean.
  • Estado final de prod (amadeus VM):

    • HEAD: 008118c.
    • Working tree limpio salvo .env.backup-20260519-2234 (recordatorio del deploy de email de ayer — limpiarlo es deuda menor sin urgencia).
    • 3 backups SQL frescos en ~electrosystems/ por si hay regresión.

2026-05-20 — rediseño móvil de /notificaciones (deploy hot)

  • Pidió Sergio: rediseño móvil de la página /notificaciones (Pages/Notificaciones/Historial.vue); el desktop estaba OK pero en móvil el botón “Marcar como leída” comprimía el contenido y el título se rompía feo con el chip.
  • Hice (commit 05e2524, merge 85c9356):
    • Header (Titulo): flex-col sm:flex-row para que el botón “Volver al inicio” caiga abajo del título en móvil.
    • Card de notif: contenedor flex-col sm:flex-row gap-4; el botón “Marcar como leída” se vuelve w-full sm:w-auto y aparece debajo del contenido en móvil, a la derecha en desktop (sm:shrink-0).
    • Header de la card (título + chip): flex-wrap items-center gap-2 para que el chip baje a nueva línea cuando el título es largo; título text-lg sm:text-xl break-words; contenido con break-words para URLs largas.
    • Padding card p-4 sm:p-6 (24px era excesivo en móvil); fechas text-xs sm:text-sm.
    • Filtros con flex-wrap como fallback.
  • Validación local: 2 notif de prueba creadas vía tinker (id=3 corta, id=4 título largo + URL larga + saltos de línea). Confirmado en devtools 375px por Sergio. Borradas antes de commit.
  • Deploy ejecutado (hot, sin maintenance):
    • Backup: ~/amadeus-pre-mobile-20260520-1936.sql.gz (1.2M).
    • git pull --ff-only origin main (203ed70..85c9356) + php artisan optimize. Sin migración, sin queue:restart.
    • HTTP 302 normal.

2026-05-20 — UX del indicador de notificaciones (deploy hot)

  • Pidió Sergio: mejorar UX del badge de notificaciones — el ”!” rojo del sidebar era muy discreto y, peor, solo se encendía para requiere_lectura=true (que ya redirige por middleware). Notif normales como la #2 de “N vehículos” no daban ninguna señal visual; tampoco había nada en topbar móvil con sidebar cerrado.
  • Diagnóstico antes de tocar:
    • HandleInertiaRequests::tieneNotificacionesNoLeidas filtraba por requiere_lectura=true — badge redundante con el middleware VerificarNotificacionesNoLeidas.
    • ModalNotificaciones.vue existía pero ningún layout lo importa; código muerto.
    • NotificacionesController::tieneNoLeidas (/notificaciones/tiene-no-leidas) sin caller desde front; reemplazado de facto por el shared prop.
  • Hice (commit 3f957a0, merge 203ed70 en main):
    • HandleInertiaRequests: shared prop notificaciones ahora { count_total, tiene_requiere_lectura }. Cuenta TODAS las no leídas relevantes (no solo urgentes).
    • InertiaPageProps.ts: tipo actualizado.
    • Sidebar.vue: badge ”!” → contador numérico (99+ si excede). Rojo + animate-pulse + ring si urgente; azul plano si normal; nada si 0. Dark mode pareado.
    • Layout.vue: nuevo botón campana a la derecha del topbar móvil (visible solo < lg). Mismo badge. Click → /notificaciones.
    • NO se tocó ModalNotificaciones.vue ni el endpoint sin uso — quedan listados como pendientes de limpieza.
  • Validación local:
    • Tinker creó notif #1 (normal) → badge azul 1 confirmado por Sergio.
    • Tinker creó notif #2 (urgente, requiere_lectura=true) → badge rojo+pulse 2 confirmado.
    • Notif de prueba borradas con detach + delete antes de commit.
  • Deploy ejecutado (hot, sin maintenance):
    • Backup: ~/amadeus-pre-notifux-20260520-1921.sql.gz (1.2M).
    • git pull --ff-only origin main (2ad9cae..203ed70) + php artisan optimize.
    • Sin migración, sin queue:restart, sin build (build commiteado).
    • HTTP 302 normal. Ventana efectiva: ~3 segundos.
  • Falta:
    • Smoke real con un usuario en prod cuando llegue una notif (la #2 “N vehículos” sigue activa; debería verse con contador 1 en azul para sus 4 receptores).
    • Decidir destino de ModalNotificaciones.vue y endpoint /notificaciones/tiene-no-leidas.
    • Rediseño móvil de /notificaciones (sigue pendiente, no fue alcance de este deploy).

2026-05-19 — bug 3 en notificaciones + notif real de “N vehículos” creada

  • Pidió Sergio: crear una notificación en prod anunciando el feature de N vehículos, dirigida a usuarios con gestionar_viajes=true.
  • Hice: Notificacion::create([...permisos_requeridos => ['gestionar_viajes' => true]]) vía tinker en prod. ID asignado: 2. Sync inicial reportó 15 usuarios elegibles. JSON guardado como bool nativo {"gestionar_viajes":true} (valida que el Bug 2 fix era necesario).
  • 🐛 Bug 3 descubierto al validar: la notif se sincronizó con 15 usuarios pero solo 4 cumplen el criterio (2 superadmins + Vitela + Ortega). Causa: en Notificacion::obtenerUsuariosElegibles, el callback de orWhereHas('permisos', ...) anidaba mal los orWhere. SQL generado:
    EXISTS (SELECT * FROM permisos WHERE usuarios.id = permisos.usuario_id OR gestionar_viajes = 1)
    El OR con la cláusula de join hacía el EXISTS verdadero para CUALQUIER usuario con al menos un row en permisos. Toda notif con permisos_requeridos no vacío se entregaba a todos.
  • Fix (commit 63455fe, merge 2ad9cae): envolver los orWhere en un closure interno $query->where(fn $sub => ...) para forzar paréntesis y mantener el AND con la cláusula de join. Test Pest agregado: “sincronizarUsuarios solo asigna a usuarios que cumplen los permisos requeridos”. 6/6 verde.
  • Deploy + cleanup (hot, sin maintenance):
    • Push + merge a0a07fc..2ad9cae → pull en prod + artisan optimize.
    • Cleanup en prod: DELETE FROM usuario_notificacion WHERE notificacion_id=2 AND usuario_id NOT IN (subquery con criterio correcto). 15 → 4 pivot rows.
    • Verificado: los 4 receptores son Sergio Valencia (superadmin), Gustavo Chavira (superadmin), Ernesto Vitela (gestionar_viajes=1), Selene Ortega (gestionar_viajes=1).
  • Deuda menor encontrada: el usuario id=13 (Karla Noriega) tiene 2 rows en la tabla permisos (ids 14 y 15, ambos con gestionar_viajes=0). Es data legacy, no rompe nada hoy pero la relación Usuario::permisos() es hasOne — debería tener un único row. Limpiable con DELETE FROM permisos WHERE id NOT IN (SELECT MAX(id) FROM permisos GROUP BY usuario_id) cuando convenga.

2026-05-21 noche — #045 cerrado: 17 tickets de Jira movidos a estado final

  • Sergio reportó: “#045 ya lo hice.”
  • Contexto: El #045 era el housekeeping de cerrar en Jira los 17 tickets que quedaron resueltos en la sesión maratón de 2026-05-20 (clasificación detallada en projects/amadeus-backlog.md):
    • DONE (5): AM-46, AM-47, AM-84, AM-134, AM-141.
    • Won’t Do (11): AM-9, AM-10, AM-58, AM-71, AM-85, AM-91, AM-97, AM-98, AM-107, AM-132, AM-146.
    • Linked a EPIC-NOTAS o duplicados (5): AM-65, AM-73, AM-90, AM-99, AM-136.
    • AM-143 NO se cerró — sigue activo en pausa de repro (#056).
  • Acción mía: registrar el cierre. La acción real fue en Jira (no tengo acceso), así que no hay verificación cross-checkable de mi lado más allá del reporte de Sergio.
  • Próximo paso natural del bloque Jira: #046 (crear sprints 15-21 según amadeus-backlog.md sección “Asignación a sprints”, 29 tickets en 7 sprints + 6 diferidos).

2026-05-22 noche — endpoints API para el bot electro-ia deployados a prod

  • Pidió Sergio: ejecutar el plan completo (~/.claude/plans/1-amadeus-es-un-mutable-boole.md) — endpoints amadeus + tools del bot + e2e.
  • Hice en branch feature/api-bot-electroia → merge a main (6b878f3) → deploy a VM amadeus:
    • Extracción de app/Services/CreadorDeViaje.php desde ViajesController::guardarViaje (líneas 77-260 originales). El event(new ViajeCreado(...)) ahora vive dentro del service para garantizar paridad web↔API. ViajesController::store/update refactor a llamar al service. Sin cambios funcionales.
    • app/Http/Controllers/Api/ViajesApiController.php + Api/SitiosApiController.php: ambos con Sanctum auth + ?dry_run=1. Body usa creado_por_id explícito; validan permiso de negocio sobre el Usuario resuelto vía Gate::forUser.
    • app/Http/Requests/GuardarSitio.php + app/Services/CreadorDeSitio.php: reglas extraídas del Nova Resource (nombre req max:255, codigo req unique max:3, normalizado a mayúsculas), permiso gestionar_viajes OR inventarios.
    • Sanctum 4.3 instalado (no estaba): composer require laravel/sanctum, HasApiTokens en Usuario, php artisan install:api publicó routes/api.php + registró middleware + corrió migration personal_access_tokens.
    • 17/17 tests Feature/Api verdes incluyendo Event::assertDispatched(ViajeCreado::class, 1) como criterio de aceptación obligatorio de paridad de eventos. Suite total: 74/35 (los 35 fallos son la deuda heredada de Sitio matriz documentada, idéntica al main pre-refactor — cero regresión).
  • Deploy a VM amadeus (commit 6b878f3 en prod):
    • Hallazgo durante deploy: composer require laravel/sanctum también bumpeó phpoffice/phpspreadsheet a 5.7.0 que ahora requiere ext-gd. La VM no tenía php8.3-gd instalado → composer install bloqueado. Sergio instaló php8.3-gd con sudo apt install -y php8.3-gd && sudo systemctl reload php8.3-fpm (autorizado per-sesión).
    • Backup: ~/amadeus-pre-apibot-20260522-2304.sql.gz (1.2M).
    • down → pull → composer install → migrate (1 DONE: personal_access_tokens) → optimize → queue:restart → up. HTTP 302 normal. route:list --path=api muestra api/viajes, api/sitios, api/whoami. curl -X POST /api/viajes sin token → {"message":"Unauthenticated."}
  • Token Sanctum del bot generado por Sergio vía php artisan tinker (no pasó por chat). Usuario “Bot Electro-IA” creado con superadmin=false, gestionar_viajes=false (correcto: el token solo prueba que el caller es el bot; el permiso de negocio lo valida amadeus sobre creado_por_id del payload).
  • Mapping telegram_user_id ↔ amadeus.usuarios.id vive en identity del bot, no en amadeus. Sergio (slug sergio) → usuarios.id=2. Gustavo (gustavo_chavira_mx) → usuarios.id=12. Ambos superadmin de amadeus.
  • Falta: smoke E2E desde Telegram que hará Sergio (pendiente #175 en PENDIENTES). Cuando lo retome: mensaje completo + mensaje parcial + sub-flujo sitio inexistente + edge cases (TTL del pending, usuario sin vincular).

2026-05-25 — housekeeping: línea “En progreso” cerrada + sprints documentados en hub

  • Pidió Sergio: cerrar la línea “Deploy de inventarios a prod” que seguía estancada en “En progreso” (el deploy se hizo el 2026-05-19), consolidar los smoke tests pendientes acumulados en un solo bloque accionable, y documentar los sprints 15-21 aquí en el hub en vez de Jira (#046).
  • Hice:
    • Sección “Tareas pendientes” reorganizada en 3 bloques: Operación (1 ítem), Smoke tests (5 ítems, incluye #175 que vive en electro-ia), Sprints (puntero al doc nuevo).
    • “En progreso” limpia (deploy ejecutado y verificado el 2026-05-19; queda solo smoke funcional).
    • Archivo nuevo projects/amadeus-sprints.md: 7 sprints (15-21) + backlog diferido, 30 tickets con ID del hub (#209-#238) y referencia AM-XX cruzada. Notas operativas y precondiciones copiadas del backlog.
    • PENDIENTES.md: #046, #053, #054, #056, #057 cerrados como “consolidado en amadeus-sprints.md”; bloque amadeus apunta al doc nuevo en lugar de listar cada sprint.
  • Falta: que Sergio decida cuándo arrancar Sprint 15 (quick wins ~2 días). El resto sigue como referencia hasta priorización.

2026-05-22 — plan: crear viajes desde el bot electro-ia

  • Pidió Sergio: poder crear viajes (y dar de alta sitios on-the-fly) desde el bot de Telegram electro-ia. Bot debe espejar el flujo vigente de la plataforma — incluyendo cuando se prenda INVENTARIO_VIAJE_LINK — sin redeploy. Y los eventos que ya dispara la UI (mail + push) deben dispararse igual cuando crea el bot.
  • Aclaración importante: confirmó que amadeus = viáticos (es el nombre interno del código). Por eso entra de lleno en el scope de electro-ia. Frontmatter actualizado con aliases: [viaticos]. Memoria del bot reforzada.
  • Diseño aprobado (~/.claude/plans/1-amadeus-es-un-mutable-boole.md):
    • Amadeus: extraer app/Services/Viajes/CreadorDeViaje.php desde ViajesController::guardarViaje, con el event(new ViajeCreado(...)) dentro del service para que web y API disparen lo mismo. Endpoint POST /api/viajes con Sanctum + ?dry_run=1. Endpoint POST /api/sitios paralelo con GuardarSitio nuevo (reglas extraídas del Nova Resource: nombre req max:255, codigo req unique max:3). Body usa creado_por_id explícito; mapping telegram_user_id ↔ usuarios.id vive del lado del bot (amadeus no se entera de Telegram).
    • Bot: tools crear_viaje y crear_sitio siguiendo el patrón pending_confirmation de write_file/send_email. El “wizard” emerge del tool-calling iterativo: dry-run devuelve 422 con missing_fields y el LLM pregunta lo que falte; cuando sitio no existe, devuelve hint para que el LLM ofrezca crearlo (dos confirmaciones consecutivas — sitio y viaje).
    • Track paralelo (no parte del MVP): aprendizaje de patrones materiales/herramientas por tipo de trabajo y sitio, con sugerencias. Diseño documentado en el plan; usa la tabla agent_memory del bot y un listener del evento ViajeCreado.
  • Decisiones tomadas en la sesión: mapping del lado del bot (no amadeus); roles del bot admin + engineer; discovery solo vía dry-run (sin endpoint /schema). Paridad de eventos = criterio de aceptación obligatorio del PR de amadeus.
  • Tasks atómicas del plan: 13 (en el harness, ver sesión). Bloqueada por autorización per-sesión para tocar el repo ~/code/amadeus (commit/push/deploy).
  • Falta: autorización de Sergio para arrancar implementación en amadeus (todo lo del bot va aparte; SSH a laptop-ia para escribir código + restart systemd ya tiene NOPASSWD acotado pero el primer paso necesita confirmación de Sergio igual).