Админка, настройки, логин, кабинеты преподавателя и студента полностью переведены на 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
61 KiB
План перехода frontend на React
Зачем выполняется миграция
Цель перехода — снизить сложность развития интерфейса, сделать состояние страниц явным, переиспользовать компоненты и тестировать пользовательские сценарии изолированно. Сам React не гарантирует меньший размер страницы: вес, время загрузки и объём запросов контролируются отдельными бюджетами.
Миграция выполняется постепенно по модели «strangler»: React и существующие ES-модули временно работают рядом, а переключение происходит по целым страницам или независимым разделам. Большой одномоментный rewrite не планируется.
Обязательные ограничения
- Сохраняются текущие URL, REST API и серверная ролевая модель.
- Access JWT остаётся только в памяти, refresh-токен — в
HttpOnlycookie. - Нельзя ослаблять 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-модулей
- Простые справочники:
classrooms,subjects,dashboard. - Реестры и формы:
users,groups,university-structure. - Ролевые рабочие места:
teacher-requests,department-workspace. - Календарь и нагрузка:
academic-calendar,auditorium-workload. - Контур публикации:
schedule-view,schedule-versions,schedule-quality,teacher-absences. - Последними —
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/(esbuildsplitting), 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, legacystudent/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, legacyteacher/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, legacyadmin/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, legacyadmin/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: внутренние разделы кафедр/специальностей/профилей, формы создания и редактирования (модалки кафедр и специальностей, форма профиля на вкладке и в модалке специальности), клиентская фильтрация и пагинация списков как в legacylistPage(поиск с 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— реплика legacypromptActionиз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», предупреждения об ошибках загрузки и датах вне семестра); сводный текст «Дата: …, Занятых слотов: …» воспроизводит legacyrenderSummary(включая «выбран один объект» и счётчик ошибок групп); при сбое части групп/сеток показываются доступные данные с предупреждением (legacyallSettled-стратегия);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-routerAdminAppпроводит переход через 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