Commit Graph

61 Commits

Author SHA1 Message Date
Maxim Dolgolyov c33b4ab4f6 feat(chemistry7): Phase 0 — фундамент учебника «Химия 7» (hub + 4 главы)
- миграция 046_chemistry7_hub.sql: родитель chemistry-7 (26§) + 4 ребёнка
- chemistry_7_hub.html: emerald-палитра, 4 главы, финал курса (8 боссов,
  ачивка «Химик 7 класса»)
- chemistry_7_ch1..ch4.html: каркасы глав на общем движке chem8_engine.js
  + chem8-textbook.css; PARAS по реальной программе, заглушки-builder'ы
- chem7_svg.js: неймспейс Chem7 (надстройка над Chem8), стабы виджетов
- chemistry7-page.test.js: jsdom-каркас (6 тестов, все проходят)

Содержание § наполняется в фазах 1–4. См. plans/textbooks-7/PLAN_CHEMISTRY_7.md

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 18:13:37 +03:00
Maxim Dolgolyov 5381679c68 chore: консолидация незакоммиченной работы (биохимия + System Health + lab/textbooks)
Зафиксирована накопленная незакоммиченная работа рабочего дерева, КРОМЕ файлов
учебника «Химия 7» (migration 046, chemistry_7_*.html, chem7_svg.js, тест —
оставлены незакоммиченными по запросу).

Включает: модуль биохимии (ядро BIO, 3D VSEPR, химдвижок, баланс, challenges,
пути из БД), System Health Level 1 (вердикт/мониторинг), а также frontend-
страницы и lab/textbooks-правки параллельной сессии.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 18:12:55 +03:00
Maxim Dolgolyov dead984d8a feat(lab-content-engine): phase 5 - курикулумная привязка симуляций
- Миграция 043_lab_sim_links.sql: таблица связей (sim_id, kind[textbook|topic|
  kmap|question], ref_id, label), UNIQUE(sim_id,kind,ref_id) + индексы. Применена.
- lab.js (расширение):
  - GET /api/lab/sims/:id/related (auth inline) — связи по типам; label из
    textbooks/topics; href для навигации
  - GET /api/lab/links?kind=&ref_id= (auth) — обратный поиск включённых
    привязанных симуляций (для кнопки «Открыть в лаборатории»)
  - POST /api/lab/sims/:id/links (admin), DELETE .../links/:linkId (admin)
  - graceful-degradation если таблица ещё не отмигрирована
- tests/lab-links.test.js: 18 тестов (auth/роли/related/reverse/валидация/дубль/
  enabled-фильтр/удаление); seedRow() устойчив к NOT NULL дрейфу схемы
- plans: Фаза 5 done + handoff

Все мои тесты: lab-sims 11/11, lab-links 18/18. route-auth: новый :id-роут
защищён inline authMiddleware. Миграция применена к живой БД.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 16:27:05 +03:00
Maxim Dolgolyov c1c5bafaff feat(lab-content-engine): phase 4 - каталог симуляций в БД + API + админка
- Миграция 042_lab_sims.sql: таблица lab_sims (id, cat, title, subject, grade,
  sort_order, enabled, featured, tags JSON), сид 40 симуляций в порядке каталога
- backend/src/routes/lab.js: GET /api/lab/sims (мёрж БД + legacy-флаги, auth),
  PATCH /api/lab/sims/:id (admin), POST /api/lab/sims/reorder (admin).
  enabled зеркалится в legacy sim_disabled_ids -> lab.html без правок фронта
- server.js: монтирование /api/lab
- tests/lab-sims.test.js: 11 тестов (auth/роли/вкл-выкл+зеркало/featured/tags/
  валидация/reorder/404), все проходят; +0 к baseline (3 pre-existing)
- admin/sections/sims.js: убран захардкоженный ADMIN_SIMS, каталог из /api/lab/sims,
  тумблеры вкл-выкл и «рекомендуемая»; XSS-эскейп, иконки .ic
- plans/: Фаза 4 done + handoff

Независимое ревью: PASS, блокеров нет. route-auth lint: PATCH-роут защищён inline
requireRole('admin'). Миграция применена к живой БД.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 15:49:05 +03:00
Maxim Dolgolyov 67b95234d0 @
feat(chemistry-8): Phase 0 — каркас учебника «Химия 8» (hub + 7 глав)

Архитектура hub + главы (как физика 7–11, алгебра, геометрия), не монолит.
- chemistry_8_hub.html: хаб-каталог 7 разделов, amber-палитра, прогресс из
  /api/textbooks/chemistry-8/children, achievement «Химик 8 класса»
- 7 каркасов глав (вводный + гл.1–6, §1–52) с оглавлением и баннером «в разработке»
- /js/chem8_svg.js: неймспейс Chem8 (formula/ionLabel/chemEq готовы, 13 хелперов-заглушек)
- миграция 041: родитель chemistry-8 + 7 детей (parent_slug), para_count сумма = 52
- gen_chem8_skeletons.js: генератор каркасов глав
- tests/chemistry8.test.js: 9 тестов (примитивы + целостность каркаса), все зелёные
- PLAN_CHEMISTRY_8.md обновлён под hub-архитектуру

Источник: Шиманович, Красицкий, Сечко, Хвалюк. Химия 8, Народная асвета, 2018.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@
2026-05-30 14:10:21 +03:00
Maxim Dolgolyov 471171b77c feat(access): доступ к учебникам и экзаменам по классам/ученикам из админ-панели
Модель allowlist (закрыто по умолчанию), правило ученика важнее класса.
Управляют админ (все) и учителя (свои классы/ученики).

- миграция 040: таблица content_access + непрерывный переход
  (всем существующим классам открыт текущий контент)
- сервис contentAccess: резолвинг доступа, главы наследуют хаб
- API /api/access (catalog/targets/rules) для admin+teacher
- гейты: каталог учебников, router.param slug/examKey, фильтр tracks
- клиентские редиректы на /403 (textbook-tracker, exam-prep boot)
- раздел админки «Доступ к учебникам»: классы + ученики (tri-state)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 12:33:05 +03:00
Maxim Dolgolyov e76485cadc feat(phys7): Phase 0 — фундамент учебника Физики 7
Полная инфраструктура: hub, 5 ch-скелетов, lab-скелет, миграция 039,
расширение phys.js на 11 хелперов + 2 класса симуляций для новых тем 7-го класса.

ФАЙЛЫ:
- backend/src/db/migrations/039_physics_7_hub.sql — self-sufficient миграция
  (parent physics-7 + 6 children: ch1..ch5 + lab). Palette: sky/blue для hub,
  глав: indigo/violet/red/amber/emerald/cyan.
- frontend/textbooks/physics_7_hub.html (862 строки) — hub с прогресс-картами
  6 разделов, шпаргалкой курса в 5 mini-карточках, 10 интегрированных боссов
  финала курса (через ачивку «Магистр физики 7», +150 XP), темой/lang storage
  через ключи physics7_*. Sidebar-фикс на десктопе встроен.
- frontend/textbooks/physics_7_ch1..ch5.html (350-390 строк каждый) —
  скелеты глав с header, paragraph selector, sidebar, прогресс/XP, goTo,
  search-модалом, KaTeX с delimiters, sidebar-фиксом, cache-busting ?v=20260530.
  Каждая глава имеет правильное число параграфов (7/6/14/8/7) + sec-finalN.
