- Миграция 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>
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>
- 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>
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>
Зафиксирована накопленная незакоммиченная работа рабочего дерева, КРОМЕ файлов
учебника «Химия 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>
- lab.js: GET /api/lab/links/all?kind= — пакетный обратный поиск (byRef map),
чтобы каталог учебников не делал N+1 запросов
- tests/lab-links.test.js: +3 теста для /links/all (group/400/401) -> 21/21
- admin/sections/sims.js: inline-редактор курикулумных связей на карточке симуляции
(кнопка «Связи» -> панель: список связей с удалением + выбор учебника + добавить);
использует /api/access/catalog, POST/DELETE /links. Без LS.modal (inline-панель)
- textbooks.html: кнопка «В лабораторию» на карточке учебника, если есть связанные
симуляции (один батч-запрос /links/all при загрузке); deep-link /lab?sim=<id>
Двусторонняя навигация sim <-> учебник готова. Иконки .ic, без эмодзи.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
GET /related и /links возвращали 200 без токена: они были ПОСЛЕ blanket
router.use(requireRole('admin')) (хрупкий порядок при повторном mount роутера
в тестах). Убрал blanket; каждая мутация (patch/reorder/links POST+DELETE)
имеет INLINE requireRole('admin'); read-роуты — auth-only.
Также lab-links seed переведён на seedRow() (NOT NULL дрейф схемы).
lab-links 18/18, lab-sims 11/11, route-auth: 0 роутов lab.js во флаге.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Миграция 043_lab_sim_links.sql: таблица связей (sim_id, kind[textbook|topic|
kmap|question], ref_id, label), UNIQUE(sim_id,kind,ref_id) + индексы. Применена.
- lab.js (расширение):
- GET /api/lab/sims/:id/related (auth inline) — связи по типам; label из
textbooks/topics; href для навигации
- GET /api/lab/links?kind=&ref_id= (auth) — обратный поиск включённых
привязанных симуляций (для кнопки «Открыть в лаборатории»)
- POST /api/lab/sims/:id/links (admin), DELETE .../links/:linkId (admin)
- graceful-degradation если таблица ещё не отмигрирована
- tests/lab-links.test.js: 18 тестов (auth/роли/related/reverse/валидация/дубль/
enabled-фильтр/удаление); seedRow() устойчив к NOT NULL дрейфу схемы
- plans: Фаза 5 done + handoff
Все мои тесты: lab-sims 11/11, lab-links 18/18. route-auth: новый :id-роут
защищён inline authMiddleware. Миграция применена к живой БД.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1. cirSim ReferenceError в _pauseAllSims/closeSim (регрессия Фазы 3): глобалы
экземпляров симуляций объявлены в ленивых файлах -> не существуют до открытия.
Предсоздаём их как window-свойства (null) -> guard'ы безопасны. (lab-init.js)
2. theory-data.js (вынос THEORY параллельной сессией) не подключался в lab.html
-> панель теории и fallback loadTheory ломались. Добавил перед _register-all.
3. _pilots.js удалён в Фазе 1, но lab.html ссылался -> 404. Убрал ссылку.
4. /api/lab/sims 500 на неотмигрированном/устаревшем инстансе -> деградация:
возвращаем пустой каталог + needs_migration вместо 500. (routes/lab.js)
Проверка: vm-доказательство (_pauseAllSims без throw), node --check всех файлов,
lab-sims тесты 11/11. ВАЖНО: на работающем dev-сервере нужен ПЕРЕЗАПУСК (сервер
не авто-мигрирует) — таблица lab_sims уже в live БД.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
По итогам ревью системы прав:
- админка: переключатель режимов «По контенту» / «По классу»
- кнопки «Открыть всем классам» / «Закрыть у всех» (и зеркально по классу)
- бейджи N/M (сколько классов открыто) в списке контента
- эндпоинты /api/access/summary и /api/access/class/:id
- вкладка «Доступ к учебникам» перенесена к «Права доступа» (группа Пользователи)
- чистка content_access при удалении класса/ученика (нет FK)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A new cosmetic family: a fixed-position overlay painted behind every
page of the app, switchable from the profile shop. 4 free presets + 6
paid (250-1200 coins) so the new economy has another sink. Every
animation respects prefers-reduced-motion and falls back to its static
gradient.
Catalogue (migration 035):
free: none, gradient-soft, dots, dark
paid: gradient-flow, grid, bubbles, stars (mid)
aurora, nebula (premium)
Backend:
• migration 035 adds users.active_background + rebuilds shop_items
CHECK to include 'background' (standard SQLite 'new + copy + swap')
and seeds 10 items
• shopController.getMyActive returns { background: { slug } } and
activateItem handles type='background' (stores bare slug in
active_background) + skips the user_purchases check for price=0
so free presets work for everyone without per-user rows
• routes/shop validate schema lets 'background' through
Frontend:
• api.js applyCosmetics injects <div id='ls-bg-fx'> at body start
and toggles class to bg-<slug>. Cleared backgrounds remove the
element so dark→light transitions don't leave artifacts.
• ls.css gains a self-contained 'ANIMATED BACKGROUNDS' block:
keyframes per animated slug (ls-bg-flow, ls-bg-grid-scan,
ls-bg-bubble-rise, ls-bg-stars-twinkle, ls-bg-aurora-spin,
ls-bg-nebula-pan) wrapped in a prefers-reduced-motion kill-switch.
Same .bg-<slug> classes are reused for the .bg-preview swatches.
• profile.html shop:
- new 'Фоны' filter button between Рамки and Титулы
- _renderItemPreview type='background' draws a real 56-aspect swatch
(same CSS as the page bg — what you see is what you apply)
- _isItemActive matches by slug for background type
- free items (price===0) treated as auto-owned in render so users
can apply them without a fake 'purchase' step
Verified: getMyActive returns { background: { slug: 'nebula' } } after
flipping users.active_background; activate path updates the row.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Coins were always 1:10 of XP. Now they have their own event log + a
helper that dedups by reason within a configurable window.
Backend:
• migration 032 creates coin_log (user_id, amount, reason, created_at)
with indices for the 'fired today?' check
• awardCoins now records into coin_log on every call (reason defaults
to 'xp_bonus' for the legacy XP-proportional path)
• awardCoinsOnce(userId, amount, reason, window) — fires the bonus
only if no row matches in the window:
'day' → DATE(created_at) = today
'week' → ISO week match
'forever' → never twice
Wired events (Phase 4 subset of the plan):
• Daily login — 10 coins, once/day. Hooked in updateStreak so the
bonus rides on the existing 'daily_activity' XP trigger.
• Daily goal completion — 15/25/40 coins (easy/medium/hard), once/day.
Sits next to the existing tier XP bonus in updateDailyGoal.
• Variant clear — 30 coins, once per (user, variant) forever. Fires
from the exam-prep attempts endpoint when the user's final correct
answer fills out a math9 variant.
Deferred (need invasive trigger hooks): weekly goal, paragraph close,
boss defeated, referral.
Verified end-to-end: awardCoinsOnce returns true→false on repeated
calls, coin_log records the first, coins balance moves once.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Until now the 'gamification' feature flag did nothing: it had no row in
app_settings, the admin couldn't toggle it, awardXP/awardCoins ignored
it, and the CSS only hid three dashboard widgets — XP bars in textbooks
stayed visible regardless.
Phase 1 closes every hole.
Backend (source of truth):
• migration 029 seeds feature_gamification_enabled=1
• new isGamificationEnabled() helper in gamification/_shared.js with a
30s cache + invalidateGamificationCache() for instant admin toggles
• awardXP / awardCoins / updateStreak / unlockAchievement /
checkAchievements all bail out when the flag is off
• /api/gamification/* and /api/shop/* (user routes) return 404 when
disabled; admin routes remain open so the switch itself is reachable
• adminController.updateFeatures gains 'gamification' in the allow-list
and invalidates the cache on flip
Frontend:
• LS.isGamificationEnabled() (synchronous, populated by loadFeatures)
so xp.js + applyCosmetics can bail without a round-trip
• xp.js load/add/flush become no-ops when the flag is off
• applyCosmetics skips the round-trip when off
• CSS .no-gamification rule expanded to cover .hero-xp-badge, .po-xp,
.xp-card, .xp-bar, #frames-section, and a universal [data-gamified]
hook for future blocks
Textbooks (Variant 2 of the plan):
• backend/scripts/wrap_textbook_xp.py — idempotent script that adds
data-gamified to 167 XP tags across 63 textbook files (chapters +
hubs, all subjects/grades). Single CSS rule now hides everything.
Verified end-to-end: with the flag off, awardXP/awardCoins write nothing;
flipping back restores normal behavior.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Recent-attempts widget on /exam-prep/:examKey was showing raw LaTeX
like '\dfrac{7}{9}' because stripPreview only removed HTML tags.
Now it also converts common LaTeX to readable unicode (fractions →
a/b, \sqrt → √, \cdot → ·, comparisons → ≤≥≠, Greek letters, etc.)
before truncating.
KaTeX rendering would be overkill for a 100-char preview row; this
just makes the existing text legible.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Practice (random) now picks tasks by ascending difficulty so the first
slot is always level 1 and the session ramps up. Adds ?exclude= to drop
specific subtopics from the random pool, with a per-section checkbox
modal in the UI.
Each task carries a topic_ref (textbook chapter + paragraph) shown as
a 'Учить тему · §N' button next to the solution, deep-linking to the
right section of /textbook/<slug>. Mapping seeded for all 15 math9
subtopics in migration 028.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- backend/uploads/avatars/preset_01..27.png — иллюстрированные персонажи
- POST /api/avatar/preset — мгновенная установка без модерации
- GET /api/avatar/presets — список доступных пресетов
- profile.html: галерея пресетов в модалке аватара, доступна студенту/учителю/админу
- кастомная загрузка с модерацией остаётся только для студентов
Учитель может выбрать любой активный учебник из каталога /api/textbooks
и открыть его в общем iframe для всех участников. По аналогии с симуляциями:
- Backend: контроллер classroom/textbook.js + 4 роута
(POST/DELETE /:id/textbook, /:id/textbook/nav, /:id/textbook/mode)
с SSE-событиями classroom_textbook_open|close|nav|mode
- Embed-режим /textbook/:slug?embed=1: сервер injectит CSS+JS-bridge
перед </head>, скрывая хедер/сайдбар и пересылая клики/скролл наверх
через postMessage (без правки 40+ HTML-учебников)
- Frontend (classroom.html): кнопка «Учебник» в header, пикер с
фильтрами по предмету, iframe-панель с режимами демо/свободно,
relay nav-событий учителя → всем студентам в demo-режиме
Прогресс работает, отладочная обвязка больше не нужна:
- tracker.js: удалены все console.log/console.warn (boot, click,
POST, HTTP-ответ, patch-успех), удалены ensureDebugBadge и
updateDebugBadge (визуальный бейдж в правом нижнем углу),
recordParaVisit больше не вызывает updateDebugBadge
- 5 хуков (bubble, capture, setParaTab-patch, .tab[refN] sidebar,
polling .active) сохранены в production-виде — без логов, но
с теми же действиями
- backend/routes/textbooks.js: убран '[progress]' console.log из
POST /:slug/progress
Pre-commit hook теперь проходит без --no-verify.
- migration 014: parent_slug column + algebra-8 hub row +
rename old algebra-8 → algebra-8-ch1 (progress сохраняется
через стабильный textbook_id=3)
- backend/routes/textbooks.js: GET / фильтрует parent_slug IS NULL;
aggregated progress для хабов; новый GET /:slug/children
- algebra_8_hub.html: новая хаб-страница с 3 карточками глав,
hero с общим прогрессом, XP-бейдж, ссылки на главы
- algebra_8/ch2/ch3: кнопки cross-chapter заменены на
одну «К алгебре 8» в шапке
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- backend: POST /api/gamification/self-award (rate-limited, validated)
- frontend/js/xp.js: load/add/flush/on клиент, ~150 LOC, дебаунс 300мс,
keepalive fetch на unload/visibilitychange hidden
- algebra_8.html и algebra_8_ch2.html: XP_LEVELS заменён на единую
формулу с сервером; addXp/loadProgress подключены к window.LS.xp
- При первой загрузке: merge max(local, server); далее сервер — источник
правды
feat: teacher_students — назначения ученикам без класса
Новая модель «Мои ученики» — учитель связывает с собой учеников
независимо от классов (репетиторский сценарий).
Backend:
- Таблица teacher_students (teacher_id, student_id, added_at, note)
+ индекс на student_id для обратного поиска
- GET/POST/PATCH/DELETE /api/teacher-students — управление списком
- Добавление по email с проверкой роли student/free_student
- Уведомление ученику при добавлении
- createDirectAssignment: проверка inClass расширена до
inClass OR (teacher_id, student_id) в teacher_students
- listStudents (/api/classes/students): возвращает объединение
учеников из классов + из teacher_students. Это автоматически
обновляет student-picker в /textbooks без правок UI.
Frontend:
- /my-students — таблица личных учеников + форма добавления
по email + заметка + счётчик созданных заданий
- Сайдбар: пункт «Мои ученики» (user-plus, только для учителей)
Миграция 006_teacher_students.sql.
Что работает end-to-end:
- Добавить ученика на /my-students
- Открыть /textbooks → «Назначить» → «Ученику» → ученик ищется
в общем списке (классовые + личные)
- Создаётся запись в assignments с user_id, видна ученику на
дашборде с пометкой «Личное задание»
@
A1 — карточка ДЗ-чтения у ученика на /dashboard:
- Новая ветка в buildAssignCard для assignments с textbook_id
- Прогресс-бар «X из Y §», цвет берётся из textbook.color
- Кнопка «Открыть / Продолжить» с deep-link на первый требуемый параграф
- В classify(): textbook_all_read → done, deadline → overdue
A2 — авто-проверка выполнения:
- При POST /:slug/progress с mark_read: проверяются активные textbook-assignments
- Если все требуемые § прочитаны → INSERT в assignment_completion
- SSE-уведомление учителю «Ученик завершил чтение: <title>»
- myAssignments возвращает completed_at и textbook_all_read
A3 — учительский UI прогресса класса:
- Новая страница /textbook-progress (учитель/админ)
- Селекторы «учебник × класс» → таблица учеников с прогрессом
- Сортировка по количеству прочитанного, дата last_at
- Кнопка «Прогресс класса» добавлена в /textbooks (видна учителям)
B4 — admin-UI управления учебниками:
- /admin-textbooks (только admin) — таблица всех учебников
- Inline-редактирование title/author, тоггл is_active
- Колонка «Читателей» (count из textbook_progress)
- Endpoints: GET /api/textbooks/admin/all, PATCH /admin/:id
C7 — закладки/заметки внутри учебника:
- Таблица textbook_bookmarks (user, textbook, para, text, note, color)
- API: GET/POST/PATCH/DELETE для CRUD закладок
- В tracker: при выделении текста (8-400 симв) появляется плавающая «+ Закладка»
- Кнопка-иконка в overlay top-left открывает панель «Мои закладки»
- Хранится paragraph-якорь, цвет, заметка, кнопка удалить
Назначение ученику (в дополнение к классу):
- В модалке /textbooks — переключатель «Классу / Ученику»
- Поиск ученика по имени/email через /api/classes/students
- Submit использует POST /api/assignments (createDirectAssignment)
- createDirectAssignment расширен textbook_slug + textbook_paragraphs
- Учитель может назначать только ученикам своих классов
myAssignments расширен: возвращает textbook fields + post-process
считает textbook_required_count, textbook_read_count, textbook_all_read.
Deep-link поддержка: /textbook/<slug>#pN в tracker.js — на load и hashchange
вызывает setParaTab(pN) (нативная функция учебника).
Миграция 005: assignment_completion + textbook_bookmarks + индексы.
Фаза 1 — структура и каталог:
- frontend/textbooks/chemistry_9.html (Шиманович, 60 §) + physics_9.html (Исаченкова, 38 §)
- frontend/textbooks.html — каталог в стиле LearnSpace (карточки с обложками)
- Маршруты: /textbooks (каталог), /textbook/<slug> (полноэкранный учебник)
- Сайдбар: пункт «Учебники» (book-open-text)
- Feature flag feature_textbooks_enabled, hideDisabledFeatures map
Фаза 2 — прогресс в localStorage + UI чтения:
- frontend/js/textbook-tracker.js — инжектится в каждый учебник:
- «← Учебники» overlay-кнопка (top-left, semi-transparent)
- «Прочитано» чекбокс рядом с каждым §-заголовком
- Зелёный dot на pill уже прочитанных параграфов
- Авто-открытие последнего параграфа при возврате
- Каталог показывает прогресс-бар «X из Y прочитано» + кнопку «Продолжить»
Фаза 3 — серверный прогресс + назначение чтения как ДЗ:
- Таблица textbooks (slug, subject, grade, title, author, color, ...)
- Таблица textbook_progress (user_id, textbook_id, JSON read[], last_para)
- Колонки assignments.textbook_id + textbook_paragraphs
- API: GET /api/textbooks (с прогрессом), GET /:slug, POST /:slug/progress,
GET /:slug/class-progress (учитель)
- tracker.js синхронизирует прогресс через POST /progress (если залогинен)
- На каталоге у учителей кнопка «Назначить чтение» — модалка с выбором
классов + параграфы («1-5» или «1,3,5») + deadline
- bulkCreateAssignment расширен: принимает textbook_slug, резолвит в id
Миграция 004 идемпотентная; сиды двух учебников включены.
Импорт 40 нечётных вариантов (v01, v03, ..., v79) в банк вопросов:
- 400 questions с allow_html=1, source_type='экзамен 9', year=2025
- 540 options (single-choice) + correct_text (short_answer)
- 40 tests (по 1 на вариант), title="Экзамен 9 — Вариант N"
- exam9_variant_tests маппинг для назначения
Назначение варианта как ДЗ на /exam9 (для учителей/админов):
- Кнопка «Назначить как ДЗ» под заголовком варианта (только если test_id есть)
- Модалка выбора классов + опциональный deadline
- POST /api/assignments/bulk с test_id из exam9_variant_tests
Поддержка HTML/SVG в вопросах банка через флаг questions.allow_html:
- Миграция 003: ALTER TABLE questions ADD COLUMN allow_html
- sessionController: SELECT возвращают allow_html и image
- test-run.html: рендер q.text и opt.text как HTML при allow_html=1
- test-result.html: то же для explanation и opt.text
- KaTeX: добавлены $...$ и $$...$$ delimiters в обеих страницах
Бонус-фикс: bulkSchema требовал class_id (single), контроллер ждёт
class_ids (array). Существующий вызов из classes.html был сломан;
исправлено вместе.
Команда: node backend/scripts/import-exam9.js (--all для всех 80)
## P0
- admin.html:2608, red-book-ecosystem.html:489-495 — XSS: u.name/node.name_ru/description обернуты в LS.esc()
- classController.js getAnnouncements — добавлена проверка teacher_id (B14: учитель A не может читать объявления класса B)
## P1 — auth & validation
- authController.js — минимум пароля 6→8 символов (register + change password + login.html)
- gamificationController adminAward — валидация max XP/coins (1M), Number coercion
- shopController adminAwardCoins — валидация max + проверка changes>0
## P1 — race conditions
- petController.buyBg — atomic UPDATE WHERE coins>=? (race-safe)
- shopController.purchaseItem — atomic conditional UPDATE
- liveController — добавлен question_id в live_answers (миграция с пересозданием таблицы), история ответов сохраняется при смене вопроса учителем
- ws-server: invalidateDrawCache экспортирован, classroomController grant/revoke вызывают его → permission revoke применяется мгновенно (раньше до 10s stale)
## P1 — rate limits & retry
- rateLimit middleware: новый параметр byUser=true (использует req.user.id вместо IP — не блокирует пользователей за NAT)
- routes/classroom.js: reactionLimiter (15/5s) на /chat/:msgId/react, handLimiter (5/5s) на raise/lower hand
- api.js sendAnswer — retry 3x с exp backoff (300/1200/2700ms), не повторяет на 4xx (F5)
## P1 — performance
- classroomController.getStrokes — LIMIT 5000 + флаг hasMore (защита от OOM на 10K+ strokes)
- whiteboard.js _liveStrokes — TTL 1.5s на каждый live preview (auto-cleanup при крашe ремоут юзера)
## Infrastructure
- config.js: TURN_URL/USER/PASS env vars
- server.js: GET /api/ice-servers возвращает STUN + опциональный TURN из env
- classroom-rtc.js: фетчит /api/ice-servers вместо хардкода (поддержка TURN для NAT/CGNAT школьных сетей)
- .env.example: документация TURN
- db.js: PRAGMA synchronous=NORMAL (5x быстрее с WAL), cache_size 16MB, temp_store=MEMORY
- ws-server.js closeAll() + server.js shutdown — graceful WS shutdown при SIGTERM
## False positives (не баги, агенты ошиблись)
- assignmentController FK на tests — на самом деле users (migrate.js:317-318)
- .env в git — gitignore корректно исключает
- admin.html без requireAuth — есть LS.initPage() который вызывает requireAuth
- submissionsController IDOR — обе ручки уже проверяют teacher_id
- screenSender = null inside try/catch — на самом деле снаружи
- SSE без backoff — есть exponential 2s→30s
- sessionController NOT IN на пустом массиве — есть guard usedIds.length>0
- getChat без LIMIT — есть LIMIT 100/200
- trust proxy — установлен на server.js:105
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Онлайн-урок:
- Кнопка «Рисовать» в баре симуляции (только учителю)
- При активации: холст доски показывается поверх iframe (z-index), фон прозрачный
- Учитель рисует прямо поверх симуляции обычными инструментами
- Студенты видят то же самое через SSE (classroom_sim_annotate)
- Выход из режима → кнопка «Вернуться к симуляции»
Планиметрия (bugfix):
- arcmark теперь рисуется всегда (не зависит от showAngles)
- altitude/median: 1 клик на вершину треугольника (авто-находит противоположную сторону)
- centroid/orthocenter: 1 клик внутри/на треугольник
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Classroom performance:
- WebSocket server (ws-server.js) for low-latency cursor & stroke preview
Replaces HTTP POST per event → eliminates per-message auth overhead
Session member cache (30s TTL) avoids SQLite query per WS message
Fallback to HTTP POST when WS not connected
- Cursor throttle reduced 100ms → 33ms (~30fps)
- Stroke preview throttle reduced 50ms → 20ms
- whiteboard.js: render() is now rAF-gated (_doRender/_rafPending)
Multiple render() calls within one frame collapse into one repaint
document.hidden check — zero CPU when tab is in background
visibilitychange listener restores canvas on tab focus
Guest board:
- guestClassroom.js route: public token-based read-only access
- guest-board.html: name entry + read-only whiteboard + SSE
- SSE: addGuestClient/removeGuestClient/emitToGuests
Screen share picker:
- Discord-style modal with tab switching (screen/window/tab)
- Live video preview before confirming share
- useExistingScreenStream() in ClassroomRTC
Fullscreen exit overlay:
- #cr-fs-exit-overlay button inside cr-board-wrap
- Visible only via CSS :fullscreen selector (touchpad users)
File sharing from library:
- Teacher picks file from library, sends as styled card in chat
- crDownloadLibraryFile() fetches with Bearer auth
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>