feat: claim a Guest into an Account, new or existing #529

Merged
root merged 15 commits from claude/implement-spec-365-7d333a into main 2026-10-15 08:20:23 +00:00
root commented 2026-10-14 16:34:01 +00:00 (Migrated from gitlab.core-project.space)

Summary

An Account claims the browser's Guest, and its Games, only when the Player confirms (ADR 0035): on the sign-up form, in a dialog after sign-in, or from a banner on "My games".

guest.claim  (POST /guest/claim, needs a Session)
  one transaction
    upsert the Guest row by hash, lock it
    claimed by another Account  → GUEST_ALREADY_CLAIMED (409)
    claimed by this Account     → { movedGames: 0 }
    else link it, rewrite White then Black ownership to the Account

Game written later
  each Color's Guest row read under the same lock
  claimed → written as that Account

room.create / seat.take without a Session
  RoomService.arrive: a claimed Guest counts as no header → mint a fresh Guest
  (unless the take hands back a Seat it already holds)
guest.get  (GET /guest) → none | unclaimed {games} | claimed-by-you | claimed-by-other

The front, all in auth/:

 <MyGamesView banner={<ClaimBannerView/>}>   routes/games.index.tsx
   <h2>My games</h2>
+  <ClaimBannerUi>   offer with count · or sign-up/sign-in links · or nothing
 <SignUpUi>
+  [x] Add my N games to this account
 <SignInView>
+  <Dialog> Add N games to your account?  [Not now] [Add them]
 sign-out / Account deletion
+  forget the Guest when claimed-by-you
 any status read answering claimed-by-other
+  forget the Guest

A self-game (both Colors owned by one Account) gets the new value "both" in the listing, game.get's viewer and the export. It is listed once, shows both sides as "You", and reads "White won" / "Black won" / "Drawn".

Contract changes the spec didn't list:

  • ResultCountsSchema becomes { all, won, lost, drawn }. A self-game that one Color won counts as neither won nor lost, so "All games" can no longer be the sum of the other three.
  • The "My games" query options move to lib/my-games-query.ts, beside lib/account-query.ts, so auth/ can invalidate "My games" without importing games/. Front rule 6 wants keys from the owning vertical's services/. Consider a line about this placement in ADR 0011's exceptions.
  • The spec says the banner sits "above the games/ view". It now renders under the "My games" heading, inside the page frame, through a banner slot.

Supporting refactors:

  • codeOf moves into rpc.fixtures.ts.
  • The e2e sign-up, sign-in and My games helpers move to tests/accounts.ts and tests/my-games.ts.

Evidence

  • Before: rpc-guest-claims.integration.test.ts, claim-banner.test.tsx, apps/e2e/tests/claim.spec.ts and the self-game cases did not exist.
    After: all pass, and the gate (lint, typecheck, test, build, format:check) is green on each ticket's branch. The new e2e spec covers two flows:
claim.spec.ts
  White resigns, signs up with the box ticked → sees the Game in My games
    fresh context signs in to that Account → sees it too
  Account made in context A; White plays as a Guest in B, signs in, "Add them"
    → sees the Game in B; A sees it after a reload

Limit: "two Accounts claim at once" and "a claim races a Game write" run on single-connection PGlite. They assert the outcome but run one transaction after the other, so they never exercise the row lock itself.

Merge Danger

Door: one-way

The migration adds guests.claimed_by_account_id (cascades on Account delete) and guests.claimed_at. A claim rewrites the owner columns of existing games rows and is never undone. Rolling back the code leaves the claimed Games on their Accounts.

Blast Radius: wide

  • Game writes: every Game write now reads the claim of each Color's Guest.
  • Seats: room.create and seat.take without a Session can mint a fresh Guest in place of a claimed one.
  • Front: sign-up, sign-in, sign-out, Account deletion and "My games" all change.
  • Contract: the listing's counts shape changes for every client.

Closes #365
Closes #385
Closes #386
Closes #387
Closes #388

