Commit Graph

244 Commits

Author SHA1 Message Date
e3bddb3757 Trust X-Forwarded-For only from ArvanCloud edges
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>
2026-10-13 18:45:27 +03:30
78c3669119 Docs: app logs archived nightly and kept forever on the backup server
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 17:56:12 +03:30
3559c43902 App logs kept forever: nightly gzip on 249 (LOG_FILE_ROTATION=external), hourly pull to 247 via a forced-command key, health warns if the archive stops
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 17:54:05 +03:30
f4831e93a7 Docs: payment event ledger and its CSV export on the backup server
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 14:47:24 +03:30
44ad2eabf3 Payment events: one central payment_result row for every path (callback, reconcile, Bazaar, renewal, 100% code), discount_applied, no duplicate order_created; Bazaar token never stored
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 14:44:50 +03:30
8d8665edf3 Payment event ledger (PaymentEvent) replicated to the backup server, plus CSV export script on 247
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-13 14:18:47 +03:30
947f946909 Question packs removed at the root; all subscriptions 4 assistant questions a day; Bazaar is subscription-only
- 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>
2026-10-12 20:33:38 +03:30
9c3adc3e60 Subscriptions only: Bazaar panel prices (598/898/1398k), assistant quota 5/3/3 from a cost calculation, packs off sale
- 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>
2026-10-12 19:57:16 +03:30
c0fa46be44 Payment providers as classes (Strategy + factory); 10342 = monthly; health logs a failed Bazaar check
- 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>
2026-10-12 18:21:45 +03:30
76b0ef976e Bazaar: plans cache cleared by migration 0014, enum name override, CLAUDE.md status
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 17:59:43 +03:30
8077ab4350 Bazaar: subscriptions (10343/10344), dynamic price (discount codes on Bazaar), auto-renewal sync
- 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>
2026-10-12 17:53:11 +03:30
2e0753587c Like/dislike + report under every question and lesson, lesson reports and vote columns in admin; practice islands refresh from file; audit fixes
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 12:56:47 +03:30
b075b93f2a Tutor/answers: bare subscripts (S_n, a_1) become Unicode; migration 0022 re-cleans DB text
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 12:18:46 +03:30
e662914efc No LaTeX/markdown anywhere students read (seed, import, DB migration 0021, tutor history, frontend guard); quiz of an island is fixed per user again
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 12:15:28 +03:30
dd10ff83f4 13,689 new quiz questions in 160 practice islands (grades 3-12), Question.figure, replay rotates the quiz, LaTeX removed from lesson/question text
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 11:58:18 +03:30
f9eea56895 CLAUDE.md: tester accounts
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 11:13:51 +03:30
3187f69714 UNLOCK_ALL_PHONES: tester accounts see every island unlocked (map and server rule)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 11:12:13 +03:30
33478179e2 CLAUDE.md: truth tables and island labels checked in the live app
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 10:57:01 +03:30
9c3596a132 CLAUDE.md: what is on the server after the subjects/figures release and what was checked
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 05:44:32 +03:30
8cb1ecb85e Subjects per grade (math, calculus, geometry, stats, discrete), 5 new textbooks, 202 new islands, long islands split, hand-written figures and lesson texts for the new books
- content.Subject + Unit.subject, /api/home?subject=, tutor grounded in the chosen book
- 5 new books ingested; ~1150 hand-written questions, islands named by hand
- node_split: islands over 7 lessons split into parts of <=6 (lesson ids and progress kept)
- Lesson.figures hand-authored and visually checked for 653 lessons of the new books;
  gen_lesson_figures got --subject and survives network errors
- seed files re-exported

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 05:35:18 +03:30
3d51b3b83a CLAUDE.md: capacity under load (measured vs estimated), open launch-day caps, how to load-test the shared server safely
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 22:27:54 +03:30
98bf05fcbd OTP code requests per IP 20 -> 100/hour so a class on one school Wi-Fi can all log in
SMS cost stays bounded by the per-phone (10/day) and system-wide daily caps.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 22:11:08 +03:30
2c708088e4 Spike readiness: AI gets its own slice of threads, cheaper hot paths, classroom-friendly login
- /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>
2026-10-11 20:55:24 +03:30
feaa5eb540 Postgres connection pool, clean 503 under overload, time indexes, 4 workers
Postgres here allows 60 connections, but each of the 96 gunicorn threads held its
own persistent one: at 100 concurrent users (local Postgres with the same limit)
21% of requests were 500 "remaining connection slots are reserved". Now each worker
has a psycopg pool (8), so the same load gives 0 errors and 714 req/s (was 460).

- A full pool answers 503 + Retry-After (DRF handler and middleware), not 500.
- The AI tutor and the OTP SMS call return their connection before the network wait.
- statement_timeout 30 s, idle-in-transaction 60 s; bad pool settings and the old
  DB_CONN_MAX_AGE stop startup instead of failing every request.
- /api/health dbPool: per-worker pool stats and workers x pool + 6 vs max_connections.
- Indexes on the columns global counters filter by time (weekly leaderboard,
  global SMS cap, global AI cap); the leaderboard recomputes once per expiry.
