Commit Graph

117 Commits

Author SHA1 Message Date
soroush.asadi 3875141f46 fix(game): prevent green-felt freeze — loading spinner + retry resync
CI/CD / CI - API (dotnet build + engine sim) (push) Successful in 2m52s
CI/CD / CI - Web (tsc + next build) (push) Successful in 1m10s
CI/CD / Deploy - local stack (db + server + web) (push) Successful in 57s
Three changes:
1. GameTable shows a spinner instead of an empty table when mode=online
   and seatPlayers is empty (waiting for first state broadcast).
2. enterServerMatch schedules a 3s interval that calls service.resync()
   until seatPlayers is populated, guaranteeing the state always lands.
3. resync() added to OnlineService interface + both implementations so
   the game store can call it without casting.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 20:05:16 +03:30
soroush.asadi 4fb5a1776f fix(matchmaking): broadcast player list so queue avatars appear for all waiting players
CI/CD / CI - API (dotnet build + engine sim) (push) Successful in 1m18s
CI/CD / CI - Web (tsc + next build) (push) Successful in 1m12s
CI/CD / Deploy - local stack (db + server + web) (push) Successful in 3m32s
BroadcastQueueLocked now sends the full waiting-player list (name/avatar/level)
alongside the count. The client maps it onto mm.players so every queued player's
avatar shows in the 4-slot grid for all waiting users, not just the slot-0 viewer.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 19:46:28 +03:30
soroush.asadi 940e2af6d2 feat(online): live queue count — friends see each other waiting
CI/CD / CI - API (dotnet build + engine sim) (push) Successful in 1m24s
CI/CD / CI - Web (tsc + next build) (push) Successful in 1m10s
CI/CD / Deploy - local stack (db + server + web) (push) Successful in 1m52s
The server only sent the queue size to the player who just joined, and the
client dropped the count entirely (emitMM ignored s.players). So two friends
queuing together never saw each other, even though the server does seat 2+
waiting humans together within ~25s.

- Server: BroadcastQueueLocked() pushes the current queue size to EVERY waiting
  player on join/cancel (not just the joiner).
- Client: thread the count through emitMM → MatchmakingState.waiting.
- MatchmakingScreen shows "N players in queue" (mm.inQueue) when ≥2 humans wait,
  so friends can tell they're queued together before bots fill the empty seats.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 19:26:13 +03:30
soroush.asadi fe3bedc631 fix(online): trump chooser only shows to the hakem, not every player
CI/CD / CI - API (dotnet build + engine sim) (push) Successful in 2m25s
CI/CD / CI - Web (tsc + next build) (push) Successful in 1m10s
CI/CD / Deploy - local stack (db + server + web) (push) Successful in 1m5s
The "pick the hokm" overlay gated on players[hakem].isHuman — true on EVERY
human client when the hakem is human, so all players saw the chooser at the
start of a round. After the seat-rotation fix the viewer is always local seat 0,
so the correct check is hakem===0 ("I am the hakem"). Same fix for the 1–4
suit keyboard shortcut. Non-hakem players now see a "{name} is choosing trump…"
waiting overlay (new TrumpWaiting component) instead.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 14:06:39 +03:30
soroush.asadi 2aac6257d6 fix(online): rotate server state to viewer's seat — non-seat-0 players can play
CI/CD / CI - API (dotnet build + engine sim) (push) Successful in 1m1s
CI/CD / CI - Web (tsc + next build) (push) Successful in 1m12s
CI/CD / Deploy - local stack (db + server + web) (push) Successful in 1m7s
The server is authoritative with ABSOLUTE seats and tells each client its own
seat via mySeat, but the client copied seats verbatim — so any player not at
absolute seat 0 had their hand at players[mySeat] while the table read players[0]
and "your turn" checked turn===0. Result: they couldn't play (server auto-played
after the timeout → "hang"), and the turn highlight was identical for everyone
instead of rotating per viewer.

Fix (client-only; server was correct): new viewerRot(mySeat) rotates every
seat-indexed value into the viewer's frame (viewer → local seat 0): players/hands,
turn, hakem, leadSeat, lastTrickWinner, currentTrick, hakemDraw, seat roster,
disconnectedSeat, and the team arrays (matchScore/roundTricks/lastRoundResult/
matchWinner — odd seats swap team order). Store mySeat and rotate reaction bubbles
too (they carried absolute seats).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 08:59:03 +03:30
soroush.asadi 0790ad6fe0 chore(prod): real leaderboard, prod guards, payment hardening
CI/CD / CI - API (dotnet build + engine sim) (push) Successful in 2m4s
CI/CD / CI - Web (tsc + next build) (push) Successful in 1m9s
CI/CD / Deploy - local stack (db + server + web) (push) Successful in 2m11s
Production-readiness pass — remove mock-in-prod and harden the server:
- leaderboard: new DB-backed LeaderboardService + /api/leaderboard (ranked by
  rating, 30s cache, bounded scan); client now calls it instead of mock fake data.
- online count: client uses real /api/stats/online (dropped the fabricated ≥50 floor).
- boot guards (Production): refuse to start if Sms:ApiKey is missing (OTP would
  run in dev mode = fixed code for any phone) or Iab:AllowUnverified is true
  (forged tokens could mint coins).
- payments: ZarinPal + IAB HttpClients get 15s timeouts; ZarinPal/FlatPay gateway
  failures are now logged instead of silently swallowed.
- OTP: periodic prune of expired codes + stale rate-limit logs (was an unbounded
  in-memory leak over a long-running process).
