make the README an entry point and move procedures into docs/

The README had grown into a 295-line mixture of onboarding, an ops runbook, a
content-authoring workflow, bulk question generation and a Docker log stack. A
reader arriving at the repo could not tell in ten seconds what this service is,
whether it is live, or where to start, and five of its claims no longer matched
the code:

- it said to export .env values by hand; settings.py has loaded .env through
  python-dotenv for a while now
- it listed six variables as "everything the app actually reads"; .env.example
  has carried many more than that for some time
- it called DJANGO_SECRET_KEY, DJANGO_ALLOWED_HOSTS and CACHE_URL required in
  production; all three now have working defaults, and the app boots without
  them on purpose so a partial .env cannot take the service down
- it claimed there is no insecure SECRET_KEY fallback; there is one, under DEBUG
- it required Python 3.12 while the project runs on 3.11 (Docker uses 3.12)

The README now states what the service is, where it runs, how healthy it is and
how to get it running in six commands, then links onward. Procedures moved to
docs/operations.md, docs/content-authoring.md, docs/architecture.md and
docs/docker.md, which is where someone follows a procedure rather than skims it.
Nothing was dropped; the deploy one-liner now includes the separate sudo restart
that deploy.sh cannot perform itself, and backup/restore is written down for the
first time.

Every number in the README was measured rather than quoted: content counts from
the live /api/health and content/seed/curriculum.json, 55 endpoints from the
generated OpenAPI schema, 481 tests from a full run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-23 01:12:04 +03:30
parent 5aa10eb7b7
commit 5492788bf8
5 changed files with 578 additions and 302 deletions

423
README.md
View File

