document the twenty environment variables the code reads but never mentioned

A mechanical cross-check of os.environ.get() in settings against .env.example
found twenty variables the app reads that were written down nowhere. Most have
harmless defaults, but three would have cost someone real time:

- NUM_PROXIES decides how many X-Forwarded-For hops are trusted, and guest rate
  limiting is keyed on the resulting IP. Set too high, a client can forge the
  header and walk past the limit; too low and everyone collapses onto the proxy's
  own IP. CLAUDE.md warns about it; the file an operator actually edits did not.
- KAVENEGAR_SENDER is read alongside the API key, so someone configuring SMS from
  the README alone would have set only half of it.
- INTERNAL_SERVICE_KEY and PAYMENT_WEBHOOK_SECRET are empty under DEBUG=0 by
  design, which quietly keeps the ingest and webhook routes shut. That is the
  right behaviour and exactly the kind of thing that reads as a bug when it is
  not written down.

The whole Cafe Bazaar block was undocumented too.

Three variables in .env.example are not read by settings and were left alone:
GRAFANA_ADMIN_PASSWORD is consumed by docker-compose, TADRISINO_SERVICE by
deploy.sh, and OPENAI_API_KEY_OPENAI_DIRECT is a documented parking spot.

CLAUDE.md's current-state section was a day stale (458 tests, now 483) and said
nothing about the documentation restructure, the RTL docs shell, the test-login
allowlist or the frontend test account, so anyone picking this up would have
rediscovered all four. It now also records that backups are still manual and
that sqlite3 is not installed on the server.

Also verified mechanically, with no findings: no docstring references an
endpoint absent from the OpenAPI schema, no docstring names a setting or
constant that does not exist, and every internal documentation link resolves.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-23 02:11:29 +03:30
parent 5003b8a852
commit 7d6abab15c
2 changed files with 79 additions and 6 deletions

View File

