Commit Graph

71 Commits

Author SHA1 Message Date
Maxim Dolgolyov 3f89030b6e feat(admin): Phase 6 sub-commit 2 — remove .user-panel overlay
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>
2026-05-17 00:08:13 +03:00
Maxim Dolgolyov bd3020067b feat(admin): Phase 6 sub-commit 1 — add deep-page sections (overlay still works)
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>
2026-05-17 00:01:22 +03:00
Maxim Dolgolyov 69113ab35e feat(admin): phase 5 — per-row quick actions for users + sessions
Hover-only action buttons (right-aligned, opacity transition, hidden on mobile).

- users.js: 4 actions (ban/unban, award coins, sessions, delete) — replaces `>` glyph cell, falls back to glyph for non-admin / self

- sessions.js: 2 actions (view, delete)

- DELETE /api/admin/sessions/:id (NEW): transactional (assignment_sessions=NULL, user_answers, session_questions, test_sessions), audit-logged, admin-only

- event.stopPropagation defence-in-depth (each button + parent .row-actions)

- LS.confirm for destructive ops; LS.modal for award-coins amount/reason

- CSS injected once via #row-actions-style id-dedup (same content in both sections)

Existing user-panel overlay + session toggle-drawer flows untouched (Phase 6 removes overlay).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 23:53:19 +03:00
Maxim Dolgolyov f562fe4a71 feat(admin): phase 4 — Cmd+K command palette
Global search modal: actions + users + tests + classes.

- GET /api/admin/search?q=X (~50L controller): 3 parameterized LIKE queries, admin-only

- frontend/js/admin/palette.js (~366L): custom lightweight modal (not LS.modal — footer-button oriented), Ctrl+K/Cmd+K capture-phase override of generic /js/search.js, debounce 150ms, race-guard via _reqSeq, min-query 2 chars, 8 hardcoded actions, ↑↓ wrap + Enter, click-outside close

- adminGlobalSearch helper: drop ignored 'limit' param (server hardcodes 5/3/3)

window.AdminPalette = { open, close, isOpen } exposed for Phase 5/6 use.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 23:39:59 +03:00
Maxim Dolgolyov 41acbdd0d0 feat(admin): phase 3 — dashboard #overview landing
GET /api/admin/overview returns 24h digest (~0.08ms/call).

- adminController.getOverview: 7 prepared statements (users 24h, sessions 24h, active users, classes count, failed sessions, banned this week, top-5 sessions)

- new section frontend/js/admin/sections/overview.js (~205L): bento-grid cards, alerts (only when >0), top-5 table, quick-links

- nav-item + tab-pane reordered: #overview is now default; #stats remains routable

Auth: admin-only (inside requireRole('admin') block, sibling of /stats).

Backward compat: all 13 existing routes unchanged.

Known follow-ups (post-merge polish):

- activeClasses counts all (label could be 'Всего классов')

- failedSessions24h includes in_progress (could tighten to abandoned only)

- topSessions24h drops NULL-score completed rows

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 23:26:59 +03:00
Maxim Dolgolyov fa67ad1294 fix(admin): expose updateCharCounter for Q-modal oninput handler
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>
2026-05-16 22:58:59 +03:00
Maxim Dolgolyov 92030b462c feat(admin): phase 2 — split admin.js into 13 section modules
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.
2026-05-16 22:50:14 +03:00
Maxim Dolgolyov 8a7bed487f feat(admin): phase 1 — hash-router
AdminRouter wraps existing switchTab for deep-linking.

- frontend/js/admin/router.js (new, 102L): parse/navigate/current/on/off, recursion guard via _navigating flag

- admin.html: +1 <script> before admin.js

- admin.js: switchTab(btn, opts) + initAdminRouter IIFE for hashchange dispatch

Backward compat: all 21 onclick=switchTab(this) callsites continue working.

F5 / back / forward / deep-link verified.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 22:22:20 +03:00
Maxim Dolgolyov bd7a9dbee2 feat(admin): pagination для users-таблицы (50/стр)
- adminGetUsers возвращает { users, total, page, limit }

- pagination-controls (← 1 … N →) с ellipsis для длинных списков

- shop/gam search callers адаптированы под новый формат ответа

- helper _renderPgnControls переиспользуем для sessions/shop

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 20:24:18 +03:00
Maxim Dolgolyov bcee5a57e3 ux(admin): Q-modal ergonomic improvements — формулы скрыты, preview по требованию
Полный 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
по-прежнему доступен как опция, если понадобится дальнейшее снижение
когнитивной нагрузки.
2026-05-16 20:04:08 +03:00
Maxim Dolgolyov 6b7d0355b6 ux(admin): lock-icons на admin-only табах + LS.state helpers
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 — для
   постепенной миграции (паттерн установлен).
2026-05-16 19:56:58 +03:00
Maxim Dolgolyov ffd7bac0ac ux(admin): sticky table headers + collapsible nav + унификация лейблов
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 — 🔵
2026-05-16 19:48:31 +03:00
Maxim Dolgolyov 5c66105fc2 refactor: ещё 6 модалок → LS.modal (dashboard, theory, course)
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)
  - Адаптив на мобилке
2026-05-16 19:33:39 +03:00
Maxim Dolgolyov d3b16f55c8 refactor: 4 модалки → LS.modal (classes ×2, library ×2)
classes.html (modal-overlay: 5 → 3):
  - modal-class — создание класса
  - modal-edit-assign — редактирование задания

library.html (modal-overlay: 5 → 3):
  - folder-modal — создание/переименование папки
  - move-modal — перемещение файла в папку

Везде один паттерн:
  1. Удалить inline <div class="modal-overlay">...</div> разметку
  2. Заменить openX/closeX функции на LS.modal({content, actions})
  3. Сохранить state в локальной переменной _xModal вместо
     document.getElementById('modal-id').classList.add('open')
  4. setError() / close() через ссылку на modal-instance
  5. Удалить орфанные closeX функции

Чистый эффект: −154 строки HTML/CSS дубликатов, единое поведение
ESC/backdrop/focus, accessibility (role/aria-modal) автоматически.

Осталось:
  classes.html — modal-assign (128 строк, complex tabs), review-modal
  library.html — folder-access-modal, assign-modal, upload-modal (все
    более сложные с tabs и multi-step)
  frontend/red-book.html (17 modal-overlay — отдельный заход)
  flashcards (5), course (4), dashboard (2), и другие
2026-05-16 19:17:49 +03:00
Maxim Dolgolyov f7b6785050 refactor: admin.html — извлечён inline <script> в /js/admin/admin.js
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 на эстетику ниже, чем у других задач.
2026-05-16 18:59:38 +03:00
Maxim Dolgolyov 91696ba089 refactor: textbooks assign modal → LS.modal (−120 строк)
Та же миграция что и в 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 строки
2026-05-16 18:51:58 +03:00
Maxim Dolgolyov bc22715734 feat: LS.modal — общий компонент модалок + миграция /exam9 + /my-students
Новый общий компонент LS.modal (api.js) — companion к LS.confirm.
Универсальная form/content-модалка с консистентным поведением:

  LS.modal({
    title, content, size: 'sm'|'md'|'lg',
    actions: [{label, primary, danger, onClick}],
    onClose,
  });
  // Returns { close, root, body, setBody, setActions, setError }

Стандартное поведение:
  - ESC и backdrop-click закрывают (опциональный dismissible:false)
  - z-index 9000 (тот же что LS.confirm — без конфликтов)
  - Auto-focus первого input/select/textarea/button в body
  - prevFocus restore при закрытии
  - Анимация scale+translateY .22s
  - Адаптив: на мобилках padding уменьшается

CSS-классы .ls-mov / .ls-mod / .ls-mod-hdr / .ls-mod-body / .ls-mod-act
впрыскиваются один раз из api.js (id=ls-modal-style), как и стили
toast/confirm.

