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 }