aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static/app.js
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/app.js')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/app.js57
1 files changed, 56 insertions, 1 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/app.js b/packages/meshbay-hub/src/meshbay_hub/static/app.js
index 18ce83a..cf33d72 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/app.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/app.js
@@ -2587,10 +2587,25 @@ function VideoPlayer({ entry, transportRef, gekRef, onClose }) {
videoRef.current.removeEventListener('seeking', onSeeking);
}
if (transport) {
+ // Tell the node first: dropping the handlers only makes us deaf, and a
+ // stream nobody is listening to still occupies a transcode slot.
+ transport.stopStream();
transport.onStreamInit = null;
transport.onStreamData = null;
transport.onStreamEnd = null;
}
+ // The queue can hold several megabytes of decrypted video.
+ queueRef.current = [];
+ const ms = msRef.current;
+ if (ms && ms.readyState === 'open') {
+ try { ms.endOfStream(); } catch { /* already ended */ }
+ }
+ if (blobUrlRef.current) {
+ URL.revokeObjectURL(blobUrlRef.current);
+ blobUrlRef.current = null;
+ }
+ sbRef.current = null;
+ msRef.current = null;
};
}, [entry, flushQueue]);
@@ -3056,6 +3071,7 @@ function AdminPage({ token }) {
const [logEvent, setLogEvent] = useState('');
const [logOffset, setLogOffset] = useState(0);
const [blocklist, setBlocklist] = useState([]);
+ const [nodes, setNodes] = useState([]);
const [detailUser, setDetailUser] = useState(null);
const [error, setError] = useState('');
@@ -3105,6 +3121,10 @@ function AdminPage({ token }) {
if (tab === 'stats') loadStats();
else if (tab === 'users') loadUsers(userSearch);
else if (tab === 'groups') loadGroups();
+ else if (tab === 'nodes') {
+ hubFetch('/v1/admin/nodes', { token })
+ .then(d => setNodes(d.nodes || [])).catch(e => setError(e.message));
+ }
else if (tab === 'logs') { setLogOffset(0); loadLogs(logEvent, 0); }
else if (tab === 'blocklist') loadBlocklist();
}, [tab]);
@@ -3157,7 +3177,7 @@ function AdminPage({ token }) {
} catch (e) { setError(e.message); }
}, [token]);
- const TABS = ['stats', 'users', 'groups', 'logs', 'blocklist'];
+ const TABS = ['stats', 'users', 'groups', 'nodes', 'logs', 'blocklist'];
return html`
<div>
@@ -3267,6 +3287,41 @@ function AdminPage({ token }) {
</table>
`}
+ ${tab === 'nodes' && html`
+ <p class="settings-hint" style="margin-bottom:10px">
+ ${t('admin.nodes_hint')}
+ </p>
+ <table class="admin-table">
+ <thead><tr>
+ <th>${t('admin.col_username')}</th>
+ <th>${t('admin.col_observed_ip')}</th>
+ <th>${t('admin.col_hint')}</th>
+ <th>${t('admin.col_last_seen')}</th>
+ <th>${t('admin.col_status')}</th>
+ </tr></thead>
+ <tbody>
+ ${nodes.length === 0 && html`
+ <tr><td colspan="5" class="admin-empty">${t('admin.no_nodes')}</td></tr>
+ `}
+ ${nodes.map(n => html`
+ <tr key=${n.id}>
+ <td>${n.username || n.user_id.slice(0, 8)}</td>
+ <td style="font-family:monospace">${n.observed_ip || '—'}</td>
+ <td style="font-family:monospace;color:var(--text-dim)">
+ ${n.endpoint_hint || '—'}
+ </td>
+ <td>${n.last_seen ? new Date(n.last_seen).toLocaleString() : '—'}</td>
+ <td>
+ <span class="badge ${n.online ? 'badge-ok' : ''}">
+ ${n.online ? t('admin.node_online') : t('admin.node_offline')}
+ </span>
+ </td>
+ </tr>
+ `)}
+ </tbody>
+ </table>
+ `}
+
${tab === 'logs' && html`
<div class="admin-toolbar">
<select class="admin-select" value=${logEvent} onChange=${e => {