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(norechazado);NoRechazadosScopeajustado (+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
85e0d16en main, prod migrado (columnasapelado_at/apelacion_motivo+ ruta OK). (Bloque A — ver bitácora) - #376 ✅ DEPLOYADO 2026-05-28 — Cola de revisión/apelaciones del admin (
/autorizacionesevolucionada: apeladas primero, rechazo-con-motivo, notifica al técnico). Commit8015dd5en 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
83b1964en main, migración corrida en prod (intentos_fallidosOK,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 (commits7e53af6/449a2cb/2506bb2/c60fc91+ PR 4 listo en local). Pendiente: (1) validación en staging/prod (correranalisis:auditar-viaje <viaje-cerrado>y compararviolaciones_politicavs 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
traccar2025deorionNO 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).aresdocumentado enelectrosystems/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 enelectrosystems/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_iden vehículos + permisonotificar_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 yamadeus-validacion-fotos.md. Solo falta prender el flag en prod = #249.
Operación
- 📅 2026-06-02 — #048 Asignar permisos
inventarios_preparar/_entregar/_revisaral resto (18 usuarios en 0) cuando Selene valide el piloto. Capa 1 ON parcial para Selene Ortega (user_id=14, los 3 permisos en1). 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énMisEntregasyViajeCompras. 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-iacon 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): headerflex-colen móvil, card de notif con botón “Marcar como leída” full-width abajo, header de card conflex-wrap, paddingp-4 sm:p-6, fechastext-xs sm:text-sm, filtros conflex-wrap. Desktop conserva layout horizontal. - (2026-05-20) Limpiar código muerto
ModalNotificaciones.vue: eliminado (carpetaNotificaciones/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 conrecibir_notif_viajes=0). Preservado id=14 (updated_at=2026-05-19 23:30:42,recibir_notif_viajes=1) — es el row que la relaciónUsuario::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 congit clean -f. Working tree de prod ahora limpio salvo.env.backup-20260519-2234. - (2026-05-20) Commitear
deploy.sh: ahora tracked enmain(commit008118c). Permisos+xpreservados en prod.
Estado del repo (revisado 2026-05-19)
- Rama actual local:
inventarios(limpia, en sync conorigin/inventarios). - Rama
inventariosestá 39 commits ahead deorigin/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
amadeusen Poseidon (2c/2G), expuesta vía reverse-proxy.
Checklist de deploy (pendiente de ejecutar)
- 📅 2026-06-05 — Decidir branch a desplegar (recomendado: merge a
mainprimero 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_*yremove_*— 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étodocasts(). - 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):
- Geofence por radio (haversine) — la base recomendada. Cada
sitiotiene 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. - 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”.
- 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
accuracydel 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 Laravelscheduled 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 enEditar.vuepara 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
c60fc91ya estaba enorigin/mainpero no se había corridodeploy.sh. - Deploy:
ssh amadeus 'cd /var/www/amadeus && ./deploy.sh'— pullAlready up to date(commit ya en remoto),Nothing to migrate(migración aditiva aconsumos_analisisya corrida),queue:restartOK. Prod ahora corre el código de PR 3:ContextoTicketBuilder+AnalizadorTicketServicecon inyección de política + tool extendido conviolaciones_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 compararviolaciones_politicavs 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(tablaspoliticas_viaticos+politicas_viaticos_historialappend-only) y2026_05_29_000002_add_politica_hash_to_auditorias_viaje(columnapolitica_hash CHAR(40) NULL+ index). Aplicadas en BD dev. - Modelos
PoliticaViaticos(derivahash_sha1del texto vía mutator de atributo — no evento, para que funcione bajoWithoutModelEventsdel seeder; observerupdatingarchiva el estado anterior en_historial; helper estáticovigente()) yPoliticaViaticosHistorial(append-only, sin timestamps). PoliticaViaticosPolicy(auto-discovery):superadminbypass + permiso existentegestionar_viaticos; delete/restore/forceDelete = false (la política se edita, no se borra). Reusé el flag que ya existía enPermiso, no inventé gate nuevo.- Nova
PoliticaViaticos(Textarea texto + Boolean vigente + Hiddeneditado_por_id=usuario actual víafillUsing+ Hash 7-char readonly + HasMany historial) y NovaPoliticaViaticosHistorial(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 enDatabaseSeeder. FactoryPoliticaViaticosFactorypara tests.
- 2 migraciones:
- 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 enViajeCompraTest/MovimientoInventarioHandoffTestson 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
/novaque un usuario congestionar_viaticospuede 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
7e53af6→git push origin main→ssh amadeus ./deploy.sh(2 migraciones corridas en prod + queue:restart) →db:seed --class=PoliticasViaticosSeeder --forceen 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 congestionar_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()cargaPoliticaViaticos::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 devuelvepolitica_hashen todas las rutas (éxito, fallback timeout/non-2xx, deshabilitado, sin api key).AuditorViajeService::auditar()persiste el hash enauditorias_viaje.politica_hash(con?? null, así el mock viejo del test no rompe).@propertyen el modelo. Pint removió de paso 2 imports muertos preexistentes. - Tests: 5 nuevos en
ResumidorPoliticaTest(Http::fake: inyección de la política ensystem, hash devuelto, solo usa la vigente, hash aun con análisis deshabilitado, y persistencia enauditorias_viajevía AuditorViajeService). 22 verdes con AuditorViajeServiceTest (sin regresión). Suite amplio análisis: 106 passed; 3 fallosViajeCompraTestpreexistentes (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).
2026-05-29 (más tarde) — #369 PR 1: link en sidebar de Nova
- El sidebar de Nova es un menú explícito (
Nova::mainMenu()enNovaServiceProvider), 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 congestionar_viaticos/superadmin). Historial NO va al menú (read-only, se llega por el HasMany). Commit449a2cb→ push → deploy (sin migración,optimizereconstruyó 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, cookieelectrosystems_session,SESSION_DOMAIN=null. La IP192.168.20.17del reporte emitees_monitoreo_session→ es la app monitoreo (es-antenas-new), no amadeus. El 419 sale de esa app (cookiessecuresobre HTTP plano por IP). - Resultado: el bug se sacó de amadeus y se recreó en
es-antenas-newcomo #415 (no #413 porque ese ID ya lo tenía vpn-clientes por colisión de sesión paralela). Veres-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.17sale 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_URLatados 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 conorigin/main) vía subagente. Veredicto: HECHO — las 3 fases del scope ya están enmain, y rebasado. El docamadeus-validacion-fotos.mdquedó desactualizado (seguía enready-to-build); el feature completo se construyó entre el 25-may y el milestone de consumos.- Modelo + tabla:
app/Models/ConsumoAnalisis.php+ migración base2026_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íadispatchAnalisisIA()detrás del doble flaganalisis.enabled+analisis.hook_consumos_enabled. - Servicio Antigravity vision:
app/Services/Vision/AnalizadorTicketService.php—/v1/messagescon imagen base64 + tool usedictaminar_ticket(tool_choice forzado), JSON estructurado, cálculo de costo, cap diario, retry. Modelo defaultclaude-sonnet-4-6. - Fase 1 (foto inválida): rechaza si
es_ticket_valido=false. Fase 2 (OCR + monto):coincideMontocon 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.
- Modelo + tabla:
- Estado real: todo el código está en
mainpero apagado por flags en prod (ANALISIS_ENABLED/ANALISIS_HOOK_CONSUMOS_ENABLEDdefault false). Prenderlo = #249 (lunes 2026-06-01). #239 se marca cerrado. - Falta: nada de código. Actualizado el design doc
amadeus-validacion-fotos.mdadone.
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_fallidosque 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 configurableanalisis.max_intentos_revision(default 3). Al llegar al tope, auto-ancla reusando el mecanismo de #375: setapelado_at+ motivo automático → entra a la cola admin de #376. Notificación técnico+admin reusaConsumoRechazadoPorIA(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: columnaintentos_fallidos(unsignedTinyInteger, default 0) enconsumos_analisis. Configmax_intentos_revision. - Job
AnalizarFotoConsumoJob: en la rama de rechazo incrementaintentos_fallidos; al alcanzar el tope (y si no estaba ya apelado) seteaapelado_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 cuandointentos_fallidos >= max(anclado). El link de editar desaparece del dashboard, consistente con rechazado/autorizado. - Frontend: flag
ancladoencamposConsumo; badge “En revisión humana” (azul) vs “En revisión” (ámbar) enListaViajesItemConsumos.vue; tipointentos_fallidos/ancladoenConsumo.ts. - Tests:
TopeReintentosConsumoTestcon 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.
- Migración
- DEPLOYADO 2026-05-28: commit
83b1964en main +deploy.shcorrió 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 (prenderANALISIS_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 (antesrechazarno capturaba nada): reusaoverride_admin_id/motivo/atcomo rastro de auditoría — la dirección la daestatus(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::rechazarahora aceptaoverride_motivoopcional, seteaestatus=rechazado+override_admin_*(si hay análisis) y disparaConsumoResueltoPorAdmin('rechazado');autorizardisparaConsumoResueltoPorAdmin('autorizado'). Evento + listenerEnviarNotificacionConsumoResuelto(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:
RevisionAdminConsumoTestcon 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.
- Backend:
- 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 en8015dd5, 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 ajm-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; tablafacturascon 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.
- #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
- 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(sigueobservado) ni el veredicto interno; solo registra la apelación enconsumos_analisis(apelado_at+apelacion_motivo). El consumo ya aparece en/autorizacionesdesde #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 enconsumos_analisis(apelado_attimestamp,apelacion_motivotext). Reversible. - Backend:
ConsumoPolicy::apelar(mismo dueño/permiso/viaje-no-entregado queupdate, + exige NO ya-apelado y NO overrideado); rutaPOST /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 deapelado_at/apelacion_motivoen el re-análisis. - Notificación al admin: evento
ConsumoApelado+ listenerEnviarNotificacionConsumoApelado(auto-discovery L11,ShouldQueue) → email aanalisis.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” enPanelAnalisisIA.vue(visible para técnico y admin); campos enConsumo.ts. - Tests:
ApelacionConsumoTestcon 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.
- Migración
- Nota — fallos preexistentes: el suite completo de amadeus tiene 35 tests rojos en inventario/compras (
MovimientoInventario*,ViajeCompra,LiderChecklist*,DashboardInventario) conModelNotFoundException— verifiqué que fallan igual enmainlimpio (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: columnasapelado_at/apelacion_motivoOK, rutaconsumos.apelarregistrada, 0 apeladas (nadie ha usado el botón aún — hook #249 sigue OFF). 35 rojos de inventario confirmados preexistentes enmain(ajenos). - Sigue: #376 (cola admin dedicada de apelaciones) — hoy las apelaciones ya caen en
/autorizacionescon 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/autorizadoquedan a decisión humana. - Diseño (clave): hay dos campos de estado —
consumos.estatus(lo que ve el técnico; lo filtraNoRechazadosScope) yconsumos_analisis.estado(veredicto interno de la IA:ok|rechazado|error). Decisión: solo cambiaconsumo.estatusderechazado→observado;analisis.estadose queda enrechazado(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:ALTERdel enumconsumos.estatus→pendiente|observado|rechazado|autorizado(guardado conDB::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 conestatus='rechazado'puestos por la IA en el batch forense 191/192 (sin override humano) se devuelven aestatus=null(“dejar esos viáticos como antes de la IA”). Preserva losconsumos_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 solorechazado(humano-terminal);observadoqueda visible. Bug latente arreglado: envolví elwhere()->orWhereNull()en closure (sin agrupar se fugaba de otroswhere, p. ej.viaje_id).ConsumosController: re-subida resetea desdeobservado; query de/autorizacionesbuscaobservado.ConsumoPolicy::update: apelación (re-subir) habilitada cuandoestatus='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” (typeconsumo_observado_ia); mail subject/body “marcado para revisión / no es definitivo”; tipoConsumo.ts. - Tests: actualizados los que asumían
consumo.estatus='rechazado'del flujo IA →observado(Job, E2E, integración, UI, notificación). Las aserciones sobreanalisis.estado='rechazado'se mantienen (veredicto interno). 83/83 verdes (Job + E2E + integración + UI + notif + dashboard + forense + comandos).pint --dirtyOK.npm run buildOK (sin errores TS).
- Migración schema
- Decisión NO tomada (fuera de scope #374, anotada): el auditor forense
AuditorViajeService::marcarPendientes(#247, herramienta manual) sigue saltando soloestatus='rechazado'; podría querer saltar tambiénobservado. No lo toqué — es otro path. Evaluar al retomar #247. - DEPLOYADO 2026-05-28 (Sergio autorizó commit+push+merge+deploy): commit
9ce3b6b, fast-forward amain, push a origin,deploy.shen 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) aestatus=null, preservando susconsumos_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
observadoreal (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:workgestionados por supervisor (/etc/supervisor/conf.d/laravel-worker.conf:numprocs=8,autostart=true,autorestart=true, userwww-data,--sleep=3 --tries=3 --timeout=3600). Up desde 26-may, estables (~1d22h, sin crash-loop).deploy.shya hacephp artisan queue:restart. - El comando no lleva
--queue→ procesan la coladefault. - En el código, el hook
ConsumosController::dispatchAnalisisIA()(línea 264, gated poranalisis.enabled && analisis.hook_consumos_enabled) haceAnalizarFotoConsumoJob::dispatch($consumo->id)sinonQueue→ el job cae endefault. El Job tampoco define$queue. - El literal
analizar-fotoNO 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, usadefault. - Prod:
QUEUE_CONNECTION=database,ANALISIS_ENABLED=true,ANALISIS_HOOK_CONSUMOS_ENABLED=false(=#249). Tablajobsvacía (nada atorado),failed_jobs=2 (históricos VAPID 2025-11, descartables). worker.logconfirma que los workers procesandefaultcon éxito — incluyendoEnviarNotificacionConsumoRechazado(listener del flujo de visión) y jobs de Telescope, todosDONE.
- 8 procesos
- 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-fotopara 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=trueen/var/www/amadeus/.envharí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:NoRechazadosScopeoculta los rechazados (incompatible con “apelable”), sitios con GPS nullable, y no existen coords de oficina ni captura de hospedaje. Creé el design docamadeus-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
rechazadoterminal (usarobservado; humano decide) para limpiar elNoRechazadosScopede 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):
- Inyectar texto-política de Gustavo en la IA de validación.
- 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. Tablapoliticas_viaticos+ historial (single-row + observer) +politica_hashenauditorias_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=5442resuelto 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-fotosin workersystemden prod (capturado como #371). Por eso el agente original “esperó fotos” en vano — los jobs encolados nunca se procesaron. Fix: comando síncronoanalisis:auditar-viajeejecutado 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/amadeusy commit + push + deploy. - Cambios revisados:
resources/js/Pages/Admin/AuditoriaViaje.vuereescrito (701 ins / 288 del). Lógica intacta — mismos handlersgenerar()/abrirFoto()/rowClase()/banderasConsumo(), misma interfazConsumoFlag/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 conbackdrop-blur-smy barra superior con consumo_id + abrir-original. Dark mode pareado en todo. Eliminadas dependenciasTable/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-lines→line-clamp-3.animate-spin-slowse dejó (no-op pero inofensivo; cambiarlo aanimate-spinharía girar molesto el ícono “Re-generar”). - Flujo de deploy:
npm run build(sail) regeneró todos los assets depublic/build/. Branchfeature/rediseno-auditoria-viajepush → merge--no-ffa main (e720e40) → push main →ssh amadeus ./deploy.shejecutado (composer install no cambios, optimize, sin migrate, queue:restart). HTTP302 0.21spost-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 adeliverables/el 2026-05-28) leyendoauditorias_viaje.id=1directo 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 era2026-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/sitiosconsumidos por el bot electro-ia (Fase 3.0). - Pre-flight check (todo verde):
- Bot
electro-iaactivo en laptop-ia desde 2026-05-22 17:39. viaticos.electrosystemsnet.com/api/{whoami,viajes,sitios}→ 401 sin token conAccept: application/json(con browser default cae a 302 a login porque entra al middleware web; el bot usa Accept JSON, OK).INVENTARIO_VIAJE_LINKno está en el.env→ flag false → bot pidematerial+herramientas(no productos del catálogo). Confirma el piloto.- Baseline
MAX(viajes.id) = 192.
- Bot
- 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
ViajeCreadodisparado — paridad total con flujo web: push 4/4 OK, mail 4/4 OK a los 4 usuarios congestionar_viajes=true(Sergio/Gustavo/Vitela/Ortega). Logs enstorage/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}.
- Sitio 51
- (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. ViajeCreadodisparado: push 4/4 + mail 4/4 OK.
- Viaje 194
- (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 —
lookupByTelegramUserIdno cargabaamadeus_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_idparasergioera 2 y paragustavo_chavira_mxera 12 (vinculados desde 2026-05-22). - Causa raíz: la función
lookupByTelegramUserIdensrc/identity.js:8-11cargaba 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 esoctx.identity.amadeus_usuario_idllegabaundefineda las toolscrear_sitio(línea 41) ycrear_viaje(línea 82), que verifican explícitamente esa propiedad antes del dry-run. - Fix (1 palabra): añadir
amadeus_usuario_idal SELECT. Aplicado víased -ien 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.
- 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
- 🐛 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_PATTERNensrc/index.js:404llamabaresolveLastPending({status:'confirmed'})— esa función filtra porexpires_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 columnastatusmantiene sustatus_checkoriginal (pending|confirmed|denied|expired). - 3 helpers en
src/db.js:offerRetryForRecentExpired({identity_id, max_age_seconds=300})— busca pending constatus='pending' AND retry_state IS NULL AND expires_at <= now() AND expires_at > now() - 300s, lo marcastatus='expired', retry_state='offered', resolved_at=now()en una transacción y devuelve la fila.findOfferedRetry({identity_id, max_age_seconds=120})— devuelve pending conretry_state='offered'reciente (no muta).consumeOfferedRetry({id, state})— marcaretry_state='consumed'o'rejected'.
- Handler
CONFIRM_PATTERNampliado con 2 fallbacks trasresolveLastPendingnull:- Si hay
findOfferedRetry→ marcaconsumedy re-ejecuta el proposer víarunTool({name, input, ctx: { identity, message_id }})→ nuevo dry-run + nuevo pending (TTL fresco 90s para viaje, 60s para sitio) + mandainstruction_for_userdel resultado al chat. - Si hay
offerRetryForRecentExpired→ manda al chat: “El pending decrear_viajeexpiró hace Xs. ¿Quieres que lo reintente con los mismos datos? Responde ‘sí’ para volver a proponerlo (TTL nuevo), o ‘no’ para cancelar.”
- Si hay
- Handler
DENY_PATTERNampliado simétricamente: si hay offered retry, marcarejectedy responde “OK, no reintentocrear_viaje.”
- Migration Postgres:
- Cobertura transversal: aplica a las 4 tools con pending (
write_file/send_email/crear_sitio/crear_viaje) sin tocar cada una — el handler vive enindex.jsy opera sobre la tablapending_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 ensrc/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-89ycrear_sitio.js:41-48; con stubs dectx.identityse 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 prendaINVENTARIO_VIAJE_LINK=trueen~/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.mddel 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
inventarioslimpia, 39 commits ahead deorigin/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 VMamadeusen 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
inventariosno 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
ViajePolicy::updaterestringe edición al creador del viaje. Hoy: cualquier usuario congestionar_viajesedita cualquier viaje. Después del deploy: solo el creador (creado_por_id) puede editar; viajes legacy concreado_por_idnull siguen abiertos a gestores. → ¿Es lo deseado?InventarioPolicy::deleteahora exigesuperadmin. Usuarios con permisoinventarios=truepierden capacidad de borrar inventarios. → ¿Aceptable?Producto::boot::createdremovido — al crear un producto ya NO se siembran automáticamente filas eninventariospor cada sitio. Si algún reporte asumía esa siembra, mostrará vacíos. → ¿Hay algo que dependa de esto?
Riesgos técnicos identificados
- Migración hybrid 2025_11_19_002335 tiene
down()vacío — rollback no es reversible para esa migración. Backup completo antes es obligatorio. APP_LOCALEdefault cambia deenaesenconfig/app.php. Si el.envde prod no fuerzaAPP_LOCALE=en, mensajes Laravel pasarán a español (probablemente OK, confirmar).- Ruta
/dev/login-asregistrada en prod — protegida porapp()->environment('local')con abort 404. Segura siAPP_ENV=production(verificar). ProductosController@indexahora retorna paginator en vez de Collection (15/pág). Vistas legacy/Nova que esperaban array pueden romperse.- Migración
add_matriz_to_sitiosinserta sitio “Matriz” con códigoMTZsin chequeo de colisión por código (solo busca por nombre). Si ya existe un sitio con códigoMTZo nombre distinto, puede colisionar. - Permisos nuevos
inventarios_preparar/_entregar/_revisarse crean enfalse— requieren asignación manual post-deploy a los usuarios correspondientes. - Flag
INVENTARIO_VIAJE_LINKcontrola 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, dropestatusenum, reintroducecantidad. 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).ProductosControllerahora paginado.routes/web.phpsolo 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_LINKno definido,APP_LOCALE(forzarensi se quiere mantener). -
📅 2026-06-08 — Confirmar que no exista sitio con código
MTZprevio. -
📅 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:
- Edición de viajes: confirmado, solo el creador puede editar (sin cambio).
- Borrar inventarios: revertir a comportamiento previo (usuarios con
permisos.inventariospueden borrar). Producto::boot::createdremovido: validar que no afecte reportes.- Arreglar las migraciones para rollback parcial.
- NO insertar sitio “Matriz” en la migración — ya existe uno en prod.
-
Hice (en
~/code/amadeus, ramainventarios, sin commit):a. Validación de
Producto::boot::created(subagent Explore):- Conclusión: NINGÚN reporte/listado depende de la siembra automática.
InventariosControlleragrupa congroupBy('producto_id')->groupBy('sitio_id')solo sobre datos existentes; no asume matriz producto×sitio completa.InventarioService::aplicarEntrada()(líneas 137-153) usafirstOrCreate()bajo demanda al procesar movimientos.ProductoImporter,DatabaseSeeder, NovaInventario.phpy tests no asumen la siembra automática.- Conclusión: post-deploy, nuevos productos no tendrán rows en
inventarioshasta que haya un movimiento — coherente con la arquitectura actual.
b.
app/Policies/InventarioPolicy.phplínea 36:delete()regresa a$usuario->permisos->inventarios(erasuperadminen la rama).restore()yforceDelete()siguen siendo superadmin (igual que enmain).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, lanzaRuntimeExceptioncon mensaje claro (no rollback silencioso ni destructivo). - Si no hay nulls: vuelve
numero_seriea NOT NULL y dropeacantidad. Rollback parcial reversible bajo esa condición.
d.
database/migrations/2026_01_17_002211_add_matriz_to_sitios_table.php: eliminado elINSERTdel sitio “Matriz”.- Sigue agregando la columna
matrizboolean 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 -len los 3 archivos modificados → sin errores.
2026-05-19 — commit + merge + push
- Pidió Sergio: “Dale al commit y merge/deploy”.
- Hice:
- Commit
a10d7f6eninventarioscon los 3 fixes pre-deploy. - Push de
inventariosal remote. - Merge
inventarios→maincon--no-ff(commitfb5e039). Auto-merge resolvió un conflicto trivial enapp/Http/Controllers/ConsumosController.php. - Push de
mainal remote (c3c85d2..fb5e039).
- Commit
- Estado: repo en remote listo.
origin/mainya 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_idenViajesController.phpyGuardarViaje.php. El hotfix se aplicó porque no se podía actualizar viajes sin cliente (form mandabacliente_id=""y la reglaexists:clientes,idfallaba). - Validación del hotfix: revisé el código de
main. El problema YA ESTÁ ARREGLADO de forma definitiva enGuardarViaje::prepareForValidation()(líneas 58-64), que normaliza""/"null"/"undefined"→nullantes de validar. Columnaviajes.cliente_idya es nullable; relaciónViaje::cliente()usawithDefault(). Hotfix descartable sin riesgo. - Pasos ejecutados (todos OK):
git restorede los 2 archivos PHP del hotfix + restauración de permisos cosméticos enartisany 10.gitignore.- Dump SQL:
~/amadeus-pre-inventarios-20260519-2004.sql.gz(1.2 MB, usuarioelectrosystemsen VM). php artisan down --refresh=15→ maintenance ON.git fetch && git pull --ff-only origin main→c3c85d2..fb5e039fast-forward limpio.composer install --no-dev --prefer-dist --optimize-autoloader→ OK.php artisan migrate --force→ 22 migraciones DONE en ~25s (la más larga:add_handoff_states_to_movimiento_inventarios_table6s;create_viaje_compras_table3s).php artisan optimize→ config/events/routes/views cached.php artisan queue:restart→ signal broadcast.php artisan up→ maintenance OFF.
- Verificación post-deploy:
- Sitio matriz:
id=50, nombre="Matriz", codigo="MTZ"marcado correctamente conmatriz=1(la migración con búsqueda case-insensitive encontró el sitio existente; NO se duplicó). - Permisos nuevos: tabla
permisostiene 20 filas, 0 coninventarios_preparar/_entregar/_revisar=1(defaults en false, asignación manual pendiente). - Última migración registrada:
2026_04_20_150100_add_lider_id_to_equipos_tableen batch 13. - HTTP smoke test:
curl -I https://viaticos.electrosystemsnet.com/→HTTP/2 302(redirect a login normal, sin 500).
- Sitio matriz:
- Hallazgos / decisiones de la sesión guardados en memoria:
feedback_amadeus_ssh_authorized.md: SSH aamadeusautorizado permanente, usuarioelectrosystems, ruta/var/www/amadeus, backups solo dump SQL.
- Falta (post-deploy):
- Asignar permisos
inventarios_preparar/_entregar/_revisara 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.shque vive untracked en la VM.
- Asignar permisos
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_jobshistóricos eran de antes de configurar VAPID (2025-11), descartables. - Hice (commit
1c65c33, mergeb744ab6):- Migración
add_recibir_notif_viajes_to_permisos(boolean default false, afterinventarios_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 (viaordenes.equipos.usuarios) + usuarios conpermisos.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 usaMAIL_SCHEME(Laravel ≥9), noMAIL_ENCRYPTION.
- Migración
- SMTP config de Workspace documentado de paso en
electrosystems/servers/nagios/README.md(cuentawebmailer@e-electrosystems.com, hostsmtp.gmail.com:587, app password vive ennagios:/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-ff→main→ push (fb5e039..b744ab6). - Backup pre-deploy:
~/amadeus-pre-notif-20260519-2234.sql.gz(1.2M) +.env.backup-20260519-2234. .envactualizado con credenciales Gmail (password leída desdenagios:/etc/postfix/sasl_passwdy 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=smtpcon host/port/username/from correctos, HTTP 302 normal.
- Push
- 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=trueen 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ó).
- Smoke test end-to-end real: Sergio crea un viaje en prod y confirma que:
2026-05-19 — smoke test SMTP confirmado + fix “Amadeus” del email
- Smoke test SMTP: enviado a
svalencia@e-electrosystems.coma 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.examplecomentario:"Amadeus - Electrosystems"→"Electrosystems"..envde 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.
- Template
- 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_idcomo cache nullable del “primer vehículo” para compat con reportes legacy. Pivotequipo_vehiculopasa a ser la fuente de verdad. Fase 2 puede dropear la columna. - Hice (commits
e0ed6adfeat +973cdbfbuild assets):- Migración
create_equipo_vehiculo_table: pivot(equipo_id, vehiculo_id, timestamps)con unique compuesto + backfill desdeequipos.vehiculo_idexistente +vehiculo_ida nullable. - Modelo
Equipo: agregavehiculos()BelongsToMany.vehiculo()BelongsTo se documenta como cache del primero del pivot. GuardarViajerequest:vehiculo_id(singular required) →vehiculos[]array required min 1 + exists validation.ViajesController::guardarViaje: sync del pivot + mantienevehiculo_id = $validated['vehiculos'][0]como cache.ViajesController::edit: cargaequipos.vehiculos.Viaje::relacionesDashboard: agregaordenes.equipos.vehiculos.transformarParaDashboard: emite los vehículos concatenados (pluck('nombre_combinado')->join(', ')), fallback al cache legacy si pivot vacío.Nova/Equipo: agregaBelongsToMany('Vehiculos'); etiqueta el BelongsTo viejo como “Vehículo principal (cache)”.- Types
Equipo.ts: marcavehiculo_id/vehiculocomo@deprecated, agregavehiculos: Vehiculo[]. - Types
Viaje.ts DatosFormaViaje:vehiculo_id: string→vehiculos: number[]. FormaViaje.vue: SelectInput único → grupo de checkbox multi-select (mismo patrón queusuarios). Populate de edición leeequipo.vehiculoscon fallback aequipo.vehiculo_id.ListaViajesItemOrden.vue: pluraliza etiqueta (“Vehículo”/“Vehículos”) según si la string trae coma.- Tests: ajustados payloads de POST/PUT (
vehiculo_id→vehiculos: [id]) enViajesTestyViajeCreadorEditTest. Fixtures de Equipo siguen convehiculo_iddirecto (columna nullable los respeta). npm run buildejecutado, 97 archivos depublic/build/regenerados.
- Migración
- 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 enequipo_vehiculo, 188 equipos distintos en el pivot. Sample muestracache=pivotexactamente. - HTTP 302 normal.
- Backup:
- 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+ pivotusuario_notificacion+ modelo + middlewareVerificarNotificacionesNoLeidas(registrado) +NotificacionesController(5 endpoints) +HandleInertiaRequestscompartenotificaciones.tiene_no_leidas+ sidebar con badge rojo ”!” + VueHistorial.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:
scopeParaUsuarioteníaif ($usuario->superadmin) return;dentro del closure where, sin agregar condición. El WHERE quedaba enpermisos_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.
- Bug 1 — superadmin no ve notificaciones con permisos requeridos:
- Fix (commits
5f2461d+ mergea0a07fc):- Bug 1:
$q->orWhereNotNull('permisos_requeridos')antes del return en branch superadmin. - Bug 2: ampliado a match las 4 representaciones (string
'true'/'1'+ JSON nativotrue/1). - Tests Pest nuevos
NotificacionParaUsuarioTestcon 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).
- Bug 1:
- Deploy ejecutado (hot, sin maintenance):
- Push + merge
--no-ff→ main973cdbf..a0a07fc. - SSH amadeus:
git pull+artisan optimize. Sin migración, sin queue:restart, sin build. - Ventana efectiva: ~5 segundos.
- Push + merge
- Falta: smoke test funcional — Sergio crea una notificación de prueba en Nova (
/nova/resources/notificaciones/new), confirma que aparece en/notificacionesy la badge del sidebar se enciende. Probar también conpermisos_requeridos(claveinventarios, valortrue) 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) + policiesMovimientoInventarioPolicy,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(validaproductos_datacondicionalmente),FormaViaje.vue:96(usarSelectorInventariopara el selector inline). - Default false + variable NO definida en prod
.env. La “activación total” requiere que Sergio agregueINVENTARIO_VIAJE_LINK=trueal.envy hagaphp artisan config:cache.
- Permisos
-
Estado en prod (2026-05-20):
- Capa 1: piloto ON parcial — Selene Ortega (user_id=14, fila
permisos.id=16) coninventarios_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.
- Capa 1: piloto ON parcial — Selene Ortega (user_id=14, fila
-
Camino recomendado:
- Selene prueba flujo manual end-to-end (preparar → entregar → revisar) desde
/movimientos-inventariosin integración a viajes. - Cuando Sergio dé luz verde, asignar permisos al resto de usuarios.
- Cuando todo el equipo esté listo, Sergio agrega
INVENTARIO_VIAJE_LINK=trueal.envde prod (capa 2 ON) y los viajes nuevos empiezan a generar movimientos borrador automáticos.
- Selene prueba flujo manual end-to-end (preparar → entregar → revisar) desde
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, mergec4f6e32):resources/js/Components/Notificaciones/ModalNotificaciones.vueeliminado (0 imports en cualquier layout).NotificacionesController::tieneNoLeidasmétodo eliminado.- Ruta
/notificaciones/tiene-no-leidas(line 98 deroutes/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.
- id=14 (
- Como
Usuario::permisos()eshasOnesin 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(*)>1vacío; Karla solo tiene id=14 conrecibir_notif_viajes=1.
#3 — Commitear
deploy.sh(commit6c2c65d, merge008118c):- 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 prodrm 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/*.cssde 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 (
amadeusVM):- 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.
- HEAD:
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, merge85c9356):- Header (Titulo):
flex-col sm:flex-rowpara 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 vuelvew-full sm:w-autoy 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-2para que el chip baje a nueva línea cuando el título es largo; títulotext-lg sm:text-xl break-words; contenido conbreak-wordspara URLs largas. - Padding card
p-4 sm:p-6(24px era excesivo en móvil); fechastext-xs sm:text-sm. - Filtros con
flex-wrapcomo fallback.
- Header (Titulo):
- 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.
- Backup:
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::tieneNotificacionesNoLeidasfiltraba porrequiere_lectura=true— badge redundante con el middlewareVerificarNotificacionesNoLeidas.ModalNotificaciones.vueexistí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, merge203ed70en main):HandleInertiaRequests: shared propnotificacionesahora{ 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+ringsi 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.vueni el endpoint sin uso — quedan listados como pendientes de limpieza.
- Validación local:
- Tinker creó notif #1 (normal) → badge azul
1confirmado por Sergio. - Tinker creó notif #2 (urgente,
requiere_lectura=true) → badge rojo+pulse2confirmado. - Notif de prueba borradas con detach + delete antes de commit.
- Tinker creó notif #1 (normal) → badge azul
- 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.
- Backup:
- Falta:
- Smoke real con un usuario en prod cuando llegue una notif (la #2 “N vehículos” sigue activa; debería verse con contador
1en azul para sus 4 receptores). - Decidir destino de
ModalNotificaciones.vuey endpoint/notificaciones/tiene-no-leidas. - Rediseño móvil de
/notificaciones(sigue pendiente, no fue alcance de este deploy).
- Smoke real con un usuario en prod cuando llegue una notif (la #2 “N vehículos” sigue activa; debería verse con contador
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 deorWhereHas('permisos', ...)anidaba mal losorWhere. SQL generado:
ElEXISTS (SELECT * FROM permisos WHERE usuarios.id = permisos.usuario_id OR gestionar_viajes = 1)ORcon la cláusula de join hacía el EXISTS verdadero para CUALQUIER usuario con al menos un row enpermisos. Toda notif conpermisos_requeridosno vacío se entregaba a todos. - Fix (commit
63455fe, merge2ad9cae): envolver losorWhereen 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).
- Push + merge
- Deuda menor encontrada: el usuario id=13 (Karla Noriega) tiene 2 rows en la tabla
permisos(ids 14 y 15, ambos congestionar_viajes=0). Es data legacy, no rompe nada hoy pero la relaciónUsuario::permisos()eshasOne— debería tener un único row. Limpiable conDELETE 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.mdsecció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.phpdesdeViajesController::guardarViaje(líneas 77-260 originales). Elevent(new ViajeCreado(...))ahora vive dentro del service para garantizar paridad web↔API.ViajesController::store/updaterefactor a llamar al service. Sin cambios funcionales. app/Http/Controllers/Api/ViajesApiController.php+Api/SitiosApiController.php: ambos con Sanctum auth +?dry_run=1. Body usacreado_por_idexplícito; validan permiso de negocio sobre elUsuarioresuelto víaGate::forUser.app/Http/Requests/GuardarSitio.php+app/Services/CreadorDeSitio.php: reglas extraídas del Nova Resource (nombrereq max:255,codigoreq unique max:3, normalizado a mayúsculas), permisogestionar_viajes OR inventarios.- Sanctum 4.3 instalado (no estaba):
composer require laravel/sanctum,HasApiTokensenUsuario,php artisan install:apipublicóroutes/api.php+ registró middleware + corrió migrationpersonal_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).
- Extracción de
- Deploy a VM amadeus (commit
6b878f3en prod):- Hallazgo durante deploy:
composer require laravel/sanctumtambién bumpeóphpoffice/phpspreadsheeta 5.7.0 que ahora requiereext-gd. La VM no teníaphp8.3-gdinstalado → composer install bloqueado. Sergio instalóphp8.3-gdconsudo 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=apimuestraapi/viajes,api/sitios,api/whoami.curl -X POST /api/viajessin token →{"message":"Unauthenticated."}✓
- Hallazgo durante deploy:
- 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 sobrecreado_por_iddel payload). - Mapping
telegram_user_id ↔ amadeus.usuarios.idvive enidentitydel bot, no en amadeus. Sergio (slugsergio) →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.phpdesdeViajesController::guardarViaje, con elevent(new ViajeCreado(...))dentro del service para que web y API disparen lo mismo. EndpointPOST /api/viajescon Sanctum +?dry_run=1. EndpointPOST /api/sitiosparalelo conGuardarSitionuevo (reglas extraídas del Nova Resource: nombre req max:255, codigo req unique max:3). Body usacreado_por_idexplícito; mappingtelegram_user_id ↔ usuarios.idvive del lado del bot (amadeus no se entera de Telegram). - Bot: tools
crear_viajeycrear_sitiosiguiendo el patrónpending_confirmationdewrite_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_memorydel bot y un listener del eventoViajeCreado.
- Amadeus: extraer
- 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).