diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..a011f4b
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,15 @@
+node_modules
+.next
+out
+android
+server
+.git
+.gitea
+*.md
+.env*
+!.env.example
+Dockerfile
+.dockerignore
+docker-compose*.yml
+npm-debug.log*
+tsconfig.tsbuildinfo
diff --git a/.gitea/workflows/ci-cd.yml b/.gitea/workflows/ci-cd.yml
new file mode 100644
index 0000000..be893d5
--- /dev/null
+++ b/.gitea/workflows/ci-cd.yml
@@ -0,0 +1,185 @@
+name: CI/CD
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+ branches: [main]
+
+concurrency:
+ group: hokm-cicd-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ # ---------------------------------------------------------------- API (.NET)
+ api-build:
+ name: "CI - API (dotnet build + engine sim)"
+ runs-on: ubuntu-latest
+ container:
+ image: mirror.soroushasadi.com/dotnet/sdk:10.0
+ options: --add-host=gitea:host-gateway
+ steps:
+ - name: Checkout
+ env:
+ TOKEN: ${{ github.token }}
+ REF: ${{ github.ref }}
+ run: |
+ git init -q
+ git remote add origin "${{ github.server_url }}/${{ github.repository }}.git"
+ git config http.extraheader "Authorization: Bearer ${TOKEN}"
+ git fetch --depth=1 origin "${REF}"
+ git checkout -q FETCH_HEAD
+
+ - name: Write NuGet config
+ run: |
+ cat > /tmp/nuget.ci.config << 'EOF'
+
+
+
+
+
+
+
+
+
+
+
+ EOF
+
+ - name: Restore
+ run: dotnet restore server/Hokm.slnx --configfile /tmp/nuget.ci.config
+ env:
+ DOTNET_CLI_TELEMETRY_OPTOUT: 1
+ DOTNET_NOLOGO: 1
+
+ - name: Build
+ run: dotnet build server/Hokm.slnx --no-restore -c Release
+
+ - name: Engine simulation (rules validation)
+ run: dotnet run --project server/tools/Hokm.Sim/Hokm.Sim.csproj -c Release --no-build
+
+ # ----------------------------------------------------------- Web (Next.js)
+ web-check:
+ name: "CI - Web (tsc + next build)"
+ runs-on: ubuntu-latest
+ container:
+ image: mirror.soroushasadi.com/node:20-alpine
+ options: --add-host=gitea:host-gateway
+ steps:
+ - name: Checkout (tarball)
+ env:
+ TOKEN: ${{ github.token }}
+ SHA: ${{ github.sha }}
+ run: |
+ wget -q --header "Authorization: Bearer ${TOKEN}" \
+ "${{ github.server_url }}/api/v1/repos/${{ github.repository }}/archive/${SHA}.tar.gz" \
+ -O /tmp/repo.tar.gz
+ tar -xzf /tmp/repo.tar.gz --strip-components=1
+
+ - name: Install
+ run: npm install --legacy-peer-deps --ignore-scripts --registry https://mirror.soroushasadi.com/repository/npm-group/
+
+ - name: TypeScript check
+ run: npx tsc --noEmit
+
+ - name: Build (static export)
+ run: npm run build
+
+ # -------------------------------------------------------------- Deploy
+ deploy:
+ name: "Deploy - local stack (db + server + web)"
+ runs-on: self-hosted
+ needs: [api-build, web-check]
+ if: github.event_name == 'push' && github.ref == 'refs/heads/main'
+ timeout-minutes: 40
+ env:
+ # act host runner starts with a minimal PATH — extend so docker is found.
+ PATH: /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin
+ steps:
+ - name: Checkout
+ env:
+ TOKEN: ${{ github.token }}
+ REF: ${{ github.ref }}
+ run: |
+ git init -q
+ git remote add origin "${{ github.server_url }}/${{ github.repository }}.git"
+ git config http.extraheader "Authorization: Bearer ${TOKEN}"
+ git fetch --depth=1 origin "${REF}"
+ git checkout -q FETCH_HEAD
+
+ - name: Write .env
+ run: printf '%s' "$ENV_FILE" > .env
+ env:
+ ENV_FILE: ${{ secrets.ENV_FILE }}
+
+ - name: Backup database (if running)
+ run: |
+ mkdir -p /opt/hokm-backups
+ if docker ps --format '{{.Names}}' | grep -q '^hokm-db$'; then
+ TS=$(date +%Y%m%d-%H%M%S)
+ docker exec hokm-db pg_dump -U hokm hokm > "/opt/hokm-backups/hokm-${TS}.sql" \
+ && echo "backed up to /opt/hokm-backups/hokm-${TS}.sql" \
+ || echo "WARN: pg_dump failed (continuing)"
+ else
+ echo "no hokm-db container yet — first deploy, nothing to back up"
+ fi
+
+ - name: Tag rollback image
+ run: |
+ CURRENT=$(docker inspect hokm-server --format='{{.Config.Image}}' 2>/dev/null || echo "")
+ if [ -n "$CURRENT" ]; then docker tag "$CURRENT" hokm-server:rollback && echo "rollback tag = $CURRENT"; fi
+
+ - name: Build images
+ run: docker compose build --parallel server web
+ env:
+ DOCKER_BUILDKIT: 1
+ COMPOSE_DOCKER_CLI_BUILD: 1
+
+ - name: Start database
+ run: docker compose up -d --no-deps db
+
+ - name: Wait for database healthy
+ run: |
+ for i in $(seq 1 20); do
+ S=$(docker inspect --format='{{.State.Health.Status}}' hokm-db 2>/dev/null || echo missing)
+ echo " [$i/20] db: $S"
+ [ "$S" = "healthy" ] && break
+ [ "$i" = "20" ] && { echo "TIMEOUT db"; docker logs --tail=40 hokm-db; exit 1; }
+ sleep 3
+ done
+
+ - name: Deploy server (stop + rm + up, no force-recreate)
+ run: |
+ docker stop hokm-server 2>/dev/null || true
+ docker rm hokm-server 2>/dev/null || true
+ docker compose up -d --no-deps server
+
+ - name: Wait for server healthy
+ run: |
+ for i in $(seq 1 24); do
+ S=$(docker inspect --format='{{.State.Health.Status}}' hokm-server 2>/dev/null || echo missing)
+ echo " [$i/24] server: $S"
+ [ "$S" = "healthy" ] && { echo "OK hokm-server healthy"; break; }
+ [ "$i" = "24" ] && { echo "TIMEOUT hokm-server"; docker compose logs --tail=60 server; exit 1; }
+ sleep 5
+ done
+
+ - name: Deploy web (stop + rm + up, no force-recreate)
+ run: |
+ docker stop hokm-web 2>/dev/null || true
+ docker rm hokm-web 2>/dev/null || true
+ docker compose up -d --no-deps web
+
+ - name: Wait for web healthy
+ run: |
+ for i in $(seq 1 18); do
+ S=$(docker inspect --format='{{.State.Health.Status}}' hokm-web 2>/dev/null || echo missing)
+ echo " [$i/18] web: $S"
+ [ "$S" = "healthy" ] && { echo "OK hokm-web healthy"; break; }
+ [ "$i" = "18" ] && { echo "TIMEOUT hokm-web"; docker compose logs --tail=40 web; exit 1; }
+ sleep 5
+ done
+
+ - name: Prune dangling images
+ if: success()
+ run: docker image prune -f
diff --git a/DEPLOY.md b/DEPLOY.md
new file mode 100644
index 0000000..ddf3f71
--- /dev/null
+++ b/DEPLOY.md
@@ -0,0 +1,85 @@
+# Deploy — Barg-e Vasat (Soroush CI/CD)
+
+CI/CD runs on **Gitea Actions** (`git.soroushasadi.com`) with all packages and
+base images pulled through the **Nexus mirror** (`mirror.soroushasadi.com`).
+Pushing to `main` triggers build → deploy.
+
+## Topology
+
+| Service | Image | Container | Host port | Notes |
+|---|---|---|---|---|
+| Postgres | `postgres:16-alpine` | `hokm-db` | `1510` | named volume `hokm_db_data` |
+| API (.NET SignalR) | `hokm-server:latest` | `hokm-server` | `1505` → 5005 | EF Core → Postgres |
+| Web (static Next → nginx) | `hokm-web:latest` | `hokm-web` | `1500` → 80 | `NEXT_PUBLIC_*` baked at build |
+
+Ports are in **1500–1600** on purpose, so the deployed stack runs alongside a
+manual `npm run dev` (:3000) and `dotnet run` (:5005) without colliding.
+
+## Pipeline (`.gitea/workflows/ci-cd.yml`)
+
+1. **CI – API**: restore (Nexus NuGet) → `dotnet build server/Hokm.slnx` → run `Hokm.Sim` (engine rules validation).
+2. **CI – Web**: `npm install` (Nexus npm) → `tsc --noEmit` → `next build` (static export).
+3. **Deploy** (`self-hosted`, push to `main` only): backup DB → tag rollback image → build images → bring up `db` (wait healthy) → `server` (stop+rm+up, wait healthy) → `web` (stop+rm+up, wait healthy) → prune.
+
+Deploy follows the safety rules: pre-deploy `pg_dump` backup to `/opt/hokm-backups`,
+rollback tag before replace, explicit `stop + rm + up --no-deps` (no
+`--force-recreate`, no bare `docker compose down`).
+
+## One-time setup
+
+1. **Secret**: fill `deploy/ENV_FILE.example` and paste into the Gitea repo secret
+ `ENV_FILE` at `.../HokmPlay/settings/secrets`. At minimum set `JWT_KEY`
+ (`openssl rand -hex 32`) and `POSTGRES_PASSWORD`.
+2. **Runner**: an `act_runner` registered with both labels
+ (`ubuntu-latest:docker://...` for CI, `self-hosted:host` for deploy) — reused
+ from existing Soroush projects.
+3. **Push**: `git push origin main` → watch `.../HokmPlay/actions`.
+
+## Reaching the stack
+
+- Same machine as the deploy host: open `http://localhost:1500`.
+- Different machine (browser elsewhere): set `NEXT_PUBLIC_SERVER_URL` and
+ `CORS_ORIGINS` in `ENV_FILE` to the host **LAN IP** (e.g.
+ `http://172.28.144.1:1505` / `http://172.28.144.1:1500`) and push again —
+ the API URL is baked into the web bundle at build time. (localhost can be
+ hijacked by the VPN; prefer the LAN IP.)
+
+## Local test (no Gitea, on your machine)
+
+```bash
+cd D:\Projects\hokm
+copy deploy\ENV_FILE.example .env # then edit JWT_KEY / POSTGRES_PASSWORD
+docker compose build server web
+docker compose up -d
+# web → http://localhost:1500 api → http://localhost:1505/
+docker compose logs -f server
+```
+
+Tear down (keeps the DB volume):
+```bash
+docker compose stop
+```
+
+## Migrations
+
+The server auto-applies EF migrations when any exist, else `EnsureCreated()`
+(current state — no migration classes yet, so the Postgres schema is created on
+first boot). When you generate them:
+
+```bash
+cd server/src/Hokm.Server
+$env:HOKM_DESIGN_CONN="Host=localhost;Port=1510;Database=hokm;Username=hokm;Password="
+dotnet ef migrations add Init
+```
+
+Then the next deploy runs `Database.Migrate()` automatically.
+
+## Rollback
+
+```bash
+docker stop hokm-server && docker rm hokm-server
+docker run -d --name hokm-server --network hokm_default -p 1505:5005 \
+ --env-file <(grep -E '^(JWT_|Database__|ConnectionStrings__|Cors__|Zarinpal__)' .env) \
+ hokm-server:rollback
+```
+(or just revert the commit and push — CI redeploys the previous code.)
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..f2db910
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,22 @@
+# Barg-e Vasat web (Next.js 16 static export → nginx)
+# The app is output:"export" (fully client-side), so we build the static `out/`
+# and serve it with nginx. NEXT_PUBLIC_* are baked at build time.
+FROM mirror.soroushasadi.com/node:20-alpine AS build
+WORKDIR /app
+COPY package*.json ./
+RUN npm install --legacy-peer-deps --ignore-scripts \
+ --registry https://mirror.soroushasadi.com/repository/npm-group/
+COPY . .
+# Live mode + the API origin the BROWSER will use (host-mapped port / LAN IP).
+ARG NEXT_PUBLIC_USE_SERVER=1
+ARG NEXT_PUBLIC_SERVER_URL=http://localhost:1505
+ENV NEXT_PUBLIC_USE_SERVER=$NEXT_PUBLIC_USE_SERVER
+ENV NEXT_PUBLIC_SERVER_URL=$NEXT_PUBLIC_SERVER_URL
+RUN npm run build
+
+FROM mirror.soroushasadi.com/nginx:alpine
+COPY --from=build /app/out /usr/share/nginx/html
+COPY nginx.conf /etc/nginx/conf.d/default.conf
+EXPOSE 80
+HEALTHCHECK --interval=10s --timeout=5s --retries=6 --start-period=10s \
+ CMD wget -q -O- http://127.0.0.1/ || exit 1
diff --git a/deploy/ENV_FILE.example b/deploy/ENV_FILE.example
new file mode 100644
index 0000000..ea5b9ae
--- /dev/null
+++ b/deploy/ENV_FILE.example
@@ -0,0 +1,36 @@
+# ──────────────────────────────────────────────────────────────────────────
+# Barg-e Vasat — ENV_FILE
+# Paste the contents of this file (filled in) into the Gitea repo secret:
+# https://git.soroushasadi.com/soroushdes/HokmPlay/settings/secrets → ENV_FILE
+# The deploy job writes it verbatim to `.env`, which docker compose reads.
+#
+# NOTE: NEXT_PUBLIC_SERVER_URL is baked into the web bundle at BUILD time —
+# changing it requires a new CI run (push a commit) to take effect.
+# ──────────────────────────────────────────────────────────────────────────
+
+# Host ports (1500–1600 range so the stack coexists with manual dev on 3000/5005)
+WEB_PORT=1500
+API_PORT=1505
+DB_PORT=1510
+
+# Database (postgres container)
+POSTGRES_PASSWORD=change-me-strong-password
+
+# JWT — generate with: openssl rand -hex 32
+JWT_KEY=CHANGE-ME-to-a-32+char-random-secret
+JWT_ISSUER=hokm
+JWT_AUDIENCE=hokm-clients
+
+# Browser-facing API origin (host-mapped api port).
+# If the browser is NOT on the deploy host, use the host LAN IP instead of
+# localhost, e.g. http://172.28.144.1:1505 (localhost can be VPN-hijacked).
+NEXT_PUBLIC_SERVER_URL=http://localhost:1505
+
+# Origins allowed by the API's CORS (comma-separated). Must include the web URL.
+CORS_ORIGINS=http://localhost:1500
+
+# ZarinPal (sandbox for now — switch in admin/panel later)
+ZARINPAL_MERCHANT_ID=299685fb-cadf-4dfc-98e2-d4af5d81528d
+ZARINPAL_SANDBOX=true
+ZARINPAL_CALLBACK_URL=http://localhost:1505/api/coins/pay/callback
+ZARINPAL_CLIENT_RETURN_URL=http://localhost:1500
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..bf14e49
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,86 @@
+# Barg-e Vasat — local/self-hosted stack.
+# Ports live in the 1500–1600 range so this stack can run alongside a manual
+# `npm run dev` (:3000) and `dotnet run` (:5005) without colliding.
+# web → http://localhost:1500
+# api → http://localhost:1505
+# db → localhost:1510 (postgres)
+# All values come from .env (the deploy job writes it from the ENV_FILE secret).
+
+services:
+ db:
+ image: mirror.soroushasadi.com/postgres:16-alpine
+ container_name: hokm-db
+ restart: unless-stopped
+ environment:
+ POSTGRES_DB: hokm
+ POSTGRES_USER: hokm
+ POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-hokm_dev_pass}
+ volumes:
+ - hokm_db_data:/var/lib/postgresql/data
+ ports:
+ - "${DB_PORT:-1510}:5432"
+ healthcheck:
+ test: ["CMD-SHELL", "pg_isready -U hokm -d hokm"]
+ interval: 5s
+ timeout: 5s
+ retries: 10
+
+ server:
+ build:
+ context: ./server
+ dockerfile: Dockerfile
+ image: hokm-server:latest
+ container_name: hokm-server
+ restart: unless-stopped
+ depends_on:
+ db:
+ condition: service_healthy
+ environment:
+ ASPNETCORE_ENVIRONMENT: Production
+ ASPNETCORE_URLS: http://0.0.0.0:5005
+ Database__Provider: postgres
+ ConnectionStrings__Default: "Host=db;Port=5432;Database=hokm;Username=hokm;Password=${POSTGRES_PASSWORD:-hokm_dev_pass}"
+ Jwt__Key: ${JWT_KEY:?set JWT_KEY in .env}
+ Jwt__Issuer: ${JWT_ISSUER:-hokm}
+ Jwt__Audience: ${JWT_AUDIENCE:-hokm-clients}
+ # Comma-separated origins the browser uses to reach the web app.
+ Cors__Origins: ${CORS_ORIGINS:-http://localhost:1500}
+ Zarinpal__MerchantId: ${ZARINPAL_MERCHANT_ID:-299685fb-cadf-4dfc-98e2-d4af5d81528d}
+ Zarinpal__Sandbox: ${ZARINPAL_SANDBOX:-true}
+ Zarinpal__CallbackUrl: ${ZARINPAL_CALLBACK_URL:-http://localhost:1505/api/coins/pay/callback}
+ Zarinpal__ClientReturnUrl: ${ZARINPAL_CLIENT_RETURN_URL:-http://localhost:1500}
+ ports:
+ - "${API_PORT:-1505}:5005"
+ healthcheck:
+ test: ["CMD", "wget", "-q", "-O-", "http://127.0.0.1:5005/"]
+ interval: 10s
+ timeout: 5s
+ retries: 12
+ start_period: 20s
+
+ web:
+ build:
+ context: .
+ dockerfile: Dockerfile
+ args:
+ # Baked into the static bundle at build time. Must be the address the
+ # BROWSER uses to reach the API (host-mapped api port, or LAN IP).
+ NEXT_PUBLIC_USE_SERVER: "1"
+ NEXT_PUBLIC_SERVER_URL: ${NEXT_PUBLIC_SERVER_URL:-http://localhost:1505}
+ image: hokm-web:latest
+ container_name: hokm-web
+ restart: unless-stopped
+ depends_on:
+ server:
+ condition: service_healthy
+ ports:
+ - "${WEB_PORT:-1500}:80"
+ healthcheck:
+ test: ["CMD", "wget", "-q", "-O-", "http://127.0.0.1/"]
+ interval: 10s
+ timeout: 5s
+ retries: 6
+ start_period: 10s
+
+volumes:
+ hokm_db_data:
diff --git a/nginx.conf b/nginx.conf
new file mode 100644
index 0000000..12811dc
--- /dev/null
+++ b/nginx.conf
@@ -0,0 +1,18 @@
+server {
+ listen 80;
+ server_name _;
+ root /usr/share/nginx/html;
+ index index.html;
+
+ # Static Next.js export: serve the file, its .html twin, or fall back to the
+ # SPA shell (the app uses client-side hash routing).
+ location / {
+ try_files $uri $uri.html $uri/ /index.html;
+ }
+
+ # Long-cache immutable build assets.
+ location /_next/static/ {
+ expires 1y;
+ add_header Cache-Control "public, immutable";
+ }
+}
diff --git a/package-lock.json b/package-lock.json
index 9f8d8f7..b1a0513 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,6 +10,8 @@
"dependencies": {
"@capacitor/app": "^8.1.0",
"@capacitor/core": "^8.4.0",
+ "@fontsource-variable/plus-jakarta-sans": "^5.2.8",
+ "@fontsource-variable/vazirmatn": "^5.2.8",
"@microsoft/signalr": "^10.0.0",
"clsx": "^2.1.1",
"framer-motion": "^12.40.0",
@@ -537,6 +539,24 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
+ "node_modules/@fontsource-variable/plus-jakarta-sans": {
+ "version": "5.2.8",
+ "resolved": "https://registry.npmjs.org/@fontsource-variable/plus-jakarta-sans/-/plus-jakarta-sans-5.2.8.tgz",
+ "integrity": "sha512-iQecBizIdZxezODNHzOn4SvvRMrZL/S8k4MEXGDynCmUrImVW0VmX+tIAMqnADwH4haXlHSXqMgU6+kcfBQJdw==",
+ "license": "OFL-1.1",
+ "funding": {
+ "url": "https://github.com/sponsors/ayuhito"
+ }
+ },
+ "node_modules/@fontsource-variable/vazirmatn": {
+ "version": "5.2.8",
+ "resolved": "https://registry.npmjs.org/@fontsource-variable/vazirmatn/-/vazirmatn-5.2.8.tgz",
+ "integrity": "sha512-2YzXfH4PNOeoZsBsgJgjbnm+IC2nonGyMLX3gXLm8FQrP+wpi1uGBIWYWlEzJ0lmntgdyAc3lRViHHfZWKvbog==",
+ "license": "OFL-1.1",
+ "funding": {
+ "url": "https://github.com/sponsors/ayuhito"
+ }
+ },
"node_modules/@humanfs/core": {
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
diff --git a/package.json b/package.json
index 482d960..2b9087e 100644
--- a/package.json
+++ b/package.json
@@ -14,6 +14,8 @@
"dependencies": {
"@capacitor/app": "^8.1.0",
"@capacitor/core": "^8.4.0",
+ "@fontsource-variable/plus-jakarta-sans": "^5.2.8",
+ "@fontsource-variable/vazirmatn": "^5.2.8",
"@microsoft/signalr": "^10.0.0",
"clsx": "^2.1.1",
"framer-motion": "^12.40.0",
diff --git a/server/.dockerignore b/server/.dockerignore
new file mode 100644
index 0000000..f8ded3f
--- /dev/null
+++ b/server/.dockerignore
@@ -0,0 +1,7 @@
+**/bin
+**/obj
+*.db
+*.db-shm
+*.db-wal
+.git
+*.user
diff --git a/server/Dockerfile b/server/Dockerfile
new file mode 100644
index 0000000..404e4c8
--- /dev/null
+++ b/server/Dockerfile
@@ -0,0 +1,21 @@
+# Hokm.Server (.NET 10 ASP.NET Core + SignalR)
+# Build context = ./server (so Hokm.Engine + Hokm.Server are both in scope)
+FROM mirror.soroushasadi.com/dotnet/sdk:10.0 AS build
+WORKDIR /src
+COPY nuget.docker.config /tmp/nuget.config
+COPY Directory.Build.props ./
+COPY src/ ./src/
+RUN dotnet restore src/Hokm.Server/Hokm.Server.csproj --configfile /tmp/nuget.config
+RUN dotnet publish src/Hokm.Server/Hokm.Server.csproj -c Release -o /out --no-restore
+
+FROM mirror.soroushasadi.com/dotnet/aspnet:10.0
+WORKDIR /app
+# aspnet image ships no wget/curl — borrow busybox so the healthcheck has wget.
+COPY --from=mirror.soroushasadi.com/busybox:1.36 /bin/busybox /usr/bin/wget
+COPY --from=build /out ./
+# Bind all interfaces (appsettings binds localhost only, unreachable across the port map).
+ENV ASPNETCORE_URLS=http://0.0.0.0:5005
+EXPOSE 5005
+HEALTHCHECK --interval=10s --timeout=5s --retries=12 --start-period=20s \
+ CMD wget -q -O- http://127.0.0.1:5005/ || exit 1
+ENTRYPOINT ["dotnet", "Hokm.Server.dll"]
diff --git a/server/nuget.docker.config b/server/nuget.docker.config
new file mode 100644
index 0000000..23e5834
--- /dev/null
+++ b/server/nuget.docker.config
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/server/src/Hokm.Server/Program.cs b/server/src/Hokm.Server/Program.cs
index b8b7ba8..8d2dae6 100644
--- a/server/src/Hokm.Server/Program.cs
+++ b/server/src/Hokm.Server/Program.cs
@@ -81,10 +81,19 @@ builder.Services
builder.Services.AddAuthorization();
// --- CORS for the Next.js client ---
+// Origins are config-driven (Cors:Origins, comma/semicolon/space separated) so a
+// deployed web origin can be allowed via env (Cors__Origins) without a code change.
+// Falls back to the local dev origins when unset.
+var corsRaw = builder.Configuration["Cors:Origins"];
+var corsOrigins = string.IsNullOrWhiteSpace(corsRaw)
+ ? new[]
+ {
+ "http://localhost:3000", "http://localhost:3002", "http://localhost:3020",
+ "http://127.0.0.1:3000", "http://127.0.0.1:3002", "http://127.0.0.1:3020",
+ }
+ : corsRaw.Split(new[] { ',', ';', ' ' }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
builder.Services.AddCors(o => o.AddDefaultPolicy(p => p
- .WithOrigins(
- "http://localhost:3000", "http://localhost:3002", "http://localhost:3020",
- "http://127.0.0.1:3000", "http://127.0.0.1:3002", "http://127.0.0.1:3020")
+ .WithOrigins(corsOrigins)
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials()));
diff --git a/src/app/globals.css b/src/app/globals.css
index c916049..2294349 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -39,6 +39,12 @@
--font-sans: var(--font-vazir), var(--font-jakarta), system-ui, sans-serif;
}
+:root {
+ /* Self-hosted @fontsource families (see app/layout.tsx imports). */
+ --font-vazir: "Vazirmatn Variable", system-ui, sans-serif;
+ --font-jakarta: "Plus Jakarta Sans Variable", system-ui, sans-serif;
+}
+
html,
body {
height: 100%;
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index e9edf9e..9b8d42d 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -1,20 +1,10 @@
import type { Metadata, Viewport } from "next";
-import { Vazirmatn, Plus_Jakarta_Sans } from "next/font/google";
+// Self-hosted fonts (no Google fetch at build time → CI builds work offline / in Iran).
+import "@fontsource-variable/vazirmatn";
+import "@fontsource-variable/plus-jakarta-sans";
import "./globals.css";
import { I18nProvider } from "@/lib/i18n";
-const vazir = Vazirmatn({
- variable: "--font-vazir",
- subsets: ["arabic", "latin"],
- display: "swap",
-});
-
-const jakarta = Plus_Jakarta_Sans({
- variable: "--font-jakarta",
- subsets: ["latin"],
- display: "swap",
-});
-
export const metadata: Metadata = {
title: "برگ وسط | Barg-e Vasat — بازی حکم آنلاین",
description: "برگ وسط — بازی حکم آنلاین ایرانی با حریفهای واقعی و هوشمند (Barg-e Vasat — online Persian Hokm)",
@@ -34,11 +24,7 @@ export default function RootLayout({
children,
}: Readonly<{ children: React.ReactNode }>) {
return (
-
+
{children}