Миграция exam9 «Назначить вариант»:
  - Убран inline <div class="ex-overlay" id="assign-overlay">…</div>
  - Убраны .ax-actions, .ax-btn, .ax-btn-primary, .ax-error, .ax-success
    CSS (теперь в общих .ls-mod-* стилях)
  - openAssignModal → LS.modal({ title, content: form, actions: [...] })
  - Удалены closeAssignModal/onAssignOverlayClick/onAssignEsc — теперь
    handle'ит LS.modal
  - Удалена unused переменная assignVariantNum (closure теперь над varNum)

  exam9.html:  −53 строк (CSS + HTML модалки)
  app.js:      переписан 90 строк → 70 строк

Миграция my-students «Убрать ученика»:
  - native confirm() → LS.confirm() с danger-стилизацией
  - alert() → LS.toast() для согласованности

Сохранён классroom-овский «ex-overlay»/«ex-panel» CSS (используется
для picker'а вариантов в exam9). Не трогаем classroom.html — у него
своя ecosystem cr-*-overlay.

Дальше — postupенная миграция модалок в textbooks/classes/admin
по мере касания этих страниц. Шаблон установлен.
2026-05-16 18:41:27 +03:00
Maxim Dolgolyov df29675cc7 ux: /textbook-progress и /admin-textbooks → вкладки в /textbooks
Раньше: 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: всё про учебники в одном месте, переключение
    мгновенное (нет полной перезагрузки страницы)
2026-05-16 17:39:13 +03:00
Maxim Dolgolyov d93664946e docs(teacher-guide): дополнено руководство — 3 новые главы + раздел 4.4
Глава 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-якорям.
2026-05-16 17:06:24 +03:00
Maxim Dolgolyov eeb79246db @
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, видна ученику на
    дашборде с пометкой «Личное задание»
@
2026-05-16 17:01:11 +03:00
Maxim Dolgolyov 3ff2f01178 feat: textbooks Phase 4 — A1+A2+A3+B4+C7 + назначение ученику
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 + индексы.
2026-05-16 16:37:11 +03:00
Maxim Dolgolyov e8018d85c1 feat: textbooks — модуль учебников + чтение как ДЗ (3 фазы)
Фаза 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 идемпотентная; сиды двух учебников включены.
2026-05-16 14:05:19 +03:00
Maxim Dolgolyov 31a51956b6 feat: exam9 — назначение варианта как ДЗ + импорт нечётных в банк
Импорт 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)
2026-05-16 13:13:06 +03:00
Maxim Dolgolyov 6cff327e88 feat: exam9 — Экзамен 9 класс по математике (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/*.
2026-05-16 12:53:49 +03:00
Maxim Dolgolyov ae31e4c4e8 refactor: distribute lab-init.js into 34 engine files
lab-init.js: 4098 -> 543 lines (infrastructure + THEORY only)

Each sim's _open*() + UI helpers moved to its engine file:
graph.js, projectile.js, collision.js, magnetic.js, triangle.js,
geometry.js, trigcircle.js, gas.js (molphys), coulomb.js, circuit.js,
reactions.js (chemistry), newton.js (dynamics), chemsandbox.js,
celldivision.js, photosynthesis.js, angrybirds.js, quadratic.js,
normaldist.js, graphtransform.js, pendulum.js, equilibrium.js,
thinlens.js, mirror.js, isoprocess.js, titration.js, refraction.js,
probability.js, bohratom.js, electrolysis.js, waves.js,
crystal.js, orbitals.js, stereo.js, hydrostatics.js

All 34 engine files syntax-checked OK.
2026-05-08 14:54:54 +03:00
Maxim Dolgolyov d5f77bb648 refactor: extract lab.html sim initializers to lab-init.js
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.
2026-05-08 13:41:38 +03:00
Maxim Dolgolyov 6b2ec38d9a security: WS auth via first-message, not query string
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>
2026-05-06 16:58:25 +03:00
Maxim Dolgolyov d9e9e65328 fix: банк вопросов — сортировка и фильтр по темам
- Исправлены ключи сортировки: oldest→date_asc, easy→diff_asc, hard→diff_desc
- loadTopics() теперь использует /api/subjects/:slug/topics вместо
  запроса 200 вопросов (не хватало для 400+ вопросов)
- Добавлен onchange для select#f-topic (без него выбор темы не срабатывал)
2026-04-23 23:10:58 +03:00
Maxim Dolgolyov 952a54f97c security+perf: полное ревью — 17 фиксов P0/P1 (XSS, IDOR, race conditions, rate limits, TURN, WAL)
## 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>
2026-04-23 12:16:08 +03:00
Maxim Dolgolyov 26ba289019 a11y: WCAG AA contrast + ARIA roles + focus management across all pages
- css/ls.css: --text-3 #8898AA → #56687A (5.1:1 contrast), min-height 44px on .btn-primary/.btn-ghost/.sb-link, new .icon-btn utility (44×44px)
- js/api.js: lsConfirm — role=dialog, aria-modal, aria-labelledby, Tab focus trap, restore focus on close; lsToast — aria-live=polite on container, role=alert on errors; live quiz — role=dialog, role=radiogroup, role=radio, aria-checked, keyboard support
- test-run.html: q-opt divs — role=radio/checkbox, aria-checked, tabindex, keyboard enter/space; confirm modal — role=dialog, aria-modal; btn-flag — aria-pressed; dots — aria-label, aria-current; touch targets 44px
- board.html: btn-del-ann — aria-label; reaction buttons — aria-label, aria-pressed
- All 18 HTML files: replace hardcoded color:#8898AA with color:var(--text-3)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 11:42:38 +03:00
Maxim Dolgolyov 3a4623a60a fix: полное ревью системы — 15 исправлений безопасности и надёжности
Безопасность:
- 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>
2026-04-16 10:59:19 +03:00
Maxim Dolgolyov 6cd0cf34d4 fix: глубокое ревью онлайн-урока — 14 исправлений (P0-P3)
P0 — краши:
- CREATE TABLE classroom_hands в migrate.js (отсутствовала)
- emit→emitToUser для allowDraw/revokeDraw/mutePeer (WS доставка)
- deleteHistorySession обёрнут в db.transaction() + добавлена очистка hands/invites

P1 — гонки и безопасность:
- deletePage: 4 SQL в транзакции (race при параллельной записи)
- postStrokes: MAX(seq) внутрь транзакции (дубли seq)
- duplicatePage: добавлен seq в INSERT (NOT NULL crash)
- hasAccess для lowerHand/getHands/reactToMessage (утечка данных)
- loadTemplate: проверка owner шаблона
- attachment_url: только /uploads/* (XSS через javascript:/data: URI)
- wbFlushBatch: backoff при ошибке (было 12.5 req/s retry)
- pagehide leave: keepalive fetch для гарантированной доставки
- _wbOwnIds: cap 2000 (утечка памяти на длинных уроках)

P2-P3:
- simState: лимит 64KB (предотвращает OOM broadcast)
- ws-server кеши: cleanup drawCache при invalidateSession

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 09:22:39 +03:00
Maxim Dolgolyov f1e6ed7f2d fix: ревью онлайн-урока — 8 исправлений багов, уязвимостей и улучшений
- 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>
2026-04-16 09:02:50 +03:00
Maxim Dolgolyov 847e9b9b4f fix: добавить video.play() для демонстрации экрана — предотвращает блокировку autoplay браузером 2026-04-15 19:22:12 +03:00
Maxim Dolgolyov 8a86ff3fe5 debug: add RTC diagnostic logs (ICE state, ontrack, sendOffer, signal flow) 2026-04-15 13:45:07 +03:00
Maxim Dolgolyov c3050b9e43 fix: WebRTC audio — glare handling (Perfect Negotiation) + autoplay + signalingState guard 2026-04-15 13:28:38 +03:00
Maxim Dolgolyov 7f23cfdacd fix: тема доски синхронизируется между учителем и учениками в реальном времени
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 21:18:44 +03:00
Maxim Dolgolyov 366ad6e13e feat: красивое модальное окно редактирования аватара с кропом и зумом
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 21:10:42 +03:00
Maxim Dolgolyov c2eb319162 feat: avatar moderation — ученик загружает фото, учитель/админ подтверждает или отклоняет
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 20:55:45 +03:00
Maxim Dolgolyov 6429e07606 fix: sidebar profile always visible — only .sb-nav scrolls, .sb-foot pinned at bottom 2026-04-14 20:40:45 +03:00
Maxim Dolgolyov f3c9ab860e feat: sound system — 12 new sounds + navigation category; dashboard FAB widget button
Sounds:
- UI: modal_open, modal_close, tab_switch, delete
- Navigation (new category): page_enter, section_reveal
- Classroom: timer_warning, wb_clear, file_shared
- Gamification: challenge_complete, daily_login
- Quiz: time_up, quiz_bonus

Dashboard:
- Widget configurator moved from header to fixed FAB (bottom-right)
  no longer pushed off-screen by wide sidebar

Profile settings:
- Added Navigation category toggle
- Expanded preview section: 12 test buttons covering all categories

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 20:26:04 +03:00
Maxim Dolgolyov 89ba25cd20 feat: user preferences sync — server-side storage, whiteboard defaults, dashboard widget visibility
- New table `user_preferences` (user_id PK, JSON blob, updated_at)
- GET/PATCH/DELETE /api/preferences with deep-merge UPSERT
- LS.prefs singleton in api.js: dot-notation get/set, debounced flush (1.5s), server sync
- classroom.html: load wb.color/width/lineStyle/theme from prefs on init; save on change
- dashboard.html: widget configurator panel (gear button) — toggle visibility per-user, persisted server-side

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 20:17:25 +03:00
Maxim Dolgolyov ba20a76839 feat: add sound & prefs settings tab in profile
- 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>
2026-04-14 19:47:59 +03:00
Maxim Dolgolyov 29aa985504 feat: add sound system (LS.sfx) — synthesized Web Audio API sounds for classroom, gamification, quiz
- New js/sound.js: shared LS.sfx module with 21 synthesized sounds (ADSR envelope, sequences, sweeps, noise)
- Classroom: lesson_start/end, user_joined/left, hand_raise, chat_message, muted, draw_permitted
- Dashboard: achievement, level_up, xp_gain, coin via SSE events
- Live quiz: quiz_start, quiz_end on question launch and results
- Settings panel: global enable toggle + volume slider + localStorage persistence
- Replaces old _crBeep() in classroom.html

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 19:43:13 +03:00
Maxim Dolgolyov d2bf3aba47 fix: цвет текста кнопок-действий в панели стереометрии 2026-04-14 14:19:28 +03:00
Maxim Dolgolyov e4f7c167eb refactor: переработана панель стереометрии — иконки, toggle-ряды, stepper для n 2026-04-14 14:14:37 +03:00
Maxim Dolgolyov d822b705c4 feat: удаление последнего измерения и очистка всех измерений 2026-04-14 14:05:40 +03:00
Maxim Dolgolyov 74fe2c4179 fix: добавить дуги оснований конуса/цилиндра в _edges для постановки точек 2026-04-14 14:02:22 +03:00
Maxim Dolgolyov fff22f7331 feat: засечки рёбер, производные точки 3D, длины рёбер в стереометрии 2026-04-14 13:59:32 +03:00
Maxim Dolgolyov 481a9aeb02 feat: стереометрия — усечённая пирамида, правильные многогранники, скрещивающиеся прямые
- Добавлены фигуры: усечённая пирамида, октаэдр, икосаэдр, додекаэдр
- Формулы V, S, r_вп, R_оп для всех новых фигур
- Инструмент '∠ скрещ. прям.' — угол и расстояние между скрещивающимися прямыми (4 клика)
- Для икосаэдра/додекаэдра — THREE.IcosahedronGeometry/DodecahedronGeometry с извлечением рёбер
- Вписанная/описанная сфера поддерживает октаэдр, икосаэдр, додекаэдр
- Параметр n добавлен для пирамиды

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 11:59:42 +03:00