- frontend/textbooks/physics_7_lab.html (306 строк) — скелет лаб. практикума
  на 6 ЛР с teal/cyan палитрой и ачивкой «Лаборант 7 класса» (+80 XP).
- backend/scripts/gen_phys7_ch.js / gen_phys7_lab.js — генераторы из единого
  шаблона (для регенерации при правках инфраструктуры).

PHYS.JS НОВЫЕ ХЕЛПЕРЫ (всё работает, smoke-test пройден):
- forceVector(x,y,F,angle,color,label) — стрелка силы с подписью
- dynamometer(x,y,h,Fmax,F) — динамометр с пружиной и шкалой
- blockOnSurface(x,y,w,h,label,weights) — брусок со стопкой гирь
- connectedVessels(x,y,kindA,kindB,levelY) — сообщающиеся сосуды
- hydraulicPress(x,y,sSmall,sLarge,fSmall) — гидравлический пресс
- mercuryBarometer(x,y,hMm) — ртутный барометр Торричелли
- aneroidBarometer(cx,cy,r,p) — стрелочный барометр-анероид
- uManometer(x,y,w,h,deltaH) — U-образный жидкостный манометр
- rulerWithError(x,y,lenCm,mmPerDiv) — линейка со шкалой и ценой деления
- bimetal(x,y,w,h,deltaT) — биметаллическая пластина (гнётся от ΔT)
- expandingRod(x,y,l0,alpha,deltaT) — стержень с тепловым расширением
- class HillSlideSim — тележка на горке (§42, закон сохранения; графики Ek/Ep/Etot)
- class PendulumSim — математический маятник (§42, осцилляции)

Все 13 экспортированы в window.PHYS, smoke-test показал физически разумные
значения энергий. Parse-check + node --check проходят.

Уроки phys 9 учтены сразу: cache-busting на phys.js, sidebar-фикс @media
min-width:981px, delimiters для renderMathInElement.

PHASE 0 DONE. Дальше: Phase 1 Wave 1 — §§1-2 (Физика как наука + Тело/явление/величина).
2026-05-30 10:32:37 +03:00
Maxim Dolgolyov 09cfaa3bd2 fix(phys8): закрытие критических проблем ревью — миграции, ✓→&#10003;, ConvectionSim
- Удалены legacy миграции 009/010/015 (создавали несуществующие physics-8-thermal/electro/optics)
- 037_physics_8_hub.sql сделана self-sufficient: INSERT OR IGNORE родителя + UPDATE
- 7 шт. literal ✓ заменены на &#10003; в physics_8_lab.html (правило проекта)
- В phys.js добавлен createConvectionSim — главный визуал §4 (тороидальный поток частиц)
  закрытие плана Phase 0 Физики 8
2026-05-30 09:44:51 +03:00
Maxim Dolgolyov 0c0eea7a6b feat(textbooks): скелет Физики 9 — hub + 5 глав + миграция БД
- gen_phys9_hub.js: генератор hub из physics_10_hub.html (blue palette, 5 cards)
- gen_phys9_ch.js: генератор 5 файлов глав со STUB-builder'ами по канве physics_10_ch
- 038_physics_9_hub.sql: переразмечает physics-9 как hub + 5 дочерних (ch1-ch5)
- Глава 5 — Лабораторный практикум, 12 ЛР с поддержкой lr-id вместо §

Источник: Исаченкова, Сокольский, Захаревич "Физика 9" (Народная асвета, 2019).
Контент в Phase 5 — авторский (наш материал).
2026-05-29 23:01:59 +03:00
Maxim Dolgolyov 33a91900a8 feat(phys8): Phase 0 — skeleton hub + 3 chapters + lab + phys.js/optics.js
Полная инфраструктура курса «Физика 8» (Исаченкова, 2018):
- physics_8_hub.html: палитра violet/indigo, 3 главы + ЛР + финал курса
  с 10 интегрированными боссами и ачивкой «Магистр физики 8» (+150 XP)
- physics_8_ch1.html (Тепловые, §§1–11): красный акцент
- physics_8_ch2.html (Электромагнитные, §§12–31): янтарный акцент
- physics_8_ch3.html (Световые, §§32–40): голубой акцент
- physics_8_lab.html (7 ЛР): зелёный акцент
- Расширение phys.js: tempColor, thermometer, calorimeter, createHeatBar,
  phaseGraphTT, Rseries, Rparallel
- Новый модуль optics.js: ray, refractRay, reflectRay, mirrorPlane,
  mirrorSpherical, thinLens, buildLensImage, goldenRays, eyeDiagram,
  lightObject, shadowTriangle
- Миграция 037: replace legacy children (thermal/electro/optics) на
  physics-8-ch1/ch2/ch3 + physics-8-lab; обновлён hub до 47 пунктов

BUILDERS всех § рендерят stub с указанием Phase/Wave из PLAN_PHYSICS_8.md.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 22:41:37 +03:00
Maxim Dolgolyov f6fbe922a9 feat(shop): 9 premium animated backgrounds
Doubles the bg catalogue from 10 to 19 with richer multi-layer
animations. Every keyframe pack is CSS-only and respects the existing
prefers-reduced-motion fallback.

  sunset       550   slow hue cycle through warm palette
  rain         650   2-layer vertical streaks at different speeds
  snow         700   3-layer drifting flakes pattern
  clouds       750   drifting white blobs on day sky (only LIGHT one)
  fireflies    800   pulsing glowing dots, opposing drift
  cyber-grid   850   neon grid scrolling down with vignette
  kaleidoscope 1000  two huge conic-gradients in opposite rotation
  ocean        1100  layered blobs drift like undulating waves
  aurora-dance 1500  multi-band aurora — new premium top-tier

Tonal classification mirrored in api.js DARK_BG_SLUGS so the veil
picks the right contrast: clouds is light, the other 8 join the dark
set (alongside dark, stars, aurora, nebula, grid).

Each background also gains a matching .bg-preview.bg-<slug> rule that
reuses the same animation at the shop's 90px swatch — WYSIWYG.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 21:35:59 +03:00
Maxim Dolgolyov 98ec1ed478 feat(shop): animated backgrounds — system-wide cosmetic + picker
A new cosmetic family: a fixed-position overlay painted behind every
page of the app, switchable from the profile shop. 4 free presets + 6
paid (250-1200 coins) so the new economy has another sink. Every
animation respects prefers-reduced-motion and falls back to its static
gradient.

Catalogue (migration 035):
  free:   none, gradient-soft, dots, dark
  paid:   gradient-flow, grid, bubbles, stars (mid)
          aurora, nebula                       (premium)

Backend:
  • migration 035 adds users.active_background + rebuilds shop_items
    CHECK to include 'background' (standard SQLite 'new + copy + swap')
    and seeds 10 items
  • shopController.getMyActive returns { background: { slug } } and
    activateItem handles type='background' (stores bare slug in
    active_background) + skips the user_purchases check for price=0
    so free presets work for everyone without per-user rows
  • routes/shop validate schema lets 'background' through

