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

225 lines
8.6 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>
<span class="chip" id="radiusState" style="background:#fee; color:#900; display:none;">RADIUS: Recargando…</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>
<button id="addUser">Añadir usuario</button>
</div>
<div id="list" class="list"></div>
<div id="modal" style="display:none; position:fixed; inset:0; background:rgba(0,0,0,0.4); align-items:center; justify-content:center;">
<div style="background:#fff; color:#222; padding:16px; border-radius:8px; min-width:300px; max-width:90%;">
<h3 style="margin-top:0;">Nuevo usuario</h3>
<div style="display:flex; flex-direction:column; gap:8px;">
<label>Usuario <input id="u_username" /></label>
<label>Contraseña <input id="u_password" type="password" /></label>
<label>VLAN <input id="u_vlan" type="number" min="1" value="2" /></label>
</div>
<div style="display:flex; gap:8px; margin-top:12px;">
<button id="saveUser">Guardar</button>
<button id="cancelUser">Cancelar</button>
</div>
</div>
</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');
const radiusState = document.getElementById('radiusState');
let total = 0;
const btnClear = document.getElementById('clear');
const btnCopy = document.getElementById('copy');
const btnExportCsv = document.getElementById('exportCsv');
const btnAddUser = document.getElementById('addUser');
const modal = document.getElementById('modal');
const u_username = document.getElementById('u_username');
const u_password = document.getElementById('u_password');
const u_vlan = document.getElementById('u_vlan');
const saveUser = document.getElementById('saveUser');
const cancelUser = document.getElementById('cancelUser');
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();
});
btnAddUser.addEventListener('click', () => {
modal.style.display = 'flex';
u_username.value = '';
u_password.value = '';
u_vlan.value = '2';
u_username.focus();
});
cancelUser.addEventListener('click', () => {
modal.style.display = 'none';
});
saveUser.addEventListener('click', async () => {
const username = u_username.value.trim();
const password = u_password.value;
const vlan = u_vlan.value || '2';
if (!username || !password) {
alert('Usuario y contraseña son obligatorios');
return;
}
try {
const r = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password, vlan })
});
const data = await r.json();
if (!data.ok) throw new Error(data.error || 'Error desconocido');
modal.style.display = 'none';
alert('Usuario guardado');
} catch (e) {
alert('Error al guardar: ' + e.message);
}
});
// 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('status', (e) => {
try {
const s = JSON.parse(e.data);
radiusState.style.display = s.radius_reloading ? 'inline-block' : 'none';
} catch {}
});
es.addEventListener('clear', () => { loadHistory(); });
}
loadHistory().then(connectSSE);
</script>
</body>
</html>