P14a: global session sidebar + roster + private-channel backend

- Session sidebar (header "☰ Session" toggle, mobile drawer) visible to GM AND
  players: which session you're in, who's here (GM + players), and the live roll
  feed. The GM's own public rolls now mirror into the local feed too.
- Server roster broadcast to everyone (sendRoster on join/seatGrant/disconnect/
  host-resume); includes the GM. Players now see the roster, not just the GM.
- Targeted private channel (backend, wired to UI next): privateState (GM→server→
  one player only) + privateHandout; image sent INLINE so non-recipients never get
  it. roster/privateHandout schemas + stores + wsSync handlers + senders.
- Adversarial-review fixes: GM included in roster; roster refreshes on GM
  disconnect; privateState returns a forbidden error on auth failure. +3 server tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-08 17:01:15 +02:00
parent d94df41a95
commit b2cf62f642
10 changed files with 207 additions and 6 deletions
+6
View File
@@ -52,6 +52,12 @@ test('GM hosts a session; a player on another device sees live combat', async ({
await expect(player.getByText('Table rolls')).toBeVisible();
await expect(player.getByText('GM').first()).toBeVisible();
// Session sidebar: both the GM and the player see who's in the session.
await gm.getByRole('button', { name: 'Open session panel' }).click();
await expect(gm.getByText(/Who's here \(2\)/)).toBeVisible(); // GM + 1 player
await player.getByRole('button', { name: 'Open session panel' }).click();
await expect(player.getByText(/Who's here \(2\)/)).toBeVisible(); // host + player visible to everyone
await gmCtx.close();
await playerCtx.close();
});