# FIX 2026-07-08 — El amarillo de cortesía no se veía (reporte hispatel) — filtro + carrera AGI/daemon

**Fecha**: 2026-07-08
**Reporte**: el usuario `hispatel` (colaborador, id 1003) no ve el icono amarillo en el
dashboard cuando una tarotista está en tiempo de cortesía. Verificado después con un caso
en vivo: AURORA (ext 311) colgó y pasó directamente a verde sin pasar por amarillo.
**Estado**: RESUELTO y verificado en producción (PILAR ext 344, 12:43:49).

---

## Qué es el "amarillo de cortesía"

Al colgar una consulta, la tarotista descansa `tiempo_cortesia_minutos` (= 4 min,
`panel_tarot.configuraciones`). Durante esa ventana el panel la muestra con el círculo
**amarillo** (estado calculado `esperando`) y no recibe llamadas. No se guarda en BD como
estado: lo calcula `api/estado_operadoras.php` con:

```
estado_actual = 'disponible' AND ultima_hora_colgada IS NOT NULL
AND TIMESTAMPDIFF(MINUTE, ultima_hora_colgada, NOW()) < 4  →  'esperando'
```

Todo cuelga de que **alguien estampe `tarotistas.ultima_hora_colgada`** al colgar.

## Diagnóstico

### Descartado: diferencia por usuario/rol

- `hispatel` es rol `colaborador` con 43 tarotistas asignadas (`colaborador_tarotistas`);
  el filtro de colaborador solo limita QUÉ tarotistas ve, no sus estados.
- Se ejecutó el endpoint real `estado_operadoras.php` con sesión CLI simulada de hispatel
  y de admin: **JSON idéntico** (mismos estados, misma cortesía).
- Diff del módulo `t_inicio_admin.php` renderizado como admin vs colaborador: solo
  cosmético (notas rápidas, reordenar reservas). `getEstadoColor()` idéntico.

### Causa 1 (UX): el filtro de estados escondía a las 'esperando'

El dropdown `#filtro-estado` del dashboard no tiene opción "esperando" y el filtro
"🟢 Disponible" comparaba `estado === 'disponible'` estricto → una tarotista en cortesía
**desaparecía de la lista filtrada** en vez de verse amarilla.

### Causa 2 (raíz): carrera AGI vs daemon al estampar la hora de cuelgue

Dos escritores de `tarotistas.estado_actual` al colgar:

| Escritor | Cuándo | Qué hace |
|---|---|---|
| AGI `hangup_processor.py` (`_sync_agent_panel_immediate`) | inmediato al Hangup | `estado_actual='disponible'` — **sin `ultima_hora_colgada`** (bug) |
| Daemon `panel-sync-asterisk` (`cron/sync_estados_mejorado.php`, ciclo 3s) | cuando AMI deja de listar al agente | si detecta transición ocupado→disponible, estampa `ultima_hora_colgada=NOW()` |

Caso AURORA (311), llamada real de 2409 s:

```
12:21:38.725  AGI HangupProcessor: UPDATE tarotistas SET estado_actual='disponible' (sin colgada)
12:21:39      daemon: AMI ya no lista a 311; la BD YA dice 'disponible'
              → sin transición ocupado→disponible → NO estampa ultima_hora_colgada
              → colgada se queda en la llamada anterior (10:46) → sin 'esperando' → VERDE directo
```

El amarillo era **intermitente**: solo salía si el daemon ganaba la carrera (p. ej. si el
cuelgue no pasaba por el AGI). No dependía del usuario que mirase el panel.

## Fixes aplicados

### 1. `/var/www/html/panel/modules/t_inicio_admin.php` (~línea 1366)

El filtro "Disponible" ahora incluye también `esperando`:

```js
if (estadoActivo === 'disponible') {
    return estadoOp === 'disponible' || estadoOp === 'esperando';
}
```

- Backup: `t_inicio_admin.php.bak_filtro_esperando_20260708` (mismo directorio)
- Se aplica al recargar el dashboard (el módulo se carga por AJAX). `php -l` OK.

