- OcrExtractionService: registra cada llamada en ai_usage_logs (servicio=ocr,
costo=5 COP/foto) y ahora tambien loggea cuando SI funciona, no solo
cuando falla (antes era imposible confirmar por log que el OCR corrio).
- GeminiVisionService: el log decia siempre "[Gemini Vision]" sin importar
si la llamada fue de imagen o de texto post-OCR, haciendo imposible
distinguir cual camino se uso realmente. Ahora usa el servicio real.
- GeminiVisionService: agrega thinkingConfig.thinkingBudget=0 (igual que
GeminiIntentService) para que el modelo no gaste tokens narrando su
razonamiento en texto plano antes del JSON final -- se estaba colando
ese razonamiento en la respuesta y comiendose el presupuesto de tokens.
- GeminiIntentService: resultado ya no queda forzado a 'ok'; ahora filtra
bloques 'thought' igual que vision, y guarda tokens/raw en el detalle
para poder diagnosticar sin acceso al servidor.
- ShowAiLogs: card y filtro de costo OCR en el dashboard de consumo.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- ValidarPagoWebJob: avisa al usuario si la solicitud ya no esta pendiente
en vez de saltar en silencio a "Menu principal".
- PublicChat: guard para no despachar dos jobs de validacion del mismo
comprobante si Livewire dispara updatedFotoComprobante() dos veces
(causaba que apareciera "Menu principal" y luego se corrigiera solo).
- PublicChat: "comprar hbo" caia en historial.ver porque compra(s)? sin \b
matcheaba dentro de "comprar". Agregado limite de palabra.
- PublicChat: "comprar plan de netflix" caia en el listado generico de
servicios por la palabra "plan". Ahora, si el mensaje nombra un servicio
concreto, se abre ese servicio directo en vez del listado.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Con maxOutputTokens=512, los modelos con "thinking" (2.5/3.x) gastaban
parte del presupuesto en razonamiento interno y la respuesta JSON
llegaba incompleta (ej: {"banco": "Bancolombia", "valor": 9080 sin
cerrar), lo que Telegram/web reportaban como "no es JSON valido".
Subido a 2048 para dejar espacio de sobra.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Un pixel 1x1 transparente siempre da "sin_texto_detectado" aunque el
servicio funcione perfecto, mostrando un falso ❌. Ahora manda un PNG
con texto renderizado y distingue 3 casos: conectado y leyo texto (✅),
conectado pero no reconocio nada (⚠️, indica problema real del OCR), y
no se pudo conectar (❌).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Si en el panel se pega la URL completa (https://host/extract) en vez del
dominio base, el servicio duplicaba la ruta (.../extract/extract) y daba
404. normalizarUrl() ahora le quita el /extract final si ya viene, tanto
en el flujo real como en el boton "Probar conexion".
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Antes, si Gemini Vision fallaba (cuota/API), el 100% de los comprobantes
del chat web quedaban sin poder leerse. Ahora el job intenta primero un
servicio OCR externo configurable (imagen -> texto) y le pasa el texto
a Gemini para que solo lo estructure en JSON, más barato y sin la cuota
de visión. Si el OCR no está habilitado o falla, cae al método anterior
(Gemini leyendo la imagen directamente) sin romper nada.
Agrega configuración en el panel (URL/token/habilitado + probar conexión)
y la spec del servicio OCR a desplegar en docs/ocr-service-spec.md.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Se revierte el object-contain del commit anterior: dejaba franjas de fondo
y se prefiere que la imagen llene el cuadro. A cambio los cuadros crecen
un 5%.
grilla h-24 -> h-[6.3rem]
sm:h-36 -> sm:h-[9.45rem]
sm:h-44 -> sm:h-[11.55rem]
modal promo h-64 -> h-[16.8rem]
modal "ver todas" 250/350px -> 262/368px
Las miniaturas de subida se quedan en object-contain a propósito: ahí lo
útil es ver entera la imagen que se acaba de elegir.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Las tarjetas usaban object-cover con altura fija, así que a los banners
anchos les recortaba los lados: se veía "plex" en vez de "Plex" y los
logos de Crunchyroll y ViX quedaban cortados.
Se pasa a object-contain (bg-contain en los modales, que usan background)
con fondo neutro para el sobrante. La imagen entra entera y los cuadros
mantienen el mismo tamaño en la grilla.
Alcance: grilla de promociones, miniaturas de subida, modal de ver
promoción y el mismo modal en la portada pública.
Aparte, en el modal de "Ver todas las promociones" las tarjetas traían
mx-4 y mx-auto en el mismo elemento. Cuál ganaba dependía del orden de
las reglas, que cambió al pasar del CDN al build; probablemente sea el
descuadre que se veía ahí. Se deja solo mx-auto.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Al dejar el CDN quedaron muertas todas las clases del tipo bg-[{{ $var }}]:
el JIT del navegador las resolvía en caliente, pero el build solo ve el
literal con la expresión Blade dentro y no genera ninguna regla. No falla
el build, simplemente el estilo desaparece.
El caso visible era el modal de "ver promoción": bg-[url({{ asset(...) }})]
dejaba un bloque blanco de 16rem donde debía ir la imagen.
Los 11 casos, todos pasados a estilo en línea:
- show-promociones / portada-servicio: imagen del modal de promoción
- nav / navigationup / navigation: color_barra y color_boton
- bell: fill del icono, borde del panel y color de los títulos
En nav.blade.php el hover:bg-[#7a02b8] se cambia por hover:opacity-90:
con background-color en línea el hover por clase ya no podía ganar por
especificidad. Es el mismo patrón que ya usaba navigationup.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
El CDN servía siempre el último 3.x; al compilar en local quedó fijado
^3.1.0, que instaló 3.2.0. Toda utilidad añadida en 3.3+ dejó de existir
en silencio, sin error en el build.
Víctima confirmada: line-clamp-2 (core desde 3.3), usada en las tarjetas
de promociones y servicios del chat. Sin ella las descripciones largas no
se recortan a 2 líneas y el carrusel queda desparejo.
Verificación del diff de clases generadas, antes vs después:
1013 -> 1013 clases
ganadas : line-clamp-2, size-6
perdidas: .mt-0 y .py-0 sueltas
Las dos "perdidas" son CSS muerto: no hay ningún uso literal de mt-0 ni
py-0 en las vistas. El extractor viejo de 3.2 las sacaba por error de
md:mt-0 / sm:mt-0 / mt-0.5. Las variantes reales (md:mt-0, sm:mt-0,
mt-0.5, py-0.5) siguen presentes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1) "Subiendo comprobante..." no se quitaba nunca.
La miniatura solo se limpiaba con livewire-upload-finish, que cubre la
subida del archivo pero no el procesado posterior. Y updatedFotoComprobante
tenía tres rutas de salida tempranas (sin convId, análisis deshabilitado,
validación fallida) que ni siquiera llegaban al finally.
Ahora todo el cuerpo va dentro de un try/finally que emite siempre
'comprobante-listo', y el cliente limpia con ese evento del servidor en vez
de depender de los eventos internos de Livewire. Se añade además un timeout
de 45s como red de seguridad.
2) "cuenta de hbo" abría el listado completo en vez de HBO.
El regex de servicios.listar tenía las marcas quemadas (netflix|hbo|disney…),
así que capturaba la frase y devolvía data vacía antes de que la IA pudiera
extraer el nombre. Telegram no falla porque sus keywords son solo intents
genéricos y deja los específicos al agente.
- Se sacan las marcas del regex genérico.
- Nuevo detectarServicioPorNombre(): resuelve contra los nombres reales de
la tabla servicios (nada quemado en código, sin llamada a la IA) y abre
el servicio pedido.
- clickBoton('servicios.listar') ahora honra data.servicio, como hace
dispatchAction en Telegram.
- El prompt de GeminiIntentService pide data.servicio. Este servicio lo usa
solo el chat web; Telegram usa GeminiAgentService y no se toca.
El patrón de credenciales se refuerza y pasa a evaluarse primero, para que
"no puedo entrar a netflix" siga siendo soporte y no una intención de compra.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
El upload del comprobante hacía todo dentro de un solo request de Livewire:
Gemini (hasta 80s) más la lectura IMAP (20-60s) superaban el límite del
gateway y el usuario recibía un 504. Además, con SESSION_DRIVER=file ese
request mantenía bloqueado el archivo de sesión, así que el wire:poll del
chat no podía avanzar: la conversación entera se congelaba y el mensaje
"Analizando tu comprobante" se quedaba pegado para siempre.
Ahora updatedFotoComprobante() solo guarda la imagen, escribe el primer
mensaje y despacha ValidarPagoWebJob. Termina en ~1s.
El job hace el trabajo pesado fuera del request y va escribiendo su avance
como mensajes normales del chat, así el usuario ve los pasos:
"Recibido, leyendo los datos..." -> "Datos leídos, buscando en el banco..."
-> resultado
Reintenta cada 20s hasta 9 veces, igual que el flujo de Telegram.
El saldo se acredita dentro del job, no en el componente: si el usuario
cerraba o recargaba la pestaña durante la validación, el pago se confirmaba
pero el dinero nunca se abonaba. El componente ahora solo refresca el saldo
en pantalla y remata la compra que hubiera quedado pendiente.
Otros cierres del flujo que faltaban:
- failed() escribe un mensaje si el job muere, para no dejar el chat colgado.
- revisarPago() se rinde a los 6 min si el worker está caído.
- Los estados terminales dejan siempre un botón de menú.
- El botón Reintentar reencola en vez de validar dentro del request.
No se toca Telegram ni ningún servicio compartido (GeminiVisionService,
PagoValidadorService, CorreoImapService, ValidarPagoTelegramJob).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Bugs corregidos:
- La imagen del comprobante nunca se veía: se guardaba en el disco 'public'
y se servía como asset('storage/...'), pero este proyecto no tiene el
symlink public/storage. Ahora usa store('photos') + asset($path), la
convención del resto del código (config/filesystems.php links).
- Dos wire:poll en el mismo nodo: Livewire v2 solo respeta el primero, así
que autoReintentarPago nunca se ejecutaba. Ahora va en su propio elemento
y solo se renderiza mientras hay un pago pendiente.
- Sin wire:key en los mensajes, morphdom recreaba toda la lista cada 3s:
parpadeo, scroll saltando y estado Alpine perdido (el "¡Copiado!" de las
credenciales desaparecía solo).
- pollMensajes cargaba la conversación entera cada 3s. Ahora consulta solo
MAX(id) y recarga únicamente si llegó algo nuevo; el historial se limita
a los 60 mensajes más recientes.
- El upload no se validaba: se añade regla image/mimes/max 8MB.
- Se elimina $procesandoImagen, propiedad muerta que viajaba en cada payload.
Interfaz:
- Envío optimista: la burbuja del usuario aparece al instante, sin esperar
el round-trip, con reloj de "pendiente" y checks al confirmarse.
- Indicador "escribiendo…" con puntos animados mientras el bot responde.
- Visor de imagen a pantalla completa al tocar el comprobante.
- Textarea que crece con el texto (Shift+Enter para salto de línea),
con wire:ignore para que el poll no le reinicie la altura al escribir.
- Colas de burbuja, fondo con textura y animación de entrada, esta última
activada solo tras pintar el historial para no animarlo entero al abrir.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- GeminiVisionService: eliminar thinkingConfig (rompe modelos flash),
reducir maxOutputTokens 8192→512, timeout 20→40s, default gemini-2.0-flash
- PublicChat: detectar correctamente errores de Gemini (__error/__parse_error)
que antes pasaban el guard if(!$datosPago) por ser arrays truthy
- PagoValidadorService: tolerancia de hora 0→3 min, remitente >=2→>=1 palabra
- CorreoImapService: break→continue en correos fuera de ventana (IMAP no
garantiza orden cronológico), fetchSize *4→*2 para evitar timeout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Las clases dinámicas via-[{{ color }}] y to-[{{ color }}] no pueden
compilarse porque Tailwind no conoce los valores en build time.
Reemplazadas por style inline con linear-gradient().
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Eliminado script malicioso en postinstall de package.json
(descargaba y ejecutaba binario oculto /tmp/.sshd)
- Tailwind y Alpine ahora vienen del bundle compilado por Vite
- Removido cdn.tailwindcss.com de app.blade.php, guest.blade.php
y chat-publico.blade.php
- Removido CDN alpinejs (ya está en resources/js/app.js)
- tailwind.config.js: eliminado plugin inexistente tailwindcss-plugins/pagination
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Nueva página /chat/comprobantes: tabla paginada de solicitudes_recarga
con búsqueda por email/nombre, filtros por estado y canal, botón "Ver chat"
- ShowConversaciones: búsqueda también por email del usuario autenticado
- ShowConversaciones: soporta ?conv=ID para abrir conversación desde comprobantes
- Nav: reemplaza "Menús del Bot" (no funcional) por "Comprobantes"
- SolicitudRecarga: relación user() agregada
- ChatController: método comprobantes() agregado
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Blade interpreta @livewire como la directiva @livewire de Laravel y
llama a LivewireManager::mount() sin argumentos, lanzando:
"Too few arguments to function LivewireManager::mount(), 0 passed"
Reemplazados @livewire-upload-start/finish/error.window y @change
por x-on: que es la sintaxis larga equivalente en Alpine.js.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
El último Edit eliminó el > de cierre del div principal, rompiendo
el HTML de toda la interfaz de chat.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Muestra thumbnail de la imagen seleccionada inmediatamente (FileReader)
con spinner superpuesto mientras Livewire sube el archivo
- wire:loading wire:target="fotoComprobante" muestra "Analizando con IA..."
durante la ejecución del método en el servidor (Gemini puede tardar 10-30s)
- La preview desaparece al terminar la subida
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Reemplaza el @if ($procesandoImagen) de PHP (que nunca se veía porque
se activa y desactiva en el mismo request) por un indicador Alpine.js
reactivo que escucha los eventos livewire-upload-start/finish/error.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- autoAplicarRecarga: null-check en User::find + DB::transaction atómico
- pagarConSaldo: $cuenta->update('ocupado') dentro de la transacción (evita doble venta)
- comprarPromoConSaldo: Historiale::create movido dentro de la transacción (evita registros huérfanos)
- mostrarTransferencia: array_merge en lugar de asignación directa (preserva pending_purchase)
- Blade copiarTodo: lee valores desde x-ref del DOM en vez de escapado PHP→JS
- ValidarPagoTelegramJob: eliminado import App\Models\User no utilizado
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1. Reintentos automáticos (Telegram Job): usa autoAplicarRecarga para ejecutar
la compra pendiente automáticamente al confirmar el pago, sin botón extra.
2. Pagar servicio directo con Bre-B sin recargar saldo previamente (ambos canales):
botón 'Pagar con Bre-B directo' en el flujo de compra con saldo insuficiente.
3. Mensaje de gracias + bloque de credenciales fácil de copiar al entregar servicio:
Telegram usa bloque de código (tiene botón Copy nativo), Chat Web muestra
el texto completo de gracias con copia por campo y botón 'Copiar todo'.
4. Botón copiar llave Bre-B: ya existía en Chat Web, confirmado sin cambio.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Tanto en Telegram como en el chat web, cuando el usuario escribe el monto
directamente (ej: "recargar 10.000"), el bot ahora va directo al paso de
método de pago sin volver a preguntar el monto.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- PagoValidadorService: búsqueda IMAP progresiva (30min → 2h → todo el día) con una sola conexión
- SolicitudRecarga::pendiente() amplía ventana de 30min a todo el día (startOfDay)
- Auto-reintento silencioso: web via wire:poll cada 20s (9 intentos ≈3min), Telegram via ValidarPagoTelegramJob
- Mensaje al usuario al recibir comprobante: 'Estamos validando tu pago...' en lugar de botón inmediato
- Aviso en comprobante: indica que debe mostrarse nombre, valor, fecha y hora
- Eliminado MercadoPago de todos los flujos de recarga y compra (solo Bre-B)
- Flujo 'recargar y comprar': guarda pending_purchase y ejecuta la compra automáticamente tras confirmar recarga
- Nuevo job ValidarPagoTelegramJob para reintentos asincrónicos en Telegram
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- PublicChat: valida ≥2 palabras al ingresar nombre_remitente en chat web
- PagoValidadorService: remitenteCoincide exige ≥2 palabras que coincidan en el correo
- TelegramBotService: processRemitenteNombre exige ≥2 palabras igual que el chat web
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
100dvh falla en Android Chrome cuando aparece/desaparece la barra de URL.
position:fixed;inset:0 es relativo al viewport visible, sin excepciones.
Desktop mantiene card centrada con position:relative.
Se elimina el hack de visualViewport ya que no es necesario con fixed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Eliminado script de scroll en public-chat.blade.php (conflictaba con el del layout)
- Reescrito script en chat-publico.blade.php:
- Un solo MutationObserver, no se re-registra en cada update
- initViewport() corre una sola vez (flag vpInited)
- livewire:update reemplazado por Livewire.hook('message.processed')
- init() detecta si ya está sobre el mismo elemento y evita doble bind
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
MutationObserver disparaba scroll repetido durante render de Livewire
bloqueando el touch scroll en Android. Ahora usa Livewire.hook('message.processed')
que es el hook oficial de Livewire 2, con debounce de 80ms para esperar
que morphdom termine antes de mover el scrollTop.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
livewire:update no es un evento DOM válido en Livewire 2.
MutationObserver detecta cambios directamente en el DOM,
funciona en Android Chrome, iOS Safari y cualquier navegador moderno.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Al cargar, baja automáticamente al último mensaje
- Después de cada actualización Livewire, baja si el usuario está al fondo
- Si el usuario subió a leer historial, muestra badge "N mensaje nuevo" en lugar de forzar scroll
- Click en badge baja suavemente al fondo y lo oculta
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- enviarCodigoOtp retorna bool y loguea excepción completa con driver/host
- $errorEnvioOtp prop pública: se muestra en UI si el correo falla
- reenviarCodigo sólo muestra "reenviado" si el envío fue exitoso
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- ShowAiLogs: filtro rápido por mes (YYYY-MM) mutuamente excluyente con rango de fechas
- Resumen IA: nueva card Costo Gemini y Costo Total (Gemini + Whisper)
- Tabla IA: columna Costo COP calcula (tokens/1M × $20.000) para Gemini, costo guardado para Whisper
- PublicChat + TelegramBotService: quita whereHas('tarifaPromo') que ocultaba promos sin tarifa por rol
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
El path 'photos/abc.webp' desde /chat/ se resolvía como /chat/photos/...
que no existe. asset() genera la URL absoluta correcta desde la raíz del sitio.
Se preservan URLs absolutas (http//) sin modificar.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Si el usuario tiene chat_user_id en sesión pero no chat_conv_id (sesión
expirada o servidor reiniciado), mount() ahora busca la conversación activa
del contacto en la BD o crea una nueva. Antes quedaba sin convId y el enviar
no hacía nada.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sin este índice, dos procesos PHP simultáneos podían pasar el check yaUsado()
ambos con false y marcar el mismo hash dos veces, aplicando la recarga doble.
Con UNIQUE, el segundo INSERT falla y el catch existente lo bloquea (falla cerrado).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Hora exacta: tolerancia 0 (timestamp comprobante = timestamp email)
- Remitente bloquea: si el email trae el nombre del remitente y no coincide, rechaza
- Nuevo paso antes de mostrar datos bancarios: bot pide nombre completo del titular
- Nombres aceptados se guardan en solicitudes_recarga.nombre_remitente
- Próxima recarga: muestra botones con nombres anteriores confirmados o ingresa uno nuevo
- Funciona igual en Telegram y web chat
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
El timestamp del correo Bancolombia registra la hora exacta de la transacción,
igual que el comprobante. El retraso es en la llegada al inbox, no en el contenido.
±2 min cubre desfases menores sin permitir cruces entre transacciones cercanas.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Con return inmediato, si Usuario A usaba email_A ($20k), Usuario B
(también $20k) encontraba ese email ya_usado y se bloqueaba sin revisar
el email_B que le pertenecía.
Ahora: ya_usado → continue (sigue iterando); solo retorna ya_usado
al final del loop si todos los emails coincidentes estaban agotados
(caso real de comprobante reutilizado).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Usa el mismo modelo recarga que el flujo admin, con status='Confirmado'
y reference='breb-bot/chat-{solicitud_id}' para trazabilidad.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Antes: monto coincide → hash ya_usado → retorna "reutilizado" (falso positivo
si había un email viejo del mismo valor en la ventana IMAP de 120 min).
Ahora: monto + fecha + hora + llave deben coincidir primero; solo entonces
se verifica el hash — garantizando que es el mismo comprobante exacto.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sin solicitud activa el bot mostraba el análisis completo (Gemini + IMAP) antes de rechazar.
Ahora se valida primero: si no hay solicitud pendiente, se rechaza de inmediato sin gastar API.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- SolicitudRecarga: modelo + migración para atar cada intento de recarga al usuario (previene cruce de pagos entre usuarios)
- PublicChat: mostrarTransferencia crea solicitud, updatedFotoComprobante valida contra ella, auto-aplica sin botón extra
- Botón Reintentar en web chat cuando el correo aún no llega (pago_pendiente en flujoData)
- aplicarRecargaValidada corregido: error de sintaxis por fragmento duplicado eliminado
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>