Sistema activo en produccion

Asterisk Panel

Panel de monitorizacion en tiempo real y estadisticas CDR para Asterisk PBX

👤 Autor: JM Fernandez + Claude (Anthropic)
📅 Fecha: 23 marzo 2026
📞 Target: Asterisk 13.38.3
Python asyncio aiohttp WebSocket SQLite WAL AMI CDR Vanilla JS systemd nginx Debian 12 Azure AD App Proxy Microsoft Graph

Que resuelve este proyecto

Reemplazar una solucion lenta y externa por una herramienta interna de alto rendimiento integrada con Asterisk.

🔔

Panel BLF en tiempo real

Visualizacion del estado de aproximadamente 1000 extensiones (libre, en llamada, sonando, en espera, DND, no registrada) con latencia inferior a 100 ms via WebSocket.

📊

CDR Search & Statistics

Busqueda y estadisticas sobre el historico completo de llamadas, reemplazando CDRStats que consultaba MySQL directamente en el servidor Asterisk con alto impacto en produccion.

🔒

Impacto minimo en produccion

Solo conexion AMI y sync incremental cada 5 minutos. El backend corre en infraestructura separada. Asterisk 13.38.3 no se modifica.

~1000
Extensiones monitorizadas
<100ms
Latencia WebSocket
~19K
CDR sincronizados en 1.25 s
8.500
Lineas de codigo (LOC)
7M+
Registros CDR historicos
5
Formatos de exportacion
~3 GB
SQLite cdr.db (1 año)

Cuatro componentes desacoplados

Arquitectura async de un solo proceso Python que combina cliente AMI, servidor HTTP/WS y sync de CDR.

📞
AMI Client
Python asyncio
Conexion TCP a Asterisk AMI (5038). Auth, parsing de eventos, mapeo de estados.
⚙️
Web Server
aiohttp
HTTP + WebSocket. API REST. Push de cambios de estado en tiempo real.
🗃
CDR Sync
pymysql + SQLite WAL
Sync incremental MySQL → SQLite local cada 5 minutos.
🌐
Browser
Vanilla JS + CSS
Grid responsivo, filtros, tema claro/oscuro, WebSocket client.
Real-time: AMI Events Python Backend WebSocket Browser
CDR: MySQL (Asterisk) CDR Sync SQLite REST API Browser

Organizacion del repositorio

Backend Python y frontend estatico bien separados. Un unico punto de entrada.

asterisk-panel/
  backend/
    main.py              — Entry point, orquesta todos los componentes
    server.py            — aiohttp HTTP + WebSocket server, API REST, admin auth
    ami_client.py        — AMI TCP client, auth, event parsing, mapeo de estados
    cdr_db.py            — Schema SQLite, queries de busqueda y estadisticas
    cdr_sync.py          — Sync incremental MySQL -> SQLite
    cdr_export.py        — Export a HTML, XLSX (openpyxl), PDF (fpdf2)
    config.py            — Carga de config desde config.ini
    user_sync.py         — Fetch periodico HTTP del directorio de usuarios
    user_editor.py       — Parse/serialize usuarios.txt (CSV round-trip)
    queue_editor.py      — Parse/serialize queues.conf (round-trip con comentarios)
    graph_client.py      — Microsoft Graph API para avatares de usuario
    import_cdr.py        — Import inicial one-time desde TSV
    filters.json         — Filtros predefinidos (grupos de extensiones)
  frontend/
    index.html           — CDR search con filtros multi-usuario
    realtime.html        — Panel BLF en tiempo real con vistas y filtros predefinidos
    stats.html           — Dashboard de estadisticas con filtros predefinidos
    extensiones.html     — Editor de extensiones (seccion admin)
    colas.html           — Editor de colas de Asterisk (seccion admin)
    filtros.html         — Editor de filtros predefinidos (seccion superadmin)
    callmap.html         — Mapa de llamadas activas (modo dev)
    app.js               — WebSocket client, grid extensiones, vistas y filtros
    cdr.js               — CDR search, paginacion, detalle de llamada vinculada
    stats.js             — Donut chart SVG, heatmap, desglose por usuario
    callmap.js           — Visualizacion SVG de llamadas activas
    usuarios.js          — Editor de extensiones con gestion de acceso
    colas.js             — Editor de colas con drag & drop de miembros
    filtros.js           — Editor de filtros predefinidos (CRUD)
    auth.js              — Admin/superadmin auth check (App Proxy header)
    user-tags.js         — Componente tag input multi-usuario compartido
    theme.js             — Toggle de tema con persistencia localStorage
    style.css            — Sistema de temas completo (claro/oscuro)
  scripts/
    serve_users.py       — Micro HTTP server en Asterisk (Python 3.5 compatible)
  deploy.sh              — Deploy a produccion en un comando
  DEPLOY.md              — Guia de despliegue completa