@@ -1,331 +1,150 @@
# Tadrisino API
بک‌اند اپلیکیشن آموزشی Tadrisino — Django + Django REST Framework.
بک‌اندِ اپلیکیشنِ آموزشیِ تدریسینو — یک REST API روی Django که برنامه‌ی درسی،
آزمون‌ها، پیشرفتِ کاربر، دستیارِ هوشِ مصنوعی، اشتراک و پرداخت را سرو می‌کند.
احراز هویت با OTP، محتوای درسی و پیشرفت کاربر، دستیار هوش مصنوعی، اشتراک و پرداخت،
تخفیف، اعلان‌ها، همکاری در فروش، پشتیبانی و حساب خانواده.
فرانت‌اند ریپوی جداگانه‌ای است ([`MathApp_frontend`](https://gitea.tadrisino2.ir/Tadrisino/MathApp_frontend)).
**قراردادِ بینِ دو طرف، اسکیمای OpenAPI همین ریپوست** — نه حافظه‌ی کسی، نه یک
فایلِ دستی. هر تغییری در API باید از آنجا دیده شود.
| | |
|---|---|
| **وضعیت** | روی پروداکشن، در حالِ سرویس‌دهی |
| **آدرسِ زنده** | `https://api.math.tadrisino2.ir` · [سلامت](https://api.math.tadrisino2.ir/api/health) |
| **استک** | Python 3.11+ · Django 5.2 · DRF · SQLite · Qdrant (RAG) |
| **API** | ۵۵ اندپوینت · مستنداتِ زنده در `/api/docs/` |
| **تست** | ۴۸۱ تست، همه سبز (`python manage.py test`) |
| **محتوا** | ۸ پایه · ۵۸ فصل · ۲۶۳ نود · ۲٬۶۱۷ درس · ۲٬۴۶۳ سوال |
---
## شروعِ سریع
```bash
python3 -m venv venv && source venv/bin/activate
pip install -r requirements.txt
cp .env.example .env # برای توسعه‌ی محلی: DJANGO_DEBUG=1 را ست کنید
python manage.py migrate
python manage.py seed_content # برنامه‌ی درسی و سوال‌های واقعی از content/seed/
python manage.py runserver
```
سرویس روی `http://127.0.0.1:8000` بالا می‌آید. `.env` خودکار خوانده می‌شود —
لازم نیست چیزی را `export` کنید.
| | |
|---|---|
| Swagger UI | `/api/docs/` |
| Redoc | `/api/redoc/` |
| اسکیمای OpenAPI | `/api/schema/` |
| پنلِ ادمین | `/admin/` |
| سلامتِ سرویس | `/api/health` |
**اولین درخواست در Swagger:** `POST /api/auth/session` با
`{"type":"guest","deviceId":"d_test1"}` بزنید، `token` را بردارید، روی
**Authorize** کلیک کنید و همان رشته را (بدونِ کلمه‌ی `Bearer`) وارد کنید.
دستورهای پرکاربردِ دیگر:
```bash
python manage.py test # کلِ تست‌سوییت
python manage.py createsuperuser # دسترسی به /admin
python manage.py seed_test_data # داده‌ی ساختگی برای توسعه (فقط محلی)
```
---
## معماری
هر اپ یک مسئولیت دارد و مدل‌های خودش را نگه می‌دارد:
| اپ | مسئولیت |
|---|---|
| `accounts` | احراز هویت، session، پروفایل کاربر |
| `content` | پایه‌ها، مباحث، درس‌ها، سوال‌ها، پیشرفت |
| `progress` | آمار، لیدربرد، streak |
| `ai_tutor` | مکالمات دستیار هوش مصنوعی |
| `rag` | لایه‌ی RAG: بازیابی از Qdrant + تولید با GPT، پشتِ `POST /api/ai/ask` |
| `subscription` / `payment` / `discount` | پلن، سفارش، تراکنش، کد تخفیف |
| `notifications`, `affiliate`, `support`, `family` | امکانات جانبی |
| `statuslog` | لاگ عمومی تغییر status، مستقل از بقیه‌ی اپ‌ها |
| `accounts` | هویت، session، ورود با OTP |
| `content` | برنامه‌ی درسی، درس، سوال، پیشرفت روی نقشه |
| `progress` | آمار، لیدربرد، streak، شخصیت‌های بازی |
| `ai_tutor` · `rag` | دستیارِ هوشِ مصنوعی: بازیابی از Qdrant + تولید با GPT |
| `subscription` · `payment` · `discount` | پلن، سفارش، تراکنش، کدِ تخفیف |
| `notifications` · `affiliate` · `support` · `family` · `campaigns` | امکاناتِ جانبی |
| `statuslog` | لاگِ عمومیِ تغییرِ وضعیت، مستقل از بقیه |
| `config` | تنظیمات، روت‌های اصلی، middleware |
سه تصمیمِ معماری که غیرِبدیهی‌اند و دانستنشان لازم است:
- **احرازِ هویت، سیستمِ خودمان است نه جنگو.** توکنِ bearerِ امضاشده و stateless
با `django.core.signing` (بدونِ کتابخانه‌ی JWT)، پشتیبانی‌شده با مدلِ
`accounts.Session`. `accounts.User` عمداً `AUTH_USER_MODEL` **نیست** — یک
مدلِ بیزنسیِ مستقل است. هدرِ نامعتبر یعنی درخواستِ ناشناس، نه خطا؛ هر ویو
خودش تصمیم می‌گیرد که آیا ورود لازم است یا نه (`accounts/authentication.py`).
- **اپ‌ها با قرارداد از هم جدا شده‌اند، نه با ForeignKey.** ارجاعِ بینِ اپ‌ها با
`user_id` و `object_id` + `label` انجام می‌شود تا هر اپ بعداً قابلِ جدا شدن
باشد. **FK بینِ اپ‌ها اضافه نکنید.**
- **محدودیتِ نرخ (throttle) متمرکز است.** هر اندپوینتِ حساس یک
`throttle_scope` دارد و نرخش در `REST_FRAMEWORK['DEFAULT_THROTTLE_RATES']`
داخلِ `config/settings.py` تعریف می‌شود. اندپوینتِ جدید یعنی **هر دو**.
جزئیاتِ بیشتر: [`docs/architecture.md`](docs/architecture.md).
---
## ۱. راه‌اندازی محیط توسعه
## دیپلوی
### پیش‌نیازها
- پایتون ۳.۱۲
- pip
### تنظیم محیط
```bash
python3 -m venv venv
source venv/bin/activate # ویندوز: venv\Scripts\activate
pip install -r requirements.txt
cp .env.example .env
```
`.env.example` (تمام متغیرهایی که واقعاً توسط اپ خونده می‌شن):
```env
DJANGO_DEBUG=1
DJANGO_SECRET_KEY=
DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1
CACHE_URL=
KAVENEGAR_API_KEY=
GRAFANA_ADMIN_PASSWORD=admin
```
مقادیر `.env` رو به شل خودتون export کنید (یا از `direnv`/`python-dotenv` استفاده کنید):
```bash
export DJANGO_DEBUG=1 # ویندوز (PowerShell): $env:DJANGO_DEBUG=1
```
> `DEBUG` دیفالتش `False` است. برای فعال‌کردنِ کمک‌کننده‌های dev-only در Swagger
> (`DEV_MASTER_OTP` و فیلدِ `devOtp` توی پاسخ‌ها)، محلی `DJANGO_DEBUG=1` رو ست کنید.
### سریع‌ترین راه اجرای پروژه
```bash
python manage.py migrate
python manage.py runserver
```
اپ روی `http://127.0.0.1:8000` بالا میاد.
### مایگریشن‌ها
```bash
python manage.py migrate
```
### دیتای تستی (Seed)
یک دیتاست کوچک و idempotent می‌سازه که همه‌ی اپ‌ها رو پوشش می‌ده (کاربر، محتوا، پیشرفت،
اشتراک، پرداخت، تخفیف، دستیار هوش مصنوعی، اعلان، افیلیت، پشتیبانی، خانواده). اجرای مکرر
امن است — با `get_or_create` هیچ رکورد تکراری‌ای نمی‌سازه. پیش‌فرض فقط وقتی `DEBUG=True`
اجرا می‌شه، مگر با `--force`.
کلِ انتشار یک خط است:
```bash
python manage.py seed_test_data # لوکال (DEBUG=True)
python manage.py seed_test_data --force # staging (DEBUG=False)
python manage.py seed_test_data --reset # پاک‌کردنِ دیتای قبلیِ seed و ساختِ دوباره
ssh -t administrator@78.38.35.249 \
'cd ~/mathapp-v2 && ./deploy.sh; sudo systemctl restart zareian-mathapp'
```
### مستندات API
| | |
|---|---|
| Swagger UI | `http://127.0.0.1:8000/api/docs/` |
| Redoc | `http://127.0.0.1:8000/api/redoc/` |
| OpenAPI schema | `http://127.0.0.1:8000/api/schema/` |
| پنل ادمین | `http://127.0.0.1:8000/admin/` |
`deploy.sh` کد را می‌گیرد، وابستگی‌ها را نصب می‌کند، مهاجرت می‌زند، محتوا را
seed می‌کند، فایل‌های استاتیک را جمع می‌کند و در پایان **ثابت می‌کند که سرویس
واقعاً ری‌استارت شده** — نه اینکه فقط فرض کند. ری‌استارت جداست چون `sudo`
داخلِ اسکریپت نمی‌تواند رمز بپرسد.
**شروع سریع در Swagger:**
1. `POST /api/auth/session` با `{"type":"guest","deviceId":"d_test1"}` → یک `token` بگیرید.
2. روی دکمه‌ی **Authorize** بزنید و فقط خودِ `token` رو (بدون کلمه‌ی Bearer) وارد کنید.
3. برای تست لاگین با شماره: `POST /api/auth/otp/request` بزنید، یا در محیط توسعه کد **0000**
رو با هر شماره‌ای در `POST /api/auth/login` امتحان کنید (dev-only، نیاز به `DEBUG=True`).
پس از هر انتشار، از بیرون و بدونِ SSH تأیید کنید:
### دستورهای پرکاربرد
```bash
python manage.py createsuperuser # اختیاری، برای /admin
python manage.py test # اجرای کل تست‌سوییت
curl https://api.math.tadrisino2.ir/api/health
```
### دستیار هوش مصنوعی (RAG)
`200` یعنی سالم، `503` یعنی چیزی واقعاً خراب است. هیچ سکرتی برنمی‌گرداند.
`POST /api/ai/ask` پاسخ را از روی محتوای واقعیِ کتاب می‌سازد: embedding سوال →
جستجوی Qdrant محدود به همان درس/فصل → تولید با GPT. تیکه‌های استفاده‌شده در
`usedChunks` برمی‌گردند.
- نیازمندِ `OPENAI_API_KEY` و `QDRANT_API_KEY` در `.env` (اپ `rag`؛ تنظیماتِ
`OPENAI_*` / `RAG_*` در `config/settings.py`). بدونِ کلید، همان پاسخِ stub قبلی.
- تست در Swagger: `POST /api/ai/ask` با
`{"userQuestion":"...","subject":"ریاضی","grade":"ششم","lessonNumber":2}`.
- `"stream": true` پاسخ را به‌صورت SSE توکن‌به‌توکن می‌فرستد (قراردادِ JSON بدونِ این فلگ دست‌نخورده).
- `python manage.py rag_catalog` فهرستِ زنده‌ی collectionهای Qdrant را نشان می‌دهد (فقط‌خواندنی).
- افزودنِ محتوای خودمان: `POST /api/rag/ingest/text` و `/api/rag/ingest/pdf` (داخلی، هدرِ `X-Internal-Key`).
رویه‌ی کامل — متغیرهای محیطی، ورودِ توسعه‌دهنده‌ها، بکاپ، عیب‌یابی:
[`docs/operations.md`](docs/operations.md).
---
## ۲. راهنمای دیپلوی پروداکشن
## مستندات
### بالا آوردنِ نسخه‌ی جدید — یک دستور
```bash
./deploy.sh
```
همین. اسکریپت به ترتیب کدِ تازه را می‌گیرد، وابستگی‌ها را نصب می‌کند، مهاجرت‌ها
را اعمال می‌کند، فایل‌های استاتیک را جمع می‌کند، سرویس را ری‌استارت می‌کند، و در
پایان **خودش بررسی می‌کند که واقعاً بالا آمده یا نه**. اگر بالا نیامد، علت‌های
رایج را چاپ می‌کند.
چرا این مهم است: قبلاً این‌ها چند کارِ دستی به ترتیبِ درست بودند و فراموش شدنِ
هر کدام اپ را می‌خواباند، با خطایی که گویا نبود. مثلاً اضافه شدنِ یک پکیجِ جدید
به `requirements.txt` بدونِ `pip install` باعثِ `ModuleNotFoundError` موقعِ بالا
آمدن می‌شود و کاربر فقط `502 Bad Gateway` می‌بیند. اسکریپت همیشه `pip install`
را اجرا می‌کند تا این حالت اصلاً پیش نیاید.
بارِ اول، نامِ سرویس را بدهید تا دفعه‌های بعد خودکار ری‌استارت شود:
```bash
TADRISINO_SERVICE=<نامِ سرویس> ./deploy.sh
```
متغیرهای اختیاری: `SKIP_PULL=1` (بدونِ git pull)، `TADRISINO_HEALTH_URL`،
`PIP_INDEX_URL` (اگر pypi.org تایم‌اوت می‌دهد).
### بررسیِ سلامت از بیرون
```bash
curl https://<host>/api/health
```
بدونِ احرازِ هویت و بدونِ SSH می‌گوید دیتابیس سالم است یا نه، مهاجرتِ
اعمال‌نشده مانده یا نه، کش کار می‌کند یا نه، و چند پایه/درس/سوال روی آن سرور
هست. `200` یعنی سالم، `503` یعنی چیزی واقعاً خراب است. هیچ سکرتی برنمی‌گرداند.
این همان چیزی‌ست که جلوی «کد رفته ولی مهاجرت اجرا نشده» را می‌گیرد — حالتی که
اپ ظاهراً بالا می‌آید و بعد زیرِ بارِ واقعی می‌شکند.
### مدیریتِ محتوا بدونِ ترمینال
هیچ محتوایی لازم نیست با دستور روی سرور گذاشته شود.
| کار | کجا |
| سند | موضوع |
|---|---|
| افزودن/ویرایشِ **یک** سوال | `/admin/content/question/` → «افزودن» (انتخابِ درس با جستجو) |
| وارد کردنِ **فله‌ای** سوال | همان صفحه → «وارد کردنِ سوال‌ها از فایل» |
| گرفتنِ خروجیِ سوال‌ها | چند سوال را انتخاب کنید → اکشنِ «دریافتِ فایل» |
| وارد کردنِ **برنامه‌ی درسی** (پایه/فصل/درس) | `/admin/content/grade/` → «وارد کردنِ برنامه‌ی درسی از فایل» |
| گرفتنِ خروجیِ برنامه‌ی درسی | پایه‌ها را انتخاب کنید → اکشنِ «دریافتِ فایلِ برنامه‌ی درسی» |
| [`docs/operations.md`](docs/operations.md) | دیپلوی، متغیرهای محیطی، سلامت، بکاپ، ورودِ توسعه‌دهنده‌ها |
| [`docs/content-authoring.md`](docs/content-authoring.md) | افزودن و ویرایشِ محتوا از پنلِ ادمین، ساختِ انبوهِ سوال |
| [`docs/architecture.md`](docs/architecture.md) | تصمیم‌های معماری و دلیلشان |
| [`docs/frontend/README.md`](docs/frontend/README.md) | راهنمای اتصالِ فرانت‌اند + کلاینتِ تایپ‌دار |
| [`docs/docker.md`](docs/docker.md) | استکِ لاگِ محلی (Loki + Alloy + Grafana) |
| [`docs/extraction-playbook.md`](docs/extraction-playbook.md) | استخراجِ محتوا از کتاب با مدلِ ویژن و هزینه‌هایش |
خروجیِ هر کدام دقیقاً همان قالبی‌ست که ورودی‌اش می‌پذیرد، پس چرخه‌ی
«بگیر → ویرایش کن → برگردان» بسته است و برای گرفتنِ نمونه لازم نیست کسی
مستندات بخواند.
---
دو تضمین:
- **سوال‌ها:** فایلِ نامعتبر هیچ چیزی نمی‌نویسد — یا همه وارد می‌شوند یا هیچ‌کدام.
- **برنامه‌ی درسی:** چیزی پاک نمی‌شود. هر چه در فایل هست ساخته/به‌روز می‌شود و
هر چه نیست دست‌نخورده می‌ماند، پس یک فایلِ ناقص محتوای سرور را از بین نمی‌برد.
## محدودیت‌های شناخته‌شده
### ساختِ انبوهِ سوال از کتاب
اینها عمداً نوشته شده‌اند تا کسی دوباره کشفشان نکند:
وقتی کتابی تازه روی Qdrant آمده و باید برایش سوال ساخته شود، مسیرِ رایگان این است:
```bash
# اسکلتِ برنامه‌ی درسی از فهرستِ خودِ کتاب (بدونِ هزینه)
python manage.py seed_curriculum_from_textbook --subject ریاضی --grade نهم --dry-run
python manage.py seed_curriculum_from_textbook --subject ریاضی --grade نهم
# جفت‌های (درس، مسئله‌ی تأییدشده) را برای سوال‌نویسی بیرون بده (بدونِ هزینه)
python manage.py generate_questions_from_chunks --subject ریاضی --grade نهم --export /tmp/batches
```
خروجی چند فایلِ JSON است به‌علاوه‌ی یک `*-RULES.txt` که قواعدِ ساختِ سوال را
دارد. سوال‌ها را با آن قواعد بنویسید و از «وارد کردنِ فله‌ای سوال» در جدولِ
بالا واردشان کنید. همان فرمان بدونِ `--export` کار را به مدلِ پولی می‌سپارد،
ولی گران‌تر است و کیفیتِ بهتری نمی‌دهد. جزئیات و هزینه‌های اندازه‌گیری‌شده در
`docs/extraction-playbook.md`.
`--dry-run` روی هر دو فرمان کار می‌کند و پیش از هر نوشتنی نشان می‌دهد چه ساخته
خواهد شد.
### داده‌ی تستی روی سرور
`seed_test_data` فقط برای محیطِ لوکال است و **نباید** روی سروری که کاربرِ واقعی
دارد اجرا شود؛ ردیف‌هایی با پیشوندِ `[SEED]`/`[DEMO]` می‌سازد که در API دیده
می‌شوند. محتوای واقعی از مسیرِ جدولِ بالا می‌رود، نه از این دستور.
### ورودِ توسعه‌دهنده‌ها به سرور (بدونِ کاوه‌نگار)
تا وقتی `KAVENEGAR_API_KEY` ست نشده، `POST /api/auth/otp/request` خطای صریحِ
۵۰۳ می‌دهد و با `DEBUG=0` هیچ‌کس نمی‌تواند وارد API شود. برای اینکه ورودِ تیمِ
فرانت به یک نفرِ خاص وابسته نباشد، `TEST_LOGIN_PHONES` را در `.env` سرور پر
کنید:
```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"}'
```
چند نکته که عمدی‌اند:
- **این با `DEV_MASTER_OTP` یکی نیست.** آن کدِ `0000` هر شماره‌ای را قبول
می‌کند و فقط با `DEBUG=1` روشن می‌شود؛ این فقط همین چند شماره را، هرکدام با
کدِ خودش.
- **کد در پاسخِ API برنمی‌گردد.** کسی که شماره را بداند هم نمی‌تواند کد را از
سرور بیرون بکشد، و سقفِ تلاشِ ورودِ هر شماره روی این‌ها هم اعمال می‌شود.
- **`/api/health` تا وقتی این متغیر پر باشد هشدار می‌دهد** (تعداد، نه شماره و
نه کد) تا روی سروری که کاربرِ واقعی دارد فراموش نشود.
- **مسیرِ مهمان دست‌نخورده است.** فرانت که امروز با
`POST /api/auth/session` و `deviceId` کار می‌کند، هیچ تغییری نمی‌بیند.
- خالی گذاشتنِ این متغیر یعنی رفتارِ قبلی، مو‌به‌مو.
### متغیرهای محیطی موردنیاز
| متغیر | لازم در پروداکشن | توضیح |
|---|---|---|
| `DJANGO_DEBUG` | باید ست‌نشده یا `0` باشه | دیفالت‌های امنیتیِ پروداکشن رو فعال می‌کنه |
| `DJANGO_SECRET_KEY` | **بله** | بدونش وقتی `DEBUG=False` است اپ اصلاً بالا نمیاد |
| `DJANGO_ALLOWED_HOSTS` | **بله** | لیستِ host ها، کاما-جدا |
| `CACHE_URL` | **بله** | آدرسِ Redis، مثلاً `redis://host:6379/0` — برای rate limitِ مشترک وقتی `DEBUG=False` لازمه |
| `KAVENEGAR_API_KEY` | **بله** | ارسال واقعیِ OTP از طریق کاوه‌نگار |
| `TEST_LOGIN_PHONES` | نه (فقط تا وقتی تیم دارد توسعه می‌دهد) | شماره‌های تستِ ورود، بخشِ بالا |
| `GRAFANA_ADMIN_PASSWORD` | فقط اگه از استکِ Grafana استفاده می‌کنید | بخشِ Docker پایین رو ببینید |
### تنظیمات پروداکشن
- `DEBUG` باید ست **نشه** (یا `0` باشه). با `DEBUG=False`:
- `SECRET_KEY` اجباریه — هیچ fallback ناامنی وجود نداره.
- `CACHE_URL` (Redis) اجباریه — `LocMemCache` فقط برای dev هست.
- `DEV_MASTER_OTP` و فیلدِ `devOtp` توی پاسخ‌ها غیرفعال می‌شن.
- برای ترافیک واقعی به‌جای SQLیت پیش‌فرض، از یک دیتابیسِ پروداکشن‌گرید استفاده کنید
(پروژه با همون SQLite شیپ می‌شه؛ عوض‌کردنِ engine یک تصمیمِ زیرساختیه، خارج از این ریپو).
- از طریق یک سرورِ WSGI/ASGI (مثلاً gunicorn) جلوی اپ سرو کنید — تنظیماتِ Docker موجود
همچنان `runserver` اجرا می‌کنه و فقط برای dev/local مستند شده (پایین رو ببینید).
### تنظیمات Docker/دیپلوی
`docker-compose.yml` موجود، اپ رو **به‌همراه** یک استکِ لاگِ متمرکز
(Loki + Grafana Alloy + Grafana) اجرا می‌کنه. این یک تنظیمِ **dev/local** هست، نه دیپلویِ
پروداکشن (سرویسِ `django` همچنان `runserver` اجرا می‌کنه، نه یک سرورِ پروداکشن).
```bash
cp .env.example .env # اختیاری — بدونش compose از دیفالت‌های dev استفاده می‌کنه
docker compose up --build
```
| سرویس | آدرس | توضیح |
|---|---|---|
| اپ Django | `http://localhost:8000` | |
| Grafana | `http://localhost:3000` | دسترسیِ anonymous admin، فقط محیطِ محلی |
| Loki | `http://localhost:3100` | ذخیره‌سازیِ لاگ، در معرضِ کاربر نیست |
| Alloy | `http://localhost:12345` | رابطِ دیباگِ اختیاری |
### ترتیبِ لازمِ بالا آمدن
Compose خودش این رو مدیریت می‌کنه:
1. اول `loki` بالا میاد.
2. `alloy` و `grafana` بعد از `loki` بالا میان (`depends_on`).
3. `django` هیچ وابستگی‌ای به استکِ لاگ نداره — خودش `migrate` بعد `runserver` رو
اجرا می‌کنه و منتظرِ Loki/Alloy/Grafana نمی‌مونه.
### دستورهای manage.py داخلِ Docker
اگه فقط از طریقِ Docker اجرا می‌کنید (بدونِ venv محلی)، `python manage.py ...` رو مستقیم
روی سیستم خودتون اجرا نکنید — باید داخلِ کانتینرِ `django` اجرا بشه:
```bash
docker compose exec django python manage.py createsuperuser
docker compose exec django python manage.py seed_test_data
docker compose exec django python manage.py test
```
(`migrate` نیازی به این نیست — طبقِ بالا، خودکار قبل از `runserver` اجرا می‌شه.)
### مایگریشن‌ها
```bash
python manage.py migrate --noinput
```
در Docker، این به‌صورت خودکار به‌عنوان بخشی از دستورِ سرویسِ `django`، قبل از `runserver` اجرا می‌شه.
### راهنمای seed برای staging/تست
- هرگز `seed_test_data` رو روی یک دیتابیسِ پروداکشنِ واقعی اجرا نکنید.
- روی staging (`DEBUG=False`)، صریحاً با `--force` اجرا کنید:
```bash
python manage.py seed_test_data --force
```
- برای پاک‌کردن و ساختِ دوباره‌ی دیتای seed بین چرخه‌های تست، از `--reset` استفاده کنید.
همه‌ی رکوردهای seed‌شده در فیلدهای متنیِ قابل‌مشاهده با تگِ `[SEED]` مشخص شدن، برای
شناساییِ راحت‌تر و پاک‌سازی.
### چطور API دیپلوی‌شده رو verify کنیم
```bash
curl -i https://<host>/api/schema/ # باید 200 برگردونه (OpenAPI schema)
curl -i https://<host>/api/docs/ # باید 200 برگردونه (Swagger UI)
curl -i -X POST https://<host>/api/auth/session \
-H "Content-Type: application/json" \
-d '{"type":"guest","deviceId":"verify-1"}' # باید یک token برگردونه
```
### نکات مهمِ پروداکشن
- `DEV_MASTER_OTP` (کد `0000`) و فیلدِ `devOtp` توی پاسخ‌ها فقط وقتی `DEBUG=True` وجود
دارن — قبل از رفتن روی محیطِ واقعی مطمئن بشید `DEBUG` خاموشه.
- احراز هویت با Bearer token امضاشده و stateless (`django.core.signing`)، هماهنگ با مدل
`Session` — بدون کتابخانه‌ی JWT خارجی.
- `accounts.User` یک مدل بیزنسیِ مستقله، **نه** `AUTH_USER_MODEL` جنگو.
- همه‌ی لاگ‌های ساختاریافته‌ی درخواست (`method`/`path`/`status`/`duration`/`user`، بدونِ
بدنه/هدر/سکرت) روی stdout و — وقتی `LOG_DIR` ست باشه (مثلِ داخلِ Docker) — در
`/var/log/django/app.log` نوشته می‌شن؛ قابلِ مشاهده در Grafana از طریقِ استکِ بالا.
- `POST /api/ai/ask` با تنظیمِ `OPENAI_API_KEY`/`QDRANT_API_KEY` پاسخِ واقعیِ RAG می‌دهد
(اپ `rag`)؛ بدونِ کلید همان stub. جزئیات: بخشِ «دستیار هوش مصنوعی (RAG)» بالاتر.
- ارسالِ OTP از طریقِ کاوه‌نگار (`KAVENEGAR_API_KEY`) انجام می‌شه؛ شکستِ ارسال به‌جای
موفقیتِ ساختگی، خطای صریحِ `503` برمی‌گردونه.
- هر خطای پیش‌بینی‌نشده (نه فقط خطاهای شناخته‌شده‌ی API) همیشه پاسخِ JSON برمی‌گردونه،
نه صفحه‌ی HTML پیش‌فرضِ جنگو، و کامل با traceback لاگ می‌شه (`config/exceptions.py`).
- **دیتابیس هنوز SQLite است**، نه Postgres. برای حجمِ فعلی کافی‌ست، ولی
تک‌نویسنده بودنش اولین گلوگاهِ مقیاس است.
- **بکاپِ خودکار وجود ندارد.** `db.sqlite3` در گیت نیست و تنها نسخه‌ی داده‌ی
کاربران است. روشِ بکاپِ دستی در [`docs/operations.md`](docs/operations.md)
آمده؛ خودکار کردنش اولین کارِ ارزشمندِ بعدی است.
- **`CACHE_URL` روی سرور ست نشده**، پس محدودیتِ نرخ بینِ workerها مشترک نیست.
Redis روی همان ماشین بالاست. `/api/health` این را هشدار می‌دهد.
- **`KAVENEGAR_API_KEY` تنظیم نشده**، پس `POST /api/auth/otp/request` برای
شماره‌های واقعی `503` می‌دهد. راهِ ورودِ تیمِ توسعه در
[`docs/operations.md`](docs/operations.md) توضیح داده شده.
- **پوششِ محتوا یکدست نیست.** پایه‌های هشتم تا دوازدهم کاملند (هر نود چهار
سوال)؛ در پایه‌های سوم/ششم/هفتم ۳۹ نود از ۱۴۵ کمتر از چهار سوال دارند —
هیچ‌کدام خالی نیست، پس روی نقشه نودِ غیرقابلِ بازی وجود ندارد.
- **محتوای ویدیویی وجود ندارد**، هرچند `Lesson.kind` و `video_url` می‌توانند
بیانش کنند.
- **`DEV_MASTER_OTP` (کد `0000`) باید پشتِ `DEBUG` بماند.** روشن بودنش یعنی هر
کسی می‌تواند به‌جای هر کاربری وارد شود. `/api/health` هشدارش می‌دهد.