@@ -133,3 +133,52 @@ DB_CONN_MAX_AGE=60
AUTH_USER_CACHE_TTL=0 AUTH_USER_CACHE_TTL=0
# سقفِ ارزانِ خواندن روی endpointهای پرترافیکِ GET (home/leaderboard/summary/...). # سقفِ ارزانِ خواندن روی endpointهای پرترافیکِ GET (home/leaderboard/summary/...).
READ_THROTTLE_RATE=120/min READ_THROTTLE_RATE=120/min
# ⚠️ تعدادِ پروکسی‌هایی که جلوی اپ نشسته‌اند (nginx = ۱). محدودیتِ نرخِ
# کاربرِ مهمان بر اساسِ IP است و IP از X-Forwarded-For خوانده می‌شود؛ اگر این
# عدد از تعدادِ واقعیِ پروکسی‌ها بیشتر باشد، کلاینت می‌تواند هدر را جعل کند و
# محدودیت را دور بزند. کمتر باشد، همه پشتِ IP پروکسی یکی شمرده می‌شوند.
NUM_PROXIES=1
# -----------------------------------------------------------------------
# بقیه — همه پیش‌فرضِ کارا دارند و معمولاً لازم نیست دست بخورند
# -----------------------------------------------------------------------
# پیامک: بدونِ SENDER هم کاوه‌نگار با خطِ پیش‌فرضِ حساب می‌فرستد، ولی اگر خطِ
# اختصاصی دارید اینجا بگذارید. قالبِ متنِ پیامک هم قابلِ تغییر است ({code} لازم).
KAVENEGAR_SENDER=
# OTP_SMS_TEMPLATE=کد ورود شما به تدریسینو: {code}
# کلیدِ endpointهای داخلی (ingest در اپ rag) و امضای وبهوکِ پرداخت. با
# DEBUG=1 مقدارِ توسعه دارند؛ با DEBUG=0 خالی‌اند و آن مسیرها بسته می‌مانند
# تا وقتی مقدارِ واقعی بگذارید.
INTERNAL_SERVICE_KEY=
PAYMENT_WEBHOOK_SECRET=
# پرداختِ درون‌برنامه‌ایِ کافه‌بازار. بدونِ این چهار تا، مسیرِ بازار
# fail-closed می‌ماند (payment/bazaar.py). REFRESH_TOKEN یک‌بار و دستی از
# پنلِ devapi گرفته می‌شود.
BAZAAR_CLIENT_ID=
BAZAAR_CLIENT_SECRET=
BAZAAR_REFRESH_TOKEN=
# BAZAAR_PACKAGE_NAME=ir.tadrisino.app
# BAZAAR_API_BASE=https://pardakht.cafebazaar.ir/devapi/v2
# آدرس‌های زرین‌پال از روی ZARINPAL_SANDBOX بالا انتخاب می‌شوند؛ فقط اگر
# زرین‌پال دامنه‌اش را عوض کرد این سه را دستی بدهید.
# ZARINPAL_API_BASE=
# ZARINPAL_STARTPAY_BASE=
# ZARINPAL_CALLBACK_URL=
# نوشتنِ لاگ در فایل علاوه بر stdout (داخلِ Docker ست می‌شود). خالی = فقط stdout.
LOG_DIR=
# سقفِ گیرنده‌ی اکشنِ «ارسالِ پیامک به انتخاب‌شده‌ها» در پنلِ ادمین. این اکشن
# داخلِ همان request اجرا می‌شود، پس سقف جلوی تایم‌اوت را می‌گیرد.
ADMIN_SMS_MAX_RECIPIENTS=200
# سقفِ نرخِ /api/health برای پایشِ بیرونی.
HEALTH_THROTTLE_RATE=60/min
# تایم‌اوت و پارامترهای RAG. EMBEDDING_DIM باید با مدلِ embedding بالا بخواند.
OPENAI_TIMEOUT_SECONDS=60
QDRANT_TIMEOUT_SECONDS=30
OPENAI_EMBEDDING_DIM=1536
# کشِ پاسخِ دستیار (ثانیه) — سوالِ تکراری دوباره به مدل نمی‌رود.
RAG_ANSWER_CACHE_TTL=1800

View File

