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) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-04-11 15:25:08 +02:00
parent 7a31469d69
commit da4e840581
2 changed files with 32 additions and 4 deletions

18
main.py
View file

@ -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]

View file

@ -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
}