Skip to content

armangrigo/TrunkView

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

TrunkView

Panel de operador web en tiempo real para Issabel 4 / Asterisk 16 que muestra el estado de cada troncal y todas las llamadas que las atraviesan — atendidas y no atendidas (ocupada, no contesta, cancelada, congestión).

TrunkView — estado de cada troncal y sus llamadas en tiempo real

Por qué existe

TrunkView está enfocado en la visibilidad por troncal: ver, para cada proveedor/carrier, todas las llamadas que lo atraviesan — incluidas las que no se contestan (ocupado, no contesta, cancelada) — reaccionando a los eventos del AMI desde que nace cada canal (Newchannel/DialBegin) hasta que cuelga (Hangup), y manteniéndolas visibles unos segundos con su disposición (lingerMs). Es especialmente útil en discado saliente / call center, donde importa la ocupación y el rendimiento de cada carrier.

No reemplaza a otros paneles

TrunkView no reemplaza a FOP2 ni a otros paneles de operador. FOP2 es una herramienta excelente y completa para la operación (extensiones, agentes, colas, control de llamadas). TrunkView tiene un objetivo distinto —el monitoreo centrado en las troncales— así que podés usarlo de forma independiente o como complemento, corriendo en paralelo sobre el mismo Asterisk.

Arquitectura (pensada para no colgarse con mucho tráfico)

Asterisk AMI ──(1 conexión persistente, solo escucha)──> AmiClient
                                                            │ eventos
                                                            ▼
                                                        CallState  (estado en memoria,
                                                            │        indexado por troncal)
                                                snapshot throttled
                                                            ▼
                        navegadores <──WebSocket (con backpressure)── servidor HTTP
  • Una sola conexión AMI para todo el panel; nunca una por navegador.
  • El panel solo escucha eventos, no interroga a Asterisk en caliente (salvo un CoreShowChannels de siembra al conectar).
  • Un navegador lento nunca frena la ingestión: si su buffer se llena, se le salta el envío y el siguiente snapshot lo pone al día.
  • Reconexión automática al AMI con re-siembra del estado.

Estructura

