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 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.
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.
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
CoreShowChannelsde 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.
| 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 |
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.
- Creá un bot con @BotFather y copiá el token (algo
como
123456789:AA...). - 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.
- Obtené tu chatId: pegá el token en
config.json(telegram.botToken) y corrénpm run telegram:chatid. Imprime el/los chatId que le escribieron. - En
config.json, bloquetelegram:enabled: true, elbotTokeny tu chatId enchatIds(la lista principal / de fallback). - 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.
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.
| 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
trunkDown→debounceSec: 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 connotifyRecovery.activeBelow→ histéresis + sostenido + cooldown + gracia:- Histéresis: avisa al bajar de
threshold, pero para dar "recuperado" tiene que subir por encima derecovery(que debe ser mayor quethreshold). 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, noactiveBelow.
- Histéresis: avisa al bajar de
- Línea base silenciosa: las troncales que ya están caídas al arrancar no generan avisos; solo se notifican los cambios posteriores.
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 alertaactiveBelowrazona sobre llamadas.
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.
npm install
node scripts/dev.js # simulador + panel en http://localhost:8088O por separado: npm run mock en una terminal y npm start en otra.
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:notifyCada uno imprime OK/FAIL por caso y TODO OK al final, y sale con código
0/1 (usable en CI).
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.
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@ISSABELy enconfig.jsonami.host: "127.0.0.1". El AMI viaja cifrado dentro del SSH y no hace falta abrir el 5038.
Son dos conexiones distintas, no las confundas:
- Panel → AMI del Issabel. La hace solo la máquina donde corre el panel.
Por eso el
permitdel usuario AMI solo tiene que autorizar la red donde está el panel, no la de los operadores. Enconfig.json:ami.host= IP del Issabel. - 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.
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
permitcompara 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) cubre10.9.0.x; si la red es más grande usá la máscara correcta (p. ej./16=255.255.0.0).
-
Crear un usuario AMI de solo lectura en
/etc/asterisk/manager_custom.conf. Elpermitautoriza 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á conasterisk -rx "manager show user panelop". -
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). -
Crear
config.json(copia deconfig.example.json) y ajustar:ami.host: IP del Issabel (o127.0.0.1si 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).
-
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 enhttp://IP:8088/
El panel detecta las troncales solo, de dos formas combinadas:
- Descubrimiento al arrancar (
trunks.autoDiscover: true, por defecto): al conectar, el panel ejecuta la acción AMISIPpeers, 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. - Heurística en vivo (
trunks.mode): clasifica cada peer que aparece.both(por defecto): usatrunks.namesy la heurística.auto: solo heurística — cualquier peer cuyo nombre no sea numérico es troncal.list: solo las troncales detrunks.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:
SIPpeersclasifica por el patrón numérico. Si en tu central alguna extensión tiene nombre alfanumérico, añádela alextensionPatterno usamode: "list". Para nombres 100% autoritativos existe además la opción de leerlos de la base de datos de FreePBX (tablatrunks); dímelo si la quieres.
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.