- DB: EnableRetryOnFailure for Postgres (transient-fault resilience).
- docker-compose: ZarinPal sandbox now defaults to false (real payments).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 09:03:12 +03:30
soroush.asadi 4739018488 feat(avatars): show the uploaded profile photo everywhere
CI/CD / CI - API (dotnet build + engine sim) (push) Successful in 2m35s
CI/CD / CI - Web (tsc + next build) (push) Successful in 1m17s
CI/CD / Deploy - local stack (db + server + web) (push) Successful in 1m12s
Previously the uploaded profile photo only appeared in a few places (profile,
top bar, leaderboard, public profile); chat, friends, game table, match intro,
post-match roster and private rooms showed the emoji avatar only.

- carry avatarImage end-to-end:
  - server DTOs: FriendDto, SeatPlayerDto, RoomPlayerDto, MatchmakeRequest +
    Player/SeatSlot/PSeat; ResolveProfile now returns avatarImage; FriendDtoFor
    fills it from the profile.
  - client types: Friend, RoomSeat.player, MatchmakingState.players,
    ServerSeatPlayer, SeatPlayer (adds avatarId + avatarImage).
  - signalr-service: send my avatarImage on StartMatchmaking/CreatePrivateRoom;
    carry it through mapRoom.
  - game-store: applyServerState + newOnlineMatch + offline match now populate
    avatarId/avatarImage (seat 0 uses your own profile photo).
- render every avatar through the shared <Avatar> component (image → emoji
  fallback): ChatScreen, FriendsScreen (requests/friends/chats), GameTable
  seats, MatchIntroOverlay, MatchPlayersList, RoomScreen.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 08:17:27 +03:30
soroush.asadi e5b48ecb26 feat(audio): music off by default — sound effects only
CI/CD / CI - API (dotnet build + engine sim) (push) Successful in 2m33s
CI/CD / CI - Web (tsc + next build) (push) Successful in 1m11s
CI/CD / Deploy - local stack (db + server + web) (push) Successful in 2m14s
Background music now defaults to OFF; the default experience is SFX only.
Players can still enable music (santoor track) in Profile → settings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 00:36:23 +03:30
soroush.asadi 23b3713b44 fix(online): green-felt freeze — replay initial state to late subscriber
CI/CD / CI - API (dotnet build + engine sim) (push) Successful in 2m55s
CI/CD / CI - Web (tsc + next build) (push) Has been cancelled
CI/CD / Deploy - local stack (db + server + web) (push) Has been cancelled
Root cause: the server sends matchFound then immediately broadcasts the first
state, but the client only subscribes to state inside enterServerMatch, which
runs a React effect later — so the ordered "state" message is dispatched while
there are no subscribers and is dropped. The server then waits for the human
hakem's trump choice that can never come → permanent freeze on the green felt.

- signalr-service: cache lastState; replay it to a late onState subscriber on a
  microtask (after enterServerMatch resets its store); clear the cache on every
  fresh-match entry (startMatchmaking / createRoom / acceptInvite) so a finished
  game's final state is never replayed into a new match.
- safety net: if no state lands within 2.5s of matchFound, the client invokes
  the new Resync hub method; server re-sends the current state to that player.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 00:33:07 +03:30
soroush.asadi f97354167d tune(mm): cap the solo wait at 25s (was 75s)
CI/CD / CI - API (dotnet build + engine sim) (push) Successful in 2m13s
CI/CD / CI - Web (tsc + next build) (push) Successful in 1m10s
CI/CD / Deploy - local stack (db + server + web) (push) Successful in 1m5s
- full table of 4 humans still starts instantly at any time
- at the 15s checkpoint, 2+ humans start together (bots fill empty seats)
- a lone player now waits until a precise 25s deadline, then AI fills and starts
- lower the client "connection stuck" hint to 40s to match the shorter wait

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 22:18:49 +03:30
soroush.asadi c0e3fdb046 feat(mm): wait longer for a real opponent; add "start with bots now"
CI/CD / CI - API (dotnet build + engine sim) (push) Successful in 34s
CI/CD / CI - Web (tsc + next build) (push) Successful in 1m9s
CI/CD / Deploy - local stack (db + server + web) (push) Successful in 1m8s
- server: a lone player in the online-league queue now keeps waiting (re-checking
  every 15s) up to 75s so an online opponent has a real chance to join; the moment
  a 2nd human queues they're matched together, and a full 4 still forms instantly.
  Add PlayNow hub method to force-start with bots on demand.
- client: matchmaking screen shows a "شروع با ربات / Start with bots" button after
  a few seconds so the player can skip the wait; waiting copy updated; raise the
  "connection stuck" hint threshold to 90s so it no longer fires during normal waits.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 22:12:48 +03:30
soroush.asadi 9901c5e6d4 feat(audio,site): calm santoor default music + card-fan logo site redesign
CI/CD / CI - API (dotnet build + engine sim) (push) Successful in 2m0s
CI/CD / CI - Web (tsc + next build) (push) Successful in 1m9s
CI/CD / Deploy - local stack (db + server + web) (push) Successful in 1m18s
- audio: default background music is now the santoor track (calm Persian),
  rebuilt as a real plucked-santoor loop — fast metallic attack, shimmer
  overtones, soft tonic drone, longer Dastgah-e-Shur phrase
