120 KiB
Журнал исправления проблем
Обновлено: 2026-07-19 (Europe/Moscow).
Область и неизменяемые данные
- В работе ровно 33 проблемы:
1и3–34. - Проблема № 2 исключена: демонстрационные учётные записи, seed-данные и документация по ним не изменяются.
BUG_REPORT.mdне редактируется.- По прямому разрешению владельца проекта миграции V2–V7 удалены, а их итоговая схема
объединена в
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.
Упоминания V2–V7 в исторических записях ниже описывают последовательность разработки до
консолидации и не означают наличие этих файлов в текущем дереве.
Исходная линия проверок
| Проверка | Результат до изменений |
|---|---|
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 |
Успешно. |
Порядок выполнения
- Безопасная production-конфигурация и внешние секреты: № 1 (с зависимой частью № 14).
- Аутентификация и границы запроса: № 3, 4, 24, 25, 28.
- Высокоприоритетные инварианты расписания: № 5–8.
- Tenant lifecycle и Kubernetes coordination: № 9, 10, 13, 14.
- Frontend-сессия и достоверность UI: № 11, 12, 33, 34.
- Календарные и DB-инварианты: № 15–17, 21, 31, 32.
- История, права, масштаб и производительность: № 18–23.
- Локальный запуск, CI/CD, воспроизводимые артефакты и timezone: № 26, 27, 29, 30.
- 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 30–300 секунд и 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 на невалидном legacyREPLACEи 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и ISOdayOfWeekс датой, наличие 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, Roleget/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 и повторяется с экспоненциальной задержкой 30–300 секунд; новая ревизия обходит backoff. Required missing/read/empty также наблюдаемы через readiness и русский лог без credentials, hash или fingerprint. - Перед
POST/DELETEwatcher под общим reentrant monitor применяет ещё не обработанный semantic baseline. Ошибка чтения/разбора/sync возвращает безопасный503до начала мутации.TenantLifecycleMutationResultвозвращает controller-у immutable-квитанцию персистенции, но не раскрывает её в HTTP. - Snapshot fence ставится только для реально сохранённой изменённой версии
(
persisted && changed) и сравнивает нормализованныеprevious/committedsnapshots, а не сырые 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иHttpOnlycookie. 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-логи загрузки оборудования, форм обучения и подгрупп также русифицированы; rawerror.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 V1–V4 не изменялись; 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 | По прямому разрешению владельца V2–V7 удалены, их итоговая схема включена в V1. |
№ 16 — инварианты учебных годов и семестров
- CRUD годов и семестров перенесён из
AcademicCalendarAdminControllerв публичный транзакционныйAcademicPeriodService. Названия годов нормализуются до duplicate-check, даты обязательны и упорядочены, а календарные диапазоны трактуются как включительные. - Учебные годы не могут пересекаться. Семестр должен полностью находиться внутри своего
года, не пересекаться с другим семестром этого года и иметь уникальный тип. Update года
отклоняет границы, которые исключат любой сохранённый семестр; update повторно проверяет
title/type и возвращает безопасный русский
409, а не JDBC500. - 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-levelADMINдля всех 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}и через Apachemod_proxyнаправляет same-origin/apiи/actuator/healthво внутренний backend. ИсходныйHostсохраняется, поэтому локальный запрос выбирает tenantdefault; 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получил обязательныйchecksjob: полный backend non-integration suite, frontendnpm 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-productionconcurrency 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 использует UTCClock, а календарную дату вычисляет в настраиваемой зонеBUSINESS_TIME_ZONEс безопасным defaultEurope/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содержит явные значения. Kubernetesapp-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 | Полный прогон после № 31–32: 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 Contrib0.153.0@sha256:93aad750175cbf1a973ae1c5886c3371f4d800f61be25cdd26870b8441ffe9fa. Версия подтверждена официальным релизом OpenTelemetry, manifest digest получен напрямую из Docker Hub Registry API. - Новых Flyway-файлов не создано; в каталоге миграций остаётся только
V1__init.sqlс SHA-256fffedf66e8bf7d25d2d8c1b1a6146b45dd5b7e52233d05a68de8bf62c817f7a7.
Фактические проверки этапа:
| Команда | Результат |
|---|---|
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, а V2–V4 отсутствуют. - 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и3–34) имеют реализованную репозиторную часть и воспроизводимое подтверждение. № 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-манифестах отсутствуют mutablelatestи:main.- Каталог Flyway содержит ровно
V1__init.sql; V2–V4 и другие дополнительные миграции отсутствуют. Финальный SHA-256 V1:fffedf66e8bf7d25d2d8c1b1a6146b45dd5b7e52233d05a68de8bf62c817f7a7.
Точка продолжения
Текущий этап: реализация и проверка всех разрешённых изменений завершены.
Оставшееся операторское действие владельца:
- при готовности production-полномочий выполнить
docs/SECURITY_RUNBOOK.md: ротацию, provisioning, rollout и очистку истории для № 1. Эти действия не выполнялись автоматически и не требуются для завершения репозиторной реализации.