feat: public profile of an Account at /players/<username> #531

Merged
root merged 18 commits from claude/profile-366 into main 2026-10-15 09:44:35 +00:00
root commented 2026-10-15 09:22:57 +00:00 (Migrated from gitlab.core-project.space)

Closes #366
Closes #389
Closes #390
Closes #391

Summary

Every Account gets a public profile at /players/<username>: username, join month, decided Games newest first. ADR 0042 supersedes 0034: an Account's decided Games are listed publicly; a Game with no Account owner stays listed only to its owners.

GET /rpc/profiles/{username}   profile.get, public (@Authentication("none"))
  AuthService.findByUsername     case-insensitive (ADR 0040); unknown/deleted/malformed → PROFILE_NOT_FOUND 404
  GamesService.profile(account)  Account owner columns only (ADR 0035), keyset on ended, id
    → { username, joinedAt, games: page of the "my games" item, cursor? }
  unreadable cursor              → UNREADABLE_CURSOR 400 (now on game.list too)
 apps/front/src/
 ├── routes/
+│   └── players.$username.tsx    # loader primes the infinite query, redirects to stored casing (replace)
 ├── game/
 │   ├── lib/player-rows.ts       # sideNameOf: one place an owner reads as Account / Guest
+│   └── components/profile-link.tsx
 ├── games/
+│   ├── profile.view.tsx / profile.ui.tsx
+│   ├── services/profile-query.ts
+│   ├── game-row.ts              # row model, shared by My games and the profile
+│   ├── components/game-row.tsx  # opponent placed with a Paraglide markup tag
+│   ├── components/not-found.tsx # shared with the Game page
 │   └── game-page.view.tsx       # each Account side links to its profile
 └── auth/account-nav.ui.tsx      # "Profile" item when signed in

Evidence

  • Before: no profile router; /players/alice is the router's 404.
    After: rpc-profiles.integration.test.ts (claimed Guest Games before and after the claim, Guest-vs-Guest on no profile, self-game once, no email, same answer with/without Session or Guest, paging edges, decided-between-reads); profile.test.tsx, profile-route.test.tsx, site-navigation.test.tsx, game-page.test.tsx; new e2e profile.spec.ts: a signed-out third context reads the username, join month and a row "won" by Resignation, then opens the Game page. Gate green on Node 24; e2e 42/42.

Merge Danger

Door: two-way for the code; the listing is one-way in effect. Once a profile is public, people can have seen and cached it.

Blast Radius: front-wide row markup

  • The Game row is now a list item whose link stretches over it (::after), so the opponent's link can sit inside it. The e2e openMyGames helper changed with it.
  • <title> moved from useShownLocale to the root route, so the profile can set its own.
  • game.list now refuses a cursor it never minted with UNREADABLE_CURSOR, where it used to answer an empty last page.

For the owner to rule on

  • A row whose opponent's Seat was vacated, or whose Account was deleted, reads "Guest". The wire keeps none apart, but the front folds it into "Guest". #390 says "a row with no opponent reads as such".
  • On a Game page, the viewer's own side reads "You" with no link. #391 says each Account side links.
  • The own-profile link is an item in the account menu, not a top-level navigation link, so desktop and phone share one placement.
  • A self-game on a profile shows the Account itself as the opponent, linked, with "White won" / "Black won".

Proposed rule changes

  • Add apps/front/src/routes/players.$username.tsx to ADR 0042's Governs column.
  • auth.usernameAvailability still takes a plain z.string() username. Contracts rule 3 calls for the branded AnyUsernameSchema introduced here.
  • pnpm lint:adr doesn't check the ADR word limit.

Noticed in passing: src/auth/sign-up.test.tsx is flaky. A pathname check right after navigation failed once in a commit hook and passed on rerun.

