Commit Graph

45 Commits

Author SHA1 Message Date
Maxim Dolgolyov e793b4ec09 feat(materials): Фаза 5 — заметка в флешкарты
Кнопка «В флешкарты» на карточке-заметке: выбор колоды (или новая «Из материалов») →
создание карточки (front=заголовок, back=текст) через существующий API флешкард.
Хелперы fcListDecks/fcCreateDeck/fcAddCard в js/api.js.

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

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

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

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

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 11:33:01 +03:00
Maxim Dolgolyov 6b148127b6 feat(permissions): C-4b — админ-UI конструктора ролей + назначение пользователю
Клиент: listRoles/createRole/updateRoleDef/deleteRole/rolePermissions. Во вкладке
«Доступ · роли» — блок «Конструктор ролей»: создать роль (имя-идентификатор +
название + базовые роли чекбоксами), список кастомных ролей, «Настроить права»
(тогглы по группам через getRolePermissions + setPermission под именем роли),
«Удалить» (возврат пользователей на базу). В списке пользователей выпадающий
список ролей теперь включает optgroup «Кастомные роли» (выбор по custom_role);
listUsers отдаёт custom_role. Phase C (произвольные роли) завершена на ветке.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 15:26:52 +03:00
Maxim Dolgolyov a250d15f9a feat(permissions): B8 — временные права (expires_at) с авто-снятием
Миграция 053: user_permissions.expires_at (NULL = бессрочно). Резолвер isEnabled
+ /me + /users/:id игнорируют просроченные оверрайды (наследуют роль); seedDefaults
чистит просроченные строки. setUserPermission принимает days → выдаёт право на
срок (datetime('now','+N days')). API отдаёт expiresAt. Клиент: setUserPermission(...,days).
В модалке прав пользователя — бейдж «до ДАТА» + кнопка «врем.» (выдать на N дней).
Тест: срок хранится/отдаётся, просроченное игнорируется и вычищается. Backend pass.

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

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

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

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

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 13:55:02 +03:00
Maxim Dolgolyov 11ec350dfa fix(toast): нормализация типа — 'warning'/'ok' больше не сливаются с фоном
В CSS есть только классы .success/.error/.info/.warn, но код принимал любой
type. 7 вызовов LS.toast(...,'warning') и 1 'ok' давали класс без фонового
градиента → белый текст на светлой странице был невидим. Добавлен alias-map
(warning→warn, ok→success, danger/err/fail→error) + fallback неизвестных в
'info', чтобы у toast всегда был фон.
2026-06-03 13:47:07 +03:00
Maxim Dolgolyov 67a70c672d feat(access): Фаза 2a — режим «Матрица» класс × контент в админке
GET /api/access/matrix (классы + карта открытого контента одним запросом,
скоуп учителя). Клиент LS.accessMatrix. Третий режим вкладки «Доступ»:
таблица контент × классы с чекбоксами (правка в один клик) + поиск по
названию (обновляет только tbody — фокус ввода сохраняется), залипающие
заголовки. Тест /api/access смонтирован в харнесс; content-access.test 11/11
(+матрица: учитель видит свои классы и открытый контент, ученику 403).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 12:43:00 +03:00
Maxim Dolgolyov ec2a207fb8 feat(classroom): тумблер «Вызов на урок» в профиле + интеграция мелодии в LS.sfx
Мелодию-вызов перевёл с кастомного Web Audio на общий движок звуков LS.sfx:
- длинный вестминстерский бой теперь в sound.js (звук lesson_start);
- api.js лениво подгружает sound.js на любой странице и играет lesson_start
  по SSE classroom_started (вместо собственного синтезатора);
- отдельный pref lessonCall + тумблер «Вызов на урок» и кнопка прослушивания
  в профиле (Настройки → Звуки); уважает мастер-тумблер и громкость;
