384 Commits

Author SHA1 Message Date
Maxim Dolgolyov 68817cc612 fix(ctmath): чистка банка — год-пачки убраны из пикера пробников
- exam-prep.js: MOCK_VARIANT_RANGE — для ctmath показываем как пробники
  только чистые 30-задачные варианты [101;1999]; год-пачки (variant=год
  2011-2024 и 0, до 114 задач) остаются пулом для тренажёра по темам,
  но скрыты из пикера/mock-start/просмотра вариантов. math9 (1..80) не затронут
  (диапазон только для ctmath).
- mock.js: пикер «По варианту» — выпадающий список реальных вариантов
  (через listVariants) вместо number-input 1..N; раньше для ctmath он
  предлагал 1..18 и не доходил до 101 → пробник по варианту не запускался.
- cleanup_ctmath_bank.js: идемпотентный скрипт — ретайр битого id=1419
  (mc с противоречивым ответом → long), variants_count → 3 (чистых вариантов).
- seed_*: variants_count считается по диапазону [101;1999] (консистентно с роутом).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 22:22:32 +03:00
Maxim Dolgolyov 6cd0a81d88 feat(ctmath): пробник РТ-2024/25 Этап III Вариант 1 (variant=103)
Завершающий пробник РТ-2024/25 (полный охват: тела вращения, сфера,
производная, сечения, параметрически сложные задачи). По 1 варианту на Этап.
1 чертёж из PDF (три окружности, А2). KaTeX-рендер 30/30, self-сверка ответов.
РТ-2024/25 оцифрован целиком: Этапы I/II/III = variants 101/102/103.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 22:01:38 +03:00
Maxim Dolgolyov 2af560b7c4 feat(ctmath): пробник РТ-2024/25 Этап II Вариант 1 (variant=102)
Чистый 30-задачный пробник Этапа II (другой набор тем, чем Этап I:
обратные тригфункции, логарифмы, производная, стереометрия). По 1 варианту
на Этап (правило «без повторов»). 3 чертежа из PDF (параллельные прямые,
панель из 5 графиков для y=|x|, график функции). KaTeX-рендер 30/30, self-сверка.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 21:34:53 +03:00
Maxim Dolgolyov 98894e31ad feat(ctmath): эталонный пробник РТ-2024/25 Этап I Вариант 1 (variant=101)
Первый чистый 30-задачный вариант-пробник для exam-prep ctmath (А1–А10 + В1–В20),
в отличие от год-пачек (variant=год). Идемпотентный seed (dry-run/--apply),
3 чертежа вырезаны из PDF (хорда/график/L-поле). Проверено: KaTeX-рендер 30/30,
self-сверка ответов через checkAnswerServer.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 21:08:19 +03:00
Maxim Dolgolyov 477d47e9e6 feat(admin): тумблер фичи для «Квантик» (паритет с другими играми)
У Квантика не было фиче-флага — его нельзя было выключить, и он всегда висел
в сайдбаре (даже у учеников без класса). Добавлено по образцу остальных игр:
- adminController.updateFeatures: 'quantik' в whitelist (PATCH принимает флаг).
- games.js: пункт «Квантик: Законы Мира» в GAME_FEATURES и FS_FEATURES
  (тумблер в админке → Игры; пишет feature_quantik_enabled).
- api.js hideDisabledFeatures: quantik -> ['/quantik','/quantik.html'] (скрытие
  из сайдбара при выключении) + '/quantik' в classOnlyHrefs/classOnlyPaths
  (скрыт у учеников без класса, как прочие игры).

Миграция не нужна: флаг «неявно включён», пока админ не выключит (features[key]
!== false => включено). Требует Ctrl+F5 (фронт).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 16:00:23 +03:00
Maxim Dolgolyov 6fed18f819 feat(admin): тумблер вкл/выкл для экзамен-модулей (exam-prep)
Не было UI для управления exam_tracks.enabled (только флаг в БД, ставился
миграцией). Добавлена админ-секция «Экзамен-модули»:
- backend exam-prep.js: GET /admin/tracks (все треки, вкл. выключенные, + число
  заданий) и PATCH /admin/track (exam_key, enabled), обе requireRole('admin').
  Пути без :examKey, чтобы не задеть гейт content_access.
- frontend: секция sections/exams.js (список треков + переключатель enabled),
  вкладка в admin.html (admin-only через ADMIN_ONLY_TABS, locked для не-админов),
  регистрация в admin.js (ROUTE_TO_SECTION).

