Files
magistr/BUG_FIX_PROGRESS.md
2026-07-17 01:27:02 +03:00

435 lines
51 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Журнал исправления проблем
Обновлено: 2026-07-17 (Europe/Moscow).
## Область и неизменяемые данные
- В работе ровно 33 проблемы: `1` и `334`.
- Проблема № 2 исключена: демонстрационные учётные записи, seed-данные и документация по ним не изменяются.
- `BUG_REPORT.md` не редактируется.
- Существующие Flyway-миграции не редактируются.
Исходное рабочее дерево перед началом реализации было чистым (`git status --short` без вывода).
Контрольные суммы исходных миграций:
| Файл | SHA-256 |
|---|---|
| `V1__init.sql` | `404f65f270dd87f4db996495fc970456c4fd0f762e7c32638199dffaa812ea7b` |
| `V2__subgroups_active_unique_name.sql` | `68f5525a50ddba4f8800a63cab3cd0f84779e301c4a1f29a6e0399f97fbdb30b` |
## Исходная линия проверок
| Проверка | Результат до изменений |
|---|---|
| `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; V3 закрепляет структурные инварианты. | 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`, `Очно`/`Онлайн` и чётные часы; строка семестра сериализует запись. V4 закрепляет чётность и точный дубль в БД. | 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 | не начато | Access JWT хранится в Web Storage; origin исполняет удалённые модули; CSP допускает несовместимые сценарии. | Access JWT только в памяти, восстановление через HttpOnly refresh-cookie; локальные pinned assets; строгий CSP и удаление inline JS. | Login/refresh/logout/reload tests; статический аудит CSP/storage/imports. |
| 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 | не начато | Не проверяются `start < end` и пересечения; duration доверяется клиенту; используемый DEFAULT scope можно изменить. | Централизованный валидатор, backend duration, DB/concurrency protection. | Overlap/adjacent/scope/concurrency tests. |
| 16 | не начато | Create/update проверяют лишь порядок дат, но не пересечения и принадлежность семестра году. | Единый calendar validator и PostgreSQL range/exclusion constraints. | Create/update + конкурентные PostgreSQL tests. |
| 17 | не начато | Изменение измерений группы/календаря оставляет несовместимые assignments/grid/subjects. | Отклонять несовместимое изменение транзакционно с `409`; контролировать `courseCount`. | Assignment сохранён после отказа; старшие строки не остаются. |
| 18 | не начато | Бизнес-решения смешивают `users.department_id` с датированными assignments и фильтруют архивные записи по текущему состоянию. | Date-aware resolver кафедры и единое использование истории. | Будущий перевод, прошлый период, архивный преподаватель, совместительство. |
| 19 | не начато | Workload принимает произвольный `departmentId` для роли `DEPARTMENT`. | Принудительный scope из `AuthContext`; глобальный доступ только разрешённым ролям. | Межкафедральный запрос кафедры не раскрывает данные. |
| 20 | не начато | Агрегаты переиспользуют интерактивный широкий поиск с лимитом 50 групп. | Специализированная batch/aggregate генерация без UI-лимита. | 51 и 100 активных групп. |
| 21 | не начато | Импорт ищет дисциплину по глобальному имени и меняет её владельца. | `409` для чужой записи, идемпотентность своей; scoped uniqueness при миграции. | Повторный свой импорт и совпадение чужой кафедры. |
| 22 | не начато | Видимые EDUCATION_OFFICE экраны вызывают backend endpoints с ADMIN-only правами. | Единая role matrix для backend/frontend; согласовать read/write capabilities. | Интеграционный сценарий календаря и форм обучения без штатного `403`. |
| 23 | не начато | Генератор повторно запрашивает semester/assignment/calendar day на каждой комбинации. | Батч-загрузка и request-scoped lookup maps. | Query-count test на 120 датах и десятках групп. |
| 24 | не начато | Истёкшие/отозванные refresh rows не удаляются. | Tenant-aware scheduled cleanup с retention и безопасностью двух pod. | Active/fresh rows сохраняются; cleanup идемпотентен и конкурентно безопасен. |
| 25 | не начато | Нет общего mapping DB constraints; контроллеры возвращают клиенту `getMessage()`. | Централизованный sanitizer/mapping `400`/`409`, убрать raw exception body. | Известный/неизвестный constraint без JDBC details. |
| 26 | не начато | Compose требует внешнюю сеть, не публикует HTTP, рассогласует DB/JWT и не имеет named volume. | Самодостаточная сеть/reverse proxy, согласованные env и volume. | Чистый `docker compose up -d --build`, HTTP `/` и `/api`. |
| 27 | не начато | Pipeline собирает/деплоит без обязательных тестов и для tag перезапускает mutable `main`. | Test gates, concurrency, immutable tag/digest rollout, rollback check. | Workflow lint/static inspection и shell tests. |
| 28 | не начато | Login не имеет shared rate limit; IP безусловно доверяет forwarded header. | PostgreSQL-backed tenant+trusted-IP+username limiter, lockout/audit, `429`/`Retry-After`. | Multi-instance integration tests, отсутствие user enumeration. |
| 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`/экранирование; `type=password` и autocomplete. | DOM/XSS static/unit test и HTML assertions. |
| 34 | не начато | В затронутых UI/log paths остаются английские метки/логи и raw exception text. | Русификация и безопасные сообщения на границе API/UI. | Поиск английских production сообщений и regression tests. |
## Завершённые этапы
### № 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 и удалённый JavaScript в одном origin**.
Следующая операция:
1. инвентаризировать `localStorage`/`sessionStorage`, refresh-cookie, удалённые `import()` и
inline-скрипты во всех frontend entrypoints и HTTP/CSP-конфигурации;
2. выбрать единый контракт access JWT только в памяти с восстановлением через HttpOnly
refresh-cookie, не оставляя токен в Web Storage после login/refresh/logout/reload;
3. локализовать и зафиксировать внешние runtime-зависимости, затем ввести CSP без
`unsafe-inline`/`unsafe-eval` и устранить несовместимые inline-вставки;
4. покрыть login/refresh/logout/reload тестами и выполнить статический аудит storage/imports/CSP
перед AutoUpdateDocs и изменением статуса № 12.