Fix CRITICAL player seat loss on WebSocket reconnect (stable playerId + seat grace)

A player who dropped briefly got a fresh server playerId on auto-reconnect and silently
lost their seat: the UI still showed 'granted' but every HP/condition/spell patch and dice
roll was rejected server-side. Now:

- The client persists a stable playerId (localStorage) and sends it on join.
- The server reuses that id when it isn't already live on another connection (so a second
  tab/peer can't hijack an active seat), and on reconnect re-binds the seat to the new
  socket and re-sends seatGranted — control is restored seamlessly.
- Seats survive a disconnect for a 5-minute grace window (marked, not deleted); sweep()
  evicts seats whose holder never returns.

Adds 3 server tests (reconnect reclaim, grace eviction, anti-hijack). 4 files: join
message gains optional playerId; server dispatch, RoomHub, and wsSync client.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-09 19:46:58 +02:00
parent 80c5d2a828
commit db829ff1a6
5 changed files with 110 additions and 10 deletions
+1 -1
View File
@@ -195,7 +195,7 @@ export function buildServer() {
const m = parsed.data;
switch (m.t) {
case 'host': hub.host(sender, m.password, m.resume); break;
case 'join': hub.join(sender, m.joinCode, m.password); break;
case 'join': hub.join(sender, m.joinCode, m.password, m.playerId); break;
case 'state': hub.state(sender, m.gmSecret, m.snapshot); break;
case 'image': hub.image(sender, m.gmSecret, m.id, m.dataUrl); break;
case 'requestImage': hub.requestImage(sender, m.id); break;