Выключенный трек скрыт у учеников и пропадает из каталога прав доступа (тот
берёт exam_tracks WHERE enabled=1). Доступ ученикам по-прежнему в «Доступ · контент».
Требует перезапуска бэкенда + Ctrl+F5.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 12:32:01 +03:00
Maxim Dolgolyov 8091b48e1c fix(ct-math): практика возвращала меньше count + перенос заголовков в навигации урока
1) exam-prep practice (strategy=random) возвращал около 0.6 от count: функция
   distributeByDifficulty раскладывает count по 5 уровням сложности, а у трека
   ctmath задания только уровней 1-3 (уровни 4-5 пустые) -> часть выборки терялась
   (20 -> 12, 15 -> 10, 10 -> 6). В pickRandomByDifficulty добавлен добор до count
   из доступных уровней. Трек math9 не затронут (там добор не требуется).
2) lesson.html: .lesson-nav-btn-title был inline-span, поэтому max-width и ellipsis
   игнорировались и длинные заголовки вылезали за кнопку. Добавлен display:block.

Бэкенд-правка требует перезапуска сервера; фронт-правка видна после Ctrl+F5.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 12:09:50 +03:00
Maxim Dolgolyov 4b23d768f2 fix(ct-math): литеральные угловые скобки в формулах уроков ломали KaTeX
Блок formula вставляет tex в HTML без экранирования, поэтому литеральная
"меньше"-скобка (напр. в "0 le r lt d") принималась браузером за HTML-тег и
формула не рендерилась (показывался сырой $$...$$). Заменено на \lt и \gt
(KaTeX рендерит их как отношения).

- seed_ctmath_lessons_rest.js: исправлены 4 формулы в исходнике (числа,
  модуль, показ/лог равносильности, производная-монотонность).
- fix_ctmath_formula_lt.js: фикс уже залитых блоков курса 13 (dry/--apply).
  Флешкарты не затронуты (mathHtmlFC через textContent экранирует сам).

Запись (UPDATE 4 блоков) запускает пользователь.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 12:05:47 +03:00
Maxim Dolgolyov a982628d04 feat(ct-math): уроки всех остальных блоков (48-55) + 4 колоды флешкарт формул
- seed_ctmath_lessons_rest.js — 8 уроков по PLAN: числа, преобразования,
  уравнения (квадратные/рацион/модуль + показ/лог/иррац+рационализация),
  функции+производная, прогрессии/текстовые, планиметрия, параметры.
  Курс 13 теперь покрывает все 9 секций (15 уроков, lessons.id=41-55).
- seed_ctmath_flashcards.js — 4 колоды формул (тригонометрия/стереометрия/
  логарифмы-степени/производная, 49 карт, flashcard_decks.id=11-14, владелец admin).
- Форматы блоков/карт сверены с рендером (lesson.html $…$/$$; flashcards $…$/\(\)/\[\]).
  Применены seed-скриптами; JSON валиден (0 битых).
- README: статус контента.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 11:48:39 +03:00
Maxim Dolgolyov 623fbde38b feat(ct-math): уроки стереометрии (44-47) + скрипт мини-фикса 866/1248
- backend/scripts/seed_ctmath_lessons_stereo.js — 4 урока блока «Стереометрия»
  по PILOT_STEREOMETRY (расположение/сечения, многогранники, тела вращения,
  координатный метод В20) в курс 13; применён (lessons.id=44-47, 60 блоков).
- backend/scripts/fix_ctmath_misc.js — точечный фикс exam_tasks id=866
  (варианты-прямые в норму) и id=1248 (битый источник → long); dry/--apply,
  идемпотентен. Запись блокируется авто-режимом — запускает пользователь.
- README: статус (уроки стерео, сайдбар, остаток).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 11:36:56 +03:00
Maxim Dolgolyov 9b1abb83f8 fix(ct-math): варианты ответа из текста → нормальный opts_json (mc ctmath)
У части mc-задач ЦТ (формат РИКЗ «укажите номер») список ответов был вшит
в текст («1) 44; 2) 22; …»), а opts содержали лишь цифры-указатели — рисовалось
«а) 1, б) 2…» + значения строкой. Скрипт fix_ctmath_inline_opts.js вытаскивает
список из текста в opts_json (метка=цифра, текст=значение), пересчитывает answer,
очищает текст. Последовательный парсер сохраняет ';' внутри значений (интервалы).
Dry: 281 кандидат → 213 чинятся чисто, 68 нестандартных пропущены (без порчи).

Запись (UPDATE 213) — запускает пользователь (--apply), как и прочие записи в БД.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 11:06:42 +03:00
Maxim Dolgolyov fd26efca53 feat(ct-math): конвертер questions→exam_tasks для отдельного модуля ctmath (dry-готов)
- backend/scripts/seed_ctmath_exam_tasks.js — переносит размеченные вопросы
  ЦТ-11 из банка questions в exam_tasks (exam_key='ctmath') для отдельного
  модуля exam-prep. Dry по умолчанию, запись только с --apply.
  Правила сверены с exam-prep: MC-метки кириллица а..д (answer=метка);
  open числовой/дробь/пара иначе long; делимитеры \( \)→$, \[ \]→$$;
  subtopic=slug из 077; variant=год; multi/multiple пропуск.
  Dry-run: 733 вопроса → 723 (525 mc + 191 open + 7 long), выборка корректна.
- BUILD_ON_QUESTIONS.md: решение «ЦТ = отдельный модуль» + план + dry-результат.

Запись в БД (применение 077 + вставка 723) — ожидает явной санкции пользователя.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 07:56:43 +03:00
Maxim Dolgolyov 31719b2e79 feat(ct-math): уроки блока «Тригонометрия» (3 урока в курсе ЦЭ/ЦТ)
- backend/scripts/seed_ctmath_lessons_trig.js — идемпотентный seed 3 уроков по
  PILOT_TRIGONOMETRY в секцию «Тригонометрия» курса 13:
  круг и значения (lessons.id=41, 18 блоков, А3), тождества и формулы (id=42,
  19 блоков, А8/В4), уравнения и отбор корней (id=43, 15 блоков, В15).
  Форматы блоков сверены с рендером frontend/lesson.html (heading/text/formula/
  callout/sim trigcircle/flashcard/quiz/matching/ordering/accordion/table;
  math $…$/$$…$$; data JSON валиден). Уроки — в DRAFT-курсе (ученикам не видны).
- BUILD_ON_QUESTIONS.md / README: статус (блок «Тригонометрия» готов).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 07:41:15 +03:00
Maxim Dolgolyov 228bd885ed feat(ct-math): диагностический тест из реальных вопросов банка (tests.id=164)
- backend/scripts/seed_ctmath_diagnostic.js — идемпотентный сбор ОДНОГО test
  «Диагностика ЦЭ/ЦТ — Математика» из размеченных вопросов ЦТ-11 (в осн. 2024):
  5 single (базовые) + 10 fill-blank (средние/сложные), по 1 на ключевую тему.
  Новых вопросов не авторит. Применён: test id=164, 15 вопросов, лимит 40 мин.
  Выдать = assignment с test_id=164.
- BUILD_ON_QUESTIONS.md / README: отметка о готовой диагностике, статус.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 22:16:27 +03:00
Maxim Dolgolyov c3816baf99 feat(ct-math): каркас курса ЦЭ/ЦТ на банке questions (темы + draft-курс + секции)
- backend/scripts/seed_ctmath_course.js — идемпотентный аддитивный seed:
  +6 тем (Преобразование выражений/Модуль/Иррациональные ур./Показательные ур./
  Производная/Параметры), DRAFT-курс «ЦЭ/ЦТ — Математика» + 9 секций.
  Применён на живой БД: course id=13 (is_published=0), topics 72-77, sections 27-35.
  Существующие данные не тронуты; повторный запуск ничего не дублирует.
- BUILD_ON_QUESTIONS.md: уточнения инспекции банка (year=2025 = «Экзамен 9»,
  без тем; реальный ЦТ-11 = ~733 размеч., Часть B = fill-blank → гоча mode='ct')
  + блок «Состояние реализации».
- README: статус каркаса.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 22:10:22 +03:00
Maxim Dolgolyov 7eb6cb2da0 docs(ct-math): план подготовки к ЦЭ/ЦТ по математике + миграция дерева тем
- plans/ct-math: модульная программа (карта теста А1–А10/В1–В20, 9 блоков
  и ~32 модуля, 3 уровня, маппинг на exam-prep платформы), 2 пилота
  (тригонометрия, стереометрия), seed дерева тем, спецификация оцифровки
  заданий РТ/ЦТ, инвентарь материалов
- backend: миграция 077 — трек ctmath + exam_topics (9 разделов, 32 подтемы),
  валидирована in-memory node:sqlite; на живую БД НЕ применялась

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 21:26:43 +03:00
Maxim Dolgolyov 69df2f8190 @
chore(quantik-game): полировка по финальному ревью + security-review

Финальное ревью: READY TO MERGE (0 блокеров). Security: SECURE (0 critical).
Применены дешёвые фиксы из ревью:
- validateSpec: блок game{} санитизируется ПОИМЁННО (chapter/subject →
  sanitizeText, order/par_ms/unlockStars → проверка типа, неизвестные ключи
  отбрасываются) — закрыт латентный хранимый XSS (раньше clean.game=spec.game).
- quantik.html: @media (prefers-reduced-motion) делает анимации мгновенными
  (не выключает — иначе forwards-появление узлов оставило бы их скрытыми).
- progress-logic.js: фикс комментария isUnlocked (сумма звёзд по ВСЕМ уровням
  с меньшим глобальным order, а не «той же главы»).
План: Ф6 (лидерборд/гонка) удалена (Amendment 1, решение пользователя);
финальные гейты отмечены; deferred-бэклог зафиксирован.
Затронутые тесты 45/45; lint:routes 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@
2026-06-14 17:00:13 +03:00
Maxim Dolgolyov c780b6fd96 @
feat(quantik-game): фаза 5 — авторинг игровых уровней в sim-builder + раздача

Учитель собирает игровой уровень без кода: новая (аддитивная, сворачиваемая)
панель в sim-builder задаёт блок goal (when/title/hint/hold/fail) + до 3
звёзд + game-мету (chapter/order/par_ms); выражения проверяются inline через
SimExpr.compile (без eval). buildSpec/loadFromSim — round-trip без потерь
(goal/game пишутся только при включённом слое; обычная sim не меняется).
Кнопка «Играть» монтирует черновик в SimEngine-модалке (HUD цели из Ф0).
QuantikLevels стал async: подмешивает custom_sims cat=game (свои+
published) в реестр (custom:<dbid>), offline-safe, строки без goal
отбрасываются; deep-link /quantik?level=custom:<id> с серверной проверкой
доступа (own|published → иначе 403/404), мимо геймплейного гейта unlockStars.
Раздача классу — реюз share Ф6 (game-aware ссылка + durable pushNotif).
Правки sim-builder строго аддитивны (параллельная сессия). npm test 259/8
baseline; quantik-authoring 6/6; lint:routes 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@
2026-06-14 16:09:10 +03:00
Maxim Dolgolyov 978448d99b @
feat(quantik-game): фаза 3 — граф-уровни (движение по f(x)) + зоны

Новый тип уровня: Квантик едет по кривой y=f(x), которую игрок собирает
слайдерами коэффициентов, проходя сквозь зоны-препятствия. Движок
(аддитивно): plot.runner → env-поля curve.runX/runY/runDone (f компилится
1 раз, питает И кривую, И бегунок-героя, без само-ссылки); type zone
(forbidden/target/collect) → булево env-поле zone.hit. Грамматика
выражений ЗАКРЫТА — никаких inzone()-предикатов, только именованные
env-поля (модель t/tries из Ф0), без eval. Глава-созвездие functions из
5 уровней (луч/синус/парабола/модуль/экспонента), разблокировка 9/11/13/
15/17 (цепочка проходима). validateSpec принимает zone+runner. Все 5
уровней независимо проверены на движке (2★ достижимы). npm test 253/8
baseline; custom-sims 26/26; lint:routes 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@
2026-06-13 17:07:33 +03:00
Maxim Dolgolyov 02ab886bee merge: SimForge (конструктор + улучшения + тумблер + руководство) в quantik-game 2026-06-13 16:32:30 +03:00
Maxim Dolgolyov 351251d652 @
feat(quantik-game): фаза 1 — оболочка игры + физ-уровень + прогресс (MVP)

Страница /quantik монтирует уровень-спеку в SimEngine (игровой режим: HUD из
Ф0 + слайдеры закона + play/reset), на победу шлёт результат и показывает
экран успеха (звёзды/время/попытки, inline SVG). Уровень phys-artillery-1
как данные (levels.js): гравитация + запуск тела из угла/скорости, портал,
бонус-звезда. Бэкенд: миграция 076 game_progress (UNIQUE user+level),
/api/game/progress (GET свой / POST upsert best time/stars, attempts++,
auth-only, валидация входа), клиент LS.gameProgress*, пункт сайдбара.
game.test.js 13/13; npm test 251 pass/8 baseline; lint:routes 0.
Уровень проверен на реальном интеграторе (311 выигрышных комбо, 31 на 3★).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@
2026-06-13 15:31:25 +03:00
Maxim Dolgolyov 225e252e3c feat(sim-builder): тумблер «Конструктор симуляций» в админке (feature_sim_builder_enabled) — гейт авторинга + скрытие/редирект 2026-06-13 15:22:59 +03:00
Maxim Dolgolyov 4b5c8077d3 @
feat(quantik-game): фаза 0 — слой целей в движке (goal/HUD/result)

Декларативный блок goal в спеке SimForge (булево SimExpr-условие победы),
вычисляемый каждый кадр: фиксация результата (победа/время/попытки/звёзды),
callback onGoal, HUD-оверлей (цель/звёзды/подсказка/баннер, inline SVG).
API инстанса: onGoal/getResult/resetResult. Серверный validateSpec
пропускает goal/game (длина выражений + escape текста, без исполнения).
Аддитивно: спека без goal ведёт себя как раньше. Смоук 40/40; npm test
238 pass/8 baseline; lint:routes 0. План фичи (7 фаз) + CONTEXT.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@
2026-06-13 15:13:02 +03:00
Maxim Dolgolyov d8717d0fbd fix(sim-builder): вайтлист цветов в validateSpec — закрыть CSS-инъекцию в шаренных спеках (финальное ревью) 2026-06-13 13:33:14 +03:00
Maxim Dolgolyov 9bd40c5d1c feat(flashcards): общие колоды — учитель назначает колоду классу/ученику
Учитель делится своей колодой с классом или конкретными учениками; карты общие
(одна копия), а прогресс у каждого свой — flashcard_reviews уже keyed по
user_id+card_id, поэтому ученик копит собственные интервалы на тех же картах.

- миграция 075: flashcard_deck_access (deck_id, type class|user, target_id) —
  зеркало folder_access; индексы по target и deck.
- deckAccess(): владелец/админ (canEdit) либо назначенный напрямую/через класс
  (canRead). listDecks отдаёт свои + назначенные (shared/can_edit/owner_name);
  getCards/getStudySession/submitReview пускают по canRead (ученик учится и
  ставит отзыв), правка карт/колоды — только владелец.
- share API (owner + роль teacher/admin): GET /shares, POST /share, DELETE
  /share?type=&target_id=; цель валидируется (свой класс / свой ученик).
- фронт: общие колоды с бейджем учителя, открываются read-only (CSS .readonly
  прячет ручки/удаление/правку, drag и inline-edit выключены), кнопка
  «Поделиться» с модалкой (вкладки Классы/Ученики, тоггл = add/remove share).
- тест flashcards-share 13/13 (шаринг класс/ученик, видимость, изучение+отзыв,
  правка 404, доступ 404, роль-гейт 403, чужой класс 403, снятие доступа).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 13:30:53 +03:00
Maxim Dolgolyov f26b522207 feat(sim-builder): фаза 7 — custom-sim на доске онлайн-урока (синхрон параметров классу, аннотации) 2026-06-13 13:25:24 +03:00
Maxim Dolgolyov 5c01a5c7ed feat(flashcards): learning-steps SR — повторный показ «Снова» в сессии, лимит новых карт/день
Tier-1 апгрейд интервального повторения:
- schedule() с состояниями learning/relearning/review вместо плоского sm2():
  новая карта проходит шаги [1,10] мин, «Снова» возвращает на шаг 0 (минуты),
  «Знаю» продвигает шаг → выпуск (1д), «Легко» выпускает сразу (4д); зрелая
  «Снова» = lapse → relearning (ef−0.2, ×0.5).
- study-сессия: динамическая очередь — недоученная карта (graduated=false)
  возвращается через 3 карты и показывается снова в той же сессии.
