From 9b5c63f23c712d2849d615e8e5b96480495d041c Mon Sep 17 00:00:00 2001 From: Nils Briggen Date: Mon, 8 Jun 2026 17:35:54 +0200 Subject: [PATCH] P15a: hosting a shared session requires a signed-in GM MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - The Host control now checks cloudUsername(): when the GM isn't signed in it shows "🔒 Sign in to host" (→ Settings) instead of "📡 Host". Players still join as guests. - Realtime e2e: the GM marks itself signed in before hosting. Co-Authored-By: Claude Opus 4.8 (1M context) --- e2e-realtime/realtime.spec.ts | 3 ++- e2e-realtime/seats.spec.ts | 3 ++- src/features/play/SessionControl.tsx | 11 +++++++++++ 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/e2e-realtime/realtime.spec.ts b/e2e-realtime/realtime.spec.ts index 64282b4..57e7b22 100644 --- a/e2e-realtime/realtime.spec.ts +++ b/e2e-realtime/realtime.spec.ts @@ -12,8 +12,9 @@ test('GM hosts a session; a player on another device sees live combat', async ({ const gmCtx = await browser.newContext(); const playerCtx = await browser.newContext(); - // GM: load the sample campaign and host a session. + // GM: load the sample campaign and host a session (hosting requires a signed-in GM). const gm = await fresh(gmCtx); + await gm.evaluate(() => localStorage.setItem('ttrpg-cloud-user', 'GM')); gm.on('dialog', (d) => d.dismiss()); // dismiss the optional-password prompt await gm.getByLabel('Settings').click(); await gm.getByRole('button', { name: 'Load sample campaign' }).click(); diff --git a/e2e-realtime/seats.spec.ts b/e2e-realtime/seats.spec.ts index e89e787..498742c 100644 --- a/e2e-realtime/seats.spec.ts +++ b/e2e-realtime/seats.spec.ts @@ -12,8 +12,9 @@ test('a player claims a seat and manages their character; edits round-trip to th const gmCtx = await browser.newContext(); const playerCtx = await browser.newContext(); - // GM: sample campaign + host. + // GM: sample campaign + host (hosting requires a signed-in GM). const gm = await fresh(gmCtx); + await gm.evaluate(() => localStorage.setItem('ttrpg-cloud-user', 'GM')); gm.on('dialog', (d) => d.dismiss()); // optional-password prompt await gm.getByLabel('Settings').click(); await gm.getByRole('button', { name: 'Load sample campaign' }).click(); diff --git a/src/features/play/SessionControl.tsx b/src/features/play/SessionControl.tsx index d5feed1..095bdf1 100644 --- a/src/features/play/SessionControl.tsx +++ b/src/features/play/SessionControl.tsx @@ -1,13 +1,16 @@ import { useState } from 'react'; +import { useNavigate } from '@tanstack/react-router'; import { useLiveQuery } from 'dexie-react-hooks'; import { useSessionStore, type SeatRequest } from '@/stores/sessionStore'; import { hostSession, stopSession, grantSeat } from '@/lib/sync/wsSync'; import { charactersRepo } from '@/lib/db/repositories'; +import { cloudUsername } from '@/lib/cloud/client'; import { useActiveCampaign } from '@/features/campaigns/hooks'; import { Button } from '@/components/ui/Button'; /** Header control: GM hosts a live session, shares the code, and approves seats. */ export function SessionControl() { + const navigate = useNavigate(); const campaign = useActiveCampaign(); const role = useSessionStore((s) => s.role); const status = useSessionStore((s) => s.status); @@ -20,6 +23,14 @@ export function SessionControl() { if (role === 'off') { if (!campaign) return null; + // Hosting a shared session requires the GM to be signed in to an account. + if (!cloudUsername()) { + return ( + + ); + } return (