Commit Graph

11 Commits

Author SHA1 Message Date
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 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 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 35849cf231 feat: планиметрия — интерактивная геометрическая симуляция
- Новый файл frontend/js/labs/geometry.js (~1200 строк):
  GeoEngine (граф объектов с каскадным удалением),
  GeoViewport (система координат math↔canvas, зум/пан),
  GeoSim (полный движок: точки, отрезки, прямые, лучи,
  окружности, треугольники, многоугольники, привязка к сетке
  и точкам, undo/redo, экспорт PNG, classroom sync)
- frontend/lab.html: карточка, ctrl, sim-geometry секция,
  функции geoSetTool/geoToggle/_openGeometry, скрипт-тег
- frontend/admin.html: geometry в ADMIN_SIMS
- backend/src/db/migrate.js: таблицы geometry_tasks,
  geometry_submissions

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 09:40:41 +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