plain_persian() strips kasra/fatha/damma/shadda/sukun/hamza-above and the written «هی» ezafe, keeping tanwin. Applied
at output, not in data: PlainPersianJSONRenderer runs one byte-level pass over every JSON response (~5 ms per MB
locally), and to_plain() cleans tutor answers including the stream. Rewriting titles in the DB/seed would have broken
question binding by title path and SeedRecord keys. Tests updated to expect the cleaned text; config/tests_plain_persian.py.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- /api/ai/ask: at most AI_MAX_CONCURRENT_PER_WORKER (12 of 32 threads) answers per worker;
beyond that 503 assistant_busy + Retry-After before any quota is reserved. Without it a
burst of questions (3-40 s each) took every thread and the map and quizzes queued behind.
The slot is held until the stream is actually finished or closed.
- Answer submission 21 -> 14 queries, 15.4 -> 11.0 ms: one query for all choices, locked
get_or_create for node progress and streak, and today's streak recorded once per day
via the cache instead of a transaction per answer.
- /api/home 12.4 -> 8.6 ms: the user's completions are read by user_id instead of an IN list
of ~600 lesson ids; the dict is returned directly (serializer stays the Swagger contract,
HomeMatchesItsSerializerTests keeps them equal).
- otp_login per-IP 30 -> 300/hour: a class of 30 on one school Wi-Fi filled it; guessing is
bounded per code and per phone.
- /api/health aiConcurrency.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
accounts.0008 drops the columns and refuses to run while guest rows exist.
Docs describe the current state; tests that only proved guest behaviour are gone.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- /api/ai/ask: the stub answer is reachable only under DEBUG or
RAG_FORCE_MOCK. Without RAG keys on the server it used to return 200 with
"this is a stub, set OPENAI_API_KEY" and spend the student's quota; now 503.
- /api/home: the `grade` alias is gone and sending it is a 400 instead of a
silent fallback to the user's own grade. No client sent it.
- activate_subscription logs ERROR when the paid order's plan no longer exists
(it silently became 30 days).
- resolve_grade_title logs instead of `except: pass`; curriculum_io no longer
reads an `intro_text` key no file contains.
- CLAUDE.md: "No silent fallbacks" rules for future agents.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
رنگ و آیکونِ مبحث تازه اضافه شده بودند و هنوز هیچکس پرشان نکرده، یعنی
کارتها تا وقتی کسی از پنلِ ادمین وقت بگذارد بیرنگ میماندند.
حالا وقتی فیلد خالی است، مقدارِ پیشفرض از پالت و آیکونهای خودِ اپ میآید
(همان رنگهای طرح و همان آیکونهایی که از lucide وارد میشوند)، تا بومی به
نظر برسد نه وصله.
چرخشی بر اساسِ جایگاهِ مبحث در پایه است، نه تصادفی: رنگی که بینِ دو بارگذاری
عوض شود بدتر از بیرنگ بودن است، و دو مبحثِ کنارِ هم نباید یکرنگ دربیایند.
مقدارِ صریحِ ادمین همیشه برنده است.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
صفحههای فرانت خوانده شدند و با API مقایسه شدند. ساختارِ ما از کتابِ واقعی
درآمده و درستتر است، پس مدل عوض نشد — ولی چند چیز کم داشت.
مهمترینش یک باگ بود: /api/home پارامتر را `grade` میخواند و کلاینتی که به
فرانت فرستادیم `gradeId` میفرستد. پارامترِ ناشناخته بیصدا نادیده گرفته
میشد، یعنی کاربر پایهای را انتخاب میکرد و نقشهی پایهی دیگری میدید
بدونِ هیچ خطایی. حالا هر دو نام پذیرفته میشوند (`gradeId` نامِ درست است،
با بقیهی API یکی). تستِ سرتاسریِ قبلی این را نگرفته بود چون home را بدونِ
آرگومان صدا زده بود.
/api/grades حالا lessonCount میدهد. فهرستِ پایههای فرانت تا دوازدهم است
ولی محتوای ما تا نهم؛ بدونِ این عدد، کاربر پایهی خالی را انتخاب میکند و
صفحهی خالی میبیند بیآنکه بفهمد چرا.
GET /api/nodes/{id}/lessons اضافه شد. /api/home فقط شناسهی درسها را
میدهد، ولی نقشهی مهارت برای هر قدم عنوان، نوع، زیرنویس و «انجام شد» لازم
دارد — بدونش فرانت باید برای هر درس یک intro جدا میگرفت.
Lesson حالا kind (آزمون/ویدیو) و متنِ سهتکه دارد: توضیح، مثال، حلِ
گامبهگام. ویدیو از قبل سوالِ بازِ محصول بود و فرانت آیتمش را دارد؛ حالا
API میتواند بیانش کند، حتی اگر محتوایش بعداً بیاید. Unit یک blurb گرفت و
Node آیکون و رنگ، تا فرانت رنگها را هاردکد نکند. هیچکدام اجباری نیست، پس
محتوای موجود دستنخورده کار میکند.
PATCH /api/user/profile حالا age را هم میپذیرد (صفحهی ثبتنامِ فرانت سن
میپرسد). ذخیرهسازی همچنان birthYear است چون کهنه نمیشود؛ این فقط ورودی
را ترجمه میکند تا دو طرف حسابِ متفاوت درنیاورند.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>