Capacidades del sistema

4.1 Panel de Extensiones en Tiempo Real

Los 6 estados posibles de cada extension, representados con color e icono:

Libre
En llamada
Sonando
En espera
DND
No registrada
Funcionalidad Detalle
WebSocket push Cambios de estado enviados a todos los clientes conectados. Latencia <100 ms.
Cards de extension Header con nombre de usuario, color dinamico por estado, timer de duracion actualizado cada segundo.
Busqueda Filtro en tiempo real por numero de extension o nombre de usuario.
Filtros por estado Badges coloreados toggleables. "No registrada" desmarcado por defecto para reducir ruido.
Sync de directorio Fetch HTTP del directorio de usuarios cada 24 h (configurable). Resuelve conflicto de conexion AMI.
Filtro de extensiones Solo se muestran extensiones de 4-5 digitos. Excluye trunks y rutas de marcado.

4.2 CDR Search & Statistics

Capacidad Detalle
Filtros de busqueda Origen, destino, caller ID, estado (ANSWERED / NO ANSWER / BUSY / FAILED), contexto, rango fecha+hora.
Estadisticas agregadas Total, contestadas, no contestadas, ocupado, fallidas, duracion media. Calculadas sobre el filtro activo.
Paginacion 50 registros por pagina con navegacion.
Export HTML autocontenido con estilos, XLSX (openpyxl con cabeceras, colores y filas alternas), PDF (fpdf2 A4 landscape).
Rendimiento SQLite con indices en calldate, src, dst, disposition. ~19K CDR sincronizados en 1.25 s.
Import inicial 7M+ registros importados desde TSV en aproximadamente 35 minutos.

4.3 UI / UX

🌌

Temas claro/oscuro

Toggle con persistencia en localStorage. Estetica coherente con el resto del ecosistema interno (spo-permissions-hub).

📱

Responsive design

Grid de extensiones adaptable. Funciona en pantallas grandes de supervision y en portatil.

🛡️

Branding corporativo

Navbar con logo y favicon. Paleta personalizada: navbar lima #e1f56e, accent magenta #b62682.

4.4 Dashboard de Estadisticas

Funcionalidad Detalle
Donut chart SVG Distribucion visual de llamadas por estado (contestada, no contestada, ocupado, fallida) con SVG puro.
Estadisticas por direccion Metricas separadas para llamadas entrantes y salientes con totales y duraciones.
Mapa de calor Grid CSS con horas 7-22, media de llamadas por dia y hora de la semana.
Desglose por usuario Tabla de estadisticas individuales cuando se busca por multiples usuarios.
Filtros guardados Presets de filtros almacenados en localStorage con chips de acceso rapido.
Exportacion HTML standalone y XLSX con estilos.

4.5 Vistas guardadas en tiempo real

Funcionalidad Detalle
Vistas por usuario Seleccion de usuarios para filtrar extensiones en el panel BLF.
Persistencia client-side Vistas guardadas en localStorage, sin dependencia del servidor.
Tag input compartido Componente de input con tags reutilizado en CDR, estadisticas y tiempo real.
Chips de seleccion Barra de chips para cambiar rapidamente entre vistas.

4.6 Gestion de Extensiones

