Files
magistr/docs/REACT_MIGRATION.md
dipatrik10 97bf3cc50a feat: завершить миграцию фронтенда на React (этапы 1-5)
Админка, настройки, логин, кабинеты преподавателя и студента
полностью переведены на React 19 (модель strangler, docs/REACT_MIGRATION.md).

- 15 вкладок админки: lazy-загрузка, общий error boundary, сервисный слой
- Удалены legacy-views и переходный слой LegacyTabView (45 файлов)
- Сборка esbuild (frontend/scripts/build-react.mjs) с бюджетами gzip на чанки
- CSP-хэши, кэширование: chunk'и immutable, CSS/HTML no-cache (react-cache.conf)
- Фиксы: lazy-импорт именованных экспортов (краш после авторизации),
  растяжение #admin-root/#settings-root на всю ширину,
  возврат subjects/lessonTypes из справочников (краш просмотра расписаний),
  отказ от заведомо широкого поиска расписания без выбранной цели,
  центрирование карточки логина
- Тесты: 237/237 (npm run check), регрессионные на lazy-вкладки и ширину
- Документация: REACT_MIGRATION.md, REACT_MIGRATION_RESULT.md, FRONTEND.md
2026-09-28 23:15:04 +03:00

61 KiB
Raw Blame History

План перехода frontend на React

Зачем выполняется миграция

Цель перехода — снизить сложность развития интерфейса, сделать состояние страниц явным, переиспользовать компоненты и тестировать пользовательские сценарии изолированно. Сам React не гарантирует меньший размер страницы: вес, время загрузки и объём запросов контролируются отдельными бюджетами.

Миграция выполняется постепенно по модели «strangler»: React и существующие ES-модули временно работают рядом, а переключение происходит по целым страницам или независимым разделам. Большой одномоментный rewrite не планируется.

Обязательные ограничения

  • Сохраняются текущие URL, REST API и серверная ролевая модель.
  • Access JWT остаётся только в памяти, refresh-токен — в HttpOnly cookie.
  • Нельзя ослаблять CSP: удалённые и inline-скрипты по-прежнему запрещены.
  • UI, ошибки и пользовательские сообщения остаются на русском языке.
  • Общие CSS-токены и светлая/тёмная темы сохраняются до отдельного решения о редизайне.
  • Каждый перенесённый срез должен иметь тесты, production-сборку и измеримый бюджет bundle.
  • Vanilla-код удаляется только после переключения соответствующего маршрута на React.

Целевая схема

frontend/
├── react/
│   ├── shared/              # Общие React-компоненты, auth/api и состояния загрузки
│   ├── login/               # Страница входа
│   ├── student/             # Кабинет студента
│   ├── teacher/             # Кабинет преподавателя
│   └── admin/               # Shell, маршруты и feature-модули админ-панели
├── dist/react/              # Скомпилированные production-bundle, не хранятся в Git
├── admin/, student/, ...    # Legacy-срезы до завершения их миграции
└── scripts/build-react.mjs  # Сборка React через esbuild

На переходном этапе Apache продолжает раздавать статические файлы и проксировать /api. React-bundle собираются в существующем Node-этапе Docker-образа. После появления нескольких React entrypoint сборка должна выделить общий runtime в кэшируемый chunk, а функциональные разделы — загружать лениво.

Этапы

Этап Содержание Критерий завершения Статус
0. Инвентаризация Карта entrypoint, маршрутов, общих модулей и рисков Выбран независимый первый срез; зафиксированы ограничения Завершён
1. Основа + вход React/React DOM, esbuild entrypoint, Docker, перенос /, тест API-контракта и бюджет bundle / работает на React без изменения auth-контракта; npm run check проходит Завершён
2. Общий React-слой react/shared: auth/api adapters, состояния loading/empty/error, Error Boundary; общий runtime chunk; DOM-тесты Следующие страницы не дублируют инфраструктурный код Завершён
3. Автономные кабинеты Сначала /student/, затем /teacher/; компоненты расписания и навигации Оба URL полностью работают на React, legacy app.js удалены Завершён: /student/ и /teacher/ перенесены
4. Настройки React-shell /admin/settings/, затем вкладки general, time-slots, edu-forms, database Роли и hash-навигация совпадают с текущим SPA Завершён: /admin/settings/ перенесён
5. Основная админ-панель Shell и hash-router, затем перенос feature-модулей волнами Все роли и вкладки работают на React, HTML-фрагменты больше не загружаются через fetch Завершён: shell, hash-router и все 15 вкладок (волны 1–6) перенесены, переходный LegacyTabView/legacy-views.js удалён
6. Очистка и оптимизация Удаление legacy-loader, дедупликация CSS, lazy loading, cache/compression, финальные performance budgets Нет исполняемого legacy UI-кода; контрольные метрики не хуже baseline Не начат

Очерёдность админских feature-модулей

  1. Простые справочники: classrooms, subjects, dashboard.
  2. Реестры и формы: users, groups, university-structure.
  3. Ролевые рабочие места: teacher-requests, department-workspace.
  4. Календарь и нагрузка: academic-calendar, auditorium-workload.
  5. Контур публикации: schedule-view, schedule-versions, schedule-quality, teacher-absences.
  6. Последними — schedule и schedule-override-panel, потому что у них больше всего связей, локального состояния и бизнес-ограничений.

