Commit Graph

86 Commits

Author SHA1 Message Date
133233144e Seed: grade 12 questions (every lesson 4+ per lesson, answers computed in code), minus-sign and doubled-x fixes in grade 10 and 11
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-17 03:35:34 +03:30
a5a93ef6ba Seed: grade 11 questions (every lesson 4+ per lesson, answers computed in code)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-17 03:27:10 +03:30
5f963e2898 Seed: grade 10 questions (every lesson 4+ per lesson, answers computed in code)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-17 03:16:51 +03:30
7bd7c637b4 Seed: grade 9 questions (every lesson 4+ per lesson, answers computed in code)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-17 03:00:27 +03:30
ac96f93abc Seed: grade 8 questions (every lesson 4+ per lesson, answers computed in code)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-17 02:48:33 +03:30
9903e0ec91 Seed: grade 5 rebuilt (every lesson 4+ questions computed in code), wording fixes
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-17 02:30:33 +03:30
759bb0d575 Seed: grade 4 rebuilt (every lesson 4+ questions computed in code), slash decimals fixed in chapter 5
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-17 02:06:21 +03:30
f8a93a37c9 Seed: grade 7 rebuilt (every lesson 5+ questions computed in code), lost-exponent questions fixed
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-17 01:47:44 +03:30
9eae99340d Seed: grade 6 rebuilt (every lesson 5+ questions, answers computed in code), supplementary/complementary text fixed in grades 4/6/7, '/' decimals to '٫', wrong keys fixed
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-17 01:31:28 +03:30
f57deb9c1d Seed: grade 3 rebuilt (every lesson 5+ questions, wrong figures/lesson text fixed), Persian digits and plain fractions across all grades, apply_fixups gains lesson/question_set, ledger save chunked for SQLite
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 21:27:22 +03:30
5fbbd371fe Seed: questions for the 499 lessons that had none; every lesson now has questions (5,406/5,406)
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>
2026-10-16 20:15:13 +03:30
e6e983f970 Figures must not give the answer away: blank titles on new figured questions, 7 trivial questions removed, 7 reworded; apply_fixups tool
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-15 02:21:02 +03:30
733796c25a kind=space (fixed oblique 3D: points, vectors, planes, box) + ~190 figured questions for grade 12 (geometry, discrete, calculus)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-15 01:04:08 +03:30
998b68e4bc Figured questions for grade-12 geometry (conics, systems, vectors: 50 new); docs: figured questions win in the island quiz
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 22:24:53 +03:30
a4e0c576bb Docs brought to the current state: CLAUDE.md rewritten, README formal, handbook and diagrams redrawn
- 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>
2026-10-14 18:45:51 +03:30
ba4ecff29e Server hardening: nginx origin locked to ArvanCloud + CSP, demo logins removed, docs
- 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>
2026-10-13 19:17:11 +03:30
2a03dc81b5 Docs: X-Forwarded-For spoofing closed; Arvan bypass (no WAF) still open
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 18:50:47 +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
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
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
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
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
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
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
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
1d7eb829e2 Oversized request body: 413 with a short warning instead of 500 and an ERROR traceback
Found on the live server (3 MB JSON to /api/auth/login answered 500). Django raises
RequestDataTooBig for bodies over DATA_UPLOAD_MAX_MEMORY_SIZE and would answer 400, but our DRF
exception handler turned every unknown exception into 500 + logger.exception, so each large request
also wrote a full ERROR traceback. Other SuspiciousOperation subclasses now get Django's 400 as well.

Tested: 2 tests (the 413 one fails without the change). Verified on the server after deploy.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:48:03 +03:30
ff370112c0 Rebuild technical-handbook.pdf from the updated Markdown
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:41:59 +03:30
d584d62904 Do not connect to the shared Redis: CACHE_URL stays empty, own Redis is an open decision
The owner decided (2026-10-07) that we do not connect to anything other teams run on the box, Redis
included. CACHE_URL was emptied on the server and our keys were removed from the shared Redis.

- settings: drop KEY_PREFIX / CACHE_KEY_PREFIX (it only existed because the Redis was shared); comments
  say CACHE_URL is intentionally empty and ResilientRedisCache is ready for our own Redis.
- config/cache.py docstring and tests updated accordingly (11 tests, still run against a local redis-server).
- CLAUDE.md: new server rule 8 (touch only our own files and service), status block, historical line
  that said "db1 is free" now says not to connect. README, operations, handbook, security doc (item 9
  reopened as an open decision) say the same.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:41:39 +03:30
