Commit Graph

94 Commits

Author SHA1 Message Date
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 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 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 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 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 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 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 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 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 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
Maxim Dolgolyov 7bf1da94e4 feat(pet): гардероб — выбор аксессуаров + новые украшения
Аксессуары больше не навешиваются авто по уровню — теперь разблокируются
и НАДЕВАЮТСЯ по выбору (один на слот). Новые: шапочка выпускника, наушники,
бабочка (бесплатные — доступны даже при 0 XP). Сохранены цилиндр/корона/очки/
звезда с прежними порогами; дефолт повторяет старый вид (без сюрпризов).

Бэкенд: миграция pet_equipped, каталог ACCESSORY_CATALOG + /api/pet/equip
(валидация разблокировки и слотов). Рендер аксессуаров строго по equipped —
в обеих копиях (pet.html и pet-sprite.js для дашборда). UI гардероба на /pet.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 13:37:45 +03:00
Maxim Dolgolyov 6e0a00fd8b feat(assistant): авто-получение лимитов моделей для любого провайдера
Новый GET /admin/assistant/models: тянет список моделей провайдера с лимитами
(OpenAI-совместимый /models: context_length+max_completion_tokens+pricing;
нативный Google generativelanguage: inputTokenLimit/outputTokenLimit) и кэширует
лимиты текущей модели на провайдере. Карточка показывает лимиты у ВСЕХ провайдеров
(не только Kilo), для отсутствующих — фоновая авто-подгрузка. В форме — кнопка
«Загрузить модели провайдера» с выбором модели и её лимитами. Так Gemini и любые
новые модели получают лимиты автоматически.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 21:28:34 +03:00
Maxim Dolgolyov e2bff24b5b feat(assistant): несколько провайдеров ИИ + выбор активного + авто-перехват при лимите
Конфиг стал списком провайдеров (assistant_providers) + активный (assistant_active).
llmConfig берёт активного; providersOrdered — активный первым, затем остальные с
ключом; callLLMFailover перебирает их при 429/сетевой ошибке (второй ключ подхватывает
при исчерпании квоты). Legacy мигрируется в список. Админ-раздел: список провайдеров
(радио-активный, Тест/Изменить/Удалить) + форма с пресетами. Эндпоинты
POST/DELETE /admin/assistant/provider(/:id), POST /admin/assistant/active.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 20:21:06 +03:00
Maxim Dolgolyov 4224a22092 feat(assistant): источники в ответах, режим-наставник, оценки, утренний бриф
- Источники: RAG возвращает sources (slug/§/ref), под ответом ссылка «по учебнику
  X, §N» на параграф (миграция 064: section_ref в textbook_chunks; headless-индексатор
  его заполняет). Статический индексатор теперь не затирает headless-данные.
- Режим-наставник: переключатель Ответ/Подсказка/Проверить решение в «Спроси»
  (mode в ask + промпт); на карточке экзамена кнопка «Подсказка» (mode hint).
- Оценка ответов: лайк/дизлайк под ответом (assistant_feedback) + сводка в админке.
- Утренний бриф на дашборде: «занимался N из 5 дн + план на сегодня».

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 19:38:47 +03:00
Maxim Dolgolyov 2252bbd666 feat(assistant): RAG по учебникам, кэш+счётчик, режим учителя
- RAG: индексатор scripts/index-textbooks.js → textbook_chunks (миграция 063);
  ask() подмешивает релевантные куски учебников (LIKE-скоринг). Покрывает
  учебники со статическим текстом; JS-рендеримые — через контекст страницы.
  Админка: тумблер RAG + кнопка «Переиндексировать» + число фрагментов.
- Кэш ответов (assistant_cache, 7 дней, только «чистые» вопросы без контекста/
  истории) + суточный счётчик (assistant_usage: ИИ/кэш/FAQ) в админке.
- Режим учителя: роль в /context, системный промпт для учителей (задания,
  план урока, учительские инструменты), подсказки-чипы для учителей.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 18:16:53 +03:00
