Add Soroush CI/CD (Gitea + Nexus) + self-host fonts for offline build
Pipeline (.gitea/workflows/ci-cd.yml), all images/packages via Nexus mirror: - CI api-build: dotnet restore/build server/Hokm.slnx + run Hokm.Sim (rules). - CI web-check: npm install + tsc --noEmit + next build (static export). - deploy (self-hosted): pre-deploy pg_dump backup, rollback image tag, build, bring up db -> server -> web with stop+rm+up --no-deps (no force-recreate, no bare compose down), health-wait each, prune. Local stack (docker-compose.yml), ports in 1500-1600 so it coexists with manual dev on 3000/5005: web :1500 (nginx static) -> server :1505 (.NET) -> db :1510 (postgres, named volume + backups). Dockerfiles: server (.NET, NuGet via nuget.docker.config, binds 0.0.0.0, busybox wget healthcheck) + web (Next static export -> nginx, NEXT_PUBLIC_* baked as build args). nginx.conf SPA fallback. Config: server CORS is now config-driven (Cors__Origins) so the deployed web origin is allowed without code edits. deploy/ENV_FILE.example documents the Gitea ENV_FILE secret; DEPLOY.md covers setup/run/LAN-IP/rollback/migrations. Fonts: switch Vazirmatn + Plus Jakarta Sans from next/font/google (build-time Google fetch -> fails on the Iran CI runner) to self-hosted @fontsource-variable packages. Build is offline and ~3x faster; 7 woff2 emitted into out/. Verified locally: dotnet build slnx + Hokm.Sim (300 matches, exit 0); tsc clean; next build clean with self-hosted fonts. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||||
@@ -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'
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<configuration>
|
||||||
|
<packageSources>
|
||||||
|
<clear />
|
||||||
|
<add key="nexus" value="https://mirror.soroushasadi.com/repository/nuget-group/index.json" protocolVersion="3" />
|
||||||
|
</packageSources>
|
||||||
|
<config>
|
||||||
|
<add key="http_retry_count" value="8" />
|
||||||
|
<add key="http_retry_delay_milliseconds" value="1000" />
|
||||||
|
</config>
|
||||||
|
</configuration>
|
||||||
|
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
|
||||||
@@ -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=<pw>"
|
||||||
|
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.)
|
||||||
+22
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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:
|
||||||
+18
@@ -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";
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+20
@@ -10,6 +10,8 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@capacitor/app": "^8.1.0",
|
"@capacitor/app": "^8.1.0",
|
||||||
"@capacitor/core": "^8.4.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",
|
"@microsoft/signalr": "^10.0.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"framer-motion": "^12.40.0",
|
"framer-motion": "^12.40.0",
|
||||||
@@ -537,6 +539,24 @@
|
|||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"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": {
|
"node_modules/@humanfs/core": {
|
||||||
"version": "0.19.2",
|
"version": "0.19.2",
|
||||||
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
|
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
|
||||||
|
|||||||
@@ -14,6 +14,8 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@capacitor/app": "^8.1.0",
|
"@capacitor/app": "^8.1.0",
|
||||||
"@capacitor/core": "^8.4.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",
|
"@microsoft/signalr": "^10.0.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"framer-motion": "^12.40.0",
|
"framer-motion": "^12.40.0",
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
**/bin
|
||||||
|
**/obj
|
||||||
|
*.db
|
||||||
|
*.db-shm
|
||||||
|
*.db-wal
|
||||||
|
.git
|
||||||
|
*.user
|
||||||
@@ -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"]
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- NuGet source for Docker image builds: Soroush Nexus group only, with retries
|
||||||
|
(the proxy can be slow on a cold cache). Used by server/Dockerfile. -->
|
||||||
|
<configuration>
|
||||||
|
<packageSources>
|
||||||
|
<clear />
|
||||||
|
<add key="nexus"
|
||||||
|
value="https://mirror.soroushasadi.com/repository/nuget-group/index.json"
|
||||||
|
protocolVersion="3" />
|
||||||
|
</packageSources>
|
||||||
|
<config>
|
||||||
|
<add key="http_retry_count" value="8" />
|
||||||
|
<add key="http_retry_delay_milliseconds" value="1000" />
|
||||||
|
</config>
|
||||||
|
</configuration>
|
||||||
@@ -81,10 +81,19 @@ builder.Services
|
|||||||
builder.Services.AddAuthorization();
|
builder.Services.AddAuthorization();
|
||||||
|
|
||||||
// --- CORS for the Next.js client ---
|
// --- CORS for the Next.js client ---
|
||||||
builder.Services.AddCors(o => o.AddDefaultPolicy(p => p
|
// Origins are config-driven (Cors:Origins, comma/semicolon/space separated) so a
|
||||||
.WithOrigins(
|
// 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://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")
|
"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(corsOrigins)
|
||||||
.AllowAnyHeader()
|
.AllowAnyHeader()
|
||||||
.AllowAnyMethod()
|
.AllowAnyMethod()
|
||||||
.AllowCredentials()));
|
.AllowCredentials()));
|
||||||
|
|||||||
@@ -39,6 +39,12 @@
|
|||||||
--font-sans: var(--font-vazir), var(--font-jakarta), system-ui, sans-serif;
|
--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,
|
html,
|
||||||
body {
|
body {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|||||||
+4
-18
@@ -1,20 +1,10 @@
|
|||||||
import type { Metadata, Viewport } from "next";
|
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 "./globals.css";
|
||||||
import { I18nProvider } from "@/lib/i18n";
|
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 = {
|
export const metadata: Metadata = {
|
||||||
title: "برگ وسط | Barg-e Vasat — بازی حکم آنلاین",
|
title: "برگ وسط | Barg-e Vasat — بازی حکم آنلاین",
|
||||||
description: "برگ وسط — بازی حکم آنلاین ایرانی با حریفهای واقعی و هوشمند (Barg-e Vasat — online Persian Hokm)",
|
description: "برگ وسط — بازی حکم آنلاین ایرانی با حریفهای واقعی و هوشمند (Barg-e Vasat — online Persian Hokm)",
|
||||||
@@ -34,11 +24,7 @@ export default function RootLayout({
|
|||||||
children,
|
children,
|
||||||
}: Readonly<{ children: React.ReactNode }>) {
|
}: Readonly<{ children: React.ReactNode }>) {
|
||||||
return (
|
return (
|
||||||
<html
|
<html lang="fa" dir="rtl" className="h-full antialiased">
|
||||||
lang="fa"
|
|
||||||
dir="rtl"
|
|
||||||
className={`${vazir.variable} ${jakarta.variable} h-full antialiased`}
|
|
||||||
>
|
|
||||||
<body className="min-h-full">
|
<body className="min-h-full">
|
||||||
<I18nProvider>{children}</I18nProvider>
|
<I18nProvider>{children}</I18nProvider>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
Reference in New Issue
Block a user