93 lines
3.7 KiB
HTML
93 lines
3.7 KiB
HTML
<!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>
|
|
|