- ?orderId must be numeric; every id in an API path is encodeURIComponent'd.
- The payment sheet shows success only from GET /payment/orders/{id}; a failed lookup says the
status is unknown instead of "paid".
- A 401 from /auth/refresh is not a sign-out when another tab already stored a rotated token.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- muted text #9e9ec0 -> #727299 (contrast 2.6 -> 4.5 on white); tab labels 11px
- map: chapter titles wrap to two lines instead of being cut off
- reduced-motion respected (MotionConfig + CSS), focus-visible ring,
no pull-to-refresh, short vibration on correct/wrong answers
- src/lib/bazaar.ts: bridge to the Android WebView wrapper; checkout uses
Bazaar IAP only inside that app, Zarinpal everywhere else (unchanged)
- android/: WebView wrapper with Poolakey; written but never built
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A 429, 5xx or network failure during token refresh used to sign the user
out. Behind one school Wi-Fi or mobile CGNAT the backend's per-IP refresh
limit was hit, and phone users were sent back to SMS login. Now those cases
keep the session and surface as a retryable error.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The wait screen counts down to the next heart second by second, fills
the first heart as time passes, shows when all five are back, and asks
the server when the time comes; 'continue' reloads the quiz. Packs and
'ask Pi meanwhile' (asking costs no hearts) are offered as ways on.
- The server now also refuses to finish a lesson/island without hearts
and answers (403 no_hearts); the quiz shows the same screen then.
- Invoices know the two admin-set statuses, canceled and refunded.
- Account deletion waits for all six digits of the SMS code.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- profile shows "User482917" under the name, no copy button
- plan cards start with "N questions a day from Pi (free: M)" taken from the plan itself
- "ورود با حسابِ دیگر" signs out cleanly instead of keeping several accounts on one device
- "حذفِ حساب": SMS code for phone users, typing "حذف" for guests
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- the parents section is gone: no parent login, invite codes or "I'm a parent" button;
"گزارشِ والدین" in the profile shows the week against the previous one (praise line,
six tiles, minutes-per-day and right/wrong-per-day charts, a table of the numbers)
- birth date as day / month / year selects (Jalali, leap Esfand handled) instead of age;
adults (19+) are asked which grade's math they want to learn
- user ID under the name, copyable
- "پرداختها و فاکتورها": every order with invoice number, discount, amount and reference code
- tutor: when the free daily quota runs out, a button to the subscription page instead of "retry"
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Measured by rendering every question, option and several tutor answers in
Chrome and comparing the on-screen x of each number (2026-09-29):
- A list inside a sentence ('از −۳ به −۲، −۱، ۰ میرسیم', 'الگویِ ۲، ۵، ۸')
was locked as one left-to-right run, so all 482 read backwards. '،' and
'؛' outside brackets now split the run; sets and coordinates in brackets
stay one run. Locked runs are isolates (isolate-override), otherwise the
comma between two of them was still pulled left-to-right.
- Without MathText 704 of 931 minus signs and 602 of 983 expressions were
misplaced; lesson and island titles, the lesson intro, unit blurb,
notification bodies and ticket messages now go through it.
- The chat box takes its direction from each line's first letter, so a
line of pure math stays left-to-right while typing. A line that starts
in Persian can still show its math reordered inside the box; a textarea
cannot isolate part of its text. The sent message renders correctly.
Four expressions with Persian words inside them ('f(x) = (ریشه دوم x) + ۵۰')
remain ambiguous.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The model provider holds the whole answer and sends it at once; a full
answer with a diagram took about 30 s on 2026-09-29. A fixed 'thinking'
line for that long looked stuck. The stage times are approximate and not
tied to what the server is actually doing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Every screen but home is its own chunk and preloads when the browser is
idle; React and motion get their own long-cached chunks. The app entry
goes from 505 kB (160 kB gzip) to 85 kB (27 kB gzip).
- Quiz prompts, options and lesson examples go through MathText, so
'10 + 3 - 7 - 2' reads from the left; an unpaired bracket at the edge of
a math run stays with the Persian sentence.
- Support lists tickets from GET /support/tickets instead of this
browser's localStorage; a ticket's messages load when it is opened.
- '·' next to a Persian zero read as another digit ('فصلِ ۱ · ۰' looked
like ۱۰۰); a thin bar separates them now. Notification bodies show
Persian digits.
- A root or vertex the model already marked with coordinates is no longer
labelled a second time on top of it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Found in a real chat with gpt-4.1-mini: the model writes a line's coefficients
as ax + by + c = 0, so eq now means that and its own earlier outputs render
right; it can also just copy the equation text (3x + 5y = 4, y = 12x - 16).
regularPolygon builds the vertices (the model's own pentagon was not regular),
exteriorAngle draws the extension of the side (the model always drew the
interior angle). Named points no item refers to are no longer drawn, and a
computed root or vertex skips its label when the model already put a point there.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- src/diagram: plane (constructed points, graphs with computed roots, vertex,
extrema, tangent, asymptotes, piecewise limits, regions, rotations and
reflections, line from ax + by = c), number line with intervals and stacked
fractions, solids with hidden edges, bar/line/pie charts, sign tables, Venn,
arrow mappings, probability trees, fraction bars and mixed numbers, grids,
groups, clock. Pure-math text is bidi-overridden: Persian digits used to turn
3 × 4 = 12 into 12 = 4 × 3 even with dir=ltr.
- MathText locks math runs inside Persian sentences in chat bubbles and figure
captions, so -1 no longer shows as 1-.
- Family screen: a student sees only the invite-code form; the parent form is
behind a link.
- Dev-only diagram gallery at ?gallery (not in the production build).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Home is now the island path of every unit with one clear Start button on the
current island; the six-card dashboard is gone.
- Start opens /nodes/{id}/quiz (4 questions) instead of one-question lessons, so
an island takes one round instead of ~11. Lessons stay one tap away in the sheet.
- Tutor answers render the backend's validated diagram (plane, number line,
fraction) as React SVG; function expressions go through a small parser, no eval.
- Tedi, the mobile-preview mascot, joins as the fourth companion with all six moods
and an onboarding slide.
- Light polish: 3D buttons, header gradients, warmer page background.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Kavenegar sends the SMS early but answers our API up to 16 s later
(2026-09-27), and the login screen waited for that answer. The server
stores the code before calling Kavenegar, so the code works immediately
(checked against a local server with an 8 s send: login 200 at 1.5 s).
Real failures still show on the code step, and resend unlocks at once.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A parent account has no grade, so the app sent the demo mother to the
"which grade are you in?" form and she never reached the parents screen.
Now an account with a name, no grade and a parent-side family link opens
straight on the parents screen (with a logout button, since it has no tab
bar), and the setup form offers "I'm a parent, not a student" for new ones.
The header's back button only shows when there is somewhere to go back to.
Found on the live site with the demo accounts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A linked child (demo user Ali) was told "you are linked to Ali" and the
screen asked for the parent-only weekly report, which 404s. GET /family/link
does not say which side the caller is on, so the role now comes from the
weekly report: it answers only for the parent. The child sees a line saying
their parent follows their progress and no invite/accept forms. Found on the
live site with the demo accounts. deploy.sh now prints the full API URL.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The API moved to mathapi.tadrisino2.ir on 2026-09-26; api.math.tadrisino2.ir
no longer resolves. .env.production is the only place that names it.
deploy.sh fetches, runs npm ci and the type-check, builds into dist.next and
swaps it in only when everything succeeded, so the site is never served half
a build. The previous build stays in dist.prev for rollback, and a
hand-edited dist/config.js on the server survives the release.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The whole app lived in one 3,071-line App.tsx on a Figma scaffold (60 unused
packages, 45 unused UI components, no type-check). The live site could not
reach the backend, Persian-keyboard digits were erased from the phone field,
guests were signed out after an hour, "mark read" hit a method the server
does not accept (405), and iOS Safari blocked the payment window.
What changed:
- src/ split into api/ (one typed client: timeouts, GET retry, single-flight
refresh, SSE), screens/ (one file each), components/, state/, nav/, lib/.
- The API address is configuration only: public/config.js at runtime, then
VITE_API_BASE_URL at build time, then same-origin /api. No domain in code.
- Responsive shell instead of a fixed 390x844 phone frame; self-hosted
Vazirmatn instead of Google Fonts; strict TypeScript, `npm run build`
type-checks first.
- Everything on the handoff list: tutor answers keep their line breaks, have
thumbs up/down, follow-ups, "from the book" badge, retry, history that
survives tab switches and reloads, "why was I wrong" from a wrong answer
with questionId; question reports; hearts with maxHearts/unlimited and a
no-hearts screen with the refill time; result screen and a visible error
when lesson completion fails; subscription status, badge, discount code
before the gateway and the payment-return message; guest refresh token;
6-digit OTP with resend timer; age saved as a Jalali birthYear; PATCH
read-all; leaderboard refetched on open with "you" highlighted; selected
companion shown across the app; logout that revokes the refresh token.
- New screens for endpoints that had none: profile, streak calendar,
support tickets and bug reports, parents, invite-a-friend (with ?ref=
tracking), guest-to-phone upgrade.
- Browser/phone back button walks the screen stack.
Checked with a headless-Chrome walkthrough against a local backend at
390x844 and 360x740 (fresh guest: onboarding, setup, a full lesson, map,
island sheet, every tab and sub-screen, one real tutor question, reload,
logout): no console errors, no failed API calls. Real SMS login and a real
Zarinpal payment were not exercised.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
اولین نسخه از روی حافظه و حدس نوشته شده بود و سه صدای مهم اصلاً کار نمیکردند.
این بار هر مسیر و هر نامِ فیلد از OpenAPI واقعیِ سرویس گرفته شد و بعد کلِ
کلاینت در برابرِ یک بکاندِ در حالِ اجرا تست شد.
چیزهایی که غلط بودند:
• ثبتِ پاسخ به /lessons/{id}/answer میرفت — چنین مسیری وجود ندارد.
مسیرِ درست /questions/{id}/answer است و فیلدش selectedOptionId، نه
questionId/choiceId.
• /ai/ask فیلدِ userQuestion میخواهد، نه question. با نامِ غلط، پرسش
اصلاً به مدل نمیرسید.
• فیدبک isHelpful بولین میخواهد، نه رشتهی up/down.
• UserProfile فیلدهای xp/coins/streak را نداشت (آنها در /user/summary
هستند) و isGuest/phone را نداشت.
• شناسهها در پاسخها رشتهاند، نه عدد.
پاسخِ دستیار هم usedChunks دارد نه sources؛ خالی بودنش یعنی از کتاب چیزی
پیدا نشده، که همان چیزیست که برچسبِ «بر اساسِ کتاب» به آن نیاز دارد.
questions.report تازه اضافه شد: بکاند حالا POST /questions/{id}/report
دارد تا کاربر بتواند ایرادِ خودِ سوال را گزارش کند.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
اپ تا الان هیچ درخواستی به سرور نمیزد و همهی دادهها داخلِ خودِ کامپوننتها
هاردکد بود. این کامیت لایهی ارتباط را اضافه میکند، بدونِ اینکه هیچ صفحهای
را تغییر دهد.
عمداً فقط فایلِ جدید اضافه شده و App.tsx دست نخورده است، چون کارِ در جریانِ
برنچِ updateApp روی همان فایل است و نباید با هم تصادف کنند. وصل کردنِ
صفحهها به این لایه کارِ بعدیست.
هیچ پکیجِ جدیدی اضافه نشد — فقط fetch استاندارد.
چیزهایی که در client.ts هست برای حالتِ خراباند، نه حالتِ خوب:
• timeout روی هر درخواست — وگرنه بکاندِ کند سوکتها را باز نگه میدارد تا
مرورگر به سقفِ ۶ درخواستِ همزمان بخورد و اپ قفل شود.
• backoff با jitter — بدونِ jitter همهی کلاینتها بعد از یک خطا همزمان
دوباره میزنند و موجِ دوم از موجِ اول بدتر است.
• تکرارِ خودکار فقط روی GET؛ POST فقط وقتی Idempotency-Key دارد (ثبتِ پاسخ،
سفارش) تا دابلکلیکِ کاربر دو بار XP یا دو سفارش نسازد.
• refresh تکنسخهای — ۲۰ درخواستِ همزمانِ ۴۰۱ نباید ۲۰ بار refresh بزنند.
• ادغامِ GETهای یکسانِ در جریان.
• احترام به هدرِ Retry-After که بکاند موقعِ throttle میفرستد.
credentials: 'omit' عمدیست: احرازِ هویت توکنِ Bearer در هدر است و سمتِ سرور
CORS_ALLOW_CREDENTIALS=False تنظیم شده.
.env هم به .gitignore اضافه شد؛ قبلاً فقط node_modules داخلش بود و آدرس/
تنظیماتِ محلی میتوانست ناخواسته کامیت شود.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>