Archivo Función
src/ami/AmiClient.js Conexión AMI persistente, parser, reconexión, keepalive
src/core/TrunkMapper.js Resuelve SIP/<peer>-<hex> → troncal o extensión
src/core/CallState.js Motor de estado; correlación de llamadas y disposiciones
src/server/index.js HTTP estático + WebSocket + cableado
src/notify/Notifier.js Notificaciones por Telegram (debounce/histéresis/cooldown)
public/ Frontend (grilla de troncales en tiempo real)
test/mockAmi.js Simulador de AMI para desarrollo sin Asterisk
test/*.test.js Tests de la lógica (ver Tests)
scripts/dev.js Arranca simulador + panel juntos (solo desarrollo)
scripts/telegram-chatid.js Helper para descubrir tu chatId de Telegram

Notificaciones por Telegram (opcional)

El panel puede avisar por Telegram. Es solo envío (el bot no recibe comandos), usa el módulo https de Node — sin dependencias externas — y toda la configuración vive en el bloque telegram de config.json.

Puesta en marcha

  1. Creá un bot con @BotFather y copiá el token (algo como 123456789:AA...).
  2. Escribile un mensaje a tu bot desde tu cuenta (Telegram no deja que un bot te escriba primero si nunca interactuaste con él). Para un grupo: agregá el bot y mandá un mensaje en el grupo.
  3. Obtené tu chatId: pegá el token en config.json (telegram.botToken) y corré npm run telegram:chatid. Imprime el/los chatId que le escribieron.
  4. En config.json, bloque telegram: enabled: true, el botToken y tu chatId en chatIds (la lista principal / de fallback).
  5. Guardá. Con el panel corriendo, el cambio se aplica en caliente (ver hot-reload) y te llega un aviso de activación. Al iniciar el panel deberías recibir el mensaje de arranque.

Configuración

"telegram": {
  "enabled": false,
  "botToken": "",                 // secreto -> solo en config.json (que está en .gitignore)
  "chatIds": ["123456789"],       // lista principal de destinatarios (fallback)
  "minIntervalMs": 1000,          // separación mínima entre mensajes (anti-flood)
  "notifications": {
    "startup":     { "enabled": true, "chatIds": [] },
    "trunkDown":   { "enabled": true, "debounceSec": 45, "notifyRecovery": true, "chatIds": [] },
    "amiDown":     { "enabled": true, "chatIds": [] },
    "activeBelow": { "enabled": true, "threshold": 35, "recovery": 50,
                     "sustainSec": 60, "cooldownSec": 900, "graceSec": 120, "chatIds": [] }
  }
}

Cada notificación es on/off independiente y puede tener su propia lista de destinatarios (chatIds). Si esa lista está vacía, usa la principal telegram.chatIds. Así podés, por ejemplo, mandar activeBelow solo a los supervisores y el resto a todo el equipo.

Notificaciones disponibles

Notificación Cuándo se dispara Parámetros
startup Al iniciar el panel (o al reconectar el AMI). Confirma que el bot está bien configurado.
trunkDown Una troncal pasa a caída (UNREACHABLE/Unregistered) o se recupera. debounceSec, notifyRecovery
amiDown Se pierde o se recupera la conexión al Asterisk (el panel deja de ver todo).
activeBelow Las llamadas activas caen por debajo del umbral, o se recuperan. threshold, recovery, sustainSec, cooldownSec, graceSec

Ejemplos de mensajes:

🟢 TrunkView iniciado — AMI: conectado · 26 troncales (24 online / 2 offline) · 55 llamadas activas · 14:32
🔴 Troncal caída: carrier1 · 14:35
🟢 Troncal recuperada: carrier1 · 14:41
⚠️ AMI DESCONECTADO — el panel no ve llamadas · 14:50
📉 Actividad baja: 30 llamadas (umbral <35) · 15:10
📈 Actividad recuperada: 51 llamadas · 15:22

Anti-ruido (por qué no te va a inundar)

  • trunkDowndebounceSec: cuando una troncal cae, el panel espera N segundos; si se recupera antes (un flap del qualify), no avisa. Solo alerta si la caída persiste. La recuperación se avisa con notifyRecovery.
  • activeBelow → histéresis + sostenido + cooldown + gracia:
    • Histéresis: avisa al bajar de threshold, pero para dar "recuperado" tiene que subir por encima de recovery (que debe ser mayor que threshold). Evita el parpadeo si queda rondando el límite.
    • sustainSec: debe mantenerse bajo el umbral ese tiempo seguido (ignora bajones momentáneos entre oleadas).
    • cooldownSec: no repite el aviso antes de ese tiempo.
    • graceSec: ignora los primeros segundos tras conectar (mientras el sistema "levanta").
    • Solo se evalúa con el AMI conectado: si el AMI se cae, la actividad se va a 0, pero eso lo cubre amiDown, no activeBelow.
  • Línea base silenciosa: las troncales que ya están caídas al arrancar no generan avisos; solo se notifican los cambios posteriores.

Cómo elegir el umbral de activeBelow

El umbral tiene que estar claramente por debajo de tu nivel normal de llamadas. Si tu operación ronda ~55 llamadas y ponés threshold: 60, la métrica va a "rebotar" alrededor de 60 y el sostenido nunca se cumple (o si baja, avisa siempre). Ponelo donde una caída signifique un problema real (carrier caído, dialer frenado), p. ej. threshold: 35 / recovery: 50 para un piso de ~55. Recordá: recovery siempre mayor que threshold.

Nota sobre llamadas vs canales. El header muestra llamadas (deduplicadas por Linkedid) y canales (patas). En un dialer cada llamada usa 2 canales (la pata del dialer + la del carrier), así que "canales" ≈ 2× "llamadas". La alerta activeBelow razona sobre llamadas.

Recarga de configuración en caliente

El panel vigila config.json y re-aplica sin reiniciar los cambios de afinado: todo el bloque telegram (umbrales, notificaciones on/off, destinatarios, activar/desactivar), calls.lingerMs y ui.broadcastThrottleMs. Guardás el archivo y listo (lo confirma por consola: [cfg] recargado en caliente).

Los cambios estructurales siguen requiriendo reiniciar (npm start) porque implican reconectar o reabrir puertos: ami (host/puerto/clave), http (puerto/bind) y trunks (modo/exclusiones). Si cambiás alguno de esos, el panel lo avisa por consola indicando que hace falta reinicio.

Probar en local (sin Asterisk)

npm install
node scripts/dev.js      # simulador + panel en http://localhost:8088

O por separado: npm run mock en una terminal y npm start en otra.

Tests

Tests de la lógica, sin dependencias ni Asterisk (usan datos ficticios, un reloj inyectado y un transporte de Telegram falso — no tocan el AMI real ni envían nada):

node test/notify.test.js     # notificaciones: debounce, histéresis, cooldown, ruteo, totales
node test/vicidial.test.js   # escena ViciDial: dirección, carrier usado, disposición
# o vía npm:
npm run test:notify

Cada uno imprime OK/FAIL por caso y TODO OK al final, y sale con código 0/1 (usable en CI).

Dónde correr el panel: en el Issabel o fuera (LAN / VPN)

El panel no tiene que correr en el Issabel. Lo único que necesita es alcanzar el puerto del AMI (5038) de tu Asterisk. Puede correr en cualquier máquina (Windows/Linux) con Node 18+ en la LAN, e incluso en otra red unida por VPN.

Seguridad: el AMI viaja sin cifrar

El protocolo AMI es texto plano (usuario, clave y todos los eventos). Nunca expongas el puerto 5038 a internet. Opciones seguras:

  • Misma LAN / VPN privada (recomendado): el panel llega al Issabel por IP privada.
  • Túnel SSH (si el panel está remoto): ssh -L 5038:127.0.0.1:5038 root@ISSABEL y en config.json ami.host: "127.0.0.1". El AMI viaja cifrado dentro del SSH y no hace falta abrir el 5038.

Los dos "caminos" a tener en cuenta

Son dos conexiones distintas, no las confundas:

  1. Panel → AMI del Issabel. La hace solo la máquina donde corre el panel. Por eso el permit del usuario AMI solo tiene que autorizar la red donde está el panel, no la de los operadores. En config.json: ami.host = IP del Issabel.
  2. Navegadores → web del panel (puerto 8088). Acá entran todas las redes desde donde miren los operadores. No requiere tocar nada de Asterisk: solo que haya ruteo hacia la máquina del panel y que su firewall permita el 8088.

Ejemplo: dos LANs unidas por VPN

Escenario real: Issabel en 10.0.0.36, y dos redes 10.0.0.0/24 y 10.9.0.0/24 unidas por VPN. Querés que los operadores accedan desde ambas redes.

  • config.json: ami.host: "10.0.0.36" (la IP del Issabel).
  • Usuario AMI (manager_custom.conf): autorizá la(s) red(es) donde pueda estar la máquina del panel. Autorizar ambas es inofensivo y a prueba de futuro:
    permit = 10.0.0.0/255.255.255.0
    permit = 10.9.0.0/255.255.255.0
  • Acceso web desde ambas redes: no requiere config de Asterisk. Con que la VPN rutee entre las dos redes y el firewall de la máquina del panel deje entrar el 8088, los navegadores de las dos LANs abren http://IP-DEL-PANEL:8088/. (En Windows, habilitá el 8088 entrante en el Firewall.)

El permit compara contra la IP de la máquina del panel (la única que conecta al AMI). Asegurate de que la máscara cubra su IP real: /24 (255.255.255.0) cubre 10.9.0.x; si la red es más grande usá la máscara correcta (p. ej. /16 = 255.255.0.0).

Despliegue paso a paso

  1. Crear un usuario AMI de solo lectura en /etc/asterisk/manager_custom.conf. El permit autoriza la red de la máquina del panel (ver Dónde correr el panel):

    [panelop]
    secret = una_clave_larga
    deny = 0.0.0.0/0.0.0.0
    permit = 127.0.0.1/255.255.255.255   ; si el panel corre EN el Issabel
    ;permit = 10.0.0.0/255.255.255.0     ; o la red LAN donde corra el panel
    read = system,call,reporting          ; solo monitoreo (eventos + CoreShowChannels/SIPpeers)
    write = system                        ; el panel no origina llamadas

    Luego: asterisk -rx "manager reload" (no corta llamadas) y verificá con asterisk -rx "manager show user panelop".

  2. Copiar el proyecto a la máquina donde va a correr (el Issabel u otra con acceso al puerto 5038) e instalar Node 18+ (npm install).

  3. Crear config.json (copia de config.example.json) y ajustar:

    • ami.host: IP del Issabel (o 127.0.0.1 si corre en el mismo servidor).
    • ami.username / ami.secret (los del paso 1).
    • telegram: opcional (ver Notificaciones).
    • trunks / calls.inboundContexts: normalmente no hace falta tocarlos (autodescubrimiento y heurística; ver abajo).
  4. Arrancar. En Windows/pruebas: npm start. En un servidor Linux, recomendado como servicio systemd:

    # /etc/systemd/system/trunkview.service
    [Unit]
    Description=TrunkView
    After=network.target asterisk.service
    
    [Service]
    WorkingDirectory=/opt/trunkview
    ExecStart=/usr/bin/node src/server/index.js
    Restart=always
    User=asterisk
    
    [Install]
    WantedBy=multi-user.target

    systemctl enable --now trunkview → panel en http://IP:8088/

Identificación de troncales (NO hay que listarlas a mano)

El panel detecta las troncales solo, de dos formas combinadas:

  1. Descubrimiento al arrancar (trunks.autoDiscover: true, por defecto): al conectar, el panel ejecuta la acción AMI SIPpeers, enumera todos los peers SIP configurados y crea la tarjeta de cada troncal aunque todavía no tenga tráfico. No hay que escribir nombres.
  2. Heurística en vivo (trunks.mode): clasifica cada peer que aparece.
    • both (por defecto): usa trunks.names y la heurística.
    • auto: solo heurística — cualquier peer cuyo nombre no sea numérico es troncal.
    • list: solo las troncales de trunks.names (para control total).

trunks.extensionPattern define qué peers son extensiones (por defecto, numéricos de 2 a 6 dígitos) y por tanto se excluyen de las troncales.

trunks.names es opcional: solo hace falta para forzar una troncal que la heurística no acierte (p. ej. una con nombre numérico) o si usas mode: "list".

Nota: SIPpeers clasifica por el patrón numérico. Si en tu central alguna extensión tiene nombre alfanumérico, añádela al extensionPattern o usa mode: "list". Para nombres 100% autoritativos existe además la opción de leerlos de la base de datos de FreePBX (tabla trunks); dímelo si la quieres.

Licencia

Este proyecto se distribuye bajo la GNU General Public License v3.0 o posterior (GPL-3.0-or-later). Ver el archivo LICENSE para el texto completo.

Copyright (C) 2026 armangrigo <https://github.com/armangrigo>

Este programa es software libre: podés redistribuirlo y/o modificarlo bajo los
términos de la GNU General Public License publicada por la Free Software
Foundation, en su versión 3 o (a tu opción) cualquier versión posterior.

Se distribuye con la esperanza de que sea útil, pero SIN NINGUNA GARANTÍA; ni
siquiera la garantía implícita de COMERCIABILIDAD o IDONEIDAD PARA UN PROPÓSITO
PARTICULAR. Ver la GNU General Public License para más detalles.

About

Panel de monitoreo de troncales y llamadas en tiempo real para Issabel/Asterisk

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages