biochemController.js: structuralMatch/canonicalHash (Morgan-подобный канонический
хеш графа) — для build-задания с data.requireStructure проверяется связность
против эталонной молекулы (molecule_id), а не только формула. Отличает изомеры:
этанол != диметиловый эфир при одной формуле C2H6O.
seed_biochem_challenges.js: +4 structure-build задания (CO2, этилен, этанол,
уксусная кислота). biochem.html: сообщение об ошибке wrong_structure.
Проверено на реальном коде против БД: этанол==этанол true, ==диметиловый эфир false.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
backend/scripts/seed_biochem_challenges.js (идемпотентно) — 16 заданий
недостающих типов (balance 5, match 3, classify 4, complete 4). Заполняет
пустовавшие фильтры заданий в редакторе; контроллер их уже валидирует.
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>() — для ЛР.
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>
- backend/src/permissions/registry.js: single source of truth (PERMISSIONS map)
with all 24 keys (16 teacher + 8 student, student keys also cover free_student).
Exports isKnown(), listKeys(), byRole(), buildDefaultsMap().
- auth.js: PERM_DEFAULTS now sourced from registry.buildDefaultsMap();
new perm() helper validates key at registration time (crashes early on typos).
requirePermission() unchanged — backward compat preserved.
- permissionsController.js: ALL_PERMISSIONS now built from registry.byRole();
inline 24-entry array removed. API response shape unchanged.
- check-route-auth.js: validates every requirePermission/perm call key against
registry; lists unknown keys as errors before exit.
perm() added to GUARDS list so it counts as route protection.
Discrepancy noted: auth.js had free_student with same 8 keys as student;
permissionsController never seeded free_student rows. Registry documents
this via roles:[] array; buildDefaultsMap() correctly covers free_student.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Импорт 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)
content/phys/ct-2024.yaml — 15 questions from ЦЭ,ЦТ 2024 across
6 topics (kinem, mol, emf, electro, magnet, optics) as proof of format.
backend/scripts/import-content.js — unified importer:
- Validates schema (subject, year, options, exactly-1-correct)
- Aliases (kinem, mol, ...) resolve to Russian topic names via get-or-create
- Deduplicates by first 80 chars of text (matches legacy seed_*.js behavior)
- Runs in a single transaction, idempotent re-runs
On fresh DB: 13 added (2 dedup collisions — same 80-char prefix, expected).
On prod DB: 0 added (all already exist from legacy seeds).
Second run on either: 0 added (dedup works).
Legacy seed_phys_ct2024.js kept as backup — see content/README.md
for migration guide.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Scans all routes/*.js for :id-bearing routes without an auth-guard
(requireOwnership, requireRole, requirePermission, authMiddleware,
parentAuth, or spread middleware arrays like ...auth/...teacher).
BASELINE=56 — any new unprotected :id route causes exit(1).
Reduce BASELINE as old routes are migrated.
Usage:
npm run lint:routes
# or mark intentional public routes:
// @public-by-design: <reason>
router.get('/:token', handler);
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>