- лимит новых карт/день (decks.new_per_day, деф.20) в getStudySession и бейдже.
- превью кнопок fcPreview() показывает минуты/дни, зеркало серверной логики.
- миграция 074: state/learning_step/lapses/created_at + new_per_day + индексы.
- тесты SRS 9/9 (шаги, lapse, лимит новых).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 13:10:00 +03:00
Maxim Dolgolyov cbb6edf372 feat(sim-builder): фаза 6 — раздача классу, клон, шаблоны, привязка к программе (custom_sims) 2026-06-13 13:06:30 +03:00
Maxim Dolgolyov 014c96db1e feat(sim-builder): фаза 3 — БД custom_sims + CRUD API с валидацией спеки и проверкой владения 2026-06-13 12:10:02 +03:00
Maxim Dolgolyov ad7265d553 feat(flashcards): Anki-стиль интервалов — кнопки различаются
Раньше на новой карте Снова/Трудно/Знаю/Легко все давали 1 день (чистый SM-2:
оценка влияла только на ease factor). Теперь интервал зависит от оценки:
новая карта Легко=4д (остальные 1д), на повторах Трудно ×1.2 / Знаю ×ef /
Легко ×ef×1.3 (easy-бонус). Серверный sm2() и клиентское превью fcNextInterval
синхронны — проверено 0 расхождений на 256 комбинациях.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 23:27:40 +03:00
Maxim Dolgolyov 9dd3522869 feat(flashcards): ИИ-генерация карточек по теме/тексту с предпросмотром в текущую колоду 2026-06-12 23:06:08 +03:00
Maxim Dolgolyov ccfb151eca fix(reliability): дневной лимит imggen в БД + ретеншн error_log (Спринт3)
- imggen: дневной счётчик генераций перенесён из in-memory Map в таблицу
  imggen_usage (миграция 070) — переживает рестарт. Cooldown остаётся в памяти,
  но добавлена периодическая чистка Map + старых строк imggen_usage (>7 дн).
- classroom-cleanup: ретеншн error_log (app_settings.error_log_retention_days,
  по умолч. 30; 0 = выкл) в том же суточном джобе.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 23:00:36 +03:00
Maxim Dolgolyov 107ca2220c feat(imggen): feature-gate «imggen» с контролем по классам/ученикам (Спринт2)
- server: requireFeature('imggen') на /api/imggen (глобальный гейт).
- imggenController: enforcement через isFeatureEnabledForUser в status()/generate()
  — учитывает глобальный флаг + оверлей класса + free_student (403 если выкл.).
- admin «games/features» + free-student: тумблер «Генерация картинок (ИИ)».
- classes.html: переключатель модуля imggen в настройках класса (per-class).
Дефолт — ON (opt-in disable), как у остальных фич. Проверено на features-движке.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 22:15:54 +03:00
Maxim Dolgolyov 09c6c2b21d fix(reliability): multer-ошибки, process-хендлеры, анти-гонка питомца, flashcards (Спринт2)
- errorHandler: MulterError → 413 «слишком большой» / 400 (а не 500).
- server: process.on(unhandledRejection/uncaughtException) — глобальная страховка
  с логированием, процесс не падает от единичной асинхронной ошибки.
- pet: атомарный CAS на кулдаунах petAction/starCatch/feedPet
  (UPDATE ... WHERE last IS ?, начисление только при changes=1) — нет двойного
  начисления при параллельных запросах. Проверено на семантике node:sqlite.
- assistant.flashcardsFromText: await callLLMFailover в try/catch → 502 вместо
  необработанного отклонения промиса.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 22:08:02 +03:00