Maxim Dolgolyov dc073e2114 feat(assistant): админ-панель LLM (ключ/URL/модель/тест) + многоходовой чат
Админка (Управление → игры/фичи): карточка «Помощник Квантик — модель» —
пресеты провайдеров, URL/модель, поле ключа, кнопки Сохранить/Проверить/
Очистить ключ, индикатор статуса. Конфиг в app_settings (без рестарта),
откат на ENV/дефолты; нет ключа → автоматически FAQ-режим. Эндпоинты
GET/PUT/POST /api/admin/assistant(/test), admin-only.

«Спроси Квантика» теперь многоходовой чат: история диалога (последние 6
реплик) уходит модели, ответы рендерятся как чат-лента, кнопка «Очистить».

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 18:04:42 +03:00
Maxim Dolgolyov 479c621e2e feat(assistant): markdown+KaTeX, «Объясни это», репетитор на экзамене, флешкарты
- Ответы модели рендерятся как markdown + формулы KaTeX (ленивая загрузка),
  модель просим оформлять формулы в LaTeX $...$.
- «Объясни это»: ask принимает context; кнопки «Объяснить выделенное» (запоминаем
  выделение) и «Объяснить/Конспект параграфа» на учебнике.
- Репетитор на экзамене: кнопка «Спросить Квантика» на карточке задания →
  Assistant.ask с условием/ответом/решением как контекстом.
- Быстрые действия: «Флешкарты из параграфа» → POST /api/assistant/flashcards
  (модель → JSON, починка обрезанного) → колода через существующий API флешкарт.
- Экспорт Assistant.ask(q,context) / explainSelection().

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 17:53:45 +03:00
Maxim Dolgolyov 3f8009c59d feat(assistant): Квантик-ассистент — Ф0/Ф1 + «Спроси» (правиловый движок)
Плавающий помощник на всех страницах (через sidebar.js + inject в учебник):
контекстные подсказки по странице, проактивные напоминания из реальных данных
(домашка с дедлайном, карточки к повторению, серия под угрозой, квест дня),
поздравления (левелап/серия) и панель «Спроси Квантика» (поиск по FAQ + точка
расширения под локальную модель). Консервативно: дневной лимит, кулдауны,
«не показывать», выключатель в профиле. Лицо — pet-sprite, данные — /api/pet.

Бэкенд: миграция 062 (assistant_enabled + assistant_seen, cross-device «видел»),
GET /api/assistant/context, POST seen/dismiss/ask, PATCH settings — гейт фичи 'pet'.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 16:17:37 +03:00
Maxim Dolgolyov 53e996e2e0 fix(materials): картинки материалов отдаются публично (рендер/открытие/скачивание)
/api/files/:id/download требует Bearer-заголовок, поэтому <img>, переход по
ссылке и «Скачать» для сохранённых картинок ломались (битое изображение,
клик не открывал). Теперь личные картинки складываются в uploads/materials и
отдаются статикой (как flashcards): POST /api/files/personal возвращает
{ url:'/uploads/materials/<file>' }. board-clip, material-save, textbook-clip
и рисовалка в my-materials сохраняют этот публичный url.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 14:30:47 +03:00
Maxim Dolgolyov 55c8c5fa51 fix(materials): личная загрузка картинок без права library.upload
POST /api/files требует teacher/admin + library.upload — поэтому сохранение
картинок в «Мои материалы» (вырезка области учебника, обрезка доски,
рисунок, аннотация) падало с 403 у учеников и учителей без этого права.

Добавлен auth-only эндпоинт POST /api/files/personal (только картинки,
is_public=1) + LS.uploadMaterialFile. На него переключены board-clip,
material-save, textbook-clip (вырезка области) и рисовалка в my-materials.
Загрузка в учительскую библиотеку (library/lesson-editor) не тронута.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 14:21:18 +03:00
Maxim Dolgolyov 8e8f54b41b feat(dashboard): блок активности — все виды учёбы, тренд, разбивка по типам, empty-state
- Бэкенд /api/dashboard/activity: per-day агрегация активности по типам (тест/экзамен/карты/уроки/
  онлайн/домашка) из 6 таблиц за ~182 дня (раньше карта считала только тесты).
