Add v9.1.1: Device registry and remote blocking system
- Implement DeviceRegistry for remote device management - Add dashboard for device tracking and blocking - Remote device blocking capability with admin control - Support for device aliasing and notes - Enhanced device management interface - Dashboard with real-time device status - Configurable registry URL in build config 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
116
dashboard/public/app.js
Normal file
116
dashboard/public/app.js
Normal file
@@ -0,0 +1,116 @@
|
||||
const tableBody = document.getElementById('devicesTable');
|
||||
const refreshBtn = document.getElementById('refreshBtn');
|
||||
const statusMessage = document.getElementById('statusMessage');
|
||||
|
||||
async function fetchDevices() {
|
||||
setStatus('Cargando dispositivos...');
|
||||
try {
|
||||
const response = await fetch('/api/devices');
|
||||
if (!response.ok) {
|
||||
throw new Error('Error ' + response.status);
|
||||
}
|
||||
const { devices } = await response.json();
|
||||
renderTable(devices || []);
|
||||
setStatus('Actualizado ' + new Date().toLocaleTimeString());
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
setStatus('No se pudo cargar el listado');
|
||||
}
|
||||
}
|
||||
|
||||
function renderTable(devices) {
|
||||
tableBody.innerHTML = '';
|
||||
if (!devices.length) {
|
||||
tableBody.innerHTML = '<tr><td colspan="7" class="empty">Sin registros</td></tr>';
|
||||
return;
|
||||
}
|
||||
devices.sort((a, b) => (a.lastSeen || '').localeCompare(b.lastSeen || '') * -1);
|
||||
for (const device of devices) {
|
||||
const tr = document.createElement('tr');
|
||||
if (device.blocked) {
|
||||
tr.classList.add('blocked');
|
||||
}
|
||||
const alias = device.alias && device.alias.trim().length ? device.alias : 'Sin alias';
|
||||
tr.innerHTML = `
|
||||
<td>
|
||||
<div class="alias">${alias}</div>
|
||||
<div class="device-name">${device.deviceName || '(sin nombre)'} </div>
|
||||
</td>
|
||||
<td>${device.deviceId}</td>
|
||||
<td>${[device.manufacturer, device.model].filter(Boolean).join(' ')}</td>
|
||||
<td>${device.appVersionName || ''} (${device.appVersionCode || ''})</td>
|
||||
<td>${formatDate(device.lastSeen)}</td>
|
||||
<td>${device.blocked ? 'Bloqueado' : 'Activo'}</td>
|
||||
<td class="actions-cell">
|
||||
<button data-action="alias">Alias</button>
|
||||
${device.blocked ? '<button data-action="unblock" class="primary">Desbloquear</button>' : '<button data-action="block" class="danger">Bloquear</button>'}
|
||||
</td>
|
||||
`;
|
||||
tr.dataset.deviceId = device.deviceId;
|
||||
tableBody.appendChild(tr);
|
||||
}
|
||||
}
|
||||
|
||||
function formatDate(value) {
|
||||
if (!value) return '-';
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) return value;
|
||||
return date.toLocaleString();
|
||||
}
|
||||
|
||||
async function blockDevice(deviceId) {
|
||||
const reason = prompt('Motivo del bloqueo (opcional):');
|
||||
await fetch(`/api/devices/${encodeURIComponent(deviceId)}/block`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ reason })
|
||||
});
|
||||
await fetchDevices();
|
||||
}
|
||||
|
||||
async function unblockDevice(deviceId) {
|
||||
await fetch(`/api/devices/${encodeURIComponent(deviceId)}/unblock`, { method: 'POST' });
|
||||
await fetchDevices();
|
||||
}
|
||||
|
||||
async function updateAlias(deviceId) {
|
||||
const alias = prompt('Nuevo alias para el dispositivo:');
|
||||
if (alias === null) {
|
||||
return;
|
||||
}
|
||||
await fetch(`/api/devices/${encodeURIComponent(deviceId)}/alias`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ alias })
|
||||
});
|
||||
await fetchDevices();
|
||||
}
|
||||
|
||||
function setStatus(message) {
|
||||
statusMessage.textContent = message;
|
||||
}
|
||||
|
||||
refreshBtn.addEventListener('click', fetchDevices);
|
||||
|
||||
tableBody.addEventListener('click', async (event) => {
|
||||
const button = event.target.closest('button');
|
||||
if (!button) return;
|
||||
const tr = button.closest('tr');
|
||||
const deviceId = tr && tr.dataset.deviceId;
|
||||
if (!deviceId) return;
|
||||
const action = button.dataset.action;
|
||||
try {
|
||||
if (action === 'block') {
|
||||
await blockDevice(deviceId);
|
||||
} else if (action === 'unblock') {
|
||||
await unblockDevice(deviceId);
|
||||
} else if (action === 'alias') {
|
||||
await updateAlias(deviceId);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
alert('Operación fallida');
|
||||
}
|
||||
});
|
||||
|
||||
fetchDevices();
|
||||
39
dashboard/public/index.html
Normal file
39
dashboard/public/index.html
Normal file
@@ -0,0 +1,39 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>StreamPlayer Device Dashboard</title>
|
||||
<link rel="stylesheet" href="styles.css" />
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>Control de Dispositivos StreamPlayer</h1>
|
||||
<p>Visualiza instalaciones activas y bloquea acceso con un clic.</p>
|
||||
</header>
|
||||
<section class="actions">
|
||||
<button id="refreshBtn">Actualizar listado</button>
|
||||
<span id="statusMessage"></span>
|
||||
</section>
|
||||
<section class="table-wrapper">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Alias / Nombre</th>
|
||||
<th>Device ID</th>
|
||||
<th>Modelo</th>
|
||||
<th>Versión app</th>
|
||||
<th>Última vez visto</th>
|
||||
<th>Estado</th>
|
||||
<th>Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="devicesTable">
|
||||
<tr>
|
||||
<td colspan="7" class="empty">No hay datos aún</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
89
dashboard/public/styles.css
Normal file
89
dashboard/public/styles.css
Normal file
@@ -0,0 +1,89 @@
|
||||
:root {
|
||||
font-family: 'Segoe UI', Tahoma, sans-serif;
|
||||
color: #202124;
|
||||
background: #f7f7f7;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
header {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
margin-bottom: 0.2rem;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
button {
|
||||
border: none;
|
||||
background: #4285f4;
|
||||
color: #fff;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
button.danger {
|
||||
background: #d93025;
|
||||
}
|
||||
|
||||
button.primary {
|
||||
background: #1a73e8;
|
||||
}
|
||||
|
||||
.table-wrapper {
|
||||
overflow-x: auto;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
th, td {
|
||||
padding: 0.8rem;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
th {
|
||||
background: #fafafa;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
tr.blocked {
|
||||
background: #fff4f4;
|
||||
}
|
||||
|
||||
.actions-cell button {
|
||||
margin-right: 0.3rem;
|
||||
}
|
||||
|
||||
.alias {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.device-name {
|
||||
font-size: 0.85rem;
|
||||
color: #5f6368;
|
||||
}
|
||||
|
||||
.empty {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
color: #5f6368;
|
||||
}
|
||||
Reference in New Issue
Block a user