Files
MP-Manager/docs/PLAYWRIGHT_SESSION.md
T
urieljareth 2a37a4ffbf Añade launchers de macOS/Linux con un click y hace runtime_control cross-platform
Equivalentes .command (doble-clic en Finder) de los .bat de Windows:
- setup_mac.command: bootstrap con un click (detecta Python 3.10+, crea .venv,
  instala requirements + Chromium de Playwright, copia .env.example -> .env).
- start/stop/restart/start_persistent_profile.command: espejo de los .bat,
  lanzan el server con nohup usando el python del .venv.
- mp_common.sh: helper compartido (raíz, venv, banners).

runtime_control.py ahora es cross-platform (IS_WINDOWS): lsof/ps/pgrep/kill en
POSIX, netstat/PowerShell/taskkill en Windows. _kill_tree_posix mata el árbol
padre+worker de uvicorn con SIGTERM.

.venv/ añadido a .gitignore. Docs actualizadas (CLAUDE.md, AGENTS.md,
PLAYWRIGHT_SESSION.md).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 15:02:47 -06:00

12 KiB

Sesión persistente de Bucéfalo en Playwright

Este documento explica cómo funcionan los scripts del repo que automatizan la UI de Bucéfalo CRM con Playwright (toggle de workflows, renombrar, eliminar, etc.), por qué a veces se cae la sesión, y cómo recuperarla.

Audiencia: quien mantiene scripts/ghl_browser_*.py o usa el dashboard para mutar workflows. Si alguien (humano o Claude) llega aquí buscando "por qué falla el delete/toggle/rename de workflows", este es el lugar.


Por qué necesitamos Playwright

La API oficial de GHL (services.leadconnectorhq.com) no expone todas las operaciones — en particular:

  • Toggle Borrador ↔ Publicado de un workflow
  • Renombrar un workflow
  • Eliminar un workflow

Para esas tres acciones, los scripts automatizan la UI web con Playwright sobre Chromium en modo headless. Toda la lógica vive en scripts/ghl_browser_workflow_manager.py; la sesión se genera con scripts/ghl_browser_session_generator.py.


Dos modos de sesión

Los scripts soportan dos modos de persistencia. Se elige con la variable de entorno GHL_BROWSER_PROFILE_DIR.

Modo 1 — Shared storage_state (por defecto)

  • Cuándo se usa: cuando GHL_BROWSER_PROFILE_DIR no está definida (el caso normal).
  • Cómo funciona: el archivo generated/browser/session.json contiene cookies + localStorage. Cada script abre un browser limpio con esas cookies, hace lo suyo, y guarda las cookies actualizadas de vuelta en generated/browser/session.json.
  • Pros: simple, soporta scripts en paralelo (el archivo se lee al inicio y se escribe al final, no hay locks).
  • Cons: GHL puede interpretar cada arranque como un "navegador nuevo" porque no hay IndexedDB ni cache compartido. Si GHL invalida la sesión por detectar dispositivos múltiples, este modo se cae más rápido.

Modo 2 — Perfil de Chrome persistente

  • Cuándo se usa: cuando defines GHL_BROWSER_PROFILE_DIR apuntando a un directorio. Lo más fácil es lanzar el servidor con start_persistent_profile.bat (Windows) o start_persistent_profile.command (macOS/Linux).
  • Cómo funciona: Playwright usa launch_persistent_context() con un perfil completo en disco — igual que un Chrome real. Persiste cookies HttpOnly, IndexedDB, cache, localStorage, service workers.
  • Pros: GHL trata el perfil como un "dispositivo" estable. Sesión mucho más duradera. Login dura semanas en vez de horas/días.
  • Cons: no puedes correr dos scripts en paralelo contra el mismo perfil — Chrome bloquea el directorio mientras un proceso lo usa. Si necesitas ejecuciones concurrentes (raro en este repo), no uses este modo.

Auto-login con 2FA por correo (IMAP)

Si configuras un archivo .env con las credenciales de Bucéfalo + IMAP de tu correo, el ghl_browser_session_generator.py hace todo el login solo: llena email + contraseña, selecciona "Email" en el selector de método 2FA, lee el código del correo vía IMAP y lo pega.