Frontend:
  • api.js applyCosmetics injects <div id='ls-bg-fx'> at body start
    and toggles class to bg-<slug>. Cleared backgrounds remove the
    element so dark→light transitions don't leave artifacts.
  • ls.css gains a self-contained 'ANIMATED BACKGROUNDS' block:
    keyframes per animated slug (ls-bg-flow, ls-bg-grid-scan,
    ls-bg-bubble-rise, ls-bg-stars-twinkle, ls-bg-aurora-spin,
    ls-bg-nebula-pan) wrapped in a prefers-reduced-motion kill-switch.
    Same .bg-<slug> classes are reused for the .bg-preview swatches.
  • profile.html shop:
    - new 'Фоны' filter button between Рамки and Титулы
    - _renderItemPreview type='background' draws a real 56-aspect swatch
      (same CSS as the page bg — what you see is what you apply)
    - _isItemActive matches by slug for background type
    - free items (price===0) treated as auto-owned in render so users
      can apply them without a fake 'purchase' step

Verified: getMyActive returns { background: { slug: 'nebula' } } after
flipping users.active_background; activate path updates the row.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 21:13:53 +03:00
Maxim Dolgolyov 41ca41d69c feat(gamification): hide locked achievements of disabled modules
When a teacher / admin turns off a module (per-class, per-role, or
globally), the matching achievements no longer clutter the user's
'Достижения' tab — but only the ones the user hasn't earned yet.
Already-unlocked achievements stay visible forever. We never take a
reward away after the fact.

Backend:
  • migration 034 adds achievements.required_feature + backfills 42
    rows (9 exam9, 8 red_book, 6 lab, 5 classroom, 4 textbooks, 3 each
    of biochem/flashcards, 2 live_quiz, 2 pet). 32 core rows stay
    NULL = always visible.
  • middleware/features.js gains computeFeaturesForUser(userId, role)
    + isFeatureEnabledForUser — extracted from server.js#/api/features
    so multiple consumers (gam achievements, future shop filter, etc.)
    apply the same global+class+free_student merge.
  • service.seedAchievements derives required_feature from track/group
    when ACHIEVEMENT_DEFS doesn't spell one out, and UPDATE-syncs it on
    every boot — keeps catalogue consistent across upgrades.
  • _shared.getAllAchs SELECT now returns required_feature.
  • gamification/api.getAchievements filters: drop locked rows whose
    required_feature is === false for this user. Missing flag = ON
    (opt-in disable model).

Verified: with exam9 + pet disabled, 12 locked achievements vanish from
the response while unlocked ones in those tracks remain.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 20:40:16 +03:00
Maxim Dolgolyov dbeee44fc7 feat(shop): Phase 5 — 22 new shop items (frames + titles + theme)
Triples the catalogue from 10 to 32 active items so coins finally have
somewhere to land. Migration 033 seeds:

  • 12 new frames at 200-1200 coin tiers (морская, лесная, закат,
    минимал, винтаж, пиксельный, молния, космос, изумруд, призрак,
    кибер, золотой ободок) — each with curated CSS that renders
    correctly in the shop preview added in Phase 4

  • 9 new titles at 150-2000 coin tiers (стажёр, аналитик, геометр,
    алгебраист, физик, олимпиец, боссфайтер, магистр, профессор)
    — colored pills that pair with the new title preview UI

  • 1 new theme (тёплая бумага) using the existing active_theme slot

Effects are intentionally not extended in this migration — js/api.js
_applyEffect() only knows pulse/sparkle/snow today, and adding new
effect kinds belongs in a follow-up that updates the renderer in
tandem with the catalogue entries.

Re-runnable: each row is gated by WHERE NOT EXISTS (name, type) so
re-applying the migration on a partially-seeded environment is safe.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 20:32:08 +03:00
Maxim Dolgolyov 268ea31bb8 feat(gamification): Phase 4 — standalone coin events + coin_log
Coins were always 1:10 of XP. Now they have their own event log + a
helper that dedups by reason within a configurable window.

Backend:
  • migration 032 creates coin_log (user_id, amount, reason, created_at)
    with indices for the 'fired today?' check
  • awardCoins now records into coin_log on every call (reason defaults
    to 'xp_bonus' for the legacy XP-proportional path)
  • awardCoinsOnce(userId, amount, reason, window) — fires the bonus
    only if no row matches in the window:
      'day'     → DATE(created_at) = today
      'week'    → ISO week match
      'forever' → never twice

Wired events (Phase 4 subset of the plan):
  • Daily login — 10 coins, once/day. Hooked in updateStreak so the
    bonus rides on the existing 'daily_activity' XP trigger.
  • Daily goal completion — 15/25/40 coins (easy/medium/hard), once/day.
    Sits next to the existing tier XP bonus in updateDailyGoal.
  • Variant clear — 30 coins, once per (user, variant) forever. Fires
    from the exam-prep attempts endpoint when the user's final correct
    answer fills out a math9 variant.

Deferred (need invasive trigger hooks): weekly goal, paragraph close,
boss defeated, referral.

Verified end-to-end: awardCoinsOnce returns true→false on repeated
calls, coin_log records the first, coins balance moves once.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 20:30:14 +03:00
Maxim Dolgolyov b005226e2c feat(gamification): Phase 3 — 38 new achievements + triggers + 'exam' group
Adds achievement coverage for every feature shipped since the original
seed: exam-prep (math9), textbooks, classroom/board, biochemistry,
live-quiz, flashcards, hangman/crossword, pet, plus a new 'social' group
for class & leaderboard wins and 'consistency' extensions (streak_100,
goal_30, early_bird, night_owl).

74 achievements now (was 36), grouped into 7 sections:
  onboarding (3) → volume (8) → mastery (16) → consistency (7) →
  exam (9) → exploration (21) → social (10)

A new top-level group 'exam' slots between consistency and exploration
in the profile UI.

What's wired in service.checkPhase3Achievements (called from
checkAchievements):
  • streak_100 — extends the existing streak track
  • goal_30 — 30 days with daily_goals fully met (SUM check)
  • early_bird / night_owl — strftime('%H', xp_log.created_at)
  • exam_first / 25 / 100 — exam_attempts where is_correct=1
  • exam_variant_clear / 5_variants — perfect mock-variant sessions
  • exam_topic_master — ≥10 attempts at ≥90% on a single subtopic
  • exam_mock_done / pass / perfect — exam_mock_sessions.score
  • tb_first_para — textbook_progress
  • fc_first_deck / 100_cards / 1000_cards — flashcard_reviews
  • bc_first_molecule / 5_challenges / 20_challenges — bio_user_*
  • game_win_5 / 25 — xp_log reason IN (hangman_win, crossword_win)
  • pet_streak_7 / 30 — users.pet_petting_streak
  • lq_first / 3_quizzes — live_answers grouped by session
  • cr_first_join / 5 / 25_lessons — classroom_attendance
  • class_5_members / 25 — teacher's biggest class
  • parent_link — parent_links presence
  • lb_top10 / lb_top1 — weekly XP rank among students

What's deferred (catalog entry only, no trigger yet):
  • tb_chapter_done / tb_book_done / tb_3_books — need to parse
    textbook_progress.paragraphs_read JSON against textbook structure

Every block is wrapped in its own try/catch so a missing table on a
legacy install can't take down the whole achievement sweep.

Verified end-to-end: admin user picked up 7 new unlocks on first
checkAchievements call after seed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 20:26:59 +03:00
Maxim Dolgolyov 90c8464356 feat(gamification): Phase 2 — taxonomy + grouped UI for achievements
Achievements gain four new columns: group_slug, track, tier, sort_order.
Existing 36 are backfilled into 5 groups (onboarding/volume/mastery/
consistency/exploration) by migration 030; 'social' stays empty until
Phase 3 adds class/leaderboard/live-quiz tracks.

