Intent. Make openTable's own promise — "a table is never open with nobody at it" — true, by giving a host a way to close a table and refusing the leave that empties one.
Measured on main today (dd58653, locally, signed identity): alice opens "Ghost", then POST /api/tables/1/leave succeeds. The table stays open with seats: [], seated: 0, host_name: alice — a host who holds no seat — and the lobby renders it forever as 0/4 seated · nobody yet. Nobody can ever start it, nobody can ever remove it, and every stranger's first view of the app is a shelf of dead tables. src/actions.ts says in a comment that this cannot happen; it can.
Done looks like
[ ] A host is refused leaveTable on their own table, with a named conflict and the sentence a player can act on ("A host closes the table instead of leaving it."). Any other seat leaves exactly as today.
[ ] closeTable(db, who, { id }) in src/actions.ts: the host of the table, or an admin, may close it while its status is open — never once it is playing, settled or impounded, where the ledger is the record and nothing may be erased. It deletes the game row, seats go with it by the cascade already in the schema, and it emits table.closed with the id.
[ ] canClose(who, table) beside canJoin/canLeave, so the page draws the button exactly when the action would agree, and no door invents the rule.
[ ] Every door, because that is the shape: POST /tables/:id/close (form, back to / with the word), DELETE /api/tables/:id, a close_table MCP tool, and a key in src/tty.ts that asks for a confirmation first (the terminal is the one door with no undo button).
[ ] A guest gets signin_required, a player who is not the host gets forbidden, a table that has started gets conflict, and a table that is not there gets not_found — each with its status at the API door.
[ ] Tests that fail without it, in src/actions.test.ts and src/server.test.ts: the host's leave is refused and the seat survives; a closed table is gone from listTables and 404 from GET /api/tables/:id; a non-host cannot close; a playing table cannot be closed.
Files I expect to change.src/actions.ts, src/routes.ts, src/mcp.ts, src/tty.ts, src/page.ts, src/actions.test.ts, src/server.test.ts, src/tty.test.ts; README.md where it lists what you can do.
Risks. Deletion is the one irreversible act in the app — hence open only, where no act has ever been written and nothing was played. Do not add a fifth status: the CHECK on game.status would need the table rebuilt, and a table that never started is not history worth keeping. Watch that DELETE /api/tables/:id is same-origin-checked and rationed like every other write (it is, in src/server.ts, because it is not a read) and that the tty confirmation cannot be answered by a stray keypress.
Who. @mason, reviewed by @sentinel (an authorising delete at four doors) — it is ready now and touches no file the three open pulls are rewriting.
Intent. Make
openTable's own promise — "a table is never open with nobody at it" — true, by giving a host a way to close a table and refusing the leave that empties one.Measured on
maintoday (dd58653, locally, signed identity): alice opens "Ghost", thenPOST /api/tables/1/leavesucceeds. The table staysopenwithseats: [],seated: 0,host_name: alice— a host who holds no seat — and the lobby renders it forever as0/4 seated · nobody yet. Nobody can ever start it, nobody can ever remove it, and every stranger's first view of the app is a shelf of dead tables.src/actions.tssays in a comment that this cannot happen; it can.Done looks like
leaveTableon their own table, with a namedconflictand the sentence a player can act on ("A host closes the table instead of leaving it."). Any other seat leaves exactly as today.closeTable(db, who, { id })insrc/actions.ts: the host of the table, or anadmin, may close it while its status isopen— never once it isplaying,settledorimpounded, where the ledger is the record and nothing may be erased. It deletes thegamerow, seats go with it by the cascade already in the schema, and it emitstable.closedwith the id.canClose(who, table)besidecanJoin/canLeave, so the page draws the button exactly when the action would agree, and no door invents the rule.POST /tables/:id/close(form, back to/with the word),DELETE /api/tables/:id, aclose_tableMCP tool, and a key insrc/tty.tsthat asks for a confirmation first (the terminal is the one door with no undo button).signin_required, a player who is not the host getsforbidden, a table that has started getsconflict, and a table that is not there getsnot_found— each with its status at the API door.src/actions.test.tsandsrc/server.test.ts: the host's leave is refused and the seat survives; a closed table is gone fromlistTablesand 404 fromGET /api/tables/:id; a non-host cannot close; aplayingtable cannot be closed.Files I expect to change.
src/actions.ts,src/routes.ts,src/mcp.ts,src/tty.ts,src/page.ts,src/actions.test.ts,src/server.test.ts,src/tty.test.ts;README.mdwhere it lists what you can do.Risks. Deletion is the one irreversible act in the app — hence
openonly, where no act has ever been written and nothing was played. Do not add a fifthstatus: theCHECKongame.statuswould need the table rebuilt, and a table that never started is not history worth keeping. Watch thatDELETE /api/tables/:idis same-origin-checked and rationed like every other write (it is, insrc/server.ts, because it is not a read) and that the tty confirmation cannot be answered by a stray keypress.Who. @mason, reviewed by @sentinel (an authorising delete at four doors) — it is ready now and touches no file the three open pulls are rewriting.