lab.html: 9273 → 5180 lines (-44%)
frontend/js/labs/lab-init.js: new file, 4098 lines
All 34 _open*() functions + sim instance vars + openSim/closeSim/
_addTouchSupport/_simShow + THEORY data extracted to lab-init.js.
Shared globals (FN_COLORS, ALL_SIM_BODIES, ALL_CTRL_BARS, sim vars)
converted from const/let to var for cross-script accessibility.
Three vars in lab.html made global: _disabledSimIds, _theoryOpen, _embedMode.
Load order preserved: lab-init.js added before main <script> block.
- migrate.js → legacy-migrate.js (kept for rollback, delete 2026-07-01)
- tests/setup.js now uses migrations-runner.run() on fresh temp DB
- npm run migrate → versioned runner (was legacy init-every-start)
- npm run migrate:legacy → legacy-migrate.js (emergency rollback only)
After `npm run migrate:bootstrap` on prod:
npm run migrate → "Nothing to apply — schema is up to date"
All 32 previously-passing tests continue to pass.
Pre-existing 3 auth.test.js failures (rate-limiter shared state) unchanged.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds backend/src/db/migrations-runner.js:
- Tracks applied migrations in _migrations table
- Applies .sql files from src/db/migrations/ in alphabetical order
- Each file runs in a transaction — fail-fast, no partial state
- `migrate:bootstrap` marks 000_baseline.sql as applied on existing DBs
000_baseline.sql — full schema snapshot from prod DB (168 objects, 2026-05-06).
Removed stale PostgreSQL migration files (001_init.sql, 002_constraints.sql)
that used SERIAL/EXTENSION syntax incompatible with SQLite.
npm scripts:
migrate → migrations-runner.js (versioned)
migrate:bootstrap → mark baseline applied (run once per env)
migrate:legacy → legacy migrate.js (kept for reference)
On prod DB after `migrate:bootstrap`: "Nothing to apply — schema is up to date".
Legacy migrate.js still in place; tests still use it via setup.js (unchanged).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
content/phys/ct-2024.yaml — 15 questions from ЦЭ,ЦТ 2024 across
6 topics (kinem, mol, emf, electro, magnet, optics) as proof of format.
backend/scripts/import-content.js — unified importer:
- Validates schema (subject, year, options, exactly-1-correct)
- Aliases (kinem, mol, ...) resolve to Russian topic names via get-or-create
- Deduplicates by first 80 chars of text (matches legacy seed_*.js behavior)
- Runs in a single transaction, idempotent re-runs
On fresh DB: 13 added (2 dedup collisions — same 80-char prefix, expected).
On prod DB: 0 added (all already exist from legacy seeds).
Second run on either: 0 added (dedup works).
Legacy seed_phys_ct2024.js kept as backup — see content/README.md
for migration guide.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Maps 70 functions from 1618-line classroomController.js into 8 files
by domain: sessions, strokes, pages, chat, permissions, sim, admin,
plus _shared helpers. Facade preserves public API unchanged.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Focused suite covering IDOR and privilege-escalation patterns:
1. Student cannot delete another teacher's class (role block)
2. Teacher cannot delete another teacher's class (ownership check)
3. Banned user blocked on all protected routes
4. Token revoked after token_version bump (password change flow)
5. Join class with wrong invite code → 404, no membership created
6. Admin-only route blocks teacher role
7. Protected route without token → 401
All 7 pass. Pre-existing auth.test.js failures (rate-limiter shared
state in test server) are unrelated and were present before this PR.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Scans all routes/*.js for :id-bearing routes without an auth-guard
(requireOwnership, requireRole, requirePermission, authMiddleware,
parentAuth, or spread middleware arrays like ...auth/...teacher).
BASELINE=56 — any new unprotected :id route causes exit(1).
Reduce BASELINE as old routes are migrated.
Usage:
npm run lint:routes
# or mark intentional public routes:
// @public-by-design: <reason>
router.get('/:token', handler);
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Tokens in URL leak through proxy access logs, browser history and
Referer headers. Now: WS opens unauthenticated, client sends
{type:'auth', token} as first message; server responds with
{type:'auth_ok'} and starts normal message processing.
5-second timeout closes any unauthenticated connection.
Frontend queues session join until auth_ok received.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Auto-running migrations on every server boot is dangerous — a broken
migration silently corrupts data or blocks server start. Now require
explicit `npm run migrate && npm run seed:permissions` before start.
Boot asserts schema exists (users + role_permissions tables) and
fails fast with a clear message otherwise.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Исправлены ключи сортировки: oldest→date_asc, easy→diff_asc, hard→diff_desc
- loadTopics() теперь использует /api/subjects/:slug/topics вместо
запроса 200 вопросов (не хватало для 400+ вопросов)
- Добавлен onchange для select#f-topic (без него выбор темы не срабатывал)
## 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>
Безопасность:
- tests/🆔 скрыть is_correct и explanation для студентов (P0)
- SQL injection: limit/offset через placeholder вместо template literal
- Stored XSS: stripTags для lesson comments, flashcards, redBook sightings
- profile.html: escape e.message в showMsg (XSS через server error)
- attachment_url: валидация только /uploads/* путей
- requestId: генерировать UUID сервером, не доверять клиенту
- register: скрыть token_version из ответа
Надёжность:
- register: обработка UNIQUE constraint race condition
- pet buyBg: re-check баланса внутри транзакции
- DB errors: скрыть e.message в testController/questionController/courseController
- preferences: лимит 50KB на размер JSON
UX:
- board.html: debounce 250ms на search input
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- draw_permitted: emit→emitToUser (WS доставка вместо SSE-only)
- raised hands: убран in-memory Map, единый источник — таблица classroom_hands
- endSession: очистка classroom_hands при завершении сессии
- VALID_THEMES: исправлен список (добавлен corkboard, убраны dark/grid/dots)
- XSS: crLoadOnlineStudents — inline onclick заменён на data-* + addEventListener
- signal(): проверка что target_user_id является участником сессии
- WS rate-limit: 120 msg/sec per connection
- invalidateSession при join/leave для мгновенной видимости новых участников
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
signal() в classroomController использовал emit() из sse.js напрямую —
сигналы не доходили до пользователей на WebSocket-соединении (учитель).
Исправлено: используем emitToUser() из ws-server.js, который роутит в WS
и падает на SSE только как fallback.
- New 'Настройки' tab in profile.html between Закладки and Безопасность
- Sound section: global enable toggle, volume slider, per-category toggles (ui/classroom/gamification/quiz)
- Test buttons to preview each sound category
- Appearance section: animation toggle, push notifications toggle
- All sound prefs persist via LS.sfx localStorage (ls_sfx key)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Добавлены фигуры: усечённая пирамида, октаэдр, икосаэдр, додекаэдр
- Формулы V, S, r_вп, R_оп для всех новых фигур
- Инструмент '∠ скрещ. прям.' — угол и расстояние между скрещивающимися прямыми (4 клика)
- Для икосаэдра/додекаэдра — THREE.IcosahedronGeometry/DodecahedronGeometry с извлечением рёбер
- Вписанная/описанная сфера поддерживает октаэдр, икосаэдр, додекаэдр
- Параметр n добавлен для пирамиды
Co-Authored-By: Claude Sonnet 4.6 <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>
- arcmark: рисуется всегда (не только при showAngles=true)
- altitude/median: 1 клик на вершину треугольника (авто-определение)
- centroid/orthocenter: 1 клик внутри/на треугольник
- thales: 3 клика O, A, B → A'B' параллельно AB, коэф. k
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Фаза 7:
- altitude: высота (клик на сторону → клик вершина → foot + отрезок + прямой угол)
- median: медиана (3 клика A,B,C → midpoint + отрезок)
- centroid: 3 клика → 3 медианы + точка G (centroid constr)
- orthocenter: 3 клика → 3 высоты + точка H (orthocenter constr + altitude_foot constr)
- gOrthocenter() math function
- Прямые углы для altitude_foot в _drawAngleMeasures
- Исправлен баг onHintChange: передавался boolean вместо numeric phase
Фаза 8:
- tick tool: метки равных сторон на отрезках и сторонах полигонов (1–3 штриха)
- arcmark tool: метки равных углов на вершинах полигонов (1–3 дуги)
- _drawTickMark(), sideMarks[], angleMarks[] на полигонах
- Новая секция «Метки» в панели инструментов
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
6.1 Стороны полигонов теперь выбираются как опорные линии (_hitTestLine),
через виртуальные сегменты (virtual:true, polyId). Cascade-удаление
исправлено на BFS (transitive deps). Теперь можно строить высоты треугольников.
6.2 Прямой угол (квадратный маркер) рисуется для всех foot-конструкций
в _drawAngleMeasures, независимо от полигона.
6.3 Удаление отдельных объектов: onDeleteRequest callback, диалог
«Только этот» (derived-точки → свободные) / «Со всеми зависимыми»
(cascade) / «Отмена». CSS-панель .geo-del-confirm поверх canvas.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- gTangentPoints(O, P, r): касательные через полярно-полярную точку M=O+v*r²/d, h=r√(d²-r²)/d
- tangent: 2 derived_line (which=0/1) из внешней точки к окружности; оба пересчитываются
при движении точки или изменении радиуса/центра; _pendingCircRef хранит окружность-источник
- translate: derived point P'=P+(B-A) по вектору AB; 3-фазный ввод с onHintChange(tool,2/3)
- _hitTestCircle(): найти окружность под курсором (HIT=12px)
- _drawLineRefHighlight(): расширен для circle (рисует дугу подсветки)
- _pendingCircRef очищается в setTool()
- lab.html: кнопки Симметрия/Перенос/Касательные, _GEO_PHASE_HINTS словарь,
_geoShowHint(name, phase) принимает числовой phase вместо boolean
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- GeoEngine: _dependsOn/recompute для constr='reflect' и 'ngon_vertex'
- reflect: производная точка-отражение (P'=2·foot-P), зависит от axis+srcPt
- ngon: правильный n-угольник по центру и вершине; вершины v1..vn-1 = derived
points (constr='ngon_vertex', хранят srcCenter/srcVertex/k/n); при движении
центра/вершины все вершины автоматически пересчитываются
- GeoSim: _ngonSides=6, setNgonSides(n), инструменты 'reflect'/'ngon' в _handleToolClick
- lab.html: кнопки Симметрия и n-угольник, +/- контроллер сторон, хинты Phase 2
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>