Commit Graph

844 Commits

Author SHA1 Message Date
Maxim Dolgolyov 71d94f45f1 refactor(admin): перенос блоков «Статистики» в «Обзор», удаление вкладки «Статистика»
Обзор теперь показывает и итоги за всё время (Пользователей, Тестов пройдено, Средний
результат) и средний результат по предметам за всё время — данные грузятся из
adminGetStats параллельно с обзором. Дублей нет: Обзор был про 24ч и контент.

Убрано полностью: nav-кнопка «Статистика», панель #tab-stats, маршрут stats в
ROUTE_TO_SECTION, подключение и файл sections/stats.js. #stats-хэш падает на #overview.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 19:10:42 +03:00
Maxim Dolgolyov ecce4b013a fix(analytics): «% ошибок» больше не превышает 100% (двойное ×100)
errorRate приходит из API уже в процентах (SUM(is_correct=0)*100/COUNT в analyticsController),
а фронт умножал ещё раз на 100 → 4130%. Убрал лишнее ×100; заодно корректно работают пороги
цвета (>=35 / >=60).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 17:24:17 +03:00
Maxim Dolgolyov 49f01fd23c fix(textbook): рабочий deep-link к § (/textbook/<slug>#sec-pN открывает нужный §)
Раньше статические страницы алгебры/геометрии игнорировали location.hash (init всегда
goTo('p10')), а textbook-tracker матчил только #pN через .para-pill — поэтому ссылки
exam-prep вида #sec-pN вели на главу, но не на §.

- server.js: /textbook/:slug всегда инжектит хелпер (и в обычном режиме, и в embed),
  _renderEmbed → _renderTextbook (кэш по filePath|mode, заголовки no-store сохранены).
- frontend/js/textbook-deeplink.js: по #sec-pN / #pN кликает .psel-card[data-id]
  (фолбэк .para-pill[data-para] → goTo → scrollIntoView). Универсально для статических
  и движковых страниц, идемпотентно, не конфликтует с textbook-tracker.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 16:32:57 +03:00
Maxim Dolgolyov c9f3eed8ed fix(exam): классификатор § — fallback при 0 совпадений + учёт opts_json; таксономия в репо
- classify(): bestScore стартует с 0 (нужно совпадение>0), иначе берётся явный fallback
  (последнее правило), а не первое. Чинит свал theory-statements→§15 и word-problems→проценты.
- optsText(): анализ текста вариантов ответа (формат пар [label, html]) — theory-statements
  размечаются по содержанию утверждений.
- alg-word-problems fallback → algebra-7-ch3 §16 (задачи уравнением), не проценты.
- Таксономия §: перенесена с gitignore-пути data/ на отслеживаемый
  backend/scripts/exam-textbook-sections.json + генератор gen-exam-textbook-sections.js.
- Результат: 784/800 (98%) размечено, спреды по подтемам корректны.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 16:29:40 +03:00
Maxim Dolgolyov d05bb386a7 test(exam): Phase 6 — тесты exam-textbook-links.test.js (9/9 pass)
Проверяет: колонки в схеме, >= 90% задач размечены, topic_ref.paragraph
числовой тип, slug-значения из известных префиксов, fallback в exam_topics.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 16:18:57 +03:00
Maxim Dolgolyov a88b69797f feat(exam): Phase 5 — исправление subtopic-фолбэков (058_exam_topics_textbook_fix.sql)
alg-equations: §10(drobno) -> algebra-8-ch2 §8 (дискриминант, массовый тип)
alg-inequalities: §13 -> algebra-8-ch3 §17 (квадратные/метод интервалов)
alg-polynomials: algebra-9 -> algebra-7-ch2 §14 (разложение, основной тип)
alg-numbers: algebra-9 hub -> math-6-ch4 (рациональные числа)
alg-arithmetic: algebra-9 hub -> math-6-ch1 (десятичные дроби)
alg-powers: algebra-9 hub -> algebra-7-ch1 §1 (натур. показатель)
alg-word-problems: algebra-9 hub -> math-6-ch2 (проценты, массовый тип)
geom-quadrilaterals: geometry-9-ch2 §9 -> geometry-8-ch1 §4 (параллелограмм)
geom-circle: geometry-9-ch2 §7 -> geometry-8-ch4 §8 (центральный/вписанный угол)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 16:18:51 +03:00
Maxim Dolgolyov a096f3bcd9 feat(exam): Phase 4 — контроллер использует task-level textbook_slug/paragraph
- SELECT добавляет t.textbook_slug, t.textbook_paragraph во все 7 запросов
  (getVariantTasks, practiceRandom, practiceUnsolved, pickRandomByDifficulty,
   topicTasksUnsolved/Any, weakBatchTasks, getTasksByIds)
- shapeTask() предпочитает task-level ссылку, fallback на refMap subtopic
- /variants/:n/tasks аналогично использует per-task поля
- getParaTitle() строит map chapter:para -> title из g9_textbook_sections.json

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 16:18:41 +03:00
Maxim Dolgolyov e210410526 feat(exam): Phase 3 — классификатор tag-exam-textbook.js (100% math9, 800/800)
Детерминированная эвристика: subtopic → кандидатные §, keyword-scoring по тексту.
Карта subtopic→primary § по PLAN.md. Флаги: --exam, --dry-run, --report.
Результат: 800 задач math9 размечены без единого null (algebra-8-ch2#8 и др.).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 16:18:29 +03:00
Maxim Dolgolyov c7cfd72e7f feat(exam): Phase 2 — схема per-task textbook link (057_exam_task_textbook.sql)
ALTER TABLE exam_tasks ADD COLUMN textbook_slug TEXT;
ALTER TABLE exam_tasks ADD COLUMN textbook_paragraph INTEGER;

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 16:18:22 +03:00
Maxim Dolgolyov b4a5b1abc2 fix(permissions): кнопка «Права» (вкл. временные права) видна не только учителям
Модалка индивидуальных прав пользователя (с кнопкой «врем.» — выдать право на
срок, B8) открывалась только для u.role==='teacher'. Временные/индивидуальные
права нужны и ученикам (магазин, лаба, тесты на срок). Показываем «Права» всем,
кроме admin (он и так байпасит все права).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 15:34:15 +03:00
Maxim Dolgolyov b9b86a3656 docs(permissions): Phase C (кастомные роли) завершена на ветке — прогресс C-1..C-4b
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 15:27:21 +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 bdc8bef857 feat(permissions): C-4a — API конструктора ролей (/api/roles, admin)
rolesController + routes/roles (admin, inline guards): GET список (с числом
пользователей), POST создать кастомную роль (имя-идентификатор + метка + base_roles;
засев прав из функциональной базы), PUT изменить, DELETE удалить (пользователей
возвращает на базу), GET /:name/permissions (эффективная карта база+оверлей + defs).
setPermission теперь принимает кастомные роли (ключ валидируется по базе, хранится
под именем роли). Смонтировано в server.js + тест-харнесс. Тест roles-api 5/5.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 15:21:44 +03:00
Maxim Dolgolyov 32c2c44b76 feat(permissions): C-3 — пер-ролевые права кастомных ролей (резолвер + конфиг)
Миграция 056: снят CHECK с role_permissions.role (пересборка) → можно хранить
набор прав произвольной кастомной роли. isEnabled(uid,permRole,baseRole,key):
user override → role_permissions[customRole] → фолбэк role_permissions[base] →
дефолт реестра(base). requirePermission передаёт permRole=customRole||role.
getMyPermissions/getUserPermissions: roleMap = база + наложение кастомной роли.
Тест C-3: права кастомной роли перекрывают базу, фолбэк на базу. custom-roles 8/8,
permissions 17/17, backend без регрессий.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 15:11:56 +03:00
Maxim Dolgolyov 7cdb2e2af2 feat(permissions): C-2 — присвоение кастомной роли пользователю (users.custom_role)
Миграция 055: ADD COLUMN users.custom_role (безопасно, без пересборки users).
Модель: users.role = функциональная база (встроенная, CHECK ок, драйвит ветки
контроллеров и резолв прав), users.custom_role = имя кастомной роли. updateRole
(PATCH /api/admin/users/:id/role) принимает кастомные роли → ставит base_roles[0]
как базу + custom_role=имя; встроенная → custom_role=NULL; неизвестная → 400.
authMiddleware/optionalAuth читают custom_role → req.user.customRole; requireRole
расширяет до effectiveRoles(customRole||role). Тесты custom-roles 7/7; backend без регрессий.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 15:03:41 +03:00
Maxim Dolgolyov 5aa2dd1a4b feat(permissions): C-1 — фундамент кастомных ролей (roles table + наследование гейтов)
Phase C, Stage C-1 (ветка feature/custom-roles): таблица roles (name, label,
base_roles JSON, is_builtin) + засев встроенных. auth.effectiveRoles(role) —
кастомная роль наследует base_roles (какие встроенные гейты проходит); встроенные
— быстрый путь без БД. requireRole() теперь проверяет пересечение allowed с
effectiveRoles → 111 существующих гейтов не задеты (встроенные ведут себя как
прежде). Дизайн: PHASE_C_DESIGN.md. Тест effectiveRoles 5/5; полный backend pass.

ВАЖНО (обнаружено): users.role в канон-схеме имеет CHECK (admin/teacher/student/
free_student), безопасно пересобрать users (FK от многих таблиц, миграции в txn)
нельзя → присвоение кастомной роли пользователю пойдёт через users.custom_role (C-2).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 14:57:10 +03:00
Maxim Dolgolyov a6ff965d80 docs(permissions): Phase B завершена (B5-B8); остаётся Phase C (архитектура)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 14:43:45 +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 0b0c113181 feat(shop): каталог товаров карточками по типам с реальным превью
Таблица заменена на сетку карточек, сгруппированных по типам
(Рамки/Титулы/Фоны/Эффекты) с заголовками и счётчиками. Каждая
карточка показывает настоящий вид товара:
- frame  → кольцо аватара по data.css
- background → .bg-preview.bg-<slug> (тот же CSS, что у клиента)
- title  → текст титула в его цвете (data.text/color)
- effect → анимация pulse / иконка-фоллбэк
Фильтр по типу, поиск и счётчик сохранены; неактивные товары
притушены; удаление компактной иконкой.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 14:24:33 +03:00
Maxim Dolgolyov 0a24a66a2e feat(permissions): B5 — группы прав (секции в UI + вкл/выкл всей группы)
registry: карта GROUP (Вопросы / Класс и ученики / Библиотека / Курсы и шаблоны /
Геймификация / Контент / Тесты и активность / Профиль), проброшена в byRole.group.
permissions.js: вкладка «Доступ · роли» рендерит права секциями по группам, у
каждой — «включить все / выключить все» (с подтверждением, если в группе есть
requireConfirmOff). Карточка вынесена в permCard(). Тест: definitions содержат group.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 14:21:52 +03:00
Maxim Dolgolyov 86935c43b0 docs(permissions): Фаза A завершена (A1-A4); заметка о неэнфорснутых ключах
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 14:18:10 +03:00
Maxim Dolgolyov 6bd1532735 feat(permissions): A4 — убрать role-level token_version bump (нет массового разлогина)
requirePermission читает права из БД на каждый запрос → серверное применение
живое. Прежний bump token_version при role-level изменении разлогинивал ВСЕХ
пользователей роли из-за одного тумблера. Убрали его: изменение применяется
сразу на сервере, клиент подхватит при следующем /permissions/me. User-level
bump оставлен (точечно одному пользователю — целевое обновление, не массовое).
Тест 3 обновлён: role-level НЕ бампает token_version + значение сохраняется.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 14:17:32 +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 1b78f675f8 feat(shop): компактный UX вкладки Магазин — статы-строка, фильтр, поиск
- 4 крупные карточки статистики → компактная строка stat-пиллов
- тулбар: фильтр по типу + поиск по названию + счётчик (N из M)
- таблица: иконка-чип по типу + название с описанием в одной ячейке,
  цветные бейджи типов, колонка ID убрана (id ушёл в подпись)
- состояния «Нет товаров» / «Ничего не найдено»

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 14:14:55 +03:00
Maxim Dolgolyov b0e385b2c6 feat(permissions): A2 — гигиена реестра (lint-тест) + ясные метки
Тест permissions-registry: каждый ключ из requirePermission/perm('…') в backend
есть в registry (ловит опечатки/дрейф; perm() падал на старте, сырой
requirePermission — нет). Заодно логирует ключи реестра, не используемые в
requirePermission (информативно — часть гейтится на клиенте через /me).
Метки theory.access/simulations.access переформулированы: «… доступен роли»
(видимость конкретного контента — по классам в «Доступ · контент»).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 14:11:52 +03:00
Maxim Dolgolyov 9ac2a612e0 feat(permissions): A1 — зависимости между правами (requires) + план переработки
registry: поле requires (questions.delete→manage, templates.public→manage,
courses.interactive→manage, simulations.quiz→access), проброшено в byRole.
auth.requirePermission: вынесен isEnabled(); право = own AND все requires
(дочернее не работает без родителя). /me и /users/🆔 effective с учётом
requires + requires в ответе. UI permissions.js: каскад — дочернее с
невыполненной зависимостью неактивно (тумблер заблокирован + «Требует: …»).
Тест зависимости. План: plans/permissions-rework/PLAN.md. Backend 216 pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 14:10:20 +03:00
Maxim Dolgolyov e37432d812 feat(shop): добавление/редактирование товара в модальном окне
Инлайн-панель формы внизу страницы заменена на модалку через LS.modal:
- shopAdminCreateItem/EditItem открывают окно openItemModal (create/edit)
- валидация: обязательное название + проверка JSON в поле «Данные»
- блокировка кнопки на время сохранения, ошибки через m.setError
- удалены инлайн-форма из admin.html и неактуальные
  shopAdminSaveItem/shopAdminCancelForm/showShopForm + стейт

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 14:09:52 +03:00
Maxim Dolgolyov 34c7886a41 refactor(shop): убрать дублирующее «Начислить монеты» из вкладки Магазин
Начисление монет осталось в «Пользователях» (быстрое действие quickAwardCoins)
и во вкладке «Геймификация». Из магазина удалены: HTML-блок «Начислить монеты»,
функции shopSearchUser/shopPickUser/shopAdminAwardCoins, их window-экспорты и
неиспользуемые стейт-переменные. Эндпоинт /shop/admin/award-coins не тронут —
им пользуется quickAwardCoins.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 14:05:14 +03:00
Maxim Dolgolyov 78a870ab70 fix(shop): форма товара скроллится в видимую область + тип «Фон» вместо мёртвого «Тема»
- shopAdminCreateItem/EditItem открывали форму под таблицей на 51 строку —
  вне экрана, выглядело как «кнопки не работают». Добавлен showShopForm():
  scrollIntoView + фокус в поле названия.
- В выпадающем списке типов «Тема» (theme) не поддерживается бэкендом
  (валидация POST: frame/title/effect/background) → создание падало с 400.
  Заменён на рабочий «Фон» (background); добавлена подпись в typeLabels.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 14:02:05 +03:00
Maxim Dolgolyov d9a89296de docs(access): Фаза 2c завершена; Фаза 3 отложена осознанно (низкий ROI)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 13:56:50 +03:00
Maxim Dolgolyov 3a59f56fb1 feat(access): Фаза 2c — две вкладки доступа читаются как один раздел «Доступ»
«Доступ к учебникам» → «Доступ · контент» (видимость контента по классам),
«Права доступа» → «Доступ · роли» (способности ролей), поставлены рядом.
Устраняет путаницу двух похоже названных вкладок (P0 из ревью). Полное слияние
в одну вкладку с под-вкладками — возможно позже (структурно крупнее).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 13:56:20 +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 6a874a341d feat(access): Фаза 2c — «Открыть весь предмет классу» в режиме «По классу»
Панель кнопок по предметам: один клик открывает выбранному классу весь контент
этого предмета (учебники/экзамены/симуляции/курсы вместе). Нормализация поля
предмета (subject|subject_slug), метки через SUBJ_LABEL. Чистый фронтенд на
существующем accSetRule. Закрывает находку ревью «нет операции открыть весь предмет».

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 13:43:49 +03:00
Maxim Dolgolyov 8467d7202a fix(admin): видимость выпадающего списка учебников в панели «Связи» симуляций
select использовал var(--bg-2,#1a1a2e) — переменная не определена в светлой
теме, поэтому фон падал на тёмно-синий, а текст оставался тёмным (--text):
список сливался с фоном. Заменено на белый фон + явные цвета option.
2026-06-03 13:41:25 +03:00
Maxim Dolgolyov d1f24736c3 feat(access): Фаза 2c (часть) — массовые операции в матрице доступа
Клик по названию контента в матрице открывает/закрывает его сразу ВСЕМ классам;
клик по имени класса (заголовок столбца) — открывает/закрывает ВЕСЬ контент этому
классу. Массовое закрытие спрашивает подтверждение; перерисовывается только tbody.
Использует существующий accSetRule (без новых эндпоинтов).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 13:36:34 +03:00
Maxim Dolgolyov 9b7585ac7b feat(access): Фаза 1c — видимость курсов по классам (Фаза 1 завершена)
Миграция 052: мост «открыть все опубликованные курсы всем существующим классам»
(тип 'course' уже в CHECK из 051). courseController.list/search фильтруют курсы
для НЕпривилегированных по allowedRefs(uid,'course') (content_ref = courses.id как
TEXT); admin/teacher — все. /api/access/catalog отдаёт курсы; CONTENT_TYPES в
админ-UI = textbook,exam,sim,course → курсы управляются во всех режимах «Доступ».
Тест course-access 4/4 (allowlist+класс+privileged+каталог). Полный набор: 213 pass.

ВАЖНО: новый опубликованный курс по умолчанию закрыт (allowlist) — открыть классам
в админке. Мост сохранил видимость текущих опубликованных курсов у существующих
классов. class_courses остаётся для назначений с дедлайном (сверх видимости).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 13:31:10 +03:00
Maxim Dolgolyov 2c7200fbad docs(access): отметка прогресса — симуляции (Фаза 1a+1b) готовы, курсы отложены
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 13:24:48 +03:00
Maxim Dolgolyov 4549b4e819 feat(access): Фаза 1b — управление доступом к симуляциям в админке
Бэкенд /api/access обобщён на тип 'sim': catalog отдаёт симуляции (lab_sims),
summary/matrix/class — карты по всем типам. Админ-секция «Доступ» теперь
показывает «Симуляции» во всех трёх режимах (по контенту / по классу / матрица)
+ поиск; helpers (bucket/keyName/itemsOf) обобщены через карты типов
(CONTENT_TYPES=textbook,exam,sim; course зарезервирован). Теперь админ/учитель
могут открывать/закрывать конкретные симуляции классам и ученикам — закрыт UX-
разрыв из 1a (новые классы без UI-управления). Тест: каталог включает sims; 210 pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 13:24:08 +03:00
Maxim Dolgolyov 9a145e5d62 feat(access): Фаза 1a — видимость симуляций по классам (добавочная модель)
Миграция 051: расширяет content_access.content_type на 'course'/'sim' (пересборка
таблицы — SQLite не умеет ALTER CHECK) + мост «открыть все включённые симуляции
всем существующим классам» → текущее поведение не меняется. GET /api/lab/sims
теперь фильтрует список для НЕпривилегированных по allowedRefs(uid,'sim'); admin/
teacher видят все. Ролевой simulations.access остаётся «модуль вкл.» (добавочно).
Тесты: lab-access (4/4, allowlist+класс+личное), lab-sims переведён на admin для
проверки полного каталога (видимость ученика — в lab-access). /api/lab в харнессе.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 13:19:29 +03:00
Maxim Dolgolyov 16d0f91622 docs(access): прогресс (Фазы 0/2a/2b done) + зафиксировано решение по Фазе 1
Фаза 1 — модель ДОБАВОЧНАЯ: ролевой simulations.access = «модуль включён для
роли», видимость конкретных sim/курсов — дополнительно по классам через
content_access (roleHasModule AND classAllowsItem). Миграция-мост открывает
всё всем классам → поведение не меняется.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 13:08:19 +03:00
Maxim Dolgolyov 596e8d8b30 feat(access): Фаза 2b — поиск/группировка по предмету + «эффективный доступ»
Режим «По контенту»: поиск по названию в левой колонке (обновляет только список,
фокус сохраняется) + подзаголовки по предмету (Математика/Физика/…). У раскрытого
класса рядом с tri-state каждого ученика — бейдж итогового доступа «видит/не видит
· лично|по классу|по умолч.» (считается клиентски из загруженных правил) — снимает
путаницу «наследовать/открыт/закрыт».

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 12:50:57 +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 1bbddc00c8 feat(access): Фаза 0 — целостность правил доступа + подтверждение массового закрытия
- contentAccess.purgeAccessFor(scope,id) — единая точка очистки content_access
  (нет FK). deleteClass и _deleteUserTx переведены на неё (убрано дублирование).
- Админ-UI: confirm() перед «Закрыть у всех / Закрыть весь» (необратимая массовая
  операция больше не срабатывает мгновенно).
- Новый тест content-access.test.js (9/9): allowlist, ученик>класс, наследование
  главой хаба, admin/teacher bypass, allowedRefs/filterTextbooks, purgeAccessFor,
  чистка правил при DELETE класса. Полный backend-набор: 203/206 (3 — baseline Auth).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 12:39:08 +03:00
Maxim Dolgolyov edb98895df docs(access): план переработки системы прав (ревью + фазы)
Единая модель видимости контента: расширение content_access на course/sim
(доступ по классам), разведение «способности (роли)» vs «видимость (классы/
ученики)», целостность (purgeAccessFor + чистка при kick), UX админки (матрица
класс×контент, поиск/группировка, эффективный доступ, групповые правила по
предмету/параллели), серверный гейт HTML через cookie-сессию. 4 фазы + риски.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 12:32:00 +03:00
Maxim Dolgolyov 5a2a1be089 feat(math5): Глава 3 «Обыкновенные дроби» — §1–18 + финал (Sonnet по эталону)
Дроби и доли, основное свойство и сокращение, смешанные числа, сравнение,
сложение/вычитание/умножение/деление дробей, задачи на дроби; геометрия:
параллельные/перпендикулярные прямые, периметр многоугольника, площадь и
площадь треугольника, среднее арифметическое, столбчатые диаграммы,
параллелепипед и объём (2D-изометрия). Inline-SVG визуалы (полоса долей,
сетка умножения, изометрия). Реализовано Sonnet-агентом инкрементально по
образцу math_5_ch1; проверено: грузится без ошибок, §1–18 без заглушек.

Учебник «Математика 5» наполнен ЦЕЛИКОМ (3 главы, 44 §). Тесты math5: 12/12.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 10:34:15 +03:00
Maxim Dolgolyov 06e9846cc3 feat(math5): Глава 2 «Выражения. Уравнения» — §1–9 + финал (Sonnet по эталону)
Числовые выражения и порядок действий, выражения с переменными, уравнение
(SVG-весы + решение/проверка корня), формулы (P,S,путь), решение задач
уравнением, угол (SVG-рисунок + классификация острый/прямой/тупой/развёрнутый),
прикладные/занимательные/исторические § + финал-боссы. Реализовано Sonnet-агентом
по образцу math_5_ch1, проверено: грузится без ошибок, §1–9 без заглушек. Тесты: 11/11.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 10:18:29 +03:00
Maxim Dolgolyov 12a08e7d42 feat(math5): Глава 1 ЗАВЕРШЕНА — §15–§17 (вокруг нас, движение, история чисел)
§15 Математика вокруг нас (задачи из жизни + прикидка в уме). §16 Движение/
взвешивание/переливание (s=v·t тренажёр + логические задачи). §17 Исторические
сведения (системы счисления; тренажёр римских цифр + квиз по истории чисел).
Глава 1 целиком: §1–17 + финал, все § наполнены (тест «нет заглушек»). Эталон
для Sonnet по Гл.2–3. Тесты math5: 9/9.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 09:41:17 +03:00