HAD-155, HAD-172 and HAD-170. Stacked on fix/stuck-waiting-games; target that branch.
Three ways the app and the record could disagree.
Writes that were never checked (HAD-155). handleResign, handleDrawOffer,
handleDrawRetract, handleDrawAccept, handleAbandon and joinGame all
mutated local state and then wrote to the PDS with no error handling. Resign
offline and your board says you resigned while the record says the game is live.
Rather than adding rollback, each handler now writes first and only moves the
local game on once the write succeeds, which is the ticket's own preferred option
and deletes the need for a clearResult() store method. moveError becomes
actionError since it now covers more than moves.
Controls you could not reach (HAD-172). A waiting game nobody joined could
never be cancelled, so it sat in the public live feed forever; and the claim-win
button was nested inside a moveCount > 0 block, so a game stalled at zero moves
could never be claimed either. Adds deleteGame, a Cancel button on the waiting
card, and moves the claim-win button out into its own block. canAbandon loses
its waiting branch: cancelling is a delete, not a win claim.
Reads that silently truncated (HAD-170). listRecords returned one page, so a
player's 101st game was invisible and a pairing lookup that ran out of pages
looked exactly like one that found nothing. That is the last remaining way to
produce the false "no match" that HAD-233 closed off from the other side.
scanGameRecords walks pages until the cursor runs out, capped at 10 pages, and
stops early once the visitor finds a match.