From 05f4feab641740c944d636f29a03f8c0dd1328c7 Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Sat, 15 Aug 2026 19:01:08 +0200 Subject: fix: stop a stream on close, count only real users, record where a node is MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Closing the viewer left the node working.** Nothing told it to stop: the player dropped its handlers, which only made the browser deaf. ffmpeg kept running and held one of the node's two transcode slots until the credit timeout expired two minutes later — which is why the next video answered "server busy". `stream_stop` ends it at once, and the viewer also drops its queue, ends the MediaSource and revokes the object URL on the way out, any of which could be holding megabytes of decrypted video. While there: `file_chunk` replies were matched to their requests by arrival order, which was true by luck rather than by construction. The reply now names the file it belongs to and is matched on that and the chunk index; a chunk nobody is waiting for is dropped instead of being handed to whatever request happens to be oldest. **The administration panel counted its own history.** A deleted account is tombstoned so the connection log stays readable, and every count and list treated that row as a user — including a group's member count, and the member list of the group itself. They do not any more. **Where a node is.** `endpoint_hint` is what a node believes its address to be, learned from a STUN server and sent to us: useful for reaching it, and a claim. The announcement that carries it is signed with the node key over a fresh timestamp, so the address that request *arrives from* is the address of whoever holds that key — that is now recorded on the node row and shown in a Nodes tab, next to the hint, with the difference spelled out. Clients get the same treatment: `webrtc_offer` is logged with the address the hub saw when a browser starts a peer connection. Verified against the live deployment: the node's row reads 90.112.206.172 after a restart, and in e2e a stopped stream goes quiet in one message and the next one starts immediately instead of being refused. Co-Authored-By: Claude Opus 5 --- packages/meshbay-hub/src/meshbay_hub/static/app.js | 57 +++++++++++++++++++++- .../meshbay-hub/src/meshbay_hub/static/i18n.js | 12 +++++ .../src/meshbay_hub/static/transport.js | 35 +++++++++++++ 3 files changed, 103 insertions(+), 1 deletion(-) (limited to 'packages/meshbay-hub/src/meshbay_hub/static') 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`
@@ -3267,6 +3287,41 @@ function AdminPage({ token }) { `} + ${tab === 'nodes' && html` +

+ ${t('admin.nodes_hint')} +

+ + + + + + + + + + ${nodes.length === 0 && html` + + `} + ${nodes.map(n => html` + + + + + + + + `)} + +
${t('admin.col_username')}${t('admin.col_observed_ip')}${t('admin.col_hint')}${t('admin.col_last_seen')}${t('admin.col_status')}
${t('admin.no_nodes')}
${n.username || n.user_id.slice(0, 8)}${n.observed_ip || '—'} + ${n.endpoint_hint || '—'} + ${n.last_seen ? new Date(n.last_seen).toLocaleString() : '—'} + + ${n.online ? t('admin.node_online') : t('admin.node_offline')} + +
+ `} + ${tab === 'logs' && html`