### 2. `/var/lib/asterisk/agi-bin/call_system/processors/hangup_processor.py` (~línea 1130)

`_sync_agent_panel_immediate()`: cuando `nuevo_estado == 'disponible'` (solo ocurre en la
liberación tras cuelgue), el UPDATE añade `ultima_hora_colgada = NOW()`:

```python
if nuevo_estado == 'disponible':
    sql_panel = """
    UPDATE panel_tarot.tarotistas
    SET estado_actual = %s,
        ultima_actualizacion = NOW(),
        ultima_hora_colgada = NOW()
    WHERE codigo_agente = %s AND activo = 1
    """
```

- Backup: `hangup_processor.py.bak_cortesia_20260708` (mismo directorio)
- Solo `hangup_processor.py` llama a esa función con 'disponible'; `agent_processor.py` y
  `trunk_processor(_v2).py` solo la usan con 'ocupado' (inicio de llamada) → sin efecto en
  otros flujos.
- Sin reinicio: el AGI es un proceso nuevo por llamada (`main_call_handler.py`).
- El daemon sigue estampando la hora cuando gana él; ambos escritores son coherentes.

## ⚠️ Zonas horarias — POR QUÉ NOW() ES CORRECTO (no "arreglar")

- `ultima_hora_colgada` y `ultima_actualizacion` son **TIMESTAMP**: MySQL guarda el
  instante en UTC y convierte al leer/escribir según el `time_zone` de cada sesión.
- La BD gestionada (DigitalOcean) va con global `SYSTEM` = **UTC**. La sesión Python del
  AGI no fija tz (→ UTC); el panel PHP fija `SET time_zone='Europe/Madrid'`
  (`panel/config/database.php:68`) igual que el daemon.
- Resultado: `NOW()` desde el AGI escribe el **instante correcto** y el panel lo lee en
  hora de Madrid. Verificado: PILAR colgó 12:43:49 local → colgada leída 12:43:49.
- **NO usar `CONVERT_TZ` ni hora calculada en Python**: en la sesión UTC del AGI grabaría
  la hora de pared de Madrid como si fuera UTC → +2 h de error → tarotistas "en cortesía"
  durante 2 h. Hay comentario en el código advirtiéndolo.

## Verificación

- 12:43:49 — PILAR (344) colgó una llamada real; la liberó el código nuevo:
  `ultima_hora_colgada = 2026-07-08 12:43:49` y la fórmula del panel devuelve
  `esperando` (amarillo). Confirmado por el usuario: "funciona bien".

## Rollback

```bash
cp -a /var/www/html/panel/modules/t_inicio_admin.php.bak_filtro_esperando_20260708 \
      /var/www/html/panel/modules/t_inicio_admin.php
cp -a /var/lib/asterisk/agi-bin/call_system/processors/hangup_processor.py.bak_cortesia_20260708 \
      /var/lib/asterisk/agi-bin/call_system/processors/hangup_processor.py
```

## Pendientes / mejoras opcionales

- Añadir opción "🟡 En cortesía (esperando)" propia al dropdown de estados.
- El emoji de "🟡 En Pausa" confunde: pausa se pinta AZUL (`m--font-info`), no amarillo.
- Caso límite ya conocido: llamadas <3 s que el sondeo AMI no ve; ahora el AGI estampa la
  hora igualmente si procesa el cuelgue, así que el impacto baja aún más.

## Utilidades del diagnóstico (para próximas veces)

- Sesiones PHP del panel: `/var/lib/php/sessions` (cookie `PANEL_TAROT_SESSION`);
  `grep -l <username>` para encontrar la sesión viva de un usuario.
- Ejecutar un endpoint autenticado en CLI: hacer `session_start()` propio ANTES del
  `include` (api_base no pisa la sesión activa) y rellenar `$_SESSION`.
- Log del AGI: `/var/log/asterisk/python_call_system.log` (STEP `AGENT_FREED`,
  `PANEL_SYNC_IMMEDIATE`).
- Log del daemon: `/var/log/panel_tarot/sync_estados.log.1` (el `.1` es el VIVO tras
  logrotate; el daemon conserva el descriptor).