- site: marketing logo is now the app's card-fan icon (Logo.tsx + icon.svg);
  hero features the big logo with gold halo, floating suit motifs, and
  polished section dividers

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 21:48:59 +03:30
soroush.asadi 6aa4f37642 fix(mm): pro players also wait the 15s queue; compact post-match roster
CI/CD / CI - API (dotnet build + engine sim) (push) Successful in 4m45s
CI/CD / CI - Web (tsc + next build) (push) Successful in 1m10s
CI/CD / Deploy - local stack (db + server + web) (push) Successful in 1m33s
- server: remove pro instant-start so all players queue for up to 15s,
  giving real players a chance to seat together before bot-fill
- post-match: render the 4 seats as a horizontal strip so every player
  is visible at once without scrolling

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 21:31:18 +03:30
soroush.asadi 60d44100a2 ui(post-match): compact the result modal so it fits mobile without scrolling
CI/CD / CI - API (dotnet build + engine sim) (push) Successful in 37s
CI/CD / CI - Web (tsc + next build) (push) Successful in 1m23s
CI/CD / Deploy - local stack (db + server + web) (push) Successful in 1m1s
The end-of-match modal (title + reward rows + XP bar + full roster + button)
was too tall on phones and scrolled. Shrink the mobile sizes (padding, emoji,
title, hero-coins, spacing) with sm: bumping back up on larger screens, and
tighten the player roster rows. Fits a portrait phone in one view.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 20:43:36 +03:30
soroush.asadi d932dbbb52 feat(game): drag a card up to the board center to play it
CI/CD / CI - API (dotnet build + engine sim) (push) Successful in 25s
CI/CD / CI - Web (tsc + next build) (push) Successful in 1m9s
CI/CD / Deploy - local stack (db + server + web) (push) Successful in 56s
Playable hand cards are now draggable (Framer drag + dragSnapToOrigin): drag one
up toward the table center and release to play it; release short and it snaps
back. Tapping still plays as before. touch-action:none so the drag gesture works
on mobile without scrolling.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 08:52:40 +03:30
soroush.asadi e1e3a716a4 ui(game): minimal, smaller scoreboard
CI/CD / CI - API (dotnet build + engine sim) (push) Successful in 5m46s
CI/CD / CI - Web (tsc + next build) (push) Successful in 1m10s
CI/CD / Deploy - local stack (db + server + web) (push) Successful in 1m5s
Compact the in-game scoreboard to a single small pill: team label + score with
trick count inline in parens (e.g. "0 (3)"), a thin dot separator, and a tiny
target number — dropping the tall 3-line columns and large fonts. Frees more
HUD room and reads at a glance.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 08:18:37 +03:30
soroush.asadi d05cce6550 feat(payments): route coin purchases through FlatRender Pay broker
CI/CD / CI - API (dotnet build + engine sim) (push) Successful in 56s
CI/CD / CI - Web (tsc + next build) (push) Successful in 1m11s
CI/CD / Deploy - local stack (db + server + web) (push) Successful in 3m38s
ZarinPal only accepts callbacks on pay.flatrender.ir, so bargevasat
pays through the shared broker and is credited via a signed webhook.

- FlatPayService: broker client (HMAC-signed /v1/pay/request) + webhook
  signature verification + in-memory idempotency guard.
- Program.cs: /api/coins/pay/request prefers the broker when configured
  (FlatPay__ApiKey/Secret set), else the legacy direct ZarinPal path;
  new public POST /api/coins/pay/webhook verifies the HMAC and credits
  coins from the echoed metadata (idempotent).
- appsettings + docker-compose: FlatPay config (empty ⇒ legacy path).
- web: recognise the broker's ?status=Paid return + re-refresh profile
  (coins are credited server-side via webhook).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 00:36:12 +03:30
soroush.asadi fefa9e2e3a fix(signalr): force Long-Polling transport so the hub connects through nginx
CI/CD / CI - API (dotnet build + engine sim) (push) Successful in 47s
CI/CD / CI - Web (tsc + next build) (push) Successful in 1m9s
CI/CD / Deploy - local stack (db + server + web) (push) Successful in 1m11s
Server logs showed REST working but ZERO hub activity — the SignalR WebSocket
upgrade isn't getting through the nginx/CDN stack and auto-fallback wasn't
recovering, so StartMatchmaking never reached the server (matchmaking spun
forever). Force the HttpTransportType.LongPolling transport — plain HTTP that
already works (same path as REST); SignalR holds the poll open so it's
effectively real-time for a turn-based game. Revertable once the api block
proxies WS upgrades.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 22:37:04 +03:30
soroush.asadi f059065d4b ui(matchmaking): always show your own avatar in the first seat
CI/CD / CI - API (dotnet build + engine sim) (push) Successful in 26s
CI/CD / CI - Web (tsc + next build) (push) Successful in 1m8s
CI/CD / Deploy - local stack (db + server + web) (push) Successful in 56s
While searching, seat 0 now shows the current player's avatar/name/level
immediately (server matchmaking only sent a count, so all seats showed "?").
Matched opponents fill the remaining seats as they arrive; the rest stay "?".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 21:00:51 +03:30
soroush.asadi 99b9ee5c91 fix(game): center played cards — bake -50% into Framer transform (RTL)
CI/CD / CI - API (dotnet build + engine sim) (push) Successful in 51s
CI/CD / CI - Web (tsc + next build) (push) Successful in 1m8s
CI/CD / Deploy - local stack (db + server + web) (push) Successful in 56s
Root cause: the trick cards used a Tailwind -translate-x-1/2 -translate-y-1/2 to
center on the felt, but Framer Motion owns `transform` (from x/y/scale), so that
centering class was clobbered. In RTL the auto-positioned card then anchored to
the right edge and the whole trick cross drifted left of center.

