diff --git a/e2e-realtime/realtime.spec.ts b/e2e-realtime/realtime.spec.ts
index af3b045..31924e3 100644
--- a/e2e-realtime/realtime.spec.ts
+++ b/e2e-realtime/realtime.spec.ts
@@ -38,6 +38,14 @@ test('GM hosts a session; a player on another device sees live combat', async ({
// enemy exact HP must NOT be shown to players
await expect(player.getByText('7/7')).toHaveCount(0);
+ // Player navigates away — the live connection persists (badge stays) and
+ // returning still shows the synced table.
+ await player.getByLabel('Primary').getByRole('link', { name: 'Dice' }).click();
+ await expect(player.getByTestId('player-live')).toBeVisible();
+ await expect(player.getByTestId('player-live')).toContainText(code);
+ await player.getByTestId('player-live').getByRole('link').click();
+ await expect(player.getByText('Lia the Brave').first()).toBeVisible();
+
await gmCtx.close();
await playerCtx.close();
});
diff --git a/src/app/RootLayout.tsx b/src/app/RootLayout.tsx
index 7435725..abede25 100644
--- a/src/app/RootLayout.tsx
+++ b/src/app/RootLayout.tsx
@@ -11,6 +11,8 @@ import { Button } from '@/components/ui/Button';
import { SessionControl } from '@/features/play/SessionControl';
import { HandoutControl } from '@/features/play/HandoutControl';
import { useSessionBroadcaster } from '@/features/play/useSessionBroadcaster';
+import { usePlayerConnection } from '@/features/play/usePlayerConnection';
+import { PlayerSessionBadge } from '@/features/play/PlayerConnection';
const NAV = [
{ to: '/', label: 'Campaigns', exact: true },
@@ -62,6 +64,7 @@ export function RootLayout() {
const activeCampaign = useActiveCampaign();
// While the GM is hosting, mirror state to players (inert unless hosting).
useSessionBroadcaster(activeCampaign ?? null);
+ usePlayerConnection();
// Global Ctrl/Cmd+K opens the command palette.
useEffect(() => {
@@ -99,6 +102,7 @@ export function RootLayout() {
})}
+
{activeCampaign &&
}