- lesson_start выведен из категории classroom (управляется своим тумблером);
- разблокировка AudioContext по первому жесту перенесена в sound.js.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 09:11:44 +03:00
Maxim Dolgolyov 63ceeaabc2 feat(classroom): мелодия-вызов длиннее — полный вестминстерский бой (5 фраз)
Расширил перезвон с одной нисходящей фразы до полного боя из 5 фраз по 4 ноты
(G4/C5/D5/E5) с паузами между фразами и протяжной финальной нотой (~7-8 с).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 09:03:52 +03:00
Maxim Dolgolyov 7d8e2220ff feat(classroom): мелодия-«вызов на урок» при старте урока у ученика
Короткий нисходящий перезвон (E5-D5-C5-G4, Вестминстер-lite) через Web Audio,
без аудиофайлов: колоколообразный тембр с мягким затуханием. Играет только на
реальном событии SSE classroom_started (не при заходе в середине урока).
AudioContext разблокируется на первом действии пользователя (автоплей-политика).
Отключение: localStorage ls_cr_chime='off'.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 09:01:28 +03:00
Maxim Dolgolyov 86a08348e0 feat(classroom): выделить вход в онлайн-урок — акцент в сайдбаре + липкий баннер
Пункт «Онлайн-урок» в сайдбаре теперь визуально выделен (акцентная иконка),
а когда урок идёт — пульсирующий бейдж «В эфире» (и точка-пульс в свёрнутом
режиме). Вместо легко пропускаемой всплывашки снизу — липкий баннер сверху
на любой странице с кнопкой «Войти», пока урок активен. Состояние берётся из
SSE classroom_started/ended + проверки /api/classroom/my/active при загрузке
(чтобы баннер появлялся и при заходе в середине урока). Для учеников.

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

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

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

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 18:12:55 +03:00
Maxim Dolgolyov 76df3b4594 feat(access): вид «по классу», массовые действия, бейджи состояния + чистка orphan-правил
По итогам ревью системы прав:
- админка: переключатель режимов «По контенту» / «По классу»
- кнопки «Открыть всем классам» / «Закрыть у всех» (и зеркально по классу)
- бейджи N/M (сколько классов открыто) в списке контента
- эндпоинты /api/access/summary и /api/access/class/:id
- вкладка «Доступ к учебникам» перенесена к «Права доступа» (группа Пользователи)
- чистка content_access при удалении класса/ученика (нет FK)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 12:47:05 +03:00
Maxim Dolgolyov 471171b77c feat(access): доступ к учебникам и экзаменам по классам/ученикам из админ-панели
Модель allowlist (закрыто по умолчанию), правило ученика важнее класса.
Управляют админ (все) и учителя (свои классы/ученики).

- миграция 040: таблица content_access + непрерывный переход
  (всем существующим классам открыт текущий контент)
- сервис contentAccess: резолвинг доступа, главы наследуют хаб
- API /api/access (catalog/targets/rules) для admin+teacher
- гейты: каталог учебников, router.param slug/examKey, фильтр tracks
- клиентские редиректы на /403 (textbook-tracker, exam-prep boot)
- раздел админки «Доступ к учебникам»: классы + ученики (tri-state)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 12:33:05 +03:00
Maxim Dolgolyov f6fbe922a9 feat(shop): 9 premium animated backgrounds
Doubles the bg catalogue from 10 to 19 with richer multi-layer
animations. Every keyframe pack is CSS-only and respects the existing
prefers-reduced-motion fallback.

  sunset       550   slow hue cycle through warm palette
  rain         650   2-layer vertical streaks at different speeds
  snow         700   3-layer drifting flakes pattern
  clouds       750   drifting white blobs on day sky (only LIGHT one)
  fireflies    800   pulsing glowing dots, opposing drift
  cyber-grid   850   neon grid scrolling down with vignette
  kaleidoscope 1000  two huge conic-gradients in opposite rotation
  ocean        1100  layered blobs drift like undulating waves
  aurora-dance 1500  multi-band aurora — new premium top-tier

Tonal classification mirrored in api.js DARK_BG_SLUGS so the veil
picks the right contrast: clouds is light, the other 8 join the dark
set (alongside dark, stars, aurora, nebula, grid).

Each background also gains a matching .bg-preview.bg-<slug> rule that
reuses the same animation at the shop's 90px swatch — WYSIWYG.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 21:35:59 +03:00
Maxim Dolgolyov d2ca0d61cc fix(bg): add translucent veil so animated bgs don't bleed UI
The single bg-fx layer was painting at full vibrancy behind the entire
app. Most UI elements use rgba() fills — chips, sub-panels, the
achievements .ach-item, the goal-tier bar — so saturated colors bled
right through, hurting readability on the Достижения / dashboard /
mocks tabs.

Layered fix:
  • bg-fx drops to z-index:-2 (the animated layer)
  • new #ls-bg-veil sits on z-index:-1 with rgba(245,247,251,.78)
    (light) or rgba(15,23,42,.55) when body[data-bg-tone='dark']
  • applyCosmetics injects both elements and tags the body with
    bg-tone based on the slug (dark/stars/aurora/nebula/grid go dark,
    everything else light)
  • clearing the bg removes both layers + the tone attribute

Result: animations stay perceptible (~22% of the chosen palette comes
through the veil), but the page chrome reads at normal contrast.

