Commit Graph

411 Commits

Author SHA1 Message Date
Maxim Dolgolyov cd7c75ff08 feat(trainer): P4 — авторинг задач учителем + раздача классу
- POST /api/practice/author: учитель пишет story/lhs/rhs/answer → та же проверка подстановкой (validateAndVerify) → пул; не сходится → 422
- POST /api/practice/assign: выдать тему классу → durable pushNotif каждому ученику (ссылка /trainer); владелец/админ, чужой → 403
- клиент: LS.practiceAuthor/Assign; в теме «Текстовые задачи» учителю кнопки «Своя задача» (модалка-форма) и «Выдать классу» (пикер классов)
- тесты: author (валид→пул, неверный→422, ученик→403), assign (владелец уведомляет, чужой→403) — practice 19/19 + practice-gen 16/16
- смоук страницы 27/27; план P4 → DONE (lean: ручной авторинг + раздача, без полного DSL-конструктора)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 14:30:02 +03:00
Maxim Dolgolyov d003a0e100 feat(trainer): P6 — учительская аналитика класса + общий прогресс
- GET /api/practice/class-stats (classStats): агрегаты по навыкам + матрица ученик×навык; доступ владелец класса/админ
- клиент: кнопка «Аналитика класса» (учителю) → модалка с тепловой картой (точность/освоено) + пикер классов; LS.practiceClassStats
- лёгкая геймификация: строка «Освоено навыков M из N · решено всего K» из агрегатов practice_progress
- тесты practice.test.js +4 (владелец видит; чужой/ученик → 403; без class_id → 400); смоук страницы 27/27; план P6 → DONE

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 14:24:05 +03:00
Maxim Dolgolyov 8c4c9bf04c feat(trainer): P3 — текстовые задачи от LLM с серверной проверкой подстановкой
- practiceVerify.js: грузит SimExpr в Node (require), verifyRoot подстановкой корня
- practiceGenService.js: LLM (инъектируемый ask) → parse → validateAndVerify (SimExpr + подстановка + санитизация) → авторетрай по фидбэку; дефолт ask = assistantController.callLLMFailover
- пул practice_problems (мигр.083); POST /api/practice/generate (учитель/админ) + GET /api/practice/pool
- инвариант: невалидная/неверная задача в БД НЕ пишется → ученику не попадёт
- клиент: LS.practicePool/Generate, тема «Текстовые задачи» (из пула; учителю кнопка «Сгенерировать»)
- тесты practice-gen.test.js 13/13 (verify, ретраи, off→503, 403 ученику, пул); смоуки страница 26/26; план P3 → DONE

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 14:00:39 +03:00
Maxim Dolgolyov 48a73d9f8e feat(trainer): P2 — умная тренировка, интервальное повторение, итог сессии
- adaptive.js (TrainerAdaptive): nextSkill (in-session повтор → серверный due → прогрессия → удержание), onWrong/onCorrect (очередь повторения), sessionStats
- умная тренировка на странице (тумблер, по умолч. вкл): авто-подбор навыка от простого к сложному, возврат ошибок
- сессия из 10 задач + экран «Итог сессии» (верно/точность/навыки/стоит повторить); неверный ответ авто-показывает решение
- сервер: SR-поля box+due_at на practice_progress (мигр.082, Leitner 0/1/3/7/16/30 дн), listProgress отдаёт box/due_at/due
- смоуки: adaptive 12/12, страница 23/23, practice.test.js 11/11 (+SR box/due); план P2 → DONE

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 13:46:29 +03:00
Maxim Dolgolyov c370eaa803 feat(trainer): ИИ-тренажёр — генераторы задач + SimExpr-верификатор, прогресс, фича-флаг
- движок _trainer_engine.js: instantiate/generateBatch/verifyRoot/checkStudentAnswer/exprToLatex
- 5 генераторов уравнений 7 класса (generators.js), приём «корень-вперёд» → целые ответы
- страница /trainer: KaTeX-рендер, чипы-темы, мгновенная проверка, подсказка/решение, авто-выбор навыка
- прогресс practice_progress (мигр.081) + /api/practice/progress|attempt + LS.practiceProgressList/Submit
- фича-флаг trainer: тумблер в админке (Модули), requireFeature, FEATURE_HREFS (скрытие сайдбара+редирект), MODULE_CATALOG
- fix: подключён Lucide CDN на странице (иначе иконки сайдбара пустые)
- тесты practice.test.js (10/10); план развития plans/ai-trainer/PLAN.md

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 13:11:47 +03:00
Maxim Dolgolyov 91917f952c fix(security): харднинг загрузки файлов, контроль доступа и XSS
Подхвачено из закрытой параллельной сессии (план project_hardening_2026).

Загрузки: magic.js получает safeExt/EXT_FOR_MIME — имя файла на диске берёт
расширение из проверенного MIME, а не из client originalname (анти stored-XSS
.html/.svg). avatar/flashcard/chat-загрузки дополнительно проверяют magic-байты:
содержимое должно соответствовать MIME, иначе файл удаляется и 400.

Доступ: fileController.getFolderAccess отдаёт список раздачи только владельцу
или админу (была утечка имён/email учеников). testController.getOne гейтит
видимость как list() — ученик не прочитает тексты заданий черновиков/вариантов
по id.

XSS: classes.html escJ() экранирует строку для JS-литерала в inline-onclick
(имя ученика с кавычкой больше не ломает обработчик).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 09:03:06 +03:00
Maxim Dolgolyov c045ea02b0 feat(assistant): больше рабочей памяти (~14 сообщений) + оформление сноски
История разговора расширена с 6 до 14 сообщений (фронт _chat.slice и бэкенд
история в ask/askStream). Сноска о памяти переоформлена: иконка-история + чище
текст, акцент на количестве.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 22:56:51 +03:00
Maxim Dolgolyov ee740817a8 fix(assistant): русские названия трудных тем в памяти
Темы экзамена в exam_tasks.topic хранятся англ. ключами (algebra, geometry,
functions, ...). Добавлена карта _EXAM_TOPIC_RU; в _studentProfile экзаменные
темы переводятся на русский перед слиянием с темами банка тестов.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 22:51:10 +03:00
Maxim Dolgolyov 29fc270c0e fix(assistant): «Забыть всё» теперь сбрасывает и производный профиль
clearMemory ставит точку отсчёта asst_forget_<uid> (datetime now); слабые
предметы/темы в _studentProfile считаются только по активности после неё, так
что панель памяти видимо очищается. Кнопка «Забыть всё» в виджете показывается
лишь при наличии заметок/слабых тем, профиль помечен как авто-обновляемый.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 22:36:29 +03:00
Maxim Dolgolyov 7b4a274aed feat(assistant): умная память Квантика — свежесть, категории, темы по всем предметам
Память об ученике (1+2+3 из плана), всё строго на русском:
- СВЕЖЕСТЬ: эффективный вес заметок с затуханием по времени (полураспад ~31 день),
  в промпт идут только актуальные (порог по effWeight). Старое тихо тает.
- УМНОЕ СЛИЯНИЕ: вместо дедупа по первым 24 символам — стем-токены (русская
  морфология) + Jaccard; похожие заметки сливаются (вес+, текст освежается),
  а не плодят дубли. Лимит 18.
- КАТЕГОРИИ: экстрактор классифицирует факт (трудность/предпочтение/цель/
  сильная сторона/личное), возвращает JSON; запоминаются и сильные стороны/
  интересы, не только проблемы. Гард по кириллице — не-русский текст не попадает.
- ТРУДНЫЕ ТЕМЫ ПО ВСЕМ ПРЕДМЕТАМ: профиль считает слабые темы из user_answers+
  topics (любой предмет, русские названия), объединяя с экзаменом, а не только math9.
- UI «Что я о тебе помню»: у заметок русская плашка-категория.
Без миграции (колонки kind/weight/updated_at уже есть). Проверено: логика 8/8.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 22:27:28 +03:00
Maxim Dolgolyov c3be921dfb fix(assistant): таймаут генерации тестов/карточек 15с был мал → 502
Симптом: POST /api/assistant/questions отдавал 502 «Не удалось сгенерировать
вопросы» ровно через ~15с. Причина: callLLM имел жёсткий таймаут 15с, а
бесплатная модель (owl-alpha) на генерацию 2200 токенов JSON порой тратит
больше — abort по таймауту, failover не выручал. Чат-ответам 15с хватает,
а генерации — нет.

callLLM/callLLMFailover получили опц. параметр timeoutMs (деф. 15с — чат не
тронут). questionsFromText → 45с, flashcardsFromText → 40с. Клиент req()
без своего таймаута, дождётся ответа.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 22:06:37 +03:00
Maxim Dolgolyov 81bf5d75eb fix(features): админ-оверрайд в requireFeature — API отключённого модуля 404-ил админа
Симптом: collection выключен, админ открывал страницу (фронтовый админ-оверрайд),
но GET /api/collection отдавал 404 — requireFeature 404-ил всех. requireFeature
идёт ДО authMiddleware (req.user нет), поэтому сам декодирую Bearer-токен: если
роль admin — пропускаем к API даже выключенного модуля. Для student/teacher всё
по-прежнему 404 (модуль скрыт). Зеркалит фронтовый _isAdminUser. Чинит ВСЕ
отключённые модули для админа, не только коллекцию.

Проверено: admin→bypass, student/teacher/нет токена/мусор/чужой секрет→404 (6/6).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 21:41:25 +03:00
Maxim Dolgolyov f37796f07b feat(assistant): описания модулей exam9/игры/quantik/live-quiz/classroom/sitemap в каталоге
В MODULE_CATALOG добавлены/исправлены записи под реальные фича-флаги:
exam9 (Подготовка к экзамену), classroom (Онлайн-урок), отдельно crossword и
hangman вместо общего games, + quantik (игра), live_quiz (Live-викторина),
sitemap (Путеводитель). Эти модули больше не «без описания» — Квантик отвечает
по ним подробно. Пометка «без описания» в админке очистится после переиндексации.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 21:38:04 +03:00
Maxim Dolgolyov 1915026bff feat(assistant): авто-переиндексация системы при смене флагов + пометка «без описания»
- updateFeatures вызывает _autoReindexSystem(): при тоггле любого модуля снимок
  знаний о системе обновляется сам (только если уже индексировали — не создаёт
  KB на пустом месте). Кнопку жать больше не нужно после смены флагов.
- getAssistant отдаёт systemUndoc — модули с фича-флагом, но без записи в
  каталоге; админ-карточка показывает «Без описания: …» (пассивная подсказка,
  без пушей), чтобы при желании дополнить «Описание системы».

Проверено: авто-реиндекс (не создаёт пустой / обновляет существующий) + undoc 3/3.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 21:34:43 +03:00
Maxim Dolgolyov 36eb0b980b feat(assistant): авто-подхват новых модулей по фича-флагам в индексации системы
buildSystemKb теперь добавляет в снимок ЛЮБОЙ фича-флаг, которого нет в
MODULE_CATALOG, как «функция X — вкл/выкл» (assistant-сам пропускается).
Новый модуль с фича-флагом попадает в знания Квантика автоматически после
«Проиндексировать», без правки кода. Для красивого описания/ссылки — запись
в каталоге или поле «Описание системы».

Проверено: авто-подхват 6/6, node --check OK.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 21:30:49 +03:00
Maxim Dolgolyov 08da26afca feat(assistant): индексация системы из админки — Квантик знает актуальные модули
Кнопка «Сохранить и проиндексировать систему» в /admin#assistant собирает снимок:
- статус модулей по фича-флагам (что ВКЛЮЧЕНО/ВЫКЛЮЧЕНО сейчас) + каталог разделов;
- редактируемое «Описание системы» админа.
Снимок кладётся в app_settings.assistant_system_kb и подмешивается в ответы:
systemContext(q) ищет по знаниям (стем-префикс под русскую морфологию) и
добавляет в контекст — Квантик опирается на актуальное состояние и не предлагает
отключённое.

Бэкенд: MODULE_CATALOG + buildSystemKb + indexSystem (POST /admin/assistant/index-system),
saveAssistant(+systemDoc), getAssistant(+systemDoc/Count/At), systemContext в ask и askStream.
Клиент: LS.adminAssistantIndexSystem. Без миграции (хранение в app_settings).
Проверено: логика снимка/поиска 5/5, node --check всех файлов.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 21:27:53 +03:00
Maxim Dolgolyov 70e1b0db53 fix(admin): синхрон вайтлиста FREE_STUDENT_MODULES с FS_FEATURES
Тумблеры imggen и quantik в админ-разделе «Бесплатный ученик» были
дохлыми: фронт (FS_FEATURES) их показывал, а бэкенд-вайтлист их отбрасывал
(updateFreeStudentFeatures: continue), getFreeStudentFeatures не возвращал —
тумблер всегда «вкл» и ничего не делал. Добавил imggen и quantik в список.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 19:16:37 +03:00
Maxim Dolgolyov ce4f1dcec1 fix(qbank): ручная транзакция вместо db.transaction (node:sqlite не имеет её)
DatabaseSync (node:sqlite) не имеет .transaction() как better-sqlite3 —
в контроллерах она добавлена обёрткой в db/db.js, а скрипт открывает БД
напрямую. Заменил на runTx() с ручным BEGIN/COMMIT/ROLLBACK. Применение
предложений (--apply) теперь работает; перегенерация JSON не требуется.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 17:21:43 +03:00
Maxim Dolgolyov 9858108556 feat(qbank): гард публикации теста + ИИ-инструмент ремонта банка вопросов
P0 целостность банка. Аудит показал: «180 битых» — ложная тревога (177 это
fill-blank с ответом в correct_text). Реально битых MCQ — 3 (single без верного
варианта), без темы — 1020 (все по математике).

- testController.update: нельзя опубликовать тест ученикам, если в нём есть
  вопрос без правильного ответа (нет верного варианта И нет correct_text);
  возвращает список brokenQuestions. unanswerableInTest() — переиспользуемо.
- scripts/fix-question-bank.js: ИИ-ремонт через шлюз Kilo. --broken (выбрать
  верный вариант среди существующих), --topics (привязать матем-вопросы к
  существующим темам, батчами). DRY-RUN по умолчанию → предложения в JSON на
  вычитку → --apply применяет. --limit N для теста. Проверено: broken 3/3
  (graph-вопросы корректно → ручная проверка), topics 12/12 в адекватные темы.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 16:53:15 +03:00
Maxim Dolgolyov 78aea47619 feat(assistant): генерация тестов в банк вопросов (фича 5/6)
Учитель: режим «Тест в банк» в Квантике — тема/текст превращается ИИ в вопросы
с выбором ответа, ревью в чате (варианты, верный подсвечен, пояснение),
кнопка «Сохранить в банк» (выбор предмета + тема) создаёт их через POST /questions.

Бэкенд: questionsFromText (по образцу flashcardsFromText, надёжный парс JSON
с починкой обрезанного) + роут POST /assistant/questions (requireRole
teacher/admin, fcLimiter). Клиент: LS.assistantQuestions. Виджет: режим quiz
только для учителя + makeQuiz (рендер и сохранение через createQuestion/getSubjects).

Проверено на живом шлюзе: 5 валидных вопросов, верный индекс в диапазоне.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 15:09:02 +03:00
Maxim Dolgolyov bc0ed1892f feat(assistant): авто-здоровье провайдеров + ручная проверка (фича 4/6)
Новый модуль assistant-health.js (по образцу classroom-cleanup): каждые 15 мин
пингует каждого провайдера (pingLLM) → app_settings.assistant_health
{ id:{ok,at,error,ms,fails} }. Авто-понижение: если активный провайдер
не отвечает 2+ раза подряд, а есть здоровый рабочий запасной — автоматически
переключает assistant_active и пишет assistant_failover (баннер «health»).
schedule() из server.js (unref).

Админка: тумблер «Авто-проверка провайдеров», кнопка «Проверить сейчас»
(POST /admin/assistant/health → runHealth), цветной индикатор здоровья на
каждой карточке провайдера (зелёный/красный + время/ошибка в title).
keyless-шлюзы и провайдеры без ключа учтены.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 15:02:37 +03:00
Maxim Dolgolyov 40c3152fe8 feat(assistant): сократический / анти-чит режим (фича 3/6)
- тумблер учителя «Сократический режим» (/admin#assistant): для УЧЕНИКОВ
  Квантик объясняет теорию полно, но конкретные задачи не решает «под ключ» —
  даёт метод, первый шаг и наводящий вопрос (assistant_socratic в app_settings)
- авто-анти-чит: явная просьба «сделай за меня / реши моё дз / do my homework»
  включает сократический режим даже без тумблера (_CHEAT_RE)
- учителей/админов и режимы hint/check не ограничивает; работает и в /ask, и в стриме

_socraticFor(role,mode,q) + проброс socratic в buildAskMessages. Бэкенд+админ-UI.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 14:57:24 +03:00
Maxim Dolgolyov 089f93b8ee feat(assistant): стриминг ответов Квантика (фича 1/6)
Ответ модели «печатается» вживую через SSE поверх POST (fetch-stream,
не EventSource). Бэкенд: callLLMStream (stream:true, парсинг SSE upstream) +
callLLMStreamFailover (failover только до первого куска) + endpoint
POST /assistant/ask/stream (события meta|delta|done; быстрые пути FAQ/кэш/мета
отдаются одним done). buildAskMessages выделен из askModel (DRY).
Клиент: LS.assistantAskStream (fetch-stream + парсер SSE). Виджет: send()
стримит дельты как plain-текст с CSS-кареткой, на done — KaTeX-рендер,
источники, ссылки, оценка. Фоллбэк на sendNonStream (старый путь) если
стриминг недоступен/упал до первого куска. Cache-Control: no-transform
отключает буферизацию compression.

Проверено против живого шлюза: 24 дельты, первый текст ~1.3с, 100% русский.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 14:50:11 +03:00
Maxim Dolgolyov 5b4d9324a4 feat(assistant): поддержка keyless-шлюзов + пресет Pollinations
Pollinations (text.pollinations.ai/openai, модель openai) даёт бесплатный
инференс БЕЗ ключа — проверено: 98% чистый русский. Чтобы такой провайдер
считался рабочим (раньше ключ требовался всем, кроме localhost):
- _noKeyNeeded/_aNoKey: localhost ИЛИ pollinations.ai → ключ не обязателен
  (используется в providersOrdered, pingLLM, active-check, testAssistant)
- пресет «Pollinations (без ключа)» в ASSISTANT_PRESETS
- бейдж провайдера: «без ключа» (зелёный) вместо «нет ключа» для keyless

Кейд-провайдеры (Kilo/Gemini/HF/…) по-прежнему требуют ключ — затронуты
только URL с pollinations.ai (спуф в пути отвергается).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 14:35:56 +03:00
Maxim Dolgolyov 4d2d02f080 feat(assistant): пресет провайдера HuggingFace Router
Добавлен OpenAI-совместимый шлюз HuggingFace Router
(router.huggingface.co/v1/chat/completions, дефолт Qwen/Qwen2.5-72B-Instruct)
в ASSISTANT_PRESETS — выбирается из выпадашки при добавлении провайдера.
Эндпоинт /models публичный (121 модель), «Загрузить модели» работает;
нужен HF access-token с правом inference.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 14:23:55 +03:00
Maxim Dolgolyov d15c15ef2a feat(assistant): сканер бесплатных моделей Kilo в админке
Кнопка «Сканировать модели» в /admin#assistant: тянет live-список со шлюза
провайдера, отбирает бесплатные чат-модели (музыка/картинки/модерация
отсекаются), прогоняет каждую тест-запросом на русском и показывает отчёт
(новые / исчезнувшие / % кириллицы / скорость). «Применить выбранные»
сохраняет список в app_settings (assistant_kilo_models); хардкод KILO_MODELS
остаётся сидом, есть «Вернуть встроенный список».

Backend: scanModels/probeModel/applyModels (admin-only роуты), _kiloModels()
делает список динамическим. Переиспользует _fetchModels. Клиент: adminAssistantScan/Probe/ApplyModels.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 14:18:04 +03:00
Maxim Dolgolyov dc5501d723 fix(assistant): актуализирован список бесплатных моделей Kilo
Сверено с live-списком шлюза kilocode.ai + протестировано на русский (2026-06-24):
- удалён исчезнувший со шлюза nex-agi/nex-n2-pro:free
- исправлены устаревшие лимиты: nemotron-super ctx 1M->262K, owl-alpha опечатка 1048756->1048576
- добавлен openrouter/free (авто-роутер, 100% русский, устойчив к пропаже отдельных моделей)
- дефолтный пресет Kilo: ultra-550b (таймаутит) -> super-120b (быстрый, чистый русский)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 14:04:13 +03:00
Maxim Dolgolyov 43df41287f feat(errors): сбор клиентских (браузерных) ошибок в админ-вкладку «Ошибки»
Глобальный репортер в api.js (грузится на всех страницах) ловит необработанные JS-ошибки
(window 'error') и rejected-промисы ('unhandledrejection') в браузере пользователя и шлёт
в POST /api/client-errors. Дедуп по сигнатуре + лимит 15/страницу, только для залогиненных,
keepalive, не флудит и сам не падает.

Бэкенд: routes/clientErrors (auth + rate-limit 20/мин на юзера) → clientErrorController
пишет в общий error_log с level='client' (message/stack/route=url/method=kind/user_id),
поля обрезаются. Появляются в существующей админ-вкладке «Ошибки» с бейджем «БРАУЗЕР»
(фиолетовый акцент vs розовый у серверных). Тест client-errors.test.js 5/5.

lint:routes 0; node --check всех файлов.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 23:17:04 +03:00
Maxim Dolgolyov 1649d6c2ec fix(admin): список сессий показывает и незавершённые (зависшие), +фильтр ?status
Корень бага «зависший тест есть в алерте, но нет в списке»: getAllSessions жёстко
фильтровал WHERE status='completed' → in_progress (зависшие) сессии в списке /admin#sessions
не появлялись. Теперь по умолчанию показываются все статусы (UI sessions.js уже null-safe:
percent/score/duration рисуются как «—»), плюс опциональный ?status=completed|in_progress|abandoned
для сужения. Зависшая сессия теперь находится и в списке, и открывается по deep-link из алерта.

admin-sessions.test.js 4/4; node --check OK.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 20:14:56 +03:00
Maxim Dolgolyov be9fdfa703 feat(wishes): трекер пожеланий по улучшению системы
Любой авторизованный пользователь подаёт пожелание (заголовок, категория, описание);
видит только свои. Админ видит все, фильтрует по статусу, ведёт по статусам
(новое → запланировано → в работе → готово / отклонено) и пишет ответ автору. Автор
получает уведомление при смене статуса (pushNotif).

Бэкенд: миграция 080 (таблица wishes), wishController (list/create/update/remove с
валидацией и whitelist категорий/статусов), routes/wishes (PATCH — только админ, DELETE —
автор«новое»/админ, проверка в хендлере), смонтировано в server.js. Тесты 15/15.

Фронт: страница /wishes (форма + список со статус-бейджами; у админа — фильтры,
смена статуса, ответ, удаление), пункт «Пожелания» в сайдбаре (все роли), фиче-флаг
feature_wishes_enabled (тумблер в админ-модулях + whitelist + FEATURE_HREFS; админ
видит всегда). Клиентские врапперы LS.wish*.

⚠️ Живой БД нужен npm run migrate (080). lint:routes 0; node --check всех файлов + инлайна.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 16:12:10 +03:00
Maxim Dolgolyov 758e1bf6cb feat(dashboard): статус «идёт онлайн-урок» с присоединением
На дашборде ученика/учителя — баннер активной classroom-сессии: заголовок урока,
для учителя «N онлайн», для ученика «Присоединиться/Вернуться», ссылка на /classroom
(там сессия подхватывается автоматически). Данные — LS.crGetMySession (учитель → своя
сессия, ученик → сессия его класса/приглашения). Нет активной сессии → баннер скрыт.

Доска работает по WebSocket, дашборд — по SSE, поэтому добавлен отдельный SSE-сигнал
classroom_live (state started/ended) ученикам класса/приглашённым/учителю в createSession
и endSession (аддитивно, в try/catch — не ломает создание/завершение сессии). Баннер
живо появляется/исчезает по этому событию + обновляется при возврате на вкладку.

Verified: рендер баннера 10/10 (ученик/учитель/нет сессии, online-счёт без вышедших,
пустой title→«Онлайн-урок»); node --check sessions.js + инлайна dashboard; sse-путь резолвится.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 14:24:14 +03:00
Maxim Dolgolyov 0d4c658d93 refactor(assignments): единый модуль assignment-utils.js (тип/«сдано»/срочность)
Логика классификации типа задания и статуса «сдано» дублировалась в трёх местах
(dashboard.html, homework.html, assignmentController.js) и начала расходиться. Вынес в
один UMD-модуль frontend/js/assignment-utils.js (грузится и в браузере, и в Node через
require — как svg-sanitize.js): type(a), isDone(a, sub, opts), urgencyScore(a).

Нюанс «сдано» для upload/file параметризован: вид ученика (acceptedOnly) — закрыто только
при принятой сдаче; учитель/обзор долгов — любая сдача не на доработке. Поведение всех трёх
поверхностей сохранено 1:1.

- homework.html: asgnType/asgnDone/urgencyScore → тонкие делегаты в AssignmentUtils.
- dashboard.html: urgencyScore делегирует; classify и upload-ветка buildAssignCard через
  AssignmentUtils.type/isDone (заодно корректнее: учебник-ДЗ больше не путается с upload).
- assignmentController: classOutstanding/_assignTypeOf → AssignmentUtils.

Verified: AU-контракт 25/25 (типы, isDone teacher vs acceptedOnly, порядок urgency);
интеграция 8/8 (classOutstanding те же 14 уч./42 просрочено; homework делегирует). node --check
всех файлов + инлайна обоих HTML.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 14:09:34 +03:00
Maxim Dolgolyov 5a4bc48027 feat(classes): вкладка «Долги» — что висит у учеников + удаление ДЗ класса/ученика
Новый read-only эндпоинт GET /api/classes/:id/outstanding (teacher/admin, ownership):
по каждому ученику класса — его незакрытые задания (классовые + личные от этого учителя)
со статусом не начато / в процессе / на доработке / просрочено и дедлайном. Логика «сдано»
совпадает с /homework (тест — завершён/исчерпаны попытки; учебник — всё прочитано;
загрузка/файл — есть сдача не на доработке). Общий запрос вынесен в assignmentRowsForUser(uid)
— им же теперь питается /assignments/my (поведение не изменилось, +поле created_by).

Фронт (classes.html): вкладка «Долги» в карточке класса — сводка «должников X из Y,
просрочено Z», по каждому должнику карточка со статус-чипами и списком зависших заданий;
бейдж с числом просрочек на вкладке. Удаление прямо из списка: личное → «у ученика»,
классовое → «у всего класса» (через DELETE /assignments/:id, ownership на бэке).

Verified: classOutstanding смоук на живой БД (14 учеников/58 позиций/42 просрочено,
ownership 403/404/admin); node --check; lint:routes 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 13:46:45 +03:00
Maxim Dolgolyov 22c7b38e9a feat(admin): сброс системы «чистый запуск» в веб-панели
Добавлено такое же действие, как [Z] в control-panel: POST /api/admin/reset-system
(+ /reset-system/plan для предпросмотра), только admin. Общая логика вынесена в
src/services/systemReset.js (classify/pickKeptAdmin/runReset) — реюзится CLI и эндпоинтом.

Веб-эндпоинт безопаснее CLI: сохраняет ТЕКУЩЕГО админа (оператор остаётся залогинен),
делает бэкап БД ДО сброса (wal_checkpoint + копия в data/backups/), требует body.confirm='СБРОС'.
UI — «Опасная зона» в overview-секции: предпросмотр плана + ввод «СБРОС» + результат с именем бэкапа.

db.js: добавлен db._path (нужен бэкапу при сбросе). Логика проверена смоуком на копии живой БД
(16 юзеров удалено, контент сохранён, REASSIGN на админа, гейм-счётчики обнулены, 0 висячих FK).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 11:45:13 +03:00
Maxim Dolgolyov 205290139d feat(control-panel): сброс системы «чистый запуск» (с бэкапом и подтверждением)
Пункт [Z] в control-panel.ps1: предпросмотр → бэкап БД → подтверждение вводом
«СБРОС» → очистка. Скрипт backend/scripts/reset-system.js (dry-run по умолчанию,
выполнение только с --apply --confirm=RESET):
• сохраняет одного админа (min id), переназначает ему авторский контент
  (courses/tests/flashcard_decks/custom_sims/шаблоны/библиотека/lab-ссылки/board);
• стирает всех остальных пользователей + классы/задания/сессии/геймификацию/
  уведомления/прогресс/историю классрума/доступы/логи;
• сохраняет контент: учебники, вопросы, темы, уроки, exam-prep, симуляции,
  биохимия, красная книга, магазин/достижения-определения, роли/права, app_settings;
• обнуляет игровые счётчики админа; классифицирует ВСЕ 119 таблиц, неизвестные не трогает;
• FK off + транзакция + foreign_key_check + VACUUM.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 11:32:44 +03:00
Maxim Dolgolyov c6d323ec6d feat(tests): витрина доступных тестов ученику + флаг «доступен ученикам»
Раньше ученик видел лишь 1 тест на предмет (дефолтный). Теперь учитель/админ
может пометить любой свой тест доступным, и он появляется в каталоге на дашборде.

- Миграция 079: tests.available_to_students (default 0).
- testController: list для ученика отдаёт тесты с available_to_students=1 и вопросами;
  create/update принимают флаг; update сделан частичным (не затирает поля при toggle).
- admin «Тесты»: бейдж «Доступен ученикам» + быстрый тумблер «Ученикам/Скрыть»
  (toggleTstAvail; конструктор доступен и учителям — видят свои тесты).
- Дашборд: виджет «Тесты» → секция «Доступные тесты» (loadAvailableTests), клик
  запускает фикс-тест. Прячется, если доступных нет.

⚠️ Живой БД нужен npm run migrate (колонка).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 11:03:42 +03:00
Maxim Dolgolyov c5d440a7a9 fix(tests): режимы доступных тестов только exam/practice + скрытие пустых предметов
Рассогласование: админ-настройка допускала режимы topic/random, но POST /api/sessions
принимает только exam/practice → клик по такому предмету падал с 400. Убрал topic/random
из валидатора subjects.js и из админ-дропдауна (SC_MODES). Дашборд: старые значения
topic/random коэрсятся в practice; предметы без вопросов в банке И без фикс-теста больше
не показываются (раньше давали 404 «No questions found» при запуске).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 10:53:43 +03:00
Maxim Dolgolyov 38f8be9389 feat(features): тумблер «Путеводитель» (/sitemap)
Пункт «Путеводитель» теперь отключается как остальные модули: ключ sitemap в
вайтлист updateFeatures (backend) + тумблер в admin → фичи (GAME_FEATURES) +
/sitemap,/sitemap.html в FEATURE_HREFS (скрытие из сайдбара + редирект при выкл).
По умолчанию ВКЛ (opt-in disable). Пустая группа схлопнётся авто (hideEmptySidebarGroups).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 17:51:36 +03:00
Maxim Dolgolyov 83f0ba9c04 fix(features): пустой блок флешкарт, лаба в сайдбаре, мигание (FOUC)
Три проблемы UX отключения модулей:
1) Пустой блок флешкарт на дашборде: виджет #w-flashcard — <div>, а скрытие шло
   только по [href]. Добавлена карта FEATURE_WIDGETS (флешкарты→#w-flashcard) —
   контейнер прячется целиком.
2) Лаборатория не уходила из сайдбара: не было ГЛОБАЛЬНОГО тумблера lab (только
   free-student). Добавлен в whitelist updateFeatures + GAME_FEATURES; map уже знал
   lab→/lab. Теперь выключение скрывает пункт и редиректит со страницы.