@@ -353,20 +353,44 @@ preferred setup, because there is then no key to leak. **Never let the server ru
`django-insecure-…` fallback from `config/settings.py`: that string is committed here, so `django-insecure-…` fallback from `config/settings.py`: that string is committed here, so
anyone who can read this repo can forge a token for any user.** anyone who can read this repo can forge a token for any user.**
## وضعیت فعلی (۲۰۲۶-۰۹-۲۲) — این را اول بخوان ## وضعیت فعلی (۲۰۲۶-۰۹-۲۳) — این را اول بخوان
سرور روی `api.math.tadrisino2.ir` زنده است و **کدِ فعلی را سرو می‌کند** (سرویس واقعاً پروژه در این تاریخ **بسته و تحویل داده شد**. سرور روی `api.math.tadrisino2.ir` زنده
ری‌استارت شد، نه فقط فایل‌ها روی دیسک). `/api/health` می‌دهد: است و کامیتِ `5003b8a` را سرو می‌کند (سرویس واقعاً ری‌استارت شد، نه فقط فایل‌ها روی
`grades: 8, lessons: 2617, questions: 2463` و هیچ مهاجرتِ اعمال‌نشده‌ای ندارد. دیسک). `/api/health` می‌دهد: `grades: 8, lessons: 2617, questions: 2463` و هیچ
مهاجرتِ اعمال‌نشده‌ای ندارد.
- **هر نودِ پایه‌های هشتم تا دوازدهم آزمونِ کاملِ چهارسوالی دارد.** پایه‌های - **هر نودِ پایه‌های هشتم تا دوازدهم آزمونِ کاملِ چهارسوالی دارد.** پایه‌های
سوم/ششم/هفتم کتابِ تیمِ دیگرند: ۳۹ نود از ۱۴۵ کمتر از چهار سوال دارند، ولی هیچ‌کدام سوم/ششم/هفتم کتابِ تیمِ دیگرند: ۳۹ نود از ۱۴۵ کمتر از چهار سوال دارند، ولی هیچ‌کدام
خالی نیست، پس روی نقشه جزیره‌ی مرده وجود ندارد. خالی نیست، پس روی نقشه جزیره‌ی مرده وجود ندارد.
- ۴۵۸ تست سبز، `check` و `check --deploy` و `makemigrations --check` تمیز، و - ۴۸۳ تست سبز، `check` و `makemigrations --check` تمیز، و `spectacular` بدونِ خطا و
`spectacular` بدونِ خطا و هشدار (قرارداد کامل است). هشدار. ۵۶ اندپوینت.
- تستِ هم‌زمانیِ campaigns که مدت‌ها flaky بود **دیگر flaky نیست** — علتش یک باگِ - تستِ هم‌زمانیِ campaigns که مدت‌ها flaky بود **دیگر flaky نیست** — علتش یک باگِ
واقعی بود، نه طبیعتِ تست؛ پایینِ همین فایل توضیح داده شده. واقعی بود، نه طبیعتِ تست؛ پایینِ همین فایل توضیح داده شده.
**مستندات در ۲۰۲۶-۰۹-۲۳ بازسازی شد.** `README.md` حالا درِ ورودی است نه دفترچه‌ی
راهنما؛ رویه‌ها رفتند به `docs/operations.md` (دیپلوی، متغیرهای محیطی، بکاپ و
بازیابی، ورودِ توسعه‌دهنده‌ها، قواعدِ سرور)، `docs/content-authoring.md`،
`docs/architecture.md` و `docs/docker.md`. **قبل از نوشتنِ هر سندِ تازه آنجا را نگاه
کن**، وگرنه همان حرف در سه جا تکرار می‌شود.
سه چیزِ کوچک که در همان روز اضافه شد و دانستنشان لازم است:
- **`TEST_LOGIN_PHONES`** — فهرستِ «شماره:کد» در `.env` که ورود بدونِ کاوه‌نگار را
ممکن می‌کند. روی سرور **عمداً خالی است**؛ انتخابِ شماره و کد یک تصمیمِ اعتبارنامه‌ای
است و مالِ صاحبِ پروژه. جزئیات در `docs/operations.md`.
- **حسابِ مهمانِ تستِ فرانت** (`deviceId = frontend-test-device`، کاربرِ ۶) با اعلان و
لیدربردِ دستی‌نوشته روی دیتابیسِ پروداکشن. `purge_demo_data` نمی‌گیردش و باید پیش
از آمدنِ کاربرِ واقعی پاک شود.
- **صفحه‌های `/api/docs/` و `/api/redoc/` از قالبِ خودمان رد می‌شوند**
(`templates/tadrisino/`) تا متنِ فارسی راست‌به‌چپ رندر شود. قالبِ drf-spectacular
دست‌نخورده است و با `{{ block.super }}` گسترش داده شده.
**بکاپ هنوز خودکار نیست.** `deploy.sh` پیش از `migrate` بکاپ نمی‌گیرد؛ روشِ دستی در
`docs/operations.md` است و در سه دیپلویِ آن روز دستی گرفته شد. خودکار کردنش
ارزشمندترین کارِ بعدی‌ست. `sqlite3` روی سرور نصب نیست — با `venv/bin/python` و
`sqlite3.Connection.backup()` بکاپ بگیر، نه با `cp`.
بخش‌های تاریخیِ زیر به‌ترتیبِ زمان نگه داشته شده‌اند چون درس‌هایشان هنوز معتبر است؛ بخش‌های تاریخیِ زیر به‌ترتیبِ زمان نگه داشته شده‌اند چون درس‌هایشان هنوز معتبر است؛
**اعدادِ محتوایی‌شان کهنه‌اند و ملاک همین بخش است.** **اعدادِ محتوایی‌شان کهنه‌اند و ملاک همین بخش است.**