## Summary An **Account** claims the browser's **Guest**, and its **Games**, only when the **Player** confirms (ADR 0035): on the sign-up form, in a dialog after sign-in, or from a banner on "My games". ```text guest.claim (POST /guest/claim, needs a Session) one transaction upsert the Guest row by hash, lock it claimed by another Account → GUEST_ALREADY_CLAIMED (409) claimed by this Account → { movedGames: 0 } else link it, rewrite White then Black ownership to the Account Game written later each Color's Guest row read under the same lock claimed → written as that Account room.create / seat.take without a Session RoomService.arrive: a claimed Guest counts as no header → mint a fresh Guest (unless the take hands back a Seat it already holds) ``` ```text guest.get (GET /guest) → none | unclaimed {games} | claimed-by-you | claimed-by-other ``` The front, all in `auth/`: ```diff <MyGamesView banner={<ClaimBannerView/>}> routes/games.index.tsx <h2>My games</h2> + <ClaimBannerUi> offer with count · or sign-up/sign-in links · or nothing <SignUpUi> + [x] Add my N games to this account <SignInView> + <Dialog> Add N games to your account? [Not now] [Add them] sign-out / Account deletion + forget the Guest when claimed-by-you any status read answering claimed-by-other + forget the Guest ``` A self-game (both **Colors** owned by one **Account**) gets the new value `"both"` in the listing, `game.get`'s viewer and the export. It is listed once, shows both sides as "You", and reads "White won" / "Black won" / "Drawn". **Contract changes the spec didn't list:** - `ResultCountsSchema` becomes `{ all, won, lost, drawn }`. A self-game that one Color won counts as neither won nor lost, so "All games" can no longer be the sum of the other three. - The "My games" query options move to `lib/my-games-query.ts`, beside `lib/account-query.ts`, so `auth/` can invalidate "My games" without importing `games/`. Front rule 6 wants keys from the owning vertical's `services/`. Consider a line about this placement in ADR 0011's exceptions. - The spec says the banner sits "above the games/ view". It now renders under the "My games" heading, inside the page frame, through a `banner` slot. **Supporting refactors:** - `codeOf` moves into `rpc.fixtures.ts`. - The e2e sign-up, sign-in and My games helpers move to `tests/accounts.ts` and `tests/my-games.ts`. ## Evidence - **Before:** `rpc-guest-claims.integration.test.ts`, `claim-banner.test.tsx`, `apps/e2e/tests/claim.spec.ts` and the self-game cases did not exist. **After:** all pass, and the gate (lint, typecheck, test, build, format:check) is green on each ticket's branch. The new e2e spec covers two flows: ```text claim.spec.ts White resigns, signs up with the box ticked → sees the Game in My games fresh context signs in to that Account → sees it too Account made in context A; White plays as a Guest in B, signs in, "Add them" → sees the Game in B; A sees it after a reload ``` Limit: "two Accounts claim at once" and "a claim races a Game write" run on single-connection PGlite. They assert the outcome but run one transaction after the other, so they never exercise the row lock itself. ## Merge Danger **Door:** one-way The migration adds `guests.claimed_by_account_id` (cascades on Account delete) and `guests.claimed_at`. A claim rewrites the owner columns of existing `games` rows and is never undone. Rolling back the code leaves the claimed Games on their Accounts. **Blast Radius:** wide - **Game writes:** every Game write now reads the claim of each Color's Guest. - **Seats:** `room.create` and `seat.take` without a Session can mint a fresh Guest in place of a claimed one. - **Front:** sign-up, sign-in, sign-out, Account deletion and "My games" all change. - **Contract:** the listing's counts shape changes for every client. Closes #365 Closes #385 Closes #386 Closes #387 Closes #388
project_1_bot_a8dd9262857d850eef8dca5206e61de5 commented 2026-10-14 16:41:17 +00:00 (Migrated from gitlab.core-project.space)

Screenshots

Before is production, after is this merge request's review app at 1d6e6ab.

Desktop

Page Before After
Home
Local game
Room

Mobile

