- registry.js: добавлен флаг requireConfirmOff для 7 критичных прав (questions.manage, classes.manage, library.upload, courses.manage, sessions.reset, theory.access, simulations.access); byRole() теперь возвращает это поле
- admin.html: subtitle в модале прав — «учителя» → «пользователя»; tooltip на кнопке «Сбросить всё по умолчанию»; поле поиска над сеткой прав; CSS .perm-modified-dot (amber, 8px)
- admin.js: badge «Инд.» → «Индивидуально» (font-size 11px); renderPermissions() рисует .perm-modified-dot когда значение отличается от registry default; togglePermission() показывает LS.confirm перед выключением критичных прав; window.filterPermissions() скрывает карточки и role-блоки по поисковому запросу
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Reset can downgrade effective access (override=1 vs role default=0),
so the user's JWT must be invalidated alongside the DELETE.
Wrapped in db.transaction for atomicity.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- backend/src/permissions/registry.js: single source of truth (PERMISSIONS map)
with all 24 keys (16 teacher + 8 student, student keys also cover free_student).
Exports isKnown(), listKeys(), byRole(), buildDefaultsMap().
- auth.js: PERM_DEFAULTS now sourced from registry.buildDefaultsMap();
new perm() helper validates key at registration time (crashes early on typos).
requirePermission() unchanged — backward compat preserved.
- permissionsController.js: ALL_PERMISSIONS now built from registry.byRole();
inline 24-entry array removed. API response shape unchanged.
- check-route-auth.js: validates every requirePermission/perm call key against
registry; lists unknown keys as errors before exit.
perm() added to GUARDS list so it counts as route protection.
Discrepancy noted: auth.js had free_student with same 8 keys as student;
permissionsController never seeded free_student rows. Registry documents
this via roles:[] array; buildDefaultsMap() correctly covers free_student.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds audit entries for:
- permission.set (role-level change)
- permission.user_set (per-user override)
- permission.user_reset (clear user override)
- feature.update (global feature flag toggle, per-key with old->new diff)
Old value captured for feature.update for full diff trail.
permissionsController: added audit import, wired audit() after each write.
adminController.updateFeatures: replaced bulk audit with per-key entries
capturing old value from app_settings before overwrite.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
setPermission / setUserPermission now bump token_version for affected
users so cached JWTs lose access immediately instead of after expiry.
Aligns with role-change pattern in adminController.updateRole.
Both writes wrapped in db.transaction() so token_version is only bumped
if the permission write itself succeeds.
Also cleaned up inline require('../db/db') calls to use top-level db.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Hardcoded inline <svg class="ic"> markers used as arrow replacements
(left over from emoji removal) were displayed as raw HTML text where
the consumer used textContent or canvas fillText:
- chemsandbox: csbar-v5 (Продукты cell) used textContent → SVG visible.
Switched to innerHTML for consistency with eq/ionNet cells.
Quiz question (qEl.textContent) and answer also receiving SVG —
cleaned via _csClean at source.
- reactions: modeTxt drawn via canvas fillText — replaced SVG with →.
- ionexchange: REACTIONS data + canvas labels — bulk SVG → Unicode arrows.
- newton: action button labels used textContent → switched to innerHTML;
canvas arrow labels: SVG → Unicode →/↓.
- collision: 'KE сохранена' canvas label — SVG checkmark → ✓.
- projectile: canvas badges + textContent wind label — SVG → Unicode ←/→/↩.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Security review caught: per-row hover actions (users.js) and async
user picker (shop.js, gam.js) interpolated user-controlled name into
JS string literals inside onclick. LS.esc() escapes & < > " but
NOT backslash; the .replace(/'/g, '\'') fallback was broken.
Attack: any authenticated user could set their name to
a\'); alert(1); //
via PATCH /api/auth/profile (stripTags doesn't strip \) — admin
viewing the users/shop/gam picker would execute arbitrary JS.
Fix: switch from JS-string interpolation to data-uid/data-name
attributes, read via dataset in handler. esc() correctly escapes
for HTML-attribute context; dataset returns the raw string with
zero parse re-entry.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Now that the deep pages (sub-commit 1) work, retire the legacy
.user-panel inline overlay entirely.
* admin.html: removed <div class="user-panel" id="user-panel"> block
inside #tab-users, removed dead .user-panel* CSS (kept .btn-close
for any external use).
* users.js: removed openUserPanel / closeUserPanel / reloadUserPanel
and their closure state (activeTr, activeUserRole). User row onclick
switched from openUserPanel(...) → AdminRouter.navigate('#users/N').
clearUserHistory / toggleBanUser / confirmDeleteUser / openEditUserModal
/ openUserPermsModal / doSet/doReset* all refactored to use the
getActiveUid() helper (reads window.activeUid, set by user-detail.init)
+ reloadDetailAndList() helper (refreshes deep page + list together).
* sessions.js: row click + eye-button switched from toggleDrawer(id)
→ gotoSession(id) → AdminRouter.navigate('#sessions/N'). Removed
toggleDrawer + renderDrawer functions (~60L) and openDrawerId state.
Inline drawer markup removed from the row template.
Verified node --check on all touched JS. ast-index confirms zero
remaining usages of openUserPanel / closeUserPanel / reloadUserPanel /
toggleDrawer across the repo.
This completes Phase 6 and the admin-redesign feature.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add user-detail.js (~370L) and session-detail.js (~180L) section
modules that render full pages for #users/:id and #sessions/:id, plus
admin.js dispatch and HTML tab-panes. The legacy .user-panel overlay
is intentionally still in place — sub-commit 2 will remove it once the
deep pages are verified.
* admin.js: DEEP_ROUTES map + activateDeepPane(); activate(route, params)
signature; initial dispatch respects hash params (so F5 on #users/123
goes straight to the deep page).
* admin.html: new tab-panes #tab-user-detail / #tab-session-detail and
two script tags. Old #user-panel overlay untouched.
* user-detail.js: header (avatar/role/email/meta) + sub-tabs
(Обзор/Сессии/Классы/Audit) with URL-synced sub-tab routing
(#users/N/sessions etc). Overview: 4 stat cards + per-subject SVG
bar chart. Sessions: clickable rows that navigate to #sessions/N.
Classes: placeholder empty-state (no per-user classes endpoint).
Audit: client-side filter of /admin/audit-log by uid match. Header
action buttons (Изменить/Права/История/Бан/Удалить) call existing
overlay handlers; window.activeUid is set before opening any modal.
* session-detail.js: full header (user/subject/score/stats) + per-
question correctness layout reusing the drawer renderer. Delete
button uses LS.adminDeleteSession then navigates to #sessions.
Clicking the user name opens the user deep page.
* users.js: quickOpenUserSessions now navigates to
#users/<uid>/sessions instead of the bare #sessions list.
Verified node --check on all new/modified JS. baseline npm test still
shows pre-existing 3 auth failures unrelated to this change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 2 review caught this: updateCharCounter was defined inside
questions.js IIFE but never exposed via window.X; admin.html:1672
calls it via oninput, would throw ReferenceError on every keypress.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace ~3500L admin.js monolith with thin orchestrator (~700L) +
14 IIFE-wrapped per-section modules under /js/admin/sections/.
Section modules expose AdminSections.<name>.init/reload (lazy init via
switchTab/router) and re-expose onclick handlers via window.X for
backward compat. Shared helpers (MODES/DIFFS, fmtDate, pctClass,
renderMath, qTypeBadge, pagination) live in /js/admin/_shared.js
exposed on window.AdminCtx.
switchTab now dispatches to AdminSections via ROUTE_TO_SECTION map;
non-extracted system tabs (topics/audit/errors/health/classroom/avatars)
remain inline in admin.js. user-panel overlay markup untouched — Phase 6
will remove it.
Полный wizard-refactor Q-modal был отложен как высокорискованный
(сложная форма с многими типами вопросов, риск регрессии). Вместо
этого — безопасные ergonomic-улучшения:
1) FORMULA BAR — collapsed by default
Раньше: 18 кнопок формул всегда занимали ~50px вертикали в модалке,
но нужны только при создании math-вопросов.
Теперь: маленькая кнопка «Вставить формулу» с chevron. Click → bar
разворачивается. Состояние сохраняется в пределах сессии (DOM-стейт).
2) PREVIEW — показывается только когда есть текст
Раньше: пустой preview-блок с placeholder «Введите текст вопроса…»
занимал ~80px независимо от состояния.
Теперь: .q-preview-wrap.hidden скрывается полностью пока textarea
пуста. Появляется по input с debounce 150ms (уже было).
Эффект: модал стал ~130px ниже в типичном кейсе (создание non-math
вопроса). На 1080p теперь умещается без скролла для single/multi
с 4 опциями.
Без wizard'а, без риска регрессии — но visible UX-win. Wizard-refactor
по-прежнему доступен как опция, если понадобится дальнейшее снижение
когнитивной нагрузки.
1) LOCK-ICONS на admin-only табах
Раньше: 7 табов (Магазин, Геймификация, Шаблоны, Симуляции, Игры,
Доступные тесты, Права доступа) скрывались от учителей через
display:none. Учитель не знал что они существуют — discoverability 0.
Теперь:
- Все табы видны всем, но для не-админа добавлен .locked класс
- .locked: opacity .42, cursor not-allowed, lock-icon справа
- title=\"Только для администраторов\" — нативный tooltip
- switchTab() при клике на .locked показывает toast вместо
переключения
Эффект: учитель видит границы своих прав; знает что есть в системе,
но не доступно ему — может попросить админа дать доступ.
2) LS.state — общий helper для loading/empty/error состояний
api.js:381 — добавлен LS.state с тремя методами:
LS.state.loading(el, msg?) — спиннер + опц. текст
LS.state.empty(el, msg, icon='inbox') — пустое состояние с иконкой
LS.state.error(el, err, retryFn?) — красная иконка + текст
+ опц. кнопка «Повторить»
Все три используют один CSS (.ls-state*) с одним визуальным языком.
inject стилей лениво (id=ls-state-style).
Демо-миграция: 3 error-handler'а в admin.js (Stats / Users /
Sessions) переписаны на LS.state.error с retry-функцией. Юзер
теперь может нажать «Повторить» вместо перезагрузки страницы.
Остальные 20+ inline error/empty/spinner'ов в admin.js — для
постепенной миграции (паттерн установлен).
3 победы из аудита админ-панели за один заход:
1) STICKY TABLE HEADERS
admin.html:142 — добавлен position:sticky; top:0; z-index:5; на <th>
Заголовки колонок теперь остаются видны при scroll длинных таблиц
(Users, Sessions, Shop, Gam — 100+ строк). Background фон поменян
на opaque #E5EAF7 чтобы строки скроллились чисто за header'ом.
Стоимость: 1 CSS-правило. Эффект: пользователи не теряют контекст
столбцов при просмотре длинного списка.
2) COLLAPSIBLE NAV GROUPS
admin.html:875+ — 4 группы (Аналитика, Контент, Пользователи,
Система) вместо плоского списка 21 кнопки с просто визуальными
сепараторами. Каждая группа сворачивается кликом по заголовку.
Состояние per-группа в localStorage (ls_adm_g_<slug>).
Группа «Система» (только админ) теперь объединяет shop, gam, sims,
games, audit, errors, health — раньше они шли вперемешку с
teacher-видимыми табами (sublog, topics, broadcast). Переместил
sublog/broadcast в группу «Пользователи», topics в «Контент» —
логичнее по смыслу.
Паттерн один-в-один как у sidebar.js (где мы это сделали ранее).
3) УНИФИКАЦИЯ ЛЕЙБЛОВ
Правило: «+ Добавить» для атомов (вопрос, тема, опция, товар),
«+ Создать» для составных объектов (тест, задание, курс).
Изменения:
- admin.html:1431 — «Создать» → «Добавить» (форма темы — атом)
- admin.html:1195 — «Новый товар» → «Добавить товар»
- admin.js:415 — q-modal title «Новый вопрос» → «Добавить вопрос»
- admin.js:2239 — shop-form-title «Новый товар» → «Добавить товар»
Теперь кнопка в toolbar и заголовок модалки/формы согласованы.
Остались крупные пункты из аудита (на отдельный заход):
- Q-modal wizard (split на 2 шага) — 🔴 высокий приоритет
- Pagination в больших таблицах — 🟡
- Standardized error/loading states — 🔵
Заголовки групп были rgba(255,255,255,0.45) — белые 45%, что под
тёмный sidebar. Но LearnSpace использует светлую тему (--bg=#EEF2FF),
из-за чего «УЧЕБНЫЙ ПРОЦЕСС» и пр. сливались с фоном.
Теперь:
color: var(--text-3, #56687A) с opacity .72
hover: var(--violet) с opacity 1
Visible contrast: было ~1.2:1 (невидимо), стало ~4:1 (WCAG AA для
небольшого uppercase-текста — годится).
dashboard.html: 2 → 0 ✅
- join-modal — вступить в класс
- qs-modal — быстрый тест с выбором предмета + режим + кол-во
theory.html: 1 → 0 ✅
- new-course-modal — создание нового курса учителем
course.html: 4 → 0 ✅
- add-section-modal — новый раздел курса
- edit-course-modal — редактирование курса
- add-lesson-modal — новый урок
- save-course-tpl-modal — сохранить курс как шаблон
Везде:
- Inline <div class=\"modal-overlay\">...</div> → удалён
- openX(): создаёт modal через LS.modal({content, actions})
- closeX() удалена — _xModal.close()
- Глобальный selectQsSubject() inline'нут как listener на body модалки
- Enter-handler на главных inputs сохранён
Не трогаю:
- biochem.html#lib-modal — кастомная тёмная тема, не подходит под
светлый LS.modal без редизайна
- library.html — 3 сложные модалки (folder-access, assign, upload)
с tabs и dynamic state — отдельный заход
- classes.html — modal-assign (128 строк, complex) + review-modal
- flashcards.html — fc-modal (не modal-overlay, своя CSS)
Прогресс миграции: 12 простых модалок → LS.modal за серию (4 ранее
+ 2 ранее + 6 сейчас). 4 страницы полностью очищены от
modal-overlay. Унифицированы:
- ESC/backdrop/focus поведение
- z-index (9000)
- Анимация (scale .22s)
- Адаптив на мобилке
admin.html: 5368 → 1922 строк (−64%, −3446 строк)
frontend/js/admin/admin.js: новый файл 3449 строк
Inline <script> блок (1915-5361) был полностью внутри HTML и не
кешировался отдельно — любое изменение HTML инвалидировало
огромный JS, и наоборот. Теперь:
- HTML загружается быстро (122 КБ vs 270 КБ)
- JS кешируется независимо (190 КБ; 7d max-age в prod)
- Любой ctrl+F по JS в редакторе теперь не требует пробираться
через тысячи строк HTML
Порядок выполнения сохранён байт-в-байт:
<script src="/js/api.js"></script>
<script src="/js/sidebar.js"></script>
<script src="/js/admin/admin.js"></script> ← было inline
... (далее остаётся как было)
<script src="/js/notifications.js"></script>
<script src="/js/search.js"></script>
<script src="/js/mobile.js"></script>
Никаких изменений в логике, scope, DOM-ready timing — чистая
эстетическая операция. Все 22 вкладки + все модалки и обработчики
продолжают работать ровно как раньше.
Это фундамент для дальнейшего сплита (если понадобится): можно
будет в /js/admin/ разнести по табам (sessions.js, classroom.js,
gamification.js и т.д.) с lazy-load по клику. Сейчас не сделано,
т.к. ROI на эстетику ниже, чем у других задач.
Та же миграция что и в exam9: убран inline-overlay HTML, дубликаты
CSS (.ex-overlay/.ex-panel/.ex-panel-* + .ax-error/.ax-success/
.ax-actions/.ax-btn) — всё это теперь .ls-mod-* из LS.modal.
Глобальные window.openAssignModal/closeAssignModal/onAssignOverlayClick/
onAssignEsc/setAssignTab/submitAssign и assignSlug/assignTitle/assignTab
переменные заменены на одну window.openAssignModal с локальным
closure по slug/title/currentTab.
Сохранены внутренние form-классы (.ax-form/.ax-classes/.ax-class/
.ax-tabs/.ax-tab/.ax-student-results/.ax-input/.ax-hint) — они
используются в body модалки.
Student search и tab-switching теперь обработчики на элементах
модалки (m.body.querySelector), а не глобальные document-listener'ы —
автоматически очищаются вместе с модалкой при close().
textbooks.html: 945 → 824 строки
Раньше: плоский список из 25 пунктов в один столбец, для учителя
видно ~20+, новички терялись.
Теперь: 4 группы со свёртывающимися заголовками + always-visible
top-блок и admin-нижний блок.
Группы:
• (top, без заголовка) Поиск · Дашборд · Путеводитель · Руководство
• «УЧЕБНЫЙ ПРОЦЕСС» (6): Классы, Мои ученики, Онлайн-урок,
Архив уроков, Live-квиз, Доска
• «КОНТЕНТ» (6): Учебники, Библиотека, Теория, Карта знаний,
Банк вопросов, Экзамен 9 класс
• «ПРАКТИКА И ИГРЫ» (7): Лаборатория, Биохимия, Красная книга,
Кроссворд, Виселица, Питомец, Коллекция
• «ОТЧЁТЫ И УПРАВЛЕНИЕ» (3, teacher+): Аналитика, Журнал, Управление
Технически:
- Helper G(slug, label, body) — создаёт группу с уникальным slug
- localStorage 'ls_sb_g_<slug>' хранит свёрнутое состояние
- Click на заголовок группы → toggle .collapsed
- Sidebar-collapsed (icon-only) режим: заголовки групп скрыты,
все пункты остаются видны (компактный режим работает как раньше)
- Стили инжектятся inline через id=sb-group-styles (защита от
повторной инжекции при HMR)
Совместимость: все ссылки и их id/class сохранены, нет правок в
других файлах. Существующие onClick'и через id (btn-board, btn-classes,
btn-admin, btn-join) работают.
Раньше: 3 отдельные страницы со своими сайдбарами, header'ами и
скриптами. /textbook-progress был доступен только через кнопку в углу,
/admin-textbooks — только по прямому URL.
Теперь: одна страница /textbooks с тремя вкладками:
• Каталог (все)
• Прогресс класса (учитель/админ)
• Управление (только админ)
URL hash routing: /textbooks#progress, /textbooks#manage. Lazy-init
для каждой вкладки (грузится при первом клике).
Старые страницы превращены в 312-байтные redirect-стабы для
сохранения старых ссылок и закладок:
/textbook-progress → /textbooks#progress
/admin-textbooks → /textbooks#manage
Effect:
- Один header, один сайдбар-load, одна загрузка api.js/sidebar.js
- HTML-страниц сокращено на ~530 строк (textbook-progress.html был
248 строк, admin-textbooks.html — 219; сейчас ~10 каждая)
- /textbooks.html: 467 → 945 строк (+478, поглотил функционал двух
страниц с собственными стилями)
- Чистый UX: всё про учебники в одном месте, переключение
мгновенное (нет полной перезагрузки страницы)
Все три endpoint'а POST для создания assignment теперь используют
общую логику валидации, резолва FK и INSERT'а:
POST /api/classes/:id/assignments → createAssignment
POST /api/assignments → createDirectAssignment
POST /api/assignments/bulk → bulkCreateAssignment
Три новых private helper'а:
_resolveAssignment(body) — валидирует и резолвит test_id/textbook_slug/
file_id → возвращает {ok: {...resolved}, error: '...'}
_insertAssignmentStmt — единственный prepared INSERT с полным
набором колонок включая textbook_id, textbook_paragraphs
_insertAssignment(target, fields, creatorId) — обёртка над INSERT,
target = {class_id} или {user_id}
_notifyAssignment(target, title) — pushNotif для class members или
одного user в зависимости от target
Каждая из трёх public-функций теперь:
- 25-40 строк (было 50-80)
- Уникальная логика только в:
• createAssignment — проверка владения классом
• createDirectAssignment — резолв ученика + class-membership
либо teacher_students проверка
• bulkCreateAssignment — цикл по class_ids в транзакции
Бонусы:
- createAssignment (через /classes/:id/assignments) теперь
поддерживает textbook_slug + textbook_paragraphs (раньше нет —
скрытый баг, проявлялся бы при попытке назначить чтение через
UI классов)
- max_attempts теперь применяется во всех трёх (был только в
createAssignment)
- Все три используют stmts.getClass вместо inline db.prepare()
API совместимость не нарушена: схемы тел запросов, return value,
коды статусов идентичны. Existing UI работает без изменений.
Файл: 734 → 744 строки (+10, но дубликат-логика выкинута).
Глава 4 — добавлен раздел 4.4 «Кому: класс / ученик / личный список»:
- Покрывает все 4 режима выпадашки «Кому» при создании задания
- Объясняет связь с «Моими учениками» (Глава 13)
Глава 11 — Учебники (новая):
11.1 Каталог: Шиманович химия 9, Исаченкова физика 9
11.2 Чтение и отметки «Прочитано», закладки выделением текста
11.3 Назначение чтения как ДЗ (классу / ученику, диапазоны §)
11.4 Учительский прогресс класса по учебнику
Глава 12 — Экзамен 9 класс (новая):
12.1 80 вариантов, тёмная сетка с прогрессом
12.2 Импорт нечётных в банк, назначение как ДЗ
+ объяснение «почему чётные — только для просмотра»
Глава 13 — Мои ученики (новая):
13.1 Когда нужно: репетиторство, доп-занятия, подготовка к ЦТ
13.2 Добавить по email (если ученика нет — ссылка на регистрацию)
13.3 Назначения — через /textbooks и в общем поиске
13.4 Удаление (задания не удаляются), счётчик заданий
TOC обновлён: CHAPTERS массив с 10 → 13 глав, счётчик «0 из 13».
Все навигационные кнопки prev/next перепрошиты под новую цепочку:
ch-10 → ch-11 → ch-12 → ch-13 → ch-1 (вернуться к началу).
Проверка: 13 chapters with matching id="ch-N", 40 sections с id=s-N-M,
все ссылки в CHAPTERS массиве соответствуют HTML-якорям.
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)
Новый отдельный модуль /exam9 в стиле LearnSpace:
- 80 вариантов × 10 заданий = 800 задач с разбором (KaTeX + SVG)
- Сайдбар: пункт «Экзамен 9 класс» (clipboard-check)
- Feature flag: feature_exam9_enabled (мигр. 002)
- Видим всем авторизованным; рендер на стороне клиента
- Прогресс в localStorage: подсветка вариантов (done/partial)
- Возобновление последнего варианта при возврате
Структура:
frontend/exam9.html — страница (LearnSpace layout)
frontend/js/exam9/app.js — рендерер
frontend/js/exam9/variants/ — 80 файлов с данными
frontend/img/exam9/ — 22 PNG/JPG фигур заданий
Картинки путей _tmp/ → /img/exam9/ переписаны автоматически.
Все маршруты проверены: 200 OK на /exam9, /js/exam9/*, /img/exam9/*.
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>