Public profile: an Account's username, join date and decided Games, listed for anyone #366

Closed
opened 2026-10-11 16:09:24 +00:00 by root · 8 comments
root commented 2026-10-11 16:09:24 +00:00 (Migrated from gitlab.core-project.space)

Imported from GitHub amaurycoudr/chess-project#366, opened by @amaurycoudr on 2026-10-11 16:09 UTC.


Problem Statement

An Account gives a Player a username, but nobody else can see what it stands for. A Player who just lost to "alice" cannot look her up, see when she joined, or browse the Games she played. A Player who wants to show their record to a friend can only share Games one link at a time. Every stored Game is listed only to its owners (ADR 0034), so there is no public place where an Account's Games add up to an identity, the third motivation of the accounts map.

Solution

Every Account has a public profile page, addressed by its username. Anyone can open it, signed in or not: it shows the username, the month the Account joined, and the Account's decided Games, newest first. Each row shows when the Game ended, the Account's Color, the opponent (a username that links to their own profile, or "Guest"), the result from the Account's side and how it ended. A row opens the Game page. Older Games load on demand. A signed-in Player reaches their own profile from the site navigation, and the usernames on a Game page link to their profiles.

A Guest has no profile. A Game with no Account on either side appears on no profile and stays listed only to its owner. The profile shows nothing else about an Account: no email, no Sessions, no claimed Guests.

User Stories

  1. As anyone, signed in or not, I want to open an Account's profile by its username and see the username and the month it joined, so that I can tell who I played.
  2. As anyone, I want the profile to list the Account's decided Games, newest first, so that I can see what they have played.
  3. As anyone, I want each row to show when the Game ended, the Account's Color, the opponent, the Account's result (won, lost, drawn) and how it ended, so that I can read the record without opening each Game.
  4. As anyone, I want a row to open that Game's page, so that I can replay it.
  5. As anyone, I want an opponent who is an Account shown by username and linked to their profile, and an opponent who is a Guest shown as "Guest" without a link, so that I can follow one Player to the next.
  6. As anyone looking at an Account with many Games, I want older Games loaded when I ask for more, so that a long record stays quick to open.
  7. As anyone looking at an Account with no decided Games, I want the profile to say so, so that an empty list does not look broken.
  8. As anyone opening a profile for a username that does not exist, or whose Account was deleted, I want a clear not-found page, so that a typo or a stale link does not look like a crash.
  9. As anyone typing a username in another case, I want the profile to open anyway, showing the username as its owner chose it and correcting the address, so that "Alice" and "alice" are the same Player.
  10. As a signed-in Player, I want a link to my own profile in the site navigation, so that I can check and share it.
  11. As anyone reading a Game page, I want each side held by an Account to link its username to that profile, so that I can find out more about who played.
  12. As a Player who claimed a Guest, I want its Games, and those decided later for Seats it still held, on my profile, so that my record includes what I played before signing up.
  13. As a Guest, I want my Games to appear on no profile except that of an Account I played against, so that playing without an account stays private.
  14. As an Account holder, I want my profile to show only my username, join date and decided Games, never my email or anything else about my Account, so that being listed reveals nothing I did not choose to make public.
  15. As anyone, I want a Game in which one Account held both Seats listed once on its profile, so that a self-game does not count twice.

Seams

  • The RPC wire, through the real app in process with the RPC fixtures on PGlite, calling the new profile read: claimed Guest Games are listed, including one decided after the claim; a Game with no Account owner appears on no profile; the answer carries no email or other Account field; a self-game is listed once. Covers 12, 13, 14, 15.
  • The front views, beside each view with its harness and MSW: the profile view (rows and their result wording, opponent link versus "Guest", show more, empty state, not-found, case-corrected address), the Game page view (usernames linked), and the app shell (own-profile link only when signed in). Covers 5, 6, 7, 8, 9, 10, 11.
  • e2e (apps/e2e), one new spec: a Player signs up and, seated against a Guest in a second browser context, plays a Game the Guest resigns. A third, signed-out context opens the profile by its URL and reads the username, the join month and one row with "won" by Resignation, then opens the row and reaches the Game page. Covers 1, 2, 3, 4.

Implementation Decisions

