Add device diagnostics and P2P access controls

This commit is contained in:
Jonathan Sykes
2026-10-14 02:45:33 +08:00
parent e9a597c415
commit 5177a4d441
11 changed files with 147 additions and 8 deletions

View File

@@ -749,6 +749,11 @@
<div class="card"><h2>24-hour p95 history</h2><div id="opsHistory" class="analytics-grid"></div></div>
<div id="opsAlerts" aria-live="polite"></div>
</section>
<section id="p-devices" class="panel">
<div class="row"><div><h1 class="page">P2P devices</h1><p class="lead">Presence, stored holdings and device-level sharing controls.</p></div><span class="grow"></span><button class="btn" id="devicesRefresh" type="button">Refresh</button></div>
<div class="analytics-grid" id="deviceHealth"></div><p id="devicesStatus" class="msg" role="status" aria-live="polite"></p>
<div class="card"><div class="scroll"><table class="tbl" id="deviceTable"></table></div></div>
</section>
<section id="p-deep-search" class="panel deep-search">
<h1 class="page">Deep search</h1>
@@ -848,6 +853,7 @@
index: '<path d="M4 19V5M4 19h16M8 15l3-4 3 2 5-7"/><circle cx="19" cy="6" r="1.5"/>',
library: '<path d="M9 18V6l10-2v12"/><circle cx="6.5" cy="18" r="2.5"/><circle cx="16.5" cy="16" r="2.5"/>',
peers: '<circle cx="6" cy="12" r="2.5"/><circle cx="18" cy="6" r="2.5"/><circle cx="18" cy="18" r="2.5"/><path d="M8.3 10.9l7.4-3.8M8.3 13.1l7.4 3.8"/>',
devices: '<rect x="6" y="3" width="12" height="18" rx="2"/><path d="M10 17h4"/>',
history: '<path d="M3 12a9 9 0 109-9 9 9 0 00-6.4 2.6L3 8"/><path d="M3 3v5h5M12 7v5l3 2"/>',
backups: '<path d="M4 5h16v15H4zM8 5V3h8v2M8 11h8M8 15h5"/>',
analytics: '<path d="M4 20h16M7 16V9M12 16V4M17 16v-5"/>',
@@ -856,7 +862,7 @@
audit: '<path d="M5 4h14v16H5zM8 8h8M8 12h8M8 16h5"/>',
access: '<circle cx="8" cy="15" r="4"/><path d="M11 12l9-9M16 7l3 3M14 9l2 2"/>',
};
const TABS = [['editor', 'Editor'], ['quality', 'Lyrics queue'], ['lyric-search', 'Lyrics search'], ['cache', 'Cache'], ['index', 'Search index'], ['reports', 'Reports'], ['library', 'Library'], ['peers', 'P2P'], ['history', 'History'], ['backups', 'Backups'], ['analytics', 'Stats'], ['ops', 'Performance'], ['deep-search', 'Deep search'], ['audit', 'Audit log'], ['access', 'API']];
const TABS = [['editor', 'Editor'], ['quality', 'Lyrics queue'], ['lyric-search', 'Lyrics search'], ['cache', 'Cache'], ['index', 'Search index'], ['reports', 'Reports'], ['library', 'Library'], ['peers', 'P2P'], ['devices', 'Devices'], ['history', 'History'], ['backups', 'Backups'], ['analytics', 'Stats'], ['ops', 'Performance'], ['deep-search', 'Deep search'], ['audit', 'Audit log'], ['access', 'API']];
let tab = 'editor';
function buildTabs() {
const html = TABS.map(([k, l]) => `<button class="tab" role="tab" data-tab="${k}" aria-selected="false"><svg viewBox="0 0 24 24">${ICONS[k]}</svg><span>${l}</span>${k === 'reports' ? '<span class="badge hidden" data-badge>0</span>' : ''}</button>`).join('');
@@ -882,6 +888,7 @@
if (name === 'access') loadTokens();
if (name === 'analytics') loadAnalytics();
if (name === 'ops') loadOpsMetrics();
if (name === 'devices') loadDevices();
if (name === 'deep-search') loadDeepSearch();
if (name === 'audit') loadAudit();
window.scrollTo({ top: 0 });
@@ -973,6 +980,25 @@
} catch (error) { status.textContent = error.message || 'Monitoring unavailable.'; }
}
$('opsRefresh')?.addEventListener('click', loadOpsMetrics);
async function loadDevices() {
$('devicesStatus').textContent = 'Loading registered devices…';
try {
const j = await api('/api/admin/p2p/devices');
$('deviceHealth').innerHTML = [['Online now',j.network.online],['Verified content',j.network.content],['Intake active',j.network.intakeActive],['Open intake',j.network.intakeOpen]].map(([a,b])=>`<div class="metric"><span>${esc(a)}</span><b>${esc(b)}</b></div>`).join('');
$('deviceTable').innerHTML = '<tr><th>Device</th><th>Presence</th><th>Holdings</th><th>Traffic</th><th>Controls</th></tr>' + (j.devices.map(d=>`<tr><td><b>${esc(d.fingerprintShort)}</b><br><small>${esc(d.profile)} · ${esc(d.userAgent)}</small></td><td>${d.banned?'Banned':d.online?'Online':'Last seen '+new Date(d.lastSeenAt).toLocaleString()}</td><td>${d.holdings} · ${bytes(d.holdingBytes)}</td><td>Sent ${bytes(d.bytesUp)} · received ${bytes(d.bytesDown)}</td><td><div class="row"><button class="btn sm" data-device-sync="${esc(d.id)}" ${!d.online?'disabled':''}>Re-announce</button><button class="btn sm" data-device-holdings="${esc(d.id)}">Revoke holdings</button><button class="btn sm dng" data-device-ban="${esc(d.id)}" data-banned="${d.banned}">${d.banned?'Unban':'Ban'}</button></div></td></tr>`).join('') || '<tr><td colspan="5" class="muted">No devices registered.</td></tr>');
$('devicesStatus').textContent = `${j.devices.length} devices shown`;
} catch (error) { $('devicesStatus').textContent = error.message || 'Could not load devices.'; }
}
$('devicesRefresh')?.addEventListener('click', loadDevices);
$('deviceTable')?.addEventListener('click', async e => {
const b=e.target.closest('button'); if(!b)return;
try {
if (b.dataset.deviceSync) await api(`/api/admin/p2p/devices/${b.dataset.deviceSync}/reannounce`,{method:'POST',body:{}});
if (b.dataset.deviceHoldings && confirm('Remove all reported holdings for this device?')) await api(`/api/admin/p2p/devices/${b.dataset.deviceHoldings}/holdings`,{method:'DELETE'});
if (b.dataset.deviceBan) await api(`/api/admin/p2p/devices/${b.dataset.deviceBan}/ban`,{method:'POST',body:{banned:b.dataset.banned!=='true'}});
await loadDevices();
} catch(error) { $('devicesStatus').textContent=error.message || 'Device action failed.'; }
});
async function loadCollections() {
try {
const j = await api('/api/admin/collections'); if (!j.ok) return;