Shop swatches keep full vibrancy — the .bg-preview is meant to show
the raw palette so users can compare.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 21:27:35 +03:00
Maxim Dolgolyov 98ec1ed478 feat(shop): animated backgrounds — system-wide cosmetic + picker
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>
2026-05-29 21:13:53 +03:00
Maxim Dolgolyov 660e7e2747 feat(gamification): Phase 1 — full kill-switch + textbook XP wrapping
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>
2026-05-29 19:43:24 +03:00
Maxim Dolgolyov 5356096349 fix(api): auto-stringify object bodies in LS.api (apiFetch)
LS.api was passing raw object bodies straight to fetch(), which coerces
them to '[object Object]' — the server then parsed empty JSON and 400'd
on missing fields. This silently broke every POST that uses LS.api
directly (EP.api.startMock, saveAttempt, mockAnswer, etc.).

LS.post already stringified, so most call sites worked. Now apiFetch
mirrors that behavior for plain objects, while FormData / Blob /
URLSearchParams / ArrayBuffer / strings still pass through unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 16:40:42 +03:00
Maxim Dolgolyov eb19ce3cf9 fix(auth): include avatar_url in login response + lazy refresh stale cache
Login was only returning {id, email, name, role}, so localStorage.ls_user
never had avatar_url for sessions started before today — and the sidebar
fell back to initials forever. Fixes:

  • login response now includes avatar_url
  • renderNavAvatar detects 'undefined' (cache predates the field) vs
    'null' (verified absent) and fires a one-shot /auth/me refresh in
    the background, then re-paints. Self-healing for existing sessions
    without forcing re-login.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 15:07:16 +03:00
Maxim Dolgolyov 46d373752c fix(profile): visual frame previews in shop + sidebar avatar sync
Shop items of type 'frame' now render a real avatar-sized preview with
the frame's CSS applied (instead of a generic lucide icon) so buyers
see exactly what they're paying for. Title items get a tag-shaped
preview in their color. The avatar-frames section above the shop also
shows the user's actual avatar inside the frame circles, not 'LS' text.

Sidebar nav-avatar now:
  • renders the uploaded avatar_url instead of always showing initials
    (LS.initPage + new LS.refreshNavAvatar helper)
  • picks up frame CSS on every page via applyCosmetics — previously
    only dashboard.html applied it
  • repaints immediately after picking/deleting an avatar preset
    (avPickPreset / avDelete now call LS.setUser + LS.refreshNavAvatar)

Backend getMyActive resolves avatar_frame to {id, css} for both
gamification frames ('fire', 'crown', ...) and shop-purchased frames
('shop_<id>'), so the client doesn't need a second round-trip to
look up the CSS.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 15:04:27 +03:00
Maxim Dolgolyov 6afe928c0d feat(labs): visual polish wave — LabFX foundation + 33 sims juiced up
ФУНДАМЕНТ (4 новых файла):
- _fx_core.js: LabFX namespace, glow.drawGlow, glow.pulse, haptic, shake
- _fx_particles.js: пул 1500 объектов, 6 shapes (dot/spark/ring/smoke/splash/dust)
- _fx_motion.js: tween + 12 easings + critically-damped spring
- _fx_sound.js: 9 procedural synth-звуков (click/tick/whoosh/chime/fizz/spark/bounce/pour/drone), Web Audio API
- Sound toggle в шапке lab.html с localStorage-persist

UX МИКРО (CSS + JS):
- Button states: hover scale+brightness, active scale-down, disabled grayscale
- Slider polish: custom thumb с тенью, filled-track gradient, hover/active
- Focus rings через :focus-visible
- Tooltip system .tt-host data-tt= с 400ms hover, fade-in
- Marching ants для selection
- Loading skeleton с shimmer
- Empty state .sim-empty-* паттерн
- Toast: progress bar внизу, icons по типу
- Cursor states utility classes
- View Transitions API для smooth sim-switch, fallback на CSS fade

PHASE 2 — визуальные эффекты для 33 симуляций:

Physics motion: projectile (launch whoosh + landing splash/shake/haptic + target chime), pendulum (max-extension tick + bob glow), collision (bounce + sparks + shake), angrybirds (whoosh/bounce/fizz/chime + confetti), newton (rocket flame trail + scene transitions), forcesandbox (spring glow + impact sparks)

Physics fields: emfield (field-lines glow + particle trail + lightning при high field + Gauss-drag haptic + rod motion sparks), circuit (energized-wire glow ∝ current + LED bloom + short-circuit shake/spark + heat shimmer smoke + place/erase/switch sounds), opticsbench (beam glow на всех режимах + caustics dust у фокуса + TIR/Brewster one-shot sounds)

