diff --git a/AGENTS.md b/AGENTS.md index bdb188e..0519e4e 100755 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,7 +4,7 @@ Проект представляет собой систему управления университетским расписанием. - **Backend**: Java 17, Spring Boot 3.5.16 (Мультитенантная архитектура: отдельная БД для каждого клиента) -- **Frontend**: Vanilla JavaScript + HTML/CSS (без фреймворков) +- **Frontend**: поэтапный переход с Vanilla JavaScript на React 19 + HTML/CSS - **Database**: PostgreSQL (множество БД, управляются через Flyway) - **Локальный URL**: localhost:80 - **Продакшн URL**: https://magistr.zuev.company @@ -25,6 +25,7 @@ magistr/ │ ├── utils/ # Утилиты │ └── src/main/resources/db/migration/ # Flyway SQL миграции (версионирование схемы БД) ├── frontend/ # Статические файлы +│ ├── react/ # React entrypoint и компоненты уже перенесённых страниц │ ├── admin/ # Интерфейс администратора │ │ └── settings/ # Страница настроек (отдельный SPA) │ ├── department/ # Redirect в кабинет кафедры внутри admin SPA diff --git a/docs/FRONTEND.md b/docs/FRONTEND.md index 37b5a7e..8f94f6c 100644 --- a/docs/FRONTEND.md +++ b/docs/FRONTEND.md @@ -4,12 +4,16 @@ | Параметр | Значение | |----------|----------| -| **Фреймворк** | Нет (Vanilla JavaScript) | +| **Фреймворк** | Переходный режим: React 19 для страницы входа, кабинетов студента и преподавателя, страницы настроек и общего слоя `react/shared`, Vanilla JavaScript для остальных разделов | | **Модульная система** | ES6 Modules (`import`/`export`) | | **Стили** | CSS (модульный подход) | | **Шрифт** | Системный стек без внешних font-CDN | | **Веб-сервер** | Apache httpd на Alpine со строгим CSP | +Пошаговый план и актуальная точка продолжения находятся в +[`REACT_MIGRATION.md`](REACT_MIGRATION.md). Миграция выполняется по целым страницам и +разделам без изменения действующих URL и REST-контрактов. + --- ## Структура файлов @@ -23,10 +27,90 @@ frontend/ ├── schedule-overview.js # Общий расчёт и рендер «Сегодня / Следующая пара» ├── schedule-overview.css # Общие карточки ближайших занятий и поискового выбора группы ├── security.conf # CSP и защитные HTTP-заголовки Apache +├── react-cache.conf # Cache-Control: immutable для chunk'ов, no-cache для entrypoint, CSS и HTML ├── telemetry/ │ └── otel-entry.js # Исходная точка сборки OpenTelemetry ├── scripts/ +│ ├── build-react.mjs # Production-сборка React entrypoint и проверка gzip-бюджета +│ ├── build-react-tests.mjs # Сборка компонентных проверок для node:test │ └── build-vendor.mjs # Сборка `/vendor/otel.js` через esbuild +├── react/ +│ ├── login/ +│ │ ├── LoginApp.jsx # React-компонент страницы входа +│ │ ├── login-service.js # Валидация и контракт POST /api/auth/login +│ │ └── main.jsx # React entrypoint для `/` +│ ├── student/ # Кабинет студента на React +│ │ ├── StudentApp.jsx # Сессия, выбор группы, неделя и состояния +│ │ ├── WeekGrid.jsx # Недельная сетка дней и занятий +│ │ ├── student-service.js # Чипы, статусы и контракты API кабинета студента +│ │ ├── main.jsx # React entrypoint для `/student/` +│ │ └── testing/ # render-student.jsx для DOM-проверок +│ ├── teacher/ # Кабинет преподавателя на React +│ │ ├── TeacherApp.jsx # Сессия, вкладки, неделя и состояния +│ │ ├── TeacherWeekGrid.jsx # Матрица слотов и мобильный список дней +│ │ ├── AbsencePanel.jsx # Заявки об отсутствии +│ │ ├── PreferencesTab.jsx # Семестровый календарь доступности +│ │ ├── RequestsTab.jsx # Журнал заявок на изменение пар +│ │ ├── RequestDialog.jsx # Диалог переноса/аудитории/отмены +│ │ ├── teacher-service.js # Слоты, матрица недели, метки и контракты API +│ │ ├── main.jsx # React entrypoint для `/teacher/` +│ │ └── testing/ # render-teacher.jsx для DOM-проверок +│ ├── settings/ # Страница настроек на React +│ │ ├── SettingsApp.jsx # Сессия, боковая панель, вкладки и тема +│ │ ├── GeneralTab.jsx # Заглушка общих настроек +│ │ ├── TimeSlotsTab.jsx # Сетки времени и CRUD временных слотов +│ │ ├── EduFormsTab.jsx # Формы обучения +│ │ ├── DatabaseTab.jsx # Статус подключения и тенанты +│ │ ├── settings-service.js # Метки сеток, валидация и контракты API +│ │ ├── main.jsx # React entrypoint для `/admin/settings/` +│ │ └── testing/ # render-settings.jsx для DOM-проверок +│ ├── admin/ # Оболочка основной админ-панели на React +│ │ ├── AdminApp.jsx # Сессия, sidebar с секциями, topbar, hash-router +│ │ ├── react-tabs.js # Реестр всех вкладок админ-панели (lazy + Suspense) +│ │ ├── admin-service.js # Маршруты, секции навигации, склонения, контракты API +│ │ ├── classrooms-service.js # Статусы, оборудование и контракты API аудиторий +│ │ ├── subjects-service.js # Состояние реестра, URL-параметры и контракты API дисциплин +│ │ ├── dashboard-service.js # Метрики, слоты, свободные аудитории и конфликты Red Zone +│ │ ├── users-service.js # Роли, состояние реестра и контракты API пользователей +│ │ ├── groups-service.js # Подгруппы, календарные графики и контракты API групп +│ │ ├── university-structure-service.js # Фильтрация списков и контракты API кафедр/специальностей/профилей +│ │ ├── teacher-requests-service.js # Состояние реестра, валидация одобрения и контракты API заявок +│ │ ├── department-workspace-service.js # Нагрузка, payload-валидация и контракты API кабинета кафедры +│ │ ├── auditorium-workload-service.js # Дедупликация занятий, чётность недель и контракты API загруженности +│ │ ├── academic-calendar-service.js # Учебные годы, сетка графика, диалог ячейки и контракты API календаря +│ │ ├── schedule-view-service.js # Разделы расписания, семестровая матрица и контракты API просмотра +│ │ ├── schedule-service.js # Слоты правил, payload-валидация, визуальная матрица и контракты API конструктора +│ │ ├── schedule-override-service.js # Источник правки, календарь семестра и контракты API разовых правок +│ │ ├── schedule-versions-service.js # Группировка версий, подписи журнала и контракты API контура публикаций +│ │ ├── schedule-quality-service.js # Диапазоны оценки, фильтры проблем и контракт API анализа качества +│ │ ├── teacher-absences-service.js # Решения мастера замены, подписи статусов и контракты API отсутствий +│ │ ├── tabs/ # Вкладки админ-панели (волны 1–6, все вкладки на React) +│ │ │ ├── DashboardTab.jsx # Метрики, мониторинг пар, Red Zone +│ │ │ ├── ClassroomsTab.jsx # Аудиторный фонд с мультиселектом оборудования +│ │ │ ├── SubjectsTab.jsx # Реестр дисциплин с пагинацией и преподавателями +│ │ │ ├── UsersTab.jsx # Реестр пользователей с фильтром роли и архивированием +│ │ │ ├── GroupsTab.jsx # Реестр групп, подгруппы и календарные графики +│ │ │ ├── UniversityStructureTab.jsx # Кафедры, специальности и профили обучения +│ │ │ ├── TeacherRequestsTab.jsx # Реестр заявок с редактируемыми строками и отклонением через PromptDialog +│ │ │ ├── DepartmentWorkspaceTab.jsx # Кабинет кафедры: дисциплины, преподаватели, нагрузка, заявки +│ │ │ ├── AuditoriumWorkloadTab.jsx # Загруженность: сводная матрица и двухнедельная сводка сущности +│ │ │ ├── AcademicCalendarTab.jsx # Календарные графики: годы, сетки, дисциплины (MaskedDateInput) +│ │ │ ├── ScheduleViewTab.jsx # Просмотр расписаний: фильтры, разделы, матрица и панель правок +│ │ │ ├── ScheduleTab.jsx # Конструктор правил: каскад версий, форма правила и визуальная матрица +│ │ │ ├── ScheduleVersionsTab.jsx # Контур публикаций: черновики, diff, архив, журнал и модальные операции +│ │ │ ├── ScheduleQualityTab.jsx # Анализ качества: скорборд, метрики, карта проблем и панель правила +│ │ │ └── TeacherAbsencesTab.jsx # Отсутствия: реестр, мастер замены, пожелания и заявки на изменение +│ │ ├── main.jsx # React entrypoint для `/admin/` +│ │ └── testing/ # render-admin.jsx для DOM-проверок +│ └── shared/ # Общий React-слой для следующих переносимых страниц +│ ├── api-client.js # requestJson/apiClient поверх auth-транспорта +│ ├── auth-client.js # Восстановление сессии и logout с redirect +│ ├── dates.js # Дата-математика (недели, ISO, форматирование) +│ ├── session-policy.js # Фазы loading/ready/unauthorized/error и русские тексты +│ ├── use-authorized-session.js # Хук проверки роли при монтировании страницы +│ ├── runtime-entry.jsx # Единая точка экспорта общего runtime-chunk +│ ├── ui/ # AppErrorBoundary, AsyncState, FormAlert, AsyncCombobox, ScheduleOverview, ConfirmDialog, Pagination +│ └── testing/ # render-shared.jsx для DOM-проверок через renderToStaticMarkup ├── tests/ │ ├── academic-calendar-grid.test.mjs # Неполная первая неделя календарного графика │ ├── academic-calendar-title.test.mjs # Составное название календарного графика @@ -34,6 +118,13 @@ frontend/ │ ├── schedule-overview.test.mjs # Текущее занятие, список на сегодня и следующая пара │ ├── admin-ui-regressions.test.mjs # Заголовки, меню, действия слотов и компоновка вкладок │ ├── auth-session.test.mjs # Login/refresh/reload/logout и single-flight refresh +│ ├── react-login.test.mjs # Валидация и API-контракт React-формы входа +│ ├── react-shared.test.mjs # Общая политика сессии, API-клиент и DOM-проверки состояний +│ ├── react-student.test.mjs # Дата-математика, чипы, контракты API и DOM-проверки кабинета студента +│ ├── react-teacher.test.mjs # Слоты, матрица недели, метки, payload заявок и DOM-проверки кабинета преподавателя +│ ├── react-settings.test.mjs # Вкладки по ролям, метки сеток, валидация и контракты API страницы настроек +│ ├── react-admin.test.mjs # Навигация по ролям, hash-маршруты, склонения и DOM-проверки оболочки админки +│ ├── react-admin-tabs.test.mjs # Сервисы и DOM-проверки вкладок волн 1–5 (дашборд, аудитории, дисциплины, пользователи, группы, структура вуза, заявки, кабинет кафедры, загруженность, календарные графики, вкладки расписания) │ ├── dashboard-conflicts.test.mjs # Регрессии дат и состояний проверки конфликтов │ ├── schedule-overrides.test.mjs # Действия, роли, недельный выбор и подбор времени разовой правки │ ├── schedule-quality.test.mjs # Метрики качества, фильтры проблем и доступность вкладки @@ -43,13 +134,12 @@ frontend/ │ ├── teacher-preferences.test.mjs # Календарь пожеланий, заявки и подсказки конструктора │ └── security-policy.test.mjs # Web Storage, XSS, пароли, язык, CSP и Dockerfile ├── index.html # 🔐 Страница авторизации (общая) -├── script.js # Логика авторизации ├── style.css # Стили страницы авторизации ├── theme-toggle.js # Переключение светлой/тёмной темы ├── Dockerfile # Multi-stage: npm bundle → Apache httpd │ ├── admin/ # 👨💼 Интерфейс администратора -│ ├── index.html # SPA-оболочка с sidebar +│ ├── index.html # Монтирует React-оболочку `/react/admin.js` │ ├── css/ │ │ ├── main.css # CSS-переменные, цвета, типографика │ │ ├── layout.css # Раскладка (sidebar, topbar, content) @@ -61,81 +151,37 @@ frontend/ │ │ ├── schedule-quality.css # Диагностический экран качества расписания │ │ └── schedule-versions.css # Контур публикации, diff и журнал версий │ ├── js/ -│ │ ├── main.js # Инициализация, маршрутизация, навигация │ │ ├── role-capabilities.js # Единая матрица вкладок admin/settings по ролям │ │ ├── api.js # HTTP-обёртка (fetch + Authorization) │ │ ├── dashboard-conflicts.js # Чистые функции дат, загрузки и состояний Red Zone │ │ ├── date-input.js # Маска ДД.ММ.ГГГГ и преобразование дат в ISO -│ │ ├── async-combobox.js # Асинхронный поиск больших справочников │ │ ├── dialog.js # Доступные подтверждения, сообщения и ввод причины │ │ ├── dirty-state.js # Защита несохранённых изменений -│ │ ├── pagination.js # Общая панель серверной пагинации │ │ ├── url-state.js # Чтение и запись фильтров в query string │ │ ├── view-state.js # Единые loading/empty/error-состояния таблиц │ │ ├── academic-period.js # Каскад учебного года и семестра в профильных вкладках │ │ ├── utils.js # Утилиты │ │ ├── schedule-period.js # Выбор ближайшего учебного периода для расписаний и нагрузки -│ │ └── views/ # Модули представлений -│ │ ├── dashboard.js # Дашборд -│ │ ├── users.js # Управление пользователями -│ │ ├── teacher-requests.js # Заявки кафедр на преподавателей -│ │ ├── groups.js # Управление группами -│ │ ├── classrooms.js # Управление аудиториями -│ │ ├── subjects.js # Управление дисциплинами -│ │ ├── university-structure.js # Кафедры, специальности и профили обучения -│ │ ├── department-workspace.js # Кабинет кафедры в общей панели -│ │ ├── schedule-view.js # Просмотр расписаний и запуск разовой правки из карточки -│ │ ├── schedule-override-panel.js # Боковая панель и реестр разовых изменений -│ │ ├── teacher-absences.js # Отсутствия, согласование пожеланий и заявок на изменение -│ │ ├── schedule-quality.js # Оценка, фильтры и переход к редактированию правил -│ │ ├── schedule-versions.js # Черновики, публикация, восстановление, diff и аудит -│ │ ├── schedule.js # Конструктор правил и подсказки пожеланий преподавателей -│ │ ├── academic-calendar-grid.js # Расчёт ISO-недели дневной сетки -│ │ ├── academic-calendar-title.js # Название из кода, профиля, формы и года -│ │ ├── academic-calendar.js # Календарные учебные графики -│ │ └── auditorium-workload.js # Загруженность аудиторий, преподавателей и кафедр -│ ├── views/ # HTML-шаблоны представлений -│ │ ├── dashboard.html -│ │ ├── users.html -│ │ ├── teacher-requests.html -│ │ ├── groups.html -│ │ ├── classrooms.html -│ │ ├── subjects.html -│ │ ├── university-structure.html -│ │ ├── department-workspace.html -│ │ ├── schedule-view.html -│ │ ├── teacher-absences.html -│ │ ├── schedule-quality.html -│ │ ├── schedule-versions.html -│ │ ├── schedule.html -│ │ ├── academic-calendar.html -│ │ └── auditorium-workload.html +│ │ └── views/ # Общие чистые хелперы (используются React-сервисами) +│ │ ├── academic-calendar-grid.js # Расчёт ISO-недели дневной сетки (общий с React-сервисом) +│ │ └── academic-calendar-title.js # Название из кода, профиля, формы и года (общий с React-сервисом) │ │ -│ └── settings/ # ⚙️ Страница настроек (отдельный SPA) -│ ├── index.html # Оболочка с собственной sidebar -│ ├── css/ -│ │ ├── main.css # CSS-переменные, базовые стили -│ │ └── layout.css # Sidebar, topbar, content -│ ├── js/ -│ │ ├── main.js # Навигация по вкладкам настроек -│ │ └── views/ -│ │ └── time-slots.js # Настройка временных слотов -│ └── views/ -│ ├── general.html # Общие настройки (заглушка) -│ └── time-slots.html # Базовая, субботняя и ручные сетки времени +│ └── settings/ # ⚙️ Страница настроек (отдельный SPA на React) +│ ├── index.html # Оболочка с корневым элементом settings-root +│ └── css/ +│ ├── main.css # CSS-переменные, базовые стили +│ └── layout.css # Sidebar, topbar, content │ -├── teacher/ # 👩🏫 Интерфейс преподавателя -│ ├── index.html # Расписание, календарь доступности и журнал заявок -│ ├── app.js # Пожелания, переносы и недельный просмотр через общий auth-session +├── teacher/ # 👩🏫 Интерфейс преподавателя (React) +│ ├── index.html # Оболочка с корневым элементом teacher-root │ └── style.css # Адаптивный кабинет в стиле диспетчерского журнала ├── department/ # 🏛 Кабинет кафедры │ └── index.html # Redirect в `/admin/#department-workspace` ├── edu-office/ # 🗓 Кабинет учебного отдела │ └── index.html # Redirect в `/admin/#schedule-view` │ -└── student/ # 🎓 Интерфейс студента - ├── index.html # CSP-совместимая HTML-оболочка - ├── app.js # Недельный просмотр и общий auth-session +└── student/ # 🎓 Интерфейс студента (React) + ├── index.html # Оболочка с корневым элементом student-root └── style.css # Стили кабинета без inline-блока ``` @@ -143,24 +189,36 @@ frontend/ ## Система маршрутизации (Admin SPA) -Админ-панель работает как **Single Page Application** без фреймворка. +Оболочка админ-панели работает на **React** (`react/admin/AdminApp.jsx`, entrypoint +`/react/admin.js`): sidebar с секциями, topbar с заголовком вкладки и счётчиком заявок, +тема, сворачивание панели, настройки с выходом и hash-router. -Навигация реализована через `data-tab` атрибуты на элементах sidebar: +Навигация реализована через hash-адреса вкладок (`#users`, `#schedule` и т. д.): -```html -Пользователи -Группы -Расписание занятий -Календарный график -``` +- при старте активная вкладка читается из `location.hash` (неразрешённые роли + значения заменяются вкладкой по умолчанию); +- слушатель `hashchange` синхронизирует вкладку с адресной строкой (работают + кнопки «назад/вперёд» и ссылки из других кабинетов); +- переключение идёт через `requestTabSwitch` с guard'ом несохранённых изменений + из общего модуля `admin/js/dirty-state.js`. -При клике на пункт меню `main.js`: -1. Загружает HTML-шаблон из `views/{tab}.html` через `fetch()` -2. Вставляет его в `#app-content` -3. Подключает соответствующий JS-модуль из `js/views/{tab}.js` -4. Обновляет заголовок страницы (`#page-title`) +Содержимое вкладки выбирается по реестру `react/admin/react-tabs.js`: -`main.js` и отдельный settings SPA получают разрешённые вкладки из единого +- **все 15 вкладок админ-панели** (волна 1: `dashboard`, `classrooms`, `subjects`; + волна 2: `users`, `groups`, `university-structure`; волна 3: + `teacher-requests`, `department-workspace`; волна 4: `auditorium-workload`, + `academic-calendar`; волна 5: `schedule-view`, `schedule-versions`, + `schedule-quality`, `teacher-absences`; волна 6: `schedule`) + рендерятся React-компонентами из `react/admin/tabs/`, каждый загружается + отдельным lazy-chunk'ом (`React.lazy` + `Suspense`, fallback — индикатор + `tab-loading`); +- переходный слой `LegacyTabView` + `legacy-views.js` и каталоги + `admin/views/*.html` / `admin/js/views/{tab}.js` удалены после волны 6 — + HTML-фрагменты больше не загружаются через `fetch`, в `admin/js/views/` + остались только общие чистые хелперы календарных графиков. + +React-оболочка админки (`react/admin/admin-service.js`) и React-оболочка настроек +(`react/settings/SettingsApp.jsx`) получают разрешённые вкладки из единого `admin/js/role-capabilities.js`; локальные дубли `ROLE_NAVIGATION`/`ROLE_TABS` удалены. | Роль | Доступные вкладки | @@ -172,10 +230,11 @@ frontend/ Пути `/department/` и `/edu-office/` оставлены как входные redirect-страницы в общую панель. Отдельные кабинеты не дублируют UI админ-панели. -Пункты меню, недоступные роли, не отображаются в sidebar: `main.js` выставляет `hidden`, а `layout.css` явно скрывает такие элементы, чтобы базовый `display: flex` у `.nav-item` не возвращал их на экран. -Верхние панели основной админки и settings SPA не содержат неработающей кнопки -бургер-меню. В светлой теме пункты и заголовки sidebar получают контрастные тёмные цвета -для всех ролей, включая `DEPARTMENT`. +Пункты меню, недоступные роли, не попадают в sidebar: React-оболочка фильтрует +секции навигации по матрице ролей до рендера. Верхние панели основной админки и +settings SPA не содержат неработающей кнопки бургер-меню. В светлой теме пункты и +заголовки sidebar получают контрастные тёмные цвета для всех ролей, включая +`DEPARTMENT`. ### Разделы админ-панели @@ -267,9 +326,9 @@ frontend/ восстанавливают поиск, фильтры, сортировку, номер и размер страницы после reload или возврата по ссылке. Просмотр расписания сохраняет вид сущности, выбранную сущность, дату, учебный год, семестр и дополнительные фильтры; конечные кабинеты сохраняют неделю, вкладку и группу. -- `pagination.js` работает с общим ответом `PageResponse`; размер страницы — 25, 50 или - 100. `view-state.js` унифицирует загрузку со spinner, пустой результат и ошибку с кнопкой - повторного запроса. +- Общий React-компонент `react/shared/ui/Pagination.jsx` работает с общим ответом + `PageResponse`; размер страницы — 25, 50 или 100. `view-state.js` унифицирует + загрузку со spinner, пустой результат и ошибку с кнопкой повторного запроса. - `AsyncCombobox` загружает варианты с сервера, сохраняет выбранный ID, поддерживает клавиатуру и роли `combobox/listbox`. Он используется для групп, преподавателей, аудиторий и совместимых календарных графиков; предварительная загрузка вариантов не @@ -288,13 +347,26 @@ frontend/ ### Страница настроек (`/admin/settings/`) -Настройки — это **отдельный SPA** со своей боковой панелью и вкладками, не связанными с основной админ-панелью. +Страница переведена на React (`react/settings/`, entrypoint `dist/react/settings.js`) и использует +общий слой `react/shared`: `useAuthorizedSession(SETTINGS_ROLES)`, `apiClient`, `AsyncState`, +`AppErrorBoundary` и общий `ConfirmDialog`/`PromptDialog` (классы `.project-dialog*`, +закрытие по Escape и клику по фону; `PromptDialog` — реплика legacy `promptAction` +с обязательным текстовым полем и ошибкой внутри диалога). Настройки — **отдельный SPA** со своей боковой панелью и вкладками, не связанными с +основной админ-панелью; как и в legacy-версии, hash-навигации нет — активная вкладка живёт +в состоянии React. -- Доступ: через dropdown «Настройки» в footer боковой панели админки для `ADMIN` и `EDUCATION_OFFICE` -- Кнопка «Назад в панель» для возврата в `/admin/` +- Доступ: через dropdown «Настройки» в footer боковой панели админки для `ADMIN` и `EDUCATION_OFFICE`; + список и вкладка по умолчанию фильтруются единой матрицей `admin/js/role-capabilities.js` +- Кнопка «Назад в панель» для возврата в `/admin/`, кнопка выхода через общий `logoutAndRedirect` +- Сворачивание боковой панели сохраняется в `localStorage.sidebar-collapsed`; тема управляется + в React (кнопка в topbar), `theme-toggle.js` на странице больше не подключается - Текущие вкладки: - **Общие настройки** — заглушка (только `ADMIN`) - **Временные слоты** — выбор сетки через выпадающий список, CRUD слотов выбранной сетки, добавление ручных сеток через модальное окно и удаление выбранной ручной сетки. Поле длительности доступно только для чтения и пересчитывается при изменении времени начала или окончания; в API отправляются только границы, а окончательное значение вычисляет backend. Базовая сетка применяется по умолчанию, субботняя — автоматически по субботам; ручное применение выполняется в сетке календарного графика. + - **Формы обучения** — создание и удаление форм обучения + - **База данных** — статус текущего подключения, реестр тенантов, проверка и добавление нового + подключения (пароль помечен `autocomplete="new-password"`); inline-стили legacy заменены + классами `.db-status-grid`/`.form-group-wide`/`.form-row-actions` --- @@ -339,11 +411,15 @@ SHA-256 style-хэшей с CSP. Там же закреплены безопас Команды выполняются из каталога `frontend/`: ```bash -npm run build # собрать локальный dist/vendor/otel.js +npm run build # собрать dist/vendor/otel.js и dist/react/login.js npm test # frontend unit/static tests -npm run check # синтаксис auth/UI-модулей + все frontend-тесты +npm run check # React-сборка, синтаксис auth/UI-модулей + все frontend-тесты ``` +React-сборка выполняется существующим esbuild без отдельного dev-сервера. Для страницы +входа установлен лимит 75 000 байт gzip; превышение завершает сборку ошибкой. При появлении +нескольких React entrypoint общий runtime будет вынесен в отдельный кэшируемый chunk. + --- ## Аутентификация (Frontend) @@ -351,7 +427,7 @@ npm run check # синтаксис auth/UI-модулей + все frontend-т ### Страница входа (`/index.html`) 1. Пользователь вводит логин/пароль -2. `script.js` отправляет `POST /api/auth/login` +2. `react/login/LoginApp.jsx` через `login-service.js` отправляет `POST /api/auth/login` 3. При успехе кладёт access JWT и профиль пользователя только в память текущей страницы. 4. Refresh-токен сохраняется браузером как `HttpOnly` cookie и недоступен JavaScript. 5. После перехода новый документ восстанавливает память через `POST /api/auth/refresh`. @@ -409,25 +485,31 @@ if (!session || !AUTHORIZED_ROLES.includes(session.role)) { ### Преподаватель (`/teacher/`) -Страница объединяет недельную сетку занятий, семестровый календарь доступности и журнал -заявок на изменение пар. ID преподавателя берётся из восстановленного в памяти профиля -сессии. +Страница переведена на React (`react/teacher/`, entrypoint `dist/react/teacher.js`) и использует +общий слой `react/shared`: `useAuthorizedSession(['TEACHER'])`, `apiClient`, `AsyncState`, +`AppErrorBoundary`, дата-математику `dates.js` и общий `ScheduleOverview` с ролью `teacher`. +ID преподавателя берётся из восстановленного в памяти профиля сессии. + +Кабинет объединяет недельную сетку занятий, семестровый календарь доступности и журнал +заявок на изменение пар в трёх вкладках; активная вкладка и дата недели сохраняются +в query-параметрах `teacherTab`/`teacherDate` через общий `url-state.js`. Основные элементы: - общий блок «Сегодня» и карточка «Следующая пара»; отдельный запрос покрывает ближайшие 14 дней, поэтому следующая пара находится и за границей текущей недели; -- навигация по неделям: предыдущая, текущая, следующая; -- выбор даты через `input[type="date"]`; +- навигация по неделям: предыдущая, текущая, следующая; выбор даты через `input[type="date"]`; - запрос `GET /api/schedule?teacherId={userId}&startDate={YYYY-MM-DD}&endDate={YYYY-MM-DD}`; +- матрица «слоты × дни недели» на десктопе и дневной список на мобильных (переключение + через `matchMedia` с живым откликом на смену ширины); - отображение дисциплины, времени, типа занятия, лабораторных подгрупп, аудитории и всех групп правила; - вкладка «Мои пожелания» с выбором семестра, режимом отметки строгих, предпочтительных и нежелательных интервалов, полностью недоступных дат, пар подряд и расписания без окон; - цветовые статусы `PENDING`, `APPROVED`, `REJECTED`, `CANCELLED`; ожидающее пожелание можно отозвать кликом по отмеченной ячейке или кнопкой в списке дат; -- кнопка «Заявка на изменение» в карточке фактического занятия и модальное окно переноса, +- кнопка «Запросить изменение» в карточке занятия из правила и модальное окно переноса, смены аудитории или отмены. Недоступные кандидаты отфильтрованы после проверки backend; -- вкладка журнала заявок с причиной, решением, ссылкой на применённый override и историей - статусов; ожидающую заявку можно отозвать; +- вкладка журнала заявок с причиной, решением и историей статусов; ожидающую заявку можно + отозвать; счётчик ожидающих заявок выводится прямо на вкладке навигации; - форма собственной заявки на отсутствие через `POST /api/teacher-absences`; - список статусов заявок и отмена ещё не подтверждённой записи через `DELETE /api/teacher-absences/{id}`. @@ -436,6 +518,11 @@ if (!session || !AUTHORIZED_ROLES.includes(session.role)) { ### Студент (`/student/`) +Страница переведена на React (`react/student/`, entrypoint `dist/react/student.js`) и использует +общий слой `react/shared`: `useAuthorizedSession(['STUDENT'])`, `apiClient`, `AsyncState`, +`AppErrorBoundary` и React-версия поискового списка `AsyncCombobox`. Модель обзора «Сегодня / +Следующая пара» переиспользует чистые функции `schedule-overview.js`. + В текущей модели студент не связан с конкретной группой, поэтому страница использует асинхронный поиск группы через `/api/groups/options`. @@ -443,9 +530,10 @@ if (!session || !AUTHORIZED_ROLES.includes(session.role)) { - поисковый combobox группы с подсказками по специальности, профилю и курсу; выбор хранится в `localStorage.studentGroupId` и `studentGroup` query string; - общий блок «Сегодня» и карточка «Следующая пара», рассчитанные по ближайшим 14 дням; -- недельная сетка по дням; +- недельная сетка по дням с навигацией «Предыдущая/Следующая неделя», «Сегодня» и выбором даты; - запрос `GET /api/schedule?groupId={groupId}&startDate={YYYY-MM-DD}&endDate={YYYY-MM-DD}`; -- отображение дисциплины, времени, преподавателя, аудитории, формата, типа занятия и лабораторных подгрупп. +- отображение дисциплины, времени, преподавателя, аудитории, формата, типа занятия и лабораторных подгрупп; +- URL-параметры `studentGroup` и `studentDate` сохраняются через общий `url-state.js`. --- diff --git a/docs/INFRASTRUCTURE.md b/docs/INFRASTRUCTURE.md index 35683b1..f3e9e62 100644 --- a/docs/INFRASTRUCTURE.md +++ b/docs/INFRASTRUCTURE.md @@ -95,10 +95,11 @@ Backend собирается через multi-stage сборку Maven: ```dockerfile FROM node:22-alpine3.23@sha256:8516dce... AS frontend-assets RUN npm ci -RUN npm run build:vendor +RUN npm run build FROM httpd:alpine3.23@sha256:4a15e9c... RUN apk upgrade --no-cache +COPY --from=frontend-assets /build/dist/react/ /usr/local/apache2/htdocs/react/ COPY --from=frontend-assets /build/dist/vendor/ /usr/local/apache2/htdocs/vendor/ COPY security.conf /usr/local/apache2/conf/extra/magistr-security.conf COPY proxy.conf /usr/local/apache2/conf/extra/magistr-proxy.conf @@ -106,7 +107,7 @@ COPY proxy.conf /usr/local/apache2/conf/extra/magistr-proxy.conf Все базовые образы backend/frontend и локальный PostgreSQL зафиксированы одновременно точным tag и manifest digest. Первый этап frontend собирает зафиксированный -OpenTelemetry bundle; второй раздаёт только runtime- +OpenTelemetry bundle и production-bundle React-страницы входа; второй раздаёт только runtime- файлы, без `node_modules`, тестов и build-исходников. В корень образа также копируются `ui-foundation.css`, `schedule-overview.css` и `schedule-overview.js`: они необходимы для общего шрифта, компонентов и обзора занятий в личных кабинетах. Apache подключает `mod_headers`, diff --git a/docs/REACT_MIGRATION.md b/docs/REACT_MIGRATION.md new file mode 100644 index 0000000..d784ef6 --- /dev/null +++ b/docs/REACT_MIGRATION.md @@ -0,0 +1,675 @@ +# План перехода 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 +``` diff --git a/docs/REACT_MIGRATION_RESULT.md b/docs/REACT_MIGRATION_RESULT.md new file mode 100644 index 0000000..a0b777e --- /dev/null +++ b/docs/REACT_MIGRATION_RESULT.md @@ -0,0 +1,101 @@ +# Итоги миграции frontend на React + +Дата завершения: 28.09.2026. Полный журнал процесса — в [`REACT_MIGRATION.md`](REACT_MIGRATION.md), +архитектура итогового решения — в [`FRONTEND.md`](FRONTEND.md). + +--- + +## Что сделано + +По модели strangler (этапы 1–5, все изменения не закоммичены — коммит по решению владельца): + +| Этап | Содержимое | Статус | +|------|-----------|--------| +| 1. Точка входа и вход в систему | `index.html`, страница логина | Завершён | +| 2. Интерфейс студента | SPA `student/` | Завершён | +| 3. Интерфейс преподавателя | SPA `teacher/` (пожелания, заявки) | Завершён | +| 4. Страница настроек | Отдельный SPA `admin/settings/` (4 вкладки) | Завершён | +| 5. Основная админ-панель | Shell + hash-router + **15 вкладок** в 6 волнах | Завершён | + +Волны этапа 5: `dashboard`, `classrooms`, `subjects` → `users`, `groups`, `university-structure` → +`teacher-requests`, `department-workspace` → `auditorium-workload`, `academic-calendar` → +`schedule-view`, `schedule-versions`, `schedule-quality`, `teacher-absences` → `schedule` +(конструктор правил + абсорбция панели разовых правок). + +**Удалено 45 legacy-файлов** (~19 000 строк vanilla-кода): все `admin/js/views/*.js` +и `admin/views/*.html`, `admin/js/main.js`, переходный слой `LegacyTabView.jsx` + `legacy-views.js`, +дублирующие утилиты `async-combobox.js` и `pagination.js` (заменены общими React-компонентами +`AsyncCombobox.jsx` и `Pagination.jsx`). + +**Добавлено 75 React-файлов** (~23 200 строк): оболочки четырёх SPA, 15 вкладок админки, +19 сервисов с чистой логикой, общая дизайн-система (`react/shared/ui/`), render-хелперы для тестов. + +--- + +## Насколько стало лучше + +### Скорость загрузки и переключения вкладок + +| Метрика (gzip) | До (legacy) | После (React) | +|----------------|-------------|---------------| +| Переключение вкладки админки | 2 запроса: `fetch` HTML-фрагмента + lazy JS | **1 запрос**: один lazy-chunk | +| Средний размер вкладки | HTML + JS отдельно | ~5,1 КБ (128 571 байт / 25 chunk'ов) | +| Общие chunk'и (кэшируются между страницами) | — | 79 217 байт (лимит 85 000) | +| Холодная загрузка админки | — | 84 857 байт (лимит 90 000) | + +- HTML-фрагменты больше **не загружаются через `fetch`** — вкладка рендерится сразу из + одного кэшируемого JS-chunk'а, исчезли двойные сетевые задержки и парсинг HTML на каждое + переключение. +- Runtime, дизайн-система и утилиты вынесены в общие chunk'и: повторные визиты и переходы + между страницами (логин → студент → админка) переиспользуют кэш браузера. +- Каждая вкладка загружается отдельно (`React.lazy` + `Suspense`) — пользователь качает + только то, что открывает. + +### Качество и надёжность + +- **237 тестов** проходят (`npm run check`), против ~170 на момент начала этапа 5: + контракты API, payload-валидация, DOM-рендер каждой вкладки, бюджеты bundle, CSP, + разрешение всех lazy-вкладок (регрессия на React error #306 поймана и закрыта тестом). +- Логика вынесена в **чистые сервисы** (`*-service.js`) — тестируются в Node без браузера + и DOM-заглушек. +- Единая дизайн-система вместо копипасты виджетов: кастомные селекты, `AsyncCombobox`, + `ConfirmDialog`, `Pagination`, `MaskedDateInput` — одинаковое поведение и a11y везде. +- Устранён класс ошибок ручного DOM: React-state вместо `innerHTML`/`classList`, + модалки — условный рендер вместо `hidden`-флагов, позиционирование меню — Web Animations API. + +### Безопасность (не ослаблена) + +- CSP-хэши: `[]` — ни одного элемента вне политики. +- JWT по-прежнему только в памяти, без inline-скриптов и inline-стилей + (проверяется тестами `security-policy.test.mjs`). +- Сообщения об ошибках не вставляют текст исключений через `innerHTML`. + +### Сопровождаемость + +- Один стек вместо двух: не нужно поддерживать параллельно vanilla-модули и React. +- −19 000 строк legacy-кода, +4 965 строк тестов. +- URL/REST-контракты и русский UI сохранены — для пользователей миграция незаметна. + +--- + +## Бюджеты bundle (итог, лимиты не поднимались с волны 1) + +| Entry point | Размер gzip | Холодная загрузка | Лимиты | +|-------------|-------------|-------------------|--------| +| Логин | 2 493 | 81 710 | 10 000 / 85 000 | +| Студент | 3 431 | 82 648 | 20 000 / 90 000 | +| Преподаватель | 10 145 | 89 362 | 20 000 / 90 000 | +| Настройки | 8 466 | 87 683 | 20 000 / 90 000 | +| Админ-панель | 5 640 | 84 857 | 20 000 / 90 000 | +| Lazy-вкладки админки | 128 571 (25 chunk'ов, по одной за раз) | — | 150 000 | + +--- + +## Проверка + +```bash +cd frontend +npm ci +npm run build +npm run check # 237/237 тестов, бюджеты, CSP +``` diff --git a/docs/README.md b/docs/README.md index 77d1d0d..c4d33ae 100644 --- a/docs/README.md +++ b/docs/README.md @@ -14,7 +14,7 @@ | Компонент | Технология | |-----------|-----------| | **Backend** | Java 17, Spring Boot 3.5.16 | -| **Frontend** | Vanilla JavaScript (ES6 Modules) + HTML/CSS | +| **Frontend** | React 19 + переходные Vanilla JavaScript ES-модули + HTML/CSS | | **База данных** | PostgreSQL (через Flyway миграции) | | **Контейнеризация** | Docker, Docker Compose | | **Продакшн** | Kubernetes, Caddy (реверс-прокси) | diff --git a/frontend/Dockerfile b/frontend/Dockerfile index bdb0cbf..20fb186 100755 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -3,9 +3,12 @@ FROM node:22-alpine3.23@sha256:8516dce0483394d5708d4b2ee6cacb79fb1d617ea4e2787c2 WORKDIR /build COPY package.json package-lock.json ./ RUN npm ci +COPY auth-session.js telemetry.js schedule-overview.js ./ +COPY admin/js/ ./admin/js/ +COPY react/ ./react/ COPY telemetry/ ./telemetry/ -COPY scripts/build-vendor.mjs ./scripts/build-vendor.mjs -RUN npm run build:vendor +COPY scripts/build-react.mjs scripts/build-vendor.mjs ./scripts/ +RUN npm run build FROM httpd:alpine3.23@sha256:4a15e9c73f25334bc03cfb3c692c9adfc103bb46ca89cee1f0b9a5fcbc7b21f6 @@ -17,15 +20,17 @@ COPY department/ /usr/local/apache2/htdocs/department/ COPY edu-office/ /usr/local/apache2/htdocs/edu-office/ COPY student/ /usr/local/apache2/htdocs/student/ COPY teacher/ /usr/local/apache2/htdocs/teacher/ -COPY index.html script.js style.css theme-toggle.js auth-session.js telemetry.js /usr/local/apache2/htdocs/ +COPY index.html style.css theme-toggle.js auth-session.js telemetry.js /usr/local/apache2/htdocs/ COPY ui-foundation.css schedule-overview.css schedule-overview.js /usr/local/apache2/htdocs/ +COPY --from=frontend-assets /build/dist/react/ /usr/local/apache2/htdocs/react/ COPY --from=frontend-assets /build/dist/vendor/ /usr/local/apache2/htdocs/vendor/ COPY security.conf /usr/local/apache2/conf/extra/magistr-security.conf +COPY react-cache.conf /usr/local/apache2/conf/extra/magistr-react-cache.conf COPY proxy.conf /usr/local/apache2/conf/extra/magistr-proxy.conf # Сервер раздаёт same-origin ресурсы и проксирует API во внутреннюю сеть Compose. RUN sed -i 's/^#LoadModule headers_module/LoadModule headers_module/' /usr/local/apache2/conf/httpd.conf \ && sed -i 's/^#LoadModule proxy_module/LoadModule proxy_module/' /usr/local/apache2/conf/httpd.conf \ && sed -i 's/^#LoadModule proxy_http_module/LoadModule proxy_http_module/' /usr/local/apache2/conf/httpd.conf \ - && printf '\nInclude conf/extra/magistr-security.conf\nInclude conf/extra/magistr-proxy.conf\n' >> /usr/local/apache2/conf/httpd.conf \ + && printf '\nInclude conf/extra/magistr-security.conf\nInclude conf/extra/magistr-react-cache.conf\nInclude conf/extra/magistr-proxy.conf\n' >> /usr/local/apache2/conf/httpd.conf \ && chown -R www-data:www-data /usr/local/apache2/htdocs/ diff --git a/frontend/admin/css/auditorium-workload.css b/frontend/admin/css/auditorium-workload.css index 79afd49..dd875c6 100644 --- a/frontend/admin/css/auditorium-workload.css +++ b/frontend/admin/css/auditorium-workload.css @@ -11,6 +11,22 @@ .workload-filter-row { flex-wrap: wrap; + align-items: flex-end; + gap: 1.5rem; + margin-bottom: 2rem; +} + +/* Заголовок карточки загруженности (вместо inline-стиля legacy-вьюхи) */ +.workload-card-header { + margin-bottom: 1.5rem; +} + +.workload-min-capacity { + width: 100px; +} + +.workload-date-field { + max-width: 200px; } .workload-display-control { diff --git a/frontend/admin/css/components.css b/frontend/admin/css/components.css index 0a28a66..1d51a8d 100755 --- a/frontend/admin/css/components.css +++ b/frontend/admin/css/components.css @@ -4186,3 +4186,251 @@ tbody tr:hover { .project-dialog-field { display: grid; gap: 0.4rem; margin-top: 1rem; } .project-dialog-field textarea { width: 100%; resize: vertical; } .project-dialog-error { min-height: 1.3rem; margin-top: 0.35rem; color: var(--danger, #dc2626); font-size: 0.85rem; } + +/* ===== Êëàññû ýòàïà 5 (âîëíà 1): çàìåíà inline-ñòèëåé ïåðåíåñ¸ííûõ âêëàäîê ===== */ + +/* Çàãîëîâîê êàðòî÷êè ñ äåéñòâèåì ñïðàâà (àóäèòîðèè) */ +.card-toolbar { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1rem; + flex-wrap: wrap; + gap: 0.5rem; +} + +.card-toolbar h2 { + margin: 0; +} + +/* Ñòðîêè è ãðóïïû ôîðì */ +.form-row--spacing { + margin-top: 1rem; +} + +.form-row--bottom { + align-items: flex-end; +} + +.form-group--wide { + flex: 2; +} + +.form-group--flush { + margin-bottom: 0; +} + +.form-group--submit { + display: flex; + align-items: flex-end; +} + +.btn-block { + width: 100%; +} + +.btn-inline { + height: fit-content; + margin-bottom: 0; +} + +/* ß÷åéêè òàáëèö */ +.th-actions { + text-align: right; +} + +.table-cell-actions { + text-align: right; + white-space: nowrap; +} + +.badge-list { + display: flex; + flex-wrap: wrap; + gap: 0.25rem; +} + +.badge--tight { + margin: 0.1rem; +} + +.badge--lesson-type { + font-size: 0.75rem; +} + +/* Ìîäàëüíûå îêíà ïåðåíåñ¸ííûõ âêëàäîê */ +.modal-content--medium { + max-width: 600px; + width: 90%; +} + +.modal-header-row { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1.5rem; + gap: 1rem; +} + +.modal-subtitle { + color: var(--text-secondary); + font-size: 0.9rem; + margin-top: 0.25rem; +} + +.modal-subcard { + margin-bottom: 1.5rem; + background: var(--bg-secondary); +} + +.modal-subcard h3 { + margin-top: 0; +} + +/* Ñîñòîÿíèå çàãðóçêè lazy-âêëàäêè */ +.tab-loading { + display: flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + padding: 2rem; + color: var(--text-secondary); +} + +/* Äàøáîðä: ìåòðèêè è ìîíèòîðèíã */ +.dashboard-metric-label { + font-size: 0.85rem; + color: var(--text-secondary); + font-weight: 500; +} + +.dashboard-metric-value { + margin: 0.25rem 0 0; + font-size: 1.5rem; + font-weight: 700; +} + +.dashboard-monitor-table table { + width: 100%; + border-collapse: collapse; +} + +.dashboard-monitor-table th, +.dashboard-monitor-table td { + text-align: left; + padding: 0.75rem; +} + +.dashboard-state-row { + padding: 1.5rem; + text-align: center; + color: var(--text-secondary); +} + +.dashboard-state-row--wide { + padding: 2rem; +} + +.dashboard-free-hint { + color: var(--text-secondary); + font-size: 0.9rem; +} + +.dashboard-free-busy { + color: var(--error); + font-size: 0.9rem; + font-weight: 500; +} + +.dashboard-free-badge { + padding: 0.4rem 0.6rem; + font-size: 0.85rem; + background: rgba(16, 185, 129, 0.1); + color: #10b981; + border: 1px solid rgba(16, 185, 129, 0.2); +} + +/* Äàøáîðä: öåíòð ïðåäóïðåæäåíèé (Red Zone) */ +.red-zone-card { + border-top: 4px solid var(--error, #ef4444); +} + +.red-zone-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1rem; + gap: 1rem; + flex-wrap: wrap; +} + +.red-zone-title { + margin: 0; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.red-zone-subtitle { + color: var(--text-secondary); + font-size: 0.85rem; + margin-top: 0.25rem; +} + +.red-zone-conflicts { + display: flex; + flex-direction: column; + gap: 0.75rem; + margin-top: 1rem; +} + +.dashboard-check-loading { + text-align: center; + padding: 2rem; + color: var(--text-secondary); +} + +/* ===== Ýòàï 5 (âîëíà 2): êëàññû inline-ñòèëåé ïåðåíåñ¸ííûõ âêëàäîê ===== */ + +/* Øèðîêèå ìîäàëüíûå îêíà (ïîäãðóïïû, ïðîôèëè, êàëåíäàðíûå ãðàôèêè) */ +.modal-content--wide { + max-width: 700px; + width: 90%; +} + +.modal-content--xwide { + max-width: 800px; + width: 90%; +} + +.modal-content--full { + max-width: 960px; + width: 90%; +} + +/* Ôîðìà ïðîôèëÿ íà âêëàäêå ñòðóêòóðû âóçà */ +.profile-form-spec { + flex: 1; + min-width: 200px; +} + +.profile-form-name { + flex: 1.5; + min-width: 250px; +} + +.profile-form-description { + flex: 2; + min-width: 250px; +} + +/* Ðÿä êíîïîê ôîðìû, âûðîâíåííûõ ïî íèæíåé êðîìêå */ +.form-actions-row { + display: flex; + gap: 0.5rem; + align-items: flex-end; +} +/* Âîëíà 3 (ýòàï 5): çàìåíà inline-ñòèëÿ èç legacy department-workspace.html */ +.schedule-section-label--flush { + margin: 0; +} \ No newline at end of file diff --git a/frontend/admin/css/layout.css b/frontend/admin/css/layout.css index bcfa5cc..4a2856e 100755 --- a/frontend/admin/css/layout.css +++ b/frontend/admin/css/layout.css @@ -265,6 +265,20 @@ margin: 0.25rem 0.5rem; } +/* ===== Корневой контейнер ===== */ + +/* body — flex-контейнер, поэтому без явного растяжения корневой + div сжимается до max-content контента: страницы с узким контентом + (карточки, формы) не занимают всю ширину монитора, а дашборд + «растягивался» только после загрузки данных. Растягиваем корень + ровно на ширину окна (flex-basis 0), а не под контент: intrinsic-замер + max-content раздувает вложенные flex-фильтры (например, панель + загруженности аудиторий) до ~1 млн px горизонтального скролла. */ +#admin-root { + flex: 1 1 0%; + min-width: 0; +} + /* ===== Main ===== */ .main { flex: 1; diff --git a/frontend/admin/index.html b/frontend/admin/index.html index 5335343..688924c 100755 --- a/frontend/admin/index.html +++ b/frontend/admin/index.html @@ -19,243 +19,9 @@
- - - - - - - -