Primera Version

master
Oscar Garcia 9 months ago
commit 67e15634f3

@ -0,0 +1,36 @@
# PJSIP Dashboard (HTML estático + logs)
Genera `/var/www/html/dashboard.html` cada minuto con el estado de extensiones
PJSIP y guarda:
- Resumen diario: `/var/log/pjsip_dashboard_summary.csv`
- Problemas del día: `/var/log/pjsip_problems/pjsip_problems-YYYY-MM-DD.csv`
## Requisitos
- Debian/Ubuntu con systemd.
- Asterisk accesible por root (o sudo) para ejecutar `asterisk -rx`.
- Servidor web sirviendo `/var/www/html` (Apache o Nginx). Si no hay, instalamos Apache.
## Instalación rápida
```bash
git clone git@git.sapian.cloud:Sapian/realtime_dialbox8.git
cd realtime_dialbox8
```
# scripts/generar_dashboard_extensiones.sh
sudo ./install.sh
# Validaciones
## Estado del timer
systemctl status pjsip-dashboard.timer
## Últimas corridas del servicio
journalctl -u pjsip-dashboard.service -n 20 --no-pager
## HTML actualizado hace menos de 2 minutos
stat -c '%y %n' /var/www/html/dashboard.html
# Últimas líneas de los logs
tail -n 10 /var/log/pjsip_dashboard_summary.csv
tail -n 10 /var/log/pjsip_problems/pjsip_problems-$(date +%F).csv