3) Мигание выключенных модулей (FOUC): hideDisabledFeatures асинхронный. Теперь
   loadFeatures кэширует /api/features в localStorage, а _applyFeatureCss инъектит
   <style id=ls-feat-hide> синхронно из кэша на ранней загрузке (api.js идёт до
   sidebar.js) — сайдбар/виджеты строятся уже скрытыми. Геймификация: класс
   no-gamification ставится на <html> (раньше body), ls.css правило body.no-gamification
   → .no-gamification (работает и для html, и для body).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 17:41:11 +03:00
Maxim Dolgolyov d5fbd0168e feat(permissions): +10 прав ролей с энфорсом (Доступ · роли)
Реестр (registry.js) пополнен правами, которыми раньше нельзя было управлять:
• Учитель: classroom.host (онлайн-уроки), livequiz.host (живые викторины),
  simbuilder.use (конструктор симуляций), flashcards.manage (общие колоды).
• Ученик: homework.submit (сдача ДЗ), materials.save («Мои материалы»),
  assistant.use (ИИ-ассистент), games.play (учебные игры),
  flashcards.access / exam.access (доступ к разделам).
Все default=1 → текущее поведение сохранено; админ может выключить по роли/классу/юзеру.

Энфорс на роутах: учительские — requirePermission (роуты уже teacher-only);
ученические на ОБЩИХ роутах (assistant/materials/games/flashcards/exam-prep) —
новый requirePermissionForStudents(key) (учитель/админ проходят всегда, проверка
только ученику — иначе isEnabled=false сломал бы учителя). PERM_DEFAULTS строится
из реестра → фолбэк до сидирования = enabled, никто не блокируется. Группы UI —
существующие (новых ярлыков нет). seedDefaults авто-сидит новые ключи на чтении.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 17:31:00 +03:00
Maxim Dolgolyov dc71d7b4d9 fix(gamification): полнота kill-switch — испытания/стрик/монеты + гейт счётчиков
Аудит выключателя геймификации выявил элементы, НЕ покрытые body.no-gamification:
испытания недели (#ch-section/.ch-widget), календарь стриков (.streak-cal),
стат-кольцо стрика (#sr-streak), монеты в профиле (#p-coins-row), чипы стрик/цель
на карточке питомца. Добавлены в CSS kill-switch (ls.css). Бэкенд: updateChallenges
и onLabExperiment писали прогресс/счётчики без проверки флага — добавлен гейт
isGamificationEnabled() (XP/coins/achievements уже гейтились в award*-функциях).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 17:04:30 +03:00
Maxim Dolgolyov d8f2a7f98d fix(features): доска уходит из сайдбара при отключении + тумблер «Теория»
1) Доска при feature_board_enabled=0 не пропадала у учителя/админа: showBoardIfAllowed()
   зовётся напрямую на ~20 страницах и показывала доску БЕЗ проверки флага, перекрывая
   hideDisabledFeatures(). Теперь функция сперва грузит features и при board===false
   держит ссылку скрытой (для всех ролей).
2) Добавлен фич-флаг theory: ключ в вайтлист updateFeatures (backend), тумблер «Теория»
   в admin → games (GAME_FEATURES), и /theory,/theory.html в map hideDisabledFeatures
   (скрытие из сайдбара + редирект с /theory при выключении). По умолчанию ВКЛ (opt-in disable).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 16:56:22 +03:00