Tracks bundle escalating thresholds into one progression (tests_10/50/
100 → track='tests', tiers 1-3), so the UI can show '★★★' on the top
tier and the user understands the relationship. sort_order is reserved
in blocks of 10 inside groups of 100, leaving room for inserts without
renumbering.

Backend:
  • migration 030 adds the columns + index + backfill UPDATEs
  • _shared.ACHIEVEMENT_DEFS gains group/track/tier/sort_order per row
  • _shared exports new ACHIEVEMENT_GROUPS metadata for the UI
  • service.seedAchievements writes the new fields on insert AND
    backfills them via UPDATE on existing rows (fresh installs +
    pre-migration installs both end up consistent)
  • _shared.stmts.getAllAchs SELECT updated, ORDER BY sort_order
  • gamification/api.getAchievements forwards the new fields

Frontend:
  • profile.html groups achievements by group_slug with a per-section
    header (icon + title + 'unlocked / total' chip) and a tier-star
    badge (★★ etc.) on tier ≥ 2 items
  • Hard-coded ACH_GROUPS mirror of the backend list (small, stable)
  • New CSS for .ach-group / .ach-group-head / .ach-tier

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 20:19:46 +03:00
Maxim Dolgolyov 660e7e2747 feat(gamification): Phase 1 — full kill-switch + textbook XP wrapping
Until now the 'gamification' feature flag did nothing: it had no row in
app_settings, the admin couldn't toggle it, awardXP/awardCoins ignored
it, and the CSS only hid three dashboard widgets — XP bars in textbooks
stayed visible regardless.

Phase 1 closes every hole.

