Pushed. The branch is green: 219 tests, including the new ones, and the server boots and hands a live no-cookie socket the spectator snapshot.
A connection that gets no seat — no session cookie, or a second client from an address that already holds one — now draws the watching view instead of throwing. static/val.js reached into snap.me on the way in (feed(timeline, data.me.id, …)), so a spectator's first frame threw and the page stopped drawing; the new static/watch.js recognises a seatless snapshot, turns it into the lines it can honestly show, and draws them on a flat panel with no map and no bodies. val.js takes that path on the way in, stops sending input it has nothing to drive, refuses pointer lock, and blanks the panels a seat owns (kit, roster, the two team tables) rather than leaving the server-rendered first paint to go stale.
Two small server changes came with it, both argued for by the issue's own description of the view: the spectator snapshot now carries the kill feed (names and a gun, never a place — the same shape a seat is sent, and the existing "no coordinate anywhere" assertion still holds), and a why of "address" or "cookie", because the two reasons have different answers for the person reading them. The team scoreboards are deliberately not sent to a watcher — the snapshot has no board, and adding per-player rosters to a sideless view is a call for whoever drew the spectator line, not for this fix — so those tables are emptied rather than left stale.
To see it in thirty seconds: open the preview, then open it again in a second tab. The second tab shows red 0 — 0 blue, the clock, the round line, and the sentence "Somebody at this address is already playing — that tab holds the seat, and this one watches", with the kill feed filling in the top right as bots trade. A private window with cookies blocked gets the other sentence. The first tab keeps playing as before.
Tests: src/watch.test.ts covers the view's text and drawing (one background rect, no world), src/client.test.ts now feeds the bundled client the server's real spectatorSnapshot() and renders a frame — it fails with TypeError: undefined is not an object (evaluating 'data.me.id') against the old val.js, which is the bug in the issue — and src/server.test.ts asserts the reason and the feed over a real socket. I could not click through a browser from here; the client was exercised through the repository's own stub-DOM harness against the live server, and the server was run and queried directly.
Pushed. The branch is green: 219 tests, including the new ones, and the server boots and hands a live no-cookie socket the spectator snapshot.
A connection that gets no seat — no session cookie, or a second client from an address that already holds one — now draws the watching view instead of throwing.
static/val.jsreached intosnap.meon the way in (feed(timeline, data.me.id, …)), so a spectator's first frame threw and the page stopped drawing; the newstatic/watch.jsrecognises a seatless snapshot, turns it into the lines it can honestly show, and draws them on a flat panel with no map and no bodies.val.jstakes that path on the way in, stops sending input it has nothing to drive, refuses pointer lock, and blanks the panels a seat owns (kit, roster, the two team tables) rather than leaving the server-rendered first paint to go stale.Two small server changes came with it, both argued for by the issue's own description of the view: the spectator snapshot now carries the kill feed (names and a gun, never a place — the same shape a seat is sent, and the existing "no coordinate anywhere" assertion still holds), and a
whyof"address"or"cookie", because the two reasons have different answers for the person reading them. The team scoreboards are deliberately not sent to a watcher — the snapshot has noboard, and adding per-player rosters to a sideless view is a call for whoever drew the spectator line, not for this fix — so those tables are emptied rather than left stale.To see it in thirty seconds: open the preview, then open it again in a second tab. The second tab shows
red 0 — 0 blue, the clock, the round line, and the sentence "Somebody at this address is already playing — that tab holds the seat, and this one watches", with the kill feed filling in the top right as bots trade. A private window with cookies blocked gets the other sentence. The first tab keeps playing as before.Tests:
src/watch.test.tscovers the view's text and drawing (one background rect, no world),src/client.test.tsnow feeds the bundled client the server's realspectatorSnapshot()and renders a frame — it fails withTypeError: undefined is not an object (evaluating 'data.me.id')against the oldval.js, which is the bug in the issue — andsrc/server.test.tsasserts the reason and the feed over a real socket. I could not click through a browser from here; the client was exercised through the repository's own stub-DOM harness against the live server, and the server was run and queried directly.🤖 Generated with Claude Code
Closes #12