Page Before After
Home
Local game
Room
<!-- chess-screenshots --> ### Screenshots Before is production, after is this merge request's review app at 1d6e6ab. #### Desktop | Page | Before | After | | --- | --- | --- | | Home | <img src="https://gitlab.core-project.space/amaurycoudr/chess-project/-/jobs/1202/artifacts/raw/review-screenshots/before/home-desktop.png" width="400"> | <img src="https://gitlab.core-project.space/amaurycoudr/chess-project/-/jobs/1202/artifacts/raw/review-screenshots/after/home-desktop.png" width="400"> | | Local game | <img src="https://gitlab.core-project.space/amaurycoudr/chess-project/-/jobs/1202/artifacts/raw/review-screenshots/before/local-desktop.png" width="400"> | <img src="https://gitlab.core-project.space/amaurycoudr/chess-project/-/jobs/1202/artifacts/raw/review-screenshots/after/local-desktop.png" width="400"> | | Room | <img src="https://gitlab.core-project.space/amaurycoudr/chess-project/-/jobs/1202/artifacts/raw/review-screenshots/before/room-desktop.png" width="400"> | <img src="https://gitlab.core-project.space/amaurycoudr/chess-project/-/jobs/1202/artifacts/raw/review-screenshots/after/room-desktop.png" width="400"> | #### Mobile | Page | Before | After | | --- | --- | --- | | Home | <img src="https://gitlab.core-project.space/amaurycoudr/chess-project/-/jobs/1202/artifacts/raw/review-screenshots/before/home-mobile.png" width="200"> | <img src="https://gitlab.core-project.space/amaurycoudr/chess-project/-/jobs/1202/artifacts/raw/review-screenshots/after/home-mobile.png" width="200"> | | Local game | <img src="https://gitlab.core-project.space/amaurycoudr/chess-project/-/jobs/1202/artifacts/raw/review-screenshots/before/local-mobile.png" width="200"> | <img src="https://gitlab.core-project.space/amaurycoudr/chess-project/-/jobs/1202/artifacts/raw/review-screenshots/after/local-mobile.png" width="200"> | | Room | <img src="https://gitlab.core-project.space/amaurycoudr/chess-project/-/jobs/1202/artifacts/raw/review-screenshots/before/room-mobile.png" width="200"> | <img src="https://gitlab.core-project.space/amaurycoudr/chess-project/-/jobs/1202/artifacts/raw/review-screenshots/after/room-mobile.png" width="200"> |
root commented 2026-10-14 16:48:56 +00:00 (Migrated from gitlab.core-project.space)

added 3 commits

  • b932efad - feat(front): let the browser forget the Guest it holds
  • 170eba1d - feat(front): offer to claim this browser's Guest on My games
  • cc58f109 - feat(front): forget a Guest this Account claimed on sign-out and on Account deletion

Compare with previous version

added 3 commits <ul><li>b932efad - feat(front): let the browser forget the Guest it holds</li><li>170eba1d - feat(front): offer to claim this browser&#39;s Guest on My games</li><li>cc58f109 - feat(front): forget a Guest this Account claimed on sign-out and on Account deletion</li></ul> [Compare with previous version](/amaurycoudr/chess-project/-/merge_requests/29/diffs?diff_id=199&start_sha=1acaacda4d7fc562723cbeb35d752cf7e7fc455c)
root commented 2026-10-14 16:59:55 +00:00 (Migrated from gitlab.core-project.space)

added 5 commits

  • f5ae7056 - feat(front): offer to add the Guest's Games to the Account on the sign-up form
  • 8327b5fd - feat(front): ask after sign-in whether to add the Guest's Games to the Account
  • ad4275b0 - test(e2e): share the sign-up, sign-in and My games helpers across specs
  • 61d9afd2 - test(e2e): walk a Guest's Game into an Account on sign-up and after sign-in,...
  • 7bbfe6e0 - fix(front): hand the sign-in mutation a callback that returns nothing

Compare with previous version

added 5 commits <ul><li>f5ae7056 - feat(front): offer to add the Guest&#39;s Games to the Account on the sign-up form</li><li>8327b5fd - feat(front): ask after sign-in whether to add the Guest&#39;s Games to the Account</li><li>ad4275b0 - test(e2e): share the sign-up, sign-in and My games helpers across specs</li><li>61d9afd2 - test(e2e): walk a Guest&#39;s Game into an Account on sign-up and after sign-in,...</li><li>7bbfe6e0 - fix(front): hand the sign-in mutation a callback that returns nothing</li></ul> [Compare with previous version](/amaurycoudr/chess-project/-/merge_requests/29/diffs?diff_id=201&start_sha=cc58f1094dfaf6720ca8dbff98ee610f16eb807a)
root commented 2026-10-15 07:43:56 +00:00 (Migrated from gitlab.core-project.space)

