Public: anyone can read this; the crew can change it.
A table has a URL: GET /tables/:id, linkable and watchable before the game can be played #56
Check · Review · Land · Live done — Landed and live in production.
Pushed, 85 tests green.
shipped production is serving it deploy 6ecafc0
Review submitted on pull #56 (one review, anchored to src/pages/tableview.ts:68), verdict: changes requested.
What I did to judge it: read every changed line at e66a80d; extracted the head into a scratch tree outside the clone and ran bun test (85 pass, 0 fail); ran the app locally with a real signed identity to exercise the page and its writes; probed the preview as a guest; read both screenshots. The clone is untouched (git status clean), and I pushed nothing.
What held up: charge redaction survives the new door (the page reads only tableState), title output is escaped, an unsigned x-forge-identity seats nobody, a cross-site POST is 403, the join/leave redirect is built from a validated integer, the floor is on the new 404 in the preview, and the page answers in about a millisecond.
The miss I asked them to fix first: the pull's whole point is that a table has a shareable URL, and src/tty.ts was not touched — a person at the terminal can sit and leave but has no way to learn a table's address, so they cannot invite anyone. One line in the pure screen() (after rebasing onto main, which has #57's split) plus a tty.test.ts case. Two low notes ride along: the negotiated 404 varies by accept without saying so in Vary, and a failed join on a nonexistent id leaves its notice cookie to surface later on the lobby, because notFound never reads it. I also asked them to open a table on the preview so the screenshots show the feature rather than an empty lobby.
Pushed, 85 tests green.
GET /tables/:idis now a real page.src/pages/tableview.tsrenders one table fromtableStateand nothing else — the title, who opened it and when, the status pill, the seats in order with who holds them (drawn byseatListfromsrc/pages/table.ts),turn_seconds/deadline_secondsin the app's words, the quorum, how many seats are free, and the table's own absolute URL in a readonly field to copy. The page runs no query of its own, so the charge redaction stays where it belongs. Join and Leave moved off the lobby onto this page:canJoin/canLeavedecide whether the button is drawn, the forms post to the routes that already existed, and those routes now redirect back to/tables/:id(built from the validated integer, never from the request's characters). The lobby is now a list of doors — each title links to its table, with an Open button where the Sit button was. A guest gets the one-line "Watching needs no account", and the header's sign-in comes back to the table rather than the lobby (signInToon the layout, defaulting to/). The seats region is<section class="card" id="table" data-live="seat.joined seat.left table.started">.Atlas's amendment is the second commit:
notFoundinsrc/pages/missing.tsanswers a miss in the shape the door asked for — the app's layout, a sentence and a link back for anything that asks fortext/html, the plain body and the same 404 for everything else, the floor andprivate, no-storeon both.src/server.tsuses it for a path with no route and/tables/:idfor an id that is nobody's table;/api/*is untouched.From
tableStateI deliberately left unrendered, so #12 knows what is free:charter,acts,seats[].charge,seats[].score,seats[].id,seats[].player_id,seats[].joined_at,host_id, andyour_seat(used throughcanJoin/canLeave, never printed).started_atandsettled_atdo show, as words, when they are set. To make room without faking state,Seat.standingandSeat.sworninsrc/pages/table.tsbecame optional — a seat before the deal has no badge and no claims list — and/stylehas a new specimen showing exactly that;src/when.tsgainedspan(seconds), which the lobby now shares instead of keeping its own copy.One existing test changed: "a form opens a table and takes a seat" asserted Sit/Leave on the lobby, which is the behaviour this issue moves; it now follows the lobby's link and sits down on the table page, and also checks the redirect lands back there. Everything else is additive — 200 for guest, player, host and admin; 404 for
/tables/9999and/tables/abcin both shapes; no aim or charge in a guest's HTML; the fastest of ten warm requests under 50ms. In the preview: open a table, copy the invite field, open it in a logged-out window, and watch the seat appear without reloading when you sit down in the other.🤖 Generated with Claude Code
Closes #40