Fix: drop the size-0 anchor; position each card at left-1/2 top-1/2 and use
Framer `transformTemplate` to prepend translate(-50%,-50%) before the animated
translate(x,y) scale — so centering survives and the pile sits dead-center in
both LTR and RTL. Burst particles re-centered too.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 20:34:43 +03:30
soroush.asadi 7c6c9fcd90 fix(game): center the trick area in RTL (felt no longer overflows its container)
CI/CD / CI - API (dotnet build + engine sim) (push) Successful in 38s
CI/CD / CI - Web (tsc + next build) (push) Successful in 1m10s
CI/CD / Deploy - local stack (db + server + web) (push) Successful in 1m3s
The felt was w-[min(96vw,560px)] inside a p-3 flex container; on phones 96vw
exceeds the padded width, and an overflowing flex item under justify-center in
an RTL layout pins to the start (right) and overflows left — so the trick area
(centered on the felt) drifted off-center. Added max-w-full to cap the felt to
its container so justify-center truly centers it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 20:03:57 +03:30
soroush.asadi c287c7d62c ui(game): compact trump/speed badges on mobile so the scoreboard fits
CI/CD / CI - API (dotnet build + engine sim) (push) Successful in 50s
CI/CD / CI - Web (tsc + next build) (push) Successful in 1m10s
CI/CD / Deploy - local stack (db + server + web) (push) Successful in 1m3s
On narrow phones the top HUD crowded the scoreboard against the trump/speed
badges + action buttons. The badges now show icon/symbol-only on mobile (labels
hidden < sm) with tighter padding, freeing horizontal space so the scoreboard
renders cleanly. Buttons stay shrink-0; scoreboard keeps shrink min-w-0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 18:27:27 +03:30
soroush.asadi 868bef0c56 revert(signalr): restore negotiate + auto-transport (CDN now bypassed)
CI/CD / CI - API (dotnet build + engine sim) (push) Successful in 42s
CI/CD / CI - Web (tsc + next build) (push) Successful in 1m8s
CI/CD / Deploy - local stack (db + server + web) (push) Successful in 54s
api.bargevasat.ir is now CDN-bypassed (origin answers directly), so the
negotiate POST works again. Drop the WS-only skipNegotiation workaround and use
the standard negotiate flow, which auto-falls back WS → SSE → long-poll if a
WebSocket upgrade isn't available.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 18:07:58 +03:30
soroush.asadi 21fd5c123e fix(signalr): skip negotiate, connect WebSockets-only (CDN 404s the POST)
CI/CD / CI - API (dotnet build + engine sim) (push) Successful in 50s
CI/CD / CI - Web (tsc + next build) (push) Successful in 1m8s
CI/CD / Deploy - local stack (db + server + web) (push) Successful in 1m0s
WCDN rejects the SignalR negotiate POST (404, wcdn-nfc-reason: Http_Method), so
the hub never connects and online matchmaking never starts. Connect directly via
WebSockets with skipNegotiation so there's no negotiate POST; the JWT rides the
?access_token query the server already accepts for /hub. The proper fix remains
bypassing the CDN for api.bargevasat.ir.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 17:53:16 +03:30
soroush.asadi 76c4b68a74 auth: store-review test login + matchmaking no-hang/watchdog
CI/CD / CI - API (dotnet build + engine sim) (push) Successful in 56s
CI/CD / CI - Web (tsc + next build) (push) Successful in 1m9s
CI/CD / Deploy - local stack (db + server + web) (push) Successful in 1m7s
- OtpService: a designated test phone (default 09120000000 / code 453115,
  overridable via Sms__TestPhone/Sms__TestCode) skips real SMS and always
  verifies — for Google Play / Bazaar / Myket reviewers. Give them these creds.
- Matchmaking UX: tapping a league now navigates to the matchmaking screen
  BEFORE awaiting the SignalR handshake, so the button can't freeze. Added a
  watchdog hint after 28s ("connection took too long, cancel & retry") so it
  never spins forever when the hub doesn't connect.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 16:40:01 +03:30
soroush.asadi a35acea7e4 feat(rooms): real server-side private games with friend invites (no bot swap)
CI/CD / CI - API (dotnet build + engine sim) (push) Successful in 27s
CI/CD / CI - Web (tsc + next build) (push) Successful in 1m10s
CI/CD / Deploy - local stack (db + server + web) (push) Successful in 1m16s
Private rooms were 100% client-simulated (the "friend" auto-accepted then bots
filled invited seats). Now they're server-authoritative over SignalR:

Server (GameManager.PrivateRooms + GameHub):
- Room registry with create/invite/accept/decline/addBot/clearSeat/start/leave.
- Invite pushes a `roomInvite` to that user (Clients.User); the seat stays
  "invited" (a pending guest with their real profile, resolved server-side) — it
  is NEVER replaced by a bot.
- StartPrivate refuses while any invite is pending; only EMPTY seats fill with
  bots. Then it spins up a live GameRoom and matchFound → both devices enter.
- Host leave / disconnect closes the room (roomClosed); members free their seat.

Client:
- signalr-service implements the room methods over the hub (+ room/roomInvite/
  roomClosed events, room mapping, onRoomInvite); mock keeps offline no-ops.
- online-store accept/declineInvite; RoomScreen blocks "Start" while an invite
  is pending and auto-enters the live game on matchFound (host + friend).
- New global InviteModal (accept/decline) + i18n (invite.*, room.waitAccept).

