# 🎨 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
├── 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
│   ├── 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-view-semesters.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 # Стили создания кафедры/специальности
│   ├── js/
│   │   ├── main.js         # Инициализация, маршрутизация, навигация
│   │   ├── role-capabilities.js # Единая матрица вкладок admin/settings по ролям
│   │   ├── api.js          # HTTP-обёртка (fetch + Authorization)
│   │   ├── dashboard-conflicts.js # Чистые функции дат, загрузки и состояний Red Zone
│   │   ├── date-input.js    # Маска ДД.ММ.ГГГГ и преобразование дат в ISO
│   │   ├── utils.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 # Боковая панель и реестр разовых изменений
│   │       ├── 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
│   │   ├── 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          # CSP-совместимая HTML-оболочка
│   ├── app.js              # Недельный просмотр и общий auth-session
│   └── style.css           # Стили кабинета без inline-блока
├── 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:

```html
<a href="#" class="nav-item" data-tab="users">Пользователи</a>
<a href="#" class="nav-item" data-tab="groups">Группы</a>
<a href="#" class="nav-item" data-tab="schedule">Расписание занятий</a>
<a href="#" class="nav-item" data-tab="academic-calendar">Календарный график</a>
```

При клике на пункт меню `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`, `/api/departments` |
| `users` | CRUD пользователей | `/api/users` |
| `groups` | CRUD групп, мультифильтр списка по формам обучения, настройка 0/2/3 подгрупп для лабораторных и назначения графиков | `/api/groups`, `/api/subgroups` |
| `classrooms` | Аудитории | `/api/classrooms` |
| `subjects` | Дисциплины | `/api/subjects` |
| `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` |
| `schedule` | Конструктор правил динамического расписания с выезжающей визуальной матрицей групп по дням и времени | `/api/admin/schedule-rules`, `/api/admin/time-slots`, `/api/admin/calendar/years`, `/api/lesson-types`, `/api/subgroups` |
| `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` и видеть статусы собственных заявок в таблице.
- Компоновка `department-workspace` использует собственные CSS-сетки `department-workspace-filter-grid` и `department-workspace-actions-grid`: фильтры периода отделены от сеток расписания, загрузка дисциплин занимает широкую колонку, формы преподавателей выравниваются справа, а списки и таблицы идут полноширинными блоками ниже.
- Вкладка `schedule-view` показывает найденные занятия в режиме одной активной таблицы. Пользователь выбирает, что смотреть: группу, преподавателя, аудиторию или кафедру; основная кнопка `Показать` расположена в заголовке блока параметров, а пустое состояние таблицы с подсказкой об обновлении содержит дополнительную кнопку `Показать расписание`. В дополнительных фильтрах доступен семестр из справочника `/api/schedule/semesters`, предназначенного только для чтения. Для текущего семестра сохраняется текущая двухнедельная точка просмотра, а при выборе другого семестра диапазон начинается с понедельника его первой недели. Frontend запрашивает две недели и собирает найденные расписания в переключатель результатов. На странице не выводится стек таблиц: виден один выбранный результат, а остальные доступны через чипы и кнопки предыдущего/следующего расписания; чипы результатов переносятся и отделены от счётчика стабильным отступом. Для режима кафедры и роли `DEPARTMENT` расписание ограничивается кафедрой пользователя; преподавательские и студенческие отдельные страницы пока остаются самостоятельными. Таблица строится как строки пар и столбцы дней недели. Нечётная неделя отображается в верхней половине ячейки, чётная — в нижней, а одинаковые занятия в обе недели схлопываются в цельную ячейку. Бейдж диапазона недель скрывается для занятий на весь семестр, а для занятий до конца семестра показывает только неделю начала в формате `(с 5 нед.)`. На мобильной ширине вместо широкой недельной матрицы показывается один день активного расписания с переключателем дней.
- Для `ADMIN` и `EDUCATION_OFFICE` карточка занятия содержит кнопку `Изменить`, а уже изменённая пара — индикатор разовой правки. Справа открывается полупрозрачная боковая панель с размытием содержимого под ней; внешний затемнённый слой также размывает страницу, а на мобильном устройстве панель занимает весь экран. Режим `Редактирование` сравнивает `Было по правилу / Станет`, позволяет изменить дату, эффективный временной слот, преподавателя, аудиторию, формат и комментарий, отменить занятие или удалить override через `Вернуть по правилу`. Селект аудитории получает записи из `/api/classrooms`, но показывает только поле `name`, без корпуса и этажа. По умолчанию выводятся семь дней исходной недели; кнопка `Выбрать другую дату` раскрывает календарь всего семестра, где неучебные даты отключены. После смены даты загружается эффективная сетка дня: сначала выбирается тот же 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`: администратор создаёт профили как из общего списка, так и через кнопку `Профили` у конкретной специальности.
- Вкладка `schedule` имеет заголовок `Конструктор правил` и не обращается к старым `lessons` API. Создание и редактирование расписания выполняется через правила `/api/admin/schedule-rules`, где каждое правило содержит группы, отдельные часы и недели начала для лекций, лабораторных и практик, а также набор базовых слотов. Группы выбираются через выпадающий мультиселект. Поле подгруппы появляется только при выборе лабораторной работы; для лекций и практик оно не отображается. Если в правиле выбрана одна группа, селект подгруппы содержит пункт `Вся группа`; если выбрано несколько групп, лабораторный слот показывает мультиселект подгрупп, чтобы выбрать разные подгруппы разных групп. Типы занятий в слоте сортируются в порядке: лекция, лабораторная работа, практика. Единственный слот имеет действие `Очистить`, которое сбрасывает его поля; при наличии нескольких слотов у каждой строки показывается действие `Удалить`. Оба действия используют размер формы, минимальную ширину `125px` и высоту `44px`, поэтому совпадают по масштабу с соседними селектами. Список слотов отображается без внутреннего вертикального скролла: при добавлении строк форма расширяется вниз, а кнопка сохранения остаётся отдельным блоком под слотами. Из календарной системы здесь используется список семестров для выбора периода действия правила. Справа доступна сворачиваемая визуальная матрица: пользователь выбирает учебный год, семестр и группы, после чего матрица строится только по правилам выбранного семестра. Столбцы — выбранные в фильтре группы, строки — только день и время, где есть активные пары, ячейки показывают дисциплину, диапазон недель, тип, формат, преподавателя, аудиторию и подгруппы. Период недель не показывается для занятия на весь семестр; если занятие идёт до конца семестра не с первой недели, выводится только неделя начала в формате `(с 5 нед.)`, а ограниченный диапазон — как `(с 1 по 3 нед.)`. Если нечётная и чётная недели отличаются, ячейка делится на две половины; одинаковые занятия схлопываются в цельную ячейку. Кнопка с тремя точками в правой части карточки пары открывает контекстное меню: можно открыть полное правило в форме, изменить только день и базовую пару выбранного слота через компактную модалку или удалить правило целиком. В списке правил действия отображаются едиными кнопками одинакового размера с отступами между ними.
- Вкладка `academic-calendar` полностью отделяет календарную систему от расписания занятий и внутри себя разделена на три вкладки: `Графики` для учебных годов, семестров и карточек календарных графиков, `Сетки` для редактора дневной сетки, `Дисциплины` для ручной привязки дисциплин из `/api/subjects` к номерам учебных семестров графика. У календарного графика нет поля ввода и отдельного столбца названия; отображаемая подпись и сохраняемое backend название автоматически собираются как `код специальности — профиль обучения — форма обучения — учебный год`. Frontend не отправляет `title` в payload. Администратор выбирает форму обучения из общего справочника `/api/education-forms`, заполняет дневную сетку по курсам и кодам активностей, назначает ручную временную сетку на конкретную дату, а сохранение сетки идёт через `/api/admin/academic-calendars/{id}/grid`. Все шесть полей дат принимают до восьми цифр, автоматически показывают их в формате `ДД.ММ.ГГГГ`, ограничивают год четырьмя цифрами и перед отправкой преобразуют значение в 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`; удаление пользователя работает как архивирование.

### Страница настроек (`/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-модуля:

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

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

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

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

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

Основные элементы:
- навигация по неделям: предыдущая, текущая, следующая;
- выбор даты через `input[type="date"]`;
- запрос `GET /api/schedule?teacherId={userId}&startDate={YYYY-MM-DD}&endDate={YYYY-MM-DD}`;
- отображение дисциплины, времени, типа занятия, лабораторных подгрупп, аудитории и всех групп правила.

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

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

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

Основные элементы:
- селект группы с сохранением выбора в `localStorage.studentGroupId`;
- недельная сетка по дням;
- запрос `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`** — Стили создания кафедры/специальности

### Темизация

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` телеметрия не включается.

```javascript
telemetryPromise = import('/vendor/otel.js');
```

---

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

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