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
This commit is contained in:
280
docs/FRONTEND.md
280
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
|
||||
<a href="#" class="nav-item" data-tab="users">Пользователи</a>
|
||||
<a href="#" class="nav-item" data-tab="groups">Группы</a>
|
||||
<a href="#" class="nav-item" data-tab="schedule">Расписание занятий</a>
|
||||
<a href="#" class="nav-item" data-tab="academic-calendar">Календарный график</a>
|
||||
```
|
||||
- при старте активная вкладка читается из `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`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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`,
|
||||
|
||||
675
docs/REACT_MIGRATION.md
Normal file
675
docs/REACT_MIGRATION.md
Normal file
@@ -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
|
||||
```
|
||||
101
docs/REACT_MIGRATION_RESULT.md
Normal file
101
docs/REACT_MIGRATION_RESULT.md
Normal file
@@ -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
|
||||
```
|
||||
@@ -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 (реверс-прокси) |
|
||||
|
||||
Reference in New Issue
Block a user