Data. This spec owns no data and adds no table or column.

  • It reads the username and the Account's creation time from Accounts v1, and the Games and their Account owners from Guest history, Accounts v1 and Claim a Guest.
  • It relies on the index those specs give the Account owner columns for "my games".
  • A claim rewrites a Guest's Games to the Account (ADR 0035), so the profile reads the Account owner columns only and never joins through claimed Guests.

Contract. A new profile router in the contracts, with one read, profile.get, on a GET route addressed by username.

  • Input:
    • the username, as the branded username type from Accounts v1, with no format check (contracts rule 3);
    • an optional opaque cursor.
  • Output:
    • the username as stored;
    • the join time;
    • one page of Games, newest first;
    • the cursor for the next page, or none on the last page.
  • Each Game item reuses the "my games" item shape (id, ended, Color, winner, ending, line, opponent name), with the Color read as the profile Account's, not the caller's. Nothing new is defined twice (root rule 6).
  • The page size is the "my games" cap, reused as one named constant.
  • Paging is keyset on ended time then id, so a Game decided while someone pages is neither repeated nor skipped. An unreadable cursor is an input validation error.
  • An unknown username is a new PROFILE_NOT_FOUND refusal, 404 in the refusal status table. A deleted Account is unknown.
  • Lookup is case-insensitive, matching the username's unique index from Accounts v1.
  • The procedure is public: it opts out of the global auth guard, never reads a Session or a Guest, and answers the same to everyone. As a GET it passes the origin guard without an Origin (ADR 0028).

API. The games feature module implements the profile read beside "my games", since it already resolves owners to names. No new folder, so the API's layer order is unchanged (ADR 0026).

  • A self-game is listed once, as "my games" lists it after Claim a Guest.
  • The refusal joins the controller's refusal table, so a missing mapping fails to compile (API rule 4).

Front.

  • The profile view lives in the games/ vertical, beside "My games", and renders the same game-row component, so a vertical never imports a sibling (ADR 0011).
  • A new route under a fixed prefix, /players/$username. Its loader primes the profile query, and "show more" fetches the next pages through an infinite query from the vertical's query options factory.
  • The prefix keeps usernames clear of the site's top-level routes. The reserved list from Accounts v1 still applies but is not needed for routing.
  • When the username in the address differs in case from the stored one, the route replaces the address with the stored casing, without adding a history entry.
  • Not-found reuses the not-found pattern of the Game page.
  • The join date reads as a localized month and year ("Joined October 2026").
  • The document title is the username.
  • The game-row component and the Game page render an Account opponent's username as a link to its profile. "Guest" stays plain text. Room names stay unlinked (out of scope).
  • The site navigation shows a link to the signed-in Player's own profile, read from the Session query Accounts v1 provides.
  • Every new string goes in the Paraglide catalogue, in English and French (ADR 0025).

ADRs.

  • Applies: 0011, 0019, 0025, 0026, 0027, 0028, 0030, 0034, 0035.
  • This spec needs a new ADR that supersedes ADR 0034. It keeps every decision of 0034 except "listed only to its owners", which becomes: an Account's decided Games are listed publicly on its profile, and a Game with no Account owner is listed only to its owner. The ADR index row for 0034 turns to superseded. The ADR is written with the owner's agreement, in its own PR or the spec's first PR, before the profile ships.

Testing Decisions

  • Prior art:
    • the RPC integration tests with their fixtures, and the "my games" wire tests from Guest history, for the profile read;
    • the "My games" and Game page view tests in games/, with their harness and MSW handlers, for the views;
    • the app shell test for the navigation link;
    • the Guest history e2e spec and the Accounts v1 sign-up helpers, with seatedPair from the e2e room fixtures, for the new e2e spec.
  • Edge and unhappy paths:
    • an unknown username;
    • a deleted Account;
    • a username in another case;
    • a username with characters outside the allowed set (not found, not a validation error);
    • an Account with no Games;
    • exactly one page and one more than a page (the cursor appears only when there is more);
    • a Game decided between two page reads;
    • an unreadable cursor;
    • each result and ending as seen from either Color;
    • an opponent who is an Account, a Guest, or none (a vacated Seat);
    • a self-game;
    • a claimed Guest's Game decided before and after the claim;
    • a Game between two Guests (on no profile);
    • the answer is identical with and without a Session or Guest header, and never holds an email.