Configuración

  1. Copia .env.example a .env:
    copy .env.example .env
    
  2. Edita .env y rellena:
    • BUCEFALO_LOGIN_EMAIL / BUCEFALO_LOGIN_PASSWORD — credenciales de tu usuario de Bucéfalo.
    • EMAIL_IMAP_HOST — servidor IMAP de tu correo (típicamente el dominio de tu hosting, ej. c1101854.sgvps.net).
    • EMAIL_IMAP_PORT — 993 (IMAPS) por defecto.
    • EMAIL_IMAP_USER / EMAIL_IMAP_PASSWORD — credenciales de la cuenta de correo a la que llega el código MFA.
  3. Verifica que el .env esté excluido de Mega/Git (el .megaignore y .gitignore del repo ya lo cubren).

Comportamiento

  • Si están todas las credenciales en .env, el session generator corre en headless=True (sin abrir ventana) y completa el login en ~30-60 s.
  • Si falta alguna, cae al modo manual: abre el navegador visible para que tú completes el login.
  • Si quieres forzar el modo manual aunque haya credenciales, usa --no-auto:
    python scripts/ghl_browser_session_generator.py --no-auto
    

Probar el lector IMAP por separado

Si solo quieres validar que IMAP funciona y el parser extrae el código:

python scripts/email_otp_reader.py

Te pide que provoques un código (intenta loggearte a Bucéfalo) y al recibirlo lo imprime.

Seguridad

  • El archivo .env contiene credenciales en texto plano. Manténlo solo en tu equipo local.
  • Si crees que el .env pudo haberse copiado a un lugar no seguro (cloud público, repo público, captura de pantalla), cambia las contraseñas inmediatamente.
  • Los .env.example y .megaignore del repo están diseñados para evitar que esto pase por descuido.

Cómo arrancar cada modo

Modo 1 (default)

start.bat

Si nunca generaste sesión, en el dashboard ve a la pestaña Workflows GHL y dale al botón "Renovar sesión Bucéfalo". Inicia sesión + MFA en la ventana que se abre — el archivo generated/browser/session.json se crea solo.

Modo 2 (perfil persistente)

start_persistent_profile.bat

El .bat:

  1. Setea GHL_BROWSER_PROFILE_DIR=<repo>/generated/browser/profile.
  2. Lanza python main.py con esa variable en el entorno.

La primera vez te toca generar el perfil:

  1. En el dashboard, dale a "Renovar sesión Bucéfalo".
  2. Login + MFA en la ventana.
  3. El perfil queda guardado en generated/browser/profile/. Próximos arranques no piden login.

⚠️ Si quieres volver al modo 1, basta con cerrar el server y abrirlo con start.bat. Los dos modos no se mezclan: el modo 1 usa generated/browser/session.json, el 2 usa generated/browser/profile/.


Síntomas y diagnóstico

"Chromium me cerró la sesión de mi navegador personal"

GHL detectó dos sesiones activas (la tuya y la de Playwright) e invalidó la más vieja.

  • Modo 1: muy probable. Cada arranque "huele" a dispositivo nuevo.
  • Modo 2: poco probable. El perfil persistente se ve como el mismo dispositivo.

Solución: cambia al modo 2 con start_persistent_profile.bat.

"ERROR: No se pudo interceptar la sesión de usuario de GHL"

El script (ya no usa este flujo, pero el mensaje puede aparecer en versiones viejas) o el flujo DOM no pudo cargar la página porque te redirigieron al login.

Solución: renueva la sesión desde el botón del dashboard. Si pasa muy seguido, cambia al modo 2.

"Sesión Bucéfalo: 49.5 h de antigüedad" en el dashboard

El indicador muestra la antigüedad de generated/browser/session.json. Una sesión vieja no garantiza que esté caducada — GHL puede aceptarla. Pero a partir de 24 h, el dashboard advierte.

  • Si la corrida falla, renueva la sesión.
  • Si la corrida funciona, el archivo se actualiza solo (el script refresca cookies al cerrar).

"ERROR de navegador: BrowserType.launch: Executable doesn't exist at …"

Faltan los binarios de Chromium. El script intenta instalarlos solo en el primer fallo:

python -m playwright install chromium

"El cambio NO persistió en GHL"

Tras un toggle exitoso visual, la API de GHL sigue devolviendo el estado viejo. Causas:

  • El workflow tiene un trigger inválido (Webhook sin URL, etc.) y GHL revierte el cambio en silencio.
  • Bug visual de la UI — el script ya hace un reload + revalida automáticamente.

Si el script reporta este error después de los reintentos, abre el workflow en el navegador, revisa los triggers, y vuelve a intentar.

"Proceso interrumpido (signal …)"