Funcionalidad Detalle
Editor web Edicion directa del fichero usuarios.txt del servidor Asterisk desde el navegador.
Tabla con avatares Cada extension muestra iniciales, numero prominente y nombre.
Modales de edicion Alta, edicion y eliminacion de extensiones con validacion.
Preservacion CSV Round-trip completo del fichero CSV, solo modifica columnas editables.
Micro HTTP server serve_users.py en el servidor Asterisk (Python 3.5 compatible) para GET/POST.

4.7 Autenticacion Admin (dos niveles)

Funcionalidad Detalle
Azure AD App Proxy Autenticacion via cabecera X-MS-CLIENT-PRINCIPAL-NAME inyectada por el proxy.
Admin (allowed_upns) Acceso a Extensiones y Colas. Protegido con 403 para usuarios no autorizados.
Superadmin (admins_upns) Acceso completo incluyendo Filtros predefinidos. Los superadmins tambien son admins.
Doble acceso La app funciona sin App Proxy (sin secciones admin) y con App Proxy (admin/superadmin visible).
Configuracion Seccion [admin] en config.ini con header, allowed_upns y admins_upns.
Endpoint /api/auth/me Devuelve {admin, superadmin, upn} para mostrar/ocultar enlaces de navegacion.

4.8 Gestion de Colas

Funcionalidad Detalle
Editor web Edicion directa de /etc/asterisk/queues.conf desde el navegador. CRUD completo de colas.
Propiedades editables strategy, timeout, retry, maxlen, weight, wrapuptime, musiconhold, ringinuse, joinempty, leavewhenempty.
Miembros estaticos Gestion de miembros con device (SIP/XXXX) y prioridad. Reordenacion con drag & drop (HTML5 API).
Round-trip Preserva comentarios, lineas en blanco y formato original del fichero. Solo modifica secciones editadas.
Parser dedicado queue_editor.py: parser linea a linea (no configparser) para soportar member => y comentarios ;.
Seccion [general] Preservada intacta, no editable desde la interfaz web.
Reload automatico Tras guardar, se ejecuta asterisk -rx "queue reload all" via hook en serve_users.py.

4.9 Filtros Predefinidos

Funcionalidad Detalle
Editor de filtros Pagina de administracion (superadmin) para crear/editar/eliminar grupos de extensiones con nombre.
Almacenamiento filters.json en el backend. API REST GET/POST /api/filters.
Integracion Real-Time Chips seleccionables en la barra de vistas. Multi-seleccion con union de extensiones.
Integracion Estadisticas Chips en la barra de filtros predefinidos. Se incluyen en la busqueda y en la exportacion.
Filtrado directo Parametro extensions en la API para filtrar por extensiones directamente, sin resolucion de nombres.
# Ejemplo de mapeo de estados AMI en ami_client.py
DEVICE_STATE_MAP = {
    "NOT_INUSE": "available",
    "Idle":       "available",  # Asterisk 13 devuelve "Idle" en ExtensionStateList
    "INUSE":      "in_call",
    "RINGING":    "ringing",
    "ONHOLD":     "on_hold",
    "UNAVAILABLE": "unregistered",
    "DND":        "dnd",
}

Produccion en Debian 12

Despliegue automatizado en un solo comando, con nginx reverse proxy y SSL wildcard.

💻 Sistema

  • Debian 12 (misma red que Asterisk)
  • Python 3.11+ con venv
  • nginx reverse proxy con SSL
  • Certificado wildcard *.sys

⚙️ systemd

  • Service con Restart=always
  • Auto-restart en fallo
  • Logs via journalctl
  • systemctl enable asterisk-panel

🚀 deploy.sh

  • git pull desde rama master
  • pip install -r requirements.txt
  • systemctl restart asterisk-panel
  • Un solo comando desde cualquier maquina

🔒 Seguridad

  • config.ini en .gitignore
  • Solo puerto 8081 expuesto externamente
  • AMI y MySQL solo accesibles en LAN
  • nginx gestiona TLS termination
# config.ini.example — credenciales nunca en el repositorio
[asterisk]
host     = 10.x.x.x
ami_port = 5038
ami_user = panel_user
ami_pass = CHANGE_ME

