feat: закрыть остаточный backlog

- keyset-пагинация аудита с курсором (occurred_at, id) и UI «Показать ещё»
- пакетный /api/v1/labs/details и переход progress на один запрос
- абсолютный потолок сессии 30 дней (identity V12 + web), очистка refresh
- админская инвентаризация legacy-активов (/admin/legacy-assets, скрипт)
- E2E в Chromium/Firefox/WebKit (18/18), CI preflight
- LDD: 51 claim, 346 тестов, docs и handoff обновлены
This commit is contained in:
2026-10-28 19:29:23 +03:00
parent d45137220c
commit 1e1e9ae0e4
64 changed files with 2124 additions and 372 deletions

View File

@@ -1,8 +1,8 @@
# 17. Handoff: состояние проекта после закрытия продуктовых рисков
**Дата:** 2026-10-28.
**Ветка:** `feature/groups-courses` (54 коммита от master до этого документа).
**HEAD на момент написания:** `1ca507b` (`docs: закрытие продуктовых рисков и новый LDD baseline`).
**Ветка:** `feature/groups-courses` (последняя история — `git log --oneline`).
**Последний коммит перед этой итерацией:** `3aae2b8` (`docs: handoff после закрытия продуктовых рисков`).
**Пуш:** не выполнялся.
**Рабочее дерево:** чистое. `exports/` и `texput.log` игнорируются и в коммиты не входят.
@@ -57,8 +57,8 @@
**Legacy-активы**
- Файл без записи в `article_assets` больше не публичен:
аноним/студент/преподаватель → `404`; админ → доступ.
- Инвентаризация: `GET /api/v1/content/legacy-assets` (ADMIN) возвращает
список файлов без привязки.
- Инвентаризация: `GET /api/v1/content/legacy-assets` (ADMIN), страница
`/admin/legacy-assets` и `scripts/legacy-assets.sh` для работы со стендом.
- Старые прямые ссылки на бесхозные файлы у обычных пользователей ломаются
осознанно.
@@ -66,17 +66,21 @@
- Access JWT: **15 минут** (`LMS_ACCESS_TOKEN_TTL`).
- Refresh-токен: **7 дней** (`LMS_REFRESH_TOKEN_TTL`), хранится как SHA-256
hash, ротируется при обновлении.
- Абсолютный потолок сессии: **30 дней** (`LMS_SESSION_ABSOLUTE_TIMEOUT`);
ротация refresh не продлевает `absolute_expires_at` (миграция `V12`).
- Web-сессия: **7 дней sliding** (`LMS_SESSION_TIMEOUT`), cookie `LMS_SESSION`;
каждый ответ перевыпускает cookie с новым `Max-Age`; access обновляется
автоматически за 2 минуты до истечения.
каждый ответ перевыпускает cookie с новым `Max-Age`, но не дольше остатка
абсолютного срока; access обновляется автоматически за 2 минуты до истечения.
- Logout и смена пароля отзывают refresh; отчисленный/архивный студент,
архивный аккаунт и закрытая группа не могут обновить сессию.
- Повторное использование старого refresh отзывает все активные сессии
аккаунта.
- Истёкшие refresh-токены старше 30 дней удаляет ночная задача
(`LMS_REFRESH_TOKEN_RETENTION`, `LMS_REFRESH_TOKEN_CLEANUP_CRON`).
- Gateway публикует без access-JWT: `/auth/login`, `/auth/setup`,
`/auth/refresh`, `/auth/logout`, `/auth/health`, `/auth/jwks.json`,
`/content/**`, `/actuator`.
- Миграция `identity V11__refresh_tokens.sql`.
- Миграции `identity V11__refresh_tokens.sql` и `V12__refresh_absolute.sql`.
### 1.5. Аудит
@@ -84,8 +88,10 @@
админский `GET /api/v1/audit`.
- Пишутся доступы/изменения курсов, групп, материалов, лабораторных, оценки,
публикации, защиты, профиль (staff), скачивания, lifecycle, promotion.
- `/admin/audit`: фильтр по действию, страницы `page`/`limit` и «Показать ещё».
- Пагинация пока offset, не keyset.
- `/admin/audit`: фильтр по действию, keyset-курсор `(occurred_at, id)`,
«Показать ещё» и «В начало»; offset `page` остался для совместимости.
- `V13` добавляет индексы `(occurred_at DESC, id DESC)` и
`(action, occurred_at DESC, id DESC)`.
### 1.6. Материалы и активы
@@ -106,26 +112,30 @@
### 1.8. LDD и проверки
- Реестр: `docs/evidence/claims.json`.
- **46 claims**: 25 P0 + 21 P1; **46/46 proven, P0 gaps = 0**.
- **319 автотестов**, 0 failures/errors/skipped.
- **51 claim**: 25 P0 + 26 P1; **51/51 proven, P0 gaps = 0**.
- **346 автотестов**, 0 failures/errors/skipped.
- Mutation testing:
- `CourseAccessService` — 75% test strength;
- `PromotionPlanner` — 69%.
- Browser E2E Playwright: 4/4 (admin, teacher, student/anonymous, сдача + mobile).
- Browser E2E Playwright: 6 сценариев × Chromium/Firefox/WebKit = **18/18**
(admin, teacher, student/anonymous, сдача + mobile, cursor-аудит,
инвентаризация активов).
- Runtime/load: 100 групп / 1000 студентов, 125 параллельных чтений,
0 ошибок, p95 < 1 c.
- CI:
- `check` на push/MR: тесты, ktlint, detekt, `evidenceCheck`;
- `smoke` на push/MR: полный стек, health, вход админа, один курс;
- `smoke` на push/MR: preflight + полный стек, health, вход админа, один курс;
- `release-evidence` обязателен для тегов, вручную для веток: тесты,
миграции, mutation, runtime/load, browser E2E, финальный evidence.
- Полный `./scripts/ldd-release.sh` локально прошёл.
- `scripts/ci-preflight.sh` проверяет Java 21, Docker Compose v2, Python 3,
Node и Playwright; первый реальный GitLab pipeline ещё не подтверждён.
- Полный `./scripts/ldd-release.sh` локально прошёл; browser E2E — 18/18.
### 1.9. Миграции
| Сервис | Диапазон | Что важно |
|---|---|---|
| identity | `V4–V11` | роли, группы, статусы, promotion, история, аудит (`V10`), refresh (`V11`) |
| identity | `V4–V13` | роли, группы, статусы, promotion, история, аудит (`V10`), refresh (`V11`), absolute (`V12`), keyset-индексы (`V13`) |
| academic | `V6–V12` | курсы, `labs.course_id`, главы/аудитории, индексы, `article_assets`, backfill |
| progress | `V13` | автор комментария/оценки; V13 идемпотентна |
@@ -145,7 +155,8 @@
сроки сессии можно переопределить.
- `docker-compose.prod.yml` прокидывает:
`LMS_ACCESS_TOKEN_TTL`, `LMS_REFRESH_TOKEN_TTL`, `LMS_SESSION_TIMEOUT`,
`LMS_INTERNAL_TOKEN`.
`LMS_SESSION_ABSOLUTE_TIMEOUT`, `LMS_REFRESH_TOKEN_RETENTION`,
`LMS_REFRESH_TOKEN_CLEANUP_CRON`, `LMS_INTERNAL_TOKEN`.
- Перед деплоем: backup, preflight, затем deploy, bootstrap курса C (если
курсов нет), postflight, smoke.
- Gateway не маршрутизирует `/api/v1/internal/**`.
@@ -158,7 +169,7 @@
# 1. Полный локальный gate
./gradlew test ktlintCheck detekt evidenceCheck
# 2. Полный LDD release (долго, нужны Docker/Java/Node/Chromium)
# 2. Полный LDD release (долго, нужны Docker/Java/Node + Chromium/Firefox/WebKit)
./scripts/ldd-release.sh
# 3. Быстрый smoke (то же, что CI smoke)
@@ -175,10 +186,11 @@ cat build/reports/evidence/index.md
```
Последний подтверждённый результат:
- `319` тестов, 0 failures;
- `46/46` claims;
- `346` тестов, 0 failures;
- `51/51` claims;
- `ldd-release.sh` — PASS;
- smoke — PASS;
- browser E2E — 18/18 (Chromium/Firefox/WebKit);
- `git diff --check` — чисто.
Артефакты: `build/evidence/{runtime,migration,mutation,browser,smoke}.json`.
@@ -187,51 +199,33 @@ cat build/reports/evidence/index.md
## 3. Что осталось внести / закрыть
Это уже не текущие P0-риски, а следующий backlog. Порядок recommended.
Это уже не P0-риски, а эксплуатационный backlog. Порядок recommended.
1. **Инвентаризация legacy-активов на реальном хранилище.**
Endpoint `/api/v1/content/legacy-assets` готов. Нужно на production/staging
получить список бесхозных файлов, привязать их к статьям или удалить;
после этого можно решить, удалять ли админский fallback.
Старые внешние ссылки на такие файлы уже не работают у обычных пользователей.
Страница `/admin/legacy-assets`, API и `scripts/legacy-assets.sh` готовы.
Нужно на production/staging получить список бесхозных файлов, привязать их
к статьям или удалить. Старые внешние ссылки на такие файлы уже не работают
у обычных пользователей.
2. **Keyset-пагинация аудита.**
Сейчас `page`/`limit` offset. Для очень больших журналов возможны сдвиги
при вставках между страницами. Нужен курсор `(occurred_at, id)`.
2. **Первый реальный CI-прогон.**
Конфиг, smoke и `scripts/ci-preflight.sh` готовы, но нужно убедиться, что в
GitLab есть shell-runners с тегами `lms-shell` (smoke) и `lms-release`
(полный release), Java 21, Docker Compose v2, Python 3, Node и браузерами
Chromium/Firefox/WebKit. `check` использует privileged `docker:dind`.
Дождаться зелёного push-MR pipeline и первого tag-job `release-evidence`.
3. **Оставшиеся N+1.**
- `AcademicClient.getCourseLabs` подтягивает детали лабораторных по одной;
- часть данных ведомости/профиля на больших курсах собирается по частям.
Имена групп курса уже bulk; аудит paginated.
Нужны пакетные эндпоинты или join-выборки, затем повторный load-тест.
3. **Визуальная регрессия и мобильный набор E2E.**
Сейчас 6 сценариев × 3 браузера, без screenshot-diff. Добавлять по мере
роста UI.
4. **Абсолютный лимит web-сессии.**
Сессия sliding 7 дней и отзывается, но при непрерывной активности может
жить бессрочно. Если политика требует повторный вход раз в N дней —
добавить абсолютный `max-age`/`absolute timeout`.
4. **Пакетные сдачи в ведомости.**
Детали лабораторных курса уже пакетные, но ведомость запрашивает сдачи по
выбранной лабораторной отдельно. Оптимизировать, если появятся медленные
курсы.
5. **Очистка refresh_tokens.**
Истёкшие записи сейчас не удаляются. Добавить scheduled cleanup
(например, раз в сутки) и метрику/аудит.
6. **CI на реальном runner.**
Конфиг готов, но нужно убедиться, что в GitLab есть shell-runners с тегами
`lms-shell` (smoke) и `lms-release` (полный release), Java 21,
Docker Compose v2, Python 3, Node/Chromium. `check` использует
privileged `docker:dind` для Testcontainers.
Проверить первый pipeline на push и первый tag-job.
7. **Browser E2E: расширение.**
Сейчас 4 сценария, один Chromium. Нет Safari/Firefox, визуальной регрессии
и полного мобильного набора. Добавлять по мере роста UI.
8. **Внешняя интеграция «Контингент»/next.uniyar.ac.ru.**
Отдельный anti-corruption слой, идемпотентная синхронизация, журнал,
отчёт дрифта, внешние идентификаторы. В текущий объём не входила.
9. **Release-evidence job на теге.**
В `.gitlab-ci.yml` он обязателен для тегов и ручной для веток, но реальный
прогон в CI ещё не подтверждён — см. п. 6.
«Контингент»/`next.uniyar.ac.ru` **исключён из плана** решением от 2026-10-28:
нет API-спецификации и доступа к внешней системе. При появлении нового ТЗ
вернуться к отдельному anti-corruption слою.
---
@@ -240,15 +234,25 @@ cat build/reports/evidence/index.md
- Не возвращать публичный доступ к legacy-активам.
- Не добавлять `number_label`/строковый номер группы: по требованиям номер
числовой, ведущего нуля быть не может.
- Не делать access JWT снова 30-дневным. 30 дней — только refresh/web session.
- Не делать access JWT снова 30-дневным. 30 дней — только абсолютный потолок
сессии.
- Gateway обязан пропускать `/api/v1/auth/refresh` и `/api/v1/auth/logout`
без access-JWT; иначе автообновление и logout ломаются.
- `LMS_ACCESS_TOKEN_TTL`, `LMS_REFRESH_TOKEN_TTL`, `LMS_SESSION_TIMEOUT`
должны попадать в environment соответствующих контейнеров; сейчас
`docker-compose.prod.yml` это делает.
- Session cookie `LMS_SESSION` перевыпускается фильтром на каждом ответе;
не убирать его, иначе 7-дневная sliding-сессия станет абсолютной.
- `LMS_ACCESS_TOKEN_TTL`, `LMS_REFRESH_TOKEN_TTL`, `LMS_SESSION_TIMEOUT`,
`LMS_SESSION_ABSOLUTE_TIMEOUT`, `LMS_REFRESH_TOKEN_RETENTION` и
`LMS_REFRESH_TOKEN_CLEANUP_CRON` должны попадать в environment
соответствующих контейнеров; сейчас `docker-compose.prod.yml` это делает.
- Session cookie `LMS_SESSION` перевыпускается фильтром на каждом ответе, но
`Max-Age` ограничен остатком абсолютного срока; не убирать ни фильтр, ни
проверку `AUTHENTICATED_AT`, иначе 30-дневный потолок перестанет работать.
- Keyset-аудит использует `(occurred_at, id)`; не менять сортировку на одну
`id` или `occurred_at` без пересчёта курсора. Offset `page` — только
совместимость.
- `article_assets` без связи → 404 для всех, кроме админа. Это намеренно.
- `/api/v1/labs/details` — пакетный контракт progress; не заменять его обратно
на цикл `/labs/{id}` без load-теста.
- WebKit на Fedora требует Docker-образа Playwright; `ldd-browser.sh` сам
переключается, не ломать этот fallback.
- Не коммитить `exports/`, `texput.log`, `data/`, `backups/`, `.env.prod`.
- Не включать Flyway `out-of-order`; progress V10 не существует.
- `labs.course_id NOT NULL` применяется bootstrap-сервисом в runtime.
@@ -264,12 +268,12 @@ cat build/reports/evidence/index.md
`feature/groups-courses`.
2. Прочитать этот файл, затем `docs/16-stage6-report.md` §7 и
`docs/evidence/baseline-2026-10-27.md`.
3. `./gradlew test ktlintCheck detekt evidenceCheck` — должно быть 319 тестов
и 46/46 claims.
3. `./gradlew test ktlintCheck detekt evidenceCheck` — должно быть 346 тестов
и 51/51 claims.
4. Для release-проверки: `./scripts/ldd-release.sh`.
5. Для быстрой проверки: `./scripts/ldd-smoke.sh`.
6. Продолжать backlog из §3, начиная с инвентаризации legacy-активов
и keyset-пагинации.
5. Для быстрой проверки: `./scripts/ldd-smoke.sh` и `./scripts/ci-preflight.sh`.
6. Продолжать backlog из §3, начиная с инвентаризации legacy-активов и
первого реального CI-прогона.
## 6. Карта ключевых файлов
@@ -278,14 +282,19 @@ cat build/reports/evidence/index.md
- `docs/16-stage6-report.md` — итоговый отчёт, deploy и ограничения.
- `docs/evidence/claims.json` — LDD-реестр.
- `docs/evidence/baseline-2026-10-27.md` — фактический baseline.
- `services/identity-service/.../auth/refresh/` — refresh-токены.
- `services/identity-service/.../db/migration/V11__refresh_tokens.sql`.
- `services/identity-service/.../audit/` — аудит.
- `services/identity-service/.../auth/refresh/` — refresh-токены, absolute,
cleanup.
- `services/identity-service/.../db/migration/V11__refresh_tokens.sql`,
`V12__refresh_absolute.sql`, `V13__audit_keyset_indexes.sql`.
- `services/identity-service/.../audit/` — аудит и keyset-курсор.
- `services/academic-service/.../content/` — материалы/активы.
- `services/academic-service/.../course/CourseGroupService.kt` — bulk-группы.
- `services/academic-service/.../lab/LabService.kt` — `details(courseId)`.
- `services/progress-service/.../AcademicClient.kt` — пакетный вызов лаб.
- `services/progress-service/.../CourseScopeService.kt` — course-scope.
- `web/.../session/SessionTokenService.kt`, `SessionCookieRefreshFilter.kt`.
- `web/.../admin/AdminAuditController.kt`.
- `web/.../session/SessionTokenService.kt`, `SessionLifetime.kt`,
`SessionCookieRefreshFilter.kt`.
- `web/.../admin/AdminAuditController.kt`, `LegacyAssetsController.kt`.
- `services/gateway/.../JwtAuthenticationFilter.kt`.
- `scripts/ldd-*.sh`.
- `scripts/ldd-*.sh`, `scripts/ci-preflight.sh`, `scripts/legacy-assets.sh`.
- `deploy/backup.sh`, `deploy/deploy.sh`, `deploy/DEPLOY.md`.