c129f64180 Docs: Redis cache behaviour during an outage, what was tested live, CACHE_KEY_PREFIX
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:21:31 +03:30
7929f6bd85 Docs and requirements: signing key replaced and shared Redis cache are live; pin dependency versions
- CLAUDE.md: status block for 2026-10-07 (what changed on the server, how it was checked, what was not).
- docs/security-before-production.md: signing key, shared cache and unit DEBUG items moved to the guard table.
- requirements.txt: ranges replaced by the versions running on the server (pip freeze, 2026-10-07).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:02:51 +03:30
4c601087dc deploy.sh: health check through the gunicorn socket, optional DEPLOY_BUNDLE when Gitea is unreachable
From the server the public URL goes through ArvanCloud and returned 504/000 (2026-10-07), so the
final health step failed even for a healthy service. It now asks the gunicorn socket (path read from
the unit) with our Host header first. DEPLOY_BUNDLE=<file> fetches from a git bundle instead of Gitea,
only when set explicitly; without it an unreachable Gitea still stops the deploy.

Tested: probe_health against a throwaway unix-socket server and a dead URL (bash -n clean).
Not tested: the whole script end to end, until the next deploy.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 13:57:50 +03:30
3e63bd0860 Docs: 7ecb337 is live; record the Gitea 504 from the server and the bundle deploy
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 13:48:20 +03:30
7ecb337263 Finishing a lesson or island needs recorded answers and an unlocked island; auto DB backup before migrate; throttles on 15 more routes
- content: POST nodes/{id}/quiz/complete and lessons/{id}/complete refuse a locked island
  (403 node_locked, same rule as /api/home) and a user who has not answered every question
  (409 answers_missing). Before, only a user with zero hearts and no answers was refused, so
  anyone else finished any island, locked or not, and finishing a locked island's lessons
  unlocked it. A node or lesson with no questions still passes when unlocked, with no XP.
  The client can no longer understate totalQuestions to buy stars.
- throttle_scope on 15 routes that had none (question bank without login, intro, hearts,
  notifications, tickets, plans, subscription status, orders, offers, affiliate, assistant
  history and feedback, logout).
- admin: OTPCode and Session can only be added/changed by a superuser.
- deploy.sh: integrity-checked DB backup before migrate (keeps the last 2); no migrate without it.
- docs and CLAUDE.md updated to say what is verified and what is not.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 13:30:24 +03:30
663252a260 Remove the guest system entirely: no is_guest/device_id, no guest throttles, phone login is the only way in
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>
2026-10-05 18:20:10 +03:30
8835e01595 Remove guest login: every user registers with a phone number
POST /api/auth/session is gone (404). Existing is_guest rows keep refresh and
upgrade-by-phone until the demo/guest rows are purged; the code that serves
them is listed in docs/security-before-production.md §10. Tests that used the
endpoint as a fixture now use accounts/testing.guest_session.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-05 17:38:18 +03:30
03e81abe7e docs: security guards table lists the new tests
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-05 09:37:11 +03:30
a973c41db2 Security review fixes: admin permissions, guest sessions, payment edge cases, assistant leaks
- Admin: refund/cancel/mark-resolved/Bazaar-link actions need change permission; question and
  curriculum import need add/delete; content browser needs view_question; guest device ids are
  shown to superusers only.
- Accounts: registering a guest's phone revokes the guest's earlier sessions; self-deletion keeps
  Redemption, UserOffer and the phone's OTP rows; the public demo guest cannot be deleted
  (DEMO_GUEST_DEVICE_IDS); display-name contact filter covers any separator and social handles.
- Payment: callback without Authority is 400; a second Bazaar purchase on a paid order is 409;
  orders snapshot credits/hearts/days at creation (payment.0011).
- Assistant: a broken diagram no longer makes a streamed answer free; a late off-topic marker
  refuses the answer (not stored, not cached); a disconnect saves the partial answer; only
  KNOWN_COLLECTIONS are searched.
- Infra: secret-key file is created atomically (workers no longer split keys); no default
  INTERNAL_SERVICE_KEY; Kavenegar key and Bazaar token kept out of error logs; Swagger/ReDoc
  pinned; schema throttled; docker compose bound to 127.0.0.1; non-ASCII digit ids are 400;
  ticket text capped at 5000.