Maxim Dolgolyov bd7dd06e47 fix(exam-prep): репаратор рендеринга ctmath — потерянные \ в опциях + <,> в $…$
Root cause: в seed-ах вариантов 101–121 опции писались как mc('$\sqrt..$') в
обычных кавычках вместо R`…` → JS-парсер съедал \s→s, \d→d и т.п., в БД легло
«$sqrt{17}$», «$dfrac{pi}{3}$» (KaTeX рисует «sqrt17», «dfracpi3»). Плюс литеральные
<,> внутри $…$ ломали HTML до KaTeX. Скрипт fix_ctmath_render.js (dry-run/--apply,
идемпотентный): восстанавливает \ перед командами (+нормализует управляющие символы)
в opts_json и заменяет <,>→\lt,\gt внутри $…$ в text/opts/solution. DRY-RUN: 307
строк (143 opts/128 text/157 sol), остаточных багов 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 19:34:59 +03:00
Maxim Dolgolyov dd69c026ec content(ctmath): вариант 121 — ЦТ-2011 (А1–А18 + В1–В12, 30 заданий)
Перенабор Вариант 1 из ЦТ 2011 В1-В10.pdf (тест полный, не только В), все 30
ответов сверены с официальной таблицей (полное совпадение). Уточнения по таблице:
A6 степень 3x+4 (→15·2^3x), A9=(3^-2)^-5→1/9, A7 корень -3, A8=10, A10=10π.
Фигурные A1/A2/B6 реконструированы (B6: y=x²-6x+9 ∩ y=1,25 → 4x₁x₂=31). Все В
числовые. Без авторских ссылок. Дедуп-гейт 0, KaTeX 30/30, DRY-RUN 30/30. Метка 121='ЦТ-2011'.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 16:53:06 +03:00
Maxim Dolgolyov 84625cd72a content(ctmath): вариант 120 — ЦТ-2012 (А1–А18 + В1–В12, 30 заданий)
Перенабор Вариант 1 из ЦТ 2012.pdf, все 30 ответов сверены с официальной
таблицей (полное совпадение). Фигурные A1/A13/B6 реконструированы с явными
данными (A1 — углы 70/40→равнобедренный; B6 — середины сторон→S=4). A15
уточнена по таблице: √(5⁵·20)=250, знаменатель ⁴√10 → 25·⁴√10. Все В числовые.
Без авторских ссылок. Дедуп-гейт 0, KaTeX 30/30, DRY-RUN 30/30. Метка 120='ЦТ-2012'.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 16:42:27 +03:00
Maxim Dolgolyov 0fb16ef85e content(ctmath): вариант 119 — ЦТ-2013 (А1–А18 + В1–В12, 30 заданий)
Перенабор Вариант 1 из ЦТ2013.pdf, все 30 ответов сверены с официальной
таблицей (полное совпадение). Фигурные A2/A3/A6/A16 реконструированы с явными
описаниями (A2 — образующая=AD, A6 — порядок лучей→40°, A16 — сечение 12×6=72).
Все В-задания числовые (long нет). Без авторских ссылок. Дедуп-гейт 0,
KaTeX 30/30, DRY-RUN 30/30. VARIANT_LABEL: 119='ЦТ-2013'.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 16:35:28 +03:00
Maxim Dolgolyov b9a82c326e content(ctmath): вариант 118 — ЦТ-2017 (А1–А18 + В1–В12, 30 заданий)
Перенабор Вариант 1 из CT-2017.pdf, все 30 ответов сверены с официальной
таблицей (полное совпадение). Фигурные A1/A3/A9/A11/A14 реконструированы с
явными числами (A9 — биссектриса, AM/MC=AB/BC→13,8). Без авторских ссылок.
Прогнан через дедуп-гейт (0 совпадений с пулом) + KaTeX-структуру + DRY-RUN 30/30.
VARIANT_LABEL: 118='ЦТ-2017'.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 16:28:06 +03:00
Maxim Dolgolyov 70cf6b3af1 tools(ctmath): check_variant_dups.js — гейт дедупликации перед добавлением варианта
Постоянный read-only инструмент: (1) без аргумента — аудит видимого пула [101;1999]
на внутренние точные дубли; (2) с seed-файлом — сверяет его TASKS с пулом ДО --apply.
Норма текста: теги/латех/пробелы убраны, ЧИСЛА сохранены (параллельные задачи дублями
не считаются). Сейчас пул = 514 задач, 514 уникальных сигнатур, 0 дублей.
Включается в конвейер тиража: новый вариант проверяется этим гейтом перед записью.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 13:31:02 +03:00
Maxim Dolgolyov 59ae4c1dea fix(exam-prep): практика/тренажёр берут только выверенные варианты (дедуп)
Тренажёр-по-темам и практика брали FROM exam_tasks без фильтра по варианту — в пул
попадали год-пачки (variant=год≥2011) и variant=0, которые ДУБЛИРУЮТ выверенные
варианты-пробники (51 дубль чистый↔пачка, 20 через variant=0). Ученик мог получить
одну задачу дважды.

