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

676 lines
61 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# План перехода frontend на React
## Зачем выполняется миграция
Цель перехода — снизить сложность развития интерфейса, сделать состояние страниц явным,
переиспользовать компоненты и тестировать пользовательские сценарии изолированно. Сам React
не гарантирует меньший размер страницы: вес, время загрузки и объём запросов контролируются
отдельными бюджетами.
Миграция выполняется постепенно по модели «strangler»: React и существующие ES-модули
временно работают рядом, а переключение происходит по целым страницам или независимым
разделам. Большой одномоментный rewrite не планируется.
## Обязательные ограничения
- Сохраняются текущие URL, REST API и серверная ролевая модель.
- Access JWT остаётся только в памяти, refresh-токен — в `HttpOnly` cookie.
- Нельзя ослаблять CSP: удалённые и inline-скрипты по-прежнему запрещены.
- UI, ошибки и пользовательские сообщения остаются на русском языке.
- Общие CSS-токены и светлая/тёмная темы сохраняются до отдельного решения о редизайне.
- Каждый перенесённый срез должен иметь тесты, production-сборку и измеримый бюджет bundle.
- Vanilla-код удаляется только после переключения соответствующего маршрута на React.
## Целевая схема
```text
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.
Команды для возобновления:
```bash
cd frontend
npm ci
npm run build
npm run check
```