- Worker count moves into gunicorn.conf.py (4 on a shared 8-core box).
- security doc: Redis item closed (server uses our own Redis since 2026-10-08).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 20:21:35 +03:30
d32d98d3f6 Full lesson audit (grades 3-12): 167 lessons rewritten for wrong or missing facts (terminology, wrong answers, incomplete/ambiguous prompts), 115 final answers added, 11 figures rebuilt and 6 removed to match the new text
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-11 15:11:11 +03:30
673a9e7e39 Lessons that depended on a missing figure or table now carry their own data (15 rewritten), figures added for 5; fixes wrong answers in 3638 and 3769
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-11 14:48:51 +03:30
1fe453095f Geometry and measurement lessons: figures for the remaining drawable lessons (shapes without invented numbers), fraction bars and number lines for pure-arithmetic lessons
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-11 14:41:01 +03:30
4a41def4b2 Lesson figures: 2,466 lessons (2,927 figures) now carry a fixed figure; graphs, tables, trees, number lines, area models, pies
Hand-written and visually checked per grade; no API calls. Server-side: seed_content only fills empty cells, so changed figures need the one-off refresh described in the deploy notes.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-11 13:34:18 +03:30
293b916544 Lesson content complete: examples/steps for all 3,778 lessons, 1,775 lessons with fixed figures, 38 quiz fixes
- curriculum.json: example/steps for every real lesson, figures (2,224) hand-written and visually checked
- questions.json: 3,703 questions (24 added, 14 keys fixed)
- gen_lesson_figures: strict check, thin-figure filter, recheck, --max-usd; API figures limited to plane/fraction/numberline/solid

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-11 01:29:36 +03:30
4821df3914 CLAUDE.md: pointer to the next-APK notes and why app payments go through pay.html
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 18:54:12 +03:30
9024c6b8d8 /api/user/summary: streak.activeToday and streak.lost for the app's lit / at-risk / frozen flame
activeToday: activity recorded today (Tehran date). lost: length of a streak that broke (last active day
before yesterday), so the app can show it frozen. Same rule as effective_current_streak; the weekly report
keeps its own two-field streak serializer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 18:27:14 +03:30
2e87a81cc5 CLAUDE.md: pre-move SQLite copy removed from the server; the old August backup script was never scheduled
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 17:14:42 +03:30
d10e3e4c9f deploy.sh: backup retention used ls on a glob that can be empty, killing the deploy under pipefail
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 16:53:57 +03:30
a9cccc6550 Live off-site Postgres copy on 247 replaces the nightly pg_dump; backup retention in deploy.sh
- ops/pgbackup: SSH tunnel + pg_receivewal (slot zareian_247) + daily verified pg_basebackup (keep 7)
  and a restore check, all run on 78.38.35.247 as systemd units
- /api/health reports offsiteBackup (slot active/lag/lost) and warns; OFFSITE_WAL_SLOT setting
- removed statuslog backup_db/backup_verify and their health warnings
- deploy.sh keeps 3 pre-deploy dumps, 3 local patches, drops manual backups after 30 days
- docs: operations backup section, security item 14 closed, CLAUDE.md status

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 16:53:10 +03:30
19072ee534 Fourth companion renamed Tedi -> Omega (ω); slug stays tedi
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 01:42:44 +03:30
a39643b8a3 CLAUDE.md: paywall and subscriptions are live
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 01:20:51 +03:30
64c4c09326 Paywall: first unit of each grade free; 1/3/6-month subscriptions; AI sold separately
- content: unit_is_free/has_unit_access; questions, node quiz, answer and lesson/node
  complete return 402 subscription_required outside the first unit without an active
  subscription. /api/home units carry requiresSubscription.
- subscription.0013: monthly 498k, quarterly 798k, six-month 1,298k toman, unlimited
  hearts, 3 AI questions/day; packs stay on sale for more questions.
- AI_DAILY_QUOTA_FREE default 3 -> 2.
- /api/subscription/status adds planTitle, planDurationDays, aiDailyQuota.
- tests: content/tests_paywall.py; existing unlock-order tests get a subscription.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 01:19:02 +03:30
7d81d193bd Scheduled Postgres backup, restore check, backup warnings in health, payment result logging
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-08 17:32:06 +03:30
a5feee053a CLAUDE.md: Bazaar package name now matches the APK (com.tadrisino.math.app)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-08 14:12:28 +03:30
22c3196343 Close /api/health details and API docs to the public
Health: public gets only ok/error; details need X-Internal-Key. Docs/schema: admin session only.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-08 13:05:19 +03:30
d6529956e1 Docs: Postgres + own Redis are the server's current state
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-08 12:43:48 +03:30
c1cc6be0cd User activity file includes failed logins matched by phone
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-08 12:41:06 +03:30
bda415e579 Admin: per-user activity file (logins, logouts, orders) with CSV export
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-08 12:17:11 +03:30
dbf0ff52d2 gitignore: server-side Postgres/Redis password files
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-08 12:02:06 +03:30
92dcce901b Ticket-reply notification receiver skips raw saves (loaddata made 5 duplicate notifications)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-08 11:59:26 +03:30
ddc6c7c9b7 NodeProgress receivers skip raw saves (loaddata duplicated rows on the Postgres move)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-08 11:54:20 +03:30
042df25738 Postgres support (DB_* env), LoginEvent audit table, Choice.value 300, deploy.sh pg_dump backup
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-08 11:48:48 +03:30
e00f6b80eb CLAUDE.md: lesson figures workflow, cost, review lessons, frontend cache and nginx headers
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 19:48:10 +03:30
b93f43ef02 Lesson figures: 1-based step index, smaller graph ranges, no tick marks; regenerated 4 calculus-12 lessons
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 16:47:35 +03:30
25b3d24eb4 gen_lesson_figures prints prompt/completion tokens
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 16:34:22 +03:30