Addresses: (1) no bot replacement, (2) game waits for acceptance, (3) invited
friend shown as a pending guest with their name/avatar.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 15:59:28 +03:30
soroush.asadi 6530096994 music: re-enable background loop (default = playful) + profile on/off; coins 2000
CI/CD / CI - API (dotnet build + engine sim) (push) Successful in 28s
CI/CD / CI - Web (tsc + next build) (push) Successful in 1m8s
CI/CD / Deploy - local stack (db + server + web) (push) Successful in 1m8s
- sound.ts: restored startMusic (was a no-op stub) playing the selected track
  through musicGain (in-game mute still applies); default track switched to
  "playful" (per user). Music auto-starts on init when enabled.
- Profile → Audio: re-added a music on/off toggle (so players can disable it
  outside the game too); SFX toggle unchanged.
- Economy tune: starting coins 1000 → 2000 (mock defaultProfile + server
  ProfileDto) so new players start a bit richer.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 13:23:08 +03:30
soroush.asadi 6502b17356 balance(achievements): strictly-escalating milestone coin rewards
CI/CD / CI - API (dotnet build + engine sim) (push) Successful in 32s
CI/CD / CI - Web (tsc + next build) (push) Successful in 1m9s
CI/CD / Deploy - local stack (db + server + web) (push) Successful in 1m0s
The old reward formula rounded (40+6g)/50 on the raw goal, so adjacent
milestones could pay the same (e.g. "1 win" and "5 wins" both 50) and the curve
was lumpy — not a standard escalating-reward ladder. Reward now scales by tier
index: 50, 150, 250 … capped at 1500, strictly increasing per milestone.
Mirrored client (gamification.ts) + server (Gamification.cs) so live grants match.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 11:28:08 +03:30
soroush.asadi 974a6bf0ae feat: shop buy-coins CTA, pin chats (max 3), surrender cooldown, OTP hardening
CI/CD / CI - API (dotnet build + engine sim) (push) Successful in 29s
CI/CD / CI - Web (tsc + next build) (push) Successful in 1m9s
CI/CD / Deploy - local stack (db + server + web) (push) Successful in 1m6s
- Shop: when short on coins the detail sheet now shows "{n} more coins" + a
  "Get coins" CTA that opens the buy-coins page (was a dead disabled button).
- Chat: pin/unpin conversations (max 3, persisted to localStorage); pinned float
  to the top with a gold pin. i18n chat.pin/unpin/pinLimit.
- Surrender: server now rate-limits forfeit asks at a human teammate
  (45s per-user cooldown) so it can't be spammed. (Bot teammate still ends
  immediately; teammate confirm dialog already existed.)
- OTP login hardening: Kavenegar send now parses the API status from the body
  (HTTP 200 can still be a failure) + logs it + 12s timeout; client auth fetch
  gets a 20s AbortController timeout so a lost response surfaces an error
  instead of freezing on "sending…".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 11:01:14 +03:30
soroush.asadi 97d3a02a3c feat: new "card fan" app icon — web favicon/PWA + Android adaptive
CI/CD / CI - API (dotnet build + engine sim) (push) Successful in 41s
CI/CD / CI - Web (tsc + next build) (push) Successful in 1m15s
CI/CD / Deploy - local stack (db + server + web) (push) Successful in 1m6s
- Master SVGs + generator in scripts/icon/ (icon.svg full design, icon-foreground.svg
  cards-only for the Android adaptive layer, gen-icons.mjs via sharp).
- Web/PWA: regenerated favicon.ico (16/32/48), src/app/apple-icon.png, public/icon.svg,
  icon-192/512, icon-maskable-512; manifest now lists png + maskable icons.
- Android (Capacitor): ic_launcher / ic_launcher_round / ic_launcher_foreground for all
  densities + ic_launcher-playstore 512; adaptive background switched from flat white
  to a navy radial-gradient drawable (matches the icon), foreground = the gold card fan.

Design: navy field, gold rounded frame, three fanned cards with a gold spade —
on-brand with the in-app "Persian luxury" look.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 04:30:34 +03:30
soroush.asadi bc695bc8e9 feat: OTP rate limit, private-room invite UX, in-game UI fixes
CI/CD / CI - API (dotnet build + engine sim) (push) Successful in 54s
CI/CD / CI - Web (tsc + next build) (push) Successful in 1m12s
CI/CD / Deploy - local stack (db + server + web) (push) Successful in 1m11s
Auth / security
- Rate-limit real SMS OTP sends (dev mode unlimited): 60s resend cooldown,
  5 per phone/hour, 300/hour global backstop. OtpService.CheckAndRecordRate;
  POST /api/auth/otp/request returns 429 {error,retryAfter}; AuthScreen shows
  auth.rateLimited. Knobs in appsettings Sms (Sms__* env).

Private rooms (invite)
- Cancel-invite button on pending seats; friend picker shows presence
  (online/offline/in-game, sorted online-first) and flags in-game players.
- Mock invite stays pending ~3.5s and a cancel truly stops the auto-accept
  (was a bug that re-seated cancelled invites).

In-game UI
- Scoreboard is compact + shrink-safe (no overflow on narrow screens).
- Played trick cards land dead-center (were ~2px off the corner anchor).