@ -0,0 +1,74 @@
---
## 2) `install.sh`
```bash
#!/usr/bin/env bash
set -euo pipefail
# ====== Parámetros ======
SVC_NAME="pjsip-dashboard"
BIN_SRC="scripts/generar_dashboard_extensiones.sh"
BIN_DST="/usr/local/bin/generar_dashboard_extensiones.sh"
WEB_ROOT="/var/www/html"
LOG_DIR_SUM="/var/log"
LOG_DIR_PROB="/var/log/pjsip_problems"
# ====== Comprobaciones ======
if [[ ! -f "$BIN_SRC" ]]; then
echo "ERROR: Falta $BIN_SRC. Copia aquí TU script actual (el que ya funciona) y vuelve a correr install.sh"
exit 1
fi
if [[ $EUID -ne 0 ]]; then
echo "Ejecuta como root: sudo ./install.sh"
exit 1
fi
echo "==> Actualizando índice de paquetes"
apt-get update -y
echo "==> Instalando dependencias mínimas"
# gawk, sed, grep, coreutils ya suelen venir; apache2 solo si no hay web root
PKGS=( gawk sed grep coreutils findutils )
if ! command -v a2enmod >/dev/null 2>&1 && [[ ! -d "$WEB_ROOT" ]]; then
PKGS+=( apache2 )
fi
# Instalar bc SOLO si tu script lo usa (buscamos 'bc')
if grep -q '\bbc\b' "$BIN_SRC"; then
PKGS+=( bc )
fi
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends "${PKGS[@]}"
# Crear web root si no existe
mkdir -p "$WEB_ROOT"
# Copiar logo si existe
if [[ -f "web/logo-sapian.png" ]]; then
cp -f web/logo-sapian.png "$WEB_ROOT/logo-sapian.png"
fi
# Copiar script generador
install -m 0755 "$BIN_SRC" "$BIN_DST"
# Crear directorios de logs
mkdir -p "$LOG_DIR_PROB"
chmod 755 "$LOG_DIR_PROB"
# Instalar unit files de systemd
install -m 0644 systemd/pjsip-dashboard.service /etc/systemd/system/pjsip-dashboard.service
install -m 0644 systemd/pjsip-dashboard.timer /etc/systemd/system/pjsip-dashboard.timer
# Recargar systemd y habilitar timer
systemctl daemon-reload
systemctl enable --now pjsip-dashboard.timer
# Forzar una corrida para generar el HTML al tiro
systemctl start pjsip-dashboard.service || true
echo "==> Listo. Abre: http://<IP>/dashboard.html"
echo " Logs resumen: /var/log/pjsip_dashboard_summary.csv"
echo " Logs problemas: /var/log/pjsip_problems/pjsip_problems-YYYY-MM-DD.csv"

@ -0,0 +1,328 @@
#!/usr/bin/env bash
# -*- coding: utf-8 -*-
# Tablero PJSIP: Tabla + Columnas, con "Solo problemas" y contadores en columnas.
set -Eeuo pipefail
LANG=C
PATH=/usr/sbin:/usr/bin:/sbin:/bin
HTML_FILE="/var/www/html/dashboard.html"
# --- Parámetros (umbral) ---
WARN_MS_DEFAULT=300
WARN_MS="$WARN_MS_DEFAULT"
if [[ -r /etc/default/pjsip-dashboard ]]; then
# Permite definir WARN_MS=300 en /etc/default/pjsip-dashboard (opcional)
# shellcheck disable=SC1091
. /etc/default/pjsip-dashboard || true
fi
[[ -n "${WARN_MS:-}" ]] || WARN_MS="$WARN_MS_DEFAULT"
# --- Capturar contactos PJSIP ---
TMP=$(mktemp)
trap 'rm -f "$TMP" "$TMP.ok" "$TMP.slow" "$TMP.bad"' EXIT
asterisk -rx "pjsip show contacts" >"$TMP.raw" || {
echo "No pude ejecutar 'pjsip show contacts'"; exit 1; }
# Parseo robusto: Contact: 100/sip:100@IP ... (Avail|Unavail) RTT
awk '
BEGIN{ OFS="|" }
/^[[:space:]]*Contact:/ {
# EXT, IP, STATUS, RTT (num o nan)
if (match($0, /^[[:space:]]*Contact:[[:space:]]*([0-9]+)\/sip:[^@]+@([^ :;]+).* (Avail|Unavail)[[:space:]]+([0-9.]+|nan)$/, m)) {
print m[1], m[2], m[3], m[4];
} else if (match($0, /^[[:space:]]*Contact:[[:space:]]*([0-9]+)\/sip:[^ ]+.* (Avail|Unavail)[[:space:]]+([0-9.]+|nan)$/, m)) {
print m[1], "", m[2], m[3];
}
}
' "$TMP.raw" > "$TMP"
# Contadores por contacto
total_contacts=$(wc -l < "$TMP" | tr -d ' ')
bad_contacts=$(awk -F'|' '$3=="Unavail"{c++} END{print c+0}' "$TMP")
avail_contacts=$(awk -F'|' '$3=="Avail"{c++} END{print c+0}' "$TMP")
slow_contacts=$(awk -F'|' -v W="$WARN_MS" '$3=="Avail" && $4 ~ /^[0-9.]+$/ && ($4+0)>=W {c++} END{print c+0}' "$TMP")
# Grupos ordenados
awk -F'|' -v W="$WARN_MS" '$3=="Unavail"{print $0}' "$TMP" | sort -t'|' -k1,1n > "$TMP.bad"
awk -F'|' -v W="$WARN_MS" '$3=="Avail" && $4 ~ /^[0-9.]+$/ && ($4+0)>=W {print $0}' "$TMP" | sort -t'|' -k4,4nr -k1,1n > "$TMP.slow"
awk -F'|' -v W="$WARN_MS" '($3=="Avail") && ( $4 !~ /^[0-9.]+$/ || ($4+0)<W ) {print $0}' "$TMP" | sort -t'|' -k1,1n > "$TMP.ok"
# Endpoints totales (solo numéricos; excluye troncales con letras)
endpoints_total=$(
asterisk -rx "pjsip show endpoints" \
| sed -E 's/\x1B\[[0-9;]*[A-Za-z]//g' \
| awk 'match($0,/^ *Endpoint:[[:space:]]*([0-9]+)\//,m){seen[m[1]]=1} END{print length(seen)+0}'
)
# Fecha
now="$(date '+%Y-%m-%d %H:%M:%S %z')"
# Utilidades de render
html_escape() {
sed -e 's/&/\&amp;/g' -e 's/</\&lt;/g' -e 's/>/\&gt;/g'
}
# Construir filas HTML (TABLA)
build_rows() {
# $1 = file; $2 = clase (ok|slow|bad)
local file="$1" cls="$2"
while IFS='|' read -r ext ip status rtt; do
[[ -n "${ext:-}" ]] || continue
local ip_show rtt_show rowcls
ip_show="${ip:-}"
if [[ "$cls" == "bad" ]]; then
rtt_show="Sin respuesta"
else
if [[ "$rtt" == "nan" || -z "$rtt" ]]; then rtt_show="—"; else rtt_show="$rtt"; fi
fi
rowcls="$cls"
printf '<tr class="%s"><td class="c-ext">%s</td><td class="c-sta">%s</td><td class="c-ip">%s</td><td class="c-rtt">%s</td></tr>\n' \
"$rowcls" "$(printf '%s' "$ext" | html_escape)" \
"$([[ "$cls" == "bad" ]] && echo "UNAVAILABLE" || echo "AVAILABLE")" \
"$(printf '%s' "$ip_show" | html_escape)" \
"$(printf '%s' "$rtt_show" | html_escape)"
done < "$file"
}
# Construir items (COLUMNAS)
build_cards() {
# $1 = file; $2 = clase (ok|slow|bad)
local file="$1" cls="$2"
while IFS='|' read -r ext ip status rtt; do
[[ -n "${ext:-}" ]] || continue
local ip_show rtt_show
ip_show="${ip:-}"
if [[ "$cls" == "bad" ]]; then
rtt_show="Sin respuesta"
else
if [[ "$rtt" == "nan" || -z "$rtt" ]]; then rtt_show="—"; else rtt_show="$rtt"; fi
fi
cat <<CARD
<div class="card $cls">
<div class="ext">$ext</div>
<div class="meta">
<div class="ip">$ip_show</div>
<div class="rtt">$rtt_show</div>
</div>
</div>
CARD
done < "$file"
}
# --- HTML ---
cat > "$HTML_FILE" <<HTML
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8" />
<title>Estado de Extensiones Tuyomotor</title>
<meta http-equiv="refresh" content="60" />
<style>
:root{
--brand:#00564f;
--head:#00423d;
--ok:#6edbd2;
--slow:#d9dc3f;
--bad:#c84141;
--ok2:#45c7bd;
--bg:#f6f7f7;
--ink:#19332f;
--muted:#6a7b77;
--table:#004a44;
}
*{box-sizing:border-box}
body{margin:0;font-family:system-ui, -apple-system, Segoe UI, Roboto, Arial; background:var(--bg); color:var(--ink)}
header{display:flex;align-items:center;gap:16px;padding:20px 24px 8px}
header h1{margin:0;font-size:28px;color:var(--brand);font-weight:800}
.logo{height:36px}
.pill{position:fixed;top:16px;right:16px;background:var(--head);color:#fff;border-radius:999px;padding:8px 14px;font-weight:700;font-size:14px}
.sub{padding:0 24px 8px;color:var(--muted);font-size:12px}
.legend{padding:0 24px 6px;display:flex;gap:18px;align-items:center;flex-wrap:wrap}
.dot{display:inline-block;width:12px;height:12px;border-radius:3px;margin-right:6px;vertical-align:middle}
.dot.ok{background:var(--ok2)} .dot.slow{background:var(--slow)} .dot.bad{background:var(--bad)}
.bullets{padding:0 24px 12px}
.bullets ul{margin:6px 0 0 18px}
.btns{padding:0 24px 8px;display:flex;gap:8px}
.btns button{padding:6px 10px;border:1px solid #cfd6d5;background:#fff;border-radius:6px;cursor:pointer}
.btns button.active{background:var(--head);color:#fff;border-color:var(--head)}
.hidden{display:none !important}
/* Tabla */
table{width:100%;border-collapse:collapse;margin:10px 0 28px}
th,td{padding:10px;border:1px solid #d5e1df}
th{background:var(--table);color:#fff;text-align:left;font-weight:700}
tr.section th{background:var(--head)}
tr.ok{background:var(--ok)}
tr.slow{background:var(--slow)}
tr.bad{background:#e66a6a;color:#fff}
td.c-sta{font-weight:700}
td.c-rtt{font-weight:800}
/* Columnas */
.cols{display:grid;grid-template-columns:repeat(3,1fr);gap:18px;padding:0 24px 28px}
@media (max-width:1100px){ .cols{grid-template-columns:1fr} }
.col{background:#fff;border:1px solid #dbe7e5;border-radius:10px;overflow:hidden}
.col h3{margin:0;padding:10px 14px;background:var(--head);color:#fff;display:flex;justify-content:space-between;align-items:center}
.col h3.bad{background:var(--bad)} .col h3.slow{background:#7d8200}
.count{opacity:.9;font-weight:700}
.box{padding:12px}
.card{display:flex;gap:12px;align-items:center;border-radius:8px;padding:10px 12px;margin-bottom:8px;border:1px solid #e1ebe9;background:#fff}
.card.ok{background:var(--ok)}
.card.slow{background:var(--slow)}
.card.bad{background:#f3b3b3}
.card .ext{font-weight:900;width:52px;text-align:center}
.card .meta{display:flex;gap:16px;justify-content:space-between;width:100%}
.card .rtt{font-weight:900}
.muted{color:var(--muted)}
</style>
</head>
<body>
<span class="pill">En uso: $total_contacts / $endpoints_total</span>
<header>
<img class="logo" src="/var/www/html/logo-sapian.png" alt="Sapian" onerror="this.style.display='none'"/>
<h1>Estado de Extensiones Tuyomotor</h1>
</header>
<div class="sub">Última actualización: <strong>$now</strong> • Umbral de alerta de respuesta: <strong>${WARN_MS} ms</strong> (menor es mejor)</div>
<div class="legend">
<span><i class="dot ok"></i>Normal</span>
<span><i class="dot slow"></i>Respuesta lenta</span>
<span><i class="dot bad"></i>No registrado</span>
</div>
<div class="bullets">
<ul>
<li>Extensiones registradas (por contacto): <strong>$avail_contacts</strong></li>
<li>Extensiones no registradas (por contacto): <strong>$bad_contacts</strong></li>
<li>Con respuesta lenta (> ${WARN_MS} ms): <strong>$slow_contacts</strong></li>
<li>Total de contactos: <strong>$total_contacts</strong></li>
</ul>
</div>
<div class="btns">
<button id="btnTable">Tabla</button>
<button id="btnCols" class="active">Columnas</button>
<button id="btnOnlyIssues">Solo problemas</button>
</div>
<!-- Vista: TABLA -->
<section id="view-table" class="hidden">
<table>
<thead>
<tr>
<th>Extensión</th>
<th>Estado</th>
<th>IP remota</th>
<th>Tiempo de respuesta (ms)</th>
</tr>
</thead>
<tbody id="sec-bad">
<tr class="section"><th colspan="4">NO REGISTRADAS (UNAVAILABLE)</th></tr>
$(build_rows "$TMP.bad" "bad")
</tbody>
<tbody id="sec-slow">
<tr class="section"><th colspan="4">DISPONIBLES CON RESPUESTA LENTA (&gt; ${WARN_MS} ms)</th></tr>
$(build_rows "$TMP.slow" "slow")
</tbody>
<tbody id="sec-ok">
<tr class="section"><th colspan="4">DISPONIBLES</th></tr>
$(build_rows "$TMP.ok" "ok")
</tbody>
</table>
</section>
<!-- Vista: COLUMNAS -->
<section id="view-columns">
<div class="cols">
<div class="col" id="col-bad">
<h3 class="bad">NO REGISTRADAS (UNAVAILABLE) <span class="count">($bad_contacts)</span></h3>
<div class="box">
$(build_cards "$TMP.bad" "bad")
</div>
</div>
<div class="col" id="col-slow">
<h3 class="slow">DISPONIBLES CON RESPUESTA LENTA (&gt; ${WARN_MS} ms) <span class="count">($slow_contacts)</span></h3>
<div class="box">
$(build_cards "$TMP.slow" "slow")
</div>
</div>
<div class="col" id="col-ok">
<h3>DISPONIBLES <span class="count">($avail_contacts)</span></h3>
<div class="box">
$(build_cards "$TMP.ok" "ok")
</div>
</div>
</div>
</section>
<script>
const vTable = document.getElementById('view-table');
const vCols = document.getElementById('view-columns');
const btnTable = document.getElementById('btnTable');
const btnCols = document.getElementById('btnCols');
const btnOnly = document.getElementById('btnOnlyIssues');
btnTable?.addEventListener('click', ()=>{
btnTable.classList.add('active'); btnCols.classList.remove('active');
vTable.classList.remove('hidden'); vCols.classList.add('hidden');
window.scrollTo({top:0,behavior:'smooth'});
});
btnCols?.addEventListener('click', ()=>{
btnCols.classList.add('active'); btnTable.classList.remove('active');
vCols.classList.remove('hidden'); vTable.classList.add('hidden');
window.scrollTo({top:0,behavior:'smooth'});
});
// Solo problemas -> oculta/mostrar "Disponibles" en ambas vistas
btnOnly?.addEventListener('click', ()=>{
document.getElementById('sec-ok')?.classList.toggle('hidden'); // tabla
document.getElementById('col-ok')?.classList.toggle('hidden'); // columnas
});
</script>
</body>
</html>
HTML
echo "✅ Dashboard actualizado (tabla+columnas + Solo problemas): $HTML_FILE"
# === LOG RESUMEN (simple) — conserva 7 días ===
HTML="/var/www/html/dashboard.html"
LOG="/var/log/pjsip_dashboard_summary.csv"
mkdir -p /var/log
ts="$(date -Is)"
# Función: encuentra la línea por patrón, quita etiquetas HTML y devuelve el primer número
get_num(){
local pat="$1"
awk -v p="$pat" 'index($0,p){print; exit}' "$HTML" \
| sed -E 's/<[^>]+>//g' \
| grep -oE '[0-9]+' | head -1
}
reg="$( get_num 'Extensiones registradas (por contacto)')"
unreg="$( get_num 'Extensiones no registradas (por contacto)')"
slow="$( get_num 'Con respuesta lenta (> 300 ms)')"
total="$( get_num 'Total de contactos')"
[ -s "$LOG" ] || echo "timestamp,registradas,no_registradas,lentas,total" > "$LOG"
echo "$ts,$reg,$unreg,$slow,$total" >> "$LOG"
# Mantener ~7 días si corre cada minuto (10 080 filas)
tail -n 10080 "$LOG" > "$LOG.tmp" && mv "$LOG.tmp" "$LOG"
# === FIN LOG RESUMEN ===
/usr/local/bin/pjsip_log_problems.sh

@ -0,0 +1,14 @@
[Unit]
Description=Generar dashboard PJSIP (HTML estático)
Wants=pjsip-dashboard.timer
[Service]
Type=oneshot
ExecStart=/usr/local/bin/generar_dashboard_extensiones.sh
# Opcional: evita saturar si hay múltiples ejecuciones solapadas
Nice=10
IOSchedulingClass=best-effort
IOSchedulingPriority=7
[Install]
WantedBy=multi-user.target

@ -0,0 +1,12 @@
[Unit]
Description=Programación del dashboard PJSIP (cada 60s)
[Timer]
OnBootSec=30s
OnUnitActiveSec=60s
AccuracySec=5s
Persistent=true
Unit=pjsip-dashboard.service
[Install]
WantedBy=timers.target

@ -0,0 +1,22 @@
#!/usr/bin/env bash
set -euo pipefail
SVC_NAME="pjsip-dashboard"
if [[ $EUID -ne 0 ]]; then
echo "Ejecuta como root: sudo ./uninstall.sh"
exit 1
fi
systemctl disable --now ${SVC_NAME}.timer || true
systemctl stop ${SVC_NAME}.service || true
rm -f /etc/systemd/system/${SVC_NAME}.service
rm -f /etc/systemd/system/${SVC_NAME}.timer
systemctl daemon-reload
# NO borramos HTML ni logs por si quieres conservarlos
# rm -f /var/www/html/dashboard.html
# rm -f /usr/local/bin/generar_dashboard_extensiones.sh
echo "Desinstalado ${SVC_NAME}. Puedes borrar manualmente HTML y logs si lo deseas."

@ -0,0 +1,23 @@
#!/usr/bin/env bash
set -euo pipefail
BIN_SRC="scripts/generar_dashboard_extensiones.sh"
BIN_DST="/usr/local/bin/generar_dashboard_extensiones.sh"
if [[ $EUID -ne 0 ]]; then
echo "Ejecuta como root: sudo ./upgrade.sh"
exit 1
fi
if [[ ! -f "$BIN_SRC" ]]; then
echo "ERROR: Falta $BIN_SRC"
exit 1
fi
install -m 0755 "$BIN_SRC" "$BIN_DST"
systemctl daemon-reload
# Forzar una corrida para ver cambios de inmediato
systemctl start pjsip-dashboard.service || true
echo "Actualizado. Revisa http://<IP>/dashboard.html"

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Loading…
Cancel
Save