Стратегия проверки

  • На каждом этапе: npm run build, npm run check и smoke-проверка Docker-образа.
  • Для чистой логики сохраняются быстрые тесты на node:test.
  • На этапе 2 добавляются DOM/component-тесты для форм, ошибок, клавиатурной навигации и ролей.
  • Перед удалением legacy-среза сравниваются URL, payload запросов, редиректы и доступность.
  • Для React entrypoint действует gzip-бюджет. Первый срез / ограничен 75 000 байт gzip; следующие feature-chunk получают отдельные лимиты после выделения общего runtime.
  • Для тяжёлых разделов измеряются число запросов, transferred bytes и время до готовности интерфейса; миграция не считается оптимизацией без улучшения или сохранения этих метрик.

Риски и меры

Риск Мера
Рост bundle из-за React runtime Общий кэшируемый chunk, lazy loading, лимит сборки
Дублирование auth/api логики Завершить этап 2 до переноса следующей страницы
Рассинхрон ролей admin/settings Использовать текущую role-capabilities.js как единый контракт до её переноса
Поломка CSP Проверять .jsx теми же security-тестами, что .js и .html
Два источника истины в одном экране Переключать целый маршрут/вкладку, не смешивать управление одним DOM-деревом
Незаметная деградация производительности Фиксировать raw/gzip размер и браузерные метрики на каждом этапе

Точка остановки

Дата: 28 сентября 2026 года
Остановились после этапа 5, волны 1 — вкладки dashboard, classrooms, subjects основной админ-панели переведены на React-компоненты с lazy-загрузкой.

Выполнено на этапе 2 (общий слой):

  • react/shared: apiClient/requestJson, auth-client + session-policy, useAuthorizedSession, AppErrorBoundary, AsyncState, FormAlert;
  • общий кэшируемый runtime-chunk dist/react/shared/chunks/ (esbuild splitting), Apache отдаёт chunk с immutable, entrypoint — no-cache;
  • DOM-проверки через renderToStaticMarkup, бюджеты сборки.

Выполнено на этапе 3 (кабинет студента):

  • react/student/: StudentApp (сессия через useAuthorizedSession(['STUDENT']), выбор группы, навигация по неделям, состояния loading/empty/error с повтором), WeekGrid, ScheduleOverview, student-service (дата-математика, чипы, контракты API);
  • React-версия поискового списка AsyncCombobox вынесена в react/shared/ui/ с той же семантикой ARIA, debounce 320 мс и клавиатурной навигацией, что и vanilla-версия;
  • модель обзора «Сегодня / Следующая пара» переиспользует чистые функции schedule-overview.js (нужные помощники экспортированы аддитивно);
  • контракты сохранены: GET /api/groups/options, GET /api/schedule, localStorage.studentGroupId, query-параметры studentGroup/studentDate, telemetry-события student_*, logout через общий logoutAndRedirect;
  • student/index.html монтирует /react/student.js, legacy student/app.js удалён;
  • бюджеты: entrypoint студента 5 819 байт gzip (лимит 20 000), холодная загрузка 78 309 байт gzip (лимит 90 000), общие chunk 72 490 байт gzip (лимит 75 000);
  • npm run check: 124 теста пройдено (13 новых для кабинета студента).

Выполнено на этапе 3 (кабинет преподавателя):

  • react/teacher/: TeacherApp (сессия через useAuthorizedSession(['TEACHER']), вкладки расписания/пожеланий/заявок, навигация по неделям), TeacherWeekGrid (матрица слотов на десктопе и дневной список на мобильных через matchMedia), AbsencePanel, PreferencesTab, RequestsTab, RequestDialog, teacher-service (слоты, матрица недели, метки статусов, payload заявок, контракты API);
  • дата-математика выделена в react/shared/dates.js (единый источник для студента и преподавателя, student-service реэкспортирует), ScheduleOverview перенесён в react/shared/ui/ и переиспользуется обоими кабинетами с ролью student/teacher;
  • контракты сохранены: GET /api/schedule?teacherId=, GET|POST|DELETE /api/teacher-preferences, GET|POST|DELETE /api/teacher-change-requests, GET /api/teacher-change-requests/candidates, GET|POST|DELETE /api/teacher-absences, query-параметры teacherTab/teacherDate, telemetry-события teacher_*;
  • teacher/index.html монтирует /react/teacher.js, legacy teacher/app.js удалён;
  • бюджеты: entrypoint преподавателя 10 095 байт gzip (лимит 20 000), холодная загрузка 84 590 байт gzip (лимит 90 000), общие chunk 74 495 байт gzip (лимит 75 000);
  • npm run check: 146 тестов пройдено (22 новых для кабинета преподавателя).

Выполнено на этапе 4 (страница настроек):

  • react/settings/: SettingsApp (сессия через useAuthorizedSession(SETTINGS_ROLES), боковая панель с фильтрацией вкладок по role-capabilities.js, сворачивание панели с сохранением в localStorage.sidebar-collapsed, тема в React вместо theme-toggle.js), вкладки GeneralTab, TimeSlotsTab, EduFormsTab, DatabaseTab, settings-service (метки сеток времени, валидация слота/тенанта, контракты API);
  • общий ConfirmDialog в react/shared/ui/ переиспользует классы .project-dialog* и закрывается по Escape/клику по фону;
  • роли и навигация совпадают с legacy-SPA: у настроек не было hash-навигации, активная вкладка живёт в состоянии React, как раньше — в памяти страницы; ADMIN видит все четыре вкладки (по умолчанию general), EDUCATION_OFFICE — time-slots и edu-forms (по умолчанию time-slots);
  • inline-стили legacy (database.html, database.js) заменены классами .db-status-grid, .form-group-wide, .form-row-actions в admin/settings/css/main.css, CSP-хэши style-src-attr в security.conf пересобраны по фактическим атрибутам;
  • контракты сохранены: GET|POST|DELETE /api/admin/time-slots/scopes, GET|POST|PUT|DELETE /api/admin/time-slots, GET|POST|DELETE /api/education-forms, GET /api/database/status, GET|POST|DELETE /api/database/tenants, POST /api/database/test;
  • admin/settings/index.html монтирует /react/settings.js, legacy admin/settings/js/ и admin/settings/views/ удалены;
  • бюджеты: entrypoint настроек 9 172 байта gzip (лимит 20 000), холодная загрузка 83 667 байт gzip (лимит 90 000), общие chunk 74 495 байт gzip (лимит 75 000);
  • npm run check: 159 тестов пройдено (13 новых для страницы настроек).

