From da4e8405812605fea5fb8fc796d77448ed55ec9e Mon Sep 17 00:00:00 2001 From: Erik Date: Sat, 11 Apr 2026 15:25:08 +0200 Subject: [PATCH] fix(vitalsharing): clear stopped peers from the NetworkUI window Peers who unsubscribed or disconnected from vital sharing were lingering forever in the Vital Sharing browser window because nothing ever deleted them from the server-side state or told the browser to drop them. Backend: - share_unsubscribe now pops the character from _vital_sharing_peer_state (not just flips connected=false) and broadcasts a share_peer_removed envelope to browser clients. - On real plugin disconnect, do the same: pop the state entry and broadcast share_peer_removed so the NetworkUI updates immediately. Frontend: - New removeVitalSharingPeer(name) deletes from the local vitalSharingPeers dict and re-renders. - socket.onmessage now routes share_peer_removed to it. - refreshVitalSharingPeers() reconciles against the server's list and prunes any local entries the server no longer knows about, catching any race where the broadcast was missed. Co-Authored-By: Claude Opus 4.6 (1M context) --- main.py | 18 ++++++++++++++---- static/script.js | 18 ++++++++++++++++++ 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/main.py b/main.py index 55b874a8..ca0aae51 100644 --- a/main.py +++ b/main.py @@ -3204,9 +3204,13 @@ async def ws_receive_snapshots( char = data.get("character_name") if char: _vital_sharing_subscribers.discard(char) - if char in _vital_sharing_peer_state: - _vital_sharing_peer_state[char]["connected"] = False + _vital_sharing_peer_state.pop(char, None) logger.info(f"🤝 VITAL_SHARING_UNSUBSCRIBED: {char}") + # Tell browser clients to drop this peer from their UI + await _broadcast_to_browser_clients({ + "type": "share_peer_removed", + "character_name": char, + }) continue if msg_type and msg_type.startswith("share_"): @@ -3233,9 +3237,15 @@ async def ws_receive_snapshots( plugin_conns.pop(name, None) live_equipment_cantrip_states.pop(name, None) live_nearby_objects.pop(name, None) + was_sharing = name in _vital_sharing_subscribers or name in _vital_sharing_peer_state _vital_sharing_subscribers.discard(name) - if name in _vital_sharing_peer_state: - _vital_sharing_peer_state[name]["connected"] = False + _vital_sharing_peer_state.pop(name, None) + if was_sharing: + # Tell browser clients to drop this peer from their UI + await _broadcast_to_browser_clients({ + "type": "share_peer_removed", + "character_name": name, + }) # Clean up any plugin registrations for this socket to_remove = [n for n, ws in plugin_conns.items() if ws is websocket] diff --git a/static/script.js b/static/script.js index c89e124e..a23197fb 100644 --- a/static/script.js +++ b/static/script.js @@ -3074,6 +3074,8 @@ function initWebSocket() { const rw = radarWindows[msg.character_name]; if (rw) rw._radarDungeonLandblock = msg.landblock; } + } else if (msg.type === 'share_peer_removed') { + removeVitalSharingPeer(msg.character_name); } else if (typeof msg.type === 'string' && msg.type.startsWith('share_')) { updateVitalSharingPeer(msg); } @@ -4438,6 +4440,14 @@ function showCommentsSection(row, issue, win) { const vitalSharingPeers = {}; let _vitalSharingPollTimer = null; +function removeVitalSharingPeer(name) { + if (!name) return; + if (vitalSharingPeers[name]) { + delete vitalSharingPeers[name]; + renderVitalSharingWindow(); + } +} + function updateVitalSharingPeer(msg) { const name = msg.character_name; if (!name) return; @@ -4504,10 +4514,18 @@ async function refreshVitalSharingPeers() { const resp = await fetch('/vital-sharing/peers'); if (!resp.ok) return; const data = await resp.json(); + const serverNames = new Set(); (data.peers || []).forEach(p => { + serverNames.add(p.character_name); const existing = vitalSharingPeers[p.character_name] || {}; vitalSharingPeers[p.character_name] = { ...existing, ...p }; }); + // Prune any local entries the server no longer knows about (unsubscribed + // or disconnected). This catches any race where the share_peer_removed + // broadcast didn't arrive. + Object.keys(vitalSharingPeers).forEach(name => { + if (!serverNames.has(name)) delete vitalSharingPeers[name]; + }); } catch (e) { // ignore transient errors }