- Карта раскрашивается по доминирующему типу активности + легенда типов; интенсивность/размер по числу.
- Недельный тренд в футере («эта неделя N · +K к прошлой»).
- Тултип и попап по клику показывают разбивку дня по типам.
- Empty-state для новичков (вместо пустой сетки — призыв + CTA). Календарь «Месяц» тоже от всех активностей.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 13:06:46 +03:00
Maxim Dolgolyov f7357adf1e feat(materials): Фаза 6b — раздатка материала ученикам/классу
- POST /api/materials/:id/share {classId|userId} (teacher/admin): создаёт независимую КОПИЮ
  материала каждому ученику класса (source_title «Раздатка: <учитель>») + уведомление через SSE.
- /my-materials: кнопка «Раздать» на карточках (видна учителю/админу) → выбор класса.
- Хелпер LS.shareMaterial. На этом план «Мои материалы» (6 фаз) завершён.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 12:26:46 +03:00
Maxim Dolgolyov 2c7e97406a feat(materials): Фаза 2 — коллекции (папки), поиск и фильтры
- Миграция 061: material_collections + student_materials.collection_id (ON DELETE SET NULL) + tags.
- API: CRUD коллекций (/api/materials/collections), GET /materials отдаёт {materials, collections}
  со счётчиками; PATCH /materials/:id принимает collection_id/tags. Хелперы в js/api.js.
- /my-materials: бар папок (Все/папки/Без папки/+папка) с фильтром, поиск по тексту, фильтр по типу,
  перенос материала в папку (select на карточке), создание/переименование/удаление папок.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 12:04:51 +03:00
Maxim Dolgolyov fd3e5c47e8 feat(materials): Фаза 1 — правка, переименование, создание заметки
- PATCH /api/materials/:id (title, body) с проверкой владельца (@public-by-design) + LS.updateMaterial.
- /my-materials: кнопка «+ Заметка» (личный блокнот с нуля), «Изменить» на карточках
  (заголовок; для заметок — и текст) через LS.modal.
- Добавлен план развития «Мои материалы»: plans/my-materials/PLAN.md (6 фаз).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 11:55:15 +03:00
Maxim Dolgolyov 44ab5e045e feat(lessons): «Мои материалы» — ученик сохраняет материалы урока к себе
Ученик на странице «Мои уроки» может сохранить к себе страницу доски (PNG) и свою заметку
из прошлой онлайн-сессии. Копия хранится у ученика и переживает удаление сессии учителем.

- Миграция 060: student_materials (kind board/note/link/image, denormalized source_title,
  source_session_id ON DELETE SET NULL).
- API /api/materials (GET/POST/DELETE, авторизация + проверка владельца) + helpers в js/api.js.
- my-lessons.html: кнопки «К себе» на доске и заметке (Whiteboard.exportBlob → /api/files → saveMaterial).
- Новая страница /my-materials (просмотр/открыть/скачать/удалить) + пункт сайдбара (ученик).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 11:33:01 +03:00
Maxim Dolgolyov 6be8a505eb feat(lessons): «Быстрый урок» — одиночный урок без ручного создания курса
Учитель жмёт «Быстрый урок» в каталоге (theory.html) → урок создаётся в скрытом личном
курсе-контейнере и сразу открывается редактор. Возни с курсом нет.

- Миграция 059: courses.is_personal (ADD COLUMN).
- POST /api/lessons/quick (teacher/admin): get-or-create личный контейнер (is_personal=1,
  один на учителя, опубликован) + создаёт урок, возвращает lessonId.