Выполнено на этапе 5 (срез 1 — React-shell основной админ-панели):

  • react/admin/: AdminApp (сессия через useAuthorizedSession(ADMIN_APP_ROLES)) и AdminWorkspace (sidebar с секциями и фильтрацией по role-capabilities.js, topbar с заголовком вкладки и счётчиком заявок, тема в React вместо theme-toggle.js, сворачивание панели localStorage.sidebar-collapsed, настройки с выходом);
  • hash-router в React: чтение location.hash при старте, слушатель hashchange, переключение через requestTabSwitch с guard'ом confirmAbandonDirtyChanges (единый модуль admin/js/dirty-state.js для React и legacy-вкладок);
  • счётчик ожидающих заявок (только ADMIN): GET /api/teacher-requests/page?status=PENDING&page=0&size=1, обновление по событию teacherRequestsChanged, склонение «заявка/заявки/заявок», бейдж в навигации и заголовке;
  • переходный слой LegacyTabView + legacy-views.js (удалён после волны 6): ещё не перенесённые вкладки загружались как lazy-chunkи (fetch HTML-фрагмента
    • init*() legacy-модуля), каждая вьюха целиком владела своим DOM-поддеревом — React и vanilla не смешивались в одном дереве; после переноса вкладки её загрузчик и legacy-файл удалялись;
  • глобальные утилиты legacy (applyRippleEffect, closeAllDropdownsOnOutsideClick, initAllCustomDropdowns, startDropdownAutoObserver) вызываются оболочкой один раз;
  • admin/index.html монтирует /react/admin.js, legacy admin/js/main.js удалён;
  • бюджеты: entrypoint админ-панели 5 399 байт gzip (лимит 20 000), холодная загрузка 79 977 байт gzip (лимит 90 000), общие chunk 74 578 байт gzip (лимит 75 000), переходные lazy-вкладки 109 512 байт gzip (лимит 150 000, загружаются по одной);
  • npm run check: 170 тестов пройдено (10 новых для оболочки админки).

