feat(node): dashboard SSE y normalización de atributos REST\n\n- Agrega dashboard web (SSE) en / con historial y eventos en vivo\n- Normaliza formato JSON de rlm_rest y registra eventos\n- Responde Access-Accept con VLAN 2 + WISPr 10/10\n\nfix(radius): corrige URI rlm_rest y acepta en lab\n\n- REST URI fija a http://node:3000 (antes expandía mal)\n- default: Auth-Type := Accept para entorno de pruebas\n- clients: añade localhost y subred 192.168.87.0/24\n\nchore: añade .env para compose

This commit is contained in:
Codex Bot
2025-09-24 14:55:18 -06:00
parent 6ef48911ef
commit d9bdfb3420
6 changed files with 183 additions and 21 deletions

View File

@@ -0,0 +1,92 @@
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>RADIUS Dashboard</title>
<style>
:root { color-scheme: light dark; }
body { font-family: system-ui, sans-serif; margin: 0; }
header { padding: 12px 16px; background: #0b5; color: #fff; }
main { padding: 16px; }
.meta { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
.chip { display: inline-block; padding: 2px 8px; border-radius: 999px; font-size: 12px; background: #eef; color: #334; }
.list { margin-top: 12px; }
.item { border: 1px solid #ccc; border-radius: 8px; padding: 8px 12px; margin-bottom: 8px; }
.type-auth { border-left: 4px solid #0b5; }
.type-acct { border-left: 4px solid #09f; }
.attrs { font-family: ui-monospace, monospace; font-size: 12px; white-space: pre-wrap; background: rgba(0,0,0,0.04); padding: 8px; border-radius: 6px; }
.toolbar { display: flex; gap: 8px; align-items: center; margin: 8px 0; }
button { padding: 6px 10px; }
</style>
</head>
<body>
<header>
<h1>RADIUS Dashboard</h1>
</header>
<main>
<div class="meta">
<span class="chip" id="status">Conectando…</span>
<span class="chip" id="count">0 eventos</span>
<span class="chip" id="band">VLAN 2 • 10/10 Mbps</span>
</div>
<div class="toolbar">
<button id="refresh">Recargar historial</button>
<label><input type="checkbox" id="autoScroll" checked /> Autoscroll</label>
</div>
<div id="list" class="list"></div>
</main>
<script>
const list = document.getElementById('list');
const statusEl = document.getElementById('status');
const countEl = document.getElementById('count');
const btnRefresh = document.getElementById('refresh');
const autoScroll = document.getElementById('autoScroll');
let total = 0;
function renderItem(ev) {
const div = document.createElement('div');
const isAuth = ev.type === 'authorize';
div.className = 'item ' + (isAuth ? 'type-auth' : 'type-acct');
const user = ev.attrs?.['User-Name'] || ev.attrs?.['User-Name*0'] || '-';
const nas = ev.attrs?.['NAS-IP-Address'] || '-';
const calling = ev.attrs?.['Calling-Station-Id'] || '-';
const called = ev.attrs?.['Called-Station-Id'] || '-';
div.innerHTML = `
<div><strong>${isAuth ? 'Authorize' : 'Accounting'}</strong> • <small>${new Date(ev.ts).toLocaleString()}</small></div>
<div>Usuario: <code>${user}</code> • NAS: <code>${nas}</code> • STA: <code>${calling}</code> • AP: <code>${called}</code></div>
${isAuth ? `<div>Decisión: <strong>${ev.decision}</strong> • VLAN: <code>${ev.vlan}</code> • BW: <code>${(ev.bandwidth?.down/1e6)||10}↓ / ${(ev.bandwidth?.up/1e6)||10}↑ Mbps</code></div>` : ''}
<div class="attrs">${JSON.escape ? JSON.escape(JSON.stringify(ev.attrs, null, 2)) : JSON.stringify(ev.attrs, null, 2)}</div>
`;
list.appendChild(div);
total += 1;
countEl.textContent = `${total} eventos`;
if (autoScroll.checked) div.scrollIntoView({ behavior: 'smooth', block: 'end' });
}
async function loadHistory() {
const r = await fetch('/api/requests');
const data = await r.json();
list.innerHTML = '';
total = 0;
data.items.forEach(renderItem);
}
btnRefresh.addEventListener('click', loadHistory);
// SSE live events
function connectSSE() {
const es = new EventSource('/events');
es.addEventListener('open', () => statusEl.textContent = 'Conectado');
es.addEventListener('error', () => statusEl.textContent = 'Reintentando…');
es.addEventListener('message', (e) => {
try { const ev = JSON.parse(e.data); renderItem(ev); } catch {}
});
}
loadHistory().then(connectSSE);
</script>
</body>
</html>