- Каталог курсов скрывает личные контейнеры от всех, кроме владельца (courseController.list).
- Свои быстрые уроки учитель видит как курс «Мои материалы» (открыв его в каталоге).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 20:42:14 +03:00
Maxim Dolgolyov a096f3bcd9 feat(exam): Phase 4 — контроллер использует task-level textbook_slug/paragraph
- SELECT добавляет t.textbook_slug, t.textbook_paragraph во все 7 запросов
  (getVariantTasks, practiceRandom, practiceUnsolved, pickRandomByDifficulty,
   topicTasksUnsolved/Any, weakBatchTasks, getTasksByIds)
- shapeTask() предпочитает task-level ссылку, fallback на refMap subtopic
- /variants/:n/tasks аналогично использует per-task поля
- getParaTitle() строит map chapter:para -> title из g9_textbook_sections.json

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 16:18:41 +03:00
Maxim Dolgolyov bdc8bef857 feat(permissions): C-4a — API конструктора ролей (/api/roles, admin)
rolesController + routes/roles (admin, inline guards): GET список (с числом
пользователей), POST создать кастомную роль (имя-идентификатор + метка + base_roles;
засев прав из функциональной базы), PUT изменить, DELETE удалить (пользователей
возвращает на базу), GET /:name/permissions (эффективная карта база+оверлей + defs).
setPermission теперь принимает кастомные роли (ключ валидируется по базе, хранится
под именем роли). Смонтировано в server.js + тест-харнесс. Тест roles-api 5/5.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 15:21:44 +03:00
Maxim Dolgolyov 8b495f1508 feat(permissions): B7 — пресеты-профили прав (применение к классу одним кликом)
PRESETS (student): «Полный доступ», «Режим фокуса» (без магазина/испытаний),
«Ограниченный» (+ без лаборатории), «Сбросить к стандарту роли». GET
/api/permissions/presets + POST /api/permissions/class/:id/preset (admin).
Рефактор: общий applyPermsToClass() (карта key→1/0/inherit) — его используют и
bulk, и preset. В блоке «Массово по классу» — кнопки пресетов (с подтверждением).
Тест: список + применение focus/reset + валидация. Backend pass (3 baseline-Auth).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 14:33:25 +03:00
Maxim Dolgolyov b95b639e75 feat(permissions): B6 — массовая выдача права классу (личный оверрайд всем ученикам)
POST /api/permissions/class/:id/bulk { permission, enabled } (admin, явный
requireRole) — выставляет user_permissions всем ученикам класса (1/0/null=сброс),
точечный token_version bump каждому. Валидация: только студенческие ключи.
Клиент LS.setClassPermission. В админке «Доступ · роли» — блок «Массово по
классу»: выбор класса → у каждого права «включить/выключить всем / сбросить».
Тест: оверрайд всем + сброс + отклонение teacher-ключа. Backend 221 pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 14:27:58 +03:00
Maxim Dolgolyov 7d474b40c0 feat(permissions): A3 — история изменений прав (endpoint + UI)
GET /api/permissions/log (admin-only) — последние изменения ролевых прав (или
?user_id= для личных оверрайдов) из admin_audit_log; читаемый текст («включил
«X» для роли «учитель»») с резолвом меток через registry. Клиент LS.permissionsLog.
Вкладка «Доступ · роли»: блок «История изменений прав ролей» с кнопкой «Показать».
Тест: admin видит записи, не-админу 403. permissions 13/13.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 14:14:56 +03:00
Maxim Dolgolyov b702b04ed2 feat(access): Фаза 2c — история правил + пресет «копировать доступ из класса»
История: GET /api/access/log (admin-only) — кто/когда открыл/закрыл/сбросил
правило для контента (из admin_audit_log, имена классов/учеников резолвятся).
Клиент LS.accessLog; в режиме «По контенту» — кнопка «История изменений».
Пресет: в режиме «По классу» — «Скопировать доступ из класса [выбор]» (дополняет
текущие правила открытыми правилами класса-источника). Тест: история (admin
видит запись, учителю 403). content-access 13/13.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 13:55:02 +03:00
Maxim Dolgolyov 9b7585ac7b feat(access): Фаза 1c — видимость курсов по классам (Фаза 1 завершена)
Миграция 052: мост «открыть все опубликованные курсы всем существующим классам»
(тип 'course' уже в CHECK из 051). courseController.list/search фильтруют курсы
для НЕпривилегированных по allowedRefs(uid,'course') (content_ref = courses.id как
TEXT); admin/teacher — все. /api/access/catalog отдаёт курсы; CONTENT_TYPES в
админ-UI = textbook,exam,sim,course → курсы управляются во всех режимах «Доступ».
Тест course-access 4/4 (allowlist+класс+privileged+каталог). Полный набор: 213 pass.

