Multiple admins

Adds Player.is_admin (creator starts with it; backfilled by migration).
Admin-key-authenticated POST /admin/set-admin grants/revokes it from the
admin panel; the creator's privileges can't be revoked. Admin-gated
backend routes (dev mode, skip character creation) and frontend controls
(corner menu, lobby start, scene start, end story) now check is_admin
instead of is_creator, and admins get the admin_key in their state blob
so granted players can open the admin panel.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-12 16:03:14 -07:00
parent e24e6d3b46
commit 5ad9c9db07
12 changed files with 203 additions and 27 deletions

View File

@@ -37,7 +37,8 @@
<div class="player-chip {p.id === state.player.id ? 'current-user' : ''}">
<span class="avatar-icon">🐀</span>
<span class="name">{p.name}</span>
{#if p.is_creator}<span class="creator-badge">Creator</span>{/if}
{#if p.is_creator}<span class="creator-badge">Creator</span>
{:else if p.is_admin}<span class="creator-badge">Admin</span>{/if}
</div>
{/each}
</div>
@@ -54,9 +55,9 @@
</div>
</div>
{#if state.player.is_creator}
{#if state.player.is_admin}
<div class="link-item admin-link-item">
<h4 class="gold-text">⚠️ Creator Admin Magic Link:</h4>
<h4 class="gold-text">⚠️ Admin Magic Link:</h4>
<p class="info-text">Save this admin link! You can use it to recover character sheets if another player loses their connection or link.</p>
<div class="copy-container">
<input type="text" readonly value="{window.location.origin}/#/game/{state.game.id}/admin" class="copy-input admin-input" id="admin-link-copy">
@@ -72,7 +73,7 @@
</div>
<div class="lobby-action">
{#if state.player.is_creator}
{#if state.player.is_admin}
{#if state.players.length >= 1}
<button on:click={startGame}
disabled={starting}

View File

@@ -44,7 +44,7 @@
$: allRolesAssigned = state.players.every(p => p.role !== null || p.needs_reroll);
$: deepPlayer = state.players.find(p => p.role === 'deep');
$: piratPlayer = state.players.find(p => p.role === 'pirat');
$: canStart = allRolesAssigned && deepPlayer && piratPlayer && (state.player.is_creator || state.player.role === 'deep');
$: canStart = allRolesAssigned && deepPlayer && piratPlayer && (state.player.is_admin || state.player.role === 'deep');
</script>
<div class="scene-setup-view text-center">

View File

@@ -344,7 +344,7 @@
{/if}
{/if}
{#if state.player.is_creator && state.game.phase === 'between_scenes'}
{#if state.player.is_admin && state.game.phase === 'between_scenes'}
<div class="end-story-box">
<p class="info-text" style="font-size: 0.85rem;">All good stories end. When the crew agrees the legend is complete:</p>
<button on:click={finishGame} class="btn btn-danger">🏴‍☠️ End the Story</button>