- subscription.0016: prices equal to the Bazaar panel (senior), ai_daily_quota 5/3/3, card features; packs on_sale=False (one paid pack order, user 26, keeps its credits; pack paths stay for that order and for admin). - Quota math in docs/ai-cost-and-guardrails.md: ~77% of a Bazaar sale reaches us; worst case (full quota every day) the assistant costs 23/27/35% of it. - Assistant quota message points to the subscription, not packs. 982 tests green. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
121 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Project
Django + DRF backend for Tadrisino, an educational app (Persian/Farsi). Codebase and API responses are in Persian; comments in source are Persian too.
Reviewing this codebase: comments are claims, not evidence
Comments here were mostly written by earlier agents to justify their own change. They
explain intent; they do not prove the code is right. An audit on 2026-09-25 found real
bugs directly under comments calling the same code "safe" or "race-free" (plan swap
at checkout, select_for_update guards that SQLite silently ignores).
- When asked to find problems, review adversarially. Judge by behaviour, not by the comment above it: write a throwaway test or call the endpoint and watch what happens. Ask "how would a cheating student, an attacker, or a confused parent break this?" A finding counts once it is reproduced; say which ones were only read.
- When writing comments, report — don't vouch. Say what the code does and why it was chosen, and name what was not checked. Avoid "this is safe / correct / race-free / the right architecture"; write "guards X; not tested under Y" instead. Record uncertainty plainly so the next reader re-checks rather than trusts.
No silent fallbacks, no speculative backward compatibility
Agents keep adding "helpful" fallbacks (x or default, except: pass, accepting an old
field name "just in case") and they cause the hardest bugs here, because the
failure never shows up anywhere. An audit on 2026-10-03 found and reproduced these:
- With RAG keys missing from
.envandDEBUG=0,/api/ai/askanswered 200 with «این یک پاسخ نمونه (stub)… OPENAI_API_KEY را تنظیم کنید» and still used up the student's daily quota. It now returns 503 (StubNeverReachesProductionTests). - A paid order whose plan had been deleted quietly became 30 days (a yearly buyer got a month). It still activates, but now logs ERROR.
?grade=on/api/homewas an alias "for clients already out there". No such client existed. Removed, and sending it is a 400, not a silent fallback to the user's own grade.resolve_grade_titlehadexcept Exception: pass(now logged);curriculum_ioreadintro_textthat no file ever contains (removed).
On the server since 2026-10-03 12:02 (7307bfc, service really restarted; backup
.deploy-backups/db-before-no-fallbacks-20261003-120205.sqlite3). Checked from outside:
/api/home?grade=8 → 400. The server had RAG configured and no paid order without a plan.
Rules:
- Fail loudly over guessing. A missing setting, row or field in production should
be an error the user and
/api/healthcan see, not a default that looks like success. A default is fine only when it is a real product choice. When you add one, say in the comment why that value is right, not "just in case". - Demo/stub paths only behind
DEBUGorRAG_FORCE_MOCK. Never reachable on the server. - No backward compatibility without a named client. Before keeping an old field, param
or code path, check who sends it (
~/Desktop/MathApp_frontend/src/api/, the server DB). The only API client is that frontend; there is no Android app yet. If nobody sends it, delete it. If you must keep one, write who uses it and when it can go. - Never
except: pass. At leastlogger.exception. Swallowing is acceptable only where the comment names the bigger thing it protects (e.g.affiliate/services.py: commission failure must not undo the user's payment) and it still logs. - Removing a fallback means making the old input fail visibly (400/503), not silently routing it somewhere else.
Fallbacks that remain on purpose (re-check before relying on them): settings boot with
default hosts/CORS/cache/secret key (/api/health warns); LocMemCache without
CACHE_URL; _annotation defaults for answer-cache entries older than a field; Bazaar's
OAuth path beside the Pishkhan token (server still uses OAuth); birthYear alone in
PATCH /api/user/profile (frontend type still allows it); totalQuestions accepted but
ignored (frontend sends it); orders with no plan_id from before 2026-09-25 (none
paid on the server on 2026-10-03); orders without the question_credits/hearts_days/
duration_days snapshot (payment.0011) grant and refund from the plan's current values.
Before any real user: read docs/security-before-production.md
The server is deliberately left in demo mode for the internship presentation: seven
fixed-code demo logins exist, demo users sit in the production DB, and the leaderboard and /admin/ are public (/api/health shows only ok/error without X-Internal-Key; Swagger/ReDoc/schema need an admin session since 2026-10-08). (The token signing key is no longer the
committed one: replaced 2026-10-07, see the status block below.) Each is listed there with its risk, why it was left, and the
fix. Do not treat this deployment as production-ready until every item there is closed,
and when one is closed, move it to that file's «حفاظهایی که هست» table with the test that holds it (the date lives in the commit message; the owner wants docs to describe the current state only).
Commands
source venv/bin/activate
python manage.py runserver
python manage.py migrate
python manage.py test # all tests
python manage.py test accounts # single app
python manage.py test accounts.tests.SomeTestCase.test_something # single test
Local dev requires DJANGO_DEBUG=1 (enables Swagger auth via DEV_MASTER_OTP/devOtp in responses). DEBUG defaults to False. Production also requires DJANGO_SECRET_KEY, DJANGO_ALLOWED_HOSTS (comma-separated), and ideally CACHE_URL (Redis). Without it the app still boots on per-process LocMemCache and /api/health warns — throttles and admin-driven cache invalidation are then per worker. On the server since 2026-10-08 it points at our own Redis (zareian-redis.service, port 6390, localhost, password-protected, 256 MB allkeys-lru, no persistence), never the company's Redis on 6379. Its outage is logged and rate limits stop applying (fail-open); not tested under real Redis failure on the server.
No pytest; uses Django's built-in test runner. No linter/formatter configured.
Docker (docker compose up --build) adds a local Loki+Grafana Alloy+Grafana log stack; not a production setup (still runserver, not gunicorn). See comments in docker-compose.yml, docker/alloy/config.alloy, and LOG_DIR in config/settings.py.
API docs: Swagger /api/docs/, Redoc /api/redoc/, admin /admin/.
Architecture
Apps (each owns its own models, one responsibility):
| app | responsibility | key models |
|---|---|---|
accounts |
identity, sessions, OTP auth | User, Profile, Session, OTPCode |
content |
lessons/curriculum | Grade, Unit, Node, Lesson, Question, Choice, NodeProgress |
progress |
stats, leaderboard, streaks, gamification characters | UserStats, LeaderboardEntry, StreakRecord, UserHearts, Character |
ai_tutor |
AI assistant chat | Conversation, Message, MessageFeedback |
subscription |
plans | Plan, Subscription |
payment |
orders/transactions (Zarinpal gateway + Cafe Bazaar IAP) | Order, Transaction |
discount |
discount codes/offers | DiscountCode, Redemption, Offer, UserOffer |
notifications |
notifications | Notification |
affiliate |
referral program | AffiliateAccount, Conversion |
support |
tickets/bug reports | BugReport, Ticket, TicketMessage |
statuslog |
generic cross-app status-change log | StatusChangeLog |
campaigns |
messaging campaign storage only, no send logic yet | MessageCampaign |
config |
project settings, root urls, middleware | — |
Auth is custom, not Django's. accounts.User is deliberately not AUTH_USER_MODEL — it's an independent business model. Auth is a stateless signed bearer token (django.core.signing, no JWT library) issued/verified in accounts/authentication.py (TadrisinoTokenAuthentication, header Authorization: Bearer <token>). The access token is only a signed user_id (1 h); accounts.Session holds refresh tokens, and POST /api/auth/logout revokes older access tokens via User.tokens_valid_after. Missing/invalid header → anonymous request, not a hard failure; each view's permission class decides if auth is required.
Apps are decoupled by convention: instead of real FKs across app boundaries, code uses loose refs (user_id, object_id + label) so any app can be split out later. Don't add cross-app FKs — follow the existing loose-reference pattern.
Throttling: DRF ScopedRateThrottle with per-endpoint throttle_scope strings, rates centralized in REST_FRAMEWORK['DEFAULT_THROTTLE_RATES'] in config/settings.py. Requests without a login are throttled by IP (NUM_PROXIES setting controls how many XFF hops are trusted — must match real proxy count in prod or IP spoofing/throttle-bypass is possible). Prod is ArvanCloud → nginx → gunicorn, so the default is 2; it was 1 until 2026-09-24, which made every visitor share ArvanCloud's IP bucket. /api/health returns clientIp — it should be your own public IP. Adding a new sensitive endpoint means adding both a throttle_scope on the view and a matching rate entry in settings.
POST /api/ai/ask is real RAG (Qdrant retrieval filtered by chapter → GapGPT generation) whenever OPENAI_API_KEY and QDRANT_API_KEY are set, which they are on the server since 2026-09-23; without them it falls back to a deterministic stub. Entry point is generate_tutor_answer in ai_tutor/views.py; usedChunks in the response names the book chunks the answer was grounded on.
Structured logging: config.middleware.APIRequestLoggingMiddleware (last in MIDDLEWARE, closest to the view) logs every /api/ request as JSON (method/path/status/duration/user, no body/headers/secrets) via config/log_formatters.py.
Postgres is the server's DB since 2026-10-08 (own cluster 14/zareian, port 5440, localhost only; DB zareian_mathapp, role zareian_app; selected by DB_NAME/DB_USER/DB_PASSWORD/DB_HOST/DB_PORT in .env; passwords in ~/mathapp-v2/.pg_password, .redis_password). It is separate from the company's main cluster on 5432 — never touch that. Without DB_NAME the app falls back to SQLite (local dev, tests); both run the full suite green. Data was moved with dumpdata/loaddata; model-count comparison matched. Guards that relied on SQLite's IMMEDIATE now rely on real row locks; concurrency tests pass on Postgres. Connections come from a per-worker pool (OPTIONS.pool, DB_POOL_MAX_SIZE 8; CONN_MAX_AGE is gone and DB_CONN_MAX_AGE in .env stops startup): Postgres here has max_connections=60, and the old one-connection-per-thread setup (3 workers × 32 threads) turned 21% of requests into 500s at 100 concurrent users in a local load test against a 60-connection Postgres; with the pool 0 errors. A full pool gives 503 + Retry-After (config/db.py), not 500; /api/health → dbPool checks workers × pool + 6 ≤ max_connections. The AI tutor and the OTP SMS call hand their connection back before the network wait (release_db_connection). Worker count lives in gunicorn.conf.py (4; the systemd unit no longer passes --workers). The AI tutor has its own slice of threads (AI_MAX_CONCURRENT_PER_WORKER, 12 of 32, ai_tutor/concurrency.py): beyond that /api/ai/ask answers 503 Retry-After: 10 before reserving quota, so a chat spike cannot starve the map and quizzes. The frontend spreads retries over 1–2× Retry-After. Not load-tested on the server itself. deploy.sh backs up with pg_dump -Fc before migrate; the off-site live copy is in «بکاپ: کپیِ زنده» below. Operator guide: ~/Desktop/گزارشات Tadrisino/گزارش-۲۹/راهنمای-Postgres-و-Redis.html. The pre-move db.sqlite3 and migration dumps were deleted from the server; no SQLite file is left there.
SMS goes through Kavenegar (KAVENEGAR_API_KEY/KAVENEGAR_SENDER); failures return explicit 503, not a fake success.
Gamification characters (progress.Character, Duolingo-mascot-style): an admin-managed catalog (like subscription.Plan — created via Django admin or seed_test_data, not by users). Unlocking is currently just UserStats.xp >= Character.unlock_xp (no separate ownership table); UserStats.selected_character_id is the user's active pick, set via POST /api/characters/select after that check. First real content beyond the model/plumbing (art assets, more characters, purchase-with-coins, etc.) is expected to land incrementally later.
Payment (Zarinpal): create_order/CreateOrderView never call Zarinpal directly — paymentUrl points at our own GET /api/payment/orders/{id}/pay (PaymentRedirectView), which calls Zarinpal's PaymentRequest API on first visit (storing Order.gateway_authority) and redirects to StartPay; this keeps order creation from depending on Zarinpal's uptime. Zarinpal redirects the user back to GET /api/payment/zarinpal/callback (ZarinpalCallbackView), which always re-verifies server-to-server via payment/zarinpal.py::verify_payment before marking an order paid — a forged Status=OK in the query string alone does nothing. Both payment/zarinpal.py and payment/bazaar.py fail closed (503 / invalid) when their credentials aren't configured, never a fake success.
Where this is heading (the owner's intent)
The owner writes this backend; another developer writes the frontend; a third person
administers the server. None of them share a machine, and until recently the two halves
were built in parallel without a shared contract — which is why reconciling them cost
rework. The goal is not to merge the repos. Two repos, one API contract is the
intended shape, and the contract lives in this repo's OpenAPI schema (/api/docs/), not
in anyone's head.
What the owner is steering toward, in rough order:
- Both halves running against each other, on real content, with nobody hand-editing files on the server. Most of the plumbing for this now exists.
- Nothing manual in a release. Adding a dependency, a setting or a service must not
add a step someone has to remember. When a change would, fold it into
deploy.shor make startup fail loudly and legibly instead. - Content editable without a developer — questions, curriculum, plan copy, all from the admin panel. A management command run over SSH is a last resort, not a workflow.
- Answer quality measurable, not assumed: per-question reports, wrong-answer rates, prompt-version feedback. The senior's stated priority is proving the AI actually teaches, especially where students get stuck.
- Scale without drama — thousands of concurrent users should be a non-event. Prefer the boring fix (an index, a cache, a cheap guard) over new infrastructure.
The owner is early in their career and learning the collaboration workflow as they go. Explain the why alongside the what, flag when something is a product decision rather than a coding one, and never assume a step was done just because it was described.
Frontend ↔ backend (read before wiring the two together)
The app is two repos and two domains: the React frontend Tadrisino/MathApp_frontend
(deployed at math.tadrisino2.ir) and this backend (mathapi.tadrisino2.ir; it was
api.math.tadrisino2.ir until 2026-09-26, whose DNS record no longer exists).
Since 2026-09-26 the owner maintains both: the frontend developer is on leave and
AliReza_Zareian has write on the frontend repo (branch protection on its main
unknown — not visible without admin). Local clone: ~/Desktop/MathApp_frontend.
State on 2026-09-27: the rebuilt frontend (typed, modular; its README is the map) is
on main and live. It is served by nginx from ~/mathapp_frontend_new/dist on the
same shared box; release with cd ~/mathapp_frontend_new && ./deploy.sh (fetch, npm ci,
type-check, build into dist.next, swap; previous build kept in dist.prev). The owner
authorized pushing the frontend straight to main and deploying it without asking.
Verified live with the demo accounts (guest 6, student 10, parent 15): all screens load
their data. Real SMS delivery and a real Zarinpal payment are still unverified.
Domains are configuration, not code. Backend: only PUBLIC_BASE_URL and
FRONTEND_URL; hosts, CSRF, CORS, Zarinpal callback/return, Bazaar redirect and the
deploy.sh health URL derive from them (config/tests_domains.py guards it). Frontend:
dist/config.js at runtime, else VITE_API_BASE_URL from .env.production. A domain
move is expected (Zarinpal -14); the procedure is in docs/operations.md
(«عوض کردنِ دامنه»).
Never hand-write the client from memory. Three of fifteen calls were silently wrong
the first time, and the rebuilt client found two more the old one had (POST instead
of PATCH on notifications, a dropped guest refresh token). Read the contract from
python manage.py spectacular --file schema.yaml --format openapi and check shapes
against a running server. content/tests_frontend_contract.py is the backend-side guard.
Android app (WebView shell in the frontend repo, android/): the installed APK lags the code. What the
next build must carry (vibrate bridge, in-app Zarinpal with paysInApp(), media-gesture setting), what to
check on a phone, and who owns signing (MH-W90) are in ~/Desktop/MathApp_frontend/android/README.md
(«نسخهی بعدیِ APK»). Until then payment from the app goes through the frontend's pay.html, because
Zarinpal refuses StartPay without our domain as Referer (checked live 2026-10-10).
Content the frontend shows but nobody has filled in yet: Unit.blurb,
Plan.features/badge, Lesson.example/steps. All optional, all editable from the
admin panel.
Operations
The whole release, as one line:
ssh -t administrator@78.38.35.249 'cd ~/mathapp-v2 && ./deploy.sh; sudo systemctl restart zareian-mathapp'
It asks for the password twice — once for ssh, once for sudo. Wait for each prompt; pasting several lines at once feeds the queued text to the password prompt and locks you out for three tries.
The trailing restart is separate because sudo inside deploy.sh cannot prompt for a
password. The script no longer pretends that worked: it compares the service's start time
before and after and exits non-zero when the restart did not happen, so a half-finished
deploy can never again look like a successful one. Giving administrator a NOPASSWD
sudoers line for just systemctl restart zareian-mathapp would collapse this to a single
command; that is the server admin's call, not ours.
./deploy.sh is the whole release: fetch, reset to origin/<branch>, install, migrate,
seed content, collectstatic, restart, then it polls /api/health and prints the likely
cause if the service does not come back.
It repairs the server rather than stopping at it. A hand-edited file used to make
git pull --ff-only fail, which aborted the whole script — and because every later step
was skipped, the server silently stayed on old code. Now local modifications (and local
commits) are saved to a timestamped patch under .deploy-backups/ and then discarded with
git reset --hard origin/<branch>, and the deploy continues. Whatever state the server is
left in, one run puts it back on exactly what Gitea has.
git clean is never run there and must never be added. db.sqlite3, .env and
.django_secret_key are untracked and live in that directory; reset --hard leaves them
alone, clean -fd would destroy the database, the config and every signed-in session.
Settings boot with working defaults (hosts, CSRF, CORS, secret key, cache), so a stale or
partial .env on the server no longer breaks startup — env values are merged with
those defaults, not replaced by them.
GET /api/health is the outside view: database, unapplied migrations, cache, content
counts, security warnings, and recent traffic. It is how you check whether anyone has
actually connected without asking a human.
Content: where it lives and how it gets to a server
content/seed/ holds the whole curriculum and question bank as committed JSON —
curriculum.json (8 grades, 58 units, 263 nodes, 2,617 lessons) and questions.json
(2,463 multiple-choice questions). deploy.sh runs seed_content on every release, so a
fresh server fills itself with one command: no Qdrant, no AI credit, no SSH session.
seed_content is idempotent and never deletes, and since 2026-10-03 it does not undo admin work: a SeedRecord ledger (content/seed_sync.py) remembers what was delivered, so anything the admin later deleted, renamed or re-worded is not recreated, and lesson intro_text is refreshed from the file only if nobody edited it. New rows in the files still arrive; an empty DB ignores the ledger. Questions bind to lessons by title path
(grade › unit › node › lesson), never by primary key — ids differ between any two
databases, and binding by id silently attaches questions to the wrong lesson.
seed_content --export writes the files back from the local DB. It refuses to overwrite a
non-empty questions.json from a DB that has no questions (that used to wipe the file
silently); use --export --only curriculum when you only mean the curriculum.
Answer quality: the verification layer
Books are read by a vision model from page images, so extraction errors enter there — a misread digit, a dropped decimal separator, a solution attached to the neighbouring problem. These are invisible in retrieval and only surface when something trusts the text.
rag_verify_solutions re-solves every extracted problem independently and compares.
LessonChunk.solution_verdict is three-state — agrees / disagrees / unjudgeable —
and the verdict also rides in the Qdrant payload under a key retrieval._payload_text
does not read, so the tutor's context is unchanged. Current state (2026-09-22, across
all 3,517 chunks): 2,466 agree, 354 disagree, 508 unjudgeable, 189 not yet checked —
those 189 are the other team's علوم/مطالعات collections, never run through the checker.
Question generation only ever reads agrees chunks.
Two things that were learned the expensive way and are already encoded in the prompts:
- A boolean verdict is wrong. With two states every "I can't tell" becomes "wrong", which reported ~3× the real rate. Read samples before trusting any bulk run.
۳/۱۰۴in these books is the decimal 3.104, not the fraction 3÷104.
solution itself is never rewritten — the extracted text stays as the record, and the
independent answer is stored separately in checked_answer.
The figure gap (largest open data issue)
508 chunks (~14%) reference a figure or table that is not in the text, so they cannot be
answered or turned into questions. The vision model saw the figure; it was simply never
asked to write it down. rag/services/pdf.py now carries a self-contained rule telling
it to inline measurements and labels into the problem text, and re-reading the five math
books under that rule is what brought this down from 881 (42%). What is left is
concentrated in the three books that predate the rule (سوم/ششم/هفتم, the other team's);
recovering them means re-ingesting those pages ($2.7 of API credit).
Quiz assembly
The app plays at node level (the island on the map) but questions hang off lessons,
and a lesson here is the finest grain — one problem type from the book, not a class
session. GET /api/nodes/{id}/quiz assembles a quiz from a node's lessons, at most one
question per lesson. Shuffle is seeded on nodeId:userId, so a refresh returns the same
quiz; questionCount is reported separately from requested so a thin node cannot look
like an error. 224 of 263 nodes hold a full four-question quiz, and every node in
grades 8–12 does; the 39 that fall short are all in grades 3/6/7.
Finishing the quiz finishes the island: POST /api/nodes/{id}/quiz/complete marks the node
completed whatever the score (the owner's call on 2026-09-24: keep it simple) and returns
nextNodeId. Since 2026-10-07 13:42 (live, 7ecb337) it refuses a node
that is locked on the map (403 node_locked, same rule as /api/home, content.views.node_is_unlocked),
and a user who has not answered every quiz question (409 answers_missing;
the required count is max(client totalQuestions, min(4, questions in node)), so the client cannot
understate it). A node or lesson with no questions has nothing to answer: it passes when unlocked (no stars, no XP) so it does not block the chain, and is still refused when locked. POST /api/lessons/{id}/complete has the same lock and answer rules. Before that,
only a user with zero hearts and no answers was refused; anyone else finished any island, locked or not. Verified by 860 local tests and by outside probes (health, 401s, throttle); not verified by a real login on the server, so do one manual quiz after the next deploy. Before it existed, a node only completed when every one of its 8–13 lessons
was completed one by one, so a student playing node quizzes was stuck after the first
island of every unit. Details in docs/architecture.md.
AI provider notes
Measured prices live in docs/model-prices.md — a ledger of what GapGPT actually billed per
model, with dates and evidence. When a price changes, add a row there; never overwrite the old one.
OPENAI_BASE_URL points at GapGPT (an OpenAI proxy reachable from Iran) and it exposes
~127 models, including reasoning ones. Two failures look alike and must not be treated
alike — is_account_limit() in rag_verify_solutions is the shared guard:
429 initial_usage_limit_exceeded— account cap, hours long. Retrying extends it.403 insufficient user quota— credit exhausted. Note the spaces: an earlier guard matchedinsufficient_quotawith an underscore, missed it, and burned 770 doomed calls.
/dashboard/billing/usage returns cents and is the only reliable balance signal; the
hard_limit_usd in /dashboard/billing/subscription is a placeholder, not a balance.
Credit was topped up again by 2026-09-25 afternoon (live tutor answered, aiProvider: ok); it has run dry twice before, so probe before any bulk run. Bulk work since then runs on Claude Code subagents
instead. Sonnet handles question authoring well (~88% yield); Haiku does not — it cannot
reliably pull the final answer out of a solution and produced ~40% broken output.
Known gaps (per README pre-deploy checklist)
- Postgres is live and streamed live to the backup server (see «بکاپ» below); restoring onto 249 itself never rehearsed.
- Throttle rates need periodic review against real traffic.
DEV_MASTER_OTP(0000) must stay gated behindDEBUG— never enabled in prod.- No video content exists yet, though
Lesson.kind/video_urlcan express it. - Grades 8–12 are fully covered: every node holds a four-question quiz. 39 of the 145 nodes in grades 3/6/7 (the other team's books) still hold fewer than four; none is empty, so every node still plays.
- Kavenegar is live on the server since 2026-09-25 and delivers since 2026-09-27: OTP
goes through the approved template
SMSVerify(verify/lookup,KAVENEGAR_OTP_TEMPLATEin the server.env), and a real code reached the owner's phone (message894247231, status 10). Every OTP costs real credit. Plainsms/sendfrom this account's line is cancelled (status 13), so never unset the template. AnOTPCoderow made in the admin (/admin/accounts/otpcode/add/) still hands a tester a login without SMS.
The server: read this before running anything on it
mathapi.tadrisino2.ir runs on a shared company machine (administrator@78.38.35.249,
hostname haghzad), not a box that belongs to this project. Our app is one tenant among
many. Also on it, all in production use by other people:
- Gitea itself (
gitea.service) — the company's git server, including this repo's origin. Breaking it means nobody can push or pull anything, anywhere. - Mail (
postfix,dovecot), DNS (named),mariadb,postgresql@14,rabbitmq,metabase,redis-server,nginx. - Other apps:
apirag,artinafront,metaverse,celerybeat, and a genericgunicorn.servicethat is not ours. - Other people's home directories:
aria,artina,behdad,zahra,git,gitea. They are mode0750— unreadable asadministrator, trivially readable undersudo. Do not read them. Nothing in this project needs them.
Our account is administrator, and sudo -l reports (ALL : ALL) ALL. It is also in
the docker and lxd groups, each of which is root by another route. There is no
technical guard rail between a careless command and the whole company's data. The only
guard rail is the operator.
Ours is exactly one directory: /home/administrator/mathapp-v2, served by
gunicorn-mathapp.service (unix socket, proxied by nginx). Note ~/mathapp also
exists, is not a git repo, and is not what runs — do not touch it.
The same unit also answers to zareian-mathapp (a symlink alias in
/etc/systemd/system/, added 2026-09-18). Prefer that name in anything a human will type
or copy: gunicorn alone is a different team's service on this box, and tadrisino is
too generic for a machine where other people's projects may claim it later.
sudo systemctl restart zareian-mathapp # ours, unambiguous
sudo systemctl restart gunicorn # NOT ours — someone else's app
Rules that follow from this, and are not negotiable:
- Never run a destructive command outside
~/mathapp-v2. Norm -rf, nogit clean, nochown -R, no dropping databases, anywhere else. When a command takes a path, write the path out in full rather than relying on the current directory. - Restart only
gunicorn-mathapp.systemctl restart gunicornis a different team's app.deploy.shno longer guesses that name, and neither should you. - Never delete the project directory or "re-clone fresh".
db.sqlite3,.envand.django_secret_keylive inside it and are all gitignored. A fresh clone silently destroys the database, the config and every logged-in session. Usegit reset --hard origin/mainwhen the tree needs to go back to a known state. - Never
git cleanthere for the same reason. - Read
.envonly through Django.python -cprintingsettings.CACHES[...]and booleans is fine;cat .envis not, and neither is pasting its values into a chat. - Take a timestamped backup before editing anything under
/etc, and say where you put it. - Disk is the standing risk. It has been sitting at ~95% full (about 6 GB free on a
113 GB volume). Check
df -h /before anything that writes a lot — a full disk on this machine takes the mail server and Gitea down with us, not just our app. - Touch only our own files and our own service. Do not connect to, write into or "just
use" anything another team runs on this box — their Redis (even an empty database),
databases, queues, nginx or firewall rules. Those are requests to the server admin
(listed in the launch report), not things to do ourselves. Owner's rule, 2026-10-07, after
an agent pointed
CACHE_URLat the shared Redis.
Handling the server password (every agent, every time)
Access is a password for the shared administrator account, not a key. Handle it exactly
like this — the steps exist for reasons, not ceremony:
printf '%s' '<password>' > "$SCRATCH/.pw" # scratchpad, never the repo
chmod 600 "$SCRATCH/.pw"
sshpass -f "$SCRATCH/.pw" ssh administrator@78.38.35.249 '<command>'
ssh administrator@78.38.35.249 'sudo -S -p "" <command>' < "$SCRATCH/.pw"
shred -u "$SCRATCH/.pw" # immediately after, not "later"
- Never put the password in the command line.
pson this box shows every process's full argv to every logged-in user, and several other teams have accounts here. - Never write it into the repo, a memory file, a commit, or a note. The scratchpad only, and only for as long as the commands take.
shred -uas soon as the work is done, not at the end of the session. Recreate it if more work comes; that costs nothing.- Never echo it back into the conversation, even partially, and never paste
.envvalues either. - Do not create SSH keys, edit
sshd_config, or change the password unless the user asks for that specifically. It is a shared account; changing it locks the admin out.
The password reaches an agent only because the user supplies it in that session. If it is
not in the conversation, it is not available — say so and ask, rather than hunting for it
on disk. If the user wants to stop pasting it into chat, the macOS Keychain is the local
fix (security add-generic-password -s tadrisino-server -a administrator -w, then read it
back with security find-generic-password -s tadrisino-server -w), and an SSH key is the
real one.
DEBUG must never be 1 on this server: it turns on DEV_MASTER_OTP (0000), which
accepts any phone number as a valid login, and renders tracebacks with settings in them.
DJANGO_DEBUG belongs in .env only — not in the systemd unit, where it once sat as
Environment="DJANGO_DEBUG=1" and was saved only by .env happening to override it.
DJANGO_SECRET_KEY is what signs our bearer tokens. It must be a real random value that
exists nowhere in git. If the env var is simply absent and DEBUG=0, config/settings.py
generates a strong one and stores it in .django_secret_key (mode 0600) — that is the
preferred setup, because there is then no key to leak. Never let the server run on the
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.
کافهبازار: اشتراک، تخفیفِ پویا، تمدیدِ خودکار — روی سرور از ۲۰۲۶-۱۰-۱۲ ۱۷:۵۴ (بکاند 8077ab4، فرانت fffdeac)
- محصولهای پیشخان (پرسشِ API با توکنِ ساختگی، ۱۰۰۰۰–۱۰۶۹۹): فقط
10342(یکماهه، تأییدِ سینیور)،10343(۹۰ روزه)،10344(۱۸۰ روزه)، زیرِcom.tadrisino.math.app؛ هر سه روی پلنها (subscription.0014،0015). بستهها دیگر فروخته نمیشوند. اتصالِ سرور به بازار از راهِ OAuthِ حسابِ شرکت است (کلیدِ پیشخان نداریم). - روشهای پرداخت در
payment/providers.py(Strategy + factory)؛ تأییدِ خودِ پرداخت (کالبک/verify) عمداً بیرونش. - فقط اشتراک (
subscription.0016، سینیور): یک/سه/ششماهه ۵۹۸/۸۹۸/۱٬۳۹۸ هزار تومان (= پیشخانِ بازار به گفتهی سینیور)، سقفِ دستیار ۵/۳/۳ در روز؛ حساب درdocs/ai-cost-and-guardrails.md«اشتراکها». بستههاon_sale=False(کاربرِ ۲۶ یک بستهی پرداختشده و ۲۰ سوال اعتبار دارد). هر کارتِ خرید مزیتها و سقفِ دستیار را خودش میگوید. - مسیر و تمدیدِ خودکار:
docs/operations.md«فروش در کافهبازار».BAZAAR_DYNAMIC_PRICE_KEYدر.envسرور است (بکاپ:.deploy-backups/env-before-bazaar-dynamic-20261012-175336)؛ توکن فقط وقتی کدِ تخفیف قیمت را کم کرده میرود. - فرانت: دو کاشیِ «کارتِ بانکی» (پیشفرض) و «کافهبازار» همیشه کنارِ هم؛ بازار جایی که ممکن نیست غیرفعال با دلیل.
داخلِ اپ قیمتِ پیشخان از Poolakey (
bazaarPrices) روی کاشیِ بازار. پلِbazaarBuy؛ کلیدِ RSA وversionCode۲ درandroid/.compileDebugKotlinسبز؛ APK ساخته نشده. - سنجیده: ۹۷۹ تست؛ روی سایتِ زنده با دموی ۱۴ و پلِ اندرویدِ ساختگی در مرورگر: کاشیها، و سرور SKUِ
10343/«subscription» را به اپ داد (سفارشِ ۱۴ بعد لغو شد). نسنجیده: هیچ خرید، JWT یا تمدیدِ واقعی در بازار؛ برابریِ قیمتِ پیشخان با ادمین.
جزیرههای تمرینِ تازه و شکل در سوال — ۲۰۲۶-۱۰-۱۲ (عصر)
- ۱۶۰ جزیرهی «تمرین: …» آخرِ فصلهای پایههای ۳ تا ۱۲، با ۱۳٬۶۸۹ سوالِ تازه (کلِ
questions.json۱۸٬۵۲۲). هر جزیره ۳ تا ۶ درس دارد و هر درس تا ۳۰ سوال. سوالها با برنامههای کوتاهِ تولید ساخته شدند که جوابِ هر سوال را محاسبه میکنند (نه حفظ)، با گزینهی غلطِ از جنسِ خطای رایج (علامت، فراموشیِ مشتقِ داخل، …)؛ بخشی هم بانکِ مفهومیِ دستنویس است. برنامهها در ریپو نیستند؛ فایلهای seed خودشان منبعند و ویرایش از ادمین/JSON است. سنجیده: ۹۵۸ تست سبز، چشمیِ نمونهی هر خانواده، و ۳۶ شکلِ نمونه در مرورگر. نسنجیده: هر سوال با چشمِ معلم؛ قالبِ سوالها در هر خانواده یکی است (عددها عوض میشوند)، پس تنوعِ مفهومی کمتر از سوالِ دستنویس است. Question.figure(content.0019): همان قالبِLesson.figures[].diagram، باrag.services.diagram.validateدر ورودِ فایل اعتبارسنجی میشود (شکلِ نامعتبر فایل را رد میکند، نه بیصدا حذف)./api/nodes/{id}/quizو/api/lessons/{id}/questionsآن را برمیگردانند؛ فرانت (QuizScreen) زیرِ صورتِ سوال میکشد.- آزمونِ هر جزیره ثابت است (تصمیمِ مالک ۲۰۲۶-۱۰-۱۲): مبنای درهمریزی
nodeId:userIdاست و هیچچیزِ دیگری (شمارِ دفعات، زمان) در آن نیست؛ دانشآموز هر بار همان سوالها را میبیند.content/tests_node_completion.py::NodeQuizStabilityTests. - هیچ LaTeX/markdownی به دانشآموز نمیرسد (تصمیمِ مالک ۲۰۲۶-۱۰-۱۲): چهار لایه، هر چهار با یک تبدیل: (۱)
content/plain_math.py::convertروی seed و روی ورودِ سوال درquestion_io؛ (۲) migrationcontent.0021روی سوال/گزینه/درس/پیامهای دستیارِ دیتابیس (و حذفِ سوالِ تکراریِ بعد از تبدیل)؛ (۳)rag.services.mathtext.to_plainروی پاسخِ زنده و تاریخچهی دستیار (ai_tutor/views.py)؛ (۴)MathApp_frontend/src/lib/plainMath.tsدرMathTextبهعنوانِ آخرین تور (دو پیادهسازی باید همرفتار بمانند). توانِ پیچیده بهصورتِ «a به توانِ (…)» نوشته میشود، نه^. تست:content/tests_plain_math.py(seed بدونِ نشانه)،ai_tutor/tests_plain_history.py. - بازخوردِ دانشآموز (
content.0023): زیرِ هر سوالِ آزمون و هر درس سه دکمه است: 👍 «خوب بود»، 👎 «خوب نبود»، «گزارشِ ایراد» (MathApp_frontend/src/components/FeedbackBar.tsx).POST /api/questions/{id}/voteو/api/lessons/{id}/vote(value۱/−۱/۰)،POST /api/lessons/{id}/report(جدیدِ درس) و…/questions/{id}/report(از قبل). ادمین: ستونِ 👍/👎 در فهرستِ سوالها و درسها، صفِ «گزارشهای سوال» و «گزارشهای درس». رأی هر کاربر روی هر هدف یکی و تغییرپذیر است (ContentVote). - اصلاحِ جزیرههای تمرین از فایل:
seed_sync._refresh_practiceگزینههای سوالِ موجودِ «تمرین: …» را در جا (همان idها) با فایل یکی میکند و سوالِ تحویلشدهی حذفشده از فایل را اگر جوابی ندارد پاک میکند؛ سوالِ دستیِ ادمین هرگز. - بازبینیِ خودکارِ سریع (۲۰۲۶-۱۰-۱۲): بازحسابِ مستقلِ ۳۴۲ سوالِ حسابِ ساده برابرِ کلید بود؛ ۱۲ سوالِ گزینهی همارزِ جواب (۰/۳ و ۰) و ۵ درسِ با سوگیریِ «جوابِ درست بلندترین گزینه» پیدا و اصلاح شد. این جایگزینِ بازبینیِ معلم نیست.
- جزیرههای تمرین آخرِ هر فصلاند و قفلِ ترتیبی دارند؛ فصلِ اولِ هر کتاب رایگان است، پس تمرینهایش هم رایگاناند.
کتابها (Subject)، جزیرههای بیشتر و شکلِ درسهای کتابهای تازه — ۲۰۲۶-۱۰-۱۲
Subject(content.0014..0018): ریاضی، حسابان، هندسه، آمار و احتمال، گسسته؛Unit.subjectاجباری است./api/home?subject=<key>(کتابِ بیربط ۴۰۰)؛subjectsدر پاسخ فقط کتابهای همان پایه است (دهم: ریاضی و هندسه؛ یازدهم: حسابان، هندسه، آمار؛ دوازدهم: حسابان، هندسه، گسسته) و نوارِ فرانت (SubjectBar) فقط وقتی بیش از یکی باشد میآید. فصلِ رایگان = اولین فصلِ هر کتاب. فایلهای seed حالاsubjectدارند و بیآن رد میشوند.- پنج کتابِ تازه (هندسه ۱۰/۱۱/۱۲، آمار ۱۱، گسسته ۱۲) روی Qdrant خوانده شدهاند. ۲۰۲ جزیرهی تازه با نامِ دستی، ۱۱۳۰ + ۲۰ سوالِ نوشتهی خودم (حلِ مستقل، نه از کلیدِ کتاب)، و مثال/قدم برای همهی درسهای هندسه (و برای درسهای آمار/گسستهای که شکل دارند).
- شکلها را دستی نوشتم، نه با API.
gen_lesson_figuresروی هندسه حدود نیمی غلط داد و کنار گذاشته شد (بازبینیِ چشمی). حالاLesson.figuresبرای ۵۰۳ درسِ هندسه (از ۵۸۹)، ۸۲ درسِ آمار و ۶۸ درسِ گسسته با مشخصهیrag.services.diagramنوشته و باrag.services.diagram.validateاعتبارسنجی شده؛ هر شکل با Chrome رندر و با چشم دیده شد (دو دور روی نمونهی تصادفی). ندارند: درسهای جبریِ ماتریسها، اثباتهای جبری، چند درسِ آمار/گسستهی مفهومی. جدولهای درستی باkind: tableکشیده شدهاند؛ ترتیبِ ستونها در پیشنمایشِ من راستبهچپ بود (به نمایشِ زنده نگاه کن). - تقسیمِ جزیرهها (
content/node_split.py،0018): جزیرهی بیش از ۷ درس به قسمتهای ≤۶ درسی شکسته میشود؛ شناسهی درس و پیشرفتِ کاربر حفظ میشود. نامِ قسمتها در کتابهای قدیمی «عنوان (۲)…» است و دستی نامگذاری نشده. gen_lesson_figuresحالا--subjectدارد و خطای شبکه یک درس را رد میکند (کلِ اجرا نمیافتد). نکته:--dry-runهم API را صدا میزند.- روی سرور از ۲۰۲۶-۱۰-۱۲ ۰۵:۳۷ (بکاند
8cb1ecb، سرویس واقعاً ریاستارت شد؛ فرانتf569ece). بکاپِ پیش از migrate:.deploy-backups/db-before-deploy-20261012-053603.pgdump.content.0014–0018اعمال شد (۲۷۸ جزیره شکسته، ۴۶۴ جزیرهی تازه، ۲٬۲۳۷ درس جابهجا، ۲۴ تکمیل کپی)،seed_content: ۲۰۲ جزیره، ۱۱۱۰ درس، ۱۱۳۰ سوالِ تازه. با کاربرِ آزمایشیِ پایههای ۱۰ و ۱۱ و ۱۲ بررسی شد: نوارِ کتابها (۲/۳/۳ دکمه)، خانه و درسها همگام، درسِ هندسه با شکل در مرورگر، آزمونِ جزیره (۴ سوال، پایان ← جزیرهی بعد)،?subject=نامعتبر ۴۰۰، و دستیار روی هندسه ۱۰ و آمار ۱۱ از chunkهای همان کتاب جواب داد. این آزمونها روی دموها اثر گذاشت: کاربر ۱۲ جزیرهی اول هندسه را تمام کرد و کاربر ۱۳ دو سوالِ دستیار پرسید. - بعداً در خودِ اپ دیده شد (۲۰۲۶-۱۰-۱۲): جدولِ درستی چپبهراست و درست رندر میشود (پیشنمایشِ بیCSS گمراهکننده بود)،
و برچسبِ جزیره در نقشه حالا تا ۳ خط میرود (
IslandPath.tsx، فرانت18eeb54) تا پسوندِ «(۲)» بریده نشود. - حسابهای آزمونگر (۲۰۲۶-۱۰-۱۲):
UNLOCK_ALL_PHONESدر.envسرور (09387731214،09211643376= کاربرهای ۳۵ و ۳۴؛ بکاپِ.env:.deploy-backups/env-before-unlockall-20261012) فقط قفلِ ترتیبِ جزیرهها را برمیدارد (user_has_all_nodes_unlocked). اشتراکِ فصلها را یک ردیفِSubscription(پلنِ یکساله تا ۲۰۲۸-۱۲-۳۱) میدهد، که قلب و سقفِ دستیار را هم عادی میکند. روی سرور باHomeViewبرای هر دو سنجیده شد: صفر جزیرهی قفل و صفر فصلِ اشتراکی در هر سه کتابِ دوازدهم. - سوالِ جزیرهها: چهار جزیرهی تقسیمشدهی پایههای ۳ تا ۷ خالی بودند و سوال گرفتند؛ آزمونِ
test_every_topic_has_at_least_one_questionنگهشان میدارد.
ظرفیت زیرِ بار — روی سرور از ۲۰۲۶-۱۰-۱۱ (بکاند 98bf05f، فرانت 16cf5c9)
گزارشِ سینیور: ~/Desktop/گزارشات Tadrisino/گزارش-۳۳/ظرفیتِ-اپ-زیرِ-بار.html (و PDF). بکاپِ تغییرهای یکبارهی
سرور: .deploy-backups/env-before-db-pool-20261011-202211 (حذفِ DB_CONN_MAX_AGE) و
unit-gunicorn-mathapp-before-db-pool-20261011-202211.service (حذفِ --workers 3؛ تعداد حالا در gunicorn.conf.py).
- سنجیده: لپتاپ با Postgresِ ۱۴ و
max_connections=60(مثلِ سرور)، ۱۰۰ کاربرِ همزمان: پیش از pool ۲۱٪ خطای ۵۰۰، بعد ۰ خطا و ۷۱۴ درخواست در ثانیه (۴ worker). روی خودِ سرور، نسخهی موقتِ ۲ worker، ۴۰ کاربر، ۱۵ ثانیه: ۰ خطا، ۱۱۲ درخواست در ثانیه، p99 ۰٫۶۷ ثانیه، وقتی CPUِ ماشین ~۸۰٪ مشغول بود. ثبتِ جواب ۱۵٫۴→۱۱ ms،/api/home۱۲٫۴→۸٫۶ ms. - برآورد (نه سنجیده): ۱۰۰۰ تا ۱۵۰۰ بازیکنِ همزمان (فرض: یک درخواست در ۸ ثانیه)، ۴ تا ۶ هزار آنلاین. سقفِ واقعی CPUِ مشترک است، نه حافظه یا دیتابیس؛ worker بیشتر روی همین ماشین ظرفیت نمیسازد، CPUِ دیگران را میگیرد.
- سقفهای تنظیمی: پیامک ۳۰۰ در روز برای کلِ سامانه و دستیار ۳۰۰۰ سوال در روز — هر دو برای روزِ لانچ کماند و
تصمیمِ مالک/سینیور است (
OTP_SMS_GLOBAL_DAILY_MAX،AI_GLOBAL_DAILY_MAXدر.env؛ هیچکدام الان در.envنیست). هر IP: ۱۰۰ درخواستِ کد و ۳۰۰ ثبتِ کد در ساعت (تصمیمِ مالک، برای کلاس روی یک وایفای). - پیشنهادشده، ساخته نشده: جلسهی آزمون به سبکِ دولینگو (سوال و کلید با هم دانلود، سنجش روی گوشی، ارسالِ یکجا و سنجشِ دوباره در سرور) بارِ بازی را حدوداً نصف میکند؛ کلید روی گوشی میرود، پس تصمیمِ محصولی است. کلِ محتوا فشرده ≈ ۱ مگابایت است.
- نسنجیده: GapGPT با ۴۸ سوالِ همزمان، کاوهنگار زیرِ رگبار، رفتارِ ArvanCloud با صدها نفر از یک IP.
تستِ بار روی سرور (ماشینِ مشترک، با احتیاط): throttleهای پروداکشن دست نمیخورند؛ یک gunicornِ موقت روی سوکتِ جدا
در ~/mathapp-v2/.lt/ با READ_THROTTLE_RATE بالا و DB_POOL_MAX_SIZE=4 --workers 2 (تا با ۳۲ اتصالِ پروداکشن از ۵۷
نگذرد)، فقط GET با یک حسابِ تستی، ۱۵ ثانیه، و آخرش پاک کردنِ .lt. در اسکریپت wait نزن: منتظرِ همان gunicornِ پسزمینه
میماند و تا ابد گیر میکند (یک بار شد). روی لپتاپ: ab در همزمانیِ بالا خودش خراب میشود و macOS صفِ اتصال را ۱۲۸ و
پورتهای کلاینت را محدود میکند؛ gunicorn را روی سوکتِ unix بالا بیاور و بارزنِ پایتونی بزن.
بکاپ: کپیِ زنده روی سرورِ ۲۴۷ — از ۲۰۲۶-۱۰-۱۰
هر تغییرِ دیتابیس لحظهای به سرورِ دیگری میرود؛ بکاپِ شبانهی خودمان (backup_db/backup_verify/cron) حذف شد.
شرحِ کامل و رویهی بازیابی: docs/operations.md «بکاپ و بازیابی». فایلها: ops/pgbackup/.
- سرورِ بکاپ:
administrator@78.38.35.247، همان رمزِ ۲۴۹ (همان فایلِ رمز). شبکهی داخلی: ۲۴۹ =192.168.200.164، ۲۴۷ =192.168.200.167. ۲۴۷ هم ماشینِ مشترک است (artina، judge0، metaverse، docker، وpostgresql@14-mainِ دیگران روی ۵۴۳۲): همان قواعدِ «The server» اینجا هم هست. مالِ ما فقط~/zareian-pgbackup/و یونیتهایzareian-pg-*است. Postgresِ آنجا را به کار نمیگیریم؛ فقط ابزارهای Postgres 14 را اجرا میکنیم. - چطور: روی ۲۴۷
zareian-pg-tunnel(SSH با کلیدِ~/.ssh/zareian_pgbackupکه درauthorized_keysِ ۲۴۹ بهpermitopen="127.0.0.1:5440"محدود است، شِل نمیدهد) →zareian-pg-wal(pg_receivewal، اسلاتِzareian_247، نقشِzareian_replicator، رمزش فقط در~/.pgpassِ ۲۴۷) →wal/؛ وzareian-pg-basebackup.timerهر روز ۰۴:۳۰ →base/(۷ تا، باpg_verifybackup؛ WALِ قدیمیتر از قدیمیترین پاک میشود). - ۲۴۹ را نمیخواباند: تکثیر ناهمزمان است (
sync_state: async)؛max_slot_wal_keep_size=1GB(باALTER SYSTEM، نه/etc) سقفِ چیزی است که در قطعیِ ۲۴۷ روی دیسکِ ۲۴۹ جمع میشود. قطعیِ طولانیتر اسلات راlostمیکند (رویه در docs). - پایش:
OFFSITE_WAL_SLOT=zareian_247در.envِ ۲۴۹؛/api/health(با کلید)offsiteBackupو هشدار میدهد (config/tests_offsite_backup.py). شکستِ نسخهی روزانه فقط درjournalctl -u zareian-pg-basebackupِ ۲۴۷ دیده میشود. - سنجیده (۲۰۲۶-۱۰-۱۰): کلید شِل نمیگیرد و تونل به پورتِ دیگر رد میشود؛
restore-check.shروی ۲۴۷ شمارش و آخرین زمانِ کاربر/ورود/سفارش/تراکنش/تغییرِ وضعیت را برابرِ ۲۴۹ داد؛ یک ردیفِ آزمایشی که بعد از نسخهی کامل روی ۲۴۹ نوشته شد ۲ ثانیه بعد در بازیابیِ ۲۴۷ بود (جدولش پاک شد). نسنجیده: ریبوتِ ۲۴۷، قطعیِ طولانی تاlost، اجرای خودکارِ timer (اولین بار ۲۰۲۶-۱۰-۱۱ ۰۴:۳۰)، و برگرداندنِ بازیابی روی خودِ ۲۴۹. - ایمیجِ روزانهی کلِ ماشین کارِ ادمینِ سرور است؛ محتوایش را ما نسنجیدهایم. کپیِ ما به آن تکیه نمیکند.
- «بکاپِ خودکارِ قبلی روی ۲۴۷» وجود نداشت:
~/backup_mathapp.shِ ۲۴۹ (کپیِ SQLite و journal با scp) فقط ۲۰۲۶-۰۸-۲۹ دستی اجرا شده و در هیچ cron/timerی نیست. خودش و خروجیهایش (~/backups/روی ۲۴۹،~/backups/mathapp/روی ۲۴۷) مردهاند؛ پاک کردنشان بیرون از~/mathapp-v2است و اجازهی جدا میخواهد.
اشتراک و دیوارِ پرداخت — روی سرور از ۲۰۲۶-۱۰-۱۰ ۰۱:۱۹ (بکاند 64c4c09، فرانت a8a5351)
- فقط فصلِ اولِ هر پایه رایگان است (
content.views.unit_is_free). بیرون از آن، بیاشتراکِ فعال، سوالهای درس، آزمونِ جزیره، ثبتِ جواب و پایانِ درس/جزیره ۴۰۲subscription_requiredمیدهند؛/api/homeبرای هر فصلrequiresSubscriptionدارد. تست:content/tests_paywall.py. - قیمتها و سقفِ دستیار از
subscription.0016(بخشِ کافهبازار بالا). رایگان ۲ سوال در روز (AI_DAILY_QUOTA_FREE). - حسابهای دمو اشتراک ندارند، پس از فصلِ دوم به بعد برایشان قفل است.
- از سرور،
git fetchاز Gitea در این روز کار کرد (بیDEPLOY_BUNDLE).
شکلِ ثابتِ درسها (Lesson.figures) و کشِ فرانت — روی سرور از ۲۰۲۶-۱۰-۰۷ ۱۶:۴۸ (b93f43e)
شکلِ درس یک بار ساخته و تأیید میشود و بعد ثابت است؛ هنگامِ نمایش هیچ فراخوانیِ مدلی نیست.
Lesson.figures فهرستِ {where: intro|example|step, index?, diagram} است (index برای step از صفر).
GET /api/lessons/{id}/intro آن را برمیگرداند و فرانت (LessonScreen) با همان Diagram دستیار میکشد.
شکل در content/seed/curriculum.json هم میرود (figures)، و seed_content آن را فقط در خانهی خالی مینشاند
(مثلِ example/steps). پس برای عوضکردنِ شکلِ درسی که روی سرور هست باید figures آن درس را خالی کرد و دوباره دیپلو کرد؛
شناسهی درس روی سرور با لپتاپ فرق دارد (۳۰۰۳..۳۰۰۶ محلی = ۲۲۹۸..۲۳۰۱ سرور)، پس با شناسهی محلی روی سرور کار نکن.
- ساخت:
python manage.py gen_lesson_figures --grade 12 --limit 4 [--lesson ID] [--redo] [--dry-run]. همان مدلِ چت و کاتالوگِ<diagram>و اعتبارسنجِrag.services.diagram.validate(مسیرِ ارزانِ دستیار). مدل میتواند[]بدهد؛ همهی درسها شکل نمیخواهند. شمارهی قدمها در پرامپت ۱بهبعد است (همان شمارهای که کاربر میبیند) و کد یکی کم میکند؛ با شمارش از صفر شکل زیرِ قدمِ اشتباه مینشست. - هزینه (سنجیده ۲۰۲۶-۱۰-۰۷): هر درس ≈ ۳٫۵ هزار توکنِ ورودی و ۰٫۱ تا ۰٫۶ هزار خروجی. صورتحسابِ GapGPT برای یک فراخوانی
≈ ۰٫۰۳ سنت داد و فرمولِ رسمیِ
gpt-4.1-mini≈ ۰٫۱۵ سنت؛ این دو نمیخوانند و علتش پیدا نشد. برآوردِ ۳۰۰۰ درس ۱ تا ۴٫۵ دلار. - باید با چشم دید. در همین چهار درس مدل خطشکسته را با
line(بیپایان) کشید، یک بار فقط نقطه بیخط داد، نمودارِ تابع را با بازهی بزرگ له کرد، و برای قدمِ ۳ شکل را زیرِ قدمِ ۲ گذاشت. چهار درسِ اولِ حسابانِ ۱۲ (فصل «تابع») بازبینی شدند؛ هیچ درسِ دیگری شکل ندارد. برچسبهایی مثلِ «A۲» و «g(x)=f(x)+۲» کنارِ لبه کمی ناجورند و رفع نشدند. - ایرادِ محتوا، نه شکل: مثال و قدمهای درسِ «تابع درجه سوم، توابع یکنوا…» کپیِ مثالِ «تبدیل نمودار توابع» است و با عنوانش
نمیخواند (از
fill_lesson_texts). شکلهایش با همان متن میخوانند. - بازبینیِ زنده بیپیامک: با یک
OTPCodeکه خودت برای یکی از دموها (مثلاً09120000105، پایهی ۱۲، کاربر ۱۴) در دیتابیسِ سرور میسازی (کدِ تصادفی، ۵ دقیقه، از stdin نه از argv) وارد شو، توکن را درlocalStorage['tadrisino.tokens']({token, refreshToken}) بگذار و باpuppeteer-core(در scratchpad، نه ریپو) از «نقشه ← فصل ← جزیره ← اول درسهاش رو بخونم» برو. آخرشPOST /api/auth/logout. توکنِ دسترسی را چاپ نکن (یک بار چاپ شد).
کشِ مرورگر: سینیور شکلِ درسها را در یک مرورگر نمیدید و در مرورگرِ دیگر میدید؛ API و bundleِ زنده درست بودند. علتِ احتمالی
(مستقیم دیده نشد): index.html فقط etag داشت و بی Cache-Control از کش بالا میآمد. در /etc/nginx/sites-available/mathapp-front
(فقط سایتِ ما؛ بکاپ کنارش …bak-20261007) حالا Cache-Control: no-cache برای همه و immutable, max-age=1y برای /assets/ هست
(nginx -t و reload، نه restart). از بیرون index.html و مسیرهای اپ no-cache دادند؛ هدرِ /assets/ را نشد دید چون
ArvanCloud فایلهای js را خودش نگه میدارد (x-cache: HIT). config.js هم از کشِ آروان میآید؛ با عوضشدنِ دامنهی API
کشِ آن را در پنلِ آروان پاک کن (نه فقط index.html). مرورگری که پیش از این تغییر index.html را کش کرده یک بار باید
«Clear site data» بزند. بارِ همزمانِ هزار نفر آزموده نشد؛ استدلال این است که فایلهای سنگین از CDN میآیند و index.html ۳۰۴ میگیرد.
وضعیت ۲۰۲۶-۱۰-۰۷ — روی سرور (2779270، ریاستارت ۱۴:۱۶)
بکاپهای همان روز در .deploy-backups/: env-before-secretkey-redis-20261007-135850،
unit-gunicorn-mathapp-before-20261007-135850.service، db-before-deploy-20261007-13*.sqlite3.
- کلیدِ امضای توکن عوض شد (بند ۱ سندِ امنیتی). از
.envو از یونیتِgunicorn-mathapp.serviceبرداشته شد (یونیت فقطDJANGO_ALLOWED_HOSTSوEnvironmentFileدارد؛DJANGO_DEBUGهم از یونیت رفت و فقط.envمیماند). برنامه خودش کلید را در.django_secret_key(۶۰۰) ساخت. سنجش: توکنِ جعلی با کلیدِ داخلِ ریپو برای کاربرهای ۱۰، ۱۱، ۱۲ پیش از تغییر200و بعدش401گرفت. توکنِ refresh در دیتابیس است، نه امضاشده، پس نشستها نمیمیرند و فرانت بیصدا توکنِ تازه میگیرد؛ این را با ورودِ واقعی نسنجیدم. - Redis: وصل نیست، عمداً (تصمیمِ مالک ۲۰۲۶-۱۰-۰۷). Redisِ روی آن ماشین مالِ تیمهای دیگر است و به آن، یا به
هیچ حافظهی موقت و سرویسِ دیگرانی، وصل نمیشویم.
CACHE_URLدر.envخالی و کشLocMemCacheاست، پس تا Redisِ اختصاصیِ خودمان نیست هر سقفِ مبتنی بر کش ≈ ۳ برابرِ عدد است (۳ worker)؛ سقفهای دیتابیسی (پیامک، حدسِ کد) درستاند. «Redisِ اختصاصی» در گزارشِ لانچ یک تصمیمِ باز است (کنارِ Postgres). کد برای آن روز آماده است:config.cache.ResilientRedisCache(فقط باCACHE_URLفعال میشود) قطعیِ Redis را ۵۰۰ نمیکند، ERROR لاگ میکند، و تا برگشتنش سقفهای مبتنی بر کش اعمال نمیشوند (fail-open) و/api/health۵۰۳ میدهد؛ ۱۱ تستِ واحد دارد، از جمله redis-serverِ محلی که وسطِ کار میمیرد و برمیگردد. یک بار ۲۰۲۶-۱۰-۰۷ چند ساعت به آن Redisِ مشترک وصل بود و قطع و کلیدهایمان پاک شد؛ آنجا آزموده شد که کش روی سرور کار میکند. آزموده نشد: قطعیِ واقعیِ Redis، و رفتارش زیرِ فشارِ حافظه. - آزمونِ زنده با کاربرِ آزمایشی (۳۲ از ۳۲ سبز؛ کاربر و دادهاش پاک شد، شمارشها با پیش از آزمون برابر): ورود با OTP، توکنِ تازه، تمدید (توکنِ تمدیدِ قدیمی ۴۰۱)، جزیرهی قفل ۴۰۳، بیجواب ۴۰۹، ادعای کمتر از تعدادِ سوال ۴۰۹، پایانِ جزیره و درس بعد از جواب، Idempotency-Key، و خروج. رابطِ کاربریِ فرانت در مرورگر رانده نشد (همان فراخوانیها با API زده شد).
- پایانِ درس/جزیره بیجواب و روی جزیرهی قفل پذیرفته نمیشود، ۱۵ مسیر سقفِ نرخ گرفتند،
deploy.shپیش ازmigrateبکاپ میگیرد و سلامت را از سوکتِ gunicorn میسنجد. - نسخهی کتابخانههای
requirements.txtقفل شد (نسخههای روی سرور).
وضعیت ۲۰۲۶-۱۰-۰۵ (عصر) — ورودِ مهمان از ریشه حذف شد
روی سرور از ۲۰۲۶-۱۰-۰۵ ۱۸:۳۹ (بکاند 5b7e6fb، سرویس واقعاً ریاستارت شد، accounts.0008 اعمال شد؛ فرانت 0db6526).
بکاپها: .deploy-backups/db-before-guest-removal-20261005-181900.sqlite3، db-before-phone-swap-20261005-182656.sqlite3،
env-before-ali-phone-20261005-183839. از بیرون سنجیده شد: POST /api/auth/session ۴۰۴، /api/health ok، ۳٬۶۷۹ سوال.
- تنها راهِ ورود شماره و کدِ پیامکی است.
User.is_guest/device_id،Session.device_id، throttleهایGuestAware*،DEMO_GUEST_DEVICE_IDSو حذفِ حساب با تایپِ «حذف» رفتند؛ حذفِ حساب فقط با کدِ پیامکی است. migrationِ0008تا مهمانی مانده باشد عمداً میشکند. ۱۵ مهمانِ سرور باdelete_usersپاک شدند (۸ کاربر ماند). - «علی رضایی» (کاربر ۱۰) حالا به شمارهی مالکِ پروژه وصل است؛ شمارهی دموی قبلیاش از
TEST_LOGIN_PHONESبرداشته شد. - ۷۲۴ سوالِ تازه برای درسهایی که سوال نداشتند (پایههای ۳ تا ۱۱) از مسئلههای تأییدشده نوشته شد و هرکدام را حلِ کورِ جدا سنجید (۱۲ ناهمخوان پاک شد). ۵۴۵ درس هنوز سوال ندارند چون مسئلهی تأییدشدهی خودکفایی در کتاب نیست. حسابانِ دوازدهم کامل است (۳۲۶ chunk در DB و Qdrant)؛ ۱۵ درسش بیسوال ماند.
- چکلیستِ لانچ:
~/Desktop/گزارشات Tadrisino/گزارش-۲۸/چکلیست-لانچ.html.
وضعیت ۲۰۲۶-۱۰-۰۵ — بازبینیِ امنیتیِ پیش از تحویل
روی سرور از ۲۰۲۶-۱۰-۰۵ ۰۵:۰۷ (بکاند a973c41، سرویس واقعاً ریاستارت شد، payment.0011 اعمال شد؛ فرانت
e850f6d). بکاپ: .deploy-backups/db-before-security-review-20261005-050627.sqlite3. از بیرون سنجیده شد: callbackِ
بیAuthority ۴۰۰، حذفِ مهمانِ نمایشی ۴۰۳، characterId="²" ۴۰۰، Swagger روی swagger-ui-dist@5.33.1.
شش بررسیکنندهی موازی (هر کدام یک بخش، فقط محلی) کد را بدبینانه وارسی کردند؛ هر یافته با تست بازتولید شد. هر
ایرادی که رفعش ارائه را به خطر نمیانداخت رفع شد و در */tests_security_review.py حفاظ دارد (۸۳۲ تست سبز)؛ مهمترینش
actionهای لغو/بازپرداخت که برای Supervisorِ فقطخواندنی هم اجرا میشدند، و ورودِ فایلِ سوال برای هر کارمند. آنچه
ماند (تصمیمِ طراحی یا خطرِ دمو) مواردِ ۲۴ تا ۳۱ِ docs/security-before-production.md است؛ گزارشِ سینیور همان
گزارش-۲۷/فهرستِ-مسائلِ-امنیتی.html (۲۳ مورد). رفعِ کلیدِ امضا حالا امن است: فایلِ .django_secret_key اتمی
ساخته میشود؛ پیش از آن سه worker در ۱۶ از ۲۰ بار کلیدِ جدا میساختند.
وضعیت ۲۰۲۶-۱۰-۰۳ (عصر) — پایهی چهارم و پنجم، خریدِ بازار از اپ، بهبودِ UI
روی سرور از ۲۰۲۶-۱۰-۰۴ ۰۳:۰۵ (بکاند 8cb326b، سرویس واقعاً ریاستارت شد، subscription.0012 اعمال شد؛
فرانت f8cf86e). بکاپ: .deploy-backups/db-before-grades45-20261004-025926.sqlite3. روی DBِ سرور: چهارم ۲۸
جزیره / ۲۰۴ سوال، پنجم ۲۸ / ۱۹۴، هیچ جزیرهی زیرِ ۴ سوال نیست؛ /api/grades ده پایه میدهد. هیچ بستهای هنوز
bazaar_sku ندارد. .deploy-backups/ آن روز ۳۴ فایل و ۸۰ مگابایت بود (دیسک ۸۳٪)؛ پاک نشد.
- چهارم و پنجم (درخواستِ دکتر: «از سوم به بعد»):
MATH_4/MATH_5درrag/services/textbooks.py(فهرست و offset از خودِ PDF؛ پنجم چاپِ ۱۴۰۵ با offset صفر). ingest با gpt-4.1-mini ≈ ۰٫۷ دلار (۵۹۰ + ۵۹۴ chunk)، verify ≈ ۰٫۲؛ هر دو روی Qdrant. هر پایه ۷ فصل و ۲۸ جزیره؛ ۲۰۴ و ۱۹۴ سوال با subagentِ Sonnet (حداکثر ۸ نامزد برای هر جزیره، نه همهی ۵۷۳ جفت)، و هر جزیره دستِکم ۴ سوال. همهی ۳۹۸ سوال یک بار کور دوباره حل شدند (subagentِ دیگر، بیدیدنِ کلید): ۳ مورد واقعاً غلط/مبهم بود و اصلاح شد (یکی کلیدِ غلط: «عدد ششمِ ۱۲۰، ۲۴۰، ۳۶۰» = ۷۲۰ نه ۶۰۰). دو جزیرهی تقارنِ پنجم در کتاب فقط رسم روی شطرنجی دارند؛ ۱۲ سوالِ بیشکل برایشان نوشته شد.seed_contentروی DBِ خالی آزموده شد. ⚠️fill_lesson_textsهمزمان باrag_verify_solutionsروی SQLite «database is locked» داد و verify را نیمهکاره گذاشت (بیصدا تا شمردنِ رأیها)؛ دو فرمانِ نویسنده را همزمان اجرا نکنید. - خریدِ بازار از مسیرِ کاربر: تا امروز سفارشِ بازار فقط با کلیدِ داخلی ساخته میشد و
paymentMethodهر مقداری را بیصدا نادیده میگرفت. حالاPlan.bazaar_sku(ادمین،subscription.0012) وPOST /api/subscription/purchaseباpaymentMethod: "bazaar"(فقط بسته)؛ کدِ تخفیف روی سفارشِ بازار ۴۰۰، Idempotency-Key با روشِ پرداختِ دیگر ۴۰۹. فرانت:src/lib/bazaar.ts(پل به اپِ اندروید) وandroid/(پوستهی WebView + Poolakey) — هرگز build نشده (این ماشین Android SDK ندارد). از سنیور لازم: اسمِ پکیج، کلیدِ RSA، SKUِ بستهها در پیشخان. شرح:docs/operations.md«فروشِ بسته در کافهبازار». - UI (فرانت
f8cf86e): رنگِ متنِ کمرنگ #727299 (کنتراست ۲٫۶ → ۴٫۵)، برچسبِ تب ۱۱px، عنوانِ فصل در نقشه دوخطی، کاهشِ حرکت، حلقهی فوکوس، بی pull-to-refresh، لرزشِ کوتاهِ جواب. با اسکرینشاتِ قبل/بعد وارسی شد. manifestِ PWA عمداً اضافه نشد (رفتارِ برگشت از زرینپال در حالتِ standalone آزموده نشده).
وضعیت ۲۰۲۶-۱۰-۰۲ — تصمیمهای سینیور پس از ارائه
- بخشِ والدین از ریشه حذف شد. اپِ
familyدیگر وجود ندارد. جدولهایش، مجوزهای ادمینش و ردش درdjango_migrationsرا migrationِprogress.0006پاک میکند.Profile.parent_phone، دو نوع اعلانِ والدین و پارامترِ?userId=در/api/progress/nodesهم رفتند. جایش «گزارشِ والدین» در پروفایلِ خودِ دانشآموز است:GET /api/user/weekly-report(progress/report.py)، ۷ روزِ اخیر در کنارِ ۷ روزِ قبل.minutesتخمین است، از فاصلهی جوابها؛ heartbeat عمداً ساخته نشد. - تاریخِ تولدِ شمسی بهجای «سن»:
birthDate(«1385-04-12»)، میلادی ذخیره میشود و تبدیلش فقط درaccounts/jalali.pyاست.birthYearاز آن پر میشود وageحذف شد. بازهی سن ۵ تا ۹۹ است. در فرانت سه فهرستِ روز/ماه/سال (BirthDatePicker) هست و برای ۱۹+ سوالِ «ریاضیِ کدوم پایه؟» پرسیده میشود. - شناسهی کاربری زیرِ نام در پروفایل. فاکتورها:
GET /api/payment/invoicesو صفحهی «پرداختها و فاکتورها».Order.list_priceوplan_titleدر لحظهی خرید ثبت میشوند تا فاکتور با عوض شدنِ پلن تغییر نکند. - دستیار فقط ریاضی (
rag/services/guardrails.py، پرامپتtutor-v11-math-only). آزمونِ زنده ۷ از ۷ رد شد؛ شرحش درdocs/ai-cost-and-guardrails.mdاست. - هزینه: هر سوال حدودِ ۰٫۲ سنت است؛ برای قیمتگذاری دستِ بالا ~۷۰۰ تومان حساب میشود و ۷۰٪ِ آن ورودی است. سقفِ روزانه: رایگان
AI_DAILY_QUOTA_FREE(پیشفرض ۳)، مشترکPlan.ai_daily_quota(ادمین، پیشفرض ۱۵). تا امروز همه ۱۵ داشتند. گزارشِ مدیرعامل و گزارشِ ۲۶ در~/Desktop/گزارشات Tadrisino/گزارش-۲۶/است. مسیرِ استریم حالا توکنها را ثبت میکند (پیش از این صفر بود). خرجِ GapGPTِ آن روز حدودِ ۴ سنت بود. - لحنِ دستیار از سن (
tone_for، پرامپتtutor-v12-age-tone): ۱۸+ در هر پایه لحنِ بزرگسال میگیرد؛ نمادگذاری از کتابِ پایه. کشِ جواب با لحن کلید میخورد. فقط یک مقایسهی زنده (سوم، ۸ و ۴۵ سال). - شناسهی نمایشی
User482917(accounts/public_id.py، از id، بیستون؛ ادمین با آن جستجو میکند). - بستهی سوال بهجای اشتراک (
subscription.0009/0010): ۲۰ / ۶۰ / ۱۵۰ سوال به ۴۵ / ۱۱۹ / ۲۵۹ هزار تومان، بیانقضا، با ۷ / ۳۰ / ۶۰ روز قلبِ نامحدود (HeartsPass). اعتبار در دفترِQuestionCredit، اول سهمیهی رایگانِ روز خرج میشود؛ هر سوالی که به API برسد اعتبار میخورد، ردِ صافی/کش/خطا نه (Message.charge). پلنهای ماهانه/سالانهon_sale=False(مشترکِ فعلی سقفش را دارد). مسیرِ قیمتها پیش از بسته: ۲۹۹ هزار سالانه → ۹۹/۹۹۰ با ۵ سوال → ۱۷۹/۱٬۷۹۰ با ۱۰ سوال. شرح:docs/ai-cost-and-guardrails.md. - قلب واقعی است:
HEARTS_ENFORCEDپیشفرض روشن؛ قلبِ صفر جلوی جواب را میگیرد (فرانت صفحهی «قلبهات تموم شد» را دارد). روی سرور از ۲۰۲۶-۱۰-۰۲ ۱۱:۴۲. - حذفِ حساب
POST /api/user/delete: شمارهدار با کدِ پیامکیِ تازه (verify_phone_otp، مشترک با ورود)، مهمان با «حذف»؛ سفارشها و statuslog میمانند؛ دموها رد میشوند. «ورود با حسابِ دیگر» فقط خروج است؛ چند حساب روی یک دستگاه عمداً نگه داشته نمیشود. - پاکسازیِ ۲۰۲۶-۱۰-۰۲ روی سرور:
delete_users --real-phones --skip-paidپنج حسابِ شمارهدار (۱، ۱۷، ۱۹، ۲۰، ۲۱) را پاک کرد؛ دموها و کاربرِ ۲۶ (پرداختِ موفق) ماندند. بکاپ:.deploy-backups/db-before-user-cleanup-20261002-061142.sqlite3. دستور فقط مدلهای خودِ پروژه را میشمارد؛ بارِ اولadmin.LogEntry(کارمندِ ادمین) را هم گرفته بود و پیش از اجرا اصلاح شد. spectacular --validateیک خطای از پیش موجود دارد (رویd5dbd1bهم بود) که به این تغییرها ربطی ندارد.- بعد از بازبینیِ مستقلِ همان روز — روی سرور از ۲۰۲۶-۱۰-۰۲ ۲۳:۱۷ (بکاند
7683920، سرویس واقعاً ریاستارت شد؛ فرانت33f5f8d؛ بکاپ:.deploy-backups/db-before-hearts-refund-20261002-231637.sqlite3): خریدِ بسته از بازار اعتبار نمیداد (planIdدرPOST /api/payment/orders)؛ صافیِ دستیار شکلهای بینیمفاصلهی «میخوام بمیرم» را نمیگرفت؛ قلبِ صفر حالا پایانِ درس/جزیرهی بیجواب را هم میبندد و چکش داخلِ تراکنش است (content/tests_hearts_gate.py)؛ لغو و بازپرداختِ سفارش با دو action در/admin/payment/order/(پول را خودِ درگاه برمیگرداند؛docs/operations.md)؛ دفترِ قیمتِ مدلهاdocs/model-prices.md. فرانت: صفحهی تازهی «قلبهات تموم شد» با شمارشِ معکوس. ۷۷۹ تست سبز. دو migration تازه:payment.0010،subscription.0011. PDFِ مستندِ فنی حالا از Markdown ساخته میشود:python docs/build_handbook.py(pandoc + Chrome).
وضعیت ۲۰۲۶-۰۹-۳۰ — بازار وصل شد
- حسابِ شرکت در بازار «اجازه» داد (۰۹-۳۰ ۱۲:۰۶،
bazaar: true). اسمِ پکیج را کسی نداد؛ با توکنِ خودمان پیدا شد: از اسمهای حدسی فقطcom.tadrisino.app«Product is not found» داد (یعنی هست و دسترسی داریم؛ اپِ دیگران ۴۰۳ میدهد). صفحهی عمومیاش ۴۰۴ است، پس منتشر نشده. اینکه همان اپِ ریاضی است را سنیور تأیید نکرده.BAZAAR_PACKAGE_NAME=com.tadrisino.appدر.envسرور — از ۲۰۲۶-۱۰-۰۸com.tadrisino.math.appاست، برابرِapplicationIdِ APKِ واقعی (aapt2)؛ ثبتبودنش در پیشخان تأیید نشده (بکاپ:.deploy-backups/env-before-bazaar-package-20260930-122311)، ریاستارت ۱۲:۲۳. با SKU و توکنِ ساختگی: تکخرید «Product is not found» (پس ۵۰۳ در کدِ ما)، اشتراکnot_found. هنوز نداریم: SKUِ پلنها و اپِ اندرویدیِ دارای Poolakey؛ هیچ خریدِ واقعیای تأیید نشده.
وضعیت ۲۰۲۶-۰۹-۲۹ (شب) — بازبینیِ نهایی پیش از ارائه (روی سرور)
- بکاند
93b451d(سرویس واقعاً ریاستارت شد، ۲۱:۲۵)، فرانتdb6d694. بکاپ:.deploy-backups/db-before-final-polish-20260929-212452.sqlite3. ۶۸۵ تست سبز، اسکیما بیخطا، ۶۱ اندپوینت. - تازه:
GET /api/support/tickets(پیش از آن اپ شناسهی تیکت را فقط در localStorage داشت)؛ اعلانِ «تمام کردی» با رقمِ فارسی؛.env.exampleاز نو و مطابقِ کد. فرانت: صفحهها lazy با preload (ورودیِ اپ ۵۰۵ ← ۸۵ کیلوبایت)،MathTextروی صورت/گزینهی آزمون و مثالِ درس، جداکنندهی «·» کنارِ صفرِ فارسی عوض شد، برچسبِ تکراریِ ریشه/رأس. - دستیار آن شب ~۳۰ ثانیه برای هر جوابِ کامل طول کشید. روی خودِ سرور اندازه شد:
gpt-4.1-miniاز GapGPT برای «فقط بگو سلام» ۶٫۸ ثانیه، و stream اولین تکه را همراهِ آخرین میدهد. کدِ ما نیست. کشِ جواب per-worker است (Redis نیست)، پس «یک بار قبل از ارائه بپرس» قابلِ اتکا نیست. - وارسیِ صفحهها با
puppeteer-coreدر scratchpad و Chromeِ نصبشده (توکن از API، گذاشتن درlocalStoragetadrisino.tokens). این تستها روی دیتای واقعی نوشتند: سارا (کاربرِ ۱۱) دو جزیره جلو رفت، و علی/نگار/سارا هرکدام ۱–۲ سؤال از دستیار پرسیدند. - راهنمای ارائه و گزارشِ نهایی در
~/Desktop/گزارشات Tadrisino/گزارش-نهایی/بهروز شدند.
وضعیت ۲۰۲۶-۰۹-۲۸/۲۹ — شکل در دستیار، آزمونِ جزیره، همراهِ چهارم، محتوای کامل (روی سرور)
-
✅ ۲۰۲۶-۰۹-۲۹ دیپلوی شد: بکاند
2c759a7(سرویس واقعاً ریاستارت شد، ۰۷:۱۹؛ migrationهایai_tutor.0006وprogress.0005اعمال شدند؛questions: 2545)، فرانت63f4ea8رویmath.tadrisino2.ir. بکاپِ پیش از آن:.deploy-backups/db-before-diagrams-20260929-071459.sqlite3. تستِ زنده روی خودِ سایت: ۵ سوال (مثلث، سوالِ ادامهای، سهمی، ضربِ بیشکل، الگو)، همه درست و شکلها درMessage.diagramذخیره شدند؛aiProvider: ok. این تست سه کاربرِ مهمانِ تستی ساخت (کاربرهای ۲۲ تا ۲۴، نامِ «کیان») که مثلِ دادهی دمو پیش از کاربرِ واقعی باید پاک شوند. فرانت ازupstream(Tadrisino/MathApp_frontend) دیپلوی میشود، نه ازoriginِ شخصی؛ پوش به هر دو. -
اعتبارِ GapGPT در ۰۹-۲۹ دوباره شارژ شد (بندِ ⛔ پایین تاریخی است).
-
⛔ اعتبارِ GapGPT تمام شد (همان روز، ~۱۷:۲۰) و دستیارِ سایتِ زنده ۵۰۳ میدهد. کلیدِ
.envِ لپتاپ همان حسابِ سرور است؛ سنجشهای شکل (~۲۵۰ فراخوانی، ≈۰٫۳ دلار) باقیمانده را خورد. هر سنجشِ انبوهِ محلی از اعتبارِ پروداکشن خرج میکند؛ پیش از آن موجودی را بسنجید. -
شکل در جوابِ دستیار: مدل در انتهای جواب
<diagram>{JSON}</diagram>مینویسد؛rag/services/diagram.pyآن را پیش ازmathtextجدا، خطاهای قالبیِ شناختهشده را تعمیر (رقمِ فارسی،2*pi، کامنت، ونِ با اسمِ فارسی) و سختگیرانه اعتبارسنجی میکند؛ رویMessage.diagram(migrationai_tutor.0006) مینشیند و درdoneِ SSE و تاریخچه میآید. فرانت (src/diagram/) خودش SVG میکشد و ریشه، رأس، مماس، پای عمود و تبدیلها را حساب میکند؛ HTML/SVGِ مدل هرگز وارد صفحه نمیشود. ۱۴ نوع (plane، numberline، fraction، grid، groups، clock، bar/linechart/pie، table، venn، mapping، tree، solid)؛ گالریِ آزمایشی در?galleryفقط در حالتِ توسعه. کاتالوگ بخشبخش است و کنارِ هر سوال فقط بخشِ مربوط میرود (hint_for)؛ سوالِ ادامهای بخشِ سوالِ قبلی را میگیرد (follow_up_hint_for)، و درخواستِ صریحِ «شکلش رو بکش» بیکلمهی موضوعی کلِ کاتالوگ را (asked_hint_for، پرامپتtutor-v8-asked-figure؛ یک بار با GapGPT آزموده شد: مستطیلِ باغچه). فرانت هیچ تشخیصی نمیدهد. -
۲۰۲۶-۰۹-۲۹، پرامپت
tutor-v10-shape-anchor: ابزارهایtriangle(مثلث از ضلع و زاویه، با قانونِ سینوس/کسینوس درsrc/diagram/shapes.ts)،quad(مستطیل، مربع، متوازیالاضلاع، لوزی، ذوزنقه)، نوعِpattern(الگوی شکلی و چوبکبریت)، نقطهیtranslate؛ شکلِ دوم روی ضلعِ مشترک یا کنارِ شکلِ قبلی مینشیند. تعمیرِ عددِ حسابی («-2 * 3.14»)، گیومهی جاافتاده، کلیدِ تکراری،kindبا نامِ شکل؛fits()ساعتِ دیواری را برای سوالِ بیساعت دور میریزد. سنجشِ ۶۴ مسئلهی تصادفیِ کتاب (همان مسئلهها قبل و بعد، وارسیِ چشمی): درست ۳۹ → ۴۰، غلط ۹ → ۷، بیشکل ۷ → ۵ — در این اندازهی نمونه معنیدار نیست؛ برآوردِ ۹۵٪: درست ۵۰–۷۳٪، غلط ۵–۲۱٪. ۴۰ مسئلهی هندسیِ جدا: ۲۲ درست، ۱۲ قابلِ قبول، ۴ غلط. خرج ≈ ۰٫۷ دلار (به اجازهی صاحبِ پروژه تا ~۱ دلار). اسکریپتِ سنجش در scratchpad بود، نه در ریپو. پرامپتtutor-v6-diagrams. سنجش روی ۹۶ مسئلهی واقعیِ کتاب: ۴۵ از ۴۸ شکلِ معتبر (دورِ ۲)، ۰ از ۸ برای سوالِ بیشکل؛ وارسیِ چشمیِ ۸۳ شکل: ۶۴ درست، ۳ ضعیف، ۱۶ غلط. اصلاحهای بعد از آن (خط از معادله، کسرِ مقایسهای، محرکِ ساعت، …) روی مدل سنجیده نشدهاند. -
فرانت آزمونِ جزیره را بازی میکند (
/nodes/{id}/quiz); خانه خودِ مسیر است با «شروع». -
امگا (ω؛ تا ۲۰۲۶-۱۰-۱۰ «تدی»، slug هنوز
tedi؛ شکلکِmobile-preview/mascot-pack، شش حالت) باprogress.0005/0007، ۵۰۰ امتیاز (همان فاصلهی ۲۰۰تاییِ دلتا تا پی؛ خواستِ صاحبِ پروژه). -
محتوا: ۸۲ سوالِ تازه، پس هیچ جزیرهای زیرِ ۴ سوال نیست (۲٬۵۴۵ سوال).
fill_lesson_texts(بیهزینه)Unit.blurbرا برای ۵۸ فصل و مثال/قدمها را برای ۵۹۰ درس از مسئلههای تأییدشدهی کتاب پر کرد، هرگز همان مسئلهی آزمونِ درس؛seed_contentاینها را فقط در خانهی خالی مینشاند، پس متنِ ادمین با دیپلوی بازنویسی نمیشود. -
محلی:
PUBLIC_BASE_URLپیشفرضش دامنهی اصلی است، پس «پرداخت» روی لپتاپ به سرورِ واقعی میرود و ۴۰۴ میگیرد؛ باگِ سرور نیست. گزارش:~/Desktop/گزارشات Tadrisino/گزارش-۲۵/.
وضعیت ۲۰۲۶-۰۹-۲۷ — دامنهی تازه و فرانتِ تازه، هر دو زنده
-
✅ پیامکِ کدِ ورود واقعاً میرسد (۲۱:۴۴). اسمِ الگو را سینیور داد:
SMSVerify، با همان سه پارامترِreceptor/token/templateکه کد از قبل میفرستاد. فقطKAVENEGAR_OTP_TEMPLATE=SMSVerifyبه.envسرور اضافه شد (بکاپ:.deploy-backups/env-before-smsverify-20260927-214351) و سرویس ریاستارت شد؛ کدی عوض نشد. یک دقیقه پیش از آن جلسهی دیگری کلیدِ بازار را عوض و ریاستارت کرده بود (env-before-bazaar-client2-…)؛ سنجیده شد که هیچ کلیدی از آن گم نشده. -
API روی
mathapi.tadrisino2.irاست؛ ادمین از ۲۰۲۶-۰۹-۲۶ ساعتِ ۲۳:۲۲ nginx را عوض کرد و رکوردِ DNSِapi.math.tadrisino2.irدیگر وجود ندارد..envسرورPUBLIC_BASE_URL=https://mathapi.tadrisino2.irشد (بکاپ:.deploy-backups/env-before-mathapi-20260927-010751وdb-before-mathapi-…sqlite3)،1f38465دیپلوی و سرویس واقعاً ریاستارت شد (۰۱:۰۹). -
⛔ زرینپال هنوز
-14میدهد («دامنهی callback با دامنهی ثبتشدهی ترمینال نمیخواند»)، این بار برایmathapi؛ فراخوانیِ زندهی PaymentRequest در همان شب. رفعش ثبتِ دامنه در پنلِ زرینپال است، نه کد. Redirect URIِ بازار هم با این دامنه عوض شد؛ لینکِ «اجازه»ِ تازه باmathapiدر ۲۰۲۶-۰۹-۲۷ ساخته شد (۷۲ ساعت، لینکهای قبلی باطل) و برای سنیور رفت؛ پیش از کلیک باید Redirect URIِ پنلِ بازار همmathapiشده باشد. -
۲۰۲۶-۰۹-۲۷ عصر: سنیور یک Client تازهی بازار داد (Redirect روی
mathapi)؛ در.envسرور جایگزین شد (بکاپ:.deploy-backups/env-before-bazaar-client2-20260927-214302)، ریاستارت ۲۱:۴۳، و لینکِ «اجازه»ِ تازه با همین Client ساخته شد (لینکهای قبلی باطل). به گفتهی سنیور «شناسهی توسعهدهنده» هنوز در بررسیِ بازار است و تا آن تمام نشود لینک خطا میدهد. -
علتِ
-14ِ زرینپال پیدا شد (فراخوانیِ زنده، ۰۹-۲۷): مرچنتِ.envما برایtadrisino.orgثبت است — callback رویtadrisino.orgکدِ ۱۰۰ میگیرد و هر آدرسِtadrisino2.ir(باmathapi)-14. بکاندِ دیگرِ شرکت (Tadrisino/Tadrisino_backend) callback را رویapi.tadrisino2.irمیفرستد و کار میکند، پس مرچنتِ آن برایtadrisino2.irاست. رفع: همان مرچنت درZARINPAL_MERCHANT_ID، نه تغییرِ کد. کدِ آنها همان فیلدهای ما را دارد جزcurrency(آنها نمیفرستند، یعنی ریال)؛ verifyِ ما هنوز با پرداختِ واقعی آزموده نشده. ۰۹-۲۷ ۲۲:۲۴ انجام شد (به خواستِ صاحبِ پروژه): مرچنتِTadrisino_backendروی خودِ سرور از~/deployment/Tadrisino_backend/MOOC/.envدر.envما کپی شد (مقدار هرگز چاپ نشد؛ بکاپ:.deploy-backups/env-before-zarinpal-merchant-20260927-222314)، ریاستارت، و PaymentRequest با callbackِmathapiحالا ۱۰۰ میدهد. پرداختهای دو اپ حالا به یک حسابِ زرینپال میروند. هنوز آزموده نشده: یک پرداختِ واقعی تا verify. -
فرانتِ بازسازیشده روی
math.tadrisino2.irزنده است (main=2b4db90). دو ایراد فقط روی سایتِ زنده با حسابهای دمو پیدا شد و رفع شد: متنِ بخشِ والدین برای فرزند غلط بود، و حسابِ والد (بیپایه) به فرمِ «کدوم پایهای؟» میافتاد. -
دادهی دمو دستنخورده است (۱۸ کاربر؛ ۶، ۱۰ تا ۱۵، ۱۷، ۱۸ با امتیاز، گفتگو، تیکت و اعلانهایشان). تستِ زنده فقط نشستِ ورود ساخت، هیچ جوابی ثبت نکرد.
وضعیت ۲۰۲۶-۰۹-۲۶ — دستیار روی gpt-4.1-mini
روی سرور: 11bbe1c دیپلوی شد و سرویس واقعاً ریاستارت شد (۰۲:۵۴). OPENAI_CHAT_MODEL
در .env سرور از gpt-4o-mini به gpt-4.1-mini عوض شد؛ بکاپ:
.deploy-backups/env-before-gpt41mini-20260926-025320. پیشفرضِ settings.py هم همین است.
یک سوالِ زنده با مهمانِ تستیِ کاربرِ ۱۸ جواب داد (پیامِ ۴۹: gpt-4.1-mini،
tutor-v5-slash-in-question، ۴ chunk، ۲٫۵ ثانیه).
- چرا: دو دور سنجش (گزارش ۲۳، شرح در
docs/architecture.md). روی ۱۰۲ مسئله با تصحیحِ دستی، 4.1-mini ۹۵–۹۶ و 4o-mini ۸۵–۹۰ گرفت. هر پاسخ ۰٫۱۲ سنت است، ۲٫۹ برابرِ 4o-mini. ورودِ کتاب هم با همین مدل است (OPENAI_VISION_MODEL). - «/» در دبستان هم ممیز است هم کسر. جملهی «در کتاب / ممیز است، نه کسر» فقط به دبیرستان میرود و قاعدهی تشخیص کنارِ سوالِ دبستانی میآید. سه مسئله از ۱۷ هنوز غلط است.
- مانده: پاسخِ دبستانی کسر را گاهی با «/» مینویسد (۵/۶)، که در نمادِ کتاب یعنی ۵٫۶.
یک خطِ پرامپت برای «بر» (v6) آزموده شد و درستی را کمی پایین آورد، پس منتشر نشد
(سرور روی v5 ماند). شرح در
docs/architecture.md؛ راهِ بعدی احتمالاً در فرانت است. - داورِ خودکارِ
rag_model_benchmark(gpt-4.1) حدودِ ۷٪ اشتباه میکند؛ نتیجهی هر سنجش را پیش از تصمیم دستی وارسی کنید.
وضعیت ۲۰۲۶-۰۹-۲۶ — اتصالِ API بازار
7d205ca روی سرور است (ریاستارت ۲۲:۲۱، migration payment.0008؛ /api/health → integrations.bazaar). Client ID/Secretِ بازار
در .env سرور است (بکاپِ پیشش: .deploy-backups/env-before-bazaar-20260926-213108).
Redirect URIِ ثبتشده در پنلِ بازار خودِ /api/payment/bazaar/verify است: GET کدِ بازار را
میگیرد، POST خریدِ اپ. اولین تأیید با حسابِ شخصیِ صاحبِ پروژه زده شد: توکن کار کرد ولی به هیچ اپی دسترسی نداشت (۴۰۳
«You don't have access to this app») و همین نشان داد بازار Bearer را نمیپذیرد. هر حسابِ
بازاری میتواند تأیید بزند؛ فقط حسابِ شرکت به کار میآید. توکن پاک شد و لینکِ تازه (۲۲:۱۸، ۷۲ ساعت)
برای سنیور رفت؛ تا تأییدِ حسابِ شرکت bazaar: false است. اسمهای حدسیِ ir.tadrisino.app /
ir.tadrisino / com.tadrisino روی بازار وجود ندارند. هنوز نداریم: اسمِ پکیجِ اپ (کد حدسِ
ir.tadrisino.app را دارد)، SKUِ پلنها، و اپِ اندرویدیِ دارای Poolakey. آدرسِ سنجشِ خرید با
سندِ فعلیِ بازار مقایسه شد و هر دو آدرسِ سنجشِ خرید غلط بودند (یعنی هیچ خریدِ واقعیای
تأیید نمیشد)؛ درست شدند، بهعلاوهی روشِ جدیدِ بازار (توکنِ ثابتِ پیشخان در
BAZAAR_PISHKHAN_API_SECRET، بیOAuth) که ترجیحی است. سندِ بازار JS است ولی متنش از
developers.cafebazaar.ir/wp-json/wp/v2/document?search=... خوانده میشود. جزئیات در docs/operations.md.
وضعیت ۲۰۲۶-۰۹-۲۵ (عصر) — پیامکِ واقعی روشن شد
روی سرور: 0580aec دیپلوی و سرویس واقعاً ریاستارت شد (۱۲:۳۲). KAVENEGAR_API_KEY
در .env سرور و لوکال است (حسابِ Master، ~۳۲۶ هزار ریال اعتبار در آن روز). بکاپهای پیش از آن:
.deploy-backups/env-before-kavenegar-20260925-122959 و
db-before-otp-hardening-20260925-122959.sqlite3.
- از این به بعد هر درخواستِ OTP پول است. برای همین: شماره به
09XXXXXXXXXنرمال میشود و غیرِ موبایلِ ایران ۴۰۰ میگیرد؛ cooldown و سقفِ روزانهی هر شماره (۱۰) و سقفِ روزانهی کلِ سامانه (۳۰۰، ضدِ SMS pumping) در دیتابیس شمرده میشوند نه کشِ per-worker. تستها درaccounts/tests_otp_limits.py. - مسیرِ کاملِ کاربرِ تازه روی سرورِ زنده سبز شد، بدونِ پیامکِ واقعی: کد مستقیم در
دیتابیس ساخته شد. کاربرِ تستیِ آن کاربرِ ۱۷ با شمارهی
09000000001(نام «کاربر تست»، یک جزیره تمامشده) روی پروداکشن مانده و مثلِ دادهی دمو پیش از کاربرِ واقعی باید پاک شود. - بررسیِ همهی اتصالها روی سرور (همان عصر، فراخوانیِ زنده، فقط بولین/کدِ پاسخ چاپ شد):
GapGPT دوباره اعتبار دارد (دستیار برای مهمانِ تستیِ کاربرِ ۱۸،
deviceId = deploy-check-ai-20260925، با ۴ chunk جواب داد وaiProviderخودشokشد)؛ Qdrant سالم (۱۲ collection). زرینپال: مرچنت معتبر ولی-14«دامنهی callback با دامنهی ثبتشدهی ترمینال نمیخواند» — ترمینال برایapi.math.tadrisino2.irثبت نشده، پس هیچ پرداختی شروع نمیشود؛ رفعش در پنلِ زرینپال است. بازار: هر سهBAZAAR_*خالیاند (فقط برای خریدِ درونبرنامهایِ اپِ اندروید لازم است).CACHE_URLوINTERNAL_SERVICE_KEYخالی (اولی کارِ ادمین، دومی فقط مسیرهای داخلی)، کلیدِ امضا هنوزdjango-insecure(تصمیمِ صاحبِ پروژه). - ⛔ اولین پیامکِ واقعی (۱۲:۳۵، به شمارهی خودِ صاحبِ پروژه) نرسید. API ۲۰۰ داد و کاوهنگار
پذیرفت، ولی وضعیت شد ۱۳ «لغو شده»؛ یک متنِ سادهِ بیکد هم لغو شد، پس ایراد از حساب/خطِ
10009000907070است نه از کد یا متن. سینیور (۰۹-۲۶): حساب سالم است، فقط اسمِ الگو را نداشتیم — این خط کدِ ورود را فقط با «ارسال با الگو» (verify/lookup) میفرستد. کدش آماده است: باKAVENEGAR_OTP_TEMPLATE=<نامِ الگو>در.envکدِ ورود از الگو میرود، بدونِ آن همانsms/sendِ قبلی (که روی این خط لغو میشود). اسمِ الگو هنوز نرسیده. وضعیت را بدونِ SSH باsms/statusbyreceptor.jsonبسنجید (latestoutboxبرای این کلید ۴۰۷ است). - ⚠️ زیاد SSH زدن پشتِ سرِ هم یک بار «Permission denied» با رمزِ درست داد؛ با
ControlMaster(سوکت در مسیرِ کوتاهِ/tmp، macOS سقفِ ۱۰۴ بایت دارد) یک اتصال باز کنید.
وضعیت ۲۰۲۶-۰۹-۲۵ — این را اول بخوان
کیفیتِ دستیار و گیرِ نقشه (جزئیات و دلیلها در docs/architecture.md). ۵۷۹ تست سبز.
روی سرور است: 42783f9 دیپلوی و سرویس واقعاً ریاستارت شد (۲۰۲۶-۰۹-۲۵ ۰۶:۲۶)،
migrationها اعمال شدند، سهیار و nextLessonId از بیرون تأیید شدند. بکاپِ پیش از آن:
.deploy-backups/db-before-audit-fixes-20260925-062458.sqlite3. پوشهی .bench پاک شد.
- پرامپت
tutor-v3-stepwise-plain: متنِ ساده (بی LaTeX/markdown، با تورِ ایمنیِrag/services/mathtext.py)، حلِ کامل و مرحلهبهمرحله با وارسی و «✅ جواب»، لحنِ سهسطحی، حافظهی ۶ پیامِ آخرِ گفتگو، و حذفِ راهحلهایdisagreesاز context. - جزیرهها: فقط درسهای سوالدار شمرده میشوند،
nextLessonId+lessonIds[0]همیشه درسِ بعدی است، و جزیرهی تمامشده تمام میماند. بدونِ این، فرانتِ فعلی (که همیشهlessonIds[0]را باز میکند) هیچ جزیرهای را تمام نمیکرد. - سهیار (
sigma/delta/pi، باز شدن با ۰/۱۰۰/۳۰۰ امتیاز) با migrationprogress.0004؛ کاتالوگ روی سرور خالی بود. /api/healthحالا تمام شدنِ اعتبارِ GapGPT را نشان میدهد (aiProvider).- ⛔ اعتبارِ GapGPT در ۲۰۲۶-۰۹-۲۵ تمام شد (وسطِ
rag_model_benchmark؛ مانده −$0.0016). تا شارژ، دستیار برای همه ۵۰۳ میدهد. انتخابِ مدلِ چت منتظرِ همین است: هزینهی واقعیِ هر پاسخ با پرامپتِ تازه — gpt-4o-mini ۰٫۰۴۴، gpt-4.1-mini ۰٫۱۱۶، gpt-5-mini ۰٫۱۷۱، gpt-5.4-mini ۰٫۲۹، o4-mini ۰٫۴۱ سنت. دقتشان هنوز اندازه نشده. تصمیمِ صاحبِ پروژه (۰۹-۲۵): فعلاً gpt-4o-mini میماند (۰۹-۲۶ با سنجشِ بعدی به gpt-4.1-mini عوض شد) (ارزانترین). اولین کاندیدا برای سنجش بعد از شارژ:deepseek-v4-flash(قیمتِ رسمی از 4o-mini کمتر، ریاضیِ خیلی قویتر، ولی فارسی و حالتِ بدونِ thinkingاش اندازه نشده) وgpt-4.1-mini. تیمِ Rag_backend برای جوابِ ریاضیo3-miniاستفاده میکند. - فرانت: PRِ ما (#3) بسته شد؛ هرچه فرانت باید بکند فقط در
~/Desktop/کارهای-فرانت-تدریسینو.htmlاست (نه PR). شاخهی محلیِzareian/ai-chat-readabilityفقط مرجع است.
بازبینیِ بدبینانهی همان روز و رفعها (۵۷۹ تست سبز؛ جزئیات در docs/architecture.md):
پلن روی سفارش (باگِ «ماهانه بده، سالانه بگیر»)، صفحهی خرید اشتراکِ فعال را
دست نمیزند، تمدید از انقضای فعلی، یک کدِ تخفیف پیش از درگاه و کدِ ۱۰۰٪ = پرداخت،
بازسنجیِ خودکارِ پرداختِ تأییدنشده، حذفِ /api/payment/webhook، پورسانتِ affiliate
روی پرداختِ واقعی (مهمان بیرون)، امتیاز فقط از جوابِ ثبتشده، HEARTS_ENFORCED
(خاموش)، OTPِ ۶ رقمی با ۵ حدس، خروج از حساب (tokens_valid_after)، مهمانِ
ثبتنامکننده همان کاربر میماند، والدِ چندفرزندی، سقفِ ورودِ ادمین، پاکسازیِ نامِ
نمایشی، questionId در دستیار، SQLite با transaction_mode: IMMEDIATE.
عمداً انجام نشد (تصمیمِ صاحبِ پروژه: برای ارائه نقطهی شکست میسازند):
عوض کردنِ DJANGO_SECRET_KEYِ سرور (همه را بیرون میاندازد) و مهاجرت به Postgres.
وضعیت ۲۰۲۶-۰۹-۲۴
سرور روی 81348ce به بعد است (سرویس واقعاً ریاستارت شد)، ۵۳۰ تست سبز، ۶۰ اندپوینت. پروژه در این تاریخ بسته شد؛ فایلهای تحویل (گزارشِ ۲۲، راهنمای ارائه، zipِ بکاند) در ~/Desktop/گزارشات Tadrisino/گزارش-۲۲/ بهروزند. آن روز کلِ
فرانت (MathApp_frontend، upstream main = d223336) صفحهبهصفحه با API زنده
تطبیق داده شد و هرچه سمتِ بکاند کم بود ساخته شد. کارهای ماندهٔ فرانت در
~/Desktop/کارهای-فرانت-تدریسینو.html برای تیمِ فرانت فرستاده شد.
- جزیره با آزمونش تمام میشود:
POST /api/nodes/{id}/quiz/complete(هر نمرهای؛ تصمیمِ صاحبِ پروژه). بدونِ آن دانشآموز بعد از جزیرهٔ اولِ هر فصل گیر میکرد. - «از پی بپرس» بدونِ lessonId هم از کتابِ پایهٔ کاربر جواب میدهد.
- استریک و جدولِ رتبهبندی زندهاند؛ قبلاً هیچجای اپ استریک نمینوشت و جدول فقط با اجرای دستی عوض میشد.
- قلبها مثلِ دولینگو: هر ۵ ساعت یکی، ۲۴ ساعت بعد از اولین قلبِ ازدسترفته همه.
- پلنِ یکساله ۲۹۹٬۰۰۰ تومان با migration ساخته شد؛ تنها مزیتِ واقعیِ اشتراک در کد هنوز «قلبِ نامحدود» است (تصمیمِ محصولی).
- زیرساخت:
NUM_PROXIES=2(ArvanCloud جلوی سرور است)،gunicorn.conf.pyبا gthread ۳×۸ و timeoutِ ۱۲۰ (قبلاً سه چتِ همزمانِ دستیار کلِ API را قفل میکرد)،Cache-Control: private, no-storeروی همهٔ/api/، بازگشت از زرینپال به خودِ اپ، refresh token برای مهمان،PATCH /api/notifications/read-all. - استریکِ حسابهای دمو از ۲۰۲۶-۰۹-۲۵ صفر نشان داده میشود (آخرین روزِ فعالشان ۰۹-۲۳ است) و لیدربردِ هفتگی بعد از ۰۹-۳۰ از آنها خالی میشود؛ این رفتارِ درستِ استریکِ زنده است، نه باگ.
- سرور هنوز روی کلیدِ
django-insecure-…است (۲۰۲۶-۰۹-۲۴ دوباره تأیید شد، فقط بهصورتِ بولین). - دستِ ما نیست: GapGPT کلِ جواب را ~۵ ثانیه نگه میدارد و بعد یکجا میفرستد.
CACHE_URL(Redis db1) به تصمیمِ صاحبِ پروژه به ادمینِ سرور سپرده شد. پورتِ ۸۰ِ سرور بدونِ آروان هم جواب میدهد (کارِ ادمین). - بکاپهای پیش از هر دیپلو در
.deploy-backups/db-before-*-20260924-*.sqlite3.
وضعیت ۲۰۲۶-۰۹-۲۳ (اعداد کهنه)
پروژه در این تاریخ بسته و تحویل داده شد. سرور روی api.math.tadrisino2.ir زنده
است و کامیتِ 5003b8a را سرو میکند (سرویس واقعاً ریاستارت شد، نه فقط فایلها روی
دیسک). /api/health میدهد: grades: 8, lessons: 2617, questions: 2463 و هیچ
مهاجرتِ اعمالنشدهای ندارد.
- هر نودِ پایههای هشتم تا دوازدهم آزمونِ کاملِ چهارسوالی دارد. پایههای سوم/ششم/هفتم کتابِ تیمِ دیگرند: ۳۹ نود از ۱۴۵ کمتر از چهار سوال دارند، ولی هیچکدام خالی نیست، پس روی نقشه جزیرهی مرده وجود ندارد.
- ۴۸۹ تست سبز،
checkوmakemigrations --checkتمیز، وspectacularبدونِ خطا و هشدار. ۵۶ اندپوینت. - تستِ همزمانیِ campaigns که مدتها flaky بود دیگر flaky نیست — علتش یک باگِ واقعی بود، نه طبیعتِ تست؛ پایینِ همین فایل توضیح داده شده.
مستندات در ۲۰۲۶-۰۹-۲۳ بازسازی شد. README.md حالا درِ ورودی است نه دفترچهی
راهنما؛ رویهها رفتند به docs/operations.md (دیپلوی، متغیرهای محیطی، بکاپ و
بازیابی، ورودِ توسعهدهندهها، قواعدِ سرور)، docs/content-authoring.md،
docs/architecture.md و docs/docker.md. قبل از نوشتنِ هر سندِ تازه آنجا را نگاه
کن، وگرنه همان حرف در سه جا تکرار میشود.
سه چیزِ کوچک که در همان روز اضافه شد و دانستنشان لازم است:
TEST_LOGIN_PHONES— فهرستِ «شماره:کد» در.envکه ورود بدونِ کاوهنگار را ممکن میکند. از ۲۰۲۶-۰۹-۲۳ شب، به تصمیمِ صاحبِ پروژه، روی سرور ۷ شمارهی دمو دارد (09120000101تا09120000107). کدها فقط در راهنمای ارائهی خودِ صاحبِ پروژهاند، نه در ریپو. جزئیات درdocs/operations.md.- RAG روی سرور روشن است (همان شب):
OPENAI_API_KEY/QDRANT_API_KEYدر.envسرور،/api/healthمیدهدqdrant: true, openai: true. هر سؤالِ کاربرِ واقعی از اعتبارِ GapGPT خرج میکند؛ سقفِ ۱۵ در روز برای هر کاربر آن را محدود نگه میدارد. - دیتای دمو روی دیتابیسِ پروداکشن، همه از مسیرِ واقعیِ viewها ساخته شده: ۵ دانشآموز
(پایه ۸ تا ۱۲، کاربرهای ۱۰ تا ۱۴)، یک والد (۱۵، وصل به ۱۰)، و حسابِ مهمانِ تستِ فرانت
(
deviceId = frontend-test-device، کاربرِ ۶) که پربارترین است. هرکدام درس، streak، گفتگوی RAG، تیکت با پاسخ و زیرمجموعه دارند؛ لیدربرد باrebuild_leaderboardاز فعالیتِ واقعی ساخته شد.purge_demo_dataهیچکدام را نمیگیرد و باید پیش از آمدنِ کاربرِ واقعی پاک شوند. بکاپِ پیش از آن:.deploy-backups/db-before-demo-20260923-213830.sqlite3. - idِ هر پایه = شمارهی پایه (migrationِ
content.0008): هشتم=۸، دوازدهم=۱۲، و فیلدِGrade.number. پیش از آن هشتم=۵ و نهم=۱ بود. FKِ Unit،grade_idِ accounts/ai_tutor/rag وtarget_filterِ کمپینها همراهش منتقل شدند؛ ادمین و API به ترتیبِ کلاساند. شرح درdocs/architecture.mdبخشِ دیتابیس. - صفحههای
/api/docs/و/api/redoc/از قالبِ خودمان رد میشوند (templates/tadrisino/) تا متنِ فارسی راستبهچپ رندر شود. قالبِ drf-spectacular دستنخورده است و با{{ block.super }}گسترش داده شده.
بکاپ: deploy.sh پیش از migrate خودش بکاپ میگیرد (db-before-deploy-*، ۳ تای آخر، بیبکاپ
migrate نمیشود) و مرحلهی «نگهداریِ بکاپها» بقیهی .deploy-backups را مرتب میکند (بکاپِ دستیِ پیش از
تغییر بعد از ۳۰ روز پاک میشود). کپیِ واقعیِ داده بیرون از ماشین است؛ بخشِ پایین.
⚠️ Gitea از روی سرور: از ۲۰۲۶-۱۰-۰۷ https://gitea.tadrisino2.ir از خودِ سرور (حلشده به IPهای
ArvanCloud) ۵۰۴ میدهد و از لپتاپ ۲۰۰؛ علتش پیدا نشد (مسیرِ آروان↔سرور). deploy.sh در git fetch میایستد.
راه: DEPLOY_BUNDLE (بخشِ «دیپلو» در docs/operations.md)، با مقایسهی هشِ کامیت. مرحلهی «بررسیِ سلامت»
حالا از سوکتِ gunicorn میسنجد و از داخلِ سرور هم کار میکند.
sqlite3 روی سرور نصب نیست — برای بکاپِ دستی با venv/bin/python و
sqlite3.Connection.backup() بگیر، نه با cp.
بخشهای تاریخیِ زیر بهترتیبِ زمان نگه داشته شدهاند چون درسهایشان هنوز معتبر است؛ اعدادِ محتواییشان کهنهاند و ملاک همین بخش است.
تستِ flakyِ campaigns یک باگِ واقعی بود (۲۰۲۶-۰۹-۲۲)
ExecuteCampaignRealConcurrencyTests ماهها بهعنوانِ «flakyِ شناختهشده» کنار گذاشته
شده بود (~۴۵٪ قرمز روی اجرای کاملِ سوئیت). علتش تصادفیبودنِ زمانبندی نبود:
execute_campaign با .iterator() روی کاربرها حلقه میزد، یعنی کرسرِ خواندن در
تمامِ طولِ حلقه باز میماند — حلقهای که وسطش CampaignDelivery مینویسد و در شاخهی
sms یک فراخوانیِ شبکهای به کاوهنگار هم میکند. روی SQLite همین کرسرِ باز جدول را برای
هر نویسندهی همزمان قفل میکند، و قفل تا پایانِ حلقه رها نمیشود — پس
_retry_on_sqlite_lock هیچوقت نمیتوانست نجاتش دهد، هر چقدر هم صبر میکرد.
حالا هر دو شاخه پیش از اولین نوشتن، نتیجه را با list(...values_list(...)) مادی
میکنند. بعد از این تغییر: ۲۰ اجرای پیاپیِ خودِ تست و ۳ اجرای کاملِ سوئیت، همه سبز.
⚠️ درسِ کلی: در این کدبیس .iterator() را دورِ حلقهای که مینویسد یا I/O بیرونی
میزند نگذارید. تا وقتی روی SQLite هستیم، این الگو در پروداکشن هم همان قفل را
میسازد — آنجا بهشکلِ «database is locked» برای درخواستهای بیربطِ دیگر.
تاریخچه — وضعیت ۲۰۲۶-۰۹-۱۷ (اعداد کهنه)
The server is live and running current code. main is at the latest commit, the
service gunicorn-mathapp was restarted against it, and /api/health reports
grades: 6, lessons: 1928, questions: 1441. /api/nodes/1/quiz returns a real
four-question quiz. 435 tests green.
What that took, and the lessons worth keeping:
- The server ran two-day-old code while every file on disk was current. The pull and
seed_contenthad happened; the service had simply never been restarted, so gunicorn still served the code it loaded on Sep 15. WhiteNoise builds its static-file index at startup too, which is whycollectstatichad run and the admin panel still had no CSS. A file on disk is not a deployed file. Checksystemctl show <service> -p ActiveEnterTimestampbefore believing anything. deploy.shreported success while this was true. It health-checked a public URL that the old process answered with 200. It now compares the service start time before and after, and exits non-zero when the restart did not happen.- Two files had been hand-edited on the server (
config/urls.py,deploy.sh), both only to add a trailing slash to the health route. That is whygit pullkept conflicting.api/health/is now registered upstream alongsideapi/health, so the edits were reverted and are not needed again.
Still open, and deliberately not acted on:
DJANGO_SECRET_KEYon the server is thedjango-insecure-…fallback committed inconfig/settings.py. Auth tokens are signed with it, so anyone who can read this repo can forge one. Fixing it means removing the key from.envand from the systemd unit and letting settings generate one. The owner decided this is the server admin's call, not ours.manage.py check --deployflags it assecurity.W009on every release.CACHE_URLis unset, so rate limiting is per-worker (LocMemCache). (A Redis runs on the box, but it belongs to other teams: do not connect to it, owner's decision 2026-10-07. The way out is our own Redis, see the status block above.)- The frontend is still not wired to any API.
src/api/was merged (PR #1) but is imported nowhere;QuestionScreeninApp.tsxstill renders four hardcoded multiplication questions, and PR #3 (zareian/api-client-2) is still open upstream. Until someone connects the screens, none of the 1,441 questions are visible to a user.
The frontend's Question type carries a type field that nothing reads, and
count_objects in Question.TYPE_CHOICES is an empty shell — no rows, no serializer
branch, no rendering anywhere. Do not build content for it without asking first.
تاریخچه — وضعیت ۱۴۰۵/۰۶/۲۹ (۲۰۲۶-۰۹-۲۰) (اعداد کهنه)
سرور روی 3026df0 است، کد و محتوا هر دو منتشر شدند، سرویس واقعاً ریاستارت شد، و
/api/health عدد questions: 1783 میدهد. /api/nodes/1/quiz چهار سوالِ واقعی برمیگرداند.
۵ کتاب روی Qdrant، همه با دیتابیس همگام: ریاضی هشتم (۵۱۸)، نهم (۴۵۰)، دهم (۴۵۸)، حسابان یازدهم (۳۷۶)، حسابان دوازدهم (۳۲۶). صفحهی گمشده: صفر. سه کتابِ تیمِ دیگر (سوم/ششم/هفتم، مجموعاً ۱٬۲۵۲) عمداً دستنخورده ماندند.
خرجِ کلِ آن روز ۳٫۴۴ دلار بود. روشِ ارزان و دقیقِ استخراج در docs/extraction-playbook.md
نوشته شده — قبل از هر ingest تازه آن را بخوان، مخصوصاً بخشِ انتخابِ مدل.
حسابان یازدهم و دوازدهم: وارد مسیرِ بازی شد (۲۰۲۶-۰۹-۲۱)
کاربر تصمیمِ محصولی را گرفت و هر دو پایه ساخته شدند. seed_curriculum_from_textbook
اسکلت را رایگان ساخت — یازدهم ۵ فصل/۲۱ نود/۳۶۹ درس، دوازدهم ۵ فصل/۱۲ نود/۳۲۰ درس — و
سوالها با subagentهای Sonnet نوشته شدند: ۵۵۹ سوال (یازدهم ۳۲۲، دوازدهم ۲۳۷).
هر ۳۳ نودِ هر دو پایه آزمونِ کاملِ چهارسوالی دارند.
GRADE_STYLE حالا برای یازدهم/دوازدهم رنگ دارد (فیروزهای و نیلی، عمداً جدا از شش
پایهی عمومی چون حسابان فقط کتابِ رشتهی ریاضیفیزیک است).
روالِ ساختِ سوال با subagent، حالا ابزار دارد
generate_questions_from_chunks --export DIR جفتهای (درس، مسئلهی تأییدشده) را در
فایلهای JSON میریزد و هیچ فراخوانیِ پولی نمیکند؛ کنارشان یک *-RULES.txt با
عینِ همان PROMPTی که به مدلِ پولی میرفت مینویسد، تا کیفیتِ دو شاخه از هم جدا نیفتد.
خروجیِ subagent با content.question_io وارد میشود (فایلِ بد هیچچیز نمینویسد).
بازده روی ۷۷۸ ورودی: ۶۸۰ سوال، ۸۷٪ — بقیه عمداً رد شدند، بیشترشان مسئلههایی که
بدونِ شکلِ کتاب خودکفا نمیشوند. چند subagent خطایِ حسابِ خودِ کتاب را هم گرفتند
(راهحلی که √ را ۶ نوشته بود ولی ۶٫۵ بود؛ راهحلی که میگفت حد وجود ندارد درحالیکه
داشت) — یعنی solution_verdict='agrees' تضمینِ کامل نیست، فقط فیلترِ اول است.
باگی که همانجا پیدا و اصلاح شد: اجرای دوم با --per-lesson بالاتر همان chunkِ
قبلی را دوباره تحویل میداد و سوالِ تقریباً تکراری میساخت. حالا _targets به اندازهی
سوالهایی که درس از قبل دارد از ترتیبِ قطعیِ chunkها میپرد. این خطا نه لاگ میداد، نه
در import گیر میافتاد (چون صورتِ سوال بازنویسی میشود و رشتهاش یکی نیست).
⚠️ سقفِ هفتگیِ Sonnet واقعی است و ناگهانی میرسد. ۲۰ subagentِ همزمان (هرکدام ~۹۵ هزار توکن برای ۳۰ سوال) آن را در یک نشست پر کرد. اگر کارِ انبوه داری، دستهها را کوچکتر بگیر و بینشان نتیجه را وارد کن — خوشبختانه بیشترِ agentها فایلشان را نوشته بودند و فقط در تماسِ آخر خوردند، پس چیزی از دست نرفت.
چیزهایی که آن روز کشف شد و تکرارشان گران است
- مدل را اندازه بگیر، از جدولِ قیمت نخوان. قیمتِ GapGPT با قیمتِ رسمی یکی نیست:
gpt-4.1-miniهم ازgpt-4oارزانتر بود هم دقیقتر، وgemini-3.6-flashبرخلافِ انتظار سه برابرِgpt-4oگران درآمد.OPENAI_VISION_MODEL=gpt-4.1-miniالان در.envاست و هر پنج کتاب با همان خوانده شد. - ساختِ سوال را با subagent انجام بده، نه با API.
generate_questions_from_chunksپیشفرضشo4-miniاست و ~۰٫۷۷ سنت برای هر سوال میگیرد (چهار برابرِ تخمینِ خودش، چون مدلِ ریزنینگ است). همان کار با subagentهای Sonnet رایگان است و بازدهاش ۹۲٪ بود. روالش: جفتهای (درس، مسئلهی تأییدشده) را به JSON بده، subagent خروجی بنویسد، و باcontent.question_ioواردش کن. --recheckدرrag_verify_solutionsهمهچیز را دوباره میفرستد، نه فقط موارد تازه. برای بازبینیِ یک زیرمجموعه، رأیشان را خالی کن و اجرای عادی بزن.- شمارندهی صفحات حینِ
--refreshمعنی ندارد — هر صفحه اول پاک و بعد خوانده میشود، پس عدد دورِ کل نوسان میکند. پیشرفت را ازIngestedPage.created_atبسنج. - در
untilِ انتظار،ps | grepخودش را میشمارد و حلقه هرگز تمام نمیشود. ازpgrep -fاستفاده کن. - کد و محتوا دو مسیرِ جدا دارند.
db.sqlite3در gitignore است؛ سوالها فقط از راهِseed_content --export --only questionsو کامیتِcontent/seed/questions.jsonبه سرور میرسند. پوش کردنِ کد بهتنهایی محتوا را نمیبرد.
CORS
پورتهای توسعه دیگر شمرده نمیشوند؛ CORS_ALLOWED_ORIGIN_REGEXES هر پورتی روی
localhost/127.0.0.1 را میپذیرد. اگر باز هم گزارشِ CORS رسید، اول بپرس Hard Refresh
زدهاند یا نه — مرورگر نتیجهی preflight را کش میکند.