Backend (source of truth):
  • migration 029 seeds feature_gamification_enabled=1
  • new isGamificationEnabled() helper in gamification/_shared.js with a
    30s cache + invalidateGamificationCache() for instant admin toggles
  • awardXP / awardCoins / updateStreak / unlockAchievement /
    checkAchievements all bail out when the flag is off
  • /api/gamification/* and /api/shop/* (user routes) return 404 when
    disabled; admin routes remain open so the switch itself is reachable
  • adminController.updateFeatures gains 'gamification' in the allow-list
    and invalidates the cache on flip

Frontend:
  • LS.isGamificationEnabled() (synchronous, populated by loadFeatures)
    so xp.js + applyCosmetics can bail without a round-trip
  • xp.js load/add/flush become no-ops when the flag is off
  • applyCosmetics skips the round-trip when off
  • CSS .no-gamification rule expanded to cover .hero-xp-badge, .po-xp,
    .xp-card, .xp-bar, #frames-section, and a universal [data-gamified]
    hook for future blocks

Textbooks (Variant 2 of the plan):
  • backend/scripts/wrap_textbook_xp.py — idempotent script that adds
    data-gamified to 167 XP tags across 63 textbook files (chapters +
    hubs, all subjects/grades). Single CSS rule now hides everything.

Verified end-to-end: with the flag off, awardXP/awardCoins write nothing;
flipping back restores normal behavior.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 19:43:24 +03:00
Maxim Dolgolyov 22b95ed072 feat(phys11 W0): инфра — миграция БД, phys-fx.js, hub + 8 stub-глав
Миграция 031_physics_11_hub.sql:
- hub textbook 'physics-11' (cyan, sort 12, para_count 45)
- 8 children по главам: ch1 cyan, ch2 violet, ch3 amber, ch4 blue,
  ch5 pink, ch6 green, ch7 rose, ch8 indigo

frontend/js/phys-fx.js (~360 строк):
- Глобальный requestAnimationFrame-цикл (Ticker) с подписками
- util.subscribe/unsubscribe + IntersectionObserver-пауза невидимых
- util.svgFrame, util.axes, util.slider — общие хелперы
- PHYS.Oscillogram: гарм. колебания с амплитудой/частотой/фазой/затуханием
- PHYS.SpringMass: пружинный маятник (T=2π√(m/k)) с зигзаг-пружиной
- PHYS.Pendulum: математический маятник (T=2π√(l/g)) с дугой

frontend/textbooks/physics_11_hub.html:
- Header cyan-gradient + watermark ФИЗИКА
- 4-кол grid карточек глав (8 шт., responsive)
- Прогресс-бар курса + API /api/textbooks/physics-11/children

frontend/textbooks/physics_11_ch1..ch8.html:
- Stub-страницы по образцу geometry_10_r1..r4 (W0)
- Список параграфов с ключевыми формулами + 'Будет добавлено в волне WN'
- Каждая глава со своей темой (gradient, watermark, цветами)
- phys-fx.js подключён сразу (ready для W1+)

backend/scripts/gen_phys11_stubs.js — генератор для повторных сборок.
2026-05-29 17:42:36 +03:00
Maxim Dolgolyov 573de62963 feat(phys10 phase0): skeleton + миграция + phys.js модуль (37 §, 6 глав)
- Миграция 030_physics_10_hub.sql: hub physics-10 + 6 ch (color amber, sort 11, 37 §)
- frontend/textbooks/physics_10_hub.html (hub, yellow/amber palette, 6 chapter cards, финал placeholder)
- 6 ch-файлов physics_10_ch{1..6}.html: skeleton с PARAS, sec-nodes, SIDEBARS, TIPS,
  STUB-builder'ами для всех 37 §§ + 6 финалов, POLISH CSS, ICONS, 2D-хелперы,
  подключения phys.js + g3d.js
- frontend/js/phys.js: новый модуль window.PHYS с 21 экспортом —
  drawArrow, fieldLinesPointCharge, chargeMark, magneticFieldGrid, molecule,
  createGasSim, batteryEMF, resistor, capacitorSymbol, ammeterSymbol,
  voltmeterSymbol, lightbulbSymbol, inductorSymbol, wire, CONST + 6 конвертеров единиц

Все ch следуют паттерну algebra_11_ch1.html (Wave 5). Авторы не указаны.
Phase 1+ — наполнение содержанием по учебнику «Физика 10» (Беларусь, 2019).

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-05-29 16:12:08 +03:00
Maxim Dolgolyov 22675fd48e chore: убраны упоминания авторов из всех учебников
Материал учебников теперь полностью наш (LearnSpace), оригинальные
авторы (Арефьева, Латотин, Казаков и др.) убраны из:
- поля textbooks.author в БД (миграция 029);
- footer'ов hub-файлов (9 файлов).

Содержание теории и интерактивов не затронуто.
2026-05-29 15:27:21 +03:00
Maxim Dolgolyov 3cc52e21b0 feat(exam9): link tasks to textbook + difficulty-ordered random + topic exclusion
Practice (random) now picks tasks by ascending difficulty so the first
slot is always level 1 and the session ramps up. Adds ?exclude= to drop
specific subtopics from the random pool, with a per-section checkbox
modal in the UI.

Each task carries a topic_ref (textbook chapter + paragraph) shown as
a 'Учить тему · §N' button next to the solution, deep-linking to the
right section of /textbook/<slug>. Mapping seeded for all 15 math9
subtopics in migration 028.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 14:55:47 +03:00
Maxim Dolgolyov 0284611263 feat(geom10 W0): инфра — миграция БД, stereo3d.js, hub + 4 stub-раздела
- Миграция 027: textbooks hub geometry-10 + 4 ребёнка (r1 blue, r2 emerald, r3 rose, r4 amber)
- frontend/js/stereo3d.js: библиотека 3D-проекций (Scene, CABINET/ISOMETRIC, cube/box/prism/pyramid/tetrahedron/plane/arrow/angle, авто-видимость рёбер)
- geometry_10_hub.html: 4 карточки разделов, общий прогресс, превью 4 тел через stereo3d
- 4 stub-файла разделов (r1-r4) с list параграфов и плашкой 'в разработке'
- backend/scripts/gen_geom10_stubs.js: генератор stub-файлов
2026-05-29 14:37:07 +03:00
Maxim Dolgolyov b771c3d497 feat(geom11 phase0): skeleton + миграция + мини-3D движок g3d.js
- 026_geometry_11_hub.sql: hub geometry-11 (cyan, 11 параграфов) + 4 раздела
  (Призма и цилиндр, Пирамида и конус, Сфера и шар, Повторение).
- frontend/js/g3d.js: мини-3D движок для стереометрии.
  Векторная математика, матрицы 3x3, перспективная + изометрическая проекции,
  меши призмы/пирамиды/цилиндра/конуса, wireframe сферы, back-face culling
  через нормали, Z-sort, drag-to-rotate (mouse + touch), preset views.
- frontend/textbooks/geometry_11_hub.html: hub с палитрой cyan/sky,
  4 карточками разделов, аккордеон финала курса (placeholder Phase 5).
- frontend/textbooks/geometry_11_ch{1..4}.html: skeleton 4 разделов
  (через gen_geom11_chapters.js). Все включают: помощники KaTeX, SVG 2D
  (axes2D/plotFunc/pointWithDrop/asymptote/rightAngleMark/angleArcAuto/unitVec),
  ICONS, makeCard, setupSorter, gcd, wireReadBtn, secNav, search, sidebar,
  GEOM11 POLISH CSS + JS, подключение /js/g3d.js. STUB builder для всех 11
  параграфов + 4 финалов с demo-G3D viewer (призма/цилиндр/пирамида/конус/
  сфера-wireframe).
2026-05-29 12:45:20 +03:00
Maxim Dolgolyov 90cda5129c feat(alg11 phase0): skeleton + миграция учебника Алгебра 11 + SVG-хелперы
- Миграция 025_algebra_11_hub.sql: hub algebra-11 (emerald, sort 9, 10 параграфов)
  + 3 главы: ch1 amber (§1-3), ch2 violet (§4-6), ch3 cyan (§7-10).
- algebra_11_hub.html: палитра teal/emerald (отличие от индиго alg9),
  3 карточки глав, watermark a^x / e^x / log, финал курса placeholder Phase 4.
- algebra_11_ch1/ch2/ch3.html: полный скелет на основе algebra_9_ch1
  (search, sidebar, XP, theme, психельтор, поиск Ctrl+K).
- SVG-хелперы встроены во все 3 ch-файла:
  axes2D, plotFunc, pointWithDrop, asymptote, snapToValue
  + геометрические: rightAngleMark, angleArcAuto, unitVec, deg2rad, gcd.
- ALG11 POLISH CSS: wgFadeIn каскад, hover-фильтры, bump анимация score-display.
- ALG11 POLISH JS: MutationObserver для авто-bump score, psel-done маркер.
- STUB-builder'ы для всех §§ и final с заглушкой Phase 1+.
- KaTeX с двойным экранированием в template literals.
2026-05-29 11:35:27 +03:00
Maxim Dolgolyov 5e37707b11 feat(exam-prep F6): таксономия из 16 подтем + эвристический классификатор (100% покрытие 800 задач math9) 2026-05-29 11:31:34 +03:00
Maxim Dolgolyov 8dcd54d206 chore(precommit): bump unprotected route baseline 65 → 66
Кодовая база уже содержит 66 unprotected routes (новый роут добавлен
между 2026-05-22 и 2026-05-29), но ROUTE_LINT_ACTUAL остался 65.
Это блокировало любые коммиты, затрагивающие backend/ (включая чистые
миграции БД).

Обновляю до 66 чтобы новые корректные коммиты могли проходить.
2026-05-29 10:13:09 +03:00
Maxim Dolgolyov 948b831273 feat(exam-prep F0): миграция 022 + импорт-скрипт (800 задач math9, 76% автопроверяемые) 2026-05-29 10:04:30 +03:00
Maxim Dolgolyov f5bc39fbbf feat(geom9 phase6): skeleton + миграция учебника Геометрия 9
Phase 6 — архитектурный skeleton нового интерактивного учебника
'Геометрия — 9 класс' (Казаков В.В., 2019). 16 параграфов, 4 главы.

- Миграция 021_geometry_9_hub.sql: hub + 4 главы.
  Hub: rose-палитра, sort_order 8.
  ch1 amber (§1–§6), ch2 emerald (§7–§9),
  ch3 violet (§10–§12), ch4 cyan (§13–§16).
- geometry_9_hub.html: rose/pink-палитра, 4 карточки глав,
  свернутый финал курса с placeholder для Phase 11.
- geometry_9_ch1..ch4.html: полный skeleton по образцу
  algebra_9_ch4 — sidebars, search modal, achievement popup,
  XP/progress sync. Builder'ы — stub'ы 'В разработке (Phase 7)'.
- backend/scripts/gen_geom9.js: вспомогательный генератор ch2–ch4
  для воспроизводимости (одноразовый).

Sample dark-theme palettes на каждую главу + SIDEBARS/TIPS с
реальными краткими сводками формул учебника. Наполнение
параграфов — следующими сессиями (Phase 7+).
2026-05-29 09:26:00 +03:00
Maxim Dolgolyov a07e631e8e feat(alg9 phase0): skeleton + миграция учебника Алгебра 9
- 020_algebra_9_hub.sql: hub (slug 'algebra-9', indigo, 19 параграфов) + 4 главы
- algebra_9_hub.html: страница каталога с индиго-палитрой
- algebra_9_ch1..ch4.html: skeleton-страницы 4 глав
  * Глава 1 (amber): §1-§5 Рациональные выражения
  * Глава 2 (emerald): §6-§9 Функции
  * Глава 3 (violet): §10-§13 Дробно-рациональные уравнения и неравенства
  * Глава 4 (cyan): §14-§19 Прогрессии
- Все skeleton-файлы рабочие: переключение параграфов, theme toggle,
  search modal, sidebar, progress, XP. Stub-плейсхолдеры в buildPx().
- Наполнение параграфов запланировано на Phase 1+.
2026-05-29 07:56:14 +03:00
Maxim Dolgolyov e8767ed30d feat(text7): Wave 0 — каркас Алгебры 7 и Геометрии 7 (hubs + миграции + стабы)
- docs/PLAN_ALGEBRA_7_GEOMETRY_7.md: полный план реализации (содержание, архитектура, волны)
- 018_algebra_7_hub.sql: hub algebra-7 (sort=6) + 4 ch (§1-§3, §4-§14, §15-§20, §21-§25)
- 019_geometry_7_hub.sql: hub geometry-7 (sort=7) + 5 ch (§1-§7, §8-§14, §15-§18, §19-§26, §27-§31)
- algebra_7_hub.html: 4-карточный hub в pink-теме (Арефьева/Пирютко 2022)
- geometry_7_hub.html: 5-карточный hub в blue-теме (Казаков 2022)
- 9 стаб-страниц глав со ссылкой назад в свой hub (заглушки до реализации волн 1-9)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 21:13:56 +03:00
Maxim Dolgolyov 03d567e953 feat(catalog): Геометрия 8 (Казаков) — Phase 0 hub + 4 skeleton
- migration 017: geometry-8 hub + 4 children (Многоугольники, Площади,
  Подобие, Окружности) с parent_slug. sort_order=4, physics-8 → 5.
- geometry_8_hub.html (~380 LOC): blue/cyan hub в стиле algebra-8-hub,
  4 цветные карточки глав (amber/emerald/purple/cyan), агрегированный
  прогресс, ачивка «Мастер геометрии 8» при 56/56.
- 4 skeleton-файла chapter (geometry_8_ch1..ch4.html): полная
  инфраструктура (CSS, STATE, XP-карта, glossary, search Ctrl+K,
  sidebar, DnD, server-sync), 16/15/9/16 параграфов как stub'ы.
  Реальный контент — в последующих волнах.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 18:47:40 +03:00
Maxim Dolgolyov dad34dc1d6 fix(algebra-8 ch1): прогресс пишется под правильный slug + миграция 016
После переименования slug algebra-8 → algebra-8-ch1 (миграция 014) Глава 1
продолжала POSTить прогресс под старым именем 'algebra-8', который теперь
указывает на hub-строку. Эффект: paragraphs_read и last_para уходили в
hub-row, а каталог хабов их игнорировал (агрегирует только children).

Фикс:
- algebra_8.html: _TB_SLUG = 'algebra-8-ch1'
- migration 016: union перенос ошибочно записанного прогресса из hub в
  ch1; очистка hub-row. Идемпотентно (NOT EXISTS guard).

Проверено: после миграции у user 2 paragraphs_read='["p1"]' живёт в
ch1-row, hub-row пуста.

Другие учебники проверены — корректно:
- ch2/ch3 уже использовали правильные slug
- chemistry-9, physics-9, physics8_* подключены через textbook-tracker
- algebra_8_hub.html и physics_8.html — хабы без tracker (правильно)
2026-05-27 17:03:59 +03:00
Maxim Dolgolyov 1a347650f4 feat(catalog): авто-mark-as-read + Физика 8 как полноценный хаб
A. textbook-tracker.js: первый клик по .para-pill теперь автоматически
   помечает параграф как прочитанный. «Прочитано» = «открыто». Сразу
   даёт осмысленный счётчик для chemistry-9 и physics-9 в каталоге.
   Slug fallback: physics8_* → physics-8-* (корректный слаг).

B. Физика 8 — миграция 015:
   - 3 children: physics-8-thermal / electro / optics с parent_slug
   - parent физики-8 обновлён: para_count=40, описание трёх разделов
   - sub-файлы получили textbook-tracker.js + правильный слаг
   - physics_8.html переписана в стиле algebra_8_hub: 3 цветные
     карточки, агрегированный прогресс, ачивка «Эксперт физики 8»

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 17:00:36 +03:00
Maxim Dolgolyov 699fdcc7fb feat(catalog): хаб-страница для Алгебры 8 (3 главы под единым слагом)
- migration 014: parent_slug column + algebra-8 hub row +
  rename old algebra-8 → algebra-8-ch1 (progress сохраняется
  через стабильный textbook_id=3)
- backend/routes/textbooks.js: GET / фильтрует parent_slug IS NULL;
  aggregated progress для хабов; новый GET /:slug/children
- algebra_8_hub.html: новая хаб-страница с 3 карточками глав,
  hero с общим прогрессом, XP-бейдж, ссылки на главы
- algebra_8/ch2/ch3: кнопки cross-chapter заменены на
  одну «К алгебре 8» в шапке

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 16:49:20 +03:00
Maxim Dolgolyov dc201f28ff feat(algebra-8): Глава 3 Wave 1 — скелет + §13 + §14
Глава 3 «Неравенства с одной переменной» по программе Арефьевой/Пирютко.
Палитра: индиго → фиолетовый → бирюза. 6 параграфов + финал.

Скелет (общая инфраструктура, копия паттернов из ch2):
- 7 параграфов: §13–§18 + final3
- LocalStorage 'algebra8_ch3_*', shared XP 'algebra8_xp'
- DnD-хелпер setupSorter, glossary с 12 терминами, поиск Ctrl+K
- XP-карта + бейдж + 7 контекстных подсказок + ачивки
- Server sync прогресса (markLastPara/markParaRead, debounce 600мс)

§ 13 «Числовые неравенства и их свойства»:
- Теория, 5 главных свойств, примеры
- INTERACT 1: Drag-сортировка 5 чисел по возрастанию (5 наборов)
- INTERACT 2: «Знак меняется или нет» (8 операций)
- INTERACT 3: Конструктор a, b, k + операция → live-сравнение
- INTERACT 4: Цепочка свойств (5 шагов выбора)
- INTERACT 5: Drag-классификация (8 переходов по 4 свойствам)
- INTERACT 6: Тренажёр «Что больше?» (10 случайных задач)

§ 14 «Сложение, умножение, оценка»:
- Теория, таблица 4 операций для оценки, пример
- INTERACT 1: Калькулятор оценок (live x+y, x-y, xy, x/y)
- INTERACT 2: Тренажёр границ (8 задач)
- INTERACT 3: Drag «Можно сложить / перемножить / нельзя»
- INTERACT 4: Пошаговое сложение (5 шагов)
- INTERACT 5: Сложи неравенства (6 multiple-choice)

DB: миграция 013 — slug 'algebra-8-ch3', sort_order=5, бамп physics-8 на 6.
Главы 1 и 2 теперь имеют кнопку «Глава 3 →» в шапке.
2026-05-27 16:14:15 +03:00
Maxim Dolgolyov 26510ff712 fix(migration 012): bump physics-8 to sort_order=5 so algebra-8-ch2 sits next to ch1 2026-05-27 15:11:25 +03:00
Maxim Dolgolyov c2f66b1e97 feat(algebra-8 ch2): Wave 4 — финал + 7 боссов + DB migration
frontend/textbooks/algebra_8_ch2.html · final2:
- Boss Arena с 7 боссами:
  · §7 «Хранитель неполных» — 5 заданий
  · §8 «Дискриминатор» — 5 заданий
  · §9 «Дух Виета» — 5 заданий
  · §10 «Разложитель» — 5 заданий
  · §11 «Архивариус задач» — 5 заданий
  · §12 «Мастер замены» — 5 заданий
  · ★ «Магистр алгебры» (финал) — 7 заданий
- Универсальный движок: select / yesno / input
- HP-бар, иконки боссов, состояние в localStorage
- Сертификат «Магистр квадратных уравнений» при всех 7 победах

Дополнительно:
- Увлекательная математика (3 spoiler-факта: история, a≠0, парадокс)
- Финальная практика: генератор случайных задач со всей главы (5 типов)
- Серия из 5 верных = достижение
- ACH_LABELS для всех boss_*, all_bosses, prac_streak

algebra_8.html: добавлена ссылка «Глава 2 →» в шапке.
migrations/012_algebra_8_ch2.sql: регистрация slug 'algebra-8-ch2'.
2026-05-27 14:54:01 +03:00
Maxim Dolgolyov ea753cf5b0 chore(textbooks): убрать упоминания авторов — учебники теперь как внутренние работы
- Миграция 011: UPDATE textbooks SET author='' (все 4 записи)
- algebra_8.html: убрано из <title> и футера
- physics_8.html (hub): убрано из title/header/intro/footer, заменено на LearnSpace
- physics8_*.html (3 файла): убраны все вхождения '· Исаченкова' в подписях §
- physics_9.html: убраны все вхождения '· Исаченкова' в подписях §
- chemistry_9.html: убраны 3 упоминания '· Шиманович' в подписях

В каталоге /textbooks автор больше не отображается под названием (так как поле пустое).
2026-05-27 11:03:25 +03:00
Maxim Dolgolyov 87e78714b7 feat(textbooks): объединить 3 части Физики 8 в один hub-учебник
Подход: hub-страница, а не слияние файлов.

Проблема: 3 готовых файла-главы (thermal/electro/optics) занимали 3 карточки в каталоге. Физическое слияние в один файл = 800КБ+, конфликты CSS/JS namespaces, риск сломать KaTeX.

Решение:
- Создан frontend/textbooks/physics_8.html — hub-страница с 3 крупными карточками-разделами (амбер/синий/фиолетовый)
- Карточки ссылаются напрямую на /textbooks/physics8_thermal.html и т.д. (express.static уже отдаёт эти файлы)
- Из каталога /textbooks теперь видна ОДНА карточка «Физика 8», sort_order 4
- Hub-страница показывает прогресс по каждой главе через LocalStorage (best-effort парсинг)
- Header «К каталогу», переключатель темы синхронизирован с главами

Миграция 010: удалила 3 прежние записи (physics-8-thermal/electro/optics), добавила физическо-8 → physics_8.html, para_count=40.

Эмодзи в hub не используются (только inline SVG). Эмодзи в файлах глав остались — это контент.
2026-05-27 09:55:24 +03:00
Maxim Dolgolyov 6a65e223b8 feat(textbooks): добавить учебник «Физика 8» (3 части)
Интегрирован готовый интерактивный учебник по физике 8 класса (40 параграфов, разбитых на 3 файла):

- physics8_thermal.html (§1–11)   — Тепловые явления
- physics8_electro.html (§12–31)  — Электрические явления
- physics8_optics.html  (§32–40)  — Световые явления

Все три самодостаточные (KaTeX через CDN, шрифт Outfit, dark mode, анимации, эмодзи).
Автор: Исаченкова Л. А.

Миграция 009 регистрирует три новых textbook-записи:
- physics-8-thermal (amber, sort 4)
- physics-8-electro (blue, sort 5)
- physics-8-optics (violet, sort 6)

После миграции доступны через /textbook/physics-8-thermal и т.д. и видны в каталоге /textbooks.

Pre-commit hook на эмодзи обойден --no-verify по разрешению пользователя: эмодзи здесь являются частью авторского контента учебника (визуальные маркеры разделов: тепловые/электрические/оптические явления), а не нашим кодом.
2026-05-27 09:51:00 +03:00
Maxim Dolgolyov dd62486074 feat(textbooks): зарегистрировать «Алгебра 8 · Глава 1» в каталоге
Файл algebra_8.html уже создан, но не появлялся в каталоге /textbooks потому что отсутствовала запись в SQLite-таблице textbooks. Миграция 008 добавляет:
- slug: algebra-8
- subject: math, grade: 8
- title: «Алгебра — 8 класс»
- author: Арефьева И. Г., Пирютко О. Н.
- html_path: algebra_8.html
- para_count: 7 (6 параграфов + Финал)
- color: pink, sort_order: 3 (после physics-9)

После применения миграции учебник доступен по /textbook/algebra-8 и виден в общем каталоге /textbooks.
2026-05-27 09:37:13 +03:00
Maxim Dolgolyov 3135402dd7 perf(db): +21 indexes for hot-path queries (audit 2026-05-22)
Adds missing indexes identified by comprehensive audit:

users:        created_at, last_login, role, (is_banned, role)

test_sessions: started_at, finished_at, subject_id, (status, finished_at DESC)

notifications: (user_id, created_at DESC) — previous idx was on is_read, not date

admin_audit_log: target, action, (action, created_at DESC)

classroom_*:  session_id FK indexes — prevents full-table scan on CASCADE

              DELETE (muted, attendance, invites, notes, draw_permissions, hands)

              + chat (session_id, created_at)

submissions:  (student_id, submitted_at DESC) — gradebook timeline

questions:    difficulty — exam builder filter

Skipped users.email — UNIQUE constraint already provides equality lookup.

Expected impact: 30-60% on admin overview / analytics, 10-20% on

notification/session timelines. CASCADE DELETE of classroom_sessions

no longer triggers full-table scans on 6 child tables.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 21:41:37 +03:00
Maxim Dolgolyov eeb79246db @
feat: teacher_students — назначения ученикам без класса

Новая модель «Мои ученики» — учитель связывает с собой учеников
независимо от классов (репетиторский сценарий).

Backend:
  - Таблица teacher_students (teacher_id, student_id, added_at, note)
    + индекс на student_id для обратного поиска
  - GET/POST/PATCH/DELETE /api/teacher-students — управление списком
  - Добавление по email с проверкой роли student/free_student
  - Уведомление ученику при добавлении

  - createDirectAssignment: проверка inClass расширена до
    inClass OR (teacher_id, student_id) в teacher_students
  - listStudents (/api/classes/students): возвращает объединение
    учеников из классов + из teacher_students. Это автоматически
    обновляет student-picker в /textbooks без правок UI.

Frontend:
  - /my-students — таблица личных учеников + форма добавления
    по email + заметка + счётчик созданных заданий
  - Сайдбар: пункт «Мои ученики» (user-plus, только для учителей)

Миграция 006_teacher_students.sql.

Что работает end-to-end:
  - Добавить ученика на /my-students
  - Открыть /textbooks → «Назначить» → «Ученику» → ученик ищется
    в общем списке (классовые + личные)
  - Создаётся запись в assignments с user_id, видна ученику на
    дашборде с пометкой «Личное задание»
@
2026-05-16 17:01:11 +03:00
Maxim Dolgolyov 3ff2f01178 feat: textbooks Phase 4 — A1+A2+A3+B4+C7 + назначение ученику
A1 — карточка ДЗ-чтения у ученика на /dashboard:
  - Новая ветка в buildAssignCard для assignments с textbook_id
  - Прогресс-бар «X из Y §», цвет берётся из textbook.color
  - Кнопка «Открыть / Продолжить» с deep-link на первый требуемый параграф
  - В classify(): textbook_all_read → done, deadline → overdue

A2 — авто-проверка выполнения:
  - При POST /:slug/progress с mark_read: проверяются активные textbook-assignments
  - Если все требуемые § прочитаны → INSERT в assignment_completion
  - SSE-уведомление учителю «Ученик завершил чтение: <title>»
  - myAssignments возвращает completed_at и textbook_all_read

A3 — учительский UI прогресса класса:
  - Новая страница /textbook-progress (учитель/админ)
  - Селекторы «учебник × класс» → таблица учеников с прогрессом
  - Сортировка по количеству прочитанного, дата last_at
  - Кнопка «Прогресс класса» добавлена в /textbooks (видна учителям)

B4 — admin-UI управления учебниками:
  - /admin-textbooks (только admin) — таблица всех учебников
  - Inline-редактирование title/author, тоггл is_active
  - Колонка «Читателей» (count из textbook_progress)
  - Endpoints: GET /api/textbooks/admin/all, PATCH /admin/:id

C7 — закладки/заметки внутри учебника:
  - Таблица textbook_bookmarks (user, textbook, para, text, note, color)
  - API: GET/POST/PATCH/DELETE для CRUD закладок
  - В tracker: при выделении текста (8-400 симв) появляется плавающая «+ Закладка»
  - Кнопка-иконка в overlay top-left открывает панель «Мои закладки»
  - Хранится paragraph-якорь, цвет, заметка, кнопка удалить

Назначение ученику (в дополнение к классу):
  - В модалке /textbooks — переключатель «Классу / Ученику»
  - Поиск ученика по имени/email через /api/classes/students
  - Submit использует POST /api/assignments (createDirectAssignment)
  - createDirectAssignment расширен textbook_slug + textbook_paragraphs
  - Учитель может назначать только ученикам своих классов

myAssignments расширен: возвращает textbook fields + post-process
  считает textbook_required_count, textbook_read_count, textbook_all_read.

Deep-link поддержка: /textbook/<slug>#pN в tracker.js — на load и hashchange
вызывает setParaTab(pN) (нативная функция учебника).

Миграция 005: assignment_completion + textbook_bookmarks + индексы.
2026-05-16 16:37:11 +03:00
Maxim Dolgolyov e8018d85c1 feat: textbooks — модуль учебников + чтение как ДЗ (3 фазы)
Фаза 1 — структура и каталог:
  - frontend/textbooks/chemistry_9.html (Шиманович, 60 §) + physics_9.html (Исаченкова, 38 §)
  - frontend/textbooks.html — каталог в стиле LearnSpace (карточки с обложками)
  - Маршруты: /textbooks (каталог), /textbook/<slug> (полноэкранный учебник)
  - Сайдбар: пункт «Учебники» (book-open-text)
  - Feature flag feature_textbooks_enabled, hideDisabledFeatures map

Фаза 2 — прогресс в localStorage + UI чтения:
  - frontend/js/textbook-tracker.js — инжектится в каждый учебник:
    - «← Учебники» overlay-кнопка (top-left, semi-transparent)
    - «Прочитано» чекбокс рядом с каждым §-заголовком
    - Зелёный dot на pill уже прочитанных параграфов
    - Авто-открытие последнего параграфа при возврате
  - Каталог показывает прогресс-бар «X из Y прочитано» + кнопку «Продолжить»

Фаза 3 — серверный прогресс + назначение чтения как ДЗ:
  - Таблица textbooks (slug, subject, grade, title, author, color, ...)
  - Таблица textbook_progress (user_id, textbook_id, JSON read[], last_para)
  - Колонки assignments.textbook_id + textbook_paragraphs
  - API: GET /api/textbooks (с прогрессом), GET /:slug, POST /:slug/progress,
    GET /:slug/class-progress (учитель)
  - tracker.js синхронизирует прогресс через POST /progress (если залогинен)
  - На каталоге у учителей кнопка «Назначить чтение» — модалка с выбором
    классов + параграфы («1-5» или «1,3,5») + deadline
  - bulkCreateAssignment расширен: принимает textbook_slug, резолвит в id

Миграция 004 идемпотентная; сиды двух учебников включены.
2026-05-16 14:05:19 +03:00
Maxim Dolgolyov 31a51956b6 feat: exam9 — назначение варианта как ДЗ + импорт нечётных в банк
Импорт 40 нечётных вариантов (v01, v03, ..., v79) в банк вопросов:
- 400 questions с allow_html=1, source_type='экзамен 9', year=2025
- 540 options (single-choice) + correct_text (short_answer)
- 40 tests (по 1 на вариант), title="Экзамен 9 — Вариант N"
- exam9_variant_tests маппинг для назначения

Назначение варианта как ДЗ на /exam9 (для учителей/админов):
- Кнопка «Назначить как ДЗ» под заголовком варианта (только если test_id есть)
- Модалка выбора классов + опциональный deadline
- POST /api/assignments/bulk с test_id из exam9_variant_tests

Поддержка HTML/SVG в вопросах банка через флаг questions.allow_html:
- Миграция 003: ALTER TABLE questions ADD COLUMN allow_html
- sessionController: SELECT возвращают allow_html и image
- test-run.html: рендер q.text и opt.text как HTML при allow_html=1
- test-result.html: то же для explanation и opt.text
- KaTeX: добавлены $...$ и $$...$$ delimiters в обеих страницах

Бонус-фикс: bulkSchema требовал class_id (single), контроллер ждёт
class_ids (array). Существующий вызов из classes.html был сломан;
исправлено вместе.

Команда: node backend/scripts/import-exam9.js  (--all для всех 80)
2026-05-16 13:13:06 +03:00
Maxim Dolgolyov 6cff327e88 feat: exam9 — Экзамен 9 класс по математике (80 вариантов)
Новый отдельный модуль /exam9 в стиле LearnSpace:
- 80 вариантов × 10 заданий = 800 задач с разбором (KaTeX + SVG)
- Сайдбар: пункт «Экзамен 9 класс» (clipboard-check)
- Feature flag: feature_exam9_enabled (мигр. 002)
- Видим всем авторизованным; рендер на стороне клиента
- Прогресс в localStorage: подсветка вариантов (done/partial)
- Возобновление последнего варианта при возврате

Структура:
  frontend/exam9.html              — страница (LearnSpace layout)
  frontend/js/exam9/app.js         — рендерер
  frontend/js/exam9/variants/      — 80 файлов с данными
  frontend/img/exam9/              — 22 PNG/JPG фигур заданий

Картинки путей _tmp/ → /img/exam9/ переписаны автоматически.

Все маршруты проверены: 200 OK на /exam9, /js/exam9/*, /img/exam9/*.
2026-05-16 12:53:49 +03:00
Maxim Dolgolyov c0f20ef020 fix: classroom review — 11 исправлений из code review
- sessions.js: endSession закрывает classroom_attendance (left_at), чистит classroom_muted
- sessions.js: joinSession восстанавливает mute-состояние при реконнекте
- strokes.js: updateStroke проверяет авторство штриха (не только canDraw)
- strokes.js: clearPage валидирует page_num как положительное целое
- strokes.js: postStrokes ограничивает массив 500 штрихами
- pages.js: duplicatePage сохраняет user_id при копировании штрихов
- pages.js: changePage валидирует page_num
- pages.js: updatePageTemplate делает INSERT OR IGNORE перед UPDATE
- permissions.js: mutePeer сохраняет в classroom_muted; добавлен unmutePeer
- permissions.js: getOnlineStudents не возвращает email
- chat.js: exportChat экранирует переводы строк в именах и сообщениях
- guestClassroom.js: санитизация имени гостя (убираем HTML-символы)
- ws-server.js: mute_peer сохраняет в БД; добавлен обработчик unmute_peer
- routes/classroom.js: rate-limit для cursor/preview/signal/strokes; маршрут DELETE /mute
- migrations/001_classroom_muted.sql: новая таблица classroom_muted
2026-05-07 14:26:19 +03:00
Maxim Dolgolyov 2fd7f6a463 refactor: switch to versioned migrations runner (phases 2+3)
- migrate.js → legacy-migrate.js (kept for rollback, delete 2026-07-01)
- tests/setup.js now uses migrations-runner.run() on fresh temp DB
- npm run migrate → versioned runner (was legacy init-every-start)
- npm run migrate:legacy → legacy-migrate.js (emergency rollback only)

After `npm run migrate:bootstrap` on prod:
  npm run migrate → "Nothing to apply — schema is up to date"

All 32 previously-passing tests continue to pass.
Pre-existing 3 auth.test.js failures (rate-limiter shared state) unchanged.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 17:50:40 +03:00