Skip to content

Web UX pass: unified sign-in, header wallet menu, /profile hub#13

Merged
vikramarun merged 11 commits into
mainfrom
claude/home-page-ux-redesign-9e4591
Jul 12, 2026
Merged

Web UX pass: unified sign-in, header wallet menu, /profile hub#13
vikramarun merged 11 commits into
mainfrom
claude/home-page-ux-redesign-9e4591

Conversation

@vikramarun

@vikramarun vikramarun commented Jul 12, 2026

Copy link
Copy Markdown
Owner

A full pass over the web app's entry UX, plus the two backend/feature changes from #14 (now closed and folded in here).

1. Auth — one button instead of two

Replaced the separate Connect Wallet + Sign in buttons with a single Sign in (AuthButton.tsx, ConnectButton.Custom): connecting auto-switches to the server's expected chain and, on a wagering server, auto-prompts the SIWE signature — one flow, no second button. Signed-in state is a compact account chip; a "Wrong network — switch" control appears if a signed-in wallet drifts off-chain.

The session token is now bound to the wallet it was issued for and cleared on disconnect / account switch, and it's exposed through a reactive useAuthToken() hook (backed by a chess-auth-changed event + cross-tab storage) so a sign-in on the same page propagates instantly instead of going stale until reload.

2. Bankroll + claims — top-right wallet menu

Deposit/withdraw moved into a header WalletMenu balance pill + popover; the inline bankroll panels were removed from Home, Tournament, and Gauntlet. Tournament payouts/refunds (ClaimWinnings + TournamentClaim) live in that same popover — they credit the same escrow balance, so they belong with the bankroll rather than scattered across the tournament page.

3. Home — decluttered

Removed the browser-bot panel, bankroll panel, and player-lookup box from the home page.

4. /profile — new hub

profile/page.tsx: stats + game history (shared ProfileStats), an Engine section combining browser-bot config + the connect-your-engine flow (ConnectEngine), and the player lookup. /player/[address] stays public; /connect remains a deep link.

