# 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](../scripts/ghl_browser_workflow_manager.py); la sesión se genera con [scripts/ghl_browser_session_generator.py](../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](../start_persistent_profile.bat).
- **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`:
   ```cmd
   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`:
  ```cmd
  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:
```cmd
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)

```cmd
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)

```cmd
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:

```cmd
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](../db.py) 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.