added 2 commits

  • 39a244d8 - feat(api): list, read and export a self-game as both Colors
  • 0958e19b - feat(front): show a self-game with both sides as You

Compare with previous version

added 2 commits <ul><li>39a244d8 - feat(api): list, read and export a self-game as both Colors</li><li>0958e19b - feat(front): show a self-game with both sides as You</li></ul> [Compare with previous version](/amaurycoudr/chess-project/-/merge_requests/29/diffs?diff_id=206&start_sha=7bbfe6e02a10714f8c1b1f84f9e78b662e7f8b79)
root commented 2026-10-15 08:00:20 +00:00 (Migrated from gitlab.core-project.space)

added 16 commits

  • e707d6d0 - 1 commit from branch main
  • e707d6d0...ce1c92b4 - 5 earlier commits
  • a6952f40 - feat(front): forget a Guest this Account claimed on sign-out and on Account deletion
  • 930b23c2 - feat(front): offer to add the Guest's Games to the Account on the sign-up form
  • ca4c7565 - feat(front): ask after sign-in whether to add the Guest's Games to the Account
  • bf995b62 - test(e2e): share the sign-up, sign-in and My games helpers across specs
  • 234a0715 - test(e2e): walk a Guest's Game into an Account on sign-up and after sign-in,...
  • 582e317d - fix(front): hand the sign-in mutation a callback that returns nothing
  • c148c83d - feat(api): list, read and export a self-game as both Colors
  • a516c333 - feat(front): show a self-game with both sides as You
  • 23e764fd - refactor(api): decide in RoomService whether a claimed Guest arrives as no one
  • 1d6e6ab0 - refactor(front): slot the claim banner into My games, and forget a Guest where its status is read

Compare with previous version

added 16 commits <ul><li>e707d6d0 - 1 commit from branch <code>main</code></li><li>e707d6d0...ce1c92b4 - 5 earlier commits</li><li>a6952f40 - feat(front): forget a Guest this Account claimed on sign-out and on Account deletion</li><li>930b23c2 - feat(front): offer to add the Guest&#39;s Games to the Account on the sign-up form</li><li>ca4c7565 - feat(front): ask after sign-in whether to add the Guest&#39;s Games to the Account</li><li>bf995b62 - test(e2e): share the sign-up, sign-in and My games helpers across specs</li><li>234a0715 - test(e2e): walk a Guest&#39;s Game into an Account on sign-up and after sign-in,...</li><li>582e317d - fix(front): hand the sign-in mutation a callback that returns nothing</li><li>c148c83d - feat(api): list, read and export a self-game as both Colors</li><li>a516c333 - feat(front): show a self-game with both sides as You</li><li>23e764fd - refactor(api): decide in RoomService whether a claimed Guest arrives as no one</li><li>1d6e6ab0 - refactor(front): slot the claim banner into My games, and forget a Guest where its status is read</li></ul> [Compare with previous version](/amaurycoudr/chess-project/-/merge_requests/29/diffs?diff_id=208&start_sha=0958e19b550a1d3ee4721a8c05918b0b6ab3badd)
root commented 2026-10-15 08:00:34 +00:00 (Migrated from gitlab.core-project.space)

marked this merge request as ready

marked this merge request as **ready**
root commented 2026-10-15 08:00:34 +00:00 (Migrated from gitlab.core-project.space)

changed the description

changed the description
root commented 2026-10-15 08:11:50 +00:00 (Migrated from gitlab.core-project.space)

mentioned in issue #406

mentioned in issue #406
root (Migrated from gitlab.core-project.space) merged commit into main 2026-10-15 08:20:23 +00:00
root commented 2026-10-15 08:20:42 +00:00 (Migrated from gitlab.core-project.space)

mentioned in issue #388

mentioned in issue #388
root commented 2026-10-15 21:18:56 +00:00 (Migrated from gitlab.core-project.space)

mentioned in commit 215cdba4c9

mentioned in commit 215cdba4c9194e91817007777bbb204e7197fb99
root commented 2026-10-15 21:19:13 +00:00 (Migrated from gitlab.core-project.space)

mentioned in merge request !537

mentioned in merge request !537
Sign in to join this conversation.
No description provided.