ВАЖНО: новый опубликованный курс по умолчанию закрыт (allowlist) — открыть классам
в админке. Мост сохранил видимость текущих опубликованных курсов у существующих
классов. class_courses остаётся для назначений с дедлайном (сверх видимости).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 13:31:10 +03:00
Maxim Dolgolyov 4549b4e819 feat(access): Фаза 1b — управление доступом к симуляциям в админке
Бэкенд /api/access обобщён на тип 'sim': catalog отдаёт симуляции (lab_sims),
summary/matrix/class — карты по всем типам. Админ-секция «Доступ» теперь
показывает «Симуляции» во всех трёх режимах (по контенту / по классу / матрица)
+ поиск; helpers (bucket/keyName/itemsOf) обобщены через карты типов
(CONTENT_TYPES=textbook,exam,sim; course зарезервирован). Теперь админ/учитель
могут открывать/закрывать конкретные симуляции классам и ученикам — закрыт UX-
разрыв из 1a (новые классы без UI-управления). Тест: каталог включает sims; 210 pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 13:24:08 +03:00
Maxim Dolgolyov 9a145e5d62 feat(access): Фаза 1a — видимость симуляций по классам (добавочная модель)
Миграция 051: расширяет content_access.content_type на 'course'/'sim' (пересборка
таблицы — SQLite не умеет ALTER CHECK) + мост «открыть все включённые симуляции
всем существующим классам» → текущее поведение не меняется. GET /api/lab/sims
теперь фильтрует список для НЕпривилегированных по allowedRefs(uid,'sim'); admin/
teacher видят все. Ролевой simulations.access остаётся «модуль вкл.» (добавочно).
Тесты: lab-access (4/4, allowlist+класс+личное), lab-sims переведён на admin для
проверки полного каталога (видимость ученика — в lab-access). /api/lab в харнессе.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 13:19:29 +03:00
Maxim Dolgolyov 67a70c672d feat(access): Фаза 2a — режим «Матрица» класс × контент в админке
GET /api/access/matrix (классы + карта открытого контента одним запросом,
скоуп учителя). Клиент LS.accessMatrix. Третий режим вкладки «Доступ»:
таблица контент × классы с чекбоксами (правка в один клик) + поиск по
названию (обновляет только tbody — фокус ввода сохраняется), залипающие
заголовки. Тест /api/access смонтирован в харнесс; content-access.test 11/11
(+матрица: учитель видит свои классы и открытый контент, ученику 403).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 12:43:00 +03:00
Maxim Dolgolyov 3d627ce782 feat(flashcards): картинки на карточках (загрузка, вставка, рендер)
- Миграция 048: колонки front_image/back_image в flashcard_cards
- Бэкенд: POST /api/flashcards/upload (multer, 5МБ, только изображения),
  валидатор safeImg (только /uploads/flashcards/..., блок XSS/traversal/external),
  картинки в add/update/quick/study/random; статик-маунт /uploads/flashcards
- Редактор: превью+кнопка загрузки+вставка (Ctrl+V) на каждую сторону,
  картинки к ещё не созданной карточке через add-bar
