Files
magistr/docs/FRONTEND.md

77 KiB
Raw Blame History

🎨 Frontend

Общая информация

Параметр Значение
Фреймворк Нет (Vanilla JavaScript)
Модульная система ES6 Modules (import/export)
Стили CSS (модульный подход)
Шрифт Системный стек без внешних font-CDN
Веб-сервер Apache httpd на Alpine со строгим CSP

Структура файлов

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
├── telemetry/
│   └── otel-entry.js       # Исходная точка сборки OpenTelemetry
├── scripts/
│   └── build-vendor.mjs    # Сборка `/vendor/otel.js` через esbuild
├── 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
│   ├── 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              # 🔐 Страница авторизации (общая)
├── script.js               # Логика авторизации
├── style.css               # Стили страницы авторизации
├── theme-toggle.js         # Переключение светлой/тёмной темы
├── Dockerfile              # Multi-stage: npm bundle → Apache httpd
│
├── admin/                  # 👨‍💼 Интерфейс администратора
│   ├── index.html          # SPA-оболочка с sidebar
│   ├── 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/
│   │   ├── 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
│   │
│   └── 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 # Базовая, субботняя и ручные сетки времени
│
├── teacher/                # 👩‍🏫 Интерфейс преподавателя
│   ├── index.html          # Расписание, календарь доступности и журнал заявок
│   ├── app.js              # Пожелания, переносы и недельный просмотр через общий auth-session
│   └── 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
    └── style.css           # Стили кабинета без inline-блока

Система маршрутизации (Admin SPA)

Админ-панель работает как Single Page Application без фреймворка.

Навигация реализована через data-tab атрибуты на элементах sidebar:

<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>

При клике на пункт меню main.js:

  1. Загружает HTML-шаблон из views/{tab}.html через fetch()
  2. Вставляет его в #app-content
  3. Подключает соответствующий JS-модуль из js/views/{tab}.js
  4. Обновляет заголовок страницы (#page-title)

main.js и отдельный settings SPA получают разрешённые вкладки из единого admin/js/role-capabilities.js; локальные дубли ROLE_NAVIGATION/ROLE_TABS удалены.

Роль Доступные вкладки
ADMIN Все вкладки
EDUCATION_OFFICE Просмотр, конструктор и анализ качества расписаний, отсутствия и замены, календарный график, загруженность и аудитории; в settings — временные слоты и формы обучения
DEPARTMENT Кабинет кафедры, просмотр расписаний, отсутствия и подтверждение заявок своих преподавателей
SCHEDULE_VIEWER Только просмотр расписаний

Пути /department/ и /edu-office/ оставлены как входные redirect-страницы в общую панель. Отдельные кабинеты не дублируют UI админ-панели.

Пункты меню, недоступные роли, не отображаются в sidebar: main.js выставляет hidden, а layout.css явно скрывает такие элементы, чтобы базовый display: flex у .nav-item не возвращал их на экран. Верхние панели основной админки и 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 или возврата по ссылке. Просмотр расписания сохраняет вид сущности, выбранную сущность, дату, учебный год, семестр и дополнительные фильтры; конечные кабинеты сохраняют неделю, вкладку и группу.
  • pagination.js работает с общим ответом 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/)

Настройки — это отдельный SPA со своей боковой панелью и вкладками, не связанными с основной админ-панелью.

  • Доступ: через dropdown «Настройки» в footer боковой панели админки для ADMIN и EDUCATION_OFFICE
  • Кнопка «Назад в панель» для возврата в /admin/
  • Текущие вкладки:
    • Общие настройки — заглушка (только ADMIN)
    • Временные слоты — выбор сетки через выпадающий список, CRUD слотов выбранной сетки, добавление ручных сеток через модальное окно и удаление выбранной ручной сетки. Поле длительности доступно только для чтения и пересчитывается при изменении времени начала или окончания; в API отправляются только границы, а окончательное значение вычисляет backend. Базовая сетка применяется по умолчанию, субботняя — автоматически по субботам; ручное применение выполняется в сетке календарного графика.

API-клиент (api.js)

Все защищённые HTTP-запросы проходят через fetchWithAuth() из auth-session.js. Access JWT и role/departmentId/userId существуют только в памяти 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/:

npm run build  # собрать локальный dist/vendor/otel.js
npm test       # frontend unit/static tests
npm run check  # синтаксис auth/UI-модулей + все frontend-тесты

Аутентификация (Frontend)

Страница входа (/index.html)

  1. Пользователь вводит логин/пароль
  2. script.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/

Проверка авторизации

Каждая защищённая страница сначала восстанавливает сессию и проверяет роль из памяти:

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/)

Страница объединяет недельную сетку занятий, семестровый календарь доступности и журнал заявок на изменение пар. ID преподавателя берётся из восстановленного в памяти профиля сессии.

Основные элементы:

  • общий блок «Сегодня» и карточка «Следующая пара»; отдельный запрос покрывает ближайшие 14 дней, поэтому следующая пара находится и за границей текущей недели;
  • навигация по неделям: предыдущая, текущая, следующая;
  • выбор даты через input[type="date"];
  • запрос GET /api/schedule?teacherId={userId}&startDate={YYYY-MM-DD}&endDate={YYYY-MM-DD};
  • отображение дисциплины, времени, типа занятия, лабораторных подгрупп, аудитории и всех групп правила;
  • вкладка «Мои пожелания» с выбором семестра, режимом отметки строгих, предпочтительных и нежелательных интервалов, полностью недоступных дат, пар подряд и расписания без окон;
  • цветовые статусы PENDING, APPROVED, REJECTED, CANCELLED; ожидающее пожелание можно отозвать кликом по отмеченной ячейке или кнопкой в списке дат;
  • кнопка «Заявка на изменение» в карточке фактического занятия и модальное окно переноса, смены аудитории или отмены. Недоступные кандидаты отфильтрованы после проверки backend;
  • вкладка журнала заявок с причиной, решением, ссылкой на применённый override и историей статусов; ожидающую заявку можно отозвать;
  • форма собственной заявки на отсутствие через POST /api/teacher-absences;
  • список статусов заявок и отмена ещё не подтверждённой записи через DELETE /api/teacher-absences/{id}.

Если refresh-cookie недействительна или роль не TEACHER, страница возвращает пользователя на вход.

Студент (/student/)

В текущей модели студент не связан с конкретной группой, поэтому страница использует асинхронный поиск группы через /api/groups/options.

Основные элементы:

  • поисковый combobox группы с подсказками по специальности, профилю и курсу; выбор хранится в localStorage.studentGroupId и studentGroup query string;
  • общий блок «Сегодня» и карточка «Следующая пара», рассчитанные по ближайшим 14 дням;
  • недельная сетка по дням;
  • запрос GET /api/schedule?groupId={groupId}&startDate={YYYY-MM-DD}&endDate={YYYY-MM-DD};
  • отображение дисциплины, времени, преподавателя, аудитории, формата, типа занятия и лабораторных подгрупп.

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-переменные позволяют поддерживать светлую/тёмную тему:

: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 событиями.
telemetryPromise = import('/vendor/otel.js');

Адаптивность

Интерфейс адаптирован под мобильные устройства:

  • Sidebar скрывается на экранах < 768px, выезжает как overlay
  • Появляется кнопка-гамбургер (#menu-toggle)
  • Кнопка-крестик закрывает sidebar на всех устройствах
  • Таблицы получают горизонтальный скролл