По итогам ревью системы прав:
- админка: переключатель режимов «По контенту» / «По классу»
- кнопки «Открыть всем классам» / «Закрыть у всех» (и зеркально по классу)
- бейджи N/M (сколько классов открыто) в списке контента
- эндпоинты /api/access/summary и /api/access/class/:id
- вкладка «Доступ к учебникам» перенесена к «Права доступа» (группа Пользователи)
- чистка content_access при удалении класса/ученика (нет FK)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Все 6 ЛР физики 7 закрыты. Файл phys7_lab_widgets.js (726 строк, 6 экспортов:
lr1..lr6). Палитра cyan. Подключение через обновлённый gen_phys7_lab.js:
script-тег + hook в goTo (удаление placeholder + вызов widgets).
Каждая ЛР содержит:
- Цель (goal card, голубая)
- Оборудование (equip card, оранжевая)
- Ход работы (steps card, фиолетовая) — пронумерованный список
- СИМ-виджет (интерактивная симуляция прибора)
- ТБЛ-виджет (таблица измерений)
- ВОПР-виджет (3 контрольных вопроса с авто-проверкой)
- Вывод (concl card, зелёная)
- Кнопка «Сдать ЛР» (+30 XP, localStorage-фиксация)
ЛР-1 «Цена деления» (§7):
- 4 виртуальных прибора (линейка/термометр/мензурка/динамометр) с SVG-шкалами
- Таблица C для всех 4
- 3 контрольных вопроса
ЛР-2 «Измерение длины» (§4, §7):
- 3 предмета на выбор (карандаш/тетрадь/брусок), SVG с линейкой ниже,
риска на длине + запись (l ± 0,5) мм
- Таблица 3 измерений
ЛР-3 «Объём вытеснением» (§4):
- 3 тела (камень/гайка/болт), 2 SVG-мензурки рядом (V1=100 и V2=100+V),
стрелка «опускаем» между ними, авто-расчёт V = V2 − V1
- Таблица 3 измерений
ЛР-4 «Неравномерное движение» (§18):
- Шарик на наклонной плоскости, slider угла 10..60°, кнопка «Запустить»,
анимация скатывания (квадратичная по времени, эмпирически быстрее на больших углах)
- Таблица 3 углов с разной средней скоростью
ЛР-5 «Плотность» (§20):
- 3 образца на выбор (54г/156г/272г, V=20 см³ каждый), SVG-весы+мензурка,
расчёт ρ = m/V и автоопределение материала (алюминий/железо/золото)
- Таблица плотностей 9 веществ
ЛР-6 «Сила трения» (§27):
- SVG: брусок с грузами, динамометр, разные поверхности из <select>
(дерево/пластик/резина/лёд: μ от 0.04 до 0.5)
- slider массы 100..500 г → авто N и Ftr через динамометр
- Таблица 5 измерений с разными грузами → видно Ftr ~ N
АЧИВКА «Лаборант 7 класса» +80 XP — автоматически при сдаче всех 6 ЛР
(проверка через localStorage в wireSubmit).
Парсинг OK, smoke (6 экспортов) OK.
Формулы в JS-литералах имели \\\\dfrac / \\\\\\\\dfrac (4/8 слэшей) вместо
\\dfrac (2). После JS-анескейпа KaTeX получал \\dfrac, трактовал \\ как
перенос строки и печатал dfrac/cdot/sqrt/pi как текст (карточка пирамиды и
конуса в geometry_11_ch2, и др.).
Схлопнуты прогоны слэшей кратные 4 перед LaTeX-командой -> 2. Прогоны из
3 слэшей (\\ перенос строки + \cmd в \begin{cases}) и перед x/цифрой не
тронуты. 150 правок в 7 файлах (algebra_11_ch1/ch2/ch3, geometry_11_ch1..ch4).
БД чиста: questions (1398) text/explanation/correct_text + options (5187) -
0 багов. Скрипт: backend/scripts/fix_overescaped_latex.js (идемпотентный,
dry-run по умолчанию, --apply, с KaTeX-валидацией).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Hub улучшения:
- .ch-card: подъём на hover (-6px scale 1.01) с тематической box-shadow
по цвету главы.
- .ch-cover::after: shimmer-overlay при наведении (diagonal sweep).
- .ch-cover-wm: micro-перемещение и scale на hover.
- .ch-action svg: стрелка едет вправо на hover.
- .po-xp: пульсирующая тень для overall progress XP-badge (3s loop).
Accessibility:
- aria-label на каждой ch-card с понятным названием темы.
- :focus-visible с 3px outline в brand-цвете для chapter cards.
- :focus-visible с белым outline для hdr-btn на градиенте.
- prefers-reduced-motion: блокирует все анимации.
Mobile responsiveness:
- @media ≤580px: уменьшение шрифта h1 1.4rem, ch-cover-wm 3.8rem.
Footer: '40 параграфов, 7 ЛР, 47 IV-6 интерактивов'.
Hero: spectrum-drift градиент (18s), солнце SVG-watermark
(rotate-анимация 40s), live-meter длины волны (400/470/550/600/700 нм
с цветами цикла).
9 section watermarks: лампа, тень, угол, зеркало, парабола,
рефракция, линза, призма, глаз.
9 IV-6 интерактивов:
§32 Источники — кнопки 'Точечный/Протяжённый' с динамической
тенью (точечный — чёткая, протяжённый — с полутенью).
§33 Тени — drag-источника по X, размер тени пересчитывается
проективно.
§34 Закон отражения — scrubber угла, лучи + нормаль.
§35 Плоское зеркало — drag-d объекта, мнимое изображение за
зеркалом на том же расстоянии (штриховая стрелка).
§36 Сферическое зеркало — drag-d, формула 1/v+1/d=1/F,
изображение с правильным знаком/размером.
§37 Преломление — scrubber угла, закон Снеллиуса (n₁=1, n₂=1.33).
§38 Линза — 3 главных луча от объекта, формула v=dF/(d-F),
изображение по принципу геометрической оптики.
§39 Дисперсия — призма с разложением белого света на 7 цветов
видимого спектра.
§40 Глаз — кнопки 'Норма/Близорукость/Дальнозоркость' с
fokus-точкой и корректирующей линзой (рассеив/собир).
§12 Charge Sandbox: canvas с динамическим добавлением зарядов.
Click → +заряд (или - через кнопку), drag для перемещения,
стрелки взаимодействия по Кулону (красные=отталкивание,
зелёные=притяжение). Кнопки '+/-', 'Очистить'.
§17 Field Visualizer: drag-зарядов с live перерисовкой
силовых линий. От каждого + рисуются 16 линий, идущих
по полю E через интегрирование шагами. Линии останавливаются
у − зарядов или вылетают за canvas.
§22 Закон Ома: SVG цепь батарея + резистор + лампа.
Scrubbers U (0.5-12 В), R (1-100 Ом). I=U/R обновляется
live, яркость лампы ∝ I (glow при I>0.3).
§25 Параллельные резисторы: SVG цепь с разветвлением.
Scrubbers R₁, R₂. Live расчёт R_общ = R₁R₂/(R₁+R₂),
I₁, I₂ для каждой ветви, общий I.
§28 Магниты: canvas с 2 drag-магнитами (N-S полюса).
При сближении inner полюсов (S-N) рисуются стрелки
притяжения с величиной по F~1/d².
§30 Опыт Эрстеда: SVG провод с током (scrubber -5..+5 А)
и компас под ним. Силовые линии магн. поля вокруг провода
(концентрические штриховые круги) с opacity ∝ |I|.
Стрелка компаса отклоняется по arctan(I), угол выводится.
Заменены оставшиеся stub'ы (Phase 1. coming soon) на реальные
интерактивы. Все 11 параграфов Ch1 теперь имеют flagship IV-6.
§2 Способы изменения U — Drag-piston:
- Цилиндр с газом, движущийся поршень (scrubber сжатия 0-100%).
- Scrubber Q для подачи тепла. Молекулы рисуются динамически
(количество ∝ T). Цвет газа по T через P8Helpers.thermal.tempColor.
- Readouts T (°C), U (отн.).
§4 Конвекция — Animated convection cell:
- Canvas-симуляция с 60 частицами, P8Anim.raf.
- Поток вверх по центру (нагретая лёгкая вода), вниз по краям.
- Скорость потоков ∝ мощности горелки (scrubber). Цвет частиц
по локальной T. Кнопки Пуск/Стоп.
§5 Излучение — Radiation balance:
- Лампа с 3 телами (чёрное, белое, зеркало) разной поглощающей
способности (0.95, 0.20, 0.05).
- Scrubber мощности лампы. Симуляция P8Anim.raf: T каждого тела
растёт ∝ absorption × power. Glow вокруг тёплых тел.
§7 Q=qm — Fuel burn:
- 3 кнопки палитры топлива (дрова q=10, уголь q=29, газ q=44 МДж/кг).
- Кастрюля с водой 1 кг. Сжигание выбранного топлива + scrubber массы.
- Q = qm, ΔT = Q/(c·m_в). Пар над кастрюлей при ΔT > 60°C.
§9 Q=λm — λ-meter:
- Select веществ (лёд, свинец, алюминий, железо) + scrubber массы.
- SVG: блок вещества + grad-arrow (Q) + расплав. Q = λ·m в реальном
времени.
§10 Скорость испарения — 3-scrubber sandbox:
- T (0-100°C), площадь (0.01-1 м²), ветер (0-10 м/с).
- Стрелки испарения вверх с количеством ∝ rate; наклон ∝ ветру.
- Качественная демонстрация трёх факторов.
§11 Скороварка — Pressure cooker:
- Canvas: кастрюля с водой, динамические пузыри.
- Scrubber давления 0.5-3 атм. T_кип = 100 + 20·log₂(p).
- Пар, T-индикатор столбиком.
Все интерактивы +10 XP при первом использовании.
Builders все на месте, JS парсится.
Предыдущий коммит eaee79d удалил builders §3, §5, §6, §8 из-за
greedy regex, который пересекал границы параграфов. Фактически
жалкие 211 КБ файла вместо 280 КБ.
redesign_p8_ch1_2.cjs переписан:
- Использует точный stub-text per-paragraph (с 'Новый интерактив §N'
в title — уникальный маркер).
- Нормализует CRLF/LF (ch1.html на диске CRLF, шаблон — LF).
- Делает простой h.replace(stubText, widget) без regex с greedy.
- Sanity-чек: все 11 builders должны остаться на месте после patch.
Восстановлены §3 Heat Conductor Bench, §6 Heat Mixer, §8 Phase
Diagram T(t) — full IV-6 interactives с drag/scrubbers/Anim.raf.
Размер ch1: 295851 байт. Все 11 builders + 5 IVs in каждом + IV-6
flagship в §1, §3, §6, §8.
Заменены stub'ы 'coming soon' на полноценные drag-and-drop виджеты:
§3 Тепловая лавочка (Heat Conductor Bench):
- SVG-sandbox 560×300 с горелкой (drop zone) и 4 стержнями
(медь λ=400, серебро λ=430, стекло λ=0.8, дерево λ=0.15).
- P8Drag.attach на каждый стержень → drop на горелку.
- При drop'е sim запускается: P8Anim.raf обновляет цвет
каждого сегмента стержня через P8Helpers.thermal.tempColor()
по log-нормализованной λ. Тепловая волна идёт по стержню.
- Readouts: материал, λ, T дальнего конца.
§6 Heat Mixer (Q=cmΔT):
- 2 ёмкости (m₁, T₁), (m₂, T₂) — рисуются с цветом по T.
- 4 scrubber'a (m₁, T₁, m₂, T₂) с live update SVG.
- Кнопка 'Смешать' → tween анимация в 1.2 с → итоговая T
через формулу теплового баланса (m₁T₁+m₂T₂)/(m₁+m₂).
- Readout T_итог, кнопка 'Сброс'.
§8 График плавления (Phase Diagram T(t)):
- T-t график 560×280 с осями (-20 до 120°C, 0 до 300 с).
- Фазовые области: лёд (синий), вода (голубой), пар (жёлтый).
- Реальная симуляция: c_льда=2100, c_воды=4200, λ=330000,
r=2300000. P8Anim.raf вычисляет накопление энергии и
фазовые переходы — плато на 0°C (плавление) и 100°C
(кипение).
- Scrubber мощности 100-2000 Вт. Кнопки Старт/Сброс.
- Readouts: фаза, T.
+10 XP за каждое успешное взаимодействие.
Визуальный редизайн ch1 Тепловые явления:
- Hero: заменён старый .hdr на новый .p8-hero с анимированным
градиентом (thermal-shift 14s), огненным SVG-watermark
справа (дышащая анимация 6s), live-meter в углу с пульсацией
и плавной анимацией значения 37 → 100 → 0 → -10 → 25 → 80 °C.
- Eyebrow 'Глава 1 · 11 параграфов', крупный title, sub-описание.
- Section watermarks: в каждой <section sec-pN> добавлены
тематические SVG (атом, конвекция, солнце, сосуд, фазовый
переход, пузыри и т.д.) с opacity .07 на правой стороне.
IV-6 §1 flagship interactive — Drag thermometer:
- SVG-sandbox 560×320 с 4 телами (лёд, вода, чай, пар) разной
T и относительной U.
- Draggable термометр (P8Drag.attach + P8Helpers.svg).
- При наведении на тело — изменяется цвет термометра по
P8Helpers.thermal.tempColor(), readout табло показывают
T (°C) и U (отн.).
- +5 XP за 12 сек исследования.
IV-6 stubs для §2-§11: 'Coming soon' плашки с тематическим
SVG-иконкой clock. Расширим в Phase 1.2.
В Физике 8 ch1 §6, §7, §9, §11 уже имели IV-4 'Тренажёр N расчётных задач'.
У §1-5, §8, §10 IV-4 был только MCQ — числовых задач не было.
inject_p8_ch1_tasks.cjs добавляет IV-5 виджет после IV-4 в build_pN:
- §1 Внутр. энергия: 5 задач (T-конверсия, U vs масса/высота)
- §2 Способы изменения U: 5 (Q=ΔU+A, кин. энергия молота → тепло)
- §3 Теплопроводность: 5 (тепловой поток P=Q/t, зависимости от d, S, λ)
- §4 Конвекция: 5 (плотности тёплого/холодного, нагрев радиатором)
- §5 Излучение: 5 (солнечный поток, Стефан-Больцман упрощённо)
- §8 Плавление: 5 (Q=λm)
- §10 Испарение: 5 (Q=rm, испарение пота, лужи)
Всего 35 новых задач с автопроверкой числового ответа (±tol),
подсказкой-решением (KaTeX) и +20 XP при прохождении всей серии.
Используется существующий design system (.wg, .tinp, .feedback,
.score-display) — уже подключён через phys-textbook-widgets.css.
Жалоба: 'каждому параграфу там [в монолите] есть задачи, тут нет'.
В монолите physics_9.html — отдельный tab-tasks блок содержит 36 ptab-pN
панелей (~1.3 KB каждая) со scaffold'ом score-bar/prog-wrap/nav-dots/
taskArea/feedback/summary. Сами задачи (TASKS_P31..P36) рендерятся в
taskAreapN через goToTask('pN', i).
migrate_phys9_tasks.cjs:
- Извлекает ptab-p31..p36 из монолита (clean emoji + FA<i>)
- Внутри каждого build_pN в ch4 после theory body добавляет <div class='wg'>
с заголовком 'Задачи §N · Тренажёр §N' и вставляет ptab HTML
- Через 80 мс после render вызывает goToTask('pN', 0) → рендерит первую
задачу из TASKS_PN
Не делаю это для §1-30: TASKS_P1..P30 не определены в монолите
(там было решение делать тренажёры только для главы 'Законы сохранения').
Жалоба пользователя по Физике 8 (но проблема общая для Phys 8 и Phys 9):
страницы глав используют классы .wg/.dnd-pool/.dnd-chip/.btn/.score-display/
.feedback/.actions/.sliders/.spoiler/.drop-box в HTML-разметке, но CSS-правила
для них живут только в physics_10_ch1.html. Из-за этого карточки-задания,
chip'ы drag-and-drop, кнопки и feedback-блоки в Phys 8 и Phys 9 рендерились
без стилей (как обычный текст).
- extract_widget_css.cjs: вытягивает CSS-блок (.btn..pre-.col-side) из
physics_10_ch1.html в frontend/css/phys-textbook-widgets.css (6.4 КБ)
- Подключает <link> в 11 файлов: physics_8_ch1/ch2/ch3/hub/lab,
physics_9_ch1..ch5, physics_9_hub
- migrate_phys9_content.js теперь инжектит ссылку на widget CSS при будущих
миграциях (рядом с FA CDN)
Три бага из жалобы пользователя:
1) phys9_legacy.js упал с 'Identifier STATE has already been declared' —
const STATE в монолите конфликтовал с const STATE в chapter inline JS.
Скрипт extract_phys9_legacy.cjs теперь оборачивает извлечённый код в IIFE
и явно экспортит через window 70 функций (upd*/draw*/init*/start*/lab*/
check*/toggle*/render*/show*/...) + 7 const-массивов (TASKS_PN, PUZ_PN).
2) В боковой панели формулы рендерились как 'Delta vecr' вместо Δr⃗ —
мой переход на JSON.stringify в gen_phys9_ch.js добавил лишний слой
escape backslash. Уменьшил \\ → \ в SIDEBAR_ROWS, TIPS_HTML,
PARA_SUBS, LR_SUBS (90 строк). Цепочка теперь: source \Delta → string
\Delta → JSON "\\Delta" → HTML JS \Delta → runtime \Delta →
KaTeX \Delta ✓.
3) 'не работают симуляции' — функции из legacy.js были доступны, но
chapter goTo(id) их не вызывал. Добавлен авто-вызов upd<N>(),
startAnim<N>(), init<N>(), draw<N>() при переключении на параграф,
и updLab<N>(), drawLab<N>() — для ЛР.
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>
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>
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>
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>
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>
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>
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>
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>
Recent-attempts widget on /exam-prep/:examKey was showing raw LaTeX
like '\dfrac{7}{9}' because stripPreview only removed HTML tags.
Now it also converts common LaTeX to readable unicode (fractions →
a/b, \sqrt → √, \cdot → ·, comparisons → ≤≥≠, Greek letters, etc.)
before truncating.
KaTeX rendering would be overkill for a 100-char preview row; this
just makes the existing text legible.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Материал учебников теперь полностью наш (LearnSpace), оригинальные
авторы (Арефьева, Латотин, Казаков и др.) убраны из:
- поля textbooks.author в БД (миграция 029);
- footer'ов hub-файлов (9 файлов).
Содержание теории и интерактивов не затронуто.
Login was only returning {id, email, name, role}, so localStorage.ls_user
never had avatar_url for sessions started before today — and the sidebar
fell back to initials forever. Fixes:
• login response now includes avatar_url
• renderNavAvatar detects 'undefined' (cache predates the field) vs
'null' (verified absent) and fires a one-shot /auth/me refresh in
the background, then re-paints. Self-healing for existing sessions
without forcing re-login.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Shop items of type 'frame' now render a real avatar-sized preview with
the frame's CSS applied (instead of a generic lucide icon) so buyers
see exactly what they're paying for. Title items get a tag-shaped
preview in their color. The avatar-frames section above the shop also
shows the user's actual avatar inside the frame circles, not 'LS' text.
Sidebar nav-avatar now:
• renders the uploaded avatar_url instead of always showing initials
(LS.initPage + new LS.refreshNavAvatar helper)
• picks up frame CSS on every page via applyCosmetics — previously
only dashboard.html applied it
• repaints immediately after picking/deleting an avatar preset
(avPickPreset / avDelete now call LS.setUser + LS.refreshNavAvatar)
Backend getMyActive resolves avatar_frame to {id, css} for both
gamification frames ('fire', 'crown', ...) and shop-purchased frames
('shop_<id>'), so the client doesn't need a second round-trip to
look up the CSS.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
- backend/uploads/avatars/preset_01..27.png — иллюстрированные персонажи
- POST /api/avatar/preset — мгновенная установка без модерации
- GET /api/avatar/presets — список доступных пресетов
- profile.html: галерея пресетов в модалке аватара, доступна студенту/учителю/админу
- кастомная загрузка с модерацией остаётся только для студентов
Учитель может выбрать любой активный учебник из каталога /api/textbooks
и открыть его в общем iframe для всех участников. По аналогии с симуляциями:
- Backend: контроллер classroom/textbook.js + 4 роута
(POST/DELETE /:id/textbook, /:id/textbook/nav, /:id/textbook/mode)
с SSE-событиями classroom_textbook_open|close|nav|mode
- Embed-режим /textbook/:slug?embed=1: сервер injectит CSS+JS-bridge
перед </head>, скрывая хедер/сайдбар и пересылая клики/скролл наверх
через postMessage (без правки 40+ HTML-учебников)
- Frontend (classroom.html): кнопка «Учебник» в header, пикер с
фильтрами по предмету, iframe-панель с режимами демо/свободно,
relay nav-событий учителя → всем студентам в demo-режиме