- Режим изучения: рендер изображения над текстом на обеих сторонах
- FAB: вставка картинки в быструю карточку

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 12:58:24 +03:00
Maxim Dolgolyov fe122b7681 feat(admin): журнал событий безопасности (Tier 1-2) + аудит чувствительных действий (Tier 3)
- security_events (миграция 047) + utils/securityLog.js (defensive, lazy stmt)
- Tier 1: login.success/fail, register, password.change в authController
- Tier 2: 403 (роль/разрешение) в middleware/auth, rate_limited в rateLimit
- Tier 3: audit() на выдачу доступа (access), начисление/сброс XP (gam), модерацию аватаров
- API GET/DELETE /api/admin/security-log (фильтр по категории + поиск, прунинг по дням)
- Frontend: вкладка «Безопасность» в admin.html + loadSecurityLog, расширены ACTION_LABELS

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 15:28:21 +03:00
Maxim Dolgolyov 29301ff87d feat(flashcards): фаза 1 полировки — хоткеи, поиск, drag-reorder, честные интервалы
- study: хоткеи Space/стрелки=флип, 1-4/←→=оценка
- превью интервалов = точная копия серверного SM-2 (было враньё «<1 мин»)
- поиск/фильтр карточек внутри колоды
- drag-reorder карточек + endpoint PUT /decks/:id/reorder (requireOwnership)
- flashcard_decks добавлен в ALLOWED_TABLES requireOwnership
- эмодзи в empty-state → inline SVG .ic
- deleteCard: нативный confirm() → LS.confirm

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 09:53:03 +03:00
Maxim Dolgolyov 1dcc4cbf6e feat(flashcards): глобальный quick-add FAB + виджет «повтори карточку»
Backend:
- POST /api/flashcards/quick — добавить карточку из любой точки; колода по
  выбору или автоколода «Быстрые карточки» (создаётся при первом обращении)
- GET /api/flashcards/random — случайная карточка из всего пула пользователя

Frontend:
- /js/flashcard-fab.js — плавающая кнопка «запомнить» на всех страницах
  (учебник, лаборатория, симуляция…). Поповер: вопрос/ответ/колода, Ctrl+Enter.
  Гейт по фиче-флагу flashcards; исключены classroom/login/error/сама /flashcards.
  Загружается лениво из sidebar.js (на 45 страницах с шапкой).
- dashboard: виджет #w-flashcard в колонке прогресса — флип-карта (вопрос↔ответ),
  кнопка «Другая», счётчик пула, CTA при пустом пуле; слушает событие
  flashcard:added для авто-обновления.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 09:38:23 +03:00
Maxim Dolgolyov b67fac6407 feat(biochem): Фаза 2.1/2.2/2.4 — серверный chem.js + /analyze + подсказки валентности
- biochem-core.js dual-export (browser window.BIO + Node module.exports), без дублей
- BIO.valency: подробные подсказки валентности (2.4), общие для редактора и сервера
- services/chem.js: серверный анализ поверх того же ядра (analyze/validate)
- POST /api/biochem/analyze (2.2); /validate переведён на ядро (+фикс формата связей)
- api.js: LS.biochemAnalyze

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 22:37:59 +03:00
Maxim Dolgolyov 4a424505a8 feat(admin/health): System Health Level 2 — метрики HTTP-запросов
backend/src/utils/metrics.js: лёгкие in-memory метрики (сброс при рестарте) —
всего запросов, req/min (скользящее окно), латентность avg/p50/p95/p99,
разбивка по статусам 2xx/3xx/4xx/5xx, топ маршрутов по частоте/латентности/
ошибкам (группировка по шаблону route.path, не по URL).

server.js: middleware (на /api, по res 'finish') пишет латентность и статус.
adminController.getMetrics + GET /api/admin/metrics (под admin-auth).

admin.js: health-страница переведена на refreshHealth/renderHealth (Level 1)
+ секция «Метрики запросов»: карточки req/min/всего/avg/p95/p99/5xx, цветная
полоса статусов, топ медленных/частых/ошибочных маршрутов.

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

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

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