5. Leaderboard query optimization (backend, from #14)

The lobby leaderboard ran a correlated COUNT(*) per user over the whole games table — O(users × games) with lower() on unindexed columns. Rewritten as a single GROUP BY pass joined back to users → O(users + games); EXPLAIN confirms one hash join over a grouped scan. Added functional lower(wallet) indexes (migration 0007) and a DATABASE_URL-gated test.

Shared abstractions (no per-page duplication)

useAuthToken, useMounted, useOnchainConfig, useEnsureChain, ProfileStats, ConnectEngine, BankrollPanel, TournamentClaim — extracted so the pages compose them rather than re-implementing config fetching, mount gates, chain-switching, or token snapshots.

Verification

  • tsc --noEmit clean; cargo check -p persistence -p ledger -p server → Finished (exit 0).
  • Home / /profile / /player / /tournament / /gauntlet render with no new console errors (only pre-existing WalletConnect-preload noise from a missing dev WC_PROJECT_ID).
  • Verified the reactive-auth propagation end-to-end in the preview.

Needs a live smoke test (no wallet/chain in sandbox)

The connected-wallet paths — auto-SIWE, account-switch guard, wrong-network control, bankroll deposit/withdraw, and tournament claim/refund — need a real injected wallet + running game server + a settled/abandoned on-chain tournament. The merged scripts/test-sepolia-tournament.sh is built for the claim/refund path. E2E tests are planned for a later session.

🤖 Generated with Claude Code

Consolidate the entry experience per a full UX review of the home page.

Auth: replace the separate Connect Wallet + Sign in buttons with one
AuthButton (RainbowKit ConnectButton.Custom). Connecting auto-switches to
the server's expected chain and, on a wagering server, auto-prompts the
SIWE signature — a single flow instead of a two-step process. Signed-in
state shows a compact account chip. The session token is now bound to the
wallet it was issued for and cleared on disconnect / account switch,
fixing a latent stale-token bug.

Bankroll: move deposit/withdraw into a top-right WalletMenu (balance pill
+ popover reusing BankrollPanel); remove the inline bankroll panels from
Home, Tournament, and Gauntlet.

Home: drop the browser-bot panel, bankroll panel, and player-lookup box.

Profile: new /profile hub with stats + game history (shared ProfileStats),
an Engine section combining browser-bot config and the connect-your-engine
pairing flow (extracted to ConnectEngine), and the player-lookup box.
Remove Connect Engine from the nav; /player/[address] stays public and
/connect remains as a deep link.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 12, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
openchess Ready Ready Preview, Comment Jul 12, 2026 8:31am

Request Review

super-vikram and others added 3 commits July 12, 2026 02:08
The leaderboard ran a correlated COUNT(*) per user over the whole games
table — O(users × games) — with lower() on unindexed wallet columns.
Rewrite it to count finished rated games per wallet in a single GROUP BY
pass, then join the counts back to users: O(users + games). EXPLAIN
confirms one hash join over a single grouped scan, no per-user subquery.
COUNT(DISTINCT id) preserves the old OR semantics (a hypothetical
self-play game counts once) and the inner join makes the "at least one
game" filter implicit.

Add functional lower(wallet) indexes (migration 0007) on games'
white/black wallet columns and users.wallet, matching the lower()
predicates used across the leaderboard join and the per-address profile
queries (player_stats / player_games / player_rating / update_ratings).

Add a DATABASE_URL-gated test covering case-insensitive count folding,
exclusion of players with no finished games, pending games not counting,
and Elo-driven ordering.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Winners of a root-settled (large) tournament and entrants of an
abandoned one had no in-app way to collect — they'd have to call the
escrow contract directly. Wire the buttons to ChessEscrow:

- TournamentClaim component reads on-chain state for the connected
  wallet and shows the right action or nothing: a Merkle "Claim payout"
  (fetches the proof from GET /tournaments/{id}/claim/{address}, then
  claimTournament), a "Claim refund" past settleTimeout (claimRefund),
  or an idle "claimed ✓" / "refund available in ~Nh" state. Renders
  nothing for non-participants, disconnected wallets, or direct-settled
  fields (already credited to bankroll).
- escrow.ts: extend ESCROW_ABI with claimTournament/claimRefund and the
  tournaments/tournamentClaimed/tournamentEntered/settleTimeout getters;
  add tidToBytes32 (matches the server's game_id_to_bytes32) and a
  fetchClaimProof helper.
- Surface the widget in the lobby card and both finished play views;
  claims credit the escrow bankroll, withdrawn via the Bankroll panel.

Mirrors the existing BankrollPanel write pattern (writeContractAsync →
waitForTransactionReceipt, chain guard, shortMessage errors).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fixes from the review of the prior commit.

Reactive session (fixes the /profile pairing regression + related staleness):
add a shared useAuthToken() hook backed by a same-tab "chess-auth-changed"
event (dispatched by setAuth/clearAuth) plus the cross-tab storage event.
ConnectEngine, Lobby, tournament, and gauntlet now consume it instead of
snapshotting authToken() into per-component state, so a sign-in completed on
the same page (e.g. the header AuthButton's auto-SIWE on /profile) or a session
cleared on account switch propagates immediately — no dead pairing code or
"sign in to stake" errors until reload. ConnectEngine also drops its minted
code when the session changes so it can't pair an engine to a previous wallet.

AuthButton hardening:
- clear a legacy token (no bound chess_addr) or a token bound to a different
  wallet on account change, so pre-upgrade sessions and switches re-sign cleanly
  and don't disagree with other components.
- guard the in-flight SIWE signature against an account switch: if the account
  changed while the signature was pending, clear instead of marking signed-in.
- restore a "Wrong network — switch" affordance for a signed-in user who drifted
  off the expected chain (the custom chip had dropped what the default
  ConnectButton showed).

Minor: gauntlet underfunded copy pointed "above" at the removed inline deposit
panel → point at the header wallet menu. The header balance pill polls on a
slow 30s interval (shares the query key, so an open BankrollPanel still drives
8s refreshes) instead of 8s on every route.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
super-vikram and others added 2 commits July 12, 2026 02:32
Adds an opt-in, #[ignore]d integration test (tournament_claim_and_refund_live)
and a runner (scripts/test-sepolia-tournament.sh) that verify the tournament
money paths against a real chain — closing the gap that the on-chain claim/
refund UI could only be reasoned about, not exercised end-to-end.

The test runs the exact production Merkle code (merkle_root / merkle_proof /
tournament_leaf) plus the server's EIP-712 root-settle, then:
  - claimTournament: a Rust-built proof verifies against the deployed Solidity
    _verifyProof and credits the winner's bankroll
  - a double-claim reverts (AlreadyClaimed), asserted via eth_call
  - claimRefund: an entrant reclaims their buy-in past the settle window
It deploys throwaway MockUSDC + escrows (short refund window), so it's
repeatable and needs only one funded key + a Base Sepolia RPC. Env-gated
(LEDGER_TEST_RPC / LEDGER_TEST_KEY / LEDGER_TEST_TIMEOUT) and #[ignore]d, so a
normal `cargo test` never runs it or spends testnet ETH.

Verified green end-to-end against a local anvil (same code path as Sepolia);
the existing Anvil test still covers the OnchainSettlement wrapper and the
happy-path claim.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Non-behavioral tidy-ups from the review's cleanup angles.

Extract the copy-pasted mount gate (useState + useEffect + placeholder) into a
useMounted() hook, used by AuthButton, WalletMenu, and the home / profile /
gauntlet / tournament pages.

Collapse AuthButton's two auto-effects (auto-switch chain, then auto-sign) into
one: runSignIn already switches to the expected chain before signing, so a
single guarded auto-sign effect covers the whole connect → switch → sign flow.
Drops the redundant switchTried ref and the standalone switch prompt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
super-vikram and others added 2 commits July 12, 2026 02:40
Fold PR 14's tournament claim/refund out of the tournament page and into the
header wallet menu, where it sits alongside deposit/withdraw — claims credit the
same escrow bankroll, so they belong together rather than scattered across the
tournament page's cards and finished-game views.

- New ClaimWinnings: discovers the connected wallet's settled/abandoned buy-in
  tournaments and renders a claim (Merkle payout) or refund per one that has
  something to collect. Rendered in the wallet popover under the bankroll panel;
  the discovery fetch is lazy (only while the popover is open).
- TournamentClaim gains an optional `label` (tournament name) and an
  `onResolved` callback so the list hides its "Tournament winnings" header when
  nothing is actually claimable (e.g. small fields credited directly).
- Remove the three inline TournamentClaim placements from the tournament page;
  its copy now points winners/entrants at the wallet menu to claim.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
De-duplicate cross-page logic surfaced while wiring the claim UI, so the pages
compose shared primitives instead of each re-implementing them.

- useOnchainConfig(): one source of the server config + `wagerOn` gate,
  replacing the fetchConfig-into-state + `!!wagerEnabled && !!escrow` pattern
  duplicated in Lobby, tournament, gauntlet, WalletMenu, and AuthButton.
- useEnsureChain(): the single "switch to the expected chain before a write"
  step, replacing the inline ensureChain in BankrollPanel, AuthButton, and
  TournamentClaim.
- lib/tournaments.ts: fetchTournaments()/fetchTournament() + the shared
  Tournament type, replacing the ids→details fan-out duplicated between the
  tournament lobby and the bankroll claim discovery (ClaimWinnings).

No behavior change; tsc clean and all pages render.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fixes from the entire-PR review.

Finding 1/8 (high) — DB-backed claimable-tournaments endpoint. The claim UI
discovered tournaments via the in-memory GET /tournaments list, which isn't
rehydrated on restart, so settled/abandoned payouts/refunds became undiscoverable
in-app after any deploy. Add GET /tournaments/claimable/{address} sourced from
Postgres: migration 0008 adds a tournaments.name column, upsert_tournament now
persists it, and claimable_tournaments(address) returns the wallet's
finished (complete|settled|abandoned) buy-in tournaments via a JSONB players
containment query. ClaimWinnings now calls this (one request, server-filtered)
instead of an N+1 client enumeration — also fixes the O(all-tournaments) cost.

Finding 2 — stale publicClient. usePublicClient({ chainId: expected }) in
BankrollPanel + TournamentClaim, so a receipt read after ensureChain switches
doesn't hit an undefined/other-chain client and falsely report a succeeded tx
as failed.

Finding 3 — reset ClaimWinnings' resolved map on account change (not just
disconnect), so a prior wallet's claimable state can't keep the header shown.

Finding 4 — include 'complete' (settlement-enqueue-failed) tournaments in the
claimable set, so a stuck buy-in stays refundable via the UI. (Now server-side.)

Finding 5 — fetchTournaments tolerates a single failing detail fetch instead of
blanking the whole list.

Findings 6/7/9 — single-source TournamentClaim's state via one `kind`
discriminant (drops the duplicated hasAction conditions); remove the dead
onClaimed prop; already-claimed no longer counts toward showing the header.

Finding 10 — fmtUsdcSigned (BigInt, no float) in escrow.ts, used by ProfileStats.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
From the re-review of the review-fix commit.

- Rate-limit the claimable endpoint (CLAUDE.md: "a new HTTP route is
  unthrottled unless you add it to a throttled router"). It's the one
  tournament route that hits Postgres (an unindexed JSONB containment scan), so
  move the handler from matchmaking (unthrottled) into players::routes(), which
  already carries the per-IP rate_limit_reads layer.
- Drop the unused buy_in from the claimable response end-to-end (Rust row +
  view, TS type) — on-chain buyIn is the source of truth; the WHERE still
  filters buy_in IS NOT NULL.
- ProfileStats: stakes are non-negative, so use fmtUsdc(g.stake) directly
  instead of fmtUsdcSigned(...).replace("+","").
- Fix a stale comment in the tournament page (Tournament type is no longer
  "shared with the bankroll claim discovery" — that uses ClaimableTournament).

net/stake are Decimal::from(u128) (scale 0) → integer base-unit strings, so
fmtUsdcSigned's BigInt parse is correct (verified, no change needed).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vikramarun
vikramarun merged commit 1720524 into main Jul 12, 2026
3 checks passed
vikramarun pushed a commit that referenced this pull request Jul 12, 2026
Brings in PR #13 (web UX pass) + PR #14 (leaderboard/claim). No conflicts:
the shared files (gauntlet page, persistence, matchmaking) merge cleanly in
non-overlapping regions. Verified: Rust workspace builds and the full suite
passes on the merged tree.
@vikramarun
vikramarun deleted the claude/home-page-ux-redesign-9e4591 branch July 17, 2026 17:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants