Files
magistr/BUG_FIX_PROGRESS.md
2026-07-19 14:40:43 +03:00

108 KiB
Raw Blame History

Журнал исправления проблем

Обновлено: 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 250cab68c9ab6d8401619447585b47d6122458d1cb00a6e76af308414f89958d. Упоминания 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-БД. Оператору остаются RBAC get/update, mount без subPath, TENANTS_CONFIG_REQUIRED=true и HTTP probes в отсутствующем ../k8s. 64 целевых теста; полный backend 169/0/0/0; LF-хеши V1V4 совпадают. Kustomize не выполнен: ../k8s отсутствует.
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 процесса; frontend date-only строится через UTC. Инъекция Clock/Europe/Moscow, UTC timestamps, локальное форматирование date-only, TZ контейнеров. Boundary tests 00:0003:00 и первые дни месяца.
30 не начато Образы/скачиваемые инструменты используют mutable tags/latest без checksum/SBOM/scan. Pin versions+digests, SHA verification, SBOM и image scan gates. Static scan production refs и CI configuration.
31 не начато Group create/update допускает неположительные значения и уменьшение ниже активных подгрупп. Общий validator, locking, новые CHECK constraints. Create/update/concurrency и migration tests.
32 не начато between(start,end) > 120 разрешает 121 включительную дату. Проверять between + 1 <= 120 в обоих сервисах. Границы 120/121.
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, поэтому манифесты не менялись и Kustomize не запускался. Для production обязательны directory mount без subPath, TENANTS_CONFIG_REQUIRED=true, Role get/update для tenants-secret и HTTP liveness / readiness probes; применение и 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 Не выполнено: внешний каталог ../k8s отсутствует в текущем workspace.

№ 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/liveness200, защищённый /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 — единая временная зона бизнес-дат.

Следующая операция:

  1. построить graphify-контекст всех LocalDate.now()/LocalDateTime.now() и frontend toISOString() в бизнес-операциях;
  2. внедрить единый Clock/ZoneId Europe/Moscow без изменения абсолютных timestamp;
  3. зафиксировать TZ/user.timezone в Compose и Kubernetes;
  4. проверить границы московской полуночи и date-only форматирование без UTC-сдвига.