diff options
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/static/app.js | 57 | ||||
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/static/i18n.js | 12 | ||||
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/static/transport.js | 35 |
3 files changed, 103 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 => { diff --git a/packages/meshbay-hub/src/meshbay_hub/static/i18n.js b/packages/meshbay-hub/src/meshbay_hub/static/i18n.js index 71f6cce..affa7c8 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/i18n.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/i18n.js @@ -195,7 +195,19 @@ const en = { 'admin.tab_stats': 'Stats', 'admin.tab_users': 'Users', 'admin.tab_groups': 'Groups', + 'admin.tab_nodes': 'Nodes', 'admin.tab_logs': 'Logs', + 'admin.col_observed_ip': 'Seen from', + 'admin.col_hint': 'Announced hint', + 'admin.col_last_seen': 'Last announce', + 'admin.node_online': 'online', + 'admin.node_offline': 'offline', + 'admin.no_nodes': 'No nodes registered', + 'admin.nodes_hint': '"Seen from" is the address the announcement arrived from, ' + + 'on a request signed with the node key — that is the one to answer a ' + + 'question with. The hint is what the node believes its own address to be, ' + + 'learned from a STUN server and sent to us; it is useful for reaching the ' + + 'node and is not evidence of anything.', 'admin.tab_blocklist': 'Blocklist', // Admin stats diff --git a/packages/meshbay-hub/src/meshbay_hub/static/transport.js b/packages/meshbay-hub/src/meshbay_hub/static/transport.js index 844a201..df2abb0 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/transport.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/transport.js @@ -562,6 +562,18 @@ class MeshBayTransport { } /** + * Nobody is watching any more. + * + * Closing the viewer used to say nothing to the node, which went on + * transcoding and holding one of its two slots until the credit timeout — so + * the next video answered "server busy". + */ + stopStream() { + if (!this._connected) return; + try { this._send({ type: 'stream_stop', v: '0.1' }); } catch { /* gone */ } + } + + /** * Push a whole file, several chunks in flight at once. * * One chunk per round trip is 48 KB of throughput per RTT no matter how much @@ -778,6 +790,11 @@ class MeshBayTransport { }, 30000); this._pending.set(id, { _reqType: obj.type, + // Chunks are the one request that runs several at a time and can be + // interleaved with anything else on the channel. Matching them by + // arrival order was only ever true by luck; this makes it true. + _key: obj.type === 'file_req' + ? `chunk:${obj.file_id}:${obj.chunk_index}` : null, resolve: (msg) => { clearTimeout(timeout); this._pending.delete(id); resolve(msg); }, reject: (err) => { clearTimeout(timeout); this._pending.delete(id); reject(err); }, }); @@ -857,6 +874,24 @@ class MeshBayTransport { return; } + if (msg.type === 'file_chunk') { + const key = `chunk:${msg.file_id}:${msg.chunk_index}`; + for (const [, handler] of this._pending) { + // A node from before the reply carried a file_id: fall back to the + // index, which is still better than the oldest pending request. + const match = msg.file_id + ? handler._key === key + : handler._key && handler._key.endsWith(`:${msg.chunk_index}`); + if (match) { + handler.resolve(msg); + return; + } + } + // Nobody asked for it any more — a cancelled download, most likely. It + // must not be handed to whatever request happens to be waiting. + return; + } + const oldest = this._pending.entries().next(); if (!oldest.done) { const [, handler] = oldest.value; |