# FIX 2026-09-10 — Llamadas aparcadas se pintaban en verde ("Atendida") en `/pbx/realtime-monitor2.php`

**Servidor**: deselpa.vozsi.com (138.68.97.77), Asterisk 22, panel PHP propio.
**Aplicado**: jueves 10-09-2026 11:18 CEST. **Verificado en producción**: 11:30-11:36 CEST (David: "funciona bien").
**Ficheros del parche y driver**: `C:\Users\A1\deselpa-inv\parking-20260910\` (orig/, new/, diff/, `aplicar_parking_20260910.sh`, README.md).
**Backups en el servidor**: sufijo `.bak_parking_20260910` junto a cada fichero.

---

## 1. Síntoma

Cuando una llamada se aparca (no hay coordinador libre) y nadie la captura, al cabo de unos minutos el monitor la mostraba en **verde** como "Atendida", como si la hubieran cogido. La llamada seguía en el slot con música y se podía capturar con `*8N`, pero visualmente parecía atendida. Petición: rojo (`Parking - *8N`) mientras nadie la coja, indefinidamente; verde/azul solo si la atienden.

## 2. Causa raíz (no era ningún timeout de Asterisk)

`parkingtime` está en **7200 s** (`/etc/asterisk/res_parking.conf:88`, subido de 45 s a 2 h el 07-04-2026). En 30 días de CEL: 285 aparcamientos, máximo 942 s, **0 `ParkedCallTimeOut`**. El "timeout" era el cron de limpieza:

1. `/var/lib/asterisk/agi-bin/register_parking_call.py` (llamado desde `[parking-with-moh]`, `extensions_parking_simple.conf:24`) insertaba en `asterisk.parking_calls` con `parked_time = NOW()` en una sesión pymysql **sin `time_zone`** → la BD gestionada de DigitalOcean es UTC → la fila nacía **2 h atrás**.
2. `/var/lib/asterisk/agi-bin/cleanup_parking_slots.py` (crontab root `*/3`) fijaba `Europe/Madrid` y marcaba `abandoned` toda fila `parked/ringing` con más de 60 min. Con el desfase, **toda llamada aparcada se abandonaba en el primer tick, entre 0 y 179 s después de aparcarla** (49 casos en 7 días, media 96 s), y además ponía `active_calls.status='completed'`.
3. El monitor (`/var/www/html/pbx/includes/realtime_functions_corrected2.php`, `get_all_active_calls_monitor2()`) solo hacía JOIN con `parking_calls` si `status IN ('parked','retrieved')`. Al perder la fila caía en la rama `has_active_bridge → 'Atendida'`, porque el canal seguía dentro del holding bridge del parking. El JS de `realtime-monitor2.php` pinta verde para "Atendida".

El "fix TZ 2025-10-30" se había aplicado solo al lado del cleanup, no al del registro, y eso invirtió el bug: en vez de no abandonar nunca, abandonaba lo vivo en segundos.

### Daños colaterales que también arregla este parche
- **Whisper perdido al capturar**: `GET_PARKED_WHISPER` (func_odbc.conf) exige `status='parked'`; tras el flip devolvía vacío. En 3 días, 9 de 16 capturas sin whisper.
- **`/root/mark_parking_retrieved.py` roto desde 10-2025**: `import MySQLdb` (solo hay pymysql). Ninguna captura escribía `retrieved`/`retrieved_by`; el estado azul "Con coordinador" no aparecía nunca. Su log `/var/log/parking_capture_mark.log` estaba congelado en 25-10-2025.
- Histórico falseado: de 599 "abandoned" en `parking_history` (60 días), 355 (59 %) fueron en realidad capturadas según CEL. Los informes de parking de esos meses no valen.

## 3. Cambios aplicados

| Fichero | Cambio |
|---|---|
| `/var/lib/asterisk/agi-bin/register_parking_call.py` | Conexión vía `mysql_connection.get_connection()` (sesión `Europe/Madrid`). `parked_time`/`last_used` en hora local. Resto idéntico. |
| `/var/lib/asterisk/agi-bin/cleanup_parking_slots.py` | Reescrito manteniendo estructura. Fuera la regla "60 min". Cierra una fila `parked/ringing` SOLO si el canal está muerto: (1) CEL `PARK_END` posterior al último `PARK_START` del uniqueid → `ParkedCallUnparked` = `retrieved` (+`retrieved_by` del `extra.retriever`, nombre, `wait_time_seconds`), `ParkedCallGiveUp` = `abandoned`, `TimeOut` = `timeout`; (2) sin PARK_END, consulta `parking show default` + `core show channels concise`: canal inexistente → `abandoned`; sigue aparcado → se mantiene; Asterisk no responde → no decide. (3) Red de seguridad por edad SOLO por encima de `parkingtime` (leído del CLI) + 15 min. Repara filas `abandoned` recientes que sigan en `parking show` → `parked`. `active_calls`/`active_manager_calls` → `completed` solo para `abandoned/timeout`. `move_old_to_history` sin cambios. Flag `--dry-run` real. Logger `cleanup_parking` (desaparecen las líneas duplicadas). |
| `/root/mark_parking_retrieved.py` | `MySQLdb` → `mysql_connection`. Escribe `status='retrieved'`, `retrieved_by`, `retrieved_by_name`, `retrieved_time`, `wait_time_seconds`. Asterisk corre como root, System() lee `/root` sin problema. **Decisión**: no inserta en `coordinator_calls` (contradecía el fix 2025-10-15 de `event_handler`; el monitor coge el coordinador de `retrieved_by`). Reactivable con `REGISTER_COORDINATOR_CALLS=True`. |
| `/var/www/html/pbx/includes/realtime_functions_corrected2.php` | Solo `get_all_active_calls_monitor2()`, entre marcadores `PARKING_20260910_QUERY_BEGIN/END`. Subquery `cel_park` = `PARK_START` en 2 h sin `PARK_END` posterior (índices `idx_cel_eventtype_eventtime` + `uniqueid_idx`). Rama CASE `park_open=1 AND retrieved_by IS NULL → 'Parking - ' + capture_extension` (o `*80`) ANTES de cualquier rama "Atendida". JOIN a `parking_calls` = fila más reciente del uniqueid SIN filtrar por status. `'Transferida'` antes que `'Con coordinador'`. Query medida: 127-135 ms. |

**No se tocan**: `realtime-monitor2.php` (JS), `func_odbc.conf`, `res_parking.conf`, dialplan. No hace falta reload de Asterisk (scripts por proceso, PHP por petición).

## 4. Verificación en producción (10-09-2026)

Primer ciclo (11:19-11:23): AJAX `ajax_monitor_data2.php` 100 % 200 (1091 peticiones hasta 11:40, 1 × 401 de sesión caducada), query 127 ms, tick cron 11:21 ejecutó el cleanup nuevo sin errores.

Prueba de punta a punta (DID 910762146 "Deselpa Sala", llamante 605…697):

| Hora | Evento | Resultado |
|---|---|---|
| 11:30:40 | PARK_START slot 701 (fila 8726, `parked_time` **11:30:40 local**, antes habría sido 09:30) | Monitor rojo `Parking - *81` |
| 11:30:53 | `*81` por dslpa208 (CAROL) | `mark_parking_retrieved.py` escribe `retrieved`, `retrieved_by=208`, `wait 13 s`; `parking_capture_mark.log` vuelve a moverse por primera vez desde 25-10-2025; CEL `PARK_END ParkedCallUnparked`. Monitor azul "Con coordinador". |
| 11:31:51 | PARK_START slot 701 (fila 8727) | Rojo |
| 11:33:02 | Tick cron | "Slots ocupados: 1, Total llamadas parkeadas: 1" → **se mantiene `parked`** (antes: `abandoned` + verde) |
| 11:35:20 | El cliente cuelga (CEL `PARK_END ParkedCallGiveUp`) | Desaparece del monitor |
| 11:36:02 | Tick cron | `abandoned via CEL PARK_END (ParkedCallGiveUp)`, `wait_time_seconds=209`, `abandoned_time=11:35:20` |

El whisper de la captura salió vacío porque ese DID no tiene `whisper_file` en `destinations` (esperado, no regresión). En DIDs con whisper vuelve a sonar porque la fila ya no se abandona.

## 5. Operativa

- **Verificar**: `cd /mnt/c/Users/A1/deselpa-inv/parking-20260910 && ./aplicar_parking_20260910.sh verify`
- **Revertir**: `ASSUME_YES=1 ./aplicar_parking_20260910.sh revert` (restaura los `.bak_parking_20260910`, comprueba md5).
- **Dry-run del cleanup en el servidor**: `/usr/bin/python3 /var/lib/asterisk/agi-bin/cleanup_parking_slots.py --dry-run`
- Logs: `/var/log/asterisk/parking_cleanup.log`, `/var/log/parking_capture_mark.log`, `/var/log/asterisk/register_parking.log`, `/var/log/php_errors.log` (los "[API] JSON Parse Error" son ruido preexistente de `panel/api/api_base.php`, no del monitor).
- El driver acepta `ASSUME_YES=1` porque `!` en Claude Code no tiene stdin interactivo. Los originales del servidor van en CRLF; `new/` se mantuvo en CRLF para que los diffs sean reales.

## 6. Pendiente / no tocado

- `/etc/cron.d/parking_cleanup` (apunta a `/root/cleanup_parking_slots.py`, duplicado con el mismo bug de TZ, actualmente no se ejecuta): retirar con `cron_parking_cleanup_disable.sh disable` (renombra a `.disabled` con backup).
- Histórico `parking_history`: 355 "abandoned" que fueron capturas; decidir si se reconcilia con CEL o se deja.
- Preexistentes: carrera de slot en `register_parking_call.py` (dos aparcamientos simultáneos), `mark_parking_retrieved` corre antes de `ParkedCall()`, `transfer_processor.py` usa la columna inexistente `parked_at`, re-aparcar una llamada que estuvo con coordinador se pinta azul por `detect_coordinator_from_cel`.
- `active_calls` mezcla TZ (`start_time` local, `answer_time`/`end_time` UTC vía `utcfromtimestamp` en `event_handler.py`).
- `/etc/asterisk/extensions.conf\r` (nombre con CR) junto al real; `monitor-pending-events.service` en `failed`.
- Opción C (parking infinito real con `comebacktoorigin=no` + re-aparcado) NO aplicada: con 7200 s no se ha dado nunca el caso.
