# 🎨 Frontend ## Общая информация | Параметр | Значение | |----------|----------| | **Фреймворк** | Переходный режим: 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-контрактов. --- ## Структура файлов ``` frontend/ ├── package.json # Exact build-зависимости и команды проверки ├── package-lock.json # Зафиксированное npm-дерево ├── auth-session.js # Access JWT и профиль сессии только в памяти вкладки ├── telemetry.js # Same-origin загрузчик собранного OTel bundle ├── 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 # Составное название календарного графика │ ├── academic-calendar-tooltip.test.mjs # Позиционирование подсказки возле курсора и границ viewport │ ├── 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 # Метрики качества, фильтры проблем и доступность вкладки │ ├── schedule-versions.test.mjs # Статусы версий, журнал и навигация в черновик/качество │ ├── schedule-view-semesters.test.mjs # Выбор семестра и расчёт двухнедельного диапазона просмотра │ ├── teacher-absences.test.mjs # Payload мастера замены и доступность вкладки по ролям │ ├── teacher-preferences.test.mjs # Календарь пожеланий, заявки и подсказки конструктора │ └── security-policy.test.mjs # Web Storage, XSS, пароли, язык, CSP и Dockerfile ├── index.html # 🔐 Страница авторизации (общая) ├── style.css # Стили страницы авторизации ├── theme-toggle.js # Переключение светлой/тёмной темы ├── Dockerfile # Multi-stage: npm bundle → Apache httpd │ ├── admin/ # 👨‍💼 Интерфейс администратора │ ├── index.html # Монтирует React-оболочку `/react/admin.js` │ ├── css/ │ │ ├── main.css # CSS-переменные, цвета, типографика │ │ ├── layout.css # Раскладка (sidebar, topbar, content) │ │ ├── components.css # Кнопки, таблицы, карточки, формы │ │ ├── modals.css # Модальные окна │ │ ├── auditorium-workload.css # Таблицы расписаний и загруженности │ │ ├── departments-data.css # Стили создания кафедры/специальности │ │ ├── teacher-absences.css # Отсутствия, пожелания и очередь заявок преподавателей │ │ ├── schedule-quality.css # Диагностический экран качества расписания │ │ └── schedule-versions.css # Контур публикации, diff и журнал версий │ ├── js/ │ │ ├── role-capabilities.js # Единая матрица вкладок admin/settings по ролям │ │ ├── api.js # HTTP-обёртка (fetch + Authorization) │ │ ├── dashboard-conflicts.js # Чистые функции дат, загрузки и состояний Red Zone │ │ ├── date-input.js # Маска ДД.ММ.ГГГГ и преобразование дат в ISO │ │ ├── dialog.js # Доступные подтверждения, сообщения и ввод причины │ │ ├── dirty-state.js # Защита несохранённых изменений │ │ ├── url-state.js # Чтение и запись фильтров в query string │ │ ├── view-state.js # Единые loading/empty/error-состояния таблиц │ │ ├── academic-period.js # Каскад учебного года и семестра в профильных вкладках │ │ ├── utils.js # Утилиты │ │ ├── schedule-period.js # Выбор ближайшего учебного периода для расписаний и нагрузки │ │ └── views/ # Общие чистые хелперы (используются React-сервисами) │ │ ├── academic-calendar-grid.js # Расчёт ISO-недели дневной сетки (общий с React-сервисом) │ │ └── academic-calendar-title.js # Название из кода, профиля, формы и года (общий с React-сервисом) │ │ │ └── settings/ # ⚙️ Страница настроек (отдельный SPA на React) │ ├── index.html # Оболочка с корневым элементом settings-root │ └── css/ │ ├── main.css # CSS-переменные, базовые стили │ └── layout.css # Sidebar, topbar, content │ ├── teacher/ # 👩‍🏫 Интерфейс преподавателя (React) │ ├── index.html # Оболочка с корневым элементом teacher-root │ └── style.css # Адаптивный кабинет в стиле диспетчерского журнала ├── department/ # 🏛 Кабинет кафедры │ └── index.html # Redirect в `/admin/#department-workspace` ├── edu-office/ # 🗓 Кабинет учебного отдела │ └── index.html # Redirect в `/admin/#schedule-view` │ └── student/ # 🎓 Интерфейс студента (React) ├── index.html # Оболочка с корневым элементом student-root └── style.css # Стили кабинета без inline-блока ``` --- ## Система маршрутизации (Admin SPA) Оболочка админ-панели работает на **React** (`react/admin/AdminApp.jsx`, entrypoint `/react/admin.js`): sidebar с секциями, topbar с заголовком вкладки и счётчиком заявок, тема, сворачивание панели, настройки с выходом и hash-router. Навигация реализована через hash-адреса вкладок (`#users`, `#schedule` и т. д.): - при старте активная вкладка читается из `location.hash` (неразрешённые роли значения заменяются вкладкой по умолчанию); - слушатель `hashchange` синхронизирует вкладку с адресной строкой (работают кнопки «назад/вперёд» и ссылки из других кабинетов); - переключение идёт через `requestTabSwitch` с guard'ом несохранённых изменений из общего модуля `admin/js/dirty-state.js`. Содержимое вкладки выбирается по реестру `react/admin/react-tabs.js`: - **все 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` удалены. | Роль | Доступные вкладки | |------|-------------------| | `ADMIN` | Все вкладки | | `EDUCATION_OFFICE` | Просмотр, конструктор и анализ качества расписаний, отсутствия и замены, календарный график, загруженность и аудитории; в settings — временные слоты и формы обучения | | `DEPARTMENT` | Кабинет кафедры, просмотр расписаний, отсутствия и подтверждение заявок своих преподавателей | | `SCHEDULE_VIEWER` | Только просмотр расписаний | Пути `/department/` и `/edu-office/` оставлены как входные redirect-страницы в общую панель. Отдельные кабинеты не дублируют UI админ-панели. Пункты меню, недоступные роли, не попадают в sidebar: React-оболочка фильтрует секции навигации по матрице ролей до рендера. Верхние панели основной админки и settings SPA не содержат неработающей кнопки бургер-меню. В светлой теме пункты и заголовки sidebar получают контрастные тёмные цвета для всех ролей, включая `DEPARTMENT`. ### Разделы админ-панели | Tab | Описание | API | |-----|----------|-----| | `dashboard` | Сводные метрики и проверка конфликтов текущей недели с явным статусом полноты данных | `/api/departments`, `/api/classrooms`, `/api/groups`, `/api/users/teachers`, `/api/schedule/search`, `/api/admin/time-slots` | | `teacher-requests` | Очередь заявок кафедр на создание преподавателей с поиском, статусом, пагинацией и редактированием перед одобрением | `/api/teacher-requests/page`, `/api/departments` | | `users` | CRUD пользователей с серверными поиском, ролевым фильтром, сортировкой и пагинацией | `/api/users/page` | | `groups` | CRUD групп, серверный поиск и пагинация, мультифильтр по формам обучения, настройка 0/2/3 подгрупп и поиск совместимого графика | `/api/groups/page`, `/api/groups/options`, `/api/admin/academic-calendars/options`, `/api/groups/{id}/subgroups` | | `classrooms` | Аудитории | `/api/classrooms` | | `subjects` | Дисциплины с серверным реестром, привязками только для текущей страницы и асинхронным выбором преподавателя | `/api/subjects/page`, `/api/teacher-subjects?subjectId=…`, `/api/users/teachers/options` | | `university-structure` | Кафедры, специальности и профили обучения; профили доступны отдельной внутренней вкладкой и через кнопку специальности | `/api/departments`, `/api/specialties`, `/api/specialties/{id}/profiles` | | `department-workspace` | Кабинет кафедры: дисциплины, импорт, комментарии, преподаватели, привязка преподавателей, заявки на новых преподавателей и нагрузка | `/api/department/*`, `/api/department/teacher-requests`, `/api/workload/teachers` | | `schedule-view` | Просмотр опубликованного расписания: учебный год и семестр выбираются каскадом в дополнительных фильтрах, поисковые списки сущностей при открытии остаются свёрнутыми и используют размытый непрозрачный слой меню, найденные расписания переключаются без общего контекста страницы, а `ADMIN` и `EDUCATION_OFFICE` редактируют конкретное занятие в боковой панели без изменения правила | `/api/schedule/semesters`, `/api/schedule/search`, `/api/edu-office/schedule/overrides`, `/api/admin/time-slots/effective` | | `teacher-absences` | Запросы преподавателей: отсутствия и мастер замены, согласование семестровых пожеланий, заявки на перенос, аудиторию или отмену; преподаватель ищется асинхронно по всем кафедрам для `ADMIN`/`EDUCATION_OFFICE` и только по своей кафедре для `DEPARTMENT`; поисковое меню размывает фон, а фильтры согласования оформлены для светлой и тёмной тем | `/api/teacher-absences`, `/api/teacher-preferences`, `/api/teacher-change-requests`, `/api/users/teachers/options` | | `schedule` | Конструктор правил: локальный каскад `Учебный год → Семестр → Версия` открывает опубликованное расписание или черновик; первичная загрузка скрывает нестабильную компоновку до подготовки полей, а таблица правил использует фиксированные колонки и серверную пагинацию; группы, преподаватели и аудитории ищутся на сервере, подгруппы загружаются только для выбранных групп; предупреждение о несохранённых изменениях появляется только после действия пользователя | `/api/admin/schedule-rules`, `/api/edu-office/schedule/versions`, `/api/teacher-preferences`, `/api/admin/time-slots`, `/api/admin/calendar/years`, `/api/lesson-types`, `/api/groups/options`, `/api/subgroups?groupId=…`, `/api/users/teachers/options`, `/api/classrooms/options` | | `schedule-versions` | Контур публикации с локальным выбором учебного года и семестра: текущая версия, черновики, diff, архив, восстановление и журнал | `/api/edu-office/schedule/versions` | | `schedule-quality` | Диагностика выбранной версии через каскад `Учебный год → Семестр → Версия`: оценка, метрики и проблемные правила с переходом к их редактированию | `/api/edu-office/schedule/quality`, `/api/admin/schedule-rules` | | `academic-calendar` | Учебные годы, семестры, создание календарных графиков, Excel-подобный редактор дневной сетки и привязка дисциплин к семестрам графика; редко используемая форма создания учебного года расположена внизу вкладки `Графики` | `/api/admin/calendar`, `/api/admin/academic-calendars`, `/api/admin/academic-calendars/{id}/subjects`, `/api/admin/calendar/activity-types`, `/api/specialties`, `/api/specialties/{id}/profiles`, `/api/education-forms`, `/api/subjects` | | `auditorium-workload` | Динамическая загруженность аудиторий, преподавателей и кафедр: сводная матрица по дате или совмещённая таблица выбранной сущности по чётной/нечётной неделе | `/api/classrooms`, `/api/users/teachers`, `/api/departments`, `/api/admin/time-slots`, `/api/equipments`, `/api/groups`, `/api/schedule`, `/api/admin/calendar/years` | ### Особенности админских вкладок - Общий `formatLocalDate()` формирует `YYYY-MM-DD` из локальных компонентов `Date`, не используя `toISOString()`. Кабинет кафедры и редактор академического календаря также выполняют календарную арифметику локальными компонентами, поэтому даты первых часов суток и границы месяца не сдвигаются из-за преобразования в UTC. - Вкладка `dashboard` формирует date-only значения из локальных компонентов даты, а текущую неделю — от отдельного объекта понедельника до `понедельник + 6 дней`. Расписания кафедр загружаются независимо через `Promise.allSettled`: `COMPLETE` означает ответы всех кафедр, `PARTIAL` — только части, `NOT_RUN` — отсутствие пригодных ответов или кафедр. Зелёная карточка «Конфликты расписания не обнаружены» разрешена только для `COMPLETE` без найденных конфликтов; частичный результат всегда остаётся предупреждением, а полный отказ показывается как «Проверка не выполнена». Технические причины отказов в DOM не выводятся. - Вкладка `groups` загружает кафедры, специальности, профили, учебные годы и календарные графики. Список групп открывается через `/api/groups?includeArchived=true`, поэтому в таблице видны активные, будущие, завершившие обучение и архивные группы со статусом. Группа создаётся через `/api/groups` с `specialtyId` и `specialtyProfileId`, а модалка редактирования использует широкую сетку полей без внутреннего пустого скролла. Блок подгрупп использует `/api/subgroups` и `/api/groups/{id}/subgroups`, а блок назначений использует `/api/groups/{id}/calendar-assignments`. После назначения графика в таблице назначений сразу выводятся дисциплины графика, сгруппированные по номерам семестров. В селекты подгрупп и назначений попадают только группы с `active=true`. - Вкладка `teacher-requests` показывает pending-заявки кафедр на создание преподавателей. Администратор может скорректировать кафедру, логин, ФИО и должность, задать пароль минимум 8 символов в скрытом поле с `autocomplete="new-password"`, затем одобрить заявку через `/api/teacher-requests/{id}/approve` или отклонить её через `/api/teacher-requests/{id}/reject`. Для роли `ADMIN` счётчик pending-заявок выводится в пункте меню «Заявки» и рядом с заголовком страницы, чтобы очередь была видна без открытия вкладки. - Вкладка `department-workspace` в блоке преподавателей объединяет данные `/api/department/teachers` и `/api/workload/teachers`: каждый преподаватель показывается одной карточкой с должностью и нагрузкой за выбранный период, преподаватели без занятий получают нулевую нагрузку, а преподаватели из расписания добавляются без дублей. Между семестрами период автоматически переносится на первые две недели ближайшего будущего семестра, чтобы будущие правила не выглядели как нулевая нагрузка; при недоступности справочника остаётся диапазон от текущей даты. Если дата начала периода выбрана позже даты окончания, поле окончания очищается, а расчёт нагрузки ждёт корректный период. - Вкладка `department-workspace` позволяет кафедре добавить существующего активного преподавателя на свою кафедру через `/api/department/teachers/{teacherId}/assignments`, отправить заявку на нового преподавателя через `/api/department/teacher-requests` и видеть статусы собственных заявок в таблице. - Вкладка `teacher-absences` доступна администратору, учебному отделу и кафедре. Верхний командный блок показывает очередь инцидентов, форма регистрирует преподавателя, период и причину, а реестр разделяет статусы согласования. Полноэкранный мастер выводит каждое затронутое занятие отдельной строкой и предлагает только кандидатов, уже проверенных backend. Пустая строка не отправляется; выбранные решения применяются одним пакетом. Ниже расположены реестр пожеланий на семестр и очередь заявок на изменение опубликованных пар. Пожелание можно принять или отклонить с комментарием. Заявку применяют только `ADMIN` и `EDUCATION_OFFICE`; кафедра видит записи своих преподавателей без управляющих действий. Фильтры обоих реестров используют тематические кастомные списки с отдельными поверхностями для светлой и тёмной тем, а поиск преподавателя — общее размытое меню `AsyncCombobox`. Каждая карточка показывает исходное и запрошенное состояние, результат предварительной проверки и хронологию решения. - Вкладка `schedule-quality` доступна администратору и учебному отделу. После каскадного выбора учебного года, семестра и версии она выводит круговую оценку от 0 до 100, карточки метрик и реестр правил с фильтрами по серьёзности и типу. Все проблемные занятия одного правила показаны одной карточкой; в ней выводятся число затронутых занятий, категории и суммарный штраф. Кнопка `Изменить правило` открывает выбранную версию в конструкторе и сразу заполняет форму этим правилом. Сценарий одинаков для `PUBLISHED` и `DRAFT`; `ARCHIVED` анализируется только для чтения. Переход между вкладками передаёт ID версии и правила через одноразовые ключи `magistr.schedule.openVersionId` и `magistr.schedule.openRuleId` в `localStorage`. - Вкладка `schedule-versions` доступна администратору и учебному отделу и оформлена как отдельный контур публикации. В верхней карточке учебный год сначала ограничивает список семестров, поэтому архив периодов не превращается в один длинный селект. Карточка показывает версию, которую видят конечные пользователи; ниже расположены черновики, сравнение правил и занятий, архив и журнал. В конструкторе локальный каскад `Учебный год → Семестр → Версия` по умолчанию выбирает опубликованную версию текущего семестра и выделяет её зелёным контуром; её изменения применяются сразу. Селект позволяет перейти в медный режим черновика. Кнопка `Создать черновик` копирует выбранное опубликованное расписание или черновик и сразу открывает новую рабочую копию. Перед публикацией интерфейс одновременно запрашивает полную валидацию и diff, требует причину и блокирует действие при ошибках. Кнопки версий передают их ID одноразово через `localStorage` и открывают конструктор или анализ качества в нужном контексте; архивную публикацию можно восстановить с причиной. - Компоновка `department-workspace` использует собственные CSS-сетки `department-workspace-filter-grid` и `department-workspace-actions-grid`: фильтры периода отделены от сеток расписания, загрузка дисциплин занимает широкую колонку, формы преподавателей выравниваются справа, а списки и таблицы идут полноширинными блоками ниже. - Вкладка `schedule-view` показывает найденные занятия в режиме одной активной таблицы. Пользователь выбирает, что смотреть: группу, преподавателя, аудиторию или кафедру; поисковые меню группы, преподавателя и аудитории используют плотный фон с `backdrop-filter`, чтобы строки не смешивались с карточками под списком. Основная кнопка `Показать` расположена в заголовке блока параметров, а пустое состояние таблицы с подсказкой об обновлении содержит дополнительную кнопку `Показать расписание`. В дополнительных фильтрах учебный год каскадно ограничивает список семестров из `/api/schedule/semesters`; оба значения сохраняются в URL. При загрузке автоматически выбирается текущий семестр, а между учебными периодами — ближайший будущий; если будущего нет, используется последний завершённый. Экран всегда показывает опубликованную версию: выбор черновика остаётся в `schedule-quality` и `schedule`. Для выбранного семестра frontend запрашивает весь период от его первой до последней даты и собирает повторяющиеся занятия в одну семестровую матрицу. На странице не выводится стек таблиц: виден один выбранный результат, а остальные доступны через чипы и кнопки предыдущего/следующего расписания; чипы результатов переносятся и отделены от счётчика стабильным отступом. Для режима кафедры и роли `DEPARTMENT` расписание ограничивается действующей кафедральной связью преподавателя на дату занятия; группы другой кафедры не отбрасываются, если занятие ведёт преподаватель текущей кафедры. Преподавательские и студенческие отдельные страницы пока остаются самостоятельными. Таблица строится как строки пар и столбцы дней недели. Нечётная неделя отображается в верхней половине ячейки, чётная — в нижней, а одинаковые занятия в обе недели схлопываются в цельную ячейку. Каждая карточка занятия явно показывает фактические границы недель: `(с 1 по 18 нед.)` либо `(7 нед.)`. На мобильной ширине вместо широкой матрицы показывается один день активного расписания с переключателем дней. - Для `ADMIN` и `EDUCATION_OFFICE` карточка занятия содержит кнопку `Изменить`, а уже изменённая пара — индикатор разовой правки. Справа открывается полупрозрачная боковая панель с размытием содержимого под ней; внешний затемнённый слой также размывает страницу, а на мобильном устройстве панель занимает весь экран. Режим `Редактирование` сравнивает `Было по правилу / Станет`, позволяет изменить дату, эффективный временной слот, преподавателя, аудиторию, формат и комментарий, отменить занятие или удалить override через `Вернуть по правилу`. Преподаватель и аудитория ищутся асинхронно через `/api/users/teachers/options` и `/api/classrooms/options`; в подписи аудитории показывается её название. По умолчанию выводятся семь дней исходной недели; кнопка `Выбрать другую дату` раскрывает календарь всего семестра, где неучебные даты отключены. После смены даты загружается эффективная сетка дня: сначала выбирается тот же ID слота, затем совпадающий интервал, иначе требуется ручной выбор. Дата или время формируют `MOVE`, а только преподаватель, аудитория или формат — `REPLACE`. - Режим `Изменения за период` загружает overrides, у которых в двухнедельный диапазон попала исходная или целевая дата. Поэтому в реестре остаются отменённые занятия и входящие/исходящие переносы, которых нет на исходном месте в таблице. После сохранения, отмены или возврата расписание и реестр обновляются без смены выбранного периода. Для `DEPARTMENT` и `SCHEDULE_VIEWER` вкладка остаётся полностью read-only: кнопка, панель и запросы управляющего API не создаются. - Вкладка `auditorium-workload` стала общей вкладкой `Загруженность`: в поле «Что смотреть» выбираются аудитории, преподаватели или кафедры. Сводная матрица по выбранной дате использует одинаковую структуру: строки — выбранный тип сущности, столбцы — эффективные временные слоты дня из `/api/admin/time-slots/effective`, занятость собирается из динамического расписания `/api/schedule` по группам. Кафедральная матрица группирует занятия по кафедре преподавателя. Для аудиторий доступны фильтры корпуса, вместимости и оборудования. В поле «Отображение» можно выбрать конкретную аудиторию, преподавателя или кафедру; тогда сводная матрица заменяется одной таблицей по дням недели и времени для двухнедельного периода от выбранной даты. Таблица выбранной сущности растягивается до нижней части экрана. Ячейка делится вертикально только если верхняя и нижняя недели отличаются: нечётная неделя отображается сверху, чётная — снизу. Если состояние или занятие одинаковое, ячейка остаётся цельной. Чётность берётся из расписания, а для свободных дней рассчитывается по семестрам из `/api/admin/calendar/years`. - Вкладка `university-structure` содержит внутренние разделы `Кафедры`, `Специальности` и `Профили`, оформленные тем же визуальным паттерном вкладок, что и `academic-calendar`: администратор создаёт профили как из общего списка, так и через кнопку `Профили` у конкретной специальности. - Таблицы правил конструктора и календарных графиков загружают страницы с сервера: по умолчанию 25 записей, доступны 50 и 100. Поиск правил работает по дисциплине, группе и ID; поиск графиков — по названию, направлению, профилю, форме обучения и году, дополнительно доступен фильтр учебного года. Поиск сбрасывает страницу, устаревшие ответы не заменяют актуальную выдачу. Скрытая визуальная матрица конструктора загружается и строится только при открытии; переход из анализа качества получает нужное правило по ID независимо от страницы таблицы. Программное обновление селектов периода обновляет обёртки без повторного вызова обработчиков смены года и семестра. - В разделе календарного графика списки графиков в «Сетках» и «Дисциплинах» используют `AsyncCombobox` и `/api/admin/academic-calendars/options`: поиск на сервере с задержкой 320 мс, до 20 результатов плюс выбранная запись. Полный справочник графиков не загружается в DOM; выбор редактора не зависит от страницы таблицы. Отмена перехода с несохранёнными дисциплинами восстанавливает выбранный график, устаревшие ответы загрузки игнорируются, а сохранение сетки требует предварительной загрузки именно выбранного графика. - В «Структуре вуза» кафедры, специальности и профили имеют отдельный поиск и клиентскую пагинацию по 25/50/100 строк. Поиск учитывает названия, коды и ID, а у профилей также специальность и описание. - В «Качестве расписания» правила после фильтрации упорядочиваются по суммарному штрафу по убыванию; при равном штрафе сохраняется исходный порядок. - Вкладка `schedule` имеет заголовок `Конструктор правил` и не обращается к старым `lessons` API. До завершения первичной загрузки справочников и правил вкладка показывает единое состояние загрузки, после чего открывает уже подготовленную форму; таблица правил использует `table-layout: fixed`, явный `colgroup` и отключённую анимацию строк, поэтому заголовки колонок не смещаются при замене состояния загрузки данными. Создание и редактирование расписания выполняется через правила `/api/admin/schedule-rules`, где каждое правило содержит группы, отдельные часы и недели начала для лекций, лабораторных и практик, а также набор базовых слотов. Для выбранных преподавателя и семестра frontend загружает согласованные `/api/teacher-preferences` и выводит под строкой слота компактные маркеры строгой недоступности, предпочтительного или нежелательного времени и пожеланий «пары подряд»/«без окон»; количество недоступных дат показывается отдельным маркером. Группы выбираются через выпадающий мультиселект. Поле подгруппы появляется только при выборе лабораторной работы; для лекций и практик оно не отображается. Если в правиле выбрана одна группа, селект подгруппы содержит пункт `Вся группа`; если выбрано несколько групп, лабораторный слот показывает мультиселект подгрупп, чтобы выбрать разные подгруппы разных групп. Типы занятий в слоте сортируются в порядке: лекция, лабораторная работа, практика. Единственный слот имеет действие `Очистить`, которое сбрасывает его поля; при наличии нескольких слотов у каждой строки показывается действие `Удалить`. Оба действия используют размер формы, минимальную ширину `125px` и высоту `44px`, поэтому совпадают по масштабу с соседними селектами. Список слотов отображается без внутреннего вертикального скролла: при добавлении строк форма расширяется вниз, а кнопка сохранения остаётся отдельным блоком под слотами. Из календарной системы здесь используется список семестров для выбора периода действия правила. Справа доступна сворачиваемая визуальная матрица: пользователь выбирает учебный год, семестр и группы, после чего матрица строится только по правилам выбранного семестра. Столбцы — выбранные в фильтре группы, строки — только день и время, где есть активные пары, ячейки показывают дисциплину, диапазон недель, тип, формат, преподавателя, аудиторию и подгруппы. Период недель не показывается для занятия на весь семестр; если занятие идёт до конца семестра не с первой недели, выводится только неделя начала в формате `(с 5 нед.)`, а ограниченный диапазон — как `(с 1 по 3 нед.)`. Если нечётная и чётная недели отличаются, ячейка делится на две половины; одинаковые занятия схлопываются в цельную ячейку. Кнопка с тремя точками в правой части карточки пары открывает контекстное меню возле нажатой кнопки, с автоматическим разворотом от границ viewport: можно открыть полное правило в форме, изменить только день и базовую пару выбранного слота через компактную модалку или удалить правило целиком. В списке правил действия отображаются едиными кнопками одинакового размера с отступами между ними. - Вкладка `academic-calendar` полностью отделяет календарную систему от расписания занятий и внутри себя разделена на три вкладки: `Графики` для учебных годов, семестров и карточек календарных графиков, `Сетки` для редактора дневной сетки, `Дисциплины` для ручной привязки дисциплин из `/api/subjects` к номерам учебных семестров графика. Форма редкого ежегодного действия — создания учебного года вместе с семестрами — расположена после реестров учебных годов и календарных графиков, в нижней части вкладки `Графики`. Пользователь вводит только четырёхзначный год начала, например `2026`; интерфейс подставляет год окончания `2027`, формирует название `2026-2027` и внутренний период `01.09.2026–30.06.2027`. Границы семестров можно скорректировать до сохранения. У календарного графика нет поля ввода и отдельного столбца названия; отображаемая подпись и сохраняемое backend название автоматически собираются как `код специальности — профиль обучения — форма обучения — учебный год`. Frontend не отправляет `title` календарного графика в payload. Администратор выбирает форму обучения из общего справочника `/api/education-forms`, заполняет дневную сетку по курсам и кодам активностей, назначает ручную временную сетку на конкретную дату, а сохранение сетки идёт через `/api/admin/academic-calendars/{id}/grid`. Каждый код активности окрашивает ячейку, подсказку, диалог и итоговый чип своим `colorCode`. REST-контракт сетки остаётся дневным: backend объединяет соседние даты с одинаковой активностью в периоды при записи и разворачивает их при чтении. Шесть полей дат принимают до восьми цифр, автоматически показывают их в формате `ДД.ММ.ГГГГ`, ограничивают год четырьмя цифрами и перед отправкой преобразуют значение в ISO `ГГГГ-ММ-ДД`; два поля учебного года принимают и показывают только четыре цифры. - При сохранении правила во вкладке `schedule` frontend различает `409 Conflict` от остальных ошибок API. Если backend возвращает `conflictRule`, открывается широкое модальное окно разрешения конфликта: пользователь видит новое и ранее созданное правило, чипы причины конфликта из `conflictReasons` и подсветку соответствующих полей (`teacher`, `classroom`, `group`). Пользователь меняет у конфликтующего правила день, чётность, пару, преподавателя или аудиторию, после чего frontend сохраняет конфликтующее правило через `PUT /api/admin/schedule-rules/{id}` и автоматически повторяет исходный `POST` или `PUT`. Если после переноса появляется следующий конфликт, показывается следующее конфликтующее правило без потери исходного черновика. - Редактор годового графика во вкладке `academic-calendar` показывает компактную табличную сетку: курсы раскрываются отдельными секциями со стрелкой, семестры внутри курса на широком экране идут рядом в две равные колонки одинаковой высоты, столбцы подписаны номерами недель учебного года, строки — днями недели, а ячейки содержат буквенный код активности. Недели выровнены по интервалам `понедельник–воскресенье`: если учебный год начинается не в понедельник, предшествующие позиции первой недели остаются пустыми, а следующий понедельник открывает неделю 2. В рамках одного курса семестровые таблицы получают одинаковое число недельных колонок: недостающие колонки заполняются пустыми ячейками, поэтому левая и правая части занимают всю ширину секции курса без внешней пустоты. Подробная расшифровка и изменение кода открываются в компактном модальном окне по клику на ячейку; при наведении возле курсора отображается кастомная подсказка с датой, курсом, кодом активности и временной сеткой. У границ viewport подсказка автоматически раскрывается в противоположную сторону и остаётся полностью видимой, а код активности в ней центрируется внутри квадратного индикатора. Ячейки можно выделять протяжкой мышью и менять код активности через ту же модалку, выбранные ячейки подсвечиваются мягкой заливкой, а кнопка «Применить» закрывает окно. - Вкладка `classrooms` теперь показывает архивные аудитории через `includeArchived=true`. Кнопка удаления заменена на архивирование: аудитория выводится из эксплуатации, но остаётся в историческом расписании. Для архивных аудиторий доступно восстановление. - Вкладка `users` поддерживает роли `EDUCATION_OFFICE`, `DEPARTMENT` и `SCHEDULE_VIEWER`; удаление пользователя работает как архивирование. ### Общие UX-компоненты первого пакета - Общей панели учебного контекста под topbar нет. Каскадные селекты периода размещены только в профильных вкладках: `schedule`, `schedule-versions`, `schedule-quality` и `schedule-view`. Календарный график и назначение графика группе сохраняют собственные локальные селекты. - Вкладка и фильтры сохраняются в URL. Реестры групп, пользователей, дисциплин и заявок восстанавливают поиск, фильтры, сортировку, номер и размер страницы после reload или возврата по ссылке. Просмотр расписания сохраняет вид сущности, выбранную сущность, дату, учебный год, семестр и дополнительные фильтры; конечные кабинеты сохраняют неделю, вкладку и группу. - Общий React-компонент `react/shared/ui/Pagination.jsx` работает с общим ответом `PageResponse`; размер страницы — 25, 50 или 100. `view-state.js` унифицирует загрузку со spinner, пустой результат и ошибку с кнопкой повторного запроса. - `AsyncCombobox` загружает варианты с сервера, сохраняет выбранный ID, поддерживает клавиатуру и роли `combobox/listbox`. Он используется для групп, преподавателей, аудиторий и совместимых календарных графиков; предварительная загрузка вариантов не раскрывает меню. Студент больше не загружает полный список групп. - `dialog.js` заменяет нативные `confirm`, `prompt` и `alert`: возвращает фокус, удерживает его внутри окна, закрывается по Escape и может требовать причину. Нативных диалогов в admin/settings-модулях нет. - `dirty-state.js` предупреждает при смене вкладки, версии, графика или закрытии страницы. Календарная сетка и привязки дисциплин показывают маркер `•` на кнопке сохранения; конструктор правил отслеживает изменения полей и состава слотов, но игнорирует программную синхронизацию селектов при загрузке и сбросе формы. - `trackUxEvent()` создаёт базовые события смены раздела, загрузки реестров и расписания, сохранения календаря/правила и просмотра ближайших занятий. В production они преобразуются в OpenTelemetry spans, а до готовности bundle хранятся в ограниченной очереди. ### Страница настроек (`/admin/settings/`) Страница переведена на 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/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` --- ## API-клиент (`api.js`) Все защищённые HTTP-запросы проходят через `fetchWithAuth()` из `auth-session.js`. Access JWT и `role`/`departmentId`/`userId` существуют только в памяти JavaScript-модуля: ```javascript export async function fetchWithAuth(endpoint, options = {}, retry = true) { const headers = new Headers(options.headers || {}); if (accessToken) headers.set('Authorization', `Bearer ${accessToken}`); const response = await fetch(endpoint, { ...options, headers, credentials: 'same-origin' }); if (response.status === 401 && retry && await refreshAccessToken()) { return fetchWithAuth(endpoint, options, false); } return response; } ``` При загрузке защищённой страницы память восстанавливается через `POST /api/auth/refresh` и `HttpOnly` cookie. При `401` клиент выполняет не более одной общей refresh-ротации для всех параллельных запросов и один раз повторяет исходный запрос. Неуспешный refresh очищает память и возвращает пользователя на страницу входа. Legacy auth-ключи только удаляются из `localStorage`/`sessionStorage`; новые данные авторизации туда не записываются. После мутаций `api.js` инвалидирует кэш не только по точному URL, но и по связанным префиксам. Для заявок и кафедральных привязок очищаются `/api/users`, `/api/users/teachers`, `/api/teacher-requests`, `/api/department/teacher-requests` и `/api/department/teachers`, чтобы таблицы заявок и списки преподавателей обновлялись без ручного сброса страницы. --- ## Frontend-тесты Регрессионные проверки используют встроенный `node:test` (Node.js 18+). Помимо тестов дашборда, `auth-session.test.mjs` покрывает login-state, reload через refresh-cookie, single-flight ротацию, повтор после `401` и logout. `security-policy.test.mjs` запрещает auth-данные в Web Storage, удалённые/inline-скрипты, динамические inline-стили и рассинхрон SHA-256 style-хэшей с CSP. Там же закреплены безопасный DOM-рендеринг ошибок без интерполяции `exception.message` в `innerHTML`, скрытые парольные поля с корректным `autocomplete`, русские метки tenant-контура и отсутствие англоязычных/raw exception сообщений в собственных frontend-логах. Команды выполняются из каталога `frontend/`: ```bash npm run build # собрать dist/vendor/otel.js и dist/react/login.js npm test # frontend unit/static tests npm run check # React-сборка, синтаксис auth/UI-модулей + все frontend-тесты ``` React-сборка выполняется существующим esbuild без отдельного dev-сервера. Для страницы входа установлен лимит 75 000 байт gzip; превышение завершает сборку ошибкой. При появлении нескольких React entrypoint общий runtime будет вынесен в отдельный кэшируемый chunk. --- ## Аутентификация (Frontend) ### Страница входа (`/index.html`) 1. Пользователь вводит логин/пароль 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`. 6. Пользователь перенаправляется на соответствующий интерфейс: - `ADMIN` → `/admin/` - `EDUCATION_OFFICE` → `/admin/#schedule-view` - `DEPARTMENT` → `/admin/#department-workspace` - `SCHEDULE_VIEWER` → `/admin/#schedule-view` - `TEACHER` → `/teacher/` - `STUDENT` → `/student/` ### Проверка авторизации Каждая защищённая страница сначала восстанавливает сессию и проверяет роль из памяти: ```javascript const session = await restoreSession(); if (!session || !AUTHORIZED_ROLES.includes(session.role)) { window.location.replace('/'); } ``` ### Выход Кнопка «Выйти» вызывает `POST /api/auth/logout`, очищает access JWT и профиль из памяти и перенаправляет на `/`. Refresh-cookie очищает backend. --- ## Кабинеты расписания ### Учебный отдел (`/admin/#schedule-view`) Учебный отдел работает в общей админ-панели с ограниченным набором вкладок: - расширенный поиск `/api/schedule/search` по дате в периоде, преподавателю, аудитории и кафедре; - конструктор расписания через `/api/admin/schedule-rules`; - разовая правка конкретного занятия через боковую панель: перенос даты/времени, замена ресурсов, отмена и возврат по правилу; - матричный просмотр загруженности аудиторий, преподавателей и кафедр через общую вкладку `Загруженность`; - аудитории, оборудование, календарный график и загруженность. ### Кафедра (`/admin/#department-workspace`) Кабинет кафедры встроен в общую панель и работает в контексте `departmentId` текущего пользователя: - список дисциплин кафедры через `/api/department/subjects`; - загрузка дисциплин из текстового списка в формате `код; название`; - комментарии к дисциплинам; - просмотр расписания кафедры через вкладку `schedule-view`; - загруженность преподавателей кафедры. ### Просмотр расписаний (`/admin/#schedule-view`) Роль `SCHEDULE_VIEWER` видит только доступную для чтения вкладку просмотра опубликованного расписания. Доступны каскад учебного года и семестра, а также фильтры по группе, преподавателю, аудитории, кафедре, дисциплине, типу занятия, чётности и разрезу таблиц. При выборе семестра найденные занятия отображаются одной матрицей на весь период с явными границами недель для каждой пары. ### Преподаватель (`/teacher/`) Страница переведена на 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"]`; - запрос `GET /api/schedule?teacherId={userId}&startDate={YYYY-MM-DD}&endDate={YYYY-MM-DD}`; - матрица «слоты × дни недели» на десктопе и дневной список на мобильных (переключение через `matchMedia` с живым откликом на смену ширины); - отображение дисциплины, времени, типа занятия, лабораторных подгрупп, аудитории и всех групп правила; - вкладка «Мои пожелания» с выбором семестра, режимом отметки строгих, предпочтительных и нежелательных интервалов, полностью недоступных дат, пар подряд и расписания без окон; - цветовые статусы `PENDING`, `APPROVED`, `REJECTED`, `CANCELLED`; ожидающее пожелание можно отозвать кликом по отмеченной ячейке или кнопкой в списке дат; - кнопка «Запросить изменение» в карточке занятия из правила и модальное окно переноса, смены аудитории или отмены. Недоступные кандидаты отфильтрованы после проверки backend; - вкладка журнала заявок с причиной, решением и историей статусов; ожидающую заявку можно отозвать; счётчик ожидающих заявок выводится прямо на вкладке навигации; - форма собственной заявки на отсутствие через `POST /api/teacher-absences`; - список статусов заявок и отмена ещё не подтверждённой записи через `DELETE /api/teacher-absences/{id}`. Если refresh-cookie недействительна или роль не `TEACHER`, страница возвращает пользователя на вход. ### Студент (`/student/`) Страница переведена на React (`react/student/`, entrypoint `dist/react/student.js`) и использует общий слой `react/shared`: `useAuthorizedSession(['STUDENT'])`, `apiClient`, `AsyncState`, `AppErrorBoundary` и React-версия поискового списка `AsyncCombobox`. Модель обзора «Сегодня / Следующая пара» переиспользует чистые функции `schedule-overview.js`. В текущей модели студент не связан с конкретной группой, поэтому страница использует асинхронный поиск группы через `/api/groups/options`. Основные элементы: - поисковый 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`. --- ## CSS-архитектура ### Модульный подход Стили разделены на модульные файлы (порядок подключения важен): 1. **`main.css`** — CSS-переменные (цвета, шрифты, отступы), глобальные стили, тёмная тема 2. **`layout.css`** — Sidebar, topbar, content area, dropdown настроек, responsive 3. **`components.css`** — Кнопки, таблицы, карточки, badge, формы, theme-toggle 4. **`modals.css`** — Модальные окна 5. **`departments-data.css`** — Стили создания кафедры/специальности ### Темизация Общий `ui-foundation.css` подключается последним во всех пяти оболочках. Текст, заголовки и элементы управления используют прежний стек `Inter`, `-apple-system`, `BlinkMacSystemFont`, `Segoe UI`, `sans-serif` без внешнего CDN. Если Inter не установлен, браузер выбирает системный шрифт без засечек. Базовые стили содержат запасной стек на случай недоступности общего файла. Список свободных аудиторий на дашборде имеет высоту 180 px и внутреннюю вертикальную прокрутку, доступную с клавиатуры. В запросах преподавателей статус, комментарий и кнопки размещаются под содержимым заявки; кнопки переносятся целиком при нехватке ширины, статусы не растягиваются по высоте. CSS-переменные позволяют поддерживать светлую/тёмную тему: ```css :root { --bg-primary: #ffffff; --text-primary: #1a1a2e; --accent: #4F46E5; } [data-theme="dark"] { --bg-primary: #0f0f23; --text-primary: #e2e8f0; --accent: #8B5CF6; } ``` ### Система кнопок Кнопки админ-панели, отдельной SPA настроек, страницы входа и личных кабинетов используют единую семантическую модель: - `btn` — обязательная база для всех кнопок и ссылок-кнопок. - Размеры: `btn-sm` — 32px для табличных строк, `btn-md` — 40px для форм и панелей, `btn-lg` — 48px для входа и главных широких действий. - Иконки: `btn-icon-sm` — 32x32, `btn-icon-md` — 40x40. Все icon-only кнопки должны иметь `aria-label` на русском языке. - Варианты: `btn-primary` для основного действия блока, `btn-secondary` для редактирования/обновления/показа, `btn-ghost` для очистки/отмены/сворачивания, `btn-danger` для удаления, `btn-danger-subtle` для архивирования, выхода и других рискованных действий без немедленного удаления. - Каждый вариант `btn`, включая `primary`, `ghost`, `danger` и disabled-состояние, сохраняет видимую обводку в светлой и тёмной теме. - Единый радиус кнопок — `8px`. Локальные переопределения `.btn-primary`, `.btn-secondary`, `.btn-delete` в feature CSS запрещены: новые экраны должны наследовать токены из `frontend/admin/css/components.css`. - Primary-акцент общий для продукта: `#4F46E5` в светлой теме и `#8B5CF6` в тёмной. Ролевые цвета кнопок в кабинетах преподавателя и студента не используются. Переключение — через `theme-toggle.js`. --- ## Боковая панель (Sidebar) - **Скрытие/раскрытие** — кнопка-крестик в правом верхнем углу sidebar - **Десктоп** (`>768px`): sidebar складывается влево, контент расширяется; состояние сохраняется в `localStorage` (`sidebar-collapsed`) - **Мобильные** (`≤768px`): sidebar скрывается за кнопкой-гамбургер, выезжает как overlay с затемнением - **Dropdown «Настройки»** в footer sidebar — содержит ссылку на страницу настроек и кнопку выхода --- ## OpenTelemetry (локальный bundle) Клиентская телеметрия (document-load, fetch, XHR) отправляется через `BatchSpanProcessor` на same-origin путь `/otel/v1/traces`. - npm-зависимости зафиксированы exact-версиями в `package-lock.json`; - `scripts/build-vendor.mjs` собирает их через esbuild в `/vendor/otel.js`; - браузер импортирует только same-origin bundle, CDN-код в runtime отсутствует; - на `localhost` телеметрия не включается. - прикладные `magistr:ux-event` в production записываются отдельными spans с безопасными скалярными атрибутами; очередь ограничена 100 событиями. ```javascript telemetryPromise = import('/vendor/otel.js'); ``` --- ## Адаптивность Интерфейс адаптирован под мобильные устройства: - Sidebar скрывается на экранах < 768px, выезжает как overlay - Появляется кнопка-гамбургер (`#menu-toggle`) - Кнопка-крестик закрывает sidebar на всех устройствах - Таблицы получают горизонтальный скролл