998 four-choice questions (2 per lesson), written from each lesson's own text,
worked example and the book's extracted problem. Mostly grades 3, 6, 7 (books
imported before us), plus 4, 5 and a few in 8-12. Hands-on lessons (drawing,
measuring, folding) got text questions that test the same idea.
Checked: structure (4 distinct choices, no LaTeX), a blind re-solve of all 998
against the key (no mismatch), and content.tests_seed_content seeding the full
file. Not checked: an independent second solver.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- CLAUDE.md: 1,225 → ~430 lines, current state only (the dated status log stays in git history).
Server rules moved to the top: what is ours on 249/247 and what is not, non-negotiable rules,
announce-before-acting, password handling, and the full "deploy it" procedure with verification.
- README: formal entry page with current numbers (10 grades, 18,522 questions, 61 API operations,
973 tests, PostgreSQL/Redis).
- Removed docs/frontend/ (Figma-era guide with guest login and its own api.ts; the frontend repo has
the real client). Swagger's external-docs link now points at the frontend repo.
- architecture/operations/security/content-authoring/loadtest/textbooks: SQLite, guest login,
demo accounts, question packs and the port-80 note replaced by what is true now.
- Technical handbook text updated throughout; all seven diagrams redrawn (PostgreSQL, phone-only
login, subscriptions, current data model); PDF rebuilt.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
- ops/nginx/: the two nginx sites as they now run on 249. Only ArvanCloud
edges reach the app (direct hits on the IP get 403, so Arvan's DDoS/WAF
can't be skipped); the frontend gets CSP (own sources only, the one inline
script by hash), X-Frame-Options, nosniff, Referrer-Policy,
Permissions-Policy, HSTS; API body limit 100M -> 20M plus HSTS. Checked from
outside and in Chrome (all tabs and an island quiz, logged in): no CSP errors.
- Demo accounts (users 11-15, 25) and TEST_LOGIN_PHONES removed from the server;
the health warning for fixed-code logins now has a test and says how to
test instead (an OTPCode made in the admin).
- SMS daily cap on the server is 2000 (launch of ~600 users).
- CLAUDE.md: never run tests with --parallel on macOS, always --noinput
(both hang forever without an error).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nginx on the server also answers on its own IP. A direct request with a fresh
X-Forwarded-For each time got a new throttle identity every time and skipped
every per-IP limit (OTP request/login, admin login): reproduced on the server
2026-10-13, 65 of 65 health requests with rotating XFF got 200.
TrustedProxyMiddleware reads X-Real-IP (set by nginx from the TCP peer) and,
when the peer is not in TRUSTED_PROXY_NETWORKS (default: ArvanCloud's
published list), replaces XFF with the peer address. Logs a WARNING once an
hour per untrusted peer that sent a proxied XFF, so a new Arvan range shows up.
Also ignore the parallel test DB clones (.test-db_N.sqlite3).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- subscription.0017/0018, payment.0013: pack plans deleted, QuestionCredit/HeartsPass models and Plan.kind /
question_credits / hearts_days / Order.question_credits / hearts_days dropped. The only paid pack order was the
company owner's test account (user 26).
- Assistant: no credit spending; quota is the daily cap only (free 2, every plan 4, gift subscription 4).
- Bazaar: verify_purchase is subscription-only (no inapp guess), start/purchase responses lose productType,
verify refuses an order without a subscription. A paid order with no subscription now logs ERROR.
- Removed POST /api/payment/orders (internal; no caller). Its permission tests stay.
- Pack-based tests rewritten on subscriptions (refund, security, Bazaar token); pack-only test files deleted.
- docs: profit table for 4/day on both payment routes plus free users; handbook, operations, CLAUDE.md current.
949 tests green.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- 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>
- payment/providers.py: ZarinpalProvider / BazaarProvider with one interface (check_plan, sku_for, payment_url,
purchase_extras, discount_blocker, reconcile_user); provider_for / provider_for_method pick one; unknown fails.
create_order, purchase view, discount redeem and reconcile_user_orders use it instead of scattered ifs.
- subscription.0015: Bazaar SKU 10342 on the one-month plan (owner: "probably the monthly one").
- config/health: the Bazaar check no longer swallows its exception without a log.
982 tests green.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Purchase with paymentMethod=bazaar now works for subscription plans with a bazaar_sku (not only packs);
response carries bazaarProductType. Migration subscription.0014 puts 10343/10344 on the 90/180-day plans.
- Verify asks Bazaar for the right product type (from the order) and stores validUntil (payment.0012).
- POST /api/payment/bazaar/orders/{id}/start: SKU, type, and an HS256 dynamic-price JWT when a discount
code lowered the price (BAZAAR_DYNAMIC_PRICE_KEY); discount on Bazaar orders allowed only with that key
and only before start.
- Auto-renewal: sync_bazaar_renewals re-asks Bazaar when the paid period is near its end (status page and
the paywall via has_subscription_access); each new period becomes a paid order.
- Default BAZAAR_PACKAGE_NAME = com.tadrisino.math.app; health shows bazaarDynamicPrice.
979 tests green. No real Bazaar purchase or renewal tested.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>