The keeper can bring back a seat a browser lost: a new token on the ledger, the old one refused, and a link that carries it in the fragment, minted by hnefatafl-reseat.sh through a /keeper door Caddy does not proxy and that turns away anything Caddy forwarded; the join page takes a linked seat only once the server accepts it and never over a seat this browser holds that still opens, a spectator at a game in play is told both ways back, the hall asks before forgetting a game in play, and the server's tests now run with the rest
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MbtUPJ24P6vc6tE1x6RS3m
This commit is contained in:
1 parent
4b6cf01949
commit
8bbc41cc87
12 files changed
+194
-9
No files matched your search
@@ -146,7 +146,14 @@
|
||||
if (c) void goto(`/join/${c}`);
|
||||
}
|
||||
|
||||
/** A game in play asks before it is forgotten: this browser may be the only thing that holds the seat. */
|
||||
let forgetting = $state<string | null>(null);
|
||||
function forget(roomId: string) {
|
||||
if (!games[roomId]?.over && forgetting !== roomId) {
|
||||
forgetting = roomId;
|
||||
return;
|
||||
}
|
||||
forgetting = null;
|
||||
forgetSeat(roomId);
|
||||
seats = allSeats();
|
||||
}
|
||||
@@ -253,7 +260,11 @@
|
||||
<span class="ledger-code">{held.roomId}</span>
|
||||
<span class="ledger-info">{status(held)}{#if unread(held) > 0} · <span class="talk-new">{unread(held)} new {unread(held) === 1 ? 'line' : 'lines'} of talk</span>{/if}</span>
|
||||
</a>
|
||||
<button type="button" class="dismiss" title="forget this game" onclick={() => forget(held.roomId)}>×</button>
|
||||
{#if forgetting === held.roomId}
|
||||
<span class="forget-ask">This browser may be all that holds your seat. <button type="button" class="quiet" onclick={() => forget(held.roomId)}>Forget it</button> <button type="button" class="quiet" onclick={() => (forgetting = null)}>Keep it</button></span>
|
||||
{:else}
|
||||
<button type="button" class="dismiss" title="forget this game" onclick={() => forget(held.roomId)}>×</button>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
{#if reports.length}
|
||||
@@ -478,6 +489,11 @@
|
||||
background: rgba(240, 165, 58, 0.08);
|
||||
}
|
||||
|
||||
.forget-ask {
|
||||
font-size: 0.85em;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.ledger-row.your-move .ledger-info {
|
||||
color: var(--ember);
|
||||
}
|
||||
|
||||
@@ -19,6 +19,12 @@ function heldSeats(): Record<string, HeldSeat> {
|
||||
}
|
||||
}
|
||||
|
||||
/** A seat the keeper sent in a link, `#seat=<seat>.<token>`: a fragment stays in the browser, out of every log. */
|
||||
export function seatInLink(hash: string): HeldSeat | null {
|
||||
const m = /^#seat=([^.]+)\.([A-Za-z0-9_-]+)$/.exec(hash);
|
||||
return m ? { seat: m[1], token: m[2] } : null;
|
||||
}
|
||||
|
||||
export function seatFor(roomId: string): HeldSeat | null {
|
||||
return heldSeats()[roomId] ?? null;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { goto, replaceState } from '$app/navigation';
|
||||
import { page } from '$app/state';
|
||||
import Board from '$lib/components/Board.svelte';
|
||||
import Lobby from '$lib/components/Lobby.svelte';
|
||||
import ReportSlip from '$lib/components/ReportSlip.svelte';
|
||||
import Preferences from '$lib/components/Preferences.svelte';
|
||||
import { seatFor } from '$lib/net/client';
|
||||
import { rememberSeat, seatFor, seatInLink, trustSeat, type HeldSeat } from '$lib/net/client';
|
||||
import { NAME_MAX, playerName, rememberName, Room } from '$lib/net/room.svelte';
|
||||
|
||||
const roomId = $derived((page.params.code ?? '').toUpperCase());
|
||||
@@ -25,16 +25,31 @@
|
||||
const link = $derived(`${location.origin}/join/${roomId}`);
|
||||
const watching = $derived(room?.spectating ?? false);
|
||||
|
||||
// Return to a held seat, or take a place in the gallery and offer one.
|
||||
// Return to a held seat, or take a place in the gallery and offer one. A link from the keeper carries a seat:
|
||||
// it leaves the address bar at once, is kept only once the server takes it, and never replaces a seat that
|
||||
// still opens, so a link someone else sends cannot take a working seat away.
|
||||
$effect(() => {
|
||||
const id = roomId;
|
||||
room = null;
|
||||
error = '';
|
||||
const sent = seatInLink(location.hash);
|
||||
if (sent) replaceState(location.pathname + location.search, {});
|
||||
const held = seatFor(id);
|
||||
const arrive = held ? Room.resume(id, held) : Room.watch(id);
|
||||
joining = !held;
|
||||
const take = (s: HeldSeat) =>
|
||||
Room.resume(id, s).then((r) => {
|
||||
rememberSeat(id, s);
|
||||
trustSeat(id);
|
||||
return r;
|
||||
});
|
||||
const arrive = held ? Room.resume(id, held).catch((e: Error) => (sent ? take(sent) : Promise.reject(e))) : sent ? take(sent).catch(() => Room.watch(id)) : Room.watch(id);
|
||||
joining = !held && !sent;
|
||||
if (!held) name = playerName();
|
||||
arrive.then((r) => (room = r)).catch((e: Error) => (error = e.message));
|
||||
arrive
|
||||
.then((r) => {
|
||||
room = r;
|
||||
if (r.spectating) joining = true;
|
||||
})
|
||||
.catch((e: Error) => (error = e.message));
|
||||
});
|
||||
|
||||
// Keep the view fresh for as long as the room is on screen.
|
||||
@@ -171,6 +186,10 @@
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
{#if watching && room?.started && !room.view.over}
|
||||
<p class="notice muted">Had a seat at this table on another device? Transfer it from there and claim the phrase in the hall, or <a href="mailto:eric@ericwagoner.com?subject={encodeURIComponent(`My seat in room ${roomId}`)}">write to the keeper</a> for a link that brings it back.</p>
|
||||
{/if}
|
||||
|
||||
{#if room && !room.started}
|
||||
<Lobby {room} {link} {copied} {copyLink} />
|
||||
{:else if room}
|
||||
|
||||
Reference in new issue
Block a user