Добавлен фильтр variant BETWEEN MV_LO..MV_HI (тот же [101;1999], что у пикера) во все
7 запросов выборки/счёта задач: practiceRandom, practiceUnsolved, topicTasksUnsolved,
topicTasksAny, listTopicsWithCounts (счётчик подтем), weakBatchTasks, pickRandomByDifficulty
(×2). Хелперы MV_LO/MV_HI (для math9 без диапазона — всё, кроме variant=0).

Результат: практика ctmath = только варианты 101–117 (496 задач, 0 дублей между собой),
год-пачки (714 задач) остаются в БД для возможного будущего, но не показываются.
Обратимо, без удаления данных. Рантайм-проверка: 5 эндпоинтов практики/тем → 200.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 13:29:02 +03:00
Maxim Dolgolyov de41b77ae3 feat(ctmath): вариант 117 — ЦТ-2021 (32 задания, А1-А18 + В1-В14)
Пробник ЦТ по математике 2021, Вариант 1. Формат: 32 задания (А1–А18 + В1–В14), А12/А16 —
с несколькими верными (тип open, ответ цифрами), В2/В3 множ.выбор, В1 на соответствие.
Источник: чистый PDF ЦТ 2021.pdf. ВСЕ 32 ответа решены и сверены с официальной таблицей
(стр.45, столбец Вариант 1) — полное совпадение, включая B9=324, B11=960, B13=460 (пары чисел),
B14=1375 (описанный четырёхугольник, r=60/7). Фигурные A7/A17/B1/B4 реконструированы; B5: в скане
∛(-7) → на деле ∛(-343)=-7 (иначе нецелый), ответ -98. Без авторских ссылок.
VARIANT_LABEL 117 -> 'ЦТ-2021'. DRY-RUN 32/32, self-check и структурный KaTeX — зелёные.
Запись в БД — пользователь: node backend/scripts/seed_ctmath_ct2021_v1.js --apply

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