Maxim Dolgolyov 646e93cf46 fix(security): пер-юзер лимиты ИИ + SSE через одноразовый тикет (Спринт1 #5,#6)
#5 rate-limit (byUser) на дорогих LLM-эндпоинтах: /assistant/ask (20/мин),
   /assistant/flashcards (10/мин), /imggen (20/мин) — поверх cooldown/дневного
   лимита. Защита от «сжигания» бюджета провайдера одним аккаунтом.
#6 SSE больше не таскает JWT в URL: добавлен authed /notifications/stream-ticket
   (одноразовый тикет, TTL 30с), клиент берёт тикет заголовком и подключается
   с ?ticket=. ?token= оставлен как временный фоллбэк для старых клиентов.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 22:00:23 +03:00
Maxim Dolgolyov dd5dfee5c9 fix(anti-cheat): анти-фарм XP в играх и при повторном завершении урока (Спринт1 #2,#3)
- games: дневной лимит начислений XP за hangman/crossword (DAILY_WIN_CAP=10,
  счёт по xp_log.reason) — нельзя бесконечно фармить циклом complete.
- lessons.markComplete: XP/монеты только при ПЕРВОМ завершении урока
  (повторные POST больше ничего не начисляют).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 21:54:41 +03:00
Maxim Dolgolyov 840bb823b9 fix(security): закрыть IDOR курсов/уроков/назначений/раздачи (Спринт1 #1)
- courses: requireOwnership(created_by) на PUT/DELETE/duplicate/publish-all
  и все мутации секций — учитель больше не может править/удалять чужой курс.
- lessonController.create: проверка владения курсом перед вставкой урока.
- assign/unassign курса классу: проверка владения классом (_ownsClass).
- materials.share по userId: получатель должен быть учеником учителя
  (класс или teacher_students), иначе 403.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 21:52:56 +03:00
Maxim Dolgolyov 5d3db90b5d perf(classroom): инкрементальный поллинг доски, картинки в файлы, ретеншн
#1 Студенческий поллинг: вместо полной перезагрузки доски каждые 2с —
   лёгкая сигнатура страницы (?meta=1 → maxSeq+count). Если доска совпадает
   с сервером (обычный случай при живом WS) — ничего не грузим. Полная
   перезагрузка только при расхождении. Счёт подтверждённых штрихов — по
   положительным id (без bookkeeping).
#2 Картинки-штрихи выносятся в файлы /uploads/classroom (вместо base64 в БД):
   меньше БД и payload поллинга. Имя с префиксом sessionId.
#5 Ретеншн: classroom-cleanup удаляет штрихи+файлы завершённых сессий старше
   N дней (app_settings.classroom_retention_days, по умолч. 30; 0 = выкл),
   историю/чат/посещаемость не трогает. Планировщик в server.js.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 13:02:26 +03:00
Maxim Dolgolyov ddc260e114 feat(admin): тумблер вкл/выкл генерации картинок
Главный выключатель в разделе «Генерация картинок» (флаг on в конфиге,
независим от наличия токена). Выключено → /api/imggen отдаёт 503
«временно выключена». Админ-тест работает и при выключенном тумблере
(generateImage проверяет только наличие конфига). Бейдж различает
«Включена / Выключена / Не настроена».

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 12:00:05 +03:00
Maxim Dolgolyov 88651d85ab feat(admin): раздел «Генерация картинок» — управление провайдером и тест
Новый админ-раздел: Account ID / токен (маскируется) / модель Cloudflare,
лимиты (пауза, дневной лимит) из БД, статистика, кнопка теста генерации.
imggenController: лимиты и модель теперь из конфига, поддержка JSON и
бинарного ответа CF, переиспользуемые generateImage() и stats().
Бэкенд GET/PUT /api/admin/imggen + POST /api/admin/imggen/test (admin-only).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 11:37:47 +03:00
Maxim Dolgolyov 4e8c0841db feat(imggen): авто-перевод промпта на английский перед FLUX
FLUX лучше понимает английский. Если в промпте есть кириллица — прогоняем
через тот же LLM-провайдер ассистента (callLLMFailover, с failover) и
отправляем перевод. При сбое перевода — исходный текст. callLLMFailover
теперь экспортируется из assistantController.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 11:20:05 +03:00
Maxim Dolgolyov 6fcdafed50 feat(imggen): фон питомца, обложки курсов, аватары и доска через ИИ
Питомец: кастомный фон (миграция 068 pet_bg_custom, POST /api/pet/bg/custom,
  карточка «Свой фон (ИИ)» в гардеробной, применение картинкой).
Курсы: обложка-картинка (миграция 069 cover_image, генерация в модалке
  редактирования, рендер вместо эмодзи).
Аватар: кнопка «Сгенерировать (ИИ)» в загрузке → кадрирование → модерация.
Доска (classroom): кнопка-инструмент «Сгенерировать картинку (ИИ)».

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 10:59:26 +03:00
Maxim Dolgolyov d6faf6b22c feat(imggen): генерация картинок ИИ (FLUX.1) — ассистент, флэшкарты, редактор уроков
Бэкенд /api/imggen (status/generate, CF Workers AI, cooldown+дневной лимит).
Переиспользуемый модал LS.imagePromptModal (js/imggen.js).
Квантик: режим «Нарисовать» в чате (inline).
Флэшкарты: кнопка «ИИ» в блоке картинки карточки.
Редактор уроков: кнопка «Сгенерировать» в блоке изображения.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 10:41:59 +03:00
Maxim Dolgolyov db2fccef56 fix(assistant): убран openrouter/free из Kilo (регресс — льёт рассуждения)
Полная проверка всех бесплатных моделей Kilo на русском: openrouter/free
теперь роутит на reasoning-модель и выводит «мысли вслух» — убран. Надёжное
ядро (чисто, без утечки): Owl Alpha, Nemotron 120B/550B, Nex N2 Pro, Laguna XS.
kilo-auto/free и stepfun/step-3.7-flash тоже текут — в список не берём.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 10:11:49 +03:00
Maxim Dolgolyov e1fbe4086c fix(assistant): актуализация моделей Kilo — убрана удалённая Qwen, добавлена Nex N2 Pro
Сверка с живым /models: qwen/qwen3.7-plus:free удалена из шлюза (висела
мёртвой в списке). Заменена на nex-agi/nex-n2-pro:free (проверено: чистый
русский, 262K, ~3с). Остальные 7 моделей живы; активная owl-alpha — ОК.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 10:07:22 +03:00
Maxim Dolgolyov b9f70ff88b feat(assistant): учитель видит профиль ученика для Квантика (агрегат, без заметок)
GET /assistant/student-profile/:id (teacher/admin): производный профиль ученика
— слабые предметы, трудные темы экзамена, цель, серия. Сырые заметки НЕ
отдаются (приватны). Доступ: свой класс или «Мои ученики»; чужой → 403; админ
— любой (проверено). На /my-students — кнопка «Профиль» с поповером. Ученику в
панели памяти уже написано «учитель видит лишь общие слабые темы».

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 23:16:07 +03:00
Maxim Dolgolyov 900fdb893d security(routes): закрыт долг по незащищённым :id-маршрутам (baseline 66→0)
check-route-auth теперь распознаёт router-level guards (router.use(<guard>)) —
ушли ложные срабатывания (admin/permissions/flashcards/lessons/… защищены на
уровне роутера, что линтер уже принимает как authMiddleware). Из 66 осталось
8 действительно безавторизационных :id-маршрутов — все публичные по дизайну
(гостевая доска по секретному токену, справочные данные Red Book, список тем
предмета): помечены @public-by-design после проверки (мутации требуют auth).
Baseline опущен до 0 — новые незащищённые маршруты теперь сразу падают в хуке.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 23:00:19 +03:00
Maxim Dolgolyov 9cfb7d1c3b feat(assistant): долгая память об ученике (персонализация)
Производный профиль (без LLM): слабые предметы, трудные темы экзамена,
цель/дата, серия — из test_sessions/exam_attempts/exam_user_plan. Подмешивается
в системный промпт → персональные ответы; такие не кэшируются глобально.
Заметки: таблица assistant_memory + фоновый LLM-экстрактор (дросселирован),
дедуп + лимит 15. Панель ученика «Что я о тебе помню» (профиль + заметки,
удаление). Админ-тумблер. API GET/DELETE /assistant/memory (/:id под
authMiddleware, владелец проверяется в хендлере).

Заодно: сверка стабильного baseline route-auth 56→66 (долг от branch-merge,
хук не идёт на merge) — новых незащищённых маршрутов не добавлено.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 22:51:04 +03:00
Maxim Dolgolyov 5417083f88 feat(pet): большое наполнение кастомизации контентом
Цвета 6→11 (розовый/оранжевый/бирюза/лайм/индиго). Узоры 5→8
(сердечки/звёздочки/клетка). Аксессуары 11→18 + новая зона «В лапах»
(бини, нимб, монокль, медаль, серёжки, палочка, шарик). Разблокировка за
учёбу: нимб (8 достижений), медаль (30 тестов). Фоны 7→11
(класс/лаборатория/зима/радуга) с градиентами и частицами. Новая вкладка
«Образы» — 4 готовых набора (Учёный/Волшебник/Чемпион/Милашка) применяют
аксессуары+узор+цвет одним кликом.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 14:52:58 +03:00
Maxim Dolgolyov 6880e1a55a feat(pet): прокачанный блок кастомизации + много контента
Контент:
- Узор тела (новая ось): пятнышки, полоски, градиент, галактика (клип по
  силуэту, рендер в обоих рендерерах) + миграция pet_pattern + /api/pet/pattern.
- +4 аксессуара: колпак, тёмные очки, шарф, цветок (все бесплатные).
- +3 фона: Сияние, Леденец, Сакура (CSS-градиенты + частицы: звёзды/пузыри/лепестки).

UI кастомизации:
- Вкладка «Узор» со свотчами-превью.
- Гардероб по зонам (Голова/Лицо/Шея/Уши/Акцент) + счётчик надетого,
  кнопки «Случайный образ» и «Снять всё».
- Фон — инлайн-сетка с превью/ценой/балансом (как раньше).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 13:58:12 +03:00