El subprocess de Playwright fue matado externamente — típicamente porque reiniciaste el server o cancelaste el task desde el dashboard. El handler de señales:

  • Cierra Chromium limpio (sin zombies).
  • Refresca cookies en generated/browser/session.json.
  • Consulta la API y te dice si el cambio sí se aplicó pese a la interrupción.

Si fue antes del click del switch: no pasa nada, reintenta. Si fue después: revisa el log — la línea [INTERRUPCIÓN] El cambio SÍ se aplicó te lo dice.


Reglas para mantener los scripts

Si tocas scripts/ghl_browser_*.py:

  1. No abras un browser = p.chromium.launch(...) manual. Usa _open_browser(p) — respeta el modo (shared / persistent).
  2. No cierres con browser.close(). Usa _close_and_save(browser, context) — refresca cookies y limpia el estado del handler de interrupciones.
  3. Registra el browser y context en _INTERRUPT_STATE justo después de abrirlos. Si no, los handlers de interrupción no podrán cerrarlos.
  4. No confíes en la UI para validar mutaciones. La fuente de verdad es la API de GHL. Tras cualquier guardado visual, llama a _verify_status_via_api(...) (o un equivalente) con reintentos.
  5. Selectores frágiles: si GHL cambia el HTML, los selectores hardcodeados (#cmp-header__btn--save-workflow, .n-switch, textos "Eliminar flujo de trabajo"/"Delete workflow") pueden romperse. Tomar screenshot de debug es el primer paso de diagnóstico (_save_debug_screenshot(page, "label")).

Cómo reportar un error útilmente

Cuando un script de Playwright falla y necesitas ayuda (humano o IA), incluir esta información acorta el diagnóstico de horas a minutos:

  1. El error_id completo. Aparece en los logs como error_id=550af7d5-…. Permite consultar error_log con todo el contexto (return_code, comando exacto, últimas 80 líneas de output).
  2. El task_id si lo ves (aparece como (task d819f354-…)). Permite cruzar con script_runs para ver el estado de la auditoría.
  3. Las últimas 20-30 líneas del log que viste. Aunque el error_log ya las guarda, a veces el truncado pierde la línea más informativa.
  4. Si hay screenshots en generated/browser/screenshots/ con timestamp cercano al fallo: nómbralos. Los scripts guardan capturas en cada punto crítico (delete_no_row_*, switch_no_change_*, post_save_state_*, etc.).
  5. El estado de la sesión: ¿cuánto tiene generated/browser/session.json? El dashboard te lo dice arriba de la tabla de Workflows. Si tiene más de 24 h, renueva primero y reintenta antes de pedir ayuda.
  6. Qué intentaste hacer: acción (toggle-status/delete/rename), location_id, workflow_id, y qué esperabas que pasara.

Ejemplo de un buen reporte:

El toggle-status del workflow 67f98059-… en Z64WQKORPVwXb5mn68Ef falló a las 14:03. Log dice error_id=9f58fa5d-… y [ERROR] La tabla de workflows no cargó. Screenshot: generated/browser/screenshots/delete_list_failed_20260523_142713.png. La sesión tiene 12 h, renovada hoy en la mañana.

Con eso ya se puede empezar a diagnosticar sin tener que pedir info de vuelta.

Comportamiento conocido de la UI de Bucéfalo

Documentado también en memory/ghl_ui_quirks.md para sesiones de Claude:

  • El builder de workflows carga dentro de un iframe en client-app-automation-workflows.leadconnectorhq.com. Cualquier selector debe operar sobre ese frame, no sobre la página principal.
  • El listado de workflows está en otro iframe distinto.
  • Al cargar el builder, el switch de Publicar/Borrador arranca en aria-checked=false y luego cambia cuando el cliente recibe el estado real. Esperar 25-30 s antes de leer.
  • El botón "Guardar/Save" dice "Guardado/Saved" por defecto incluso sin cambios. No es señal de estado cargado.
  • Aparece un modal "AI Builder habilitado" con botón "Entendido" que tapa el switch — hay que cerrarlo preemptivamente, y a veces reaparece tras el click. Por eso el script reintenta el toggle hasta 3 veces.
  • Tras "Guardar", GHL puede tardar hasta 20 s en propagar el cambio a la API. La validación contra API hace 6 reintentos con backoff incremental.
  • A veces se necesita un F5 manual para destrabar bugs visuales — el script lo hace automático tras los primeros reintentos.