Выполнено на этапе 5 (волна 1 — dashboard, classrooms, subjects):

  • react/admin/react-tabs.js — реестр перенесённых вкладок: каждая вкладка загружается отдельным lazy-chunk через React.lazy + Suspense (fallback — tab-loading); вкладки без записи в реестре едут через LegacyTabView;
  • react/admin/tabs/DashboardTab.jsx: метрики, мониторинг текущей пары (обновление каждые 30 с), свободные аудитории, быстрый переход, Red Zone; чистая логика выявления конфликтов (накладки преподавателей/аудиторий, превышение вместимости) перенесена в dashboard-service.js (detectScheduleConflicts возвращает сегменты с выделением вместо HTML-строк), презентации состояния — из общего dashboard-conflicts.js;
  • react/admin/tabs/ClassroomsTab.jsx: форма создания с React-мультиселектом оборудования (замена initMultiSelect), таблица с переключением статуса, модалки редактирования аудитории и справочника оборудования;
  • react/admin/tabs/SubjectsTab.jsx: реестр с поиском (debounce 320 мс), фильтром кафедры, сортировкой, пагинацией (общий react/shared/ui/Pagination.jsx), сохранением состояния в URL (subjectQuery/subjectDepartment/subjectPage/ subjectSize/subjectSort), модалка управления преподавателями с AsyncCombobox; контракты GET /api/subjects/page, GET|POST|DELETE /api/teacher-subjects сохранены;
  • legacy-файлы вкладок удалены (admin/js/views/{dashboard,classrooms,subjects}.js, admin/views/{dashboard,classrooms,subjects}.html), загрузчики убраны из legacy-views.js, у маршрутов перенесённых вкладок в admin-service.js больше нет viewFile;
  • inline-стили вкладок заменены классами в admin/css/components.css (дашборд: метрики/Red Zone/свободные аудитории; общие: card-toolbar, form-row--*, modal-content--medium, th-actions, badge-list и др.); CSP-хэши style-src-attr в security.conf пересчитаны (50 → 22);
  • классификация chunk'ов в build-react.mjs переведена на достижимость от entrypoint'ов: chunk, достижимый только из dist/react/admin.js, — lazy-вкладка (в общий runtime-бюджет не входит), даже если внутри есть модуль react/shared (например, Pagination внутри chunk'а SubjectsTab);
  • бюджеты: entrypoint админ-панели 5 530 байт gzip (лимит 20 000), холодная загрузка 83 877 байт gzip (лимит 90 000), общие chunk 78 347 байт gzip (лимит поднят 75 000 → 85 000: ConfirmDialog и AsyncCombobox стали общими chunk'ами — их теперь используют и React-вкладки админки; лимит холодной загрузки входа поднят 80 000 → 85 000 по той же причине), lazy-вкладки 112 753 байта gzip в 26 chunk'ах (лимит 150 000, загружаются по одной);
  • npm run check: 182 теста пройдено (12 новых: react-admin-tabs.test.mjs).

Выполнено на этапе 5 (волна 2 — users, groups, university-structure):

  • react/admin/tabs/UsersTab.jsx: форма создания пользователя (пароль — type="password" + autoComplete="new-password"), реестр с поиском (debounce 320 мс), фильтром роли, сортировкой и пагинацией, состояние в URL (userQuery/userRole/userPage/userSize/userSort), архивирование через ConfirmDialog и восстановление; контракты GET /api/users/page, POST /api/users, DELETE /api/users/{id}, POST /api/users/{id}/restore сохранены; чистая логика — users-service.js;
  • react/admin/tabs/GroupsTab.jsx: форма создания с каскадом специальность → профиль, реестр с мультиселектом форм обучения (React-фильтр с чекбоксами «Все» + формы, как EquipmentMultiSelect), поиском, сортировкой, сбросом фильтров и пагинацией, состояние в URL (включая массив groupEducationForm); модалки редактирования группы, подгрупп (режимы 0/2/3, подсказанные вместимости, валидация суммы, legacy-порядок синхронизации: лишние удаляются, затем обновления — сначала уменьшающие вместимость, затем создания) и календарных графиков (учебный год + AsyncCombobox совместимых графиков, список назначений с дисциплинами по семестрам); контракты GET /api/groups/page, POST|PUT|DELETE /api/groups..., /subgroups, /calendar-assignments, GET /api/admin/academic-calendars/options сохранены; чистая логика — groups-service.js (заголовки графиков — общий admin/js/views/academic-calendar-title.js);
  • react/admin/tabs/UniversityStructureTab.jsx: внутренние разделы кафедр/специальностей/профилей, формы создания и редактирования (модалки кафедр и специальностей, форма профиля на вкладке и в модалке специальности), клиентская фильтрация и пагинация списков как в legacy listPage (поиск с debounce 250 мс, toLocaleLowerCase('ru')); контракты GET|POST|PUT|DELETE /api/departments, /api/specialties..., /api/specialties/profiles сохранены; чистая логика — university-structure-service.js;
  • legacy-файлы вкладок удалены (admin/js/views/{users,groups, university-structure}.js, admin/views/{users,groups, university-structure}.html), загрузчики убраны из legacy-views.js, у маршрутов в admin-service.js больше нет viewFile;
  • inline-стили заменены классами в admin/css/components.css (modal-content--wide/--xwide/--full, profile-form-*, form-actions-row); CSP-хэши style-src-attr пересчитаны (22 → 6);
  • бюджеты: entrypoint админ-панели 5 508 байт gzip (лимит 20 000), холодная загрузка 83 855 байт gzip (лимит 90 000), общие chunk 78 347 байт gzip (лимит 85 000), lazy-вкладки 118 009 байт gzip в 26 chunk'ах (лимит 150 000, загружаются по одной) — лимиты не поднимались;
  • npm run check: 195 тестов пройдено (13 новых: контракты, валидация подгрупп, фильтрация списков, DOM-рендеры вкладок волны 2).

Выполнено на этапе 5 (волна 3 — teacher-requests, department-workspace):

  • react/admin/tabs/TeacherRequestsTab.jsx: реестр заявок с поиском (debounce 320 мс), фильтром статуса (по умолчанию PENDING), пагинацией и состоянием в URL (requestQuery/requestStatus/requestPage/requestSize); редактируемые строки (кафедра, логин, ФИО, должность, пароль — type="password" + autoComplete="new-password"), одобрение с валидацией legacy (все поля + пароль от 8 символов), отклонение через новый общий PromptDialog (реплика promptAction: обязательная причина, ошибка внутри диалога); событие teacherRequestsChanged для счётчика sidebar диспетчируется как в legacy (count актуален только для нефильтрованного списка PENDING); контракты GET /api/teacher-requests/page, POST /api/teacher-requests/{id}/approve|reject сохранены; чистая логика — teacher-requests-service.js;
  • react/shared/ui/ConfirmDialog.jsx: добавлен PromptDialog — реплика legacy promptAction из admin/js/dialog.js (разметка project-dialog*, обязательное поле с requiredMessage, Escape/клик по фону закрывают);
  • react/admin/tabs/DepartmentWorkspaceTab.jsx: карточка управления (кафедра — скрыта для роли DEPARTMENT, период, обновление), загрузка дисциплин строками (добавить/удалить, валидация legacy), привязка преподавателя (доступные = все минус привязанные), заявка на преподавателя, сводка «Преподаватели и нагрузка» (сопоставление по id: и нормализованному имени, непривязанная нагрузка — «Есть занятия в расписании кафедры», как в legacy), таблицы заявок и дисциплин кафедры, комментарии к дисциплинам (карточка открывается по кнопке «Открыть»); период по умолчанию — из справочника семестров (defaultSchedulePeriod) с fallback «сегодня + 7 дней»; сессия — getSession() из admin/js/api.js, как в legacy; контракты /api/department/*, /api/workload/teachers, /api/schedule/semesters, /api/users/teachers сохранены; чистая логика — department-workspace-service.js;
  • legacy-файлы вкладок удалены (admin/js/views/{teacher-requests, department-workspace}.js, admin/views/{teacher-requests, department-workspace}.html), загрузчики убраны из legacy-views.js, у маршрутов в admin-service.js больше нет viewFile;
  • inline-стиль style="margin:0" из legacy HTML заменён классом schedule-section-label--flush в admin/css/components.css; CSP-хэши style-src-attr пересчитаны (6 → 5);
  • бюджеты: entrypoint админ-панели 4 371 байт gzip (лимит 20 000), холодная загрузка 83 268 байт gzip (лимит 90 000), общие chunk 78 897 байт gzip (лимит 85 000; PromptDialog добавил ~550 байт gzip), lazy-вкладки 121 368 байт gzip в 27 chunk'ах (лимит 150 000, загружаются по одной) — лимиты не поднимались;
  • npm run check: 205 тестов пройдено (10 новых: состояние и контракты заявок, payload-валидация кабинета кафедры, сопоставление нагрузки, DOM-рендеры вкладок волны 3).

Выполнено на этапе 5 (волна 4, срез 1 — auditorium-workload):

  • react/admin/tabs/AuditoriumWorkloadTab.jsx: переключатель «Что смотреть» (аудитории/преподаватели/кафедры) и селект «Отображение» (все/конкретная сущность), сводная таблица занятости на дату (ось — сущности, колонки — эффективные слоты, карточки занятий, «Свободно»), фильтры аудиторий (корпус/мин. вместимость/оборудование — React-мультиселекты, применяются к уже загруженным данным без перезапроса), двухнедельная сводка по выбранной сущности (чёт/нечёт недели в ячейках одного дня, сводка «Занято: N из M», предупреждения об ошибках загрузки и датах вне семестра); сводный текст «Дата: …, Занятых слотов: …» воспроизводит legacy renderSummary (включая «выбран один объект» и счётчик ошибок групп); при сбое части групп/сеток показываются доступные данные с предупреждением (legacy allSettled-стратегия);
  • react/admin/auditorium-workload-service.js: контракты GET /api/classrooms, /api/equipments, /api/groups, /api/admin/calendar/years (сбой молча даёт []), /api/users/teachers, /api/departments, /api/admin/time-slots/effective?date=, /api/schedule?groupId=&startDate=&endDate=; чистая логика: дедупликация занятий (uniqueLessons с мерджем групп/подгрупп), группировка по сущность×слот и дата×слот, сопоставление преподаватель→кафедра (departmentId, fallback по названию), двухнедельный период с понедельника, номера учебных недель и чётность (паритет занятия перекрывает расчёт по семестру, вне семестра — fallback по индексу даты), строки слотов сводной недели, состояния ячеек (busy/free/missing/empty) и сравнение наборов занятий подписью;
  • legacy-файлы вкладки удалены (admin/js/views/auditorium-workload.js, admin/views/auditorium-workload.html), загрузчик убран из legacy-views.js, у маршрута в admin-service.js больше нет viewFile;
  • inline-стили legacy HTML (отступы заголовка/фильтров, ширина поля вместимости, дата) заменены классами workload-card-header, workload-filter-row (дополнен), workload-min-capacity, workload-date-field в admin/css/auditorium-workload.css; CSP-хэши style-src-attr пересчитаны (5 → 1: в корпусе остался один inline-стиль в schedule-view.js);
  • бюджеты: entrypoint админ-панели 4 372 байта gzip (лимит 20 000), холодная загрузка 83 266 байт gzip (лимит 90 000), общие chunk 78 894 байта gzip (лимит 85 000), lazy-вкладки 122 252 байта gzip в 27 chunk'ах (лимит 150 000, загружаются по одной) — лимиты не поднимались;
  • npm run check: 212 тестов пройдено (7 новых: период и чётность недель, дедупликация/группировка занятий, фильтры и опции, контракты API, сводки и счётчики, DOM-рендер вкладки).

Следующее действие при продолжении: этап 5, волна 5 — контур публикации (schedule-view, schedule-versions, schedule-quality, teacher-absences) по той же схеме: выделить чистую логику, переиспользовать react/shared, заменить загрузчик в legacy-views.js на запись в react-tabs.js, удалить legacy-файл вкладки. Последними (волна 6) — schedule + schedule-override-panel.

Выполнено на этапе 5 (волна 4, срез 2 — academic-calendar):

  • react/admin/tabs/AcademicCalendarTab.jsx: три внутренних раздела (Графики/Сетки/Дисциплины) с подтверждением ухода при несохранённых изменениях; раздел «Графики»: таблица учебных годов (редактирование года и отдельного семестра, удаление через ConfirmDialog), форма календарного графика (год, специальность → профиль, форма обучения, число курсов), реестр графиков (поиск с debounce 300 мс, фильтр по году, пагинация, кнопки «Изменить»/«Сетка»/«Дисциплины»/«Удалить»), конструктор учебного года (ввод года начала автоматически считает год окончания и даты обоих семестров, 4 поля дат с маской); раздел «Сетки»: выбор графика через AsyncCombobox, загрузка/сохранение сетки, заполнение диапазона (курс/даты/код активности), чипы итогов с маркером выделения, блоки курсов с таблицами семестров, диалог ячейки (одно/множественное выделение, код активности на всё выделение, информационная временная сетка, «Убрать ручную сетку»), drag-выделение ячеек и подсказка через портал в document.body с позиционированием el.animate (реплика legacy, без inline-стилей); раздел «Дисциплины»: выбор графика, семестр/дисциплина, привязки по семестрам чипами с удалением, добавление/сохранение с dirty-индикатором «•»;
  • react/admin/academic-calendar-service.js: контракты GET /api/admin/academic-calendars/page|options, GET|PUT /api/admin/academic-calendars/{id}/grid|subjects, POST|PUT|DELETE /api/admin/calendar/years, DELETE /api/admin/academic-calendars/{id}, /api/specialties/{id}/profiles, /api/admin/time-slots/*; чистая логика: расчёт семестров учебного года и валидация пересечений, построение сетки по датам учебного года, блоки семестров, итоги по кодам активностей, тексты диалога/подсказки, расчёт позиции подсказки; заголовки графиков — общий admin/js/views/academic-calendar-title.js (как в groups-service);
  • MaskedDateInput внутри вкладки — React-реплика bindDateInputMask из admin/js/date-input.js (форматирование при вводе, нормализация и aria-invalid при потере фокуса), переиспользует чистые функции formatDateInputValue/dateInputValueToIso/isoDateToInputValue;
  • мёртвый код legacy не переносился: «Применить» в диалоге ячейки только закрывает окно, выбор временной сетки информационный (задокументировано в FRONTEND.md), «Убрать ручную сетку» реально удаляет назначение;
  • legacy-файлы вкладки удалены (admin/js/views/academic-calendar.js, admin/views/academic-calendar.html; общие хелперы academic-calendar-title.js/academic-calendar-grid.js сохранены — используются React-сервисом), загрузчик убран из legacy-views.js, у маршрута в admin-service.js больше нет viewFile;
  • inline-стилей в legacy HTML вкладки не было — CSP-хэши style-src-attr не изменились (1 хэш, inline-стиль в schedule-view.js);
  • бюджеты: entrypoint админ-панели 4 375 байт gzip (лимит 20 000), холодная загрузка 83 848 байт gzip (лимит 90 000), общие chunk 79 473 байта gzip (лимит 85 000), lazy-вкладки 124 206 байт gzip в 26 chunk'ах (лимит 150 000, загружаются по одной) — лимиты не поднимались;
  • npm run check: 217 тестов пройдено (5 новых: контракты API и эндпоинты, ввод года и чтение дат, подписи и сортировки, построение сетки и тексты диалога/подсказки, DOM-рендер вкладки).

Следующее действие при продолжении: этап 5, волна 5 — контур публикации (schedule-view, schedule-versions, schedule-quality, teacher-absences) по той же схеме: выделить чистую логику, переиспользовать react/shared, заменить загрузчик в legacy-views.js на запись в react-tabs.js, удалить legacy-файл вкладки. Последними (волна 6) — schedule + schedule-override-panel.

Выполнено на этапе 5 (волна 5, срез 1 — schedule-view):

  • react/admin/tabs/ScheduleViewTab.jsx: карточка параметров (цель просмотра — для DEPARTMENT заблокирована на «Кафедра», AsyncCombobox группы/ преподавателя/аудитории, обычный select кафедры, скрытое поле даты, дополнительные фильтры с каскадом «учебный год → семестр», дисциплина, тип занятия, чётность), карточка результатов (счётчик, чипы разделов, кнопки предыдущего/следующего расписания, кнопка реестра правок) и одна активная таблица (семестровая матрица, совмещённая двухнедельная таблица выбранной сущности, мобильный блок одного дня с переключателем дней); состояние таблиц выводится производно из sections/activeSectionIndex/range/isMobile, пустое состояние с подсказкой содержит собственную кнопку «Показать расписание»;
  • schedule-override-panel.js (634 строки) сохранён как общий хелпер волны 6: React-вкладка рендерит drawer как статичный JSX (все schedule-override-* id сохранены) и вызывает createScheduleOverridePanel в эффекте после загрузки справочников; колбэки панели (loadData, getRange) читают только latestRef/rangeRef, поэтому захваченные панелью замыкания не протухают;
  • react/admin/schedule-view-service.js: контракты GET /api/departments, /api/subjects, /api/lesson-types, /api/schedule/semesters, /api/schedule/search, /api/edu-office/schedule/overrides, /api/groups/options, /api/users/teachers/options, /api/classrooms/options; чистая логика: разделы по цели просмотра (выбранная группа собирает все занятия в один раздел — фильтрует сервер), семестровая матрица с чётностью и границами недель, совмещённые двухнедельные блоки, подписи недель из академических часов, URL-состояние фильтров (кафедра не сохраняет цель — она зафиксирована ролью);
  • прелоад трёх комбобоксов на монтировании воспроизводит legacy combo.preload(''): справочники сущностей наполняются заранее, подпись восстановленного из URL значения обновляется после загрузки опций;
  • legacy-файлы вкладки удалены (admin/js/views/schedule-view.js, admin/views/schedule-view.html), загрузчик убран из legacy-views.js, у маршрута в admin-service.js больше нет viewFile;
  • inline-стиль legacy (flex в заголовке карточки занятия) оказался избыточным — правило уже есть в классе .schedule-view-lesson-title (auditorium-workload.css); CSP-хэши style-src-attr стали пустыми (1 → 0), директива unsafe-hashes сохранена;
  • мёртвый код legacy не переносился: кнопок периода prev-period/today/ next-period в разметке не было (в legacy они были null);
  • бюджеты: entrypoint админ-панели 4 383 байта gzip (лимит 20 000), холодная загрузка 83 856 байт gzip (лимит 90 000), общие chunk 79 473 байта gzip (лимит 85 000), lazy-вкладки 126 170 байт gzip в 26 chunk'ах (лимит 150 000, загружаются по одной) — лимиты не поднимались;
  • npm run check: 224 теста пройдено (7 новых: контракты API просмотра, разделы по цели, двухнедельная сетка и матрица семестра, подписи недель, URL-состояние, пополнение справочников, DOM-рендер вкладки).

Следующее действие при продолжении: этап 5, волна 5, срезы 2–4 — schedule-versions, schedule-quality, teacher-absences по той же схеме: выделить чистую логику, переиспользовать react/shared, заменить загрузчик в legacy-views.js на запись в react-tabs.js, удалить legacy-файл вкладки. Последними (волна 6) — schedule + schedule-override-panel.

Выполнено на этапе 5 (волна 5, срез 2 — schedule-versions):

  • react/admin/tabs/ScheduleVersionsTab.jsx: hero с каскадом «учебный год → семестр» (восстановление из localStorage-ключей magistr.schedule.*), статусная строка с data-tone (loading/success/error), карточка опубликованной версии (LIVE/ПУСТО), панель черновиков со счётчиком, панель сравнения (Δ / загрузка / ошибка / сводка «Что изменится после публикации» с карточками правил), архив публикаций, журнал действий и модальное окно операций (создание черновика, публикация со сводкой валидации, восстановление) с закрытием по Escape/фону и блокировкой отправки при непройденной проверке;
  • переходы «Редактировать»/«Качество» передают контекст через localStorage (magistr.schedule.openVersionId, magistr.quality.*) и переключают вкладку через location.hash — hash-router AdminApp проводит переход через guard несохранённых изменений, как клик по пункту меню в legacy;
  • react/admin/schedule-versions-service.js: контракты GET /api/schedule/semesters, /api/edu-office/schedule/versions?semesterId=, .../history?semesterId=, .../{id}/diff, .../{id}/validate, POST /api/edu-office/schedule/versions (черновик), POST .../{id}/publish|restore (причина); чистая логика: группировка версий (published/drafts/archive), подписи действий журнала и типов изменений, основа черновика, сводка валидации перед публикацией, формат даты-времени; localStorage-ключи экспортированы константами для волны 6;
  • до первой загрузки панели пусты, как в legacy HTML (пустые блоки «Нет активных черновиков» и т. п. появляются только после успешной загрузки); мёртвый CSS version-modal-summary[data-tone] не поддержан (в legacy атрибут никогда не ставился);
  • legacy-файлы вкладки удалены (admin/js/views/schedule-versions.js, admin/views/schedule-versions.html), загрузчик убран из legacy-views.js, у маршрута в admin-service.js больше нет viewFile;
  • inline-стилей в legacy HTML вкладки не было — CSP-хэши style-src-attr остались пустыми;
  • бюджеты: entrypoint админ-панели 4 378 байт gzip (лимит 20 000), холодная загрузка 83 851 байт gzip (лимит 90 000), общие chunk 79 473 байта gzip (лимит 85 000), lazy-вкладки 127 193 байта gzip в 26 chunk'ах (лимит 150 000, загружаются по одной) — лимиты не поднимались;
  • npm run check: 227 тестов пройдено (3 новых: контракты API контура публикаций, группировка версий и подписи, DOM-рендер вкладки).

Следующее действие при продолжении: этап 5, волна 5, срезы 3–4 — schedule-quality, teacher-absences по той же схеме: выделить чистую логику, переиспользовать react/shared, заменить загрузчик в legacy-views.js на запись в react-tabs.js, удалить legacy-файл вкладки. Последними (волна 6) — schedule + schedule-override-panel.

Выполнено на этапе 5 (волна 5, срез 3 — schedule-quality):

  • react/admin/tabs/ScheduleQualityTab.jsx: каскад «учебный год → семестр → версия» (восстановление из localStorage-ключей magistr.quality.*, версия по умолчанию — сохранённая, иначе опубликованная, иначе первая), статусная строка с data-tone (включая warning), кнопка анализа с состоянием «Анализируем…», скорборд с SVG-кольцом (stroke-dasharray по клампу счёта, data-band), метрики, карта проблем с фильтрами важности/категории и сортировкой по штрафу, панель выбранного правила с переходом в конструктор;
  • блок результатов скрывается на время анализа и остаётся скрытым при ошибке (как hidden в legacy), пустые блоки фильтра — только после успешного анализа;
  • переход «Открыть правило в конструкторе» передаёт контекст через localStorage (magistr.schedule.semesterId, openVersionId, openRuleId) и переключает вкладку через location.hash;
  • react/admin/schedule-quality-service.js: контракт GET /api/edu-office/schedule/quality?semesterId=&versionId=; чистая логика: диапазоны оценки, фильтрация и сортировка проблем по категориям (окна/нагрузка/аудитории/пожелания), подписи статусов версий и важности, формат метрик и дат, склонение «занятие/занятия/занятий», сводка «Показаны первые N из M правил»; семестры и версии переиспользуют контракты schedule-versions-service.js;
  • legacy-файлы вкладки удалены (admin/js/views/schedule-quality.js, admin/views/schedule-quality.html), загрузчик убран из legacy-views.js, у маршрута в admin-service.js больше нет viewFile;
  • бюджеты: entrypoint админ-панели 4 357 байт gzip (лимит 20 000), холодная загрузка 83 830 байт gzip (лимит 90 000), общие chunk 79 473 байта gzip (лимит 85 000), lazy-вкладки 130 276 байт gzip в 26 chunk'ах (лимит 150 000, загружаются по одной) — лимиты не поднимались;
  • npm run check: 233 теста пройдено (3 новых: контракт API анализа, подписи и диапазоны, DOM-рендер вкладки).

Выполнено на этапе 5 (волна 5, срез 4 — teacher-absences):

  • react/admin/tabs/TeacherAbsencesTab.jsx: сводка статистики, форма регистрации отсутствия (React-AsyncCombobox поиска преподавателя — для роли DEPARTMENT подставляет departmentId из сессии, даты по умолчанию сегодня), реестр инцидентов с фильтром статуса и действиями (подтвердить/отклонить/отменить/мастер), мастер замены (overlay с блокировкой прокрутки absence-wizard-open на body, решения по каждому занятию, журнал решений, «Применить выбранные» с проверкой «Выберите хотя бы одно решение»), согласование пожеланий на семестр (фильтры семестра/статуса, счётчик ожидающих, комментарий обязателен при отклонении) и заявки на изменение (применение доступно только ADMIN/EDUCATION_OFFICE, история решения);
  • модальные сообщения об ошибках действий воспроизведены локальным диалогом с классами project-dialog (разметка и поведение alertAction из dialog.js: закрытие по Escape/фону, кнопка «Понятно»);
  • до инициализации списки согласования пусты, реестр показывает «Загрузка...» — как в legacy HTML; мастер замены рендерится только при открытии;
  • react/admin/teacher-absences-service.js: контракты GET/POST/DELETE /api/teacher-absences (+ /{id}/review, /{id}/wizard, /{id}/resolve), GET /api/teacher-preferences/meta, GET/POST /api/teacher-preferences, GET/POST /api/teacher-change-requests, GET /api/users/teachers/options; чистая логика: варианты решений мастера и разбор REPLACE_TEACHER|id / MOVE_TIME|date|slot / CHANGE_CLASSROOM|id, подписи статусов и решений, склонение «решение/решения/решений», сводка статистики, цель заявки на изменение;
  • legacy-файлы вкладки удалены (admin/js/views/teacher-absences.js, admin/views/teacher-absences.html), загрузчик убран из legacy-views.js, у маршрута в admin-service.js больше нет viewFile; в legacy-views.js остался единственный загрузчик — schedule (волна 6);
  • бюджеты: lazy-вкладки 130 276 байт gzip в 26 chunk'ах (лимит 150 000), остальные без изменений — лимиты не поднимались;
  • npm run check: 233 теста пройдено (3 новых: контракты API отсутствий, подписи и сводки, DOM-рендер вкладки).

Выполнено на этапе 5 (волна 6, финальная — schedule + schedule-override-panel):

  • react/admin/tabs/ScheduleTab.jsx: конструктор правил — каскад учебный год → семестр → версия (контекст-бар с eyebrow/заголовком/заметкой, выбор версии по localStorage-ключам magistr.schedule.openVersionId/ openRuleId из schedule-versions-service.js, модалка создания черновика с копией правил), форма правила (дисциплина, семестр синхронизирован с версией, часы/недели начала по типам занятий, мультиселект групп с поиском и кэшем выбранных, слоты с подгруппами только для лабораторных, подсказки согласованных пожеланий преподавателя), реестр правил с поиском, пагинацией и удалением, модалка конфликта (причины, редактируемые слоты конфликтующего правила, повтор с сохранением payload), визуальная матрица (drawer с фильтром года/семестра/групп, ячейки по чётности, контекстное меню пары — позиционирование через Web Animations API, действия «открыть правило»/ «изменить день и пару»/«удалить»), dirty-guard формы через общий dirty-state.js;
  • react/admin/schedule-service.js: чистая логика и контракты API — слоты правил и payload-валидация (часы по типам, подгруппы только лабораторных и по одной на группу), payload конфликтующего правила, подписи правил/черновиков, группировка опций версий, визуальная матрица (ячейки по чётности, активные недели, дедупликация занятий), позиционирование контекстного меню, GET /api/admin/schedule-rules/page|/{id}, POST|PUT|DELETE /api/admin/schedule-rules, GET /api/groups/options, GET /api/groups/subgroups, GET /api/teacher-preferences?status=APPROVED;
  • боковая панель разовых правок schedule-override-panel.js абсорбирована в ScheduleViewTab.jsx: drawer переписан на React-состояние (forwardRef + useImperativeHandle: openLesson/openRegistry), реестром правок за период владеет вкладка (кнопка «Изменения за период» со счётчиком), вкладка «Редактирование» — сравнение «было/станет», недельный выбор дат, календарь семестра, время по эффективным слотам, преподаватель/аудитория через React-AsyncCombobox с пополнением справочников, отмена занятия и возврат по правилу; чистая логика — react/admin/schedule-override-service.js (контракты GET /api/edu-office/schedule/overrides/availability, GET /api/admin/time-slots/effective, POST|PUT|DELETE /api/edu-office/schedule/overrides);
  • переходный слой удалён: react/admin/LegacyTabView.jsx, react/admin/legacy-views.js, admin/views/schedule.html, admin/js/views/schedule.js, admin/js/views/schedule-override-panel.js, а также оставшиеся без пользователей admin/js/async-combobox.js и admin/js/pagination.js (React-аналоги — react/shared/ui/AsyncCombobox.jsx и Pagination.jsx); все 15 вкладок админ-панели — React, HTML-фрагменты больше не загружаются через fetch;
  • бюджеты: entrypoint админ-панели 5 640 байт gzip (лимит 20 000), холодная загрузка 84 857 байт gzip (лимит 90 000), общие chunk 79 217 байт gzip (лимит 85 000), lazy-вкладки 128 571 байт gzip в 25 chunk'ах (лимит 150 000, загружаются по одной) — лимиты не поднимались;
  • npm run check: 237 тестов пройдено (обновлены импорты тестов с legacy-модулей на React-сервисы, добавлены payload-валидация, DOM-рендер конструктора и контроль разрешения lazy-вкладок; проверки LegacyTabView/legacy-views заменены на контроль отсутствия legacy-модулей).

Починено при первом браузерном smoke-тесте после волны 6: реестр react-tabs.js использовал голый import() внутри React.lazy, а компоненты вкладок экспортируются именованными экспортами — без default React падал при открытии любой вкладки (React error #306, «Интерфейс временно недоступен» после авторизации). Исправлено явным отображением .then(m => ({ default: m.XTab })); добавлен регрессионный тест, проверяющий разрешение всех 15 lazy-вкладок.

Этап 5 закрыт: основная админ-панель полностью работает на React.

Команды для возобновления:

cd frontend
npm ci
npm run build
npm run check