1093 Commits

Author SHA1 Message Date
Maxim Dolgolyov 40df8893cc fix(lab): значок «связанной симуляции» на карточках учебников не скрывался при выключенной лаборатории
В каталоге учебников (textbooks.html) у карточек есть кнопка .tb-lab-btn «открыть
связанную симуляцию» (openLabSim → /lab?sim=…). Это <button onclick>, а не <a href="/lab">,
поэтому kill-switch `[href="/lab"]` её не ловил, и значок-колба оставался при отключённой
«Лаборатории».

Фикс: добавил `.tb-lab-btn` в FEATURE_WIDGETS.lab → api.js скрывает её через инъекцию
при lab=false (работает и без ls.css). Плюс страховка в openLabSim: при lab=false не
открываем (тост «Лаборатория отключена»); админ — всегда (admin-override).

Verified vm-смоук на реальном api.js 4/4 (lab off → .tb-lab-btn скрыта; lab on → нет;
admin → ничего). node --check api.js + инлайн textbooks.html.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 23:36:11 +03:00
Maxim Dolgolyov 8027d9fda0 fix(gamification): kill-switch не доходил до учебников (нет ls.css)
Учебники (frontend/textbooks/*.html, 112 шт.) грузят api.js, но НЕ ls.css. api.js ставил
класс .no-gamification на <html>, но сами правила kill-switch (`[data-gamified]`, попап XP)
живут в ls.css → до учебников не доходили, и встроенная XP-механика (бейдж/карточка XP,
ачивки, level-up попап #ach-popup) продолжала отображаться при выключенной геймификации.

Фикс — централизованно в _applyFeatureCss: при gamification=false дублируем правила
`.no-gamification [data-gamified],#ach-popup{display:none}` в инъектируемый <style>, поэтому
kill-switch работает на ЛЮБОЙ странице с api.js, без ls.css. Плюс на страницах без сайдбара
(учебники/embed) теперь авторитетно дёргаем loadFeatures() (только для залогиненных,
in-memory-дедуп) — кэш фич там не обновлялся. Админ по-прежнему видит всё (admin-override).

Verified vm-смоук на реальном api.js 7/7 (student+off → правило инъектится; student+on → нет;
admin → ничего).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 23:25:45 +03:00
Maxim Dolgolyov 43df41287f feat(errors): сбор клиентских (браузерных) ошибок в админ-вкладку «Ошибки»
Глобальный репортер в api.js (грузится на всех страницах) ловит необработанные JS-ошибки
(window 'error') и rejected-промисы ('unhandledrejection') в браузере пользователя и шлёт
в POST /api/client-errors. Дедуп по сигнатуре + лимит 15/страницу, только для залогиненных,
keepalive, не флудит и сам не падает.

Бэкенд: routes/clientErrors (auth + rate-limit 20/мин на юзера) → clientErrorController
пишет в общий error_log с level='client' (message/stack/route=url/method=kind/user_id),
поля обрезаются. Появляются в существующей админ-вкладке «Ошибки» с бейджем «БРАУЗЕР»
(фиолетовый акцент vs розовый у серверных). Тест client-errors.test.js 5/5.

lint:routes 0; node --check всех файлов.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 23:17:04 +03:00
Maxim Dolgolyov db1db68488 fix(wishes): TypeError в toggleForm — lucide заменял <i> на <svg>
Кнопка «Поделиться идеей» падала: btn.querySelector('i') возвращал null, т.к. lucide.createIcons
при первом рендере заменяет <i data-lucide> на <svg>. Обернул иконку в стабильный
контейнер #wq-new-ic и пере-вставляю свежий <i> в его innerHTML перед icons() (с guard).

Headless-смоук toggleForm 5/5 (open/close, смена иконки chevron-up/plus, без throw).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 23:02:54 +03:00
Maxim Dolgolyov 3c45c606bf feat(admin/tests): пикер вопросов — серверный поиск по всему банку + «Показать ещё» + фильтры
Раньше «Добавить вопросы» в конструкторе тестов грузил лишь первые 100 вопросов предмета
(дефолтный лимит API), а поиск фильтровал клиентски только эти 100 — для математики
(1753 вопроса) 1653 были недоступны и не находились.

Теперь пикер ходит на сервер (бэкенд уже умеет q/difficulty/type/page): поле поиска
(debounce 300мс) ищет по ВСЕМУ банку предмета; кнопка «Показать ещё» подгружает
страницами по 100 с индикатором «Показано N из total»; добавлены фильтры по сложности
и типу. Поиск/фильтры сохраняются между перерисовками (после добавления вопроса).

Чистый фронтенд (tests.js + CSS в admin.html); бэкенд не тронут. Verified:
backend list q/difficulty/type/paging 8/8; headless-смоук пикера 12/12.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 22:17:17 +03:00
Maxim Dolgolyov 1649d6c2ec fix(admin): список сессий показывает и незавершённые (зависшие), +фильтр ?status
Корень бага «зависший тест есть в алерте, но нет в списке»: getAllSessions жёстко
фильтровал WHERE status='completed' → in_progress (зависшие) сессии в списке /admin#sessions
не появлялись. Теперь по умолчанию показываются все статусы (UI sessions.js уже null-safe:
percent/score/duration рисуются как «—»), плюс опциональный ?status=completed|in_progress|abandoned
для сужения. Зависшая сессия теперь находится и в списке, и открывается по deep-link из алерта.

admin-sessions.test.js 4/4; node --check OK.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 20:14:56 +03:00
Maxim Dolgolyov 4b5be8442b fix(admin): «Открыть» зависшей сессии ведёт на её детали, а не в пустой список
Алерт «Зависла» (in_progress >1ч) вёл на /admin#sessions, но список сессий показывает
ТОЛЬКО completed (getAllSessions: WHERE status='completed') — поэтому зависшей сессии там
не было (симптом: «показывает зависший тест, но в списке его нет»). Теперь «Открыть»
делает deep-link на детали конкретной сессии /admin#sessions/<id> — страница деталей
открывает сессию при любом статусе (getSessionDetail без фильтра по статусу) и позволяет
её посмотреть и удалить.

node --check OK; id присутствует в payload overview (stuckSessions → ts.id).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 20:02:53 +03:00
Maxim Dolgolyov 3898080f04 fix(features): админ открывает отключённые модули — пейдж-гейты уважают admin-override
Причина бага «из админа конструктор симуляций редиректит на дашборд»: у sim-builder.html
свой пейдж-гейт, который при feature_sim_builder=false уводил на /dashboard НЕЗАВИСИМО от
роли (мой прошлый admin-override был только в hideDisabledFeatures, а этот гейт его не знал).

Тот же недочёт нашёлся ещё у 3 страниц с собственным фича-редиректом (на /403):
collection.html, knowledge-map.html, red-book.html. Во все 4 добавил обход для админа
(админ управляет модулями → видит и открывает всё, даже отключённое) — согласно правилу
admin-override. Поведение для ученика/учителя не изменилось.

node --check инлайна всех 4 страниц — OK.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 16:59:51 +03:00
Maxim Dolgolyov efba722977 feat(wishes): редизайн страницы — удобнее и красивее
Полный фронт-редизайн /wishes (бэкенд не тронут):
- Hero с градиентной иконкой; «Поделиться идеей» — сворачиваемая форма (по умолчанию
  свёрнута, если пожелания уже есть; список сразу виден).
- Визуальный выбор категории чипами с иконками/цветом вместо select; счётчик символов.
- Статус-пилюли вверху с counts — кликабельный фильтр (для всех ролей, не только админ).
- Подбар: фильтр по категориям + живой поиск (по заголовку/тексту/автору); адаптивно
  скрывается, когда мало данных.
- Карточки: цветная иконка категории, статус-бейдж с иконкой, ответ админа в выделенном
  блоке, анимация появления, hover. Дружелюбные empty-состояния (нет идей / ничего не найдено)
  и скелетоны при загрузке.
- Клиентская фильтрация (один fetch, мгновенно) + точечные обновления списка без перезагрузки
  после создания/сохранения/удаления.

Verified: рендер-смоук 13/13 (карточки, иконки категорий, статусы, ответ, фильтры
status/cat/поиск с тогглом, empty); node --check инлайна; эмодзи нет (иконки — lucide).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 16:26:32 +03:00
Maxim Dolgolyov be9fdfa703 feat(wishes): трекер пожеланий по улучшению системы
Любой авторизованный пользователь подаёт пожелание (заголовок, категория, описание);
видит только свои. Админ видит все, фильтрует по статусу, ведёт по статусам
(новое → запланировано → в работе → готово / отклонено) и пишет ответ автору. Автор
получает уведомление при смене статуса (pushNotif).

Бэкенд: миграция 080 (таблица wishes), wishController (list/create/update/remove с
валидацией и whitelist категорий/статусов), routes/wishes (PATCH — только админ, DELETE —
автор«новое»/админ, проверка в хендлере), смонтировано в server.js. Тесты 15/15.

Фронт: страница /wishes (форма + список со статус-бейджами; у админа — фильтры,
смена статуса, ответ, удаление), пункт «Пожелания» в сайдбаре (все роли), фиче-флаг
feature_wishes_enabled (тумблер в админ-модулях + whitelist + FEATURE_HREFS; админ
видит всегда). Клиентские врапперы LS.wish*.

⚠️ Живой БД нужен npm run migrate (080). lint:routes 0; node --check всех файлов + инлайна.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 16:12:10 +03:00
Maxim Dolgolyov 758e1bf6cb feat(dashboard): статус «идёт онлайн-урок» с присоединением
На дашборде ученика/учителя — баннер активной classroom-сессии: заголовок урока,
для учителя «N онлайн», для ученика «Присоединиться/Вернуться», ссылка на /classroom
(там сессия подхватывается автоматически). Данные — LS.crGetMySession (учитель → своя
сессия, ученик → сессия его класса/приглашения). Нет активной сессии → баннер скрыт.

Доска работает по WebSocket, дашборд — по SSE, поэтому добавлен отдельный SSE-сигнал
classroom_live (state started/ended) ученикам класса/приглашённым/учителю в createSession
и endSession (аддитивно, в try/catch — не ломает создание/завершение сессии). Баннер
живо появляется/исчезает по этому событию + обновляется при возврате на вкладку.

Verified: рендер баннера 10/10 (ученик/учитель/нет сессии, online-счёт без вышедших,
пустой title→«Онлайн-урок»); node --check sessions.js + инлайна dashboard; sse-путь резолвится.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 14:24:14 +03:00
Maxim Dolgolyov 0d4c658d93 refactor(assignments): единый модуль assignment-utils.js (тип/«сдано»/срочность)
Логика классификации типа задания и статуса «сдано» дублировалась в трёх местах
(dashboard.html, homework.html, assignmentController.js) и начала расходиться. Вынес в
один UMD-модуль frontend/js/assignment-utils.js (грузится и в браузере, и в Node через
require — как svg-sanitize.js): type(a), isDone(a, sub, opts), urgencyScore(a).

Нюанс «сдано» для upload/file параметризован: вид ученика (acceptedOnly) — закрыто только
при принятой сдаче; учитель/обзор долгов — любая сдача не на доработке. Поведение всех трёх
поверхностей сохранено 1:1.

- homework.html: asgnType/asgnDone/urgencyScore → тонкие делегаты в AssignmentUtils.
- dashboard.html: urgencyScore делегирует; classify и upload-ветка buildAssignCard через
  AssignmentUtils.type/isDone (заодно корректнее: учебник-ДЗ больше не путается с upload).
- assignmentController: classOutstanding/_assignTypeOf → AssignmentUtils.

Verified: AU-контракт 25/25 (типы, isDone teacher vs acceptedOnly, порядок urgency);
интеграция 8/8 (classOutstanding те же 14 уч./42 просрочено; homework делегирует). node --check
всех файлов + инлайна обоих HTML.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 14:09:34 +03:00
Maxim Dolgolyov 5a4bc48027 feat(classes): вкладка «Долги» — что висит у учеников + удаление ДЗ класса/ученика
Новый read-only эндпоинт GET /api/classes/:id/outstanding (teacher/admin, ownership):
по каждому ученику класса — его незакрытые задания (классовые + личные от этого учителя)
со статусом не начато / в процессе / на доработке / просрочено и дедлайном. Логика «сдано»
совпадает с /homework (тест — завершён/исчерпаны попытки; учебник — всё прочитано;
загрузка/файл — есть сдача не на доработке). Общий запрос вынесен в assignmentRowsForUser(uid)
— им же теперь питается /assignments/my (поведение не изменилось, +поле created_by).

Фронт (classes.html): вкладка «Долги» в карточке класса — сводка «должников X из Y,
просрочено Z», по каждому должнику карточка со статус-чипами и списком зависших заданий;
бейдж с числом просрочек на вкладке. Удаление прямо из списка: личное → «у ученика»,
классовое → «у всего класса» (через DELETE /assignments/:id, ownership на бэке).

Verified: classOutstanding смоук на живой БД (14 учеников/58 позиций/42 просрочено,
ownership 403/404/admin); node --check; lint:routes 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 13:46:45 +03:00
Maxim Dolgolyov 73ba5a3530 fix(homework): блок ДЗ — только задания с флагом is_homework; ясная подпись типа
(1) На /homework блок «Актуальные задания» и выпадашка загрузки теперь показывают
только задания с флагом ДЗ (is_homework). Обычные тесты/экзамены и не-ДЗ файлы сюда
не попадают. Выпадашка привязки — только типы upload/file (куда ученик реально сдаёт
файл), без тест-ДЗ и учебников.

(2) В конструкторе задания (classes.html) вкладка типа «Сдать работу» → «Загрузка
работы»: остальные вкладки — существительные-типы контента (Файл, Готовый тест), а
«Сдать работу» читалась как действие ученика. Это тип upload — ДЗ, куда ученик грузит
файл (saveAssignment: is_homework=1, count=1); сам тип нужен, поправлена только подпись.

Headless-смоук фильтра ДЗ 6/6.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 13:08:57 +03:00
Maxim Dolgolyov a7f2ae9937 fix(features): админ видит и открывает все модули, даже отключённые
Скрытие фич в сайдбаре (и kill-switch геймификации) применялось независимо от роли —
у админа тоже пропадали пункты отключённых модулей. Админ управляет модулями, поэтому
должен видеть и открывать всё.

Добавлен admin-override (_isAdminUser, читает getUser() синхронно из localStorage —
работает и на ранней FOUC-инъекции): для админа _applyFeatureCss чистит <style>-скрытие
и снимает .no-gamification; hideDisabledFeatures выходит до любых скрытий/редиректов;
showBoardIfAllowed показывает доску админу всегда. Поведение для student/teacher не
изменилось. Verified vm-смоук на реальном api.js 10/10.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 13:03:09 +03:00
Maxim Dolgolyov 748b0aaab1 feat(homework): блок «Актуальные задания» на странице /homework
Раньше страница «Домашние задания» показывала только историю сдач, а сам список
актуальных ДЗ (что нужно сделать, с дедлайнами) жил лишь на дашборде. Теперь у ученика
сверху секция «Актуальные задания» на тех же данных (LS.myAssignments) — карточки с
дедлайном/просрочкой/срочностью, действие по типу задания: тест → Начать/Продолжить
(LS.startAssignment), учебник → Открыть §, файл → Скачать, ДЗ-загрузка → Сдать
(прокрутка к области загрузки + преднабор задания). Закрытые задания (пройденный тест,
прочитанный учебник, принятая работа) скрыты; пустая секция не показывается.

Заодно убрано ограничение «только первый класс» в выпадашке загрузки: задания берутся
по всем классам, а класс для отправки выводится из выбранного задания (data-class) —
чинит сдачу для учеников в нескольких классах.

Чисто фронтенд, аддитивно. Headless-смоук рендера 19/19.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 12:57:50 +03:00
Maxim Dolgolyov 22c7b38e9a feat(admin): сброс системы «чистый запуск» в веб-панели
Добавлено такое же действие, как [Z] в control-panel: POST /api/admin/reset-system
(+ /reset-system/plan для предпросмотра), только admin. Общая логика вынесена в
src/services/systemReset.js (classify/pickKeptAdmin/runReset) — реюзится CLI и эндпоинтом.

Веб-эндпоинт безопаснее CLI: сохраняет ТЕКУЩЕГО админа (оператор остаётся залогинен),
делает бэкап БД ДО сброса (wal_checkpoint + копия в data/backups/), требует body.confirm='СБРОС'.
UI — «Опасная зона» в overview-секции: предпросмотр плана + ввод «СБРОС» + результат с именем бэкапа.

db.js: добавлен db._path (нужен бэкапу при сбросе). Логика проверена смоуком на копии живой БД
(16 юзеров удалено, контент сохранён, REASSIGN на админа, гейм-счётчики обнулены, 0 висячих FK).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 11:45:13 +03:00
Maxim Dolgolyov 205290139d feat(control-panel): сброс системы «чистый запуск» (с бэкапом и подтверждением)
Пункт [Z] в control-panel.ps1: предпросмотр → бэкап БД → подтверждение вводом
«СБРОС» → очистка. Скрипт backend/scripts/reset-system.js (dry-run по умолчанию,
выполнение только с --apply --confirm=RESET):
• сохраняет одного админа (min id), переназначает ему авторский контент
  (courses/tests/flashcard_decks/custom_sims/шаблоны/библиотека/lab-ссылки/board);
• стирает всех остальных пользователей + классы/задания/сессии/геймификацию/
  уведомления/прогресс/историю классрума/доступы/логи;
• сохраняет контент: учебники, вопросы, темы, уроки, exam-prep, симуляции,
  биохимия, красная книга, магазин/достижения-определения, роли/права, app_settings;
• обнуляет игровые счётчики админа; классифицирует ВСЕ 119 таблиц, неизвестные не трогает;
• FK off + транзакция + foreign_key_check + VACUUM.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 11:32:44 +03:00
Maxim Dolgolyov c6d323ec6d feat(tests): витрина доступных тестов ученику + флаг «доступен ученикам»
Раньше ученик видел лишь 1 тест на предмет (дефолтный). Теперь учитель/админ
может пометить любой свой тест доступным, и он появляется в каталоге на дашборде.

- Миграция 079: tests.available_to_students (default 0).
- testController: list для ученика отдаёт тесты с available_to_students=1 и вопросами;
  create/update принимают флаг; update сделан частичным (не затирает поля при toggle).
- admin «Тесты»: бейдж «Доступен ученикам» + быстрый тумблер «Ученикам/Скрыть»
  (toggleTstAvail; конструктор доступен и учителям — видят свои тесты).
- Дашборд: виджет «Тесты» → секция «Доступные тесты» (loadAvailableTests), клик
  запускает фикс-тест. Прячется, если доступных нет.

⚠️ Живой БД нужен npm run migrate (колонка).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 11:03:42 +03:00
Maxim Dolgolyov c5d440a7a9 fix(tests): режимы доступных тестов только exam/practice + скрытие пустых предметов
Рассогласование: админ-настройка допускала режимы topic/random, но POST /api/sessions
принимает только exam/practice → клик по такому предмету падал с 400. Убрал topic/random
из валидатора subjects.js и из админ-дропдауна (SC_MODES). Дашборд: старые значения
topic/random коэрсятся в practice; предметы без вопросов в банке И без фикс-теста больше
не показываются (раньше давали 404 «No questions found» при запуске).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 10:53:43 +03:00
Maxim Dolgolyov 1aa95a6776 fix(dashboard): hero «Лаборатория дня» виден при выключенной лабе
Hero-карточка #hc-lab имела href="/lab", но loadLabOfDay меняет его на
/lab?sim=<id> → CSS [href="/lab"] больше не матчит, карточка оставалась видной.
Прячем по стабильному id: #hc-lab/#hc-pet/#hc-read добавлены в FEATURE_WIDGETS
(lab/pet/textbooks). .hero-row переведён на grid auto-fit (minmax 240) — сетка сама
подстраивается под видимые карточки без дыры; syncHeroRow прячет весь ряд, если
карточек не осталось (мобайл-медиазапрос не трогаем — без инлайн-колонок).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 10:37:41 +03:00
Maxim Dolgolyov 399a222b65 fix(dashboard): пустой бокс колонки прогресса, когда флешкарты отключены
#w-flashcard прятался, но он — секция внутри #w-progress-col (один .widget-бокс с
рамкой/паддингом: карточка + прогресс по предметам + результаты). Если все секции
скрыты (флешкарты выкл и нет данных), оставался пустой бокс. Добавлена
syncProgressCol(): прячет #w-progress-col, если ни одна секция не видна (computed-
display, учитывает и инъект-CSS флешкарт). Зовётся в конце loadFlashcardWidget /
loadLastResultsWidget / loadSubjProgressWidget.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 00:26:33 +03:00
Maxim Dolgolyov 796a2416cb chore(admin): секция «Игры» → «Модули» (там уже не только игры)
Вкладка/заголовок админ-панели переименованы: nav «Игры»→«Модули» (иконка
gamepad-2→layout-grid), section-title «Управление играми»→«Управление модулями»,
описание про «отключённые игры»→«отключённые модули». В секции теперь лаба,
теория, путеводитель, доска, классрум и т.д. — не только игры. Free-student
подсекция уже звалась «Модули…» — консистентно.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 19:11:09 +03:00
Maxim Dolgolyov 604fa7ac0b fix(sidebar): убрать мигание ссылок «Подготовка к экзамену» при отключении
Ссылки exam-prep (/exam-prep/*) скрывались отдельным async-механизмом (/api/exam-prep/
tracks), не входящим в синхронный кэш-CSS → мелькали на долю секунды после обновления.
Теперь hideDisabledFeatures кэширует точные хрефы скрытых треков в localStorage
(ls_examhide), а _applyFeatureCss добавляет их в инъект-CSS синхронно из кэша на ранней
загрузке (до сборки сайдбара). При включении трека он убирается из кэша → снова виден
(re-apply _applyFeatureCss после свежего fetch). hideEmptySidebarGroups перенесён в конец
hideDisabledFeatures (учитывает скрытие exam-prep).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 17:56:12 +03:00
Maxim Dolgolyov 38f8be9389 feat(features): тумблер «Путеводитель» (/sitemap)
Пункт «Путеводитель» теперь отключается как остальные модули: ключ sitemap в
вайтлист updateFeatures (backend) + тумблер в admin → фичи (GAME_FEATURES) +
/sitemap,/sitemap.html в FEATURE_HREFS (скрытие из сайдбара + редирект при выкл).
По умолчанию ВКЛ (opt-in disable). Пустая группа схлопнётся авто (hideEmptySidebarGroups).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 17:51:36 +03:00
Maxim Dolgolyov c04a8c2178 fix(sidebar): прятать пустые группы (заголовок без видимых пунктов)
Когда все пункты группы сайдбара скрыты (фичи отключены / teacher-only у ученика),
оставался висеть пустой заголовок-аккордеон (напр. «Практика и игры»). Добавлена
hideEmptySidebarGroups(): по .sb-group проверяет computed-display пунктов .sb-link
в теле и прячет группу, если ни одного видимого. Зовётся синхронно из sidebar.js
после сборки (по кэш-CSS — без мигания) и в конце hideDisabledFeatures (по свежим
данным; re-show при включении).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 17:49:51 +03:00
Maxim Dolgolyov 83f0ba9c04 fix(features): пустой блок флешкарт, лаба в сайдбаре, мигание (FOUC)
Три проблемы UX отключения модулей:
1) Пустой блок флешкарт на дашборде: виджет #w-flashcard — <div>, а скрытие шло
   только по [href]. Добавлена карта FEATURE_WIDGETS (флешкарты→#w-flashcard) —
   контейнер прячется целиком.
2) Лаборатория не уходила из сайдбара: не было ГЛОБАЛЬНОГО тумблера lab (только
   free-student). Добавлен в whitelist updateFeatures + GAME_FEATURES; map уже знал
   lab→/lab. Теперь выключение скрывает пункт и редиректит со страницы.
3) Мигание выключенных модулей (FOUC): hideDisabledFeatures асинхронный. Теперь
   loadFeatures кэширует /api/features в localStorage, а _applyFeatureCss инъектит
   <style id=ls-feat-hide> синхронно из кэша на ранней загрузке (api.js идёт до
   sidebar.js) — сайдбар/виджеты строятся уже скрытыми. Геймификация: класс
   no-gamification ставится на <html> (раньше body), ls.css правило body.no-gamification
   → .no-gamification (работает и для html, и для body).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 17:41:11 +03:00
Maxim Dolgolyov d5fbd0168e feat(permissions): +10 прав ролей с энфорсом (Доступ · роли)
Реестр (registry.js) пополнен правами, которыми раньше нельзя было управлять:
• Учитель: classroom.host (онлайн-уроки), livequiz.host (живые викторины),
  simbuilder.use (конструктор симуляций), flashcards.manage (общие колоды).
• Ученик: homework.submit (сдача ДЗ), materials.save («Мои материалы»),
  assistant.use (ИИ-ассистент), games.play (учебные игры),
  flashcards.access / exam.access (доступ к разделам).
Все default=1 → текущее поведение сохранено; админ может выключить по роли/классу/юзеру.

Энфорс на роутах: учительские — requirePermission (роуты уже teacher-only);
ученические на ОБЩИХ роутах (assistant/materials/games/flashcards/exam-prep) —
новый requirePermissionForStudents(key) (учитель/админ проходят всегда, проверка
только ученику — иначе isEnabled=false сломал бы учителя). PERM_DEFAULTS строится
из реестра → фолбэк до сидирования = enabled, никто не блокируется. Группы UI —
существующие (новых ярлыков нет). seedDefaults авто-сидит новые ключи на чтении.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 17:31:00 +03:00
Maxim Dolgolyov 54be84e74a fix(admin): глобальный мастер-тумблер «Геймификация» в админ-UI
Геймификация (gamification) была только в FS_FEATURES (free-student grid) — UI
позволял выключить её лишь для роли «свободный ученик», а ГЛОБАЛЬНОГО тумблера в
GAME_FEATURES не было, хотя бэкенд флаг feature_gamification_enabled и kill-switch
это поддерживают. Добавлен в GAME_FEATURES как «Геймификация (всё)» — теперь админ
может выключить XP/монеты/ачивки/магазин/стрики/лидерборд глобально из UI.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 17:08:59 +03:00
Maxim Dolgolyov dc71d7b4d9 fix(gamification): полнота kill-switch — испытания/стрик/монеты + гейт счётчиков
Аудит выключателя геймификации выявил элементы, НЕ покрытые body.no-gamification:
испытания недели (#ch-section/.ch-widget), календарь стриков (.streak-cal),
стат-кольцо стрика (#sr-streak), монеты в профиле (#p-coins-row), чипы стрик/цель
на карточке питомца. Добавлены в CSS kill-switch (ls.css). Бэкенд: updateChallenges
и onLabExperiment писали прогресс/счётчики без проверки флага — добавлен гейт
isGamificationEnabled() (XP/coins/achievements уже гейтились в award*-функциях).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 17:04:30 +03:00
Maxim Dolgolyov d8f2a7f98d fix(features): доска уходит из сайдбара при отключении + тумблер «Теория»
1) Доска при feature_board_enabled=0 не пропадала у учителя/админа: showBoardIfAllowed()
   зовётся напрямую на ~20 страницах и показывала доску БЕЗ проверки флага, перекрывая
   hideDisabledFeatures(). Теперь функция сперва грузит features и при board===false
   держит ссылку скрытой (для всех ролей).
2) Добавлен фич-флаг theory: ключ в вайтлист updateFeatures (backend), тумблер «Теория»
   в admin → games (GAME_FEATURES), и /theory,/theory.html в map hideDisabledFeatures
   (скрытие из сайдбара + редирект с /theory при выключении). По умолчанию ВКЛ (opt-in disable).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 16:56:22 +03:00
Maxim Dolgolyov 9d35aaf673 fix(admin/access): нативные confirm() → стилизованная модалка LS.confirm
Раздел «Доступ · контент» использовал браузерный confirm() («Подтвердите
действие на localhost…») для закрытия доступа у всех классов и копирования.
Заменены все 5 вызовов на LS.confirm (та же модалка, что в остальном админ-
разделе): «Закрыть доступ» (danger) и «Скопировать доступ» (danger:false).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 16:48:24 +03:00
Maxim Dolgolyov bd7dd06e47 fix(exam-prep): репаратор рендеринга ctmath — потерянные \ в опциях + <,> в $…$
Root cause: в seed-ах вариантов 101–121 опции писались как mc('$\sqrt..$') в
обычных кавычках вместо R`…` → JS-парсер съедал \s→s, \d→d и т.п., в БД легло
«$sqrt{17}$», «$dfrac{pi}{3}$» (KaTeX рисует «sqrt17», «dfracpi3»). Плюс литеральные
<,> внутри $…$ ломали HTML до KaTeX. Скрипт fix_ctmath_render.js (dry-run/--apply,
идемпотентный): восстанавливает \ перед командами (+нормализует управляющие символы)
в opts_json и заменяет <,>→\lt,\gt внутри $…$ в text/opts/solution. DRY-RUN: 307
строк (143 opts/128 text/157 sol), остаточных багов 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 19:34:59 +03:00
Maxim Dolgolyov f381873c34 fix(exam-prep): список «Варианты» показывает метку (ЦТ-2015…), а не «Вариант N»
variants.js хардкодил «Вариант ${n}» в гриде-пикере, заголовке и подписи кнопки,
игнорируя поле label из listVariants (бэкенд его уже отдаёт через examVariantLabel).
Добавлен хелпер labelOf(n) → подставляет v.label с фолбэком. mock.js дропдаун уже
использовал label — там достаточно перезапуска сервера, чтобы бэкенд отдал метки.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 19:20:45 +03:00
Maxim Dolgolyov dd69c026ec content(ctmath): вариант 121 — ЦТ-2011 (А1–А18 + В1–В12, 30 заданий)
Перенабор Вариант 1 из ЦТ 2011 В1-В10.pdf (тест полный, не только В), все 30
ответов сверены с официальной таблицей (полное совпадение). Уточнения по таблице:
A6 степень 3x+4 (→15·2^3x), A9=(3^-2)^-5→1/9, A7 корень -3, A8=10, A10=10π.
Фигурные A1/A2/B6 реконструированы (B6: y=x²-6x+9 ∩ y=1,25 → 4x₁x₂=31). Все В
числовые. Без авторских ссылок. Дедуп-гейт 0, KaTeX 30/30, DRY-RUN 30/30. Метка 121='ЦТ-2011'.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 16:53:06 +03:00
Maxim Dolgolyov 84625cd72a content(ctmath): вариант 120 — ЦТ-2012 (А1–А18 + В1–В12, 30 заданий)
Перенабор Вариант 1 из ЦТ 2012.pdf, все 30 ответов сверены с официальной
таблицей (полное совпадение). Фигурные A1/A13/B6 реконструированы с явными
данными (A1 — углы 70/40→равнобедренный; B6 — середины сторон→S=4). A15
уточнена по таблице: √(5⁵·20)=250, знаменатель ⁴√10 → 25·⁴√10. Все В числовые.
Без авторских ссылок. Дедуп-гейт 0, KaTeX 30/30, DRY-RUN 30/30. Метка 120='ЦТ-2012'.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 16:42:27 +03:00
Maxim Dolgolyov 0fb16ef85e content(ctmath): вариант 119 — ЦТ-2013 (А1–А18 + В1–В12, 30 заданий)
Перенабор Вариант 1 из ЦТ2013.pdf, все 30 ответов сверены с официальной
таблицей (полное совпадение). Фигурные A2/A3/A6/A16 реконструированы с явными
описаниями (A2 — образующая=AD, A6 — порядок лучей→40°, A16 — сечение 12×6=72).
Все В-задания числовые (long нет). Без авторских ссылок. Дедуп-гейт 0,
KaTeX 30/30, DRY-RUN 30/30. VARIANT_LABEL: 119='ЦТ-2013'.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 16:35:28 +03:00
Maxim Dolgolyov b9a82c326e content(ctmath): вариант 118 — ЦТ-2017 (А1–А18 + В1–В12, 30 заданий)
Перенабор Вариант 1 из CT-2017.pdf, все 30 ответов сверены с официальной
таблицей (полное совпадение). Фигурные A1/A3/A9/A11/A14 реконструированы с
явными числами (A9 — биссектриса, AM/MC=AB/BC→13,8). Без авторских ссылок.
Прогнан через дедуп-гейт (0 совпадений с пулом) + KaTeX-структуру + DRY-RUN 30/30.
VARIANT_LABEL: 118='ЦТ-2017'.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 16:28:06 +03:00
Maxim Dolgolyov 70cf6b3af1 tools(ctmath): check_variant_dups.js — гейт дедупликации перед добавлением варианта
Постоянный read-only инструмент: (1) без аргумента — аудит видимого пула [101;1999]
на внутренние точные дубли; (2) с seed-файлом — сверяет его TASKS с пулом ДО --apply.
Норма текста: теги/латех/пробелы убраны, ЧИСЛА сохранены (параллельные задачи дублями
не считаются). Сейчас пул = 514 задач, 514 уникальных сигнатур, 0 дублей.
Включается в конвейер тиража: новый вариант проверяется этим гейтом перед записью.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 13:31:02 +03:00
Maxim Dolgolyov 59ae4c1dea fix(exam-prep): практика/тренажёр берут только выверенные варианты (дедуп)
Тренажёр-по-темам и практика брали FROM exam_tasks без фильтра по варианту — в пул
попадали год-пачки (variant=год≥2011) и variant=0, которые ДУБЛИРУЮТ выверенные
варианты-пробники (51 дубль чистый↔пачка, 20 через variant=0). Ученик мог получить
одну задачу дважды.

Добавлен фильтр variant BETWEEN MV_LO..MV_HI (тот же [101;1999], что у пикера) во все
7 запросов выборки/счёта задач: practiceRandom, practiceUnsolved, topicTasksUnsolved,
topicTasksAny, listTopicsWithCounts (счётчик подтем), weakBatchTasks, pickRandomByDifficulty
(×2). Хелперы MV_LO/MV_HI (для math9 без диапазона — всё, кроме variant=0).

Результат: практика ctmath = только варианты 101–117 (496 задач, 0 дублей между собой),
год-пачки (714 задач) остаются в БД для возможного будущего, но не показываются.
Обратимо, без удаления данных. Рантайм-проверка: 5 эндпоинтов практики/тем → 200.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 13:29:02 +03:00
Maxim Dolgolyov de41b77ae3 feat(ctmath): вариант 117 — ЦТ-2021 (32 задания, А1-А18 + В1-В14)
Пробник ЦТ по математике 2021, Вариант 1. Формат: 32 задания (А1–А18 + В1–В14), А12/А16 —
с несколькими верными (тип open, ответ цифрами), В2/В3 множ.выбор, В1 на соответствие.
Источник: чистый PDF ЦТ 2021.pdf. ВСЕ 32 ответа решены и сверены с официальной таблицей
(стр.45, столбец Вариант 1) — полное совпадение, включая B9=324, B11=960, B13=460 (пары чисел),
B14=1375 (описанный четырёхугольник, r=60/7). Фигурные A7/A17/B1/B4 реконструированы; B5: в скане
∛(-7) → на деле ∛(-343)=-7 (иначе нецелый), ответ -98. Без авторских ссылок.
VARIANT_LABEL 117 -> 'ЦТ-2021'. DRY-RUN 32/32, self-check и структурный KaTeX — зелёные.
Запись в БД — пользователь: node backend/scripts/seed_ctmath_ct2021_v1.js --apply

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 12:22:42 +03:00
Maxim Dolgolyov 59c691dcfc feat(ctmath): вариант 116 — ЦТ-2020 (32 задания, формат А1-А20)
Пробник ЦТ по математике 2020, Вариант 1. ⚠️ Новый формат: 32 задания (А1–А20 + В1–В12),
В1 на соответствие, В2 множ.выбор. Машинерия параметризована N_TASKS=32. Источник: чистый
PDF ЦТ 2020.pdf. ВСЕ 32 ответа решены и сверены с официальной таблицей (стр.44, столбец
Вариант 1) — полное совпадение, включая A20=37√13/3, B5=-335, B8=-320, B9=160, B10=577,
B11=-16, B12=336 (сфера через 4 точки куба). Фигурные A9/A11 реконструированы; без авторских
ссылок (политика «все учебники наши»). VARIANT_LABEL 116 -> 'ЦТ-2020'.
DRY-RUN 32/32, self-check и структурный KaTeX — зелёные.
Запись в БД — пользователь: node backend/scripts/seed_ctmath_ct2020_v1.js --apply

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 12:09:17 +03:00
Maxim Dolgolyov c0af5502bf chore(textbooks): убрать сторонних авторов — все учебники наши (author=LearnSpace)
Политика «все учебники наши»: нигде не упоминаются сторонние авторы.
- Миграции (15 файлов): колонка author → 'LearnSpace'; из описаний убран оборот
  «по учебнику <автор>:»; авторские фамилии вычищены из комментариев. Покрыты
  Арефьева/Пирютко, Казаков, Латотин/Чеботаревский/Горбунова/Цыбулько, Исаченкова,
  Жилко/Маркович/Сокольский, Герасимов/Лобанов.
- HTML: physics_9_ch5 («по канве учебника Исаченковой» → «по учебной программе»),
  physics_11_hub (hdr-sub с авторами → описание курса), mocks-redesign (карточки-авторы → LearnSpace).
- Генераторы gen_phys9_ch.js/gen_phys11_stubs.js — шаблоны без авторов.
- НОВОЕ: update_textbook_authors.js — идемпотентный апдейтер ЖИВОЙ БД (миграции уже
  применены): author→'LearnSpace' у всех 107 учебников + чистка описаний. DRY-RUN по умолч.

⚠️ Живую БД правит ПОЛЬЗОВАТЕЛЬ: node backend/scripts/update_textbook_authors.js --apply
(в БД сейчас author пуст у всех, видимые упоминания были в описаниях «по учебнику …»).
review_geom10/11.js не тронуты — там фамилии как поисковые шаблоны детектора, не атрибуция.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 11:52:06 +03:00
Maxim Dolgolyov fec638135f chore(ctmath): убрать упоминания сторонних авторов из ссылок-учебников
Поле ref в решениях задач (показывается ученику как «Учебник: …») содержало фамилии
авторов чужих учебников (Арефьева, Казаков, Латотин, Герасимов). Заменено на обобщённые
ссылки нашего курса: «Алгебра, 7 класс, гл. 1» и т.п. (фамилии и кавычки-ёлочки убраны).
452 замены в 15 seed_ctmath_*.js. Синтаксис OK, валидация 30/30.
Применённые варианты (112,113) обновятся при повторном --apply (upsert solution_html).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 11:33:25 +03:00
Maxim Dolgolyov 5881787492 feat(ctmath): вариант 115 — ЦТ-2019 (30 заданий)
Пробник ЦТ по математике 2019, Вариант 1 (А1–А18 + В1–В12; В1 на соответствие, В2 множ.выбор)
для трека exam-prep ctmath. Источник: чистый PDF ЦТ 2019.pdf. Все 30 решены и сверены: столбец
Вариант 1 в таблице (стр.45) затемнён, но читаемые ячейки совпали; методы B5/B6/B7/B11/B12
перекрёстно подтверждены на Варианте 10 (его задания на стр.43-44, ответы читаемы → 81/56/-1071/
624/540 ровно по таблице). Тяжёлые: B7=-264 (период+нечётность), B11=288, B12=110 (т.Гульдина).
Фигурные A1/A7/A9 разрешены по столбцу 1 (D/2√34/2,4); B1/B2 — данные текстом.
VARIANT_LABEL 115 -> 'ЦТ-2019'. DRY-RUN 30/30, self-check и структурный KaTeX — зелёные.
Запись в БД — пользователь: node backend/scripts/seed_ctmath_ct2019_v1.js --apply

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 11:17:46 +03:00
Maxim Dolgolyov 7990b33fd0 feat(ctmath): вариант 114 — ЦТ-2018 (30 заданий)
Пробник ЦТ по математике 2018, Вариант 1 (А1–А18 + В1–В12, В1 на соответствие) для трека
exam-prep ctmath. Источник: чистый PDF ЦТ 2018.pdf. ВСЕ 30 ответов решены и сверены с
официальной таблицей (стр.32, столбец Вариант 1) — полное совпадение, включая B8=-18,
B9=-130 (двугранный угол), B11=32, B12=45 (координатный метод, PT=5/2).
Фигурные/несогласованные (А3 точки, А9 графики→пути, А11 квадраты, В1/В2 функция по узлам,
В8 экв. показательное, В10 множитель (6-x)² по ответу) реконструированы/адаптированы.
VARIANT_LABEL 114 -> 'ЦТ-2018'. DRY-RUN 30/30, self-check и структурный KaTeX — зелёные.
Запись в БД — пользователь: node backend/scripts/seed_ctmath_ct2018_v1.js --apply

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 11:04:12 +03:00
Maxim Dolgolyov c86d5b9ad4 feat(ctmath): вариант 113 — ЦТ-2016 (30 заданий)
Пробник ЦТ по математике 2016, Вариант 1 (А1–А18 + В1–В12) для трека exam-prep ctmath.
Источник: чистый PDF ЦТ 2016.pdf. ВСЕ 30 ответов решены и сверены с официальной таблицей
(стр.35, столбец Вариант 1) — полное совпадение, включая B5=-22, B9=712, B11=56, B12=724.
Фигурные задания (А2 угол через MN||BC, А3 числа на прямой, А6 таблица, А7 площадь по
координатам, А8 область значений, А11 круговая диаграмма) реконструированы/адаптированы
в самодостаточные авто-проверяемые формы. VARIANT_LABEL 113 -> 'ЦТ-2016'.
DRY-RUN 30/30, self-check и структурный KaTeX — зелёные.
Запись в БД — пользователь: node backend/scripts/seed_ctmath_ct2016_v1.js --apply

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 10:37:13 +03:00
Maxim Dolgolyov 7e8082bda6 feat(ctmath): вариант 112 — ЦТ-2015 (30 заданий)
Пробник ЦТ по математике 2015, Вариант 1 (А1–А18 + В1–В12) для трека exam-prep ctmath.
Источник: чистый PDF ЦТ 2015.pdf (10 изоморфных вариантов, таблица ответов стр.35).
Ответы решены и сверены: читаемые ячейки столбца В1 (B2=-15,B3=7,B7=147,B8=-6 совпали)
+ изоморфные варианты 2–10. Фигурные задания (А4 центр.симметрия, А6 множество решений,
А11 таблица-данные, А12 парабола, А15 координаты, В11 лог-выражение) адаптированы в
самодостаточные авто-проверяемые формы с сохранением ответа (как в ЦТ-2014/вар.110).
VARIANT_LABEL 112 -> 'ЦТ-2015'. DRY-RUN 30/30, self-check и структурный KaTeX — зелёные.
Запись в БД — пользователь: node backend/scripts/seed_ctmath_ct2015_v1.js --apply

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 10:24:59 +03:00
Maxim Dolgolyov 2e9a0ebfb1 feat(panel): обновление из репо, обслуживание БД, авто-прунинг, цветные логи и Сторож
- [U] Обновление из репозитория: бэкап -> git pull --ff-only -> npm install -> миграции
  -> рестарт -> health-check; при провале миграций/health предлагает откат (git reset --hard
  + восстановление БД из свежего бэкапа). Текущая версия (git short-hash + subject) в шапке.
- [M] Обслуживание БД: backend/scripts/db-maintain.js (node:sqlite) — integrity_check ->
  WAL checkpoint(TRUNCATE) -> VACUUM; VACUUM пропускается на битой БД. Авто-бэкап + стоп/старт.
- Авто-прунинг бэкапов: Backup-Db хранит последних 10 (Prune-Backups), Copy-DbFrom вынесен
  общим (реюз в Restore-Db и откате обновления), запоминается путь последнего бэкапа.
- Живые логи: отдельный tools/tail-logs.ps1 — раскраска уровней (ERROR/FATAL красным,
  WARN жёлтым, успех зелёным) вместо сырого tail; вынос из inline-команды (PS 5.1 quoting).
- Экран «Сторож»: дашборд в рамке с перерисовкой — статус-маркер, счётчики проверок/
  перезапусков, последнее событие; выход по клавише.
Все .ps1 — UTF-8 BOM, парсинг OK; db-maintain протестирован на копии БД (10.7->10.5 МБ);
рендер-смоук подтвердил выравнивание.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 23:10:52 +03:00
Maxim Dolgolyov 27f51f1a61 style(panel): дашборд с рамкой, цветной статус-маркер, сгруппированное меню
UI консольной панели:
- Закрытая рамка из box-символов (╔═╗║╠╣╚╝) вместо голых ===; правый бордюр выровнен
  на всех строках (хелперы B-Top/B-Mid/B-Bot/B-Line + B-Status с цветным маркером ●).
- Статус-блок: ● зелёный РАБОТАЕТ / серый ОСТАНОВЛЕН, health цветом по состоянию,
  строка БД (размер · юзеры · миграция-номер · бэкапов), Node/JWT/LLM/время обновления, URL.
- Меню в две выровненные колонки СЕРВЕР | ОБСЛУЖИВАНИЕ (ключи голубые, подписи серые),
  отдельная строка ДИАГНОСТИКА; промпт с ▶.
Чистый рефактор отрисовки — логика switch/функции не тронуты. UTF-8 BOM, парсинг OK,
рендер-смоук показал ровное выравнивание.

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