[mysql]
host     = 10.x.x.x
db       = asteriskcdrdb
user     = cdr_reader
password = CHANGE_ME

[server]
host     = 0.0.0.0
port     = 8081
sync_interval_minutes = 5

[admin]
header       = X-MS-CLIENT-PRINCIPAL-NAME
allowed_upns = [email protected]

Problemas resueltos durante el desarrollo

Cada obstaculo encontrado, diagnosticado y resuelto con una solucion definitiva.

📞

Asterisk 13 "Idle" vs "NOT_INUSE"

ExtensionStateList devuelve "Idle" en lugar de "NOT_INUSE" en Asterisk 13. Solucion: se anadio un mapping extra en DEVICE_STATE_MAP para que ambos valores se traduzcan al estado "available".

🚾

FortiGate bloqueando puertos AMI y MySQL desde dev

Los puertos 5038 (AMI) y 3306 (MySQL) estaban bloqueados desde el entorno de desarrollo por el firewall perimetral. Solucion: desplegar el backend en la misma red que Asterisk. Solo se expone el puerto 8081 hacia exterior via nginx.

AMI concurrent read — race condition en la conexion

UserSync y listen_events compartian la misma conexion AMI, causando "readuntil() called while another coroutine is already waiting". Solucion: el sync de usuarios se realiza via HTTP a un micro-servidor externo en lugar de usar el canal AMI.

🔨

AMI Command no soporta shell interactivo

El comando !cat usuarios.txt solo funciona en la CLI interactiva de Asterisk, no via AMI. Solucion: se levanta un micro HTTP server en el host Asterisk que sirve usuarios.txt directamente por HTTP.

📈

CDR performance — CDRStats sobre MySQL en produccion

CDRStats consultaba directamente la base de datos MySQL de Asterisk, generando carga en produccion y tiempos de respuesta lentos. Solucion: SQLite local con indices optimizados en calldate, src, dst, disposition y sync incremental cada 5 minutos.

🌐

Wildcard SSL no cubre subdominios multinivel

El certificado wildcard *.sys.dominio no cubre subdominios de mas de un nivel como panel.xxx.sys.dominio. Solucion: cambiar el DNS para que el servicio quede en un unico nivel dentro del dominio cubierto.

📄

CSV round-trip en editor de extensiones

El fichero usuarios.txt tiene 11+ columnas pero solo se editan 3. Solucion: almacenar la fila CSV completa como _raw y solo parchear las columnas editables al guardar.

🔐

App Proxy header no llega al backend

nginx filtraba la cabecera X-MS-CLIENT-PRINCIPAL-NAME. Solucion: proxy_set_header explicito en la configuracion de nginx.

👁

CSS .hidden sin regla generica

El link de Extensiones seguia visible porque no existia una regla generica .hidden. Solucion: anadir regla .hidden { display: none !important }.

Dependencias y tecnologias

Capa Tecnologia Uso
Backend runtime Python 3.11+ / asyncio Concurrencia sin threads: AMI client + HTTP server + CDR sync en el mismo proceso.
HTTP + WebSocket aiohttp Servidor web async, API REST y WebSocket server para push de eventos.
Conexion MySQL pymysql Lectura incremental de CDR desde la base de datos de Asterisk.
Base de datos local SQLite (WAL mode) Almacenamiento de CDR con WAL para lecturas concurrentes sin bloqueo.
Export Excel openpyxl XLSX con cabeceras coloreadas, filas alternas y autoajuste de columnas.
Export PDF fpdf2 PDF A4 landscape con tabla de CDR.
Frontend Vanilla JS + CSS Sin dependencias de framework. WebSocket nativo del navegador.
Proxy / TLS nginx Reverse proxy con SSL termination. Unico puerto publico: 443.
Init system systemd Gestion del proceso con auto-restart y logs centralizados.
Microsoft Graph API graph_client.py Avatares de usuario en panel de tiempo real.
Azure AD App Proxy Cabeceras SSO Autenticacion admin via cabeceras SSO inyectadas por el proxy.