435 lines
51 KiB
Markdown
435 lines
51 KiB
Markdown
# Журнал исправления проблем
|
||
|
||
Обновлено: 2026-07-17 (Europe/Moscow).
|
||
|
||
## Область и неизменяемые данные
|
||
|
||
- В работе ровно 33 проблемы: `1` и `3–34`.
|
||
- Проблема № 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. Высокоприоритетные инварианты расписания: № 5–8.
|
||
4. Tenant lifecycle и Kubernetes coordination: № 9, 10, 13, 14.
|
||
5. Frontend-сессия и достоверность UI: № 11, 12, 33, 34.
|
||
6. Календарные и DB-инварианты: № 15–17, 21, 31, 32.
|
||
7. История, права, масштаб и производительность: № 18–23.
|
||
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-хеши V1–V4 совпадают. 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 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 | не начато | Не проверяются `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:00–03: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 и повторяется с экспоненциальной
|
||
задержкой 30–300 секунд; новая ревизия обходит 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.
|