Files
magistr/BUG_REPORT.md
2026-07-10 22:20:50 +03:00

615 lines
46 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Отчёт по ошибкам и рискам проекта Magistr
Дата проверки: 2026-07-10
## Объём и способ проверки
Проверены:
- 166 Java-файлов backend, JPA-модели, репозитории, контроллеры, сервисы и конфигурация мультитенантности;
- обе Flyway-миграции без изменения существующих файлов;
- 23 JavaScript-файла, встроенный JavaScript страниц преподавателя/студента, HTML/CSS и SPA-маршрутизация;
- `compose.yaml`, Dockerfile, Gitea Actions;
- production-манифесты `../k8s/`, которые AGENTS.md относит к внешним зависимостям проекта;
- проектная документация и существующий граф связей `graphify-out/graph.json`.
Выполненные проверки:
- `mvn test` в Maven-контейнере: **30 тестов, 0 failures, 0 errors, BUILD SUCCESS**;
- `node --check` для всех внешних JS-файлов и извлечённых inline-скриптов: ошибок синтаксиса нет;
- `docker compose config --quiet`: Compose синтаксически корректен;
- `kubectl kustomize ../k8s`: Kustomize-манифесты собираются;
- сопоставление JPA-таблиц с Flyway DDL: отсутствующих таблиц для сущностей не найдено;
- `git diff --check`: проблем с пробелами и маркерами конфликтов нет.
Ограничения проверки: не выполнялись E2E-тесты в браузере, нагрузочное тестирование, тестирование с реальным PostgreSQL и одновременной работой двух backend-pod. Конкурентные дефекты ниже подтверждены по коду и воспроизводимому сценарию, но не запускались против production-кластера.
## Сводка
| Приоритет | Количество |
|---|---:|
| Критический | 2 |
| Высокий | 10 |
| Средний | 19 |
| Низкий | 3 |
| **Всего** | **34** |
## Критические проблемы
### 1. Production-манифесты содержат известные JWT/DB-секреты и хранят пароли в ConfigMap
**Файлы:**
- `backend/src/main/resources/application.properties:18-22`
- `backend/src/main/java/com/magistr/app/config/auth/JwtProperties.java:13-17`
- `../k8s/config.yaml:15-26`
- `../k8s/backend.yaml:1-23`
- `../k8s/otel-collector.yaml:1-25`
**Проблема:** приложение имеет известный JWT-секрет по умолчанию и небезопасный дефолт `refresh-cookie-secure=false`. Production-манифест `config.yaml` передаёт другой, но также заранее известный placeholder-секрет, который удовлетворяет проверке длины. Пароли PostgreSQL записаны открытым текстом в `Secret.stringData`, tenant ConfigMap и ConfigMap коллектора OpenTelemetry. Kubernetes Secret в YAML не шифрует значение в исходном файле, а ConfigMap вообще не предназначен для секретов.
**Риск:** подделка access JWT, компрометация всех tenant-БД и метрик, повторное использование опубликованных credentials. Если эти значения когда-либо применялись, их нужно считать скомпрометированными.
**Как исправить:**
- Немедленно ротировать JWT- и DB-секреты во всех окружениях.
- Удалить реальные/фиксированные значения из файлов и истории репозиториев.
- Использовать External Secrets, SOPS/Sealed Secrets или секреты CI/CD; tenant credentials и credentials OTel хранить в Secret, а не ConfigMap.
- На production-профиле завершать запуск при пустом, дефолтном или известном placeholder `JWT_SECRET` и при `JWT_REFRESH_COOKIE_SECURE != true`.
---
### 2. Flyway создаёт во всех новых tenant-БД предсказуемые учётные записи и демонстрационные данные
**Файлы:**
- `backend/src/main/resources/db/migration/V1__init.sql:21-24,38-42,85-92,221-265`
- `docs/README.md:45-54`
**Проблема:** V1 создаёт администратора с публично документированным коротким паролем, а также несколько тестовых пользователей с одинаковым известным паролем. Эта же миграция запускается для tenant-БД, добавленных в production, и одновременно добавляет демонстрационные кафедры, группы и другие данные.
**Риск:** немедленный административный доступ к свежему tenant, если пароль не сменили вручную; загрязнение production-БД тестовыми сущностями.
**Как исправить:**
- Не менять V1. Добавить новую миграцию `V3__disable_demo_accounts.sql`, которая архивирует/удаляет демонстрационные аккаунты и данные там, где они не были явно сохранены.
- Создавать первого администратора отдельной bootstrap-процедурой с одноразовым случайным секретом.
- Разнести demo/dev seed и обязательную production-схему.
- Ротировать пароли уже созданных tenant-БД.
## Высокий приоритет
### 3. Race condition при ротации refresh-токена
**Файлы:**
- `backend/src/main/java/com/magistr/app/config/auth/RefreshTokenService.java:50-91`
- `backend/src/main/java/com/magistr/app/repository/AuthRefreshTokenRepository.java:10`
**Проблема:** `rotate()` читает токен, проверяет его активность, а затем отзывает старый и создаёт новый без блокировки строки или условного атомарного UPDATE. Два параллельных запроса с одним cookie могут оба пройти проверку и создать две новые сессии.
**Риск:** повторное использование refresh-токена, раздвоение цепочки ротации и обход ожидаемой семантики single-use.
**Как исправить:** использовать `PESSIMISTIC_WRITE` либо атомарный `UPDATE ... WHERE revoked_at IS NULL AND expires_at > now()` и создавать следующий токен только при обновлении одной строки. Добавить конкурентный интеграционный тест с PostgreSQL.
---
### 4. `AuthContext` остаётся в servlet-потоке после отказа по роли
**Файл:** `backend/src/main/java/com/magistr/app/config/auth/AuthorizationInterceptor.java:45-49,65-68`
**Проблема:** пользователь записывается в ThreadLocal до проверки `@RequireRoles`. Если interceptor возвращает `false`, его собственный `afterCompletion()` не вызывается, поэтому контекст не очищается.
**Риск:** следующий запрос на том же потоке может увидеть данные предыдущего пользователя, особенно если он проходит через публичный auth endpoint или останавливается в более раннем interceptor.
**Как исправить:** очищать контекст в начале `preHandle()` и перед каждым `return false`; ещё лучше — устанавливать пользователя только после успешной проверки роли. Добавить MockMvc-тест последовательных запросов на одном executor-потоке.
---
### 5. Точечные изменения расписания создаются без проверки конфликтов и факта существования пары
**Файлы:**
- `backend/src/main/java/com/magistr/app/controller/ScheduleOverrideController.java:71-126`
- `backend/src/main/java/com/magistr/app/service/ScheduleQueryService.java:67-141`
**Проблема:** для `MOVE`/`REPLACE` проверяется только существование сущностей. Не проверяется занятость преподавателя, аудитории и групп в новой паре. Также можно сохранить override на дату, когда базовый слот вообще не генерируется, либо сохранить фактически пустой `REPLACE`.
**Риск:** двойное назначение ресурсов; накопление неработающих изменений, которые API считает успешными.
**Как исправить:** перед сохранением построить исходную пару на указанную дату, проверить action-specific поля и пересечения уже применённого расписания. При конфликте возвращать `409 Conflict`.
---
### 6. Расписание преподавателя не показывает пары, назначенные ему через override
**Файлы:**
- `backend/src/main/java/com/magistr/app/service/ScheduleQueryService.java:47-63`
- `backend/src/main/java/com/magistr/app/repository/ScheduleRuleRepository.java:36-58`
- `backend/src/main/java/com/magistr/app/repository/ScheduleOverrideRepository.java:14-26`
**Проблема:** запрос только по `teacherId` сначала выбирает базовые правила этого преподавателя. Если override заменяет преподавателя A на B, правило A не попадает в исходную выборку B, поэтому последующее `applyOverrides()` уже не может добавить пару.
**Риск:** преподаватель B не видит назначенную ему замену.
**Как исправить:** учитывать overrides с `newTeacher.id = teacherId` за период и достраивать соответствующие базовые пары до финальной фильтрации.
---
### 7. Валидатор правил расписания пропускает несколько нарушений инвариантов
**Файлы:**
- `backend/src/main/java/com/magistr/app/controller/ScheduleRuleAdminController.java:220-228,440-481,524-565`
- `backend/src/main/java/com/magistr/app/service/ScheduleGeneratorService.java:252-293`
- `backend/src/main/resources/db/migration/V1__init.sql:707-726`
**Проблемы:**
- конфликт ищется только с уже сохранёнными правилами; два конфликтующих/дублирующих слота внутри одного нового правила не сравниваются между собой;
- пользователь слота проверяется на архивность, но не на роль `TEACHER`;
- `lessonFormat` проверяется только на непустоту, хотя DB допускает лишь «Очно»/«Онлайн»;
- нечётное число академических часов принимается, хотя генератор списывает по два часа и может выдать больше часов, чем задано.
**Риск:** дубли в один момент времени, назначение студента/администратора преподавателем, HTTP 500 на DB CHECK и неверный расход часов.
**Как исправить:** валидировать пары новых слотов между собой, роль пользователя, enum формата и кратность часов двум; продублировать ключевые ограничения новой Flyway-миграцией там, где это возможно.
---
### 8. Ошибка в строке календарной сетки возвращает 400, но удаляет старую сетку и коммитит часть новой
**Файл:** `backend/src/main/java/com/magistr/app/controller/AcademicCalendarController.java:125-146`
**Проблема:** `saveGrid()` работает в транзакции, сначала удаляет всю старую сетку, затем валидирует и сохраняет строки по одной. `IllegalArgumentException` ловится внутри transactional-метода и не выходит за границу proxy, поэтому транзакция считается успешной.
**Воспроизведение:** передать список, где первая строка корректна, а вторая содержит неверный день/дату. API вернёт 400, но старая сетка будет удалена, а первая новая строка сохранится.
**Риск:** тихая потеря календарного учебного графика при ошибке пользователя.
**Как исправить:** сначала провалидировать весь список и уникальность ключей, затем одним шагом заменять данные; либо не перехватывать исключение внутри транзакции/помечать транзакцию rollback-only.
---
### 9. Обновление tenant может удалить рабочее подключение и успешно сохранить нерабочее
**Файлы:**
- `backend/src/main/java/com/magistr/app/controller/DatabaseController.java:104-124,175-182`
- `backend/src/main/java/com/magistr/app/config/tenant/TenantRoutingDataSource.java:135-148`
- `backend/src/main/java/com/magistr/app/config/tenant/TenantConfigWatcher.java:133-160`
**Проблема:** существующий DataSource сначала удаляется и закрывается. Новый Hikari pool создаётся с `initializationFailTimeout=-1`. Ошибка Flyway проглатывается, а результат записи ConfigMap игнорируется. API способен вернуть успех после неудачной миграции или неудачной персистенции.
**Риск:** одна опечатка в URL/пароле отключает рабочий tenant; разные pod получают разную конфигурацию.
**Как исправить:** создать временный pool, проверить соединение и миграции, затем атомарно заменить старый. Ошибки миграции и ConfigMap должны прерывать операцию; нужен compensating rollback.
---
### 10. Два backend-pod могут потерять изменения tenant-конфигурации, а probes не видят отказ БД
**Файлы:**
- `../k8s/backend.yaml:31,86-97`
- `backend/src/main/java/com/magistr/app/controller/DatabaseController.java:177-182`
- `backend/src/main/java/com/magistr/app/config/tenant/ConfigMapUpdater.java:62-92`
**Проблема:** deployment имеет две реплики. Каждая PATCH-операция перезаписывает весь `tenants.json` из локальной in-memory карты без resourceVersion/compare-and-swap. Параллельное добавление A на pod-1 и B на pod-2 приводит к last-write-wins и потере A или B. TCP readiness/liveness считает pod здоровым, даже если все tenant-БД недоступны и Flyway завершился ошибкой.
**Риск:** потерянные конфигурационные изменения и маршрутизация трафика на фактически неработающий pod.
**Как исправить:** вынести операции в единый coordinator/CRD/БД либо применять optimistic locking к ConfigMap с повтором; добавить health endpoint, проверяющий миграции и доступность обязательных tenant.
---
### 11. Дашборд может показать зелёное «конфликтов нет», когда проверка фактически не выполнена
**Файл:** `frontend/admin/js/views/dashboard.js:148-168,274-287`
**Проблемы:**
- диапазон недели вычисляется двойным мутированием одного `Date`; в первые дни месяца конец диапазона попадает в начало предыдущего месяца и оказывается раньше начала;
- каждый запрос кафедры имеет `catch(() => [])`, поэтому сетевые/серверные ошибки превращаются в пустое расписание;
- после этого пустой результат отображается как «Конфликты расписания не обнаружены».
**Проверенный пример:** для 2026-07-02 код строит диапазон примерно 2026-06-29 … 2026-06-05 вместо 2026-06-29 … 2026-07-05.
**Риск:** ложное подтверждение безопасности расписания именно в панели контроля.
**Как исправить:** вычислять воскресенье как `monday + 6 дней` на отдельном объекте, форматировать локальную дату без UTC и различать состояния «нет конфликтов»/«проверка частично или полностью не выполнена».
---
### 12. Access JWT и удалённый JavaScript выполняются в одном origin
**Файлы:**
- `frontend/admin/js/api.js:5-7,155-163`
- `frontend/script.js:5-28`
- `frontend/admin/js/otel.js:1-8`
**Проблема:** JWT хранится в `localStorage`. Страница входа динамически исполняет неприкреплённые по версии модули с `esm.sh`, а admin — удалённые модули без SRI. Такой код имеет доступ к DOM, введённому паролю и `localStorage`.
**Риск:** компрометация CDN/пакета или XSS сразу даёт учётные данные и Bearer-токен.
**Как исправить:** собирать и раздавать telemetry dependencies локально с lockfile; держать access token в памяти или перейти на защищённую cookie-сессию; добавить строгий CSP и убрать inline-скрипты.
## Средний приоритет
### 13. Watcher не применяет изменения существующего tenant и не повторяет неудачную синхронизацию
**Файл:** `backend/src/main/java/com/magistr/app/config/tenant/TenantConfigWatcher.java:60-71,102-125`
**Проблема:** сравниваются только множества доменов — смена URL, логина или пароля существующего домена игнорируется. Кроме того, новый hash записывается до JSON parsing/sync; если синхронизация падает, следующий poll считает файл уже обработанным и не повторяет операцию.
**Риск:** pod продолжает использовать старые credentials либо навсегда остаётся в частично синхронизированном состоянии до следующего изменения файла/рестарта.
**Как исправить:** сравнивать весь нормализованный `TenantConfig`, записывать hash только после полного успеха и сохранять retry/backoff-состояние.
---
### 14. Полностью отключена проверка TLS Kubernetes API
**Файл:** `backend/src/main/java/com/magistr/app/config/tenant/ConfigMapUpdater.java:74-76,104-119`
**Проблема:** trust manager принимает любой сертификат. Kubernetes CA уже доступен в serviceaccount volume.
**Риск:** MITM к API server, утечка serviceaccount token и подмена tenant ConfigMap.
**Как исправить:** загружать `/var/run/secrets/kubernetes.io/serviceaccount/ca.crt`, использовать стандартную проверку цепочки и hostname.
---
### 15. Редактор временных слотов позволяет создать пересекающиеся интервалы и сломать базовые правила
**Файлы:**
- `backend/src/main/java/com/magistr/app/controller/TimeSlotAdminController.java:152-224`
- `backend/src/main/java/com/magistr/app/service/ScheduleGeneratorService.java:418-435`
**Проблемы:**
- проверяется только уникальность номера пары, но не пересечение временных интервалов внутри сетки;
- update разрешает перенести существующий базовый слот в MANUAL scope, хотя правила обязаны ссылаться на DEFAULT;
- `durationMinutes` может не соответствовать разнице `startTime/endTime`.
**Риск:** два занятия реально идут одновременно, но конфликтный анализ считает их разными слотами; существующие правила исчезают из ожидаемой базовой сетки.
**Как исправить:** запретить пересечение интервалов, фиксировать scope используемого базового слота и вычислять duration на backend.
---
### 16. Учебные годы и семестры допускают пересечения и выход семестра за границы года
**Файлы:**
- `backend/src/main/java/com/magistr/app/controller/AcademicCalendarAdminController.java:46-129,188-237`
- `backend/src/main/java/com/magistr/app/service/AcademicDateService.java:32-34`
- `backend/src/main/java/com/magistr/app/repository/SemesterRepository.java:13-17`
**Проблема:** проверяется только порядок двух дат. Можно пересечь учебные годы, пересечь осенний и весенний семестры, вынести семестр за границы года. При update также не проверяется дубликат title/type до DB constraint.
**Риск:** `findFirst...` выбирает произвольный семестр для даты; номер недели и чётность становятся недетерминированными, а часть update-запросов заканчивается HTTP 500.
**Как исправить:** централизовать календарную валидацию, запретить пересечения и добавить DB-level exclusion/range constraints новой миграцией.
---
### 17. Изменение календаря или группы нарушает уже сохранённые назначения
**Файлы:**
- `backend/src/main/java/com/magistr/app/controller/AcademicCalendarController.java:85-98,178-214`
- `backend/src/main/java/com/magistr/app/controller/GroupController.java:173-212,322-359`
**Проблема:** после назначения графика группе можно изменить у графика учебный год, специальность, профиль, форму или количество курсов; можно также изменить эти поля у группы. Существующее `student_group_calendar_assignments` не перепроверяется и не удаляется. При уменьшении courseCount остаются строки сетки старших курсов.
**Риск:** расписание строится по графику, который больше не соответствует группе/году, хотя API проверял соответствие при первоначальном назначении.
**Как исправить:** запрещать изменение ключевых измерений у используемого графика либо транзакционно перевалидировать назначения, grid и subjects.
---
### 18. Историческая модель кафедр преподавателя используется непоследовательно
**Файлы:**
- `backend/src/main/java/com/magistr/app/controller/UserController.java:218-259,310-324`
- `backend/src/main/java/com/magistr/app/controller/WorkloadController.java:51-60,85-100`
- `backend/src/main/java/com/magistr/app/controller/TeacherSubjectController.java:102-115`
**Проблемы:**
- будущий перевод немедленно меняет `users.department_id`, и преподаватель до `validFrom` появляется одновременно в старой и новой кафедре;
- исторический список фильтрует `User::isActiveRecord`, поэтому архивный преподаватель исчезает даже для даты, когда он работал;
- workload прошлых периодов относится к текущему `users.department_id`;
- дополнительная активная кафедральная связь учитывается в списке преподавателей, но `TeacherSubjectController` разрешает связь только по legacy `users.department_id`.
**Риск:** неверные исторические отчёты и неработоспособность дополнительного назначения преподавателя.
**Как исправить:** считать кафедру через `teacher_department_assignments` на дату; legacy-поле обновлять только в дату вступления перевода в силу либо не использовать для бизнес-решений.
---
### 19. Роль `DEPARTMENT` может запрашивать workload чужой кафедры
**Файл:** `backend/src/main/java/com/magistr/app/controller/WorkloadController.java:43-135`
**Проблема:** endpoints принимают произвольный `departmentId` или отсутствие фильтра и не сопоставляют его с `AuthContext.departmentId`.
**Риск:** межкафедральное раскрытие отчётов и обход ограничений, которые уже применяются в `DepartmentWorkspaceController` и части `UserController`.
**Как исправить:** для `DEPARTMENT` принудительно использовать кафедру из токена; явно документировать endpoints, где глобальный просмотр действительно разрешён.
---
### 20. Глобальные workload/free-classrooms ломаются при количестве активных групп больше 50
**Файлы:**
- `backend/src/main/java/com/magistr/app/controller/WorkloadController.java:43-135`
- `backend/src/main/java/com/magistr/app/service/ScheduleQueryService.java:160-180`
**Проблема:** агрегирующие endpoints переиспользуют общий поиск с ограничением 50 групп. `free-classrooms` вообще не принимает scope, поэтому в крупном tenant неизбежно получает `IllegalArgumentException` вместо списка свободных аудиторий.
**Риск:** штатная функция перестаёт работать при росте университета.
**Как исправить:** сделать специализированные агрегирующие запросы/батч-генерацию, а не использовать ограничение интерактивного широкого поиска.
---
### 21. Импорт кафедры может переназначить дисциплину другой кафедры
**Файл:** `backend/src/main/java/com/magistr/app/controller/DepartmentWorkspaceController.java:80-90`
**Проблема:** дисциплина ищется по глобальному имени, после чего `departmentId` безусловно меняется на текущую кафедру.
**Риск:** потеря принадлежности и изменение расписаний/календарей другой кафедры.
**Как исправить:** не менять чужую запись; решить бизнес-правило уникальности и при необходимости перейти на `(department_id, lower(name))` новой миграцией.
---
### 22. Права frontend и backend расходятся для учебного отдела
**Файлы:**
- `backend/src/main/java/com/magistr/app/controller/SpecialityController.java:23-24,38-58,196-213`
- `backend/src/main/java/com/magistr/app/controller/EducationFormController.java:17-18,33-59`
- `frontend/admin/js/views/academic-calendar.js:106-118,207-218`
- `frontend/admin/settings/js/main.js:47-52`
**Проблемы:**
- `EDUCATION_OFFICE` видит вкладку календарных графиков, но её обязательные GET `/api/specialties` и profiles разрешены только `ADMIN`; инициализация попадает в 403;
- settings показывает учебному отделу CRUD форм обучения, но POST/DELETE backend разрешены только `ADMIN`.
**Риск:** видимые штатные экраны частично или полностью не работают.
**Как исправить:** согласовать role matrix в одном источнике; либо расширить read/write права, либо скрыть недоступные действия.
---
### 23. Генерация расписания имеет выраженный N+1 по дням, правилам и группам
**Файлы:**
- `backend/src/main/java/com/magistr/app/service/ScheduleGeneratorService.java:64-90,107-124,187-229,297-308`
- `backend/src/main/java/com/magistr/app/service/AcademicDateService.java:58-87`
**Проблема:** семестр повторно ищется внутри каждого правила, а `isTheoryDay()` для каждой группы и даты выполняет отдельные запросы assignment + calendar day. В teacher mode это повторяется по всем группам правила.
**Риск:** диапазон 120 дней и десятки групп порождают тысячи/десятки тысяч SQL-запросов, таймауты и нагрузку на каждую tenant-БД.
**Как исправить:** заранее батчем загружать semesters, assignments и calendar days за диапазон; передавать уже найденный semester в `processRuleForDate()`. Добавить метрики query count и нагрузочный тест.
---
### 24. Истёкшие и отозванные refresh-токены никогда не удаляются
**Файлы:**
- `backend/src/main/java/com/magistr/app/config/auth/RefreshTokenService.java:34-107`
- `backend/src/main/java/com/magistr/app/repository/AuthRefreshTokenRepository.java:8-11`
**Проблема:** каждый login/refresh добавляет строку, ротация лишь ставит `revoked_at`. Cleanup job/repository delete отсутствует.
**Риск:** неограниченный рост `auth_refresh_tokens` и индексов.
**Как исправить:** периодически удалять давно истёкшие/отозванные строки с разумным audit retention; покрыть job тестом.
---
### 25. DB constraint violations превращаются в 500 и иногда раскрывают текст драйвера
**Файлы:**
- `backend/src/main/java/com/magistr/app/controller/GlobalExceptionHandler.java:23-55`
- `backend/src/main/java/com/magistr/app/controller/GroupController.java:166-169,216-219`
- `backend/src/main/java/com/magistr/app/controller/SubjectController.java:122-125`
- `backend/src/main/java/com/magistr/app/controller/DatabaseController.java:121-124,166-171`
**Проблема:** нет общего обработчика `DataIntegrityViolationException`/constraint name. Часть контроллеров ловит общий `Exception` и возвращает клиенту `e.getMessage()`.
**Риск:** неправильный HTTP status, английские/технические сообщения в UI и раскрытие деталей схемы/JDBC.
**Как исправить:** добавить единый перевод constraint → 409/400 с русским сообщением; не отдавать внутренний exception text.
---
### 26. Чистый локальный запуск по документации не публикует приложение и имеет противоречивую конфигурацию БД
**Файлы:**
- `compose.yaml:1-43`
- `backend/src/main/resources/application.properties:3-6,18-22`
- `docs/README.md:33-43`
- `docs/INFRASTRUCTURE.md:3-37`
**Проблемы:**
- frontend/backend не имеют `ports`, а Caddy не входит в Compose, поэтому на чистой машине `localhost:80` после указанной команды недоступен;
- `POSTGRES_DB` может изменить создаваемую DB, но backend URL жёстко указывает `app_db`;
- JWT-переменные из `.env` не передаются контейнеру backend;
- для PostgreSQL не объявлен именованный volume, поэтому после down/recreate данные могут остаться в потерянном anonymous volume.
**Как исправить:** добавить локальный reverse proxy/ports, передавать согласованный JDBC URL и JWT env, объявить named volume и обновить quick start.
---
### 27. CI/CD разворачивает код без запуска тестов, а tag pipeline перезапускает `:main`
**Файлы:**
- `.gitea/workflows/docker-build.yaml:3-8,35-40,63-68,73-96`
- `backend/Dockerfile:1-6`
- `../k8s/backend.yaml:45-46`
- `../k8s/frontend.yaml:20-21`
**Проблема:** workflow не имеет test job, а Dockerfile выполняет `mvn package -DskipTests`. На событии tag metadata публикует tag-образ, но deployment продолжает ссылаться на mutable `:main` и просто перезапускается.
**Риск:** production получает непроверенный commit; release-tag может перезапустить старый main-образ вместо релиза.
**Как исправить:** обязательные test/compile/static-check jobs до push/deploy; деплой image digest или конкретного release tag; concurrency lock и rollback при failed rollout.
---
### 28. Нет ограничения попыток входа
**Файлы:**
- `backend/src/main/java/com/magistr/app/controller/AuthController.java:57-86`
- `../k8s/ingress.yaml:1-45`
**Проблема:** login не имеет rate limit, задержки, lockout или CAPTCHA; в Ingress также нет соответствующего middleware/аннотации.
**Риск:** online brute force и password spraying, особенно опасные вместе с известными seed-аккаунтами.
**Как исправить:** rate limit по tenant+IP+username, прогрессивная задержка и аудит неудачных входов без логирования пароля.
---
### 29. Часовой пояс backend не зафиксирован, а часть frontend дат строится через UTC
**Файлы:**
- `backend/src/main/java/com/magistr/app/model/LifecycleEntity.java:94-98`
- `backend/src/main/java/com/magistr/app/controller/UserController.java:85,164,235,271`
- `backend/src/main/java/com/magistr/app/service/ScheduleGeneratorService.java:236`
- `frontend/admin/js/views/dashboard.js:48,159-160`
- `frontend/admin/js/views/department-workspace.js:494-504`
- `compose.yaml` и `../k8s/backend.yaml`
**Проблема:** бизнес-даты используют `LocalDate.now()`/`LocalDateTime.now()` с timezone JVM, но контейнеры не получают `TZ`/`user.timezone`. Frontend использует `toISOString()`, поэтому в Москве до 03:00 получает предыдущую календарную дату.
**Риск:** архивирование, перевод, доступность ресурсов и «сегодняшнее расписание» сдвигаются на день/несколько часов в зависимости от окружения.
**Как исправить:** внедрить `Clock` и явный `ZoneId` для бизнес-даты, хранить абсолютные timestamps как UTC/`Instant`, форматировать date-only локальными компонентами.
---
### 30. Сборки и deployment используют mutable/непроверяемые артефакты
**Файлы:**
- `backend/Dockerfile:7`
- `../k8s/otel-collector.yaml:59`
- `../k8s/backend.yaml:45-46`
- `../k8s/frontend.yaml:20-21`
- `.gitea/workflows/docker-build.yaml:83-87`
**Проблема:** javaagent и kubectl скачиваются как latest/stable без checksum, collector использует `latest`, приложения — mutable `main`.
**Риск:** невоспроизводимые сборки, неожиданные несовместимости и supply-chain подмена.
**Как исправить:** pin версии и image digest, проверять SHA256/signature, генерировать SBOM и сканировать образы.
---
### 31. Группы допускают некорректную численность и ломают существующее деление на подгруппы
**Файлы:**
- `backend/src/main/java/com/magistr/app/controller/GroupController.java:173-212,250-272`
- `backend/src/main/java/com/magistr/app/controller/SubgroupController.java:151-175`
- `backend/src/main/resources/db/migration/V1__init.sql:203-219`
**Проблема:** `groupSize` проверяется только на null, `yearStartStudy` — только на ноль; DB не имеет CHECK для положительной численности. При update можно уменьшить группу ниже суммы активных подгрупп, хотя SubgroupController такой результат запрещает при редактировании подгрупп.
**Риск:** отрицательная/нулевая численность, неверный расчёт вместимости и внутренне противоречивые подгруппы.
**Как исправить:** единый валидатор create/update, проверка суммы подгрупп и новая миграция с CHECK constraints.
## Низкий приоритет
### 32. Лимит «120 дней» фактически разрешает 121 дату
**Файлы:**
- `backend/src/main/java/com/magistr/app/service/ScheduleQueryService.java:148-157`
- `backend/src/main/java/com/magistr/app/service/ScheduleGeneratorService.java:135-144`
**Проблема:** проверяется `DAYS.between(start, end) > 120`, но обе границы включены. Разница 120 означает 121 календарную дату.
**Как исправить:** проверять включительную длину (`between + 1`) либо изменить текст/документацию.
---
### 33. Остались XSS-точки в сообщениях ошибок и видимые поля пароля
**Файлы:**
- `frontend/admin/settings/js/views/database.js:39,49`
- `frontend/admin/js/main.js:260-262`
- `frontend/admin/settings/js/main.js:130-133`
- `frontend/admin/views/users.html:11-12`
- `frontend/admin/js/views/teacher-requests.js:47-51`
**Проблема:** несколько `e.message` вставляются через `innerHTML` без экранирования. Поля пароля при создании пользователя/одобрении заявки имеют `type="text"`.
**Риск:** потенциальный DOM XSS при попадании управляемого текста ошибки; пароль виден на экране и может сохраняться как обычный текст формы.
**Как исправить:** использовать `textContent`/`escapeHtml`, а для паролей — `type="password"` и корректные `autocomplete`.
---
### 34. Проектный языковой регламент нарушен в UI и логах
**Файлы:**
- `backend/src/main/java/com/magistr/app/config/tenant/TenantRoutingDataSource.java:65,78,95`
- `backend/src/main/java/com/magistr/app/config/tenant/TenantDataSourceConfig.java:60,123`
- `frontend/admin/settings/js/views/database.js:14-16,60-62`
- `frontend/admin/js/otel.js:47`
**Проблема:** присутствуют английские production-логи и UI-метки `Online/Offline`; JDBC exception также может попасть в UI на английском.
**Как исправить:** привести пользовательские сообщения и проектные логи к русскому языку; технические идентификаторы оставлять в structured fields.
## Что покрыть тестами в первую очередь
1. PostgreSQL/Testcontainers + Flyway: чистая БД, upgrade существующей БД, отсутствие активных demo-аккаунтов.
2. Два параллельных refresh-запроса с одним токеном.
3. Цепочка interceptor: 403, затем публичный/защищённый запрос на том же потоке.
4. Override с конфликтом и поиск преподавателя, назначенного через `newTeacher`.
5. Внутренние конфликты одного нового правила, роль преподавателя, нечётные часы и недопустимый формат.
6. `saveGrid` с корректной и ошибочной строкой: после 400 старая сетка должна остаться целой.
7. Изменение tenant URL/credentials, неудачная миграция, параллельные PATCH от двух pod.
8. Более 50 активных групп для workload/free-classrooms.
9. Future-dated перевод преподавателя и исторические отчёты.
10. E2E для `EDUCATION_OFFICE`: календарные графики и настройки форм обучения.
11. Frontend-тест дат в первые дни месяца и в 00:0003:00 Europe/Moscow.
## Рекомендуемый порядок исправления
1. Ротация/удаление секретов и отключение seed-аккаунтов.
2. Data-loss и security races: calendar grid, refresh rotation, AuthContext.
3. Атомарность tenant-конфигурации и реальные health checks.
4. Конфликты overrides/rules и корректность расписания преподавателя.
5. Согласование ролей, календарных инвариантов и исторической модели кафедр.
6. Производительность генератора, Compose/CI/CD и frontend hardening.
Существующие Flyway-миграции не изменять. Все исправления схемы оформлять новой миграцией, начиная с `V3__...sql`.