- docs/security-before-production.md: remaining open items 24-31 and the guard tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 04:52:25 +03:30
645baf846b Signed payment links, hashed refresh tokens, login-only leaderboard, load test tools
- /api/payment/orders/{id}/pay requires the short-lived signature that paymentUrl carries;
  without it (or with another order's) it is the same 404 as an unknown order
- Session.refresh_token stores the SHA-256 of the token; migration accounts.0007 hashes the
  existing rows so tokens clients already hold keep working
- /api/leaderboard requires a token (guests have one); names of children were scrapable
- RAG mock only for `manage.py test`, not any command with an argument named "test"
- tools/loadtest: user factory, load generator, fake GapGPT+Qdrant, with method notes
- security-before-production.md lists only open items; closed ones become a guards table
  with the tests that hold them. Comments describe current behaviour, not history

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 21:12:28 +03:30
2cf2d14dd4 Security re-check and load test 2026-10-04: 32 threads, data file permissions, patched deps
- gunicorn gthread 8 -> 32 threads per worker: each assistant stream holds a thread for
  the whole answer; with 3x8, about 20 concurrent asks already queued ordinary page loads
  for seconds (up to 23 s at 50 asks locally, 7 s at 20 asks on the live server). With 3x32,
  75 concurrent asks kept page loads under 110 ms (laptop, simulated model and Qdrant)
- deploy.sh: new step closes the project dir to other accounts and sets the database and
  backups to 0600/0700 on every release; user `nobody` on the shared server could read
  db.sqlite3 and every backup
- requirements: python-dotenv 1.2.2, urllib3>=2.8.0 (known CVEs in the installed versions),
  gunicorn pinned to the server's version (was only installed by hand)
- security-before-production.md: items 19-23 (file permissions, DEBUG=1 still in the systemd
  unit, dependencies, frontend headers, guest AI cap per shared IP) and the reproduced
  X-Forwarded-For spoof through the origin IP
- model-prices.md: 0.183 cents per answer from 50 answers in the live load test

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 14:39:11 +03:30
8cb326ba94 Grades 4 and 5, Bazaar purchase from the app, pack SKUs
- rag: MATH_4 / MATH_5 textbooks (TOC and page offsets read from the PDFs)
- content: grades 4 and 5 in the seed files: 7 units / 28 nodes each,
  204 + 194 questions (every node >= 4), all re-solved blind once
- subscription: Plan.bazaar_sku (admin); POST /api/subscription/purchase
  accepts paymentMethod=bazaar for packs, rejects unknown methods (was ignored)
- payment: idempotency key cannot switch provider; discount codes refused
  on Bazaar orders (Bazaar charges the fixed SKU price)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 20:01:44 +03:30
ab5e53730a Audit fixes 2026-10-03: seed keeps admin edits, plan cache, refresh limit, payment and tutor guards
- seed_content no longer undoes admin work: a SeedRecord ledger (content/seed_sync.py)
  stops deleted/renamed/re-worded content from being recreated and keeps admin-edited
  lesson text; new rows in the seed files still arrive (migration content.0010)
- plan list cache is invalidated on Plan save (receiver used a stale 'v1' key)
- token refresh per-IP cap 30/hour -> 600/hour (TOKEN_REFRESH_IP_RATE)
- a failed gateway result only changes a pending order (forged NOK no longer
  overwrites refunded/canceled)
- tutor gets the correct answer only after the student has answered
- support reply notification moved to a TicketMessage post_save signal
- global daily cap on paid AI questions (AI_GLOBAL_DAILY_MAX, default 3000)
- Bazaar verify uses only the order's SKU; bazaar orders require a SKU
- /api/health warns on the committed signing key; deploy.sh saves local commits;
  400 for bad ?unit= / ?period=; account delete keeps the referrer's Conversion
- earlier today: no stub answers outside DEBUG, ?grade= removed, logged fallbacks

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 16:05:27 +03:30
7307bfcadb Remove silent fallbacks found in the 2026-10-03 audit
- /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>
2026-10-03 10:38:42 +03:30
7683920b0b docs: handbook, diagrams and guides brought up to 2026-10-02
- technical-handbook: question packs instead of subscriptions, real
  hearts with the new wait screen (screenshot), math-only tutor and what
  is charged (new 9-7), admin cancel/refund, Bazaar planId, account
  deletion, 59 endpoints, 779 tests, 14 apps, status and open decisions.
- Diagrams redrawn: use-case (buy a pack, refunds), data model
  (QuestionCredit, HeartsPass, Profile.birth_date), pack payment sequence.
- The PDF is now built from the Markdown by docs/build_handbook.py
  (pandoc + Chrome, font in docs/assets); the hand-made HTML it used to
  come from was not in the repo and had been lost.
- architecture, operations, security-before-production (item 13 closed),
  ai-cost-and-guardrails and README follow the same changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 23:10:50 +03:30
d7d168e82f admin: cancel a pending order, refund a paid one
There was no way to take an order back: staff could only edit the
status by hand, and turning 'paid' into 'failed' left the credits,
heart days or subscription days the order had given. Two OrderAdmin
actions now do it:
- cancel a pending order (a payment that does arrive later still counts);
- refund a paid order, after a confirmation page: takes back the pack's
  unspent credits (never below zero), its heart days, or the plan's
  subscription days, and undoes the referrer's commission.
The money itself is returned in the Zarinpal / Bazaar panel; the action
says so. Status is read-only in the order form. Steps in
docs/operations.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 22:37:30 +03:30
0f0b779a4e docs: plan with 700 toman per question; pack profit margins
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 13:19:06 +03:30