# عملیات دیپلوی، پیکربندی، سلامت و بازیابی. برای شروعِ توسعه‌ی محلی `README.md` را ببینید. ## دیپلوی ```bash ssh -t administrator@78.38.35.249 \ 'cd ~/mathapp-v2 && ./deploy.sh; sudo systemctl restart zareian-mathapp' ``` دو بار رمز می‌پرسد — یک‌بار برای `ssh` و یک‌بار برای `sudo`. **منتظرِ هر پرامپت بمانید**؛ چسباندنِ چند خط با هم، متنِ صف‌شده را به پرامپتِ رمز می‌دهد و بعد از سه تلاش قفل می‌شوید. `deploy.sh` به ترتیب: fetch، برگرداندنِ درخت به `origin/`، نصبِ وابستگی‌ها، `migrate`، `seed_content`، `collectstatic`، ری‌استارت، و در پایان poll کردنِ `/api/health`. دو رفتارش را بدانید: - **سرور را تعمیر می‌کند، جلویش نمی‌ایستد.** فایلِ دستی‌ویرایش‌شده قبلاً `git pull --ff-only` را می‌شکست و چون بقیه‌ی مراحل رد می‌شدند، سرور بی‌صدا روی کدِ قدیمی می‌ماند. حالا تغییراتِ محلی در `.deploy-backups/` به‌صورتِ patch ذخیره و بعد با `git reset --hard` دور ریخته می‌شوند، و کار ادامه می‌یابد. هر اجرای کامل، سرور را دقیقاً روی همان چیزی می‌گذارد که در Gitea است. - **ری‌استارت را اثبات می‌کند.** زمانِ `ActiveEnterTimestamp` را قبل و بعد مقایسه می‌کند و اگر عوض نشده باشد با کدِ غیرِصفر خارج می‌شود. قبلاً فقط آدرسِ عمومی را چک می‌کرد و **پروسه‌ی قدیمی** همان `200` را می‌داد — سرور دو روز کدِ قدیمی سرو کرد در حالی که همه‌ی فایل‌ها روی دیسک تازه بودند. > **فایل روی دیسک، فایلِ دیپلوی‌شده نیست.** قبل از باور کردنِ هر چیزی: > `systemctl show zareian-mathapp -p ActiveEnterTimestamp` متغیرهای اختیاری: `SKIP_PULL=1`، `TADRISINO_SERVICE`، `TADRISINO_HEALTH_URL`، `PIP_INDEX_URL` (وقتی pypi.org تایم‌اوت می‌دهد). ## بررسیِ سلامت ```bash curl https://api.math.tadrisino2.ir/api/health ``` بدونِ احرازِ هویت و بدونِ SSH می‌گوید: دیتابیس سالم است یا نه، مهاجرتِ اعمال‌نشده مانده یا نه، کش کار می‌کند یا نه، چند پایه/درس/سوال روی آن سرور هست، کدام سرویسِ بیرونی تنظیم شده (فقط بله/خیر)، و چه هشدارهای امنیتی‌ای فعال‌اند. `200` سالم، `503` خرابیِ واقعی. **هیچ سکرتی برنمی‌گرداند.** مهم‌ترین بررسی‌اش «مهاجرتِ اعمال‌نشده» است: شایع‌ترین حالتِ دیپلویِ خراب این است که کد رفته ولی `migrate` اجرا نشده — اپ بالا می‌آید و بعد زیرِ بارِ واقعی با خطای ستونِ ناموجود می‌شکند. تأییدِ سریعِ اینکه API واقعاً جواب می‌دهد: ```bash curl -i https://api.math.tadrisino2.ir/api/schema/ curl -i -X POST https://api.math.tadrisino2.ir/api/auth/session \ -H 'Content-Type: application/json' \ -d '{"type":"guest","deviceId":"verify-1"}' ``` ## بکاپ و بازیابی `db.sqlite3` در گیت **نیست** و تنها نسخه‌ی داده‌ی کاربران است. `deploy.sh` پیش از `migrate` بکاپ نمی‌گیرد؛ تا وقتی این خودکار نشده، دستی بگیرید: ```bash cd /home/administrator/mathapp-v2 mkdir -p .deploy-backups venv/bin/python - <<'PY' import sqlite3, datetime dst = ".deploy-backups/db-%s.sqlite3" % datetime.datetime.now().strftime("%Y%m%d-%H%M%S") src, out = sqlite3.connect("db.sqlite3"), sqlite3.connect(dst) with out: src.backup(out) # نسخه‌ی سازگار حتی اگر هم‌زمان کسی بنویسد out.close(); src.close() print("backup:", dst, sqlite3.connect(dst).execute("PRAGMA integrity_check").fetchone()[0]) PY ``` `sqlite3` به‌عنوانِ فرمانِ خط فرمان روی آن سرور نصب **نیست** — از پایتونِ خودِ پروژه استفاده کنید، نه `cp`؛ کپیِ ساده وسطِ یک نوشتن، فایلِ نیمه‌کاره می‌دهد. بازیابی: سرویس را بخوابانید، فایل را برگردانید، دوباره بالا بیاورید. ```bash sudo systemctl stop zareian-mathapp cp .deploy-backups/db-.sqlite3 db.sqlite3 sudo systemctl start zareian-mathapp curl https://api.math.tadrisino2.ir/api/health ``` ## متغیرهای محیطی `.env` خودکار خوانده می‌شود (`python-dotenv`) و مقادیرش **با پیش‌فرض‌های کد ادغام می‌شوند، نه جایگزینشان**. پس یک `.env` ناقص روی سرور دیگر باعثِ بالا نیامدن نمی‌شود. `.env.example` توضیحِ کاملِ هر کدام را دارد. | متغیر | اگر ست نشود چه می‌شود | |---|---| | `DJANGO_DEBUG` | خاموش (`0`) — که حالتِ درستِ پروداکشن است | | `DJANGO_SECRET_KEY` | یک کلیدِ تصادفیِ قوی ساخته و در `.django_secret_key` (مجوز `0600`) ذخیره می‌شود | | `DJANGO_ALLOWED_HOSTS` | دامنه‌های خودمان پیش‌فرض‌اند | | `CSRF_TRUSTED_ORIGINS` · `CORS_ALLOWED_ORIGINS` · `PUBLIC_BASE_URL` | پیش‌فرضِ درست دارند | | `CACHE_URL` | سرویس بالا می‌آید ولی نرخ بینِ workerها مشترک نیست؛ `/api/health` هشدار می‌دهد | | `KAVENEGAR_API_KEY` | `POST /api/auth/otp/request` خطای صریحِ `503` می‌دهد، نه موفقیتِ ساختگی | | `ZARINPAL_MERCHANT_ID` | مسیرِ پرداخت fail-closed می‌ماند (`503`) | | `OPENAI_API_KEY` · `QDRANT_API_KEY` | `POST /api/ai/ask` همان پاسخِ stub را می‌دهد | | `TEST_LOGIN_PHONES` | هیچ حسابِ تستی باز نیست (بخشِ بعد) | > **`DJANGO_SECRET_KEY` توکن‌های ورود را امضا می‌کند.** بهترین حالت این است که > اصلاً ست نشود تا خودِ تنظیمات یکی بسازد — آن‌وقت کلیدی وجود ندارد که لو > برود. **هرگز نگذارید سرور روی fallbackِ `django-insecure-…` کار کند**: آن > رشته در همین ریپو کامیت شده، پس هر کسی که ریپو را بخواند می‌تواند به‌جای هر > کاربری توکن جعل کند. `manage.py check --deploy` با کدِ `security.W009` > علامتش می‌زند. > **`DJANGO_DEBUG` هرگز روی سرور `1` نشود.** `DEV_MASTER_OTP` (کد `0000`) را > روشن می‌کند که هر شماره‌ای را به‌عنوانِ ورودِ معتبر می‌پذیرد، و صفحه‌های خطا > کلِ تنظیمات را نشان می‌دهند. جایش فقط `.env` است، نه یونیتِ systemd. ## ورودِ توسعه‌دهنده‌ها (بدونِ سرویسِ پیامک) تا وقتی `KAVENEGAR_API_KEY` ست نشده، با `DEBUG=0` هیچ‌کس نمی‌تواند با شماره وارد شود. برای اینکه ورودِ تیم به یک نفرِ خاص وابسته نباشد، چند شماره‌ی تست با کدِ ثابت تعریف کنید: ```bash TEST_LOGIN_PHONES=09120000001:834512,09120000002:110947 ``` برای این شماره‌ها پیامکی ارسال نمی‌شود و همان کد همیشه معتبر است. هر شماره‌ی دیگری مسیرِ عادیِ OTP را می‌رود، بی‌هیچ تغییری. ```bash curl -X POST https://api.math.tadrisino2.ir/api/auth/otp/request \ -H 'Content-Type: application/json' -d '{"phone":"09120000001"}' curl -X POST https://api.math.tadrisino2.ir/api/auth/login \ -H 'Content-Type: application/json' \ -d '{"phone":"09120000001","otp":"834512"}' ``` این همان الگویی‌ست که Firebase Auth (با نامِ fictional phone numbers) و Apple (حسابِ دمو برای بررسیِ App Store) برای همین مسئله دارند. - **با `DEV_MASTER_OTP` یکی نیست.** آن کدِ `0000` **هر** شماره‌ای را می‌پذیرد و فقط با `DEBUG=1` روشن می‌شود. این فقط همین چند شماره را، هرکدام با کدِ خودش. - **کد در پاسخِ API برنمی‌گردد**، پس دانستنِ شماره برای ورود کافی نیست. سقفِ تلاشِ ورودِ هر شماره روی این‌ها هم اعمال می‌شود، پس حدسِ کد بسته است. - **مقدارِ نامعتبر سرویس را بالا نمی‌آورد** و پیامِ صریح می‌دهد — بی‌صدا رد نمی‌شود. کد باید ۴ تا ۶ رقم باشد (`LoginRequestSerializer` سقفِ ۶ کاراکتر دارد). - **`/api/health` تا وقتی پر باشد هشدار می‌دهد** — فقط تعداد، نه شماره و نه کد. - شماره را **دقیقاً** همان‌طور بنویسید که کلاینت می‌فرستد؛ این کدبیس شماره را normalize نمی‌کند، پس `09121111111` و `+989121111111` دو کاربرِ جدا هستند. - کدِ تصادفی بگذارید، نه `1234`، و پیش از باز شدنِ سرویس روی کاربرانِ واقعی خالی‌اش کنید. ### حسابِ مهمانِ تستِ فرانت مسیرِ مهمان (`POST /api/auth/session` با یک `deviceId`) اصلاً به پیامک کار ندارد و همیشه کار می‌کند — هر رشته‌ای به‌عنوانِ `deviceId` یک حسابِ تازه می‌سازد. از ۲۰۲۶-۰۹-۲۴ پاسخش `refreshToken` هم دارد (مثلِ ورود با شماره)، پس مهمان بعد از انقضای توکنِ یک‌ساعته با `POST /api/auth/refresh` ادامه می‌دهد. یک `deviceId` مشخص روی پروداکشن از قبل داده دارد: ```bash curl -X POST https://api.math.tadrisino2.ir/api/auth/session \ -H 'Content-Type: application/json' \ -d '{"type":"guest","deviceId":"frontend-test-device"}' ``` کاربرِ شماره‌ی ۶، «حساب تست فرانت». رویش ۲۴ ردیفِ `Notification` و ۲۰ ردیفِ `LeaderboardEntry` (هفتگی و کل، با ترتیبِ عمداً متفاوت) نشسته تا تیمِ فرانت بتواند آن صفحه‌ها را قبل از وجودِ ترافیکِ واقعی ببیند. ⚠️ **این داده دستی روی دیتابیسِ پروداکشن نوشته شده و در گیت نیست.** پیش از باز شدنِ سرویس روی کاربرانِ واقعی باید برود، وگرنه اعلان‌ها و لیدربردِ ساختگی را کاربرِ واقعی می‌بیند. `purge_demo_data` این‌ها را **نمی‌گیرد** — آن فرمان فقط دنبالِ پیشوندِ `[SEED]`/`[DEMO]` روی Grade/Plan/Character/User می‌گردد. پاک کردن یعنی حذفِ دستیِ ردیف‌های `Notification` و `LeaderboardEntry` از پنلِ ادمین (اول بکاپ، بخشِ بالا). ## سرور `api.math.tadrisino2.ir` روی یک **ماشینِ مشترکِ شرکت** اجرا می‌شود (`administrator@78.38.35.249`)، نه سروری که مالِ این پروژه باشد. روی همان ماشین Gitea، سرویسِ ایمیل، DNS، چند دیتابیس و اپ‌های تیم‌های دیگر در حالِ کارند. مالِ ما **دقیقاً یک دایرکتوری** است: `/home/administrator/mathapp-v2`، که با `gunicorn-mathapp.service` سرو می‌شود. همان یونیت با نامِ `zareian-mathapp` هم پاسخ می‌دهد و **همیشه همین نام را بنویسید**: ```bash sudo systemctl restart zareian-mathapp # مالِ ما sudo systemctl restart gunicorn # مالِ ما نیست — اپِ تیمِ دیگر ``` قواعدی که قابلِ مذاکره نیستند: 1. **هیچ فرمانِ مخربی بیرون از `~/mathapp-v2` اجرا نشود.** مسیر را کامل بنویسید، به دایرکتوریِ جاری تکیه نکنید. 2. **`git clean` آنجا هرگز اجرا نشود.** `db.sqlite3`، `.env` و `.django_secret_key` هیچ‌کدام در گیت نیستند و همان‌جا زندگی می‌کنند؛ `reset --hard` کاری با آن‌ها ندارد ولی `clean -fd` دیتابیس، پیکربندی و همه‌ی نشست‌های فعال را نابود می‌کند. 3. **پروژه هرگز پاک و «از نو clone» نشود** — به همان دلیل. برای برگرداندنِ درخت به حالتِ سالم از `git reset --hard origin/main` استفاده کنید. 4. **`.env` فقط از راهِ Django خوانده شود.** `cat .env` نه، و مقادیرش در هیچ چتی paste نشود. 5. **`df -h /` را قبل از هر کارِ پرنوشت چک کنید.** دیسک مدتی‌ست حدودِ ۹۴٪ پر است؛ پر شدنش سرویسِ ایمیل و Gitea را هم با ما پایین می‌کشد. ## لاگ‌ها هر درخواستِ `/api/` به‌صورتِ یک شیءِ JSON در هر خط لاگ می‌شود (`method`/`path`/`status`/`duration`/`user` — بدونِ بدنه، هدر یا سکرت) توسطِ `config.middleware.APIRequestLoggingMiddleware`. خروجی روی stdout است و اگر `LOG_DIR` ست باشد در `app.log` هم نوشته می‌شود. هر خطای پیش‌بینی‌نشده همیشه پاسخِ JSON برمی‌گرداند — نه صفحه‌ی HTML پیش‌فرضِ جنگو — و کامل با traceback لاگ می‌شود (`config/exceptions.py`).