Out of Scope

  • Changing a username, display names other than the username, avatars, bios, or any setting that hides a profile.
  • Ratings, statistics, win counts, or filters and search on the profile.
  • A directory or search of Accounts.
  • A profile for a Guest.
  • Linking names in a live Room.
  • A reporting flow for usernames (no reporting flow in v1, per Last product decisions before the account specs).
  • Rate limiting the profile read, and search-engine indexing rules.
  • Any change to who owns a Game, how a claim works, or how an Account is deleted.

Further Notes

> _Imported from GitHub [amaurycoudr/chess-project#366](https://github.com/amaurycoudr/chess-project/issues/366), opened by [@amaurycoudr](https://github.com/amaurycoudr) on 2026-10-11 16:09 UTC._ <!-- github-issue:366 --> --- ## Problem Statement An **Account** gives a **Player** a username, but nobody else can see what it stands for. A **Player** who just lost to "alice" cannot look her up, see when she joined, or browse the **Games** she played. A **Player** who wants to show their record to a friend can only share **Games** one link at a time. Every stored **Game** is listed only to its owners (ADR 0034), so there is no public place where an **Account**'s **Games** add up to an identity, the third motivation of the accounts map. ## Solution Every **Account** has a public profile page, addressed by its username. Anyone can open it, signed in or not: it shows the username, the month the **Account** joined, and the **Account**'s decided **Games**, newest first. Each row shows when the **Game** ended, the **Account**'s **Color**, the opponent (a username that links to their own profile, or "Guest"), the result from the **Account**'s side and how it ended. A row opens the **Game** page. Older **Games** load on demand. A signed-in **Player** reaches their own profile from the site navigation, and the usernames on a **Game** page link to their profiles. A **Guest** has no profile. A **Game** with no **Account** on either side appears on no profile and stays listed only to its owner. The profile shows nothing else about an **Account**: no email, no **Sessions**, no claimed **Guests**. ## User Stories 1. As anyone, signed in or not, I want to open an **Account**'s profile by its username and see the username and the month it joined, so that I can tell who I played. 2. As anyone, I want the profile to list the **Account**'s decided **Games**, newest first, so that I can see what they have played. 3. As anyone, I want each row to show when the **Game** ended, the **Account**'s **Color**, the opponent, the **Account**'s result (won, lost, drawn) and how it ended, so that I can read the record without opening each **Game**. 4. As anyone, I want a row to open that **Game**'s page, so that I can replay it. 5. As anyone, I want an opponent who is an **Account** shown by username and linked to their profile, and an opponent who is a **Guest** shown as "Guest" without a link, so that I can follow one **Player** to the next. 6. As anyone looking at an **Account** with many **Games**, I want older **Games** loaded when I ask for more, so that a long record stays quick to open. 7. As anyone looking at an **Account** with no decided **Games**, I want the profile to say so, so that an empty list does not look broken. 8. As anyone opening a profile for a username that does not exist, or whose **Account** was deleted, I want a clear not-found page, so that a typo or a stale link does not look like a crash. 9. As anyone typing a username in another case, I want the profile to open anyway, showing the username as its owner chose it and correcting the address, so that "Alice" and "alice" are the same **Player**. 10. As a signed-in **Player**, I want a link to my own profile in the site navigation, so that I can check and share it. 11. As anyone reading a **Game** page, I want each side held by an **Account** to link its username to that profile, so that I can find out more about who played. 12. As a **Player** who claimed a **Guest**, I want its **Games**, and those decided later for **Seats** it still held, on my profile, so that my record includes what I played before signing up. 13. As a **Guest**, I want my **Games** to appear on no profile except that of an **Account** I played against, so that playing without an account stays private. 14. As an **Account** holder, I want my profile to show only my username, join date and decided **Games**, never my email or anything else about my **Account**, so that being listed reveals nothing I did not choose to make public. 15. As anyone, I want a **Game** in which one **Account** held both **Seats** listed once on its profile, so that a self-game does not count twice. ## Seams - **The RPC wire**, through the real app in process with the RPC fixtures on PGlite, calling the new profile read: claimed **Guest** **Games** are listed, including one decided after the claim; a **Game** with no **Account** owner appears on no profile; the answer carries no email or other **Account** field; a self-game is listed once. Covers 12, 13, 14, 15. - **The front views**, beside each view with its harness and MSW: the profile view (rows and their result wording, opponent link versus "Guest", show more, empty state, not-found, case-corrected address), the **Game** page view (usernames linked), and the app shell (own-profile link only when signed in). Covers 5, 6, 7, 8, 9, 10, 11. - **e2e** (`apps/e2e`), one new spec: a **Player** signs up and, seated against a **Guest** in a second browser context, plays a **Game** the **Guest** resigns. A third, signed-out context opens the profile by its URL and reads the username, the join month and one row with "won" by **Resignation**, then opens the row and reaches the **Game** page. Covers 1, 2, 3, 4. ## Implementation Decisions **Data.** This spec owns no data and adds no table or column. - It reads the username and the **Account**'s creation time from Accounts v1, and the **Games** and their **Account** owners from Guest history, Accounts v1 and Claim a Guest. - It relies on the index those specs give the **Account** owner columns for "my games". - A claim rewrites a **Guest**'s **Games** to the **Account** (ADR 0035), so the profile reads the **Account** owner columns only and never joins through claimed **Guests**. **Contract.** A new `profile` router in the contracts, with one read, `profile.get`, on a GET route addressed by username. - Input: - the username, as the branded username type from Accounts v1, with no format check (contracts rule 3); - an optional opaque cursor. - Output: - the username as stored; - the join time; - one page of **Games**, newest first; - the cursor for the next page, or none on the last page. - Each **Game** item reuses the "my games" item shape (id, ended, **Color**, winner, ending, line, opponent name), with the **Color** read as the profile **Account**'s, not the caller's. Nothing new is defined twice (root rule 6). - The page size is the "my games" cap, reused as one named constant. - Paging is keyset on ended time then id, so a **Game** decided while someone pages is neither repeated nor skipped. An unreadable cursor is an input validation error. - An unknown username is a new `PROFILE_NOT_FOUND` refusal, 404 in the refusal status table. A deleted **Account** is unknown. - Lookup is case-insensitive, matching the username's unique index from Accounts v1. - The procedure is public: it opts out of the global auth guard, never reads a **Session** or a **Guest**, and answers the same to everyone. As a GET it passes the origin guard without an Origin (ADR 0028). **API.** The games feature module implements the profile read beside "my games", since it already resolves owners to names. No new folder, so the API's layer order is unchanged (ADR 0026). - A self-game is listed once, as "my games" lists it after Claim a Guest. - The refusal joins the controller's refusal table, so a missing mapping fails to compile (API rule 4). **Front.** - The profile view lives in the `games/` vertical, beside "My games", and renders the same game-row component, so a vertical never imports a sibling (ADR 0011). - A new route under a fixed prefix, `/players/$username`. Its loader primes the profile query, and "show more" fetches the next pages through an infinite query from the vertical's query options factory. - The prefix keeps usernames clear of the site's top-level routes. The reserved list from Accounts v1 still applies but is not needed for routing. - When the username in the address differs in case from the stored one, the route replaces the address with the stored casing, without adding a history entry. - Not-found reuses the not-found pattern of the **Game** page. - The join date reads as a localized month and year ("Joined October 2026"). - The document title is the username. - The game-row component and the **Game** page render an **Account** opponent's username as a link to its profile. "Guest" stays plain text. **Room** names stay unlinked (out of scope). - The site navigation shows a link to the signed-in **Player**'s own profile, read from the **Session** query Accounts v1 provides. - Every new string goes in the Paraglide catalogue, in English and French (ADR 0025). **ADRs.** - Applies: 0011, 0019, 0025, 0026, 0027, 0028, 0030, 0034, 0035. - This spec needs a new ADR that supersedes ADR 0034. It keeps every decision of 0034 except "listed only to its owners", which becomes: an **Account**'s decided **Games** are listed publicly on its profile, and a **Game** with no **Account** owner is listed only to its owner. The ADR index row for 0034 turns to superseded. The ADR is written with the owner's agreement, in its own PR or the spec's first PR, before the profile ships. ## Testing Decisions - Prior art: - the RPC integration tests with their fixtures, and the "my games" wire tests from Guest history, for the profile read; - the "My games" and **Game** page view tests in `games/`, with their harness and MSW handlers, for the views; - the app shell test for the navigation link; - the Guest history e2e spec and the Accounts v1 sign-up helpers, with `seatedPair` from the e2e room fixtures, for the new e2e spec. - Edge and unhappy paths: - an unknown username; - a deleted **Account**; - a username in another case; - a username with characters outside the allowed set (not found, not a validation error); - an **Account** with no **Games**; - exactly one page and one more than a page (the cursor appears only when there is more); - a **Game** decided between two page reads; - an unreadable cursor; - each result and ending as seen from either **Color**; - an opponent who is an **Account**, a **Guest**, or none (a vacated **Seat**); - a self-game; - a claimed **Guest**'s **Game** decided before and after the claim; - a **Game** between two **Guests** (on no profile); - the answer is identical with and without a **Session** or **Guest** header, and never holds an email. ## Out of Scope - Changing a username, display names other than the username, avatars, bios, or any setting that hides a profile. - Ratings, statistics, win counts, or filters and search on the profile. - A directory or search of **Accounts**. - A profile for a **Guest**. - Linking names in a live **Room**. - A reporting flow for usernames (no reporting flow in v1, per [Last product decisions before the account specs](https://github.com/amaurycoudr/chess-project/issues/362)). - Rate limiting the profile read, and search-engine indexing rules. - Any change to who owns a **Game**, how a claim works, or how an **Account** is deleted. ## Further Notes - Map: [Wayfinder: accounts](https://github.com/amaurycoudr/chess-project/issues/346). Decisions: [Last product decisions before the account specs](https://github.com/amaurycoudr/chess-project/issues/362) (point 7, the public profile; points 4 and 6, the username and names); [What a finished Game keeps, and which guest identity owns it](https://github.com/amaurycoudr/chess-project/issues/348) (point 7, public listing waits for the profile); [How does an account relate to the Seat token?](https://github.com/amaurycoudr/chess-project/issues/351); [How are guest games merged into an account?](https://github.com/amaurycoudr/chess-project/issues/352). - Blocked by, in order: [Postgres in place](https://github.com/amaurycoudr/chess-project/issues/354), [Guest history](https://github.com/amaurycoudr/chess-project/issues/355), [Accounts v1: sign up with a username, sign in on any device, reset a password by email](https://github.com/amaurycoudr/chess-project/issues/364), [Claim a Guest into an Account, new or existing](https://github.com/amaurycoudr/chess-project/issues/365). - Likely one PR after the superseding ADR: one read procedure, one view, two links. `implement` on this spec.
root commented 2026-10-11 16:09:24 +00:00 (Migrated from gitlab.core-project.space)

GitHub issue relations, imported as "related" links (GitLab CE has no sub-issues or blocking links).

> _GitHub issue relations, imported as "related" links (GitLab CE has no sub-issues or blocking links)._ <!-- github-relations --> - Sub-issues: #389, #390, #391
root commented 2026-10-13 08:39:51 +00:00 (Migrated from gitlab.core-project.space)

mentioned in issue #389

mentioned in issue #389
root commented 2026-10-13 08:39:52 +00:00 (Migrated from gitlab.core-project.space)

mentioned in issue #390

mentioned in issue #390
root commented 2026-10-13 08:39:52 +00:00 (Migrated from gitlab.core-project.space)

mentioned in issue #391

mentioned in issue #391
root commented 2026-10-13 08:42:31 +00:00 (Migrated from gitlab.core-project.space)

marked this issue as related to #389

marked this issue as related to #389
root commented 2026-10-13 08:42:32 +00:00 (Migrated from gitlab.core-project.space)

marked this issue as related to #390

marked this issue as related to #390
root commented 2026-10-13 08:42:32 +00:00 (Migrated from gitlab.core-project.space)

marked this issue as related to #391

marked this issue as related to #391
root commented 2026-10-15 09:23:10 +00:00 (Migrated from gitlab.core-project.space)

mentioned in merge request !531

mentioned in merge request !531
root (Migrated from gitlab.core-project.space) closed this issue 2026-10-15 09:44:36 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
amaurycoudr/chess-project#366
No description provided.