Plus the in-flight typing-indicator work (GameHub, ChatScreen).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 00:30:20 +03:30
soroush.asadi 78878efc22 fix(auth): fully clear profile on logout (no stale name/gender after sign-out)
CI/CD / CI - API (dotnet build + engine sim) (push) Successful in 25s
CI/CD / CI - Web (tsc + next build) (push) Successful in 1m11s
CI/CD / Deploy - local stack (db + server + web) (push) Successful in 1m4s
The mock service intentionally KEPT the persisted profile (hokm.profile) on
signOut, and getProfile() reloads it — so after logout the previous user's
name/gender/avatar resurrected from localStorage. Now signOut clears the
in-memory + persisted profile, and the SignalR service also clears its mock
fallback so the post-logout guest profile is fresh.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 09:17:49 +03:30
soroush.asadi 53759be8b7 ui: raise in-game emoji button above the hand + gender = male/female/unknown
CI/CD / CI - API (dotnet build + engine sim) (push) Successful in 29s
CI/CD / CI - Web (tsc + next build) (push) Successful in 1m13s
CI/CD / Deploy - local stack (db + server + web) (push) Successful in 1m4s
- GameTable reactions button (and its tray) moved up from the bottom-right so it
  no longer overlaps the player's cards on mobile portrait.
- Gender options are now Male / Female / Unknown — removed "other" from the
  Gender type, GENDER_META, and the profile picker; the empty value renders as
  «نامشخص» / "Unknown".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 08:59:15 +03:30
soroush.asadi 1954992203 fix(auth): advance to OTP code step in production + clear profile on logout
CI/CD / CI - API (dotnet build + engine sim) (push) Successful in 39s
CI/CD / CI - Web (tsc + next build) (push) Successful in 1m12s
CI/CD / Deploy - local stack (db + server + web) (push) Successful in 59s
- AuthScreen gated the code-entry step on devCode != null, so with real SMS
  (no devCode) it got stuck after "send". Gate on a `sent` flag instead; add
  sending state, send-failure message, "code sent" hint, change-number, and
  raise the code input cap to 6 (codes are 5 digits).
- signOut now resets the store to a fresh guest profile, and the SignalR
  service clears its cachedProfile — so the previous user's name/avatar no
  longer linger after logout.
- i18n: auth.sending / sendFailed / codeSent / invalidPhone / changeNumber.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 08:21:20 +03:30
soroush.asadi d1bd279eba feat(iap): native Myket in-app billing plugin (AIDL) + wire purchase/consume
CI/CD / CI - API (dotnet build + engine sim) (push) Successful in 24s
CI/CD / CI - Web (tsc + next build) (push) Successful in 1m8s
CI/CD / Deploy - local stack (db + server + web) (push) Successful in 1m1s
Implements real Myket IAB for the Capacitor app (Myket has no purchase
deep-link like Bazaar — it uses the classic Google Play IAB v3 AIDL bound to
the Myket app):

- AIDL: com.android.vending.billing.IInAppBillingService (Myket-compatible).
- MyketBillingPlugin (Capacitor): binds ir.mservices.market via
  "ir.mservices.market.InAppBillingService.BIND", runs getBuyIntent →
  startIntentSenderForResult, verifies INAPP_DATA_SIGNATURE with the RSA key
  (Security.java, SHA1withRSA), returns the purchaseToken; consume() too.
- MainActivity registers the plugin + forwards the purchase activity result.
- Manifest: ir.mservices.market.BILLING permission + <queries> for Android 11+
  package visibility.
- build.gradle: enable buildFeatures.aidl (AGP 8 disables it by default).
- storeBilling: Myket goes through the plugin (RSA key embedded); after server
  verify, BuyCoins consumes the purchase so coins can be re-bought.

Bazaar (deep-link) and web (ZarinPal) paths unchanged. Needs on-device testing
with the Myket app installed + published products.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 20:59:56 +03:30
soroush.asadi cd5742d623 iap: wire coin packs to Cafe Bazaar SKUs + auto-select Bazaar billing in the APK
CI/CD / CI - API (dotnet build + engine sim) (push) Successful in 32s
CI/CD / CI - Web (tsc + next build) (push) Successful in 1m7s
CI/CD / Deploy - local stack (db + server + web) (push) Successful in 1m7s
- Pack ids now equal the Bazaar/Myket SKUs (Coin5K / Coin12K / Coin28K /
  Coin65K) in both the server source-of-truth (ProfileService.Packs) and the
  mock, so the IAB credit path (Id == ProductId) grants the right coins.
  Coin totals + prices already matched the registered products.
- storeBilling.getStore(): when running inside the Capacitor Android shell and
  no explicit NEXT_PUBLIC_STORE flavor is set, default to "bazaar" so the APK
  uses Cafe Bazaar IAB (the web build stays on the ZarinPal gateway). Myket's
  native bridge still overrides when present.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 14:37:13 +03:30
soroush.asadi 66c83991d4 portrait-only: drop landscape rotate prompt + lock to portrait
CI/CD / CI - API (dotnet build + engine sim) (push) Successful in 38s
CI/CD / CI - Web (tsc + next build) (push) Successful in 1m6s
CI/CD / Deploy - local stack (db + server + web) (push) Successful in 1m24s
- Remove RotatePrompt (the "rotate to landscape" overlay) — the app is portrait
  now, so it only blocked the UI.
- page.tsx: best-effort orientation lock switched landscape → portrait.
- Add Playwright-based store-screenshot + icon scripts (scripts/shots.js,
  game.js, icon.js); generated images are gitignored.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 13:33:01 +03:30
soroush.asadi 7f08249fa7 fix(iab): correct package name to com.bargevasat.app + slot for Bazaar RSA key
CI/CD / CI - API (dotnet build + engine sim) (push) Successful in 36s
CI/CD / CI - Web (tsc + next build) (push) Successful in 1m9s
CI/CD / Deploy - local stack (db + server + web) (push) Successful in 1m5s
- storeBilling.ts and IabService PackageName defaulted to com.bargevasat.hokm,
  but the real app id is com.bargevasat.app (capacitor + android applicationId).
  The mismatch would break Bazaar deep-link purchases and server validation.
- Add IabOptions.BazaarRsaPublicKey to hold the Bazaar in-app billing RSA public
  key (documented; for the Poolakey local-signature flow, unused by the current
  deep-link + server pardakht verification).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 08:55:17 +03:30
soroush.asadi 6c431fee3e portrait: lock orientation + portrait-optimized felt table
CI/CD / CI - API (dotnet build + engine sim) (push) Successful in 38s
CI/CD / CI - Web (tsc + next build) (push) Successful in 1m9s
CI/CD / Deploy - local stack (db + server + web) (push) Successful in 57s
- Lock the app to portrait: AndroidManifest screenOrientation="portrait" and PWA
  manifest orientation "portrait".
- GameTable felt now occupies the middle band (between top HUD and the hand) with
  portrait proportions (w<=560, tall) so the you/partner/opponents diamond fits a
  tall screen comfortably instead of a wide landscape ellipse.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 07:51:04 +03:30
soroush.asadi a7c0900c3b ui: unified rounded navbar everywhere, vertical home actions, no bot disconnect spam
CI/CD / CI - API (dotnet build + engine sim) (push) Successful in 8m9s
CI/CD / CI - Web (tsc + next build) (push) Successful in 1m10s
CI/CD / Deploy - local stack (db + server + web) (push) Successful in 57s
- NavRail: one rounded "pill" tab bar on every screen (matches home). ScreenShell
  lays out as a portrait column and floats the nav with margins + safe-area;
  dropped the landscape side-rail variant.
- Home: the three mode cards now stack vertically as full-width rows (portrait
  friendly) instead of a 3-up landscape row.
- Disconnect: removed the simulated random opponent "disconnect" in local games
  (DISCONNECT_CHANCE) and the in-game DisconnectBanner — bots/filled seats just
  auto-play their turn; no message, no pause. (Live reconnect grace still tracked
  internally but no longer shows a banner.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 01:12:26 +03:30
soroush.asadi 55c0407d73 build(android): release signing + mirror/JDK setup; native-feel CSS
CI/CD / CI - API (dotnet build + engine sim) (push) Successful in 4m5s
CI/CD / CI - Web (tsc + next build) (push) Successful in 1m6s
CI/CD / Deploy - local stack (db + server + web) (push) Successful in 1m1s
- Release signing via android/keystore.properties (git-ignored); build.gradle
  signs release builds when the props file is present, stays unsigned otherwise.
- android/mirror-init.gradle: injects the myket.ir Maven mirror into every
  project's buildscript (dl.google.com is unreachable here) and pins Build-Tools
  to the installed 36.0.0. Build with:
    gradlew assembleRelease bundleRelease -I mirror-init.gradle
  (JAVA_HOME must point at a JDK 21 — Capacitor 8 compiles against Java 21.)
- gitignore keystores, keystore.properties, and /dist artifacts.
- Native-app feel: kill tap-highlight, long-press callout, and stray text
  selection (inputs/messages opt back in); touch-action: manipulation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 22:34:15 +03:30
soroush.asadi 857287fa84 mobile: fullscreen (immersive Android + PWA) + auto-hide reported nudity avatars
CI/CD / CI - API (dotnet build + engine sim) (push) Successful in 23s
CI/CD / CI - Web (tsc + next build) (push) Successful in 1m11s
CI/CD / Deploy - local stack (db + server + web) (push) Successful in 1m3s
Fullscreen on mobile:
- Android (Capacitor): MainActivity now runs edge-to-edge and hides the status +
  navigation bars (immersive, transient-on-swipe), re-asserted on focus.
- PWA: manifest display -> "fullscreen" with display_override fallback chain;
  viewport gains viewport-fit: cover for proper safe-area/edge-to-edge handling.

Moderation auto-hide:
- ProfileService.ReportUser now de-dupes nudity reports per reporter and, once
  NudityHideThreshold (3) distinct players flag a target's avatar as nudity,
  auto-removes their custom photo (reverts to default avatar). Counted from the
  ledger, so still no schema change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 19:32:49 +03:30
soroush.asadi 6641741669 feat: photo upload at level 3 + report a player (nudity avatar / chat insult)
CI/CD / CI - API (dotnet build + engine sim) (push) Successful in 2m58s
CI/CD / CI - Web (tsc + next build) (push) Successful in 1m9s
CI/CD / Deploy - local stack (db + server + web) (push) Successful in 1m0s
Photo upload:
- Lower the custom profile-photo gate from level 25 to level 3 (client const +
  i18n hint + server gate in ProfileService.Update). The level-25 "Expert" title
  is unrelated and unchanged.

Report a player:
- New ReportReason type + service.reportUser(targetId, reason, details?).
- Report entry points: a "گزارش تخلف" button + reason picker (nudity / insult /
  other) in the public-profile modal, and a flag button in the chat header
  (reports the peer for an insulting chat) with a confirmation toast.
- Mock records reports to localStorage; SignalR POSTs /api/report.
- Server: POST /api/report → ProfileService.ReportUser stores the report in the
  write-only ledger (kind="report", ref="{targetId}|{reason}|{details}") so no
  schema change is needed (server uses EnsureCreated, not migrations).
- i18n: report.* keys (fa + en).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 19:12:02 +03:30
soroush.asadi 8033023a1f matchmaking: deterministic 15s wait before bots fill empty seats
CI/CD / CI - API (dotnet build + engine sim) (push) Successful in 22s
CI/CD / CI - Web (tsc + next build) (push) Successful in 1m10s
CI/CD / Deploy - local stack (db + server + web) (push) Successful in 1m4s
Both the mock and the .NET server already waited then bot-filled, but used a
random 12-18s window. Make it exactly 15s on both sides so the rule is clear:
wait 15s for real online players to join, then replace any unfilled seats with
bots and start.

- client: new MATCH_QUEUE_WAIT_MS = 15000 in gamification.ts; mock beginSearch
  uses it instead of randInt(12000,18000).
- server: GameManager QueueWaitMs = 15000 (was randomized 12-18s per ticket).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 18:27:46 +03:30
soroush.asadi ad5b42db06 feat(profile): "set your city" gamification box → one-time 500-coin reward
CI/CD / CI - API (dotnet build + engine sim) (push) Successful in 28s
CI/CD / CI - Web (tsc + next build) (push) Successful in 1m10s
CI/CD / Deploy - local stack (db + server + web) (push) Successful in 1m6s
- New searchable city picker (src/lib/iran-cities.ts, ~60 Iranian cities,
  fa/en search) shown as a gold reward card at the top of the profile Basic tab.
- First time a non-empty city is set, the player earns 500 coins (CITY_REWARD),
  granted server-authoritatively. Collapses to a compact summary afterwards with
  a "change city" option (no re-reward).
- Frontend: UserProfile.city + cityRewardClaimed; mock-service grants on first
  set; session/service updateProfile accept `city`; celebratory toast + sfx.
- Backend (.NET): ProfileDto.City/CityRewardClaimed (JSON blob → no migration);
  ProfileService.Update grants +500 once and writes a "city" ledger entry.
- i18n: city.* keys (fa + en).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 18:11:45 +03:30
soroush.asadi efefbcec3d Lobby: leagues are play buttons w/ arrow; remove background music feature
CI/CD / CI - API (dotnet build + engine sim) (push) Successful in 35s
CI/CD / CI - Web (tsc + next build) (push) Successful in 1m14s
CI/CD / Deploy - local stack (db + server + web) (push) Successful in 58s
- OnlineLobbyScreen: each league row is now a tappable play button (queues a
  ranked match at that league's stake) with a forward arrow; the cheapest
  enterable league is highlighted gold. Drops the redundant separate "ranked
  random" CTA and the select-then-play step.
- Remove the background-music feature entirely: deleted the floating MusicToggle,
  the TopBar music button, and the Profile audio music toggle + style picker.
  sound.startMusic() is now an inert no-op so music never plays (sfx unchanged).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 17:23:26 +03:30
soroush.asadi deb83cf77c UX: landscape result screen, chat emojis, unread badges, remove XP text
CI/CD / CI - API (dotnet build + engine sim) (push) Successful in 4m33s
CI/CD / CI - Web (tsc + next build) (push) Successful in 1m7s
CI/CD / Deploy - local stack (db + server + web) (push) Successful in 55s
- PostMatchRewardsModal: short-height (landscape) compaction so the win/forfeit
  result fits without overflow (smaller emoji/coins/padding, max-h 94dvh, wider).
- Chat: emoji/sticker picker (owned reactions) — tap to send; hidden on focus.
- Unread messages: online-store now tracks a total `unread` (from
  listConversations); NavRail Friends icon shows a badge (unread + requests),
  refreshed every 12s on every screen; Friends «پیام‌ها» tab badged too.
  (Per-conversation unread badges already existed.)
- Remove "XP گران است" / "XP is expensive" from shop.xpHint.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 14:58:43 +03:30
soroush.asadi 24a2c251ad UX batch 2: room landscape-fit, rank vs league naming
CI/CD / CI - API (dotnet build + engine sim) (push) Successful in 3m43s
CI/CD / CI - Web (tsc + next build) (push) Successful in 1m6s
CI/CD / Deploy - local stack (db + server + web) (push) Successful in 58s
- Room: teams side-by-side in landscape so all 4 seats fit (still scrolls).
- Achievements: rename the 5 rating tiers from «لیگ» (league) to «رتبه» (rank)
  + category «رتبه» — so "league" only means the 3 playable match leagues.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 13:21:28 +03:30
soroush.asadi 494683b63b UX batch: lobby trim, private stake, coin shop, minimal toast
CI/CD / CI - API (dotnet build + engine sim) (push) Successful in 3m13s
CI/CD / CI - Web (tsc + next build) (push) Successful in 1m6s
CI/CD / Deploy - local stack (db + server + web) (push) Successful in 58s
- Lobby: remove private-room CTA (it's on Home now) → fits without scroll.
- Home: private rooms now cost 150 coins/player (stake 150).
- Buy Coins: drop the "secure payment" note; redesign packs as game-shop coin
  boxes (coin pile + amount + gold buy-price CTA), 2/3/4-col responsive.
- Notifications: minimal single-line corner toast, explicit ✕ close, hidden
  during play so it never disturbs the game.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 13:09:19 +03:30
soroush.asadi 3d3241b976 UNO polish: center nav-rail items, drop per-page XP bar, shop category tabs
CI/CD / CI - API (dotnet build + engine sim) (push) Successful in 6m33s
CI/CD / CI - Web (tsc + next build) (push) Successful in 1m6s
CI/CD / Deploy - local stack (db + server + web) (push) Successful in 1m0s
- NavRail: vertically center items in the side rail (was top-aligned).
- ScreenHeader: showXp defaults off — the level/XP bar no longer clutters every
  sub-page (it lives on Home's chip + the Profile page).
- Shop: category tabs (avatars / fronts / backs / reactions / stickers / titles
  / XP) so only one category shows at a time — no more endless scroll.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 11:53:10 +03:30