Thermo+waves: waves (mode-switch whoosh + Mach-cone particles + spectrum harmonic chime + waveform glow), hydrostatics (pour sound + splash при погружении + valve click), isoprocess (PV-trail dust), heatengine (drone цикла + hot/cold reservoir smoke/dust + phase-change ticks), radioactive (Geiger tick throttle + decay sparks + half-life chime + α/β/γ glow)

Chemistry: chemsandbox (pour splash + fizz bubbles/dust/spark по типу реакции + shake при горении), equilibrium/electrolysis/reactions/titration/flask/ionexchange/redox (pour/fizz/spark/chime по событиям, частицы при ключевых моментах), stoichiometry (fizz bubbles + recipe-change click)

Math+geom: geometry (tool-click + object-create tick + locus glow + challenge confetti via LabFX + haptic), triangle (vertex-drop tick + special-point glow), stereo (figure-change whoosh + cross-section chime, no particles — Three.js), trigcircle (drag-haptic + pitch∝angle tick + sin/cos glow), graph/graphtransform/quadratic (slider-tick throttled + curve glow + discriminant-cross chime), probability (bounce + finish-chime burst), normaldist (pulsing shade + bell glow)

Bio+misc: celldivision (phase-change whoosh + prophase dust + anaphase poles spark + cytokinesis chime), photosynthesis (photon tick + ATP chime + glucose sparkle), bohratom (electron-jump chime ∝ levels + photon spark), orbitals/crystal (mode-change whoosh — Three.js, sound only), logic (gate-place + wire-connect + LED bloom + HIGH wire flowing dots + preset chime), gas/brownian/diffusion/states (preset whoosh, throttled tick по событиям)

Все LabFX вызовы обёрнуты в if (window.LabFX) guard — graceful degradation если фундамент не загружен. 47 JS файлов прошли syntax check.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 13:58:49 +03:00
Maxim Dolgolyov 703c4b5edf feat(admin): surface classroom feature toggle in tab-games
The feature_classroom_enabled flag was fully wired in backend
(classroom/sessions.js:11-14 returns 403 when '0', initialized in
legacy-migrate.js:870 to '1') but had no UI control — admin could
only flip it via direct SQL.

- adminController.updateFeatures: classroom was ALREADY in allowed list
- admin/sections/games.js: new toggle row with video icon added to GAME_FEATURES
- js/api.js hideDisabledFeatures: classroom path mapping added ('/classroom')

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 20:16:26 +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 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 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 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 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 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 90f6a1d91e fix: красная книга не скрывалась в сайдбаре при отключении (href без .html) 2026-05-07 13:46:15 +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 c2eb319162 feat: avatar moderation — ученик загружает фото, учитель/админ подтверждает или отклоняет
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 20:55:45 +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 edb4c211a0 feat: universal sidebar via sidebar.js + stale ID cleanup
- Add js/sidebar.js: generates full sidebar HTML into #app-sidebar,
  handles role-based visibility, active link (with prefix matching),
  toggle wiring, collapsed state, board/features/notif init
- Replace <aside class="sidebar">...</aside> with <aside id="app-sidebar">
  across all 35 standard-layout pages via scripts/apply-sidebar.js
- Add notifications.js to 5 pages that were missing it
- Fix api.js initPage(): skip toggle re-wiring if data-sb-wired set,
  fix active link selector .sb-item → .sb-link
- Remove stale sbl-*/nav-admin/btn-upload-nav getElementById calls
  that crashed after sidebar replacement (lab, classes, collection,
  crossword, hangman, knowledge-map, library, pet, profile)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 21:22:21 +03:00
Maxim Dolgolyov fd29acbbdd feat: WebSocket real-time + rAF render gate + guest board + screen picker
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>
2026-04-13 18:04:59 +03:00
Maxim Dolgolyov be4d43105e LearnSpace: full-stack educational whiteboard platform
Node.js/Express backend + vanilla JS frontend.
Features: real-time collaborative whiteboard (SSE), multi-page support,
LaTeX formulas, shapes/connectors, coordinate systems, number lines,
compass, zoom/pan, Catmull-Rom pencil smoothing, ruler/protractor with
rotation & resize controls, minimap navigation overlay, auto-measurements,
multi-page thumbnails sidebar, PNG export, page templates.
Student/teacher workflows: classes, assignments, library, dashboard.
Mobile responsive. SQLite (better-sqlite3).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-12 10:10:37 +03:00