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": {
|
||||
"@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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
|
||||
// --- 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()));
|
||||
|
||||
@@ -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%;
|
||||
|
||||
+4
-18
@@ -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 (
|
||||
<html
|
||||
lang="fa"
|
||||
dir="rtl"
|
||||
className={`${vazir.variable} ${jakarta.variable} h-full antialiased`}
|
||||
>
|
||||
<html lang="fa" dir="rtl" className="h-full antialiased">
|
||||
<body className="min-h-full">
|
||||
<I18nProvider>{children}</I18nProvider>
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user