Closes #366 Closes #389 Closes #390 Closes #391 ## Summary Every **Account** gets a public profile at `/players/<username>`: username, join month, decided **Games** newest first. ADR 0042 supersedes 0034: an **Account**'s decided **Games** are listed publicly; a **Game** with no **Account** owner stays listed only to its owners. ```text GET /rpc/profiles/{username} profile.get, public (@Authentication("none")) AuthService.findByUsername case-insensitive (ADR 0040); unknown/deleted/malformed → PROFILE_NOT_FOUND 404 GamesService.profile(account) Account owner columns only (ADR 0035), keyset on ended, id → { username, joinedAt, games: page of the "my games" item, cursor? } unreadable cursor → UNREADABLE_CURSOR 400 (now on game.list too) ``` ```diff apps/front/src/ ├── routes/ +│ └── players.$username.tsx # loader primes the infinite query, redirects to stored casing (replace) ├── game/ │ ├── lib/player-rows.ts # sideNameOf: one place an owner reads as Account / Guest +│ └── components/profile-link.tsx ├── games/ +│ ├── profile.view.tsx / profile.ui.tsx +│ ├── services/profile-query.ts +│ ├── game-row.ts # row model, shared by My games and the profile +│ ├── components/game-row.tsx # opponent placed with a Paraglide markup tag +│ ├── components/not-found.tsx # shared with the Game page │ └── game-page.view.tsx # each Account side links to its profile └── auth/account-nav.ui.tsx # "Profile" item when signed in ``` ## Evidence - **Before:** no `profile` router; `/players/alice` is the router's 404. **After:** `rpc-profiles.integration.test.ts` (claimed Guest Games before and after the claim, Guest-vs-Guest on no profile, self-game once, no email, same answer with/without Session or Guest, paging edges, decided-between-reads); `profile.test.tsx`, `profile-route.test.tsx`, `site-navigation.test.tsx`, `game-page.test.tsx`; new e2e `profile.spec.ts`: a signed-out third context reads the username, join month and a row "won" by Resignation, then opens the Game page. Gate green on Node 24; e2e 42/42. ## Merge Danger **Door:** two-way for the code; the listing is one-way in effect. Once a profile is public, people can have seen and cached it. **Blast Radius:** front-wide row markup - The Game row is now a list item whose link stretches over it (`::after`), so the opponent's link can sit inside it. The e2e `openMyGames` helper changed with it. - `<title>` moved from `useShownLocale` to the root route, so the profile can set its own. - `game.list` now refuses a cursor it never minted with `UNREADABLE_CURSOR`, where it used to answer an empty last page. ### For the owner to rule on - A row whose opponent's **Seat** was vacated, or whose **Account** was deleted, reads "Guest". The wire keeps `none` apart, but the front folds it into "Guest". #390 says "a row with no opponent reads as such". - On a **Game** page, the viewer's own side reads "You" with no link. #391 says each **Account** side links. - The own-profile link is an item in the account menu, not a top-level navigation link, so desktop and phone share one placement. - A self-game on a profile shows the **Account** itself as the opponent, linked, with "White won" / "Black won". ### Proposed rule changes - Add `apps/front/src/routes/players.$username.tsx` to ADR 0042's Governs column. - `auth.usernameAvailability` still takes a plain `z.string()` username. Contracts rule 3 calls for the branded `AnyUsernameSchema` introduced here. - `pnpm lint:adr` doesn't check the ADR word limit. Noticed in passing: `src/auth/sign-up.test.tsx` is flaky. A pathname check right after navigation failed once in a commit hook and passed on rerun.
project_1_bot_a8dd9262857d850eef8dca5206e61de5 commented 2026-10-15 09:29:48 +00:00 (Migrated from gitlab.core-project.space)

Screenshots

Before is production, after is this merge request's review app at 4623f0b.

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 4623f0b. #### Desktop | Page | Before | After | | --- | --- | --- | | Home | <img src="https://gitlab.core-project.space/amaurycoudr/chess-project/-/jobs/1231/artifacts/raw/review-screenshots/before/home-desktop.png" width="400"> | <img src="https://gitlab.core-project.space/amaurycoudr/chess-project/-/jobs/1231/artifacts/raw/review-screenshots/after/home-desktop.png" width="400"> | | Local game | <img src="https://gitlab.core-project.space/amaurycoudr/chess-project/-/jobs/1231/artifacts/raw/review-screenshots/before/local-desktop.png" width="400"> | <img src="https://gitlab.core-project.space/amaurycoudr/chess-project/-/jobs/1231/artifacts/raw/review-screenshots/after/local-desktop.png" width="400"> | | Room | <img src="https://gitlab.core-project.space/amaurycoudr/chess-project/-/jobs/1231/artifacts/raw/review-screenshots/before/room-desktop.png" width="400"> | <img src="https://gitlab.core-project.space/amaurycoudr/chess-project/-/jobs/1231/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/1231/artifacts/raw/review-screenshots/before/home-mobile.png" width="200"> | <img src="https://gitlab.core-project.space/amaurycoudr/chess-project/-/jobs/1231/artifacts/raw/review-screenshots/after/home-mobile.png" width="200"> | | Local game | <img src="https://gitlab.core-project.space/amaurycoudr/chess-project/-/jobs/1231/artifacts/raw/review-screenshots/before/local-mobile.png" width="200"> | <img src="https://gitlab.core-project.space/amaurycoudr/chess-project/-/jobs/1231/artifacts/raw/review-screenshots/after/local-mobile.png" width="200"> | | Room | <img src="https://gitlab.core-project.space/amaurycoudr/chess-project/-/jobs/1231/artifacts/raw/review-screenshots/before/room-mobile.png" width="200"> | <img src="https://gitlab.core-project.space/amaurycoudr/chess-project/-/jobs/1231/artifacts/raw/review-screenshots/after/room-mobile.png" width="200"> |
root commented 2026-10-15 09:35:35 +00:00 (Migrated from gitlab.core-project.space)

added 1 commit

  • 4623f0b0 - fix(front): render My games rows only in the chosen filter's panel

Compare with previous version

added 1 commit <ul><li>4623f0b0 - fix(front): render My games rows only in the chosen filter&#39;s panel</li></ul> [Compare with previous version](/amaurycoudr/chess-project/-/merge_requests/31/diffs?diff_id=212&start_sha=febccf71f7fe7837475351786c312562b568b9f9)
root (Migrated from gitlab.core-project.space) merged commit into main 2026-10-15 09:44:35 +00:00
Sign in to join this conversation.
No description provided.