Files
LMS/docs/17-handoff.md
Vladislav Kopnin 1e1e9ae0e4 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 обновлены
2026-10-29 12:01:44 +03:00

301 lines
18 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.

# 17. Handoff: состояние проекта после закрытия продуктовых рисков
**Дата:** 2026-10-28.
**Ветка:** `feature/groups-courses` (последняя история — `git log --oneline`).
**Последний коммит перед этой итерацией:** `3aae2b8` (`docs: handoff после закрытия продуктовых рисков`).
**Пуш:** не выполнялся.
**Рабочее дерево:** чистое. `exports/` и `texput.log` игнорируются и в коммиты не входят.
Этот файл — точка восстановления контекста. Если после сжатия контекста
непонятно, что уже сделано и с чего продолжать, читать сначала его, затем
[16-stage6-report.md](16-stage6-report.md) и
[evidence/README.md](evidence/README.md).
---
## 1. Что уже сделано
### 1.1. Доменная модель и роли
- Роли `ADMIN`, `TEACHER`, `STUDENT`.
- Группы-когорты: `stream`, `course`, `number`, `degree`, `enrollment_year`,
`status`; история членства `membership_history`.
- Импорт групп Excel v2, перевод/академ/отчисление/выпуск/архив/восстановление.
- Удаление студента с усиленным подтверждением (`login` или ФИО).
- Вход закрыт для отчисленных, выпущенных, архивных студентов и архивных
аккаунтов; архивные/выпущенные группы также блокируют вход.
- Ведущие нули номера группы **не являются требованием**: номер числовой,
отдельной поддержки `01` нет. Не добавлять `number_label` без нового ТЗ.
### 1.2. Курсы и лабораторные
- Курс C «Промышленное программирование на языке C» создаётся bootstrap-ом;
все лабораторные и главы переносятся в курс.
- `labs.course_id` становится `NOT NULL` в runtime после bootstrap
(`CourseBootstrapService`), а не отдельной Flyway-миграцией.
- У курса есть человекочитаемый `course_no`; материалы открываются по
`/learn/course/{course_no}/...`.
- Bootstrap идемпотентен, сериализуется advisory lock, проверяет дубли
номеров лабораторных.
- Копирование лабораторной в другой курс; перенос между курсами запрещён.
- Копия не переносит прогресс, оценки, сдачи, защиты и назначения.
### 1.3. Права преподавателя
- Преподаватель своего курса может:
- вести лабораторные/уровни/варианты;
- работать с ведомостью: оценки, комментарии, публикация, уровни, варианты,
распределение, защиты;
- привязывать/отвязывать группы курса;
- авторствовать материалы курса (главы/разделы/статьи, публикация).
- Все проверки скоупа выполняются на сервисах; чужие курс/лаба/студент/группа
дают `403` или `404`.
- Глобальные материалы и админские функции остаются только у `ADMIN`.
### 1.4. Безопасность (закрытые продуктовые риски)
**Legacy-активы**
- Файл без записи в `article_assets` больше не публичен:
аноним/студент/преподаватель → `404`; админ → доступ.
- Инвентаризация: `GET /api/v1/content/legacy-assets` (ADMIN), страница
`/admin/legacy-assets` и `scripts/legacy-assets.sh` для работы со стендом.
- Старые прямые ссылки на бесхозные файлы у обычных пользователей ломаются
осознанно.
**JWT и сессия**
- 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 минуты до истечения.
- 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` и `V12__refresh_absolute.sql`.
### 1.5. Аудит
- `identity.audit_events` (V10), внутренний API `POST /api/v1/internal/audit`,
админский `GET /api/v1/audit`.
- Пишутся доступы/изменения курсов, групп, материалов, лабораторных, оценки,
публикации, защиты, профиль (staff), скачивания, lifecycle, promotion.
- `/admin/audit`: фильтр по действию, keyset-курсор `(occurred_at, id)`,
«Показать ещё» и «В начало»; offset `page` остался для совместимости.
- `V13` добавляет индексы `(occurred_at DESC, id DESC)` и
`(action, occurred_at DESC, id DESC)`.
### 1.6. Материалы и активы
- Курсовые материалы `/learn/course/{course_no}/...`; публичные глобальные
материалы `audience=ALL`; `STUDENTS`/`TEACHERS` требуют роль.
- Видимость `article_assets` наследуется от статьи/раздела/главы.
- `V12__asset_links_backfill.sql` восстанавливает связи из markdown-ссылок.
- Legacy-файлы без ссылок теперь закрыты (см. §1.4).
### 1.7. Повышение и жизненный цикл
- Promotion: dry-run, apply, idempotency, rollback, cancel, scheduler
31 августа МСК и catch-up, advisory locks, conflict detection.
- Student lifecycle: academic leave/return/expel/archive/restore.
- Concurrent apply/rollback покрыты интеграционными тестами.
- Apply/rollback на сгенерированных когортах покрыты model-based тестом.
### 1.8. LDD и проверки
- Реестр: `docs/evidence/claims.json`.
- **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: 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: preflight + полный стек, health, вход админа, один курс;
- `release-evidence` обязателен для тегов, вручную для веток: тесты,
миграции, mutation, runtime/load, browser E2E, финальный evidence.
- `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–V13` | роли, группы, статусы, promotion, история, аудит (`V10`), refresh (`V11`), absolute (`V12`), keyset-индексы (`V13`) |
| academic | `V6–V12` | курсы, `labs.course_id`, главы/аудитории, индексы, `article_assets`, backfill |
| progress | `V13` | автор комментария/оценки; V13 идемпотентна |
- **Progress V10 отсутствует.** Не создавать и не включать `out-of-order`.
- Если dev/staging когда-то применял старую feature-миграцию progress V10,
удалить только строку истории через `flyway repair`/SQL из
`deploy/preflight/progress-v10-v13.sql`, объекты не удалять.
- Production/master: progress V11/V12 уже есть, затем V13.
- `labs.course_id NOT NULL` — post-bootstrap, не Flyway.
### 1.10. Деплой и backup
- `deploy/backup.sh`: БД + submissions + assets, checksums, retention,
verify/restore; `deploy.sh up` требует backup.
- Ежедневный systemd timer.
- Production `.env.prod`: обязательны `LMS_DB_PASSWORD`, `LMS_INTERNAL_TOKEN`;
сроки сессии можно переопределить.
- `docker-compose.prod.yml` прокидывает:
`LMS_ACCESS_TOKEN_TTL`, `LMS_REFRESH_TOKEN_TTL`, `LMS_SESSION_TIMEOUT`,
`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/**`.
---
## 2. Проверки: что и как запускать
```bash
# 1. Полный локальный gate
./gradlew test ktlintCheck detekt evidenceCheck
# 2. Полный LDD release (долго, нужны Docker/Java/Node + Chromium/Firefox/WebKit)
./scripts/ldd-release.sh
# 3. Быстрый smoke (то же, что CI smoke)
./scripts/ldd-smoke.sh
# 4. Отдельные проверки
./scripts/ldd-migration-upgrade.sh
./scripts/ldd-mutation.sh
./scripts/ldd-runtime.sh # с LDD_SCALE=1 — 100 групп/1000 студентов
./scripts/ldd-browser.sh
# 5. Отчёт
cat build/reports/evidence/index.md
```
Последний подтверждённый результат:
- `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`.
---
## 3. Что осталось внести / закрыть
Это уже не P0-риски, а эксплуатационный backlog. Порядок recommended.
1. **Инвентаризация legacy-активов на реальном хранилище.**
Страница `/admin/legacy-assets`, API и `scripts/legacy-assets.sh` готовы.
Нужно на production/staging получить список бесхозных файлов, привязать их
к статьям или удалить. Старые внешние ссылки на такие файлы уже не работают
у обычных пользователей.
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. **Визуальная регрессия и мобильный набор E2E.**
Сейчас 6 сценариев × 3 браузера, без screenshot-diff. Добавлять по мере
роста UI.
4. **Пакетные сдачи в ведомости.**
Детали лабораторных курса уже пакетные, но ведомость запрашивает сдачи по
выбранной лабораторной отдельно. Оптимизировать, если появятся медленные
курсы.
«Контингент»/`next.uniyar.ac.ru` **исключён из плана** решением от 2026-10-28:
нет API-спецификации и доступа к внешней системе. При появлении нового ТЗ
вернуться к отдельному anti-corruption слою.
---
## 4. Важные gotchas
- Не возвращать публичный доступ к legacy-активам.
- Не добавлять `number_label`/строковый номер группы: по требованиям номер
числовой, ведущего нуля быть не может.
- Не делать 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`,
`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.
- Перед production deploy — backup; без него deploy.sh не продолжит.
- Внутренние пути `/api/v1/internal/**` не должны появляться в gateway.
- `LMS_INTERNAL_TOKEN` должен быть одинаковым у identity/academic/progress.
---
## 5. Если контекст потерян: порядок восстановления
1. `git status --short --branch` — ожидается чистое дерево на
`feature/groups-courses`.
2. Прочитать этот файл, затем `docs/16-stage6-report.md` §7 и
`docs/evidence/baseline-2026-10-27.md`.
3. `./gradlew test ktlintCheck detekt evidenceCheck` — должно быть 346 тестов
и 51/51 claims.
4. Для release-проверки: `./scripts/ldd-release.sh`.
5. Для быстрой проверки: `./scripts/ldd-smoke.sh` и `./scripts/ci-preflight.sh`.
6. Продолжать backlog из §3, начиная с инвентаризации legacy-активов и
первого реального CI-прогона.
## 6. Карта ключевых файлов
- `docs/07-groups-courses.md` — ТЗ.
- `docs/08-groups-courses-plan.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-токены, 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`, `SessionLifetime.kt`,
`SessionCookieRefreshFilter.kt`.
- `web/.../admin/AdminAuditController.kt`, `LegacyAssetsController.kt`.
- `services/gateway/.../JwtAuthenticationFilter.kt`.
- `scripts/ldd-*.sh`, `scripts/ci-preflight.sh`, `scripts/legacy-assets.sh`.
- `deploy/backup.sh`, `deploy/deploy.sh`, `deploy/DEPLOY.md`.