Files
radiusNucleo/node-api/public/index.html

160 lines
6.1 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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>
<button id="selfTest">SelfTest RADIUS</button>
<span class="chip" id="selfTestStatus">Idle</span>
<button id="clear">Limpiar</button>
<button id="copy">Copiar</button>
<button id="exportCsv">Exportar CSV</button>
</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 btnSelfTest = document.getElementById('selfTest');
const selfTestStatus = document.getElementById('selfTestStatus');
const autoScroll = document.getElementById('autoScroll');
let total = 0;
const btnClear = document.getElementById('clear');
const btnCopy = document.getElementById('copy');
const btnExportCsv = document.getElementById('exportCsv');
let history = [];
function renderItem(ev) {
const div = document.createElement('div');
const isAuth = ev.type === 'authorize' || ev.type === 'authorize-inner';
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'] || '-';
const kind = ev.type === 'authorize-inner' ? 'EAP Inner' : (isAuth ? 'Authorize' : 'Accounting');
div.innerHTML = `
<div><strong>${kind}</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>${ev.vlan?` • VLAN: <code>${ev.vlan}</code>`:''}${ev.bandwidth?` • 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 = '';
history = data.items || [];
total = 0;
history.forEach(renderItem);
}
btnRefresh.addEventListener('click', loadHistory);
btnSelfTest.addEventListener('click', async () => {
selfTestStatus.textContent = 'Running…';
try {
const r = await fetch('/test/radius', { method: 'POST' });
const data = await r.json();
if (data.ok) selfTestStatus.textContent = `OK (${data.result.code}, ${data.result.rtt_ms}ms)`;
else selfTestStatus.textContent = `Error (${data.error||r.status})`;
} catch (e) {
selfTestStatus.textContent = `Error (${e.message})`;
}
});
btnClear.addEventListener('click', async () => {
if (!confirm('¿Limpiar historial de eventos?')) return;
await fetch('/api/requests', { method: 'DELETE' });
await loadHistory();
});
btnCopy.addEventListener('click', async () => {
const text = JSON.stringify(history, null, 2);
try {
async function copyText(t) {
if (navigator.clipboard && navigator.clipboard.writeText) {
return navigator.clipboard.writeText(t);
}
const ta = document.createElement('textarea');
ta.value = t;
ta.style.position = 'fixed';
ta.style.opacity = '0';
ta.setAttribute('readonly', '');
document.body.appendChild(ta);
ta.focus();
ta.select();
ta.setSelectionRange(0, ta.value.length);
const ok = document.execCommand('copy');
document.body.removeChild(ta);
if (!ok) throw new Error('copy command failed');
}
await copyText(text);
alert('Copiado al portapapeles');
} catch (e) {
alert('No se pudo copiar: ' + (e && e.message ? e.message : e));
}
});
btnExportCsv.addEventListener('click', () => {
const a = document.createElement('a');
a.href = '/api/requests.csv';
a.download = '';
document.body.appendChild(a);
a.click();
a.remove();
});
// 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 {}
});
es.addEventListener('clear', () => { loadHistory(); });
}
loadHistory().then(connectSSE);
</script>
</body>
</html>