Files
magistr/BUG_FIX_PROGRESS.md
2026-07-19 20:16:12 +03:00

1068 lines
120 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.
# Журнал исправления проблем
Обновлено: 2026-07-19 (Europe/Moscow).
## Область и неизменяемые данные
- В работе ровно 33 проблемы: `1` и `334`.
- Проблема № 2 исключена: демонстрационные учётные записи, seed-данные и документация по ним не изменяются.
- `BUG_REPORT.md` не редактируется.
- По прямому разрешению владельца проекта миграции V2V7 удалены, а их итоговая схема
объединена в `V1__init.sql`: проект находится на этапе разработки, клиентских tenant-БД нет.
Для этой редакции требуется полный сброс локальной БД.
Исходное рабочее дерево перед началом реализации было чистым (`git status --short` без вывода).
Контрольные суммы исходных миграций до разрешённой консолидации (историческая справка):
| Файл | SHA-256 |
|---|---|
| `V1__init.sql` | `404f65f270dd87f4db996495fc970456c4fd0f762e7c32638199dffaa812ea7b` |
| `V2__subgroups_active_unique_name.sql` | `68f5525a50ddba4f8800a63cab3cd0f84779e301c4a1f29a6e0399f97fbdb30b` |
Текущая единая baseline-миграция: `V1__init.sql`, SHA-256
`fffedf66e8bf7d25d2d8c1b1a6146b45dd5b7e52233d05a68de8bf62c817f7a7`.
Упоминания V2V7 в исторических записях ниже описывают последовательность разработки до
консолидации и не означают наличие этих файлов в текущем дереве.
## Исходная линия проверок
| Проверка | Результат до изменений |
|---|---|
| `mvn -f backend/pom.xml test` | Maven на хосте отсутствует. Контейнерный эквивалент `maven:3.9-eclipse-temurin-17` завершён успешно: 30 тестов, 0 failures, 0 errors. |
| `find frontend ... node --check` | Успешно. |
| `docker compose config --quiet` | Успешно. |
| `kubectl kustomize ../k8s >/dev/null` | Успешно. |
| `bash -n ../k8s/deploy.sh` | Успешно. |
| `git diff --check` | Успешно. |
## Порядок выполнения
1. Безопасная production-конфигурация и внешние секреты: № 1 (с зависимой частью № 14).
2. Аутентификация и границы запроса: № 3, 4, 24, 25, 28.
3. Высокоприоритетные инварианты расписания: № 58.
4. Tenant lifecycle и Kubernetes coordination: № 9, 10, 13, 14.
5. Frontend-сессия и достоверность UI: № 11, 12, 33, 34.
6. Календарные и DB-инварианты: № 1517, 21, 31, 32.
7. История, права, масштаб и производительность: № 1823.
8. Локальный запуск, CI/CD, воспроизводимые артефакты и timezone: № 26, 27, 29, 30.
9. AutoUpdateDocs, полный регрессионный прогон и финальный аудит 33 пунктов.
## Чек-лист
Статусы меняются только после реализации и воспроизводимой проверки.
| № | Статус | Подтверждённая первопричина | Решение и основные файлы | Проверка |
|---:|---|---|---|---|
| 1 | требуется внешнее действие | В коде и production YAML были встроенные/placeholder JWT и DB credentials; tenant и OTel credentials находились в отслеживаемой конфигурации; Secure-cookie не был обязательным для production. | Встроенный JWT default удалён; добавлен fail-fast validator; production использует только внешние `app-secret`, `tenants-secret`, `otel-postgres-secret`; создан `docs/SECURITY_RUNBOOK.md`. Репозиторная часть исправлена. Оператору остаются ротация, provisioning, rollout и очистка истории. | 11 JWT unit/context tests; общий backend: 45/0/0; `scripts/check-production-secrets.sh`; Kustomize и shell syntax — успешно. |
| 3 | исправлено и проверено | Ротация refresh-токена выполняла read-check-write без блокировки/условного update. | `RefreshTokenService.rotate()` использует блокирующий `findByTokenHashForUpdate()` с `PESSIMISTIC_WRITE` и `JOIN FETCH` внутри транзакции. | Два синхронных запроса к Spring proxy и PostgreSQL: один успех, один отказ, один активный новый токен. Полный backend: 46/0/0/0. |
| 4 | исправлено и проверено | `AuthContext` устанавливался до role-check и сохранялся в servlet-потоке, когда `preHandle=false` не приводил к `afterCompletion`. | `AuthorizationInterceptor` очищает контекст в начале каждого запроса и устанавливает пользователя только после успешной проверки ролей; финальная очистка сохранена. | Один MockMvc-поток: `403`, публичный запрос, разрешённый запрос и очистка после завершения. Полный backend: 47/0/0/0. |
| 5 | исправлено и проверено | Override сохранялся без доказательства исходной пары, action-specific полей и ресурсных конфликтов. | `ScheduleOverrideService` строит base/effective day, валидирует матрицу и фактическое изменение, проверяет ресурсы и сериализует CRUD через PostgreSQL advisory locks; структурные CHECK включены в V1. | 27 целевых unit/MockMvc/PostgreSQL tests; полный backend: 74/0/0/0. |
| 6 | исправлено и проверено | Teacher-only поиск загружал только базовые правила исходного преподавателя до применения override. | Один snapshot overrides; релевантные `newTeacher`-замены достраивают exact base occurrences по уникальным датам, затем применяются overrides, финальный teacher-фильтр и deduplicate. | 5 новых A→B/phantom/cache/dedup/regression тестов; узкий прогон 10/0/0/0, полный backend 79/0/0/0. |
| 7 | исправлено и проверено | Контроллер сравнивал новые слоты только с сохранёнными правилами, не проверял роль преподавателя и enum формата, а нечётные часы приводили к перерасходу генератора. | `ScheduleRuleService` валидирует весь кандидат до мутации, попарно проверяет дубли/ресурсы, требует `TEACHER`, `Очно`/`Онлайн` и чётные часы; строка семестра сериализует запись. Чётность и точный дубль закреплены в V1. | 23 service/MockMvc/Flyway/PostgreSQL tests; полный backend: 102/0/0/0. |
| 8 | исправлено и проверено | `saveGrid` удалял данные до полной валидации и ловил исключение внутри transactional controller-метода, поэтому Spring коммитил delete и уже обработанные строки. | `AcademicCalendarGridService` полностью валидирует и строит replacement до delete, атомарно заменяет строки и очищает кэш через `afterCommit`; контроллер стал HTTP-адаптером. | 8 unit + 1 PostgreSQL proxy test; fingerprint старой сетки после отказа неизменен; полный backend 111/0/0/0. |
| 9 | исправлено и проверено | При update старый pool закрывался до создания нового; Hikari допускал ленивый нерабочий pool, Flyway проглатывал ошибку, а отказ persistence мог сочетаться с изменённым локальным состоянием. | `TenantLifecycleService` выполняет prepare → connection validation → Flyway → Secret persistence → atomic snapshot swap; candidate закрывается при отказе, прежний route сохраняется, Secret компенсируется, старый pool закрывается после drain. | 18 целевых unit/MockMvc/PostgreSQL tests; create/update, credentials, invalid connection, Flyway, persistence, swap, HTTP `503` и in-flight connection. Полный backend: 132/0/0/0. |
| 10 | исправлено и проверено | Два pod перезаписывали целый tenant-документ без `resourceVersion`; TCP probes не отражали готовность tenant-БД. | Backend применяет доменные upsert/remove через GET → условный PUT по `resourceVersion`, ограниченный retry и безопасную компенсацию; Actuator разделяет process-only liveness и readiness обязательных tenant-БД. Kubernetes содержит RBAC `get/update`, directory mount без `subPath`, required tenant-config и HTTP probes. | 64 целевых теста; полный backend 277/0/0/0; Kustomize, secret-scan и синтаксис deploy-скрипта прошли. |
| 11 | исправлено и проверено | Воскресенье вычислялось повторным мутированием одной даты, date-only строился через UTC, а ошибки кафедральных запросов превращались в пустой успешный результат. | `dashboard-conflicts.js` формирует локальный диапазон на отдельных объектах, загружает кафедры через `Promise.allSettled` и fail-closed различает `COMPLETE`/`PARTIAL`/`NOT_RUN`; зелёная карточка разрешена только для полного результата без конфликтов. | 12 `node:test`: 02.07.2026 00:15 Europe/Moscow, переход года, полный/частичный/нулевой успех и строгий UI-контракт; `npm run check` и синтаксис 25 JS/MJS-файлов — успешно. |
| 12 | исправлено и проверено | Четыре frontend-клиента хранили access JWT и профиль в Web Storage; login/admin исполняли OTel с `esm.sh`, а inline JS/styles не позволяли включить строгий CSP. | Общий `auth-session.js` держит access JWT только в памяти и восстанавливает его через HttpOnly refresh-cookie; OTel собирается из exact npm-зависимостей в same-origin bundle; Apache выдаёт CSP без `unsafe-inline`/`unsafe-eval`. | 21 frontend test, `npm audit` 0 vulnerabilities, Docker build и live HTTP/CSP/bundle check — успешно. |
| 13 | исправлено и проверено | Watcher сравнивал только домены, подтверждал hash до полного успеха, а API после мутации мог принять запаздывающую mounted-проекцию за актуальную. | Полное нормализованное сравнение, prepare/verify/Flyway/atomic swap без повторной записи Secret, hash-after-success, retry 30300 секунд и semantic snapshot fence по `TenantSecretUpdateReceipt`. | 53 целевых теста, 2 PostgreSQL lifecycle-теста; полный backend 200/0/0/0. |
| 14 | исправлено и проверено | Kubernetes HTTP-клиент имел trust-all TLS fallback. | `KubernetesTenantSecretUpdater` загружает service-account CA, строит PKIX trust store, включает HTTPS hostname verification и не имеет небезопасного fallback. | Mock HTTPS: доверенный CA принят; чужой CA, неверный hostname и отсутствующий CA отклонены. Backend 45/0/0. |
| 15 | исправлено и проверено | CRUD слотов жил в контроллере, проверял только номер пары и принимал клиентскую длительность; DB не запрещала overlap и перенос используемого базового слота. | Транзакционный `TimeSlotService` блокирует сетки, валидирует границы, вычисляет duration и отклоняет overlap; V1 содержит duration CHECK, GiST exclusion и триггеры привязки правил к `DEFAULT`; UI показывает расчётное readonly-поле. | 6 unit; общий backend без Testcontainers 209/0/0/0; чистая V1 и конкурентные DB-сценарии проверены на PostgreSQL 16.3. |
| 16 | исправлено и проверено | CRUD проверял только порядок дат; update не проверял duplicate title/type, годы и семестры пересекались, а семестр мог выходить за год. | `AcademicPeriodService` централизует inclusive-range validation и блокирует родительский год; V1 содержит GiST exclusion constraints и взаимные триггеры границ года/семестра. | 8 unit; общий backend без Testcontainers 209/0/0/0; чистая V1, adjacent/overlap/bounds и конкурентные годы проверены на PostgreSQL 16.3. |
| 17 | исправлено и проверено | Изменение измерений группы/календаря оставляло несовместимые assignments/grid/subjects. | `AcademicStructureService` транзакционно блокирует группу/график, перевалидирует назначения, курсы, сетку и дисциплины; V1 дублирует инварианты конкурентно безопасными триггерами. Несовместимое изменение даёт русский `409` без мутации данных. | 7 unit; общий backend без Testcontainers 209/0/0/0; чистая V1, DB-отказы и гонка двух транзакций проверены на PostgreSQL 16.3. |
| 18 | исправлено и проверено | Бизнес-решения смешивали `users.department_id` с датированными assignments и фильтровали архивные записи по текущему состоянию. | `TeacherDepartmentService` централизует разрешение кафедры на дату; списки, права и нагрузка используют историю, будущий перевод не меняет текущую принадлежность; V1 запрещает overlap основных периодов. | 5 service + 7 controller tests; полный backend 216/0/0/0; чистая V1, overlap и конкурентная запись проверены на PostgreSQL 16.3. |
| 19 | исправлено и проверено | Workload принимал произвольный `departmentId` или глобальный scope для роли `DEPARTMENT`. | Все пять workload-сценариев вычисляют scope через `AuthContext`: кафедра получает только свою выборку, чужой ID даёт русский `403`; глобальный scope сохранён уполномоченным ролям. | 5 controller tests; полный backend 219/0/0/0; compose и diff-check успешны. |
| 20 | исправлено и проверено | Workload и free-classrooms переиспользовали интерактивный широкий поиск, который отклонял tenant с более чем 50 активными группами. | Добавлен узкий `searchForAggregation`: общие validation/overrides/filter/dedup сохранены, UI-лимит отключён только для агрегатов; обычный search не изменён. | 51 и 100 активных групп обработаны полностью; целевые 9/0/0/0, полный backend 221/0/0/0. |
| 21 | исправлено и проверено | Импорт находил дисциплину по глобальному имени и без проверки менял её `department_id`. | Транзакционный `SubjectImportService` сохраняет глобальное владение: свой повтор обновляет тот же ID, чужой даёт `409`; V1 содержит case-insensitive уникальность названия. | 4 unit; полный backend 225/0/0/0; чистая V1, регистровый дубль и конкурентная вставка проверены на PostgreSQL 16.3. |
| 22 | исправлено и проверено | Календарный график учебного отдела получал `403` на specialties/profiles, а видимый CRUD форм обучения был ADMIN-only. | EDUCATION_OFFICE получила read-only specialities/profiles и CRUD education forms; frontend admin/settings используют единый `role-capabilities.js`. | 2 MockMvc role-сценария через реальный interceptor; frontend role test; полный backend 227/0/0/0, npm check успешен. |
| 23 | исправлено и проверено | Генератор искал semester внутри дат и правил, а assignment/calendar day и эффективную сетку времени — внутри комбинаций дат и групп. | `AcademicDateService.ScheduleSnapshot`, batch-репозитории и `buildScheduleForGroups()` загружают данные один раз на запрос; lookup-карты используются во всех внутренних циклах. | 120 дат, 30 групп и 20 правил: ровно 7 repository reads, 30 корректных занятий; полный backend 228/0/0/0. |
| 24 | исправлено и проверено | Login/refresh добавляли строки, а revoke/rotate только ставили `revoked_at`; cleanup отсутствовал. | `RefreshTokenCleanupJob` обходит snapshot tenant-БД, удаляет старые строки ограниченными транзакционными пачками с `FOR UPDATE SKIP LOCKED`; retention и расписание настраиваются, cleanup-индексы включены в V1. | 3 unit; полный backend 231/0/0/0; точная V1 и две конкурентные PostgreSQL-транзакции удалили разные пачки, fresh/active сохранены, повтор удалил 0. |
| 25 | исправлено и проверено | Не было общего mapping DB constraints; несколько контроллеров включали `Exception.getMessage()` в HTTP-body. | `GlobalExceptionHandler` и `DatabaseConstraintViolationMapper` централизованно преобразуют CHECK в безопасный русский `400`, UNIQUE/FK/GiST и неизвестные нарушения — в `409`; raw exception body удалён. | 4 новых теста известных и неизвестных constraints без JDBC/SQL details; полный backend 235/0/0/0. |
| 26 | исправлено и проверено | Compose требовал внешнюю сеть, не публиковал HTTP, рассогласовывал DB/JWT и использовал anonymous volume. Live startup также выявил два неоднозначных Spring-конструктора. | Внутренняя bridge-сеть, Apache reverse proxy и `HTTP_PORT`; согласованные datasource/JWT env, healthchecks, named volume; production-конструкторы явно помечены для DI. | `up --build --wait`; PostgreSQL/backend/frontend healthy, `/` и liveness `200`, API `401`, named volume; полный backend 235/0/0/0. |
| 27 | исправлено и проверено | Pipeline собирал и деплоил без тестов, а release tag только перезапускал Deployment на mutable `:main`. | Обязательный checks job, production concurrency lock, SHA/release build tags и deploy только digest из build outputs; общий shell rollback обоих Deployment. | Workflow YAML/static scan, shell success/failure/rollback, production-secret scan и Kustomize — успешно. |
| 28 | исправлено и проверено | Login не имел shared rate limit, различал архивного пользователя и безусловно доверял forwarded header. | Транзакционный PostgreSQL limiter по tenant + NFKC username + проверенному IP; прогрессивная блокировка, audit/cleanup, единый `401`, `429`/`Retry-After` и trusted proxy chain. | 12 целевых unit/PostgreSQL tests; два service instance дали ровно один `401` и один `429`, общий failure count 2; V1 и cleanup проверены. |
| 29 | исправлено и проверено | Business dates зависели от timezone процесса, абсолютные моменты смешивались с `LocalDateTime`, frontend date-only местами строился через UTC. | `BusinessTimeService` с `Clock`/`Europe/Moscow`; абсолютные моменты переведены на `Instant`/`TIMESTAMPTZ`/UTC; frontend использует локальные компоненты; Compose и Kubernetes фиксируют бизнес-зону и timezone JVM. | Boundary tests московской полуночи, frontend date-only, Kustomize и полный backend PostgreSQL/Testcontainers-прогон 277/0/0/0 прошли. |
| 30 | исправлено и проверено | Образы/скачиваемые инструменты использовали mutable refs либо не имели общего контроля digest/SBOM/scan. | Docker/Compose/Testcontainers, OTel Collector и CI images закреплены tag+digest, Actions — commit SHA; checksum, provenance, SBOM и Trivy gate проверяются скриптом. | Shell-регрессии, Workflow/Compose YAML, repo и полный `K8S_DIR=../k8s` artifact-scan прошли. |
| 31 | исправлено и проверено | Group create/update допускал неположительные значения и уменьшение ниже активных подгрупп; параллельные изменения могли вместе превысить размер группы. | Общий validator create/update, блокировка родительской группы, положительные CHECK и перекрёстные триггеры непосредственно в V1. | 11 service tests и 4 PostgreSQL migration/concurrency tests; полный backend 277/0/0/0. |
| 32 | исправлено и проверено | `between(start,end) > 120` разрешал 121 включительную дату. | Оба сервиса считают включительные даты как `between + 1` и разрешают не больше 120. | Границы 120/121 в query/generator tests; полный backend 277/0/0/0. |
| 33 | исправлено и проверено | Несколько `e.message` вставлялись как HTML; парольные поля имели `type=text`. | Ошибки вкладок, БД и заявок рендерятся фиксированным текстом через `textContent`/`replaceChildren`; поля создания пользователя, tenant и одобрения заявки используют `type=password` и `autocomplete=new-password`. | 3 новых static regression tests; полный frontend: 24 проверки, 0 ошибок; CSP-хэши синхронизированы. |
| 34 | исправлено и проверено | В затронутых UI/log paths оставались английские метки/логи и raw exception text. | Статусы БД и пользовательские ошибки русифицированы; начальная tenant-конфигурация, маршрутизация и interceptor пишут русские production-сообщения с безопасным типом исключения. | Static-проверка исходных английских сообщений; полный backend 200/0/0/0; live Docker HTTP/CSP — успешно. |
## Завершённые этапы
### № 1 и зависимая № 14
- `JwtProperties` больше не содержит секрет по умолчанию; production startup отклоняет пустое, короткое, известное legacy/placeholder значение и `JWT_REFRESH_COOKIE_SECURE=false`.
- `../k8s/config.yaml` больше не создаёт Secret с литеральными значениями.
- `../k8s/backend.yaml` монтирует tenant-конфигурацию из `tenants-secret`.
- `../k8s/otel-collector.yaml` использует только `${env:...}` ссылки и `otel-postgres-secret`.
- `KubernetesTenantSecretUpdater` заменил `ConfigMapUpdater`, использует base64-поле Kubernetes Secret, service-account CA и hostname verification.
- `../k8s/deploy.sh` проверяет обязательные Secrets/ключи до rollout без вывода значений.
- Созданы `docs/SECURITY_RUNBOOK.md` и `scripts/check-production-secrets.sh`; документация синхронизирована через AutoUpdateDocs.
Фактические проверки этапа:
| Команда | Результат |
|---|---|
| Контейнерный `mvn test` | 45 тестов, 0 failures, 0 errors, BUILD SUCCESS. |
| `bash scripts/check-production-secrets.sh` | Успешно. |
| `kubectl kustomize ../k8s >/dev/null` | Успешно. |
| `bash -n ../k8s/deploy.sh` | Успешно. |
| `git diff --check` | Успешно. |
| Повторные SHA-256 V1/V2 | Совпадают с исходными. |
Изменённые вне Git-корня файлы: `../k8s/config.yaml`, `backend.yaml`,
`otel-collector.yaml`, `rbac.yaml`, `deploy.sh`, `README.md`. Их состояние проверяется
отдельно от `git diff`.
### № 3 — атомарная ротация refresh-токена
- Репозиторий захватывает строку исходного токена через `PESSIMISTIC_WRITE` и загружает
пользователя в том же запросе.
- `RefreshTokenService.rotate()` выполняет проверку активности, отзыв исходного токена и
создание следующего токена в одной транзакции после захвата блокировки.
- Добавлен интеграционный тест с двумя синхронными потоками, Spring transactional proxy,
Flyway V1/V2 и реальным PostgreSQL 16.3 через Testcontainers.
- Одновременная ротация подтверждает ровно один успешный результат, один отказ, две строки
в БД и ровно один активный следующий токен.
- `docs/ARCHITECTURE.md` и `docs/API.md` синхронизированы через AutoUpdateDocs.
Фактические проверки этапа:
| Команда | Результат |
|---|---|
| Контейнерный узкий `mvn -Dtest=RefreshTokenServiceTest,RefreshTokenServiceConcurrencyIntegrationTest test` | 5 тестов, 0 failures, 0 errors, 0 skipped, BUILD SUCCESS. |
| Контейнерный полный `mvn test` | 46 тестов, 0 failures, 0 errors, 0 skipped, BUILD SUCCESS. |
| `git diff --check` | Успешно. |
### № 4 — границы `AuthContext`
- `AuthorizationInterceptor.preHandle()` очищает `AuthContext` до проверки URI, метода,
публичного endpoint и bearer-токена.
- Аутентифицированный пользователь помещается в `ThreadLocal` только после успешной
проверки `@RequireRoles`; ответы `401` и `403` оставляют контекст пустым.
- `afterCompletion()` продолжает очищать контекст после успешно допущенного запроса.
- Регрессионный MockMvc-тест выполняет на одном JUnit-потоке запрещённый защищённый,
публичный и разрешённый защищённый запросы и проверяет контекст внутри обработчиков и
после завершения.
- `docs/ARCHITECTURE.md` синхронизирован через AutoUpdateDocs.
Фактические проверки этапа:
| Команда | Результат |
|---|---|
| Контейнерный `mvn -Dtest=AuthorizationInterceptorTest test` | 1 тест, 0 failures, 0 errors, 0 skipped, BUILD SUCCESS. |
| Контейнерный полный `mvn test` | 47 тестов, 0 failures, 0 errors, 0 skipped, BUILD SUCCESS. |
| `git diff --check` | Успешно. |
### № 5 — безопасные точечные изменения расписания
- `ScheduleOverrideController` стал тонким HTTP-адаптером, а CRUD перенесён в публичные
транзакционные методы `ScheduleOverrideService`.
- Перед сохранением строится реальная базовая пара на дату с учётом семестра, календаря,
чётности, lifecycle и остатка часов; несуществующая occurrence возвращает `400`.
- Матрица `CANCEL`/`MOVE`/`REPLACE`, формат и action-specific фактическое изменение
проверяются до мутации managed entity.
- Сохранённые overrides и кандидат применяются общей логикой `ScheduleQueryService`;
полуоткрытые интервалы проверяются по преподавателю, аудитории, целой группе и
подгруппам. Конфликт возвращается как русский `409 Conflict`.
- PostgreSQL transaction advisory lock сериализует изменения даты между pod. Update/delete
дополнительно блокируют ID; старая и новая даты update захватываются в стабильном порядке,
после чего строка перечитывается с `PESSIMISTIC_WRITE`.
- Добавлена `V3__schedule_override_invariants.sql`: preflight legacy-строк, CHECK для
payload каждого действия и формата. Миграционный тест подтверждает полный rollback на
невалидном legacy `REPLACE` и clean upgrade до V3.
- `docs/API.md`, `docs/BUSINESS_LOGIC.md`, `docs/DATABASE.md` и `docs/ARCHITECTURE.md`
синхронизированы через AutoUpdateDocs.
Фактические проверки этапа:
| Команда | Результат |
|---|---|
| Целевые контейнерные Maven-прогоны № 5 | 22 unit/MockMvc/PostgreSQL теста, затем 24 усиленных unit/MockMvc теста; оба прогона без failures/errors/skipped. Все 27 тестов № 5 входят в полный прогон. |
| Контейнерный полный `mvn test` | 74 теста, 0 failures, 0 errors, 0 skipped, BUILD SUCCESS. |
| `git diff --check` | Успешно. |
| Повторные SHA-256 V1/V2 | Совпадают с исходными. |
| SHA-256 новой V3 | `eb5315b58a2bad95b0ad36718052788168be0d8f347649c5c98782949865357b`. |
### № 6 — замена преподавателя в результирующем расписании
- `ScheduleQueryService.search()` читает overrides диапазона один раз и переиспользует
один снимок для достраивания и применения изменений.
- Teacher-only путь по-прежнему строит базовое расписание преподавателя напрямую. Только
при наличии overrides с совпадающим `newTeacher` он группирует целевые слоты по дате и
один раз строит полный базовый день каждой релевантной даты.
- Из дня добавляются только occurrences с точным `baseRuleSlotId + lessonDate`; отсутствующая
базовая пара не создаёт фантомное занятие.
- Overrides применяются до окончательного teacher-фильтра: новый преподаватель видит замену,
исходный — нет. Итоговая дедупликация удаляет совпавшие базовые и достроенные фрагменты.
- `CANCEL` и `MOVE` продолжают применяться из того же snapshot; при отсутствии релевантной
замены `groupRepository.findAll()` не вызывается.
- `docs/API.md`, `docs/BUSINESS_LOGIC.md` и `docs/ARCHITECTURE.md` синхронизированы через
AutoUpdateDocs.
Фактические проверки этапа:
| Команда | Результат |
|---|---|
| Узкий контейнерный Maven-прогон трёх `ScheduleQueryService*Test` | 10 тестов, 0 failures, 0 errors, 0 skipped, BUILD SUCCESS. |
| Контейнерный полный `mvn test` | 79 тестов, 0 failures, 0 errors, 0 skipped, BUILD SUCCESS. |
| `git diff --check` | Успешно. |
### № 7 — полная валидация правил расписания
- `ScheduleRuleAdminController` стал тонким HTTP-адаптером; create/update/archive перенесены
в публичные транзакционные методы `ScheduleRuleService`.
- До обращений к справочникам сервис проверяет обязательные поля, неотрицательность и
чётность каждого лимита часов, положительную сумму, недели начала и структуру payload.
Ноль разрешён для неиспользуемого типа занятия.
- Слот принимает только активного пользователя с ролью `TEACHER`, базовый временной слот и
формат `Очно`/`Онлайн`; отсутствующая вложенная ссылка возвращает безопасный `400`.
- Кандидат строится отдельно от managed entity. Точные дубли и ресурсные конфликты слотов
нового payload проверяются попарно до сохранения; `ODD` и `EVEN` разрешены одновременно,
а `BOTH` пересекается с ними. Конфликтный update не изменяет существующее правило.
- Создание первым DB-запросом блокирует строку семестра. Update блокирует строку правила и
старый/новый семестры в стабильном порядке; реальный PostgreSQL-тест подтверждает, что из
двух параллельных create проходит один, а второй после commit видит `409 Conflict`.
- `V4__schedule_rule_even_hours.sql` выполняет русский preflight нечётных legacy-часов и
точных дублей слотов, затем добавляет провалидированный CHECK чётности и UNIQUE точного
payload слота. При legacy-нарушении V4 полностью откатывается и остаётся версия V3.
- `docs/API.md`, `docs/BUSINESS_LOGIC.md`, `docs/DATABASE.md` и `docs/ARCHITECTURE.md`
синхронизированы через AutoUpdateDocs.
Фактические проверки этапа:
| Команда | Результат |
|---|---|
| Целевые service/MockMvc/Flyway/PostgreSQL прогоны № 7 | 23 теста, 0 failures, 0 errors, 0 skipped. |
| Контейнерный полный `mvn test` | 102 теста, 0 failures, 0 errors, 0 skipped, BUILD SUCCESS. |
| `git diff --check` | Успешно. |
| Повторные SHA-256 V1/V2/V3 | Совпадают с зафиксированными значениями. |
| SHA-256 новой V4 | `211babace0f1620cac2186b42b6d3039c8ee8518284473b49da0a4d47186e490`. |
### № 8 — атомарная замена календарной сетки
- `AcademicCalendarController.saveGrid()` больше не содержит транзакцию, bulk delete,
построчное сохранение и внутренний catch; endpoint делегирует публичному
`AcademicCalendarGridService.replaceGrid()`.
- Сервис до первого write полностью валидирует непустой payload: `null`-строки, границы
курса и учебного года, согласованность `weekNumber` и ISO `dayOfWeek` с датой, наличие
activity type и уникальность `(courseNumber, date)`.
- Все `AcademicCalendarDay` строятся в памяти до `deleteByCalendarId`. Валидный набор
сохраняется как `delete → flush → saveAll → flush` в одной транзакции; исключения выходят
за Spring proxy и приводят к rollback.
- Очистка кэша зарегистрирована как `afterCommit`, поэтому не выполняется при rollback и не
создаёт окно повторного кэширования старой сетки между flush и commit.
- PostgreSQL-тест сравнивает count+MD5 старой seed-сетки: payload с корректной первой и
ошибочной второй строкой оставляет fingerprint неизменным и не добавляет первую строку;
следующий валидный запрос атомарно оставляет ровно две новые строки.
- `docs/API.md`, `docs/BUSINESS_LOGIC.md` и `docs/ARCHITECTURE.md` синхронизированы через
AutoUpdateDocs.
Фактические проверки этапа:
| Команда | Результат |
|---|---|
| Unit `AcademicCalendarGridServiceTest` | 8 тестов, 0 failures, 0 errors, 0 skipped. |
| PostgreSQL `AcademicCalendarGridServiceIntegrationTest` | 1 тест, 0 failures, 0 errors, 0 skipped; rollback/fingerprint и after-commit подтверждены. |
| Контейнерный полный `mvn test` | 111 тестов, 0 failures, 0 errors, 0 skipped, BUILD SUCCESS. |
| `git diff --check` | Успешно. |
### № 9 — безопасная замена tenant DataSource
- `DatabaseController` делегирует create/update/delete в единый `TenantLifecycleService` и
возвращает lifecycle-ошибки как русский безопасный `503`, а не ложный `200` или raw
JDBC/Flyway message.
- Новый Hikari candidate не публикуется в маршрутизации до явного `getConnection()` /
`isValid(5)`, успешного Flyway и подтверждённой доменной мутации в актуальном
`tenants-secret`.
- `TenantRoutingDataSource` одной volatile-публикацией заменяет неизменяемый снимок
`TenantConfig + DataSource`; прежняя конфигурация и pool остаются рабочими при ошибке
credentials, миграции, persistence или доатомарной активации.
- Неуспешная persistence не меняет локальный route. Неопределённый результат записи сначала
сверяется повторным чтением и не даёт права на опасный откат. Если swap/remove отказывает
после подтверждённой записи, прежний снимок восстанавливается только при точном совпадении
`resourceVersion`; более новое изменение другого pod не перезаписывается.
- После успешного swap старый pool не закрывается синхронно: `RetiredTenantPoolService`
прекращает новые маршруты, сохраняет уже выданные соединения и закрывает Hikari после
drain либо grace timeout.
- Unit-тесты дополнены отдельной create-веткой, восстановлением снимка без нового domain,
`Connection.isValid(false)` и MockMvc-проверкой `503` без технических деталей.
- PostgreSQL 16.3/Testcontainers-тест подтверждает сохранность старого route при неверном
пароле, повреждённом Flyway checksum и отказе persistence, работоспособность in-flight
соединения во время swap и последующий drain старого pool.
- `docs/API.md`, `docs/ARCHITECTURE.md` и `docs/INFRASTRUCTURE.md` синхронизированы через
AutoUpdateDocs; исправлено устаревшее описание порядка «in-memory до Secret».
Фактические проверки этапа:
| Команда | Результат |
|---|---|
| Контейнерный целевой `mvn -Dtest=TenantLifecycleServiceTest,TenantLifecyclePostgreSqlIntegrationTest,TenantRoutingDataSourceTest,DatabaseControllerTest test` | 18 тестов, 0 failures, 0 errors, 0 skipped, BUILD SUCCESS. |
| Контейнерный полный `mvn test` | 132 теста, 0 failures, 0 errors, 0 skipped, BUILD SUCCESS. |
| Повторные LF-нормализованные SHA-256 V1/V2/V3/V4 | Совпадают с ранее зафиксированными значениями. |
### № 10 — межподовая координация tenant-конфигурации и health probes
- Введён `TenantConfigStore` с явными операциями upsert/remove. Kubernetes-реализация перед
каждой мутацией читает актуальный `tenants-secret`, нормализует домены и отправляет полный
объект условным `PUT` с прочитанным `metadata.resourceVersion`.
- `409`, `408`, `429`, `5xx` и транспортные ошибки обрабатываются ограниченно: максимум три
попытки с возрастающей задержкой и повторным применением только собственной доменной
мутации к свежему состоянию. Идемпотентная операция не выполняет `PUT`.
- Успех записи и компенсации больше не определяется одним HTTP 2xx: backend разбирает
возвращённый Secret, сравнивает `tenants.json` с ожидаемым и использует `resourceVersion`
только проверенного объекта. Пустой, неполный или неоднозначный ответ сверяется повторным
`GET` без выдачи квитанции, разрешающей небезопасный откат.
- Компенсация выполняется только для подтверждённой собственной версии. Если текущий Secret
уже получил другой `resourceVersion`, откат прекращается и не затирает изменение другого
pod. Отсутствующий `data.tenants.json`, повторяющиеся домены и Kubernetes mode без
ServiceAccount token обрабатываются fail-closed.
- Добавлен Spring Boot Actuator. `/actuator/health/liveness` зависит только от жизнеспособности
процесса; `/actuator/health/readiness` требует непустой набор обязательных tenant, успешную
миграцию и свежую успешную проверку каждого соединения. H2-заглушка, ошибка обязательного
конфига, migration failure, timeout, недоступность или просроченная проверка дают `503`.
- `TenantDatabaseHealthMonitor` проверяет tenant-БД ограниченно-параллельно в фоне; endpoint
читает только атомарный кэш. Общий deadline отменяет лишь незавершённые задачи, уже
завершившиеся результаты не теряются и получают фактическое время проверки. Scheduler
имеет два потока, поэтому долгий health-pass не блокирует watcher.
- `TenantDataSourceConfig` и watcher поддерживают `TENANTS_CONFIG_REQUIRED=true`: отсутствие,
пустой список или ошибка чтения production-файла фиксируют configuration failure, а H2 не
маскирует отказ. Actuator endpoints исключены из tenant-interceptor и не раскрывают
components, домены, JDBC URL или credentials.
- AutoUpdateDocs синхронизировал `docs/API.md`, `docs/ARCHITECTURE.md` и
`docs/INFRASTRUCTURE.md`. На этом этапе ограничение watcher по обновлению URL/credentials
существующего домена было оставлено проблеме № 13 и устранено следующим этапом.
- В текущем внешнем `../k8s` подтверждены directory mount без `subPath`,
`TENANTS_CONFIG_REQUIRED=true`, Role `get/update` для `tenants-secret` и HTTP liveness /
readiness probes. Манифесты успешно собираются Kustomize; production rollout остаётся
отдельным действием оператора.
Фактические проверки этапа:
| Команда | Результат |
|---|---|
| `KubernetesTenantSecretUpdaterTest` | 21 тест, 0 failures, 0 errors, 0 skipped; два конкурентных updater, CAS conflict/retry, 408/5xx reconciliation, проверка 2xx и безопасная компенсация. |
| Health/startup target (`TenantDataSourceConfigTest`, registry/monitor/indicator, Actuator, MVC exclusion) | 19 тестов, 0 failures, 0 errors, 0 skipped. |
| Lifecycle/controller/watcher target | 24 теста, 0 failures, 0 errors, 0 skipped; PostgreSQL lifecycle отдельно повторён успешно после единичной Docker-сетевой флуктуации. |
| Контейнерный полный `mvn test` | 169 тестов, 0 failures, 0 errors, 0 skipped, BUILD SUCCESS; 40 suites. |
| `git diff --check` | Успешно. |
| Повторные LF-нормализованные SHA-256 V1/V2/V3/V4 | Совпадают с ранее зафиксированными значениями. |
| `kubectl kustomize ../k8s`, production secret-scan и `bash -n ../k8s/deploy.sh` | Успешно в финальном аудите. |
### № 13 — полная и повторяемая синхронизация tenant-конфигурации
- `TenantLifecycleService.synchronizeFromPersistedConfig()` валидирует весь входной список,
отклоняет нормализованные дубли до изменения runtime/readiness и сравнивает `name`, `domain`,
`url`, `username`, `password`. Новый или изменённый tenant проходит candidate prepare,
проверку соединения, Flyway и атомарный swap; persisted-путь не вызывает `TenantConfigStore`.
- Hash mounted `tenants.json` становится применённым только после успешного разбора и полной
lifecycle-синхронизации. Ошибка сохраняет прежний hash и повторяется с экспоненциальной
задержкой 30300 секунд; новая ревизия обходит backoff. Required missing/read/empty также
наблюдаемы через readiness и русский лог без credentials, hash или fingerprint.
- Перед `POST`/`DELETE` watcher под общим reentrant monitor применяет ещё не обработанный
semantic baseline. Ошибка чтения/разбора/sync возвращает безопасный `503` до начала мутации.
`TenantLifecycleMutationResult` возвращает controller-у immutable-квитанцию персистенции,
но не раскрывает её в HTTP.
- Snapshot fence ставится только для реально сохранённой изменённой версии
(`persisted && changed`) и сравнивает нормализованные `previous/committed` snapshots, а не
сырые JSON-хеши. Поэтому H0/H1 не откатывают две быстрые мутации до H2, форматирование JSON
не влияет на решение, merged snapshot другого pod применяется, а local/no-op receipt ничего
не блокирует.
- Ошибка публикации readiness после успешного swap больше не помечает активный новый pool как
`FAILED`: повтор проверяет действующее соединение и восстанавливает readiness без второго
Flyway/swap. Старый pool передаётся на drain сразу после успешной атомарной публикации.
- PostgreSQL 16.3/Testcontainers подтверждает сохранность прежнего route при неверных
credentials и Flyway checksum, успешную замену на валидную БД, работу in-flight соединения,
drain старого pool и отсутствие повторной записи Secret в persisted-пути.
- Windows/Docker portability TLS-теста стабилизирована явным `https://localhost:<port>`,
совпадающим с SAN тестового сертификата; production hostname verification не ослаблялась.
- AutoUpdateDocs повторно синхронизировал `docs/API.md`, `docs/ARCHITECTURE.md` и
`docs/INFRASTRUCTURE.md` с финальной receipt/snapshot-схемой.
Фактические проверки этапа:
| Команда | Результат |
|---|---|
| `TenantConfigWatcherTest,TenantLifecycleServiceTest,DatabaseControllerTest,TenantRoutingDataSourceTest` | 53 теста, 0 failures, 0 errors, 0 skipped, BUILD SUCCESS. |
| `TenantLifecyclePostgreSqlIntegrationTest` | 2 теста, 0 failures, 0 errors, 0 skipped; реальный PostgreSQL 16.3/Testcontainers. |
| `KubernetesTenantSecretUpdaterTest` | 21 тест, 0 failures, 0 errors, 0 skipped; TLS, CAS, reconciliation и компенсация. |
| Полный `mvn -Dapi.version=1.44 test` | 200 тестов, 0 failures, 0 errors, 0 skipped, BUILD SUCCESS. |
| `git diff --check` | Успешно. |
| Повторные LF-нормализованные SHA-256 V1/V2/V3/V4 | Совпадают с ранее зафиксированными значениями; миграционные файлы не изменялись. |
### № 11 — достоверный статус проверки конфликтов на дашборде
- `formatLocalDate()` формирует `YYYY-MM-DD` из локальных компонентов `Date` без
`toISOString()`. `currentWeekDateRange()` создаёт отдельный понедельник и вычисляет
воскресенье как `понедельник + 6 дней`, поэтому начало месяца и переход года не мутируют
исходную дату повторно.
- Расписания кафедр загружаются независимо через `Promise.allSettled`. Результат хранит число
ответивших кафедр и одно из состояний: `COMPLETE`, `PARTIAL` или `NOT_RUN`; технические
причины отказов не входят в UI-модель.
- `conflictCheckPresentation()` проверяет диапазон, количество конфликтов и согласованность
счётчиков fail-closed. Зелёная карточка разрешена только для `COMPLETE`, когда проверены все
кафедры и конфликтов нет. `PARTIAL` всегда показывает предупреждение и не скрывает найденные
по доступной части конфликты, а полный отказ отображается как «Проверка не выполнена».
- Карточки используют русские безопасные тексты, экранирование, явные success/warning/error
состояния, период и число проверенных кафедр. Контейнер получил live-region и `aria-busy`,
кнопка — `type="button"`; после ошибки начальной загрузки «Перепроверить» повторно получает
справочники, а не анализирует пустой список.
- Тот же локальный formatter устранит сдвиг даты сегодняшнего расписания до 03:00 по Москве.
Остальная часть общей timezone-проблемы остаётся в объёме № 29.
- Добавлены dependency-free тесты на встроенном `node:test` и воспроизводимые npm-команды.
AutoUpdateDocs синхронизировал структуру, контракт Red Zone и команды проверки в
`docs/FRONTEND.md`.
Фактические проверки этапа:
| Команда | Результат |
|---|---|
| `npm test` в `frontend/` | 12 тестов, 12 passed, 0 failed; локальная полночь `2026-07-02 00:15 Europe/Moscow`, диапазон `2026-06-29…2026-07-05`, переход года и состояния отказов. |
| `npm run check` в `frontend/` | Синтаксис `dashboard-conflicts.js` и `dashboard.js`, затем 12 тестов — успешно. |
| Рекурсивный `node --check` для `frontend/**/*.js` и `frontend/**/*.mjs` | 25 файлов, ошибок нет. |
| Полный backend-регресс | Backend на этапе № 11 не изменялся; последний полный прогон этапа № 13 остаётся зелёным: 200/0/0/0. |
| `git diff --check` | Успешно. |
| Защищённые файлы | `BUG_REPORT.md` и существующие Flyway-миграции не изменялись. |
### № 12 — access JWT только в памяти и same-origin JavaScript
- Добавлен единый `frontend/auth-session.js` для login, admin/settings, teacher и student.
Access JWT, роль, кафедра и ID пользователя существуют только в памяти ES-модуля;
legacy-ключи удаляются из `localStorage` и `sessionStorage`, но больше не читаются и не
записываются. После navigation/reload новый документ восстанавливает профиль через
`POST /api/auth/refresh` и `HttpOnly` cookie.
- `fetchWithAuth()` добавляет bearer-токен из памяти, дедуплицирует параллельные refresh-
запросы одним Promise и повторяет исходный запрос не более одного раза. Logout отзывает
серверную cookie-сессию и очищает память даже при сетевой ошибке.
- Inline CSS/JS кабинетов преподавателя и студента вынесены в `style.css`/`app.js`; inline-
редиректы кафедры и учебного отдела удалены. Admin/settings запускают UI только после
успешного async session bootstrap и проверяют роль из общего профиля.
- Удалён `admin/js/otel.js` с runtime-import из `esm.sh`. OpenTelemetry 2.9.0/0.220.0 и
auto-instrumentations 0.65.0 зафиксированы exact-версиями в `package-lock.json`, собираются
esbuild в `/vendor/otel.js` на первом этапе Dockerfile и загружаются только с текущего
origin. `npm audit` не обнаруживает уязвимостей.
- Apache подключает `security.conf`: `script-src 'self'`, `script-src-attr 'none'`,
`style-src 'self'`, exact SHA-256 для статических style-атрибутов, `connect-src 'self'`,
запрет frame/object и дополнительные защитные заголовки. `unsafe-inline`, `unsafe-eval`,
удалённые JS imports и динамические inline-стили отсутствуют; тест проверяет полный набор
CSP-хэшей. Базовые frontend-образы зафиксированы digest; остальные пункты № 30 остаются в
его собственном объёме.
- AutoUpdateDocs синхронизировал `docs/FRONTEND.md`, `docs/INFRASTRUCTURE.md`,
`docs/ARCHITECTURE.md`, `docs/API.md` и устаревший пример в `docs/DEVELOPMENT.md`.
Фактические проверки этапа:
| Команда | Результат |
|---|---|
| `npm run check` в `frontend/` | 21 тест: 12 dashboard + 5 auth session + 4 CSP/security; синтаксис критических модулей и все тесты успешны. |
| `npm audit --audit-level=moderate` | `found 0 vulnerabilities`. |
| Локальный `npm run build:vendor` | Same-origin bundle `dist/vendor/otel.js`, 327471 байт; удалённых imports нет. |
| `docker build -t magistr-frontend-codex:bug12 frontend` | Multi-stage npm/esbuild → Apache завершён успешно. |
| Live container HTTP check | `/` и `/vendor/otel.js` вернули `200`; CSP и дополнительные headers присутствуют, `unsafe-inline`/`unsafe-eval` отсутствуют. Временный контейнер остановлен и удалён. |
| `git diff --check` | Успешно. |
| Защищённые файлы | `BUG_REPORT.md` и существующие Flyway-миграции не изменялись. |
### № 33 и № 34 — безопасный DOM, скрытые пароли и языковой регламент
- Для ошибок загрузки SPA-вкладок, статуса/списка tenant-БД и заявок преподавателей добавлены
общие `renderStatusMessage()`/`renderTableMessage()`. Они создают DOM-узлы, записывают
фиксированный русский текст через `textContent` и атомарно заменяют содержимое через
`replaceChildren`; `exception.message` больше не попадает в эти UI-границы.
- Поля пароля при создании пользователя, настройке tenant-БД и одобрении заявки преподавателя
используют `type="password"` и `autocomplete="new-password"`. Поля логина помечены
`autocomplete="username"`.
- Метки доступности БД `Online`/`Offline` заменены на `Доступно`/`Недоступно`. Затронутые
production-логи `TenantDataSourceConfig`, `TenantRoutingDataSource` и `TenantInterceptor`
переведены на русский; вместо текста исключения фиксируется только безопасный технический
тип, а стек остаётся в отдельной русскоязычной записи `DEBUG`. Англоязычные frontend-логи
загрузки оборудования, форм обучения и подгрупп также русифицированы; raw `error.message`
больше не дописывается в собственные browser-console сообщения.
- CSP пересчитан после удаления двух неиспользуемых style-атрибутов. Статический тест требует
точного равенства множества SHA-256-хэшей реальным атрибутам и не допускает ослабления
`unsafe-inline`/`unsafe-eval`.
- AutoUpdateDocs синхронизировал `docs/FRONTEND.md`, `docs/ARCHITECTURE.md` и
`docs/LOGGING.md` с безопасным DOM, парольными полями и правилами журналирования.
Фактические проверки этапа:
| Команда | Результат |
|---|---|
| `npm run check` в `frontend/` | 24 проверки: 12 dashboard + 5 auth session + 7 security; все успешны. |
| `npm audit --audit-level=moderate` | `found 0 vulnerabilities`. |
| Полный контейнерный `mvn -Dapi.version=1.44 test` | 200 тестов, 0 failures, 0 errors, 0 skipped, BUILD SUCCESS. |
| `docker build -t magistr-frontend-codex:bug12-34 frontend` | Multi-stage npm/esbuild → Apache завершён успешно. |
| Live container HTTP check | `/` и `/vendor/otel.js` вернули `200`; актуальный CSP и дополнительные headers присутствуют. Временный контейнер остановлен и удалён. |
| `git diff --check` | Успешно. |
| Защищённые файлы | `BUG_REPORT.md` и Flyway V1V4 не изменялись; SHA-256 совпадают с ранее зафиксированными значениями. |
### № 15 — инварианты временных слотов
- CRUD слотов перенесён из `TimeSlotAdminController` в публичный транзакционный
`TimeSlotService`. Сервис проверяет положительный номер пары, существование сетки,
`startTime < endTime`, длительность не меньше минуты и вычисляет `durationMinutes` только
по границам времени, игнорируя одноимённое поле входного DTO.
- Создание блокирует строку целевой сетки, update — строку слота и старую/новую сетки в
стабильном порядке. Внутри сетки запрещены повторный номер и пересечение по условию
`start < other.end AND end > other.start`; касающиеся границами интервалы разрешены.
- Используемый правилом слот нельзя перенести из `DEFAULT`; удаление используемого слота
также отклоняется до SQL-ошибки. Конфликт номера или интервала возвращается как русский
`409 Conflict` через общий `ScheduleConflictException`.
- Раздел инвариантов временных слотов в `V1__init.sql` проверяет seed, добавляет
`btree_gist`, CHECK вычисленной длительности, GiST exclusion constraint с
полуоткрытыми интервалами и триггеры, сериализующие создание правила с переносом слота
или изменением режима сетки.
- В настройках поле минут стало `readonly`, всегда пересчитывается при изменении границ и
больше не отправляется как доверенное значение. AutoUpdateDocs синхронизировал
`docs/API.md`, `docs/BUSINESS_LOGIC.md`, `docs/DATABASE.md` и `docs/FRONTEND.md`.
Фактические проверки этапа:
| Команда | Результат |
|---|---|
| Контейнерный `mvn -Dtest=TimeSlotServiceTest test` без Docker socket | 6 тестов, 0 failures, 0 errors, 0 skipped, BUILD SUCCESS. |
| Контейнерный `mvn -Dtest=!*IntegrationTest test` без Docker socket | 194 теста, 0 failures, 0 errors, 0 skipped, BUILD SUCCESS. |
| `TimeSlotInvariantMigrationIntegrationTest` | 2 PostgreSQL-сценария базовой V1 добавлены и успешно скомпилированы; запуск Testcontainers из Maven-контейнера среда отклонила из-за запрета передачи Docker socket. |
| Изолированный PostgreSQL 16.3 без томов | Единая V1 применилась с нуля; соседние слоты приняты; overlap (`23P01`), неверная длительность (`23514`) и три нарушения базовой сетки (`23514`) отклонены. |
| Две параллельные пересекающиеся транзакции | Ровно один `COMMIT`; вторая операция отклонена `ex_time_slots_scope_no_overlap`. Одноразовый контейнер удалён. |
| `npm run check` в `frontend/` | 24 проверки, все успешны. |
| `git diff --check` | Успешно. |
| Консолидация Flyway | По прямому разрешению владельца V2V7 удалены, их итоговая схема включена в V1. |
### № 16 — инварианты учебных годов и семестров
- CRUD годов и семестров перенесён из `AcademicCalendarAdminController` в публичный
транзакционный `AcademicPeriodService`. Названия годов нормализуются до duplicate-check,
даты обязательны и упорядочены, а календарные диапазоны трактуются как включительные.
- Учебные годы не могут пересекаться. Семестр должен полностью находиться внутри своего
года, не пересекаться с другим семестром этого года и иметь уникальный тип. Update года
отклоняет границы, которые исключат любой сохранённый семестр; update повторно проверяет
title/type и возвращает безопасный русский `409`, а не JDBC `500`.
- Semester create/update блокируют строку родительского года; update затем перечитывает
строку семестра с `PESSIMISTIC_WRITE`. Инвалидация кэша расписания регистрируется только
после успешного commit.
- Раздел учебных периодов в `V1__init.sql` проверяет seed, добавляет GiST exclusion
constraints годов и семестров с
`daterange(..., '[]')` и два триггера. Блокировка родительской строки в триггере закрывает
гонку между изменением года и конкурентной записью семестра.
- AutoUpdateDocs синхронизировал `docs/API.md`, `docs/BUSINESS_LOGIC.md`,
`docs/DATABASE.md` и `docs/ARCHITECTURE.md`.
Фактические проверки этапа:
| Команда | Результат |
|---|---|
| Контейнерный `mvn -Dtest=AcademicPeriodServiceTest test` без Docker socket | 8 тестов, 0 failures, 0 errors, 0 skipped, BUILD SUCCESS. |
| Контейнерный `mvn -Dtest=!*IntegrationTest test` без Docker socket | 202 теста, 0 failures, 0 errors, 0 skipped, BUILD SUCCESS. |
| `AcademicPeriodInvariantMigrationIntegrationTest` | 2 PostgreSQL-сценария базовой V1 добавлены и успешно скомпилированы; Testcontainers-прогон из Maven-контейнера недоступен из-за запрета передачи Docker socket. |
| Изолированный PostgreSQL 16.3 без томов | Единая V1 применилась с нуля; соседние периоды приняты; overlap года/семестра (`23P01`), выход семестра и сужение года (`23514`) отклонены. |
| Две параллельные пересекающиеся транзакции годов | Ровно один `COMMIT`; вторая операция отклонена `ex_academic_years_no_overlap`. Одноразовый контейнер удалён. |
| `git diff --check` | Успешно. |
| Текущая baseline V1 | SHA-256 `5f9326b545d14a8afb3c75de9e59ea9a35b36ed02fe5a8a822e8430fc3e972cd`. |
### № 17 — совместимость сохранённых календарных назначений и сеток
- Добавлен публичный транзакционный `AcademicStructureService`. Обновление графика
блокирует его строку и до мутации проверяет все назначения, старшие курсы сетки,
дисциплины старших семестров и даты сетки относительно нового учебного года.
- Обновление группы блокирует её строку и повторно проверяет год начала обучения,
специальность, профиль и форму для всех назначений. При несовместимости
`ScheduleConflictException` преобразуется в русский `409 Conflict`; сущность и назначения
остаются без изменений.
- Сохранение назначения блокирует группу, график, учебный год и существующую запись,
проверяет совпадение измерений и вычисленный курс. Инвалидация кэша выполняется только
после commit.
- V1 содержит триггеры совместимости назначения, обратной защиты группы и графика,
размерности строк сетки и дисциплин, а также защиты зависимостей при изменении границ
учебного года. Seed назначения теперь сразу связывает календарь по году, специальности,
профилю и форме обучения.
- Добавлены 7 unit-тестов сервиса и 2 Testcontainers-сценария базовой схемы, включая гонку
изменения группы с созданием назначения.
Фактические проверки этапа:
| Команда | Результат |
|---|---|
| Контейнерный `mvn -Dtest=AcademicStructureServiceTest test` | 7 тестов, 0 failures, 0 errors, BUILD SUCCESS. |
| Контейнерный `mvn -Dtest=!*IntegrationTest test` | 209 тестов, 0 failures, 0 errors, 0 skipped, BUILD SUCCESS. |
| Контейнерный `mvn -DskipTests test` | Все 46 test source-файлов, включая DB-интеграционные, успешно скомпилированы. |
| Чистая PostgreSQL 16.3 без томов | Единая V1 применилась; 0 несовместимых seed-назначений; созданы 4 ключевых constraint и 5 dependency-trigger. |
| Прямые DB-изменения | Изменения формы графика/группы, уменьшение `course_count`, строка курса 5 и дисциплина семестра 9 отклонены с `23514`; назначение сохранено. |
| Две конкурентные транзакции | Ровно один `COMMIT`; несовместимое назначение отклонено с `23514`, итоговое состояние согласовано. |
| `git diff --check` | Успешно. |
### № 18 — историческая модель кафедр преподавателя
- Добавлен публичный транзакционный `TeacherDepartmentService`. Он разрешает основную и
дополнительную кафедру на целевую дату, пакетно загружает назначения за диапазон и
возвращает исторических преподавателей по периоду действия пользователя и назначения.
- `UserController`, `DepartmentWorkspaceController`, `TeacherSubjectController` и
`WorkloadController` больше не используют `users.department_id` как источник бизнес-
решений. Legacy-поле остаётся зеркалом основной кафедры на текущую дату.
- Будущий перевод закрывает прежний основной период днём перед `validFrom`, но сохраняет
текущую принадлежность до вступления новой записи в силу. Перевод на текущую или прошлую
дату синхронизирует legacy-зеркало. Пользователь и история блокируются до изменения.
- Нагрузка определяет кафедру отдельно на дату каждого занятия: сначала по основной, затем
по дополнительной связи. Перевод внутри диапазона разделяет агрегат одного преподавателя
между кафедрами, а прошлый отчёт не зависит от его текущей кафедры.
- V1 содержит GiST exclusion constraint
`ex_teacher_primary_department_no_overlap`, запрещающий пересечение любых основных
периодов одного преподавателя, включая конкурентную запись из нескольких pod.
- AutoUpdateDocs синхронизировал `docs/API.md`, `docs/BUSINESS_LOGIC.md`,
`docs/DATABASE.md` и `docs/ARCHITECTURE.md`.
Фактические проверки этапа:
| Команда | Результат |
|---|---|
| Контейнерный `mvn -Dtest=!*IntegrationTest test` без Docker socket | 216 тестов, 0 failures, 0 errors, 0 skipped, BUILD SUCCESS; все 49 test source-файлов скомпилированы. |
| Целевые service/controller tests | 12 тестов: будущий и текущий перевод, архивная история, приоритет основной связи, fallback на дополнительную, доступ к дисциплине и разделение нагрузки; все успешны. |
| Чистая PostgreSQL 16.3 без томов | Точная текущая V1 применилась с нуля; constraint `ex_teacher_primary_department_no_overlap` создан. |
| Прямые DB-изменения | Соседние основные периоды и дополнительное назначение приняты; пересекающийся основной период отклонён с `23P01`. |
| Две конкурентные транзакции | Ровно один `COMMIT`; вторая пересекающаяся основная запись отклонена с `23P01`. |
| `npm run check` в `frontend/` | 3 test suite и синтаксические проверки завершены успешно. |
| `docker compose config --quiet` | Успешно. |
| `git diff --check` | Успешно. |
| Текущая baseline V1 | SHA-256 `5f9326b545d14a8afb3c75de9e59ea9a35b36ed02fe5a8a822e8430fc3e972cd`. |
### № 19 — ограничение кафедрального доступа к workload
- `WorkloadController` вычисляет разрешённый `departmentId` до любого вызова
`ScheduleQueryService`. Для роли `DEPARTMENT` отсутствие фильтра означает кафедру из
`AuthContext`, а несовпадающий ID отклоняется с русским `403 Forbidden`.
- Единое правило применено к отчётам преподавателей, аудиторий, кафедр, временных слотов и
свободным аудиториям. Последний endpoint не принимает `departmentId`, но его внутренняя
выборка для кафедры также больше не является глобальной.
- `ADMIN`, `EDUCATION_OFFICE` и `SCHEDULE_VIEWER` сохраняют явно документированное право на
глобальный workload или выбор конкретной кафедры. Отсутствующий `AuthContext` обрабатывается
fail-closed с русским `401`.
- AutoUpdateDocs синхронизировал `docs/API.md`, `docs/BUSINESS_LOGIC.md` и
`docs/ARCHITECTURE.md`.
Фактические проверки этапа:
| Команда | Результат |
|---|---|
| Контейнерный `mvn -Dtest=WorkloadControllerTest test` без Docker socket | 5 тестов, 0 failures, 0 errors, 0 skipped, BUILD SUCCESS. |
| Межкафедральный сценарий | Чужой `departmentId` даёт `403` до вызова поиска; отсутствие ID и все пять endpoints используют кафедру из `AuthContext`. |
| Привилегированный сценарий | `EDUCATION_OFFICE` сохраняет глобальный scope без `departmentId`. |
| Контейнерный `mvn -Dtest=!*IntegrationTest test` без Docker socket | 219 тестов, 0 failures, 0 errors, 0 skipped, BUILD SUCCESS. |
| `docker compose config --quiet` | Успешно. |
| `git diff --check` | Успешно. |
### № 20 — workload без интерактивного лимита 50 групп
- В `ScheduleQueryService` выделен узкий публичный `searchForAggregation(departmentId,
timeSlotId, startDate, endDate)`. Он не принимает остальные UI-фильтры и поэтому не
становится вторым общим поисковым API.
- Обычный `search()` продолжает отклонять широкий пользовательский запрос, если тот затрагивает
больше 50 активных групп. Оба пути используют единый внутренний алгоритм проверки диапазона,
выбора групп, генерации, применения одного снимка overrides, фильтрации и дедупликации.
- Все workload-отчёты и free-classrooms переведены на aggregate-путь. Кафедральный scope из
№19 вычисляется до генерации и сохранён без изменений.
- Параметризованный тест доказывает полный вызов генератора для 51 и 100 активных групп;
отдельный regression-test сохраняет отказ интерактивного поиска на 51-й группе.
- AutoUpdateDocs синхронизировал `docs/API.md`, `docs/BUSINESS_LOGIC.md` и
`docs/ARCHITECTURE.md`.
Фактические проверки этапа:
| Команда | Результат |
|---|---|
| Контейнерный `mvn -Dtest=ScheduleQueryServiceTest,WorkloadControllerTest test` | 9 тестов, 0 failures, 0 errors, 0 skipped, BUILD SUCCESS. |
| Aggregate 51/100 групп | Все активные группы переданы генератору, overrides прочитаны один раз, исключение лимита не возникает. |
| Интерактивный regression | Обычный широкий `search()` с 51 активной группой по-прежнему отклонён с просьбой уточнить scope. |
| Контейнерный `mvn -Dtest=!*IntegrationTest test` без Docker socket | 221 тест, 0 failures, 0 errors, 0 skipped, BUILD SUCCESS. |
| `docker compose config --quiet` | Успешно. |
| `git diff --check` | Успешно. |
### № 21 — безопасный импорт дисциплин кафедры
- Импорт вынесен из `DepartmentWorkspaceController` в транзакционный
`SubjectImportService`. Весь payload нормализуется и дедуплицируется по названию без учёта
регистра до мутации; повтор внутри payload обрабатывается один раз.
- Глобальная уникальность названия, уже заявленная в бизнес-документации, сохранена. Если
запись принадлежит текущей кафедре, повторный импорт обновляет её код, восстанавливает из
архива и сохраняет прежний ID. Совпадение с другой кафедрой даёт русский `409 Conflict`,
не меняя владельца или код чужой записи.
- В V1 прежний case-sensitive `UNIQUE(name)` заменён функциональным уникальным индексом
`uq_subjects_name_ci` на `lower(name)`. Он закрывает регистровые дубли и гонку импортов
разных кафедр; admin-create также проверяет имя без учёта регистра.
- Ошибка уникальности при `saveAllAndFlush` преобразуется в безопасный доменный конфликт.
Новых миграционных файлов не создано.
- AutoUpdateDocs синхронизировал `docs/API.md`, `docs/BUSINESS_LOGIC.md`,
`docs/DATABASE.md` и `docs/ARCHITECTURE.md`.
Фактические проверки этапа:
| Команда | Результат |
|---|---|
| Контейнерный `mvn -Dtest=SubjectImportServiceTest,DepartmentWorkspaceControllerTest test` | 7 тестов, 0 failures, 0 errors, 0 skipped, BUILD SUCCESS. |
| Повторный собственный импорт | Два последовательных импорта возвращают тот же объект/ID, обновляют код и восстанавливают архивную запись. |
| Совпадение чужой кафедры | Получен доменный `409`; чужие `departmentId` и код не изменены, `saveAllAndFlush` не вызван. |
| Контейнерный `mvn -Dtest=!*IntegrationTest test` без Docker socket | 225 тестов, 0 failures, 0 errors, 0 skipped, BUILD SUCCESS; 51 test source-файл скомпилирован. |
| Чистая PostgreSQL 16.3 без томов | Точная текущая V1 применилась с нуля; `информатика` отклонена как дубль seed `Информатика` по `uq_subjects_name_ci` (`23505`). |
| Две конкурентные транзакции | Первая запись `Теория систем` закоммичена; параллельная `теория систем` после ожидания отклонена `uq_subjects_name_ci` (`23505`). |
| `docker compose config --quiet` | Успешно. |
| `git diff --check` | Успешно. |
| Текущая baseline V1 | SHA-256 `5f9326b545d14a8afb3c75de9e59ea9a35b36ed02fe5a8a822e8430fc3e972cd`. |
### № 22 — согласованная матрица прав учебного отдела
- `SpecialityController` сохранил class-level `ADMIN` для всех write-операций, но обязательные
GET специальностей, общего списка профилей и профилей специальности явно разрешены
`EDUCATION_OFFICE`.
- `EducationFormController` разрешает `ADMIN` и `EDUCATION_OFFICE` создавать и удалять формы
обучения. Широкий read-only GET для остальных ранее разрешённых ролей сохранён method-level
аннотацией; защита используемых группами/графиками форм не ослаблена.
- Дубли матриц `ROLE_NAVIGATION` и `ROLE_TABS` удалены. Основной admin SPA и settings SPA
используют единый `frontend/admin/js/role-capabilities.js`, где для учебного отдела явно
перечислены календарный график, временные слоты и формы обучения.
- MockMvc-тест проходит через реальный `AuthorizationInterceptor`: GET specialties/profiles и
GET/POST/DELETE education forms возвращают `200`, а POST специальности остаётся `403`.
- Frontend-тест импортирует матрицу как ES-модуль, проверяет capabilities учебного отдела и
отсутствие локальных дубликатов в двух entrypoint-файлах.
- AutoUpdateDocs синхронизировал `docs/API.md`, `docs/BUSINESS_LOGIC.md`,
`docs/FRONTEND.md` и `docs/ARCHITECTURE.md`.
Фактические проверки этапа:
| Команда | Результат |
|---|---|
| Контейнерный `mvn -Dtest=EducationOfficeRoleMatrixTest test` | 2 теста, 0 failures, 0 errors, 0 skipped, BUILD SUCCESS. |
| MockMvc role matrix | Штатная инициализация справочников календаря и CRUD форм обучения не получают `403`; изменение специальности учебным отделом отклонено. |
| `npm run check` в `frontend/` | Синтаксис entrypoints и 3 test suite, включая единую role matrix, завершены успешно. |
| Контейнерный `mvn -Dtest=!*IntegrationTest test` без Docker socket | 227 тестов, 0 failures, 0 errors, 0 skipped, BUILD SUCCESS; 52 test source-файла скомпилированы. |
| `docker compose config --quiet` | Успешно. |
| `git diff --check` | Успешно. |
### № 23 — batch-снимок генерации расписания
- `ScheduleQueryService` больше не вызывает генератор отдельно для каждой группы: обычный
поиск, workload-агрегаты и построение полного базового дня передают весь набор групп в
`ScheduleGeneratorService.buildScheduleForGroups()`.
- `AcademicDateService.ScheduleSnapshot` одним набором запросов загружает пересекающиеся
семестры, назначения календарей групп и `academic_calendar_days` от начала затронутого
семестра до конца диапазона. Внутренние проверки semester/assignment/activity работают по
неизменяемым lookup-картам.
- Правила группы и преподавателя получили batch-запросы по набору семестров. Генератор
индексирует их по `groupId + semesterId`, поэтому репозиторий не вызывается из циклов дат,
правил и групп.
- Ручные назначения сетки звонков, weekday scopes и временные слоты также читаются один раз
на диапазон; фактический слот выбирается по `date/scope/orderNumber` без дополнительных
запросов на каждое занятие.
- `ScheduleGenerationQueryCountTest` обрабатывает 120 включительных дат, 30 групп и 20
правил. Тест измеряет ровно 7 чтений репозиториев, запрещает старые point-методы и
одновременно подтверждает 30 корректных занятий с эффективной weekday-сеткой.
- AutoUpdateDocs синхронизировал `docs/BUSINESS_LOGIC.md` и `docs/ARCHITECTURE.md`.
Фактические проверки этапа:
| Команда | Результат |
|---|---|
| `ScheduleGenerationQueryCountTest` | 1 тест, 0 failures, 0 errors, 0 skipped; 120 дат, 30 групп, 20 правил, 7 repository reads. |
| `ScheduleQueryServiceTest,ScheduleQueryServiceTeacherOverrideTest` | 9 тестов, 0 failures, 0 errors, 0 skipped; batch-путь и overrides сохранены. |
| Контейнерный `mvn -Dtest=!*IntegrationTest test` без Docker socket | 228 тестов, 0 failures, 0 errors, 0 skipped, BUILD SUCCESS; 53 test source-файла скомпилированы. |
### № 24 — tenant-aware cleanup refresh-сессий
- `RefreshTokenCleanupJob` получает атомарный snapshot доменов из
`TenantRoutingDataSource`, устанавливает `TenantContext` до вызова repository proxy и
очищает каждую tenant-БД отдельно. Исходный tenant потока восстанавливается в `finally`.
- По умолчанию истёкшие и отозванные строки сохраняются 30 дней для аудита. Retention,
включение job, размер пачки, максимум пачек, interval и initial delay задаются через
`JWT_REFRESH_CLEANUP_*`.
- Native batch-delete выбирает только записи с `expires_at < cutoff` либо старым
`revoked_at`, использует `FOR UPDATE SKIP LOCKED` и отдельную транзакцию repository-метода.
Два pod разбирают непересекающиеся строки; повторная очистка безопасно возвращает 0.
- Локальный `AtomicBoolean` не допускает наложения двух запусков внутри одного pod, а лимит
пачек ограничивает длительность одного прохода. Ошибка одной tenant-БД логируется по-русски
без технического текста и не останавливает очистку остальных.
- В V1 добавлены частичный индекс по `revoked_at` и индекс по `expires_at`; новых файлов
миграций не создавалось. Текущий SHA-256 V1:
`5f9326b545d14a8afb3c75de9e59ea9a35b36ed02fe5a8a822e8430fc3e972cd`.
- AutoUpdateDocs синхронизировал `docs/DATABASE.md`, `docs/ARCHITECTURE.md` и
`docs/INFRASTRUCTURE.md`.
Фактические проверки этапа:
| Команда | Результат |
|---|---|
| `RefreshTokenCleanupJobTest,RefreshTokenServiceTest` | 7 тестов, 0 failures, 0 errors, 0 skipped; два job, retention, idempotency, overlap guard и disabled mode. |
| Контейнерный `mvn -Dtest=!*IntegrationTest test` без Docker socket | 231 тест, 0 failures, 0 errors, 0 skipped, BUILD SUCCESS; 54 test source-файла скомпилированы. |
| Точная V1 на чистой PostgreSQL 16.3 | Схема и два cleanup-индекса созданы без ошибок. |
| Две конкурентные PostgreSQL-транзакции | При незавершённой первой транзакции обе удалили разные пачки по 3 строки; повтор удалил 0, fresh-expired/fresh-revoked/active сохранены. |
| Testcontainers через Maven-контейнер | Не запускался: среда отклонила проброс `/var/run/docker.sock`; PostgreSQL-специфичный SQL проверен безопасным отдельным контейнером без socket mount. |
### № 25 — безопасное преобразование DB/JDBC-ошибок
- `GlobalExceptionHandler` перехватывает `DataIntegrityViolationException` до общего `500`
и делегирует определение безопасного ответа в `DatabaseConstraintViolationMapper`.
- Именованные CHECK-ограничения текущей V1 преобразуются в русские `400`; UNIQUE, FK и
GiST exclusion constraints — в русские `409`. Неизвестный SQLState класса integrity
получает обобщённый `409` без текста драйвера.
- Технические `constraint` и SQLState доступны только в структурированном журнале.
JDBC/SQL message, stack trace, запросы и значения параметров в HTTP JSON не включаются.
- Локальные `catch Exception`, которые формировали тело ответа через `getMessage()`, удалены:
persistence-ошибки доходят до общего handler, а контролируемые доменные сообщения сохранены.
- AutoUpdateDocs синхронизировал `docs/API.md`, `docs/ARCHITECTURE.md` и
`docs/DEVELOPMENT.md`.
Фактические проверки этапа:
| Команда | Результат |
|---|---|
| `GlobalExceptionHandlerDataIntegrityTest` | 4 теста, 0 failures, 0 errors, 0 skipped; известные CHECK/UNIQUE и неизвестные нарушения не раскрывают constraint, JDBC URL, SQL или пароль. |
| Связанные controller tests | `ScheduleOverrideControllerTest` и `ScheduleRuleAdminControllerTest`: 5 тестов, 0 failures, 0 errors. |
| Контейнерный `mvn -Dtest=!*IntegrationTest test` без Docker socket | 235 тестов, 0 failures, 0 errors, 0 skipped, BUILD SUCCESS; 55 test source-файлов скомпилированы. |
| `docker compose config --quiet` | Успешно. |
| `git diff --check` | Успешно. |
| Каталог Flyway | Содержит только `V1__init.sql`; SHA-256 не изменился: `5f9326b545d14a8afb3c75de9e59ea9a35b36ed02fe5a8a822e8430fc3e972cd`. |
### № 26 — самодостаточный локальный Docker Compose
- Compose больше не требует заранее созданную внешнюю сеть: bridge-сеть `magistr`, DNS
сервисов и именованный том `postgres_data` создаются самим проектом.
- Frontend публикует `${HTTP_PORT:-80}` и через Apache `mod_proxy` направляет same-origin
`/api` и `/actuator/health` во внутренний backend. Исходный `Host` сохраняется, поэтому
локальный запрос выбирает tenant `default`; backend и PostgreSQL наружу не публикуются.
- `${POSTGRES_DB:-app_db}` одновременно задаёт создаваемую БД и
`SPRING_DATASOURCE_URL`. Backend получает обязательные `POSTGRES_PASSWORD`, `JWT_SECRET`,
JWT TTL и cleanup-параметры из `.env`; безопасный шаблон добавлен в `.env.example`.
- PostgreSQL закреплён на версии 16.3 и хранит данные в named volume. Backend имеет
liveness healthcheck, а frontend запускается после его готовности; `up --wait` теперь
подтверждает не только старт процесса, но и рабочий Spring context.
- Live startup обнаружил, что Spring не мог выбрать production-конструкторы
`TenantDatabaseHealthMonitor` и `RefreshTokenCleanupJob`, хотя unit-тесты создавали их
вручную. Оба конструктора явно помечены для injection; backend стабильно выходит в
`Started Application`.
- Скачивание mutable OpenTelemetry `latest`, блокировавшее чистую сборку, заменено
фиксированным Maven-артефактом `2.28.1` с обязательной SHA-256 проверкой. Локальный OTel
SDK по умолчанию отключён, так как Collector не входит в Compose.
- AutoUpdateDocs синхронизировал `docs/README.md` и `docs/INFRASTRUCTURE.md`.
Фактические проверки этапа:
| Команда | Результат |
|---|---|
| `docker compose up -d --build --wait` | PostgreSQL, backend healthcheck и frontend готовы; внутренняя сеть и `magistr_postgres_data` созданы автоматически. |
| Live HTTP через frontend proxy | `/` → `200`, `/actuator/health/liveness` → `200`, защищённый `/api/departments` → ожидаемый `401`. |
| Публикация порта | Порт 80 уже занят пользовательским Caddy; эквивалентная проверка успешно выполнена через документированный `HTTP_PORT=18080`, чужой сервис не останавливался. |
| PostgreSQL mount | Docker inspect: named volume `magistr_postgres_data` смонтирован в `/var/lib/postgresql/data`; backend и DB не имеют host ports. |
| Java Agent supply chain | Maven Central `opentelemetry-javaagent:2.28.1`, SHA-256 `faa89bdeebf9b1f52be4a4374689176717b02a59df2d8f8b6eb9aa39f9292589`, build check `OK`. |
| Контейнерный `mvn -Dtest=!*IntegrationTest test` без Docker socket | 235 тестов, 0 failures, 0 errors, 0 skipped, BUILD SUCCESS. |
| `docker compose config --quiet` и `git diff --check` | Успешно. |
| Каталог Flyway | По-прежнему содержит только `V1__init.sql`; новых миграций нет. |
### № 27 — обязательные CI/CD gates и immutable rollout
- `.gitea/workflows/docker-build.yaml` получил обязательный `checks` job: полный backend
non-integration suite, frontend `npm ci && npm run check`, Compose validation и shell-тест
immutable deployment. Build/push jobs имеют `needs: checks`, поэтому непроверенный commit
не публикуется и не разворачивается.
- Metadata публикует полный `sha-<commit>` и release tag без `latest`. Оба build job отдают
registry digest, а deploy формирует ссылки `image@sha256:...`; release tag больше не
перезапускает старый `:main`.
- Workflow-wide `magistr-production` concurrency lock сериализует production-запуски.
`kubectl v1.33.12` загружается с официального CDN и проходит проверку закреплённого
SHA-256 до установки.
- `scripts/deploy-images.sh` отклоняет любые tag-only ссылки, сохраняет текущие backend и
frontend image refs, применяет оба новых digest и ждёт rollout. Ошибка `set image` или
readiness запускает восстановление обоих предыдущих образов и повторную проверку.
- `scripts/test-deploy-images.sh` использует fake kubectl для трёх сценариев: tag отклонён,
успешные digests применены, failed backend rollout вернул оба предыдущих digest.
- Во внешних `../k8s/backend.yaml` и `frontend.yaml` удалены `:main`; нулевой digest-sentinel
не может случайно выбрать mutable image. `../k8s/deploy.sh` требует два реальных digest и
подставляет их локально до apply. Backend probes переведены на HTTP liveness/readiness,
а ConfigMap теперь задаёт корректные `TENANTS_CONFIG_PATH` и
`TENANTS_CONFIG_REQUIRED=true`.
- AutoUpdateDocs синхронизировал `docs/INFRASTRUCTURE.md` и `../k8s/README.md`.
Фактические проверки этапа:
| Команда | Результат |
|---|---|
| `bash scripts/test-deploy-images.sh` | Mutable tag отклонён; success rollout завершён; failed rollout восстановил backend и frontend, тест завершён успешно. |
| Workflow static validation | Ruby YAML parser — успешно; отсутствуют `:main`, `stable.txt` и `rollout restart`; build jobs зависят от checks и экспортируют digest. |
| `bash -n` | `scripts/deploy-images.sh`, `scripts/test-deploy-images.sh` и `../k8s/deploy.sh` синтаксически корректны. |
| `K8S_DIR=../k8s bash scripts/check-production-secrets.sh` | Успешно. |
| `kubectl kustomize ../k8s >/dev/null` | Успешно. |
| `docker compose config --quiet` и `git diff --check` | Успешно. |
| Изменённые внешние файлы | `../k8s/backend.yaml`, `frontend.yaml`, `config.yaml`, `deploy.sh`, `README.md`; они находятся вне Git-корня `magistr` и отдельно проверены. |
| Production rollout | Не выполнялся: это внешняя необратимая операция, требующая отдельных полномочий и реальных build digests. |
### № 28 — общий PostgreSQL rate limit входа
- `LoginRateLimitService` выполняет создание/блокировку строки, bcrypt-проверку, изменение
счётчика и запись аудита в одной транзакции. Ключ состоит из tenant, NFKC-нормализованного
username в нижнем регистре и проверенного IP. `INSERT ... ON CONFLICT DO NOTHING` вместе
с `PESSIMISTIC_WRITE` сериализует первые и последующие попытки на разных backend-pod.
- Пять отказов за 15 минут по умолчанию дают блокировку на 60 секунд. Следующая ошибка после
окончания блокировки удваивает срок до максимума 15 минут. Controller возвращает `429`
с округлённым вверх `Retry-After`; успешный вход удаляет состояние.
- Неизвестный, архивный и ошибочный пользователь проходят одинаковый bcrypt-путь и получают
одинаковый `401`. В production-журнал попадает только SHA-256 fingerprint нормализованного
имени; пароль и признак существования пользователя не сохраняются и не логируются.
- `ClientIpResolver` игнорирует `X-Forwarded-For` от недоверенного непосредственного
источника, ограничивает длину/число hops и разбирает допустимую цепочку справа налево.
Compose доверяет внутренней Docker-сети, K3s ConfigMap — стандартной pod-сети Traefik;
нестандартный CIDR документирован как обязательная настройка оператора.
- В V1 добавлены `auth_login_rate_limits`, `auth_login_attempt_audit`, уникальный ключ,
CHECK и cleanup-индексы. `LoginAttemptAuditCleanupJob` раз в сутки tenant-aware пачками
`FOR UPDATE SKIP LOCKED` удаляет audit и неактивные счётчики старше 90 дней. Новых файлов
миграций не создано; текущий SHA-256 V1:
`250cab68c9ab6d8401619447585b47d6122458d1cb00a6e76af308414f89958d`.
- AutoUpdateDocs синхронизировал `docs/API.md`, `docs/ARCHITECTURE.md`,
`docs/DATABASE.md`, `docs/INFRASTRUCTURE.md` и внешний `../k8s/README.md`.
Фактические проверки этапа:
| Команда | Результат |
|---|---|
| `ClientIpResolverTest`, `LoginRateLimitServiceTest`, `AuthControllerTest` | 10 тестов, 0 failures; trusted chain, spoofed header, uniform reject, progressive block и `Retry-After`. |
| `LoginRateLimitConcurrencyIntegrationTest` | 2 теста, 0 failures; две отдельные service instance одной PostgreSQL-БД атомарно получили `REJECTED` + `BLOCKED`, failure count 2; cleanup сохранил fresh/active rows. |
| Полный контейнерный `mvn test` | 266 тестов: 263 успешны; все 12 тестов № 28 прошли. Три ранее добавленных конкурентных migration-теста нестабильны: два получили допустимый PostgreSQL `40P01` вместо ожидаемого только `23P01`, один запуск не воспроизвёл гонку. Стабилизация оставлена в финальном регрессионном аудите. |
| Чистая PostgreSQL 16.3 | Единственная V1 многократно применена Flyway без ошибок; обе новые таблицы и индексы созданы. |
| Compose, Kustomize, secret-scan, `git diff --check` | Успешно. |
| Изменённые внешние файлы | `../k8s/config.yaml`, `../k8s/README.md`; каталог вне Git-корня и проверен отдельно. |
### № 29 — единая временная зона бизнес-дат
- Добавлен `BusinessTimeService`: production использует UTC `Clock`, а календарную дату
вычисляет в настраиваемой зоне `BUSINESS_TIME_ZONE` с безопасным default
`Europe/Moscow`. Контроллеры пользователей, групп, кабинета кафедры, заявок и дисциплин,
генератор расписания и история кафедр получают текущую бизнес-дату через этот сервис.
- Абсолютные моменты refresh/login-аудита, lifecycle, overrides, заявок, комментариев и
назначений переведены с `LocalDateTime` на `Instant`. Hibernate и Jackson настроены на
UTC; все соответствующие колонки единственной `V1__init.sql` используют `TIMESTAMPTZ`.
- `formatLocalDate()` и календарная арифметика frontend используют локальные компоненты
`Date`; `toISOString().slice(0, 10)` для date-only в frontend больше не используется.
- Compose передаёт `BUSINESS_TIME_ZONE`, `TZ` и
`JAVA_TOOL_OPTIONS=-Duser.timezone=...`; `.env.example` содержит явные значения.
Kubernetes `app-config` содержит те же production-поля, поэтому backend pod не зависит от
timezone node или базового образа.
- AutoUpdateDocs синхронизировал `docs/API.md`, `docs/ARCHITECTURE.md`,
`docs/DATABASE.md`, `docs/DEVELOPMENT.md`, `docs/FRONTEND.md` и
`docs/INFRASTRUCTURE.md`.
- Новых миграционных файлов не создано; каталог Flyway содержит только `V1__init.sql`,
текущий SHA-256 после завершения № 31:
`fffedf66e8bf7d25d2d8c1b1a6146b45dd5b7e52233d05a68de8bf62c817f7a7`.
Фактические проверки этапа:
| Команда | Результат |
|---|---|
| `BusinessTimeServiceTest` | В первом полном прогоне обе границы московской полуночи прошли. |
| `npm test`, `node --check` | Все 4 frontend test-suite и изменённые JS-файлы прошли. |
| Static time scan | В production Java нет `LocalDate.now()`/`LocalDateTime.now()` без явной зоны; во frontend нет date-only через `toISOString`; в V1 и документации нет `TIMESTAMP` без timezone. |
| Compose и diff | `docker compose config --quiet` с обязательными секретами и `git diff --check` успешны. |
| Kubernetes timezone | Kustomize-рендер содержит `BUSINESS_TIME_ZONE=Europe/Moscow`, `TZ=Europe/Moscow` и `JAVA_TOOL_OPTIONS=-Duser.timezone=Europe/Moscow`. |
| Первый полный `mvn test` | 269 тестов: 263 успешны. После прогона исправлены новое lifecycle-ожидание, JDBC binding `Instant` и три ранее нестабильных ожидания PostgreSQL `40P01`; также усилена блокировка календарного assignment. |
| Повторный Maven/Testcontainers | Полный прогон после № 3132: 277 тестов, 0 failures, 0 errors, 0 skipped, BUILD SUCCESS. |
### № 30 — воспроизводимые и проверяемые артефакты
- Backend build/runtime, PostgreSQL Compose и все PostgreSQL Testcontainers закреплены
одновременно точной версией и manifest digest. Ранее закреплённые frontend base images
сохранены.
- Все сторонние Actions в Gitea workflow закреплены полными 40-символьными commit SHA;
Maven, Node.js и Trivy запускаются только по `tag@sha256:...`.
- OpenTelemetry Java Agent и `kubectl` имеют точные версии и обязательную SHA-256 проверку до
использования.
- Оба build job публикуют BuildKit provenance `mode=max` и SBOM-attestation. Отдельный
`security-scan` проверяет backend/frontend digests закреплённым Trivy и блокирует deploy при
исправимых `HIGH`/`CRITICAL` уязвимостях.
- Добавлены `scripts/check-artifact-pinning.sh` и его регрессионный shell-тест. Проверка
контролирует Dockerfile, Compose, Testcontainers, Actions, checksum, SBOM/provenance, scan
gate и зависимость deploy от scan. Опциональный `K8S_DIR` расширяет проверку на внешние
production-манифесты.
- AutoUpdateDocs синхронизировал `docs/DATABASE.md`, `docs/DEVELOPMENT.md`,
`docs/INFRASTRUCTURE.md` и `DEVOPS.md`.
- Внешний `../k8s/otel-collector.yaml` использует официальный Collector Contrib
`0.153.0@sha256:93aad750175cbf1a973ae1c5886c3371f4d800f61be25cdd26870b8441ffe9fa`.
Версия подтверждена официальным релизом OpenTelemetry, manifest digest получен напрямую
из Docker Hub Registry API.
- Новых Flyway-файлов не создано; в каталоге миграций остаётся только `V1__init.sql` с
SHA-256 `fffedf66e8bf7d25d2d8c1b1a6146b45dd5b7e52233d05a68de8bf62c817f7a7`.
Фактические проверки этапа:
| Команда | Результат |
|---|---|
| `bash scripts/check-artifact-pinning.sh` | Успешно: repo image refs, Actions, checksum, SBOM и scan gates закреплены. |
| `bash scripts/test-artifact-pinning.sh` | Успешно: незакреплённый base image и удалённая SBOM гарантированно отклоняются. |
| `bash scripts/test-deploy-images.sh` | Успешно: immutable refs, rollout и парный rollback. |
| Workflow YAML, Compose config, `git diff --check` | Успешно. |
| `K8S_DIR=../k8s bash scripts/check-artifact-pinning.sh` | Успешно: все production-образы имеют полный digest. |
| `kubectl kustomize ../k8s` | Успешно; рендер содержит закреплённый OTel Collector. |
### № 31 — инварианты численности групп и подгрупп
- Создание и изменение групп теперь проходят через общий validator
`AcademicStructureService`: численность, год начала обучения и все идентификаторы должны
быть положительными, обязательные поля не могут отсутствовать.
- При обновлении сервис блокирует группу и запрещает уменьшать `groupSize` ниже суммы
численностей активных подгрупп. Создание, обновление и архивация подгрупп также блокируют
строку родительской группы до завершения транзакции.
- `subgroups.student_capacity` стал обязательным. В V1 добавлены CHECK-ограничения
положительной численности группы, года начала обучения и численности подгруппы.
- Два триггера V1 защищают перекрёстный инвариант при прямых и конкурентных записях:
сумма активных подгрупп не может превышать размер группы. Ошибка сопоставляется с
безопасным русским ответом через `DatabaseConstraintViolationMapper`.
- Новых миграций не создано: вся итоговая схема находится в `V1__init.sql`, а V2V4
отсутствуют.
- AutoUpdateDocs синхронизировал `docs/API.md`, `docs/ARCHITECTURE.md`,
`docs/BUSINESS_LOGIC.md` и `docs/DATABASE.md`.
Фактические проверки этапа:
| Команда | Результат |
|---|---|
| `AcademicStructureServiceTest` | 11 тестов: общий create/update validator, положительные значения и запрет уменьшения ниже подгрупп прошли. |
| `AcademicStructureInvariantIntegrationTest` | 4 PostgreSQL-теста: CHECK, уменьшение группы и гонка уменьшения с добавлением подгруппы прошли; инвариант сохранён. |
| Полный контейнерный `mvn test` | 277 тестов, 0 failures, 0 errors, 0 skipped, BUILD SUCCESS. |
| Каталог Flyway | Ровно один `V1__init.sql`, SHA-256 `fffedf66e8bf7d25d2d8c1b1a6146b45dd5b7e52233d05a68de8bf62c817f7a7`. |
### № 32 — точный включительный лимит диапазона расписания
- `ScheduleQueryService` и `ScheduleGeneratorService` одинаково вычисляют количество
календарных дат как `ChronoUnit.DAYS.between(startDate, endDate) + 1`.
- Диапазон из 120 дат разрешён, а из 121 даты возвращает русскую ошибку валидации.
- AutoUpdateDocs уточнил включительную семантику лимита в `docs/API.md`,
`docs/ARCHITECTURE.md` и `docs/BUSINESS_LOGIC.md`.
Фактические проверки этапа:
| Команда | Результат |
|---|---|
| `ScheduleQueryServiceTest.rangeLimitCountsBothBoundaryDates` | Граница 120 разрешена, 121 отклонена. |
| `ScheduleGenerationQueryCountTest` | Существующая генерация 120 дат сохранена, запрос 121 даты отклонён до генерации. |
| Полный контейнерный `mvn test` | 277 тестов, 0 failures, 0 errors, 0 skipped, BUILD SUCCESS. |
## Финальный аудит
- Все 33 проблемы из области (`1` и `334`) имеют реализованную репозиторную часть и
воспроизводимое подтверждение. № 2 не изменялась, `BUG_REPORT.md` сохранён как исходный
реестр.
- Полностью закрыты и проверены 32 пункта. Для № 1 остаются только операторские ротация,
provisioning, rollout и очистка истории, которые исходный prompt прямо запрещает
выполнять без отдельных production-полномочий.
- Полный backend PostgreSQL/Testcontainers-прогон: 277 тестов, 0 failures, 0 errors,
0 skipped. Четыре frontend test-suite, `npm run check`, Compose validation,
Workflow YAML, artifact/deploy shell-тесты и `git diff --check` прошли.
- `kubectl kustomize ../k8s`, production secret-scan, синтаксис deploy-скрипта и расширенный
artifact-scan прошли. В production-манифестах отсутствуют mutable `latest` и `:main`.
- Каталог Flyway содержит ровно `V1__init.sql`; V2V4 и другие дополнительные миграции
отсутствуют. Финальный SHA-256 V1:
`fffedf66e8bf7d25d2d8c1b1a6146b45dd5b7e52233d05a68de8bf62c817f7a7`.
## Точка продолжения
Текущий этап: **реализация и проверка всех разрешённых изменений завершены**.
Оставшееся операторское действие владельца:
1. при готовности production-полномочий выполнить `docs/SECURITY_RUNBOOK.md`: ротацию,
provisioning, rollout и очистку истории для № 1. Эти действия не выполнялись
автоматически и не требуются для завершения репозиторной реализации.