docs: add HANDOFF.md (full project state) + point CLAUDE.md at it
CI/CD / CI - API (dotnet build + engine sim) (push) Successful in 21s
CI/CD / CI - Web (tsc + next build) (push) Successful in 1m1s
CI/CD / Deploy - local stack (db + server + web) (push) Failing after 1s

Single source of truth for any agent/session continuing the project: run
instructions (dev + Docker stack), architecture, the client<->server
gamification sync rule, full feature status, CI/CD + Nexus HTTP-mirror cert
workaround, gotchas, and the TODO list.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-05 10:47:51 +03:30
parent b739b503eb
commit e450a6a2ed
2 changed files with 120 additions and 0 deletions
+103
View File
@@ -0,0 +1,103 @@
# Barg-e Vasat (برگ وسط) — Project Handoff
Persian **Hokm** card game for the Iran market. Web/PWA + Android (Capacitor),
vs-AI and online multiplayer, with a full gamification economy. This doc is the
single source of truth for any agent/session continuing the project.
---
## 1. Repo, remotes, run
- **Code root:** `D:\Projects\hokm` (Next.js app at root; .NET backend under `server/`).
- **Git remote `origin`** = Gitea: `https://git.soroushasadi.com/soroushdes/HokmPlay.git`, branch **`main`**. (No GitHub remote.) Pushing `origin main` triggers CI/CD (Gitea Actions).
- **Brand:** name «برگ وسط» / "Barg-e Vasat" (insider Hokm slang — middle card when weak). Don't rename to "Hokm"; folder/repo stay hokm/HokmPlay.
### Run locally (dev)
```bash
# frontend (root) — http://localhost:3000
npm run dev
# backend (.NET 10 + SignalR) — http://localhost:5005
cd server && HOKM_USE_SQLITE=1 dotnet run --project src/Hokm.Server/Hokm.Server.csproj
```
`.env.local` for live mode: `NEXT_PUBLIC_USE_SERVER=1`, `NEXT_PUBLIC_SERVER_URL=http://localhost:5005`.
Without `NEXT_PUBLIC_USE_SERVER=1` the app runs fully offline against the in-memory **mock** service.
### Run the Docker stack (local, prod-like) — what the user actually tests
Ports are in **15001600** so they coexist with `npm run dev`/`dotnet run`:
- web `http://localhost:1500` (nginx serving the static export) → api `:1505` (.NET) → `:1510` postgres.
```bash
cd D:\Projects\hokm
copy deploy\ENV_FILE.example .env # set JWT_KEY, POSTGRES_PASSWORD; registries default to HTTP Nexus
docker compose build server web
docker compose up -d
```
**Gotcha:** after rebuilding the web image, `docker compose up -d` sometimes says "Running" and keeps the old image. Force it:
```bash
docker stop hokm-web && docker rm hokm-web && docker compose up -d --no-deps web
```
### Build / verify (run before committing)
```bash
rm -rf .next && npx tsc --noEmit # types (rm .next first — stale .next/dev breaks typedRoutes)
npx tsx scripts/sim.ts # engine + gamification self-test (200 matches + 500 rewards)
dotnet build server/Hokm.slnx -c Release # server
npm run build # next static export
```
---
## 2. Architecture (the important bits)
- **Frontend:** Next 16 (App Router, `output:"export"` → static), React 19, Tailwind v4, Framer Motion, Zustand. RTL Persian default + English; custom i18n in `src/lib/i18n.tsx` (NOT next-intl) — **every string must be added to BOTH the `fa` and `en` dicts**.
- **Engine:** pure-TS Hokm engine/AI in `src/lib/hokm/`; mirrored as C# (`server/src/Hokm.Engine`, static class **`Rules`** — not `Engine`, to avoid namespace clash). Validated by `scripts/sim.ts` (TS) and `server/tools/Hokm.Sim` (C#).
- **Service seam:** all networking goes through `OnlineService` (`src/lib/online/service.ts`). Two impls: `MockOnlineService` (offline) and `SignalrService` (live). `getService()` picks via `NEXT_PUBLIC_USE_SERVER==="1"`.
- **Backend:** .NET 10 ASP.NET Core + SignalR (`server/src/Hokm.Server`), EF Core (SQLite dev / Npgsql Postgres prod), JWT. Hub `/hub/game`; REST under `/api/*`. Profile stored as a JSON blob (`ProfileRow`) + coin `Ledger`. In-memory matchmaking/rooms in `GameManager`/`GameRoom`.
- **⚠️ CRITICAL — keep gamification in sync:** `src/lib/online/gamification.ts` (client) and `server/src/Hokm.Server/Profiles/Gamification.cs` (server) implement the SAME rules (rating/Elo, coins, XP, achievements, titles). **In live mode the server is authoritative** — ranked games run server-side and push `reward`/`profile` over the hub; client-run (vs-computer/private) games submit a `MatchSummary` to `POST /api/match/result`. If you change a rule, change BOTH files identically (ids/goals/coins/metrics/formulas).
- **Zustand stores:** `ui-store` (History-API screen routing via hash), `session-store` (auth + profile, subscribes hub `onProfile`), `online-store` (friends/chat/leaderboard/matchmaking), `game-store` (the table driver — `mode: ai|online`, `live`, turn timer, minimize/resume, forfeit), `sound-store`, `notification-store`, `celebration-store`.
---
## 3. Feature status (DONE)
- Full offline vs-AI game (engine, AI, turn timer + auto-play, disconnect/reconnect sim).
- Online multiplayer over SignalR: matchmaking (pro skips queue, bots fill after wait), live server-run ranked games, server-authoritative entry/rewards.
- **Economy:** coins; ranked entry = stake (win +stake [+kot 40], lose stake); free vs-computer/private rooms. Buy-coins via **ZarinPal sandbox** (merchant `299685fb-cadf-4dfc-98e2-d4af5d81528d`, config-driven). Coin packs: starter 50k/95,000﷼, … Stores (Bazaar/Myket) must use their **IAB** (`/api/coins/iab/verify` scaffolded; token verification TODO).
- **XP/levels:** every game grants XP, **winner ×2**; **premium (pro) ×1.5**; max level 100; curve `100*lvl + 15*lvl²`. **Store sells XP packs** (xp1 +200/5k, xp2 +600/12k, xp3 +1500/25k coins; consumable; unlocks level achievements).
- **Achievements:** ~100, metric-driven generator (categories: victory/kot/streak/hakem/level/rank/veteran), incl. "7× hakem", "70 sweep". Dedicated **AchievementsScreen** (tabbed) + Profile summary. Some unlock **sticker packs**.
- **Cosmetics:** avatars, titles (incl. expert/professional/captain/leader ladder), card **front**+**back**, reaction packs, sticker packs (custom SVG art incl. crown/seven-zip/streak-fire). Profile **photo upload gated at level ≥ 25**.
- **Social:** friends + chat **server-persisted** (`Social/SocialService`, REST + hub `friendRequest`/`social`/`chat`); friend remove needs confirm. **Premium chat = animated gold bubbles**.
- **Forfeit:** request + teammate-confirm (server `GameRoom` forfeit flow); penalty = **lose 2× coins + 0 XP** (NO kot, and never mention kot); confirm dialog alerts the penalty.
- **End-of-game roster:** `MatchPlayersList` on the final screen (reward modal + AI match-over) lists everyone; **Add-friend** button for real non-bot players (seat `userId` threaded from server).
- **Celebrations:** `celebration-store` + `CelebrationOverlay` — animated XP count-up, level-up pop, achievement unlock; fires from shop purchases (XP/cosmetic) and unlocks. Reusable via `celebrate({...})`.
- **UX/UI:** "Persian luxury" palette (navy/teal/gold, glass) + **UNO-style tactile UX** rolled out to Home (hero Play), Shop (detail sheets), Lobby, Matchmaking, Profile, Leaderboard. Primitives in `globals.css`: `.press-3d` (tactile press), `.safe-top/.safe-bottom/.safe-x` (notch), `.hud-shadow`, `.premium-chat`. Online count floored at **≥50**. Match stays alive on exit (minimize/resume + ResumeGameBar). **No fake/periodic notifications** (removed as spam).
- **Capacitor Android APK** builds (Myket maven mirror at root `https://maven.myket.ir`; init script pins buildTools 36 + JDK17). See `ANDROID.md`.
- **CI/CD** (Gitea Actions + Nexus mirror) + Docker stack. See `DEPLOY.md`.
---
## 4. CI/CD + mirrors (Iran constraints)
- `.gitea/workflows/ci-cd.yml`: **api-build** (dotnet build slnx + Hokm.Sim), **web-check** (tsc + next build), **deploy** (self-hosted; pg_dump backup → rollback tag → build → stop+rm+up `--no-deps` → health-wait → prune). Set the **`ENV_FILE`** repo secret (see `deploy/ENV_FILE.example`).
- **NuGet/npm go through the Nexus mirror over PLAIN HTTP** `http://171.22.25.73:8081/repository/{nuget,npm}-group/` — the HTTPS mirror serves a **partial cert chain** that container trust stores reject (NU1301 PartialChain / npm UNABLE_TO_GET_ISSUER). npm also uses `--strict-ssl=false`; NuGet HTTP source needs `allowInsecureConnections="true"`. Local Windows dev + Docker base-image pulls work over HTTPS (Windows trust store has the intermediate) — only in-container package feeds use HTTP.
- **Fonts are self-hosted** (`@fontsource-variable/vazirmatn` + `plus-jakarta-sans`) — `next/font/google` fetches Google at build time and FAILS on the Iran runner. Do not reintroduce `next/font/google`.
- Memory: localhost can be VPN-hijacked (EonVPN) → reach local services via LAN IP if needed.
---
## 5. TODO / next
1. **Generate real EF migrations** (`dotnet ef migrations add Init`, DesignTimeDbContextFactory targets Postgres) + point at live **Supabase**; today the server uses `EnsureCreated()` (auto-switches to `Migrate()` once migrations exist).
2. **Deeper game-table UNO restyle** (bigger tactile cards, clearer turn/HUD, punchier win/trick feedback) — the last UI surface not yet refreshed.
3. Store **IAB** token verification (Cafe Bazaar Poolakey / Myket) — `/api/coins/iab/verify` is a stub.
4. Iranian **push** provider for closed-app notifications (FCM/APNs blocked); in-app + real-time notifications already work.
5. Optional: colored-chat visibility to OTHER players (needs sender-plan on chat messages); route daily-reward through the celebration overlay.
---
## 6. Working notes / gotchas
- **Can't use the headless preview** to verify visuals (it pauses animations/screenshots) — verify via builds + ask the user for screenshots. UI changes have been shipped "by the numbers".
- Server binds **0.0.0.0** in Docker via `ENTRYPOINT … --urls http://0.0.0.0:5005` (appsettings `Urls=localhost` wins over env, so command-line args are used).
- After schema changes in SQLite dev with `EnsureCreated()`, delete `server/src/Hokm.Server/hokm.db*` to recreate.
- Background `dotnet run &` from a Git-Bash shell dies when the shell exits; use a tracked background runner.
- Commit messages end with `Co-Authored-By: Claude …`. Both `messages/`-style i18n strings live in `src/lib/i18n.tsx` (fa+en).