- preflight занятого логина и неактивной целевой группы - applied=false с номером строки, без частичных студентов - LDD-IMPORT-003; baseline 58 claims / 368 тестов
312 lines
19 KiB
Markdown
312 lines
19 KiB
Markdown
# 17. Handoff: состояние проекта после закрытия продуктовых рисков
|
||
|
||
**Дата:** 2026-10-28.
|
||
**Ветка:** `feature/groups-courses`.
|
||
**История:** `git log --oneline`; последние изменения — Woodpecker CI и
|
||
изоляция тестовой среды на отдельной VM.
|
||
**Пуш:** не выполнялся.
|
||
**Рабочее дерево:** чистое. `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` для работы со стендом.
|
||
- Старые прямые ссылки на бесхозные файлы у обычных пользователей ломаются
|
||
осознанно.
|
||
- Студенческие страницы материалов не показывают админские действия статей
|
||
(удаление/правка/публикация/перемещение); регрессия закрыта тестом
|
||
`LearnSectionTemplateTest` в master-хотфиксе `920d96a`.
|
||
|
||
**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`.
|
||
- **58 claims**: 27 P0 + 31 P1; **58/58 proven, P0 gaps = 0**.
|
||
- **368 автотестов**, 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: Gitea + Woodpecker 3.x, конфигурация в `.woodpecker/`:
|
||
- `check` на push/MR: тесты, ktlint, detekt, `evidenceCheck`;
|
||
- `smoke` на push/MR: после `check` — preflight + полный стек, health,
|
||
вход админа, один курс;
|
||
- `release` на тег или вручную: тесты, миграции, mutation, runtime/load,
|
||
browser E2E, финальный evidence.
|
||
- workflows требуют метку `isolation=dedicated-vm`; шаги идут в nested dind
|
||
без хост-Docker сокета, агент — на отдельной VM без production-доступа.
|
||
- `scripts/ci-preflight.sh` проверяет Java 21, Docker Compose v2, Python 3,
|
||
Node и Playwright. Установка server/agent — `deploy/WOODPECKER.md`.
|
||
- Первый реальный Woodpecker 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 (то же, что workflow 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
|
||
```
|
||
|
||
Последний подтверждённый результат:
|
||
- `368` тестов, 0 failures;
|
||
- `58/58` claims, P0 gaps = 0;
|
||
- `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-прогон.**
|
||
Развернуть Woodpecker server и Docker-backend агент на отдельной VM по
|
||
`deploy/WOODPECKER.md`, включить репозиторий в Woodpecker и дождаться
|
||
зелёных workflow `check`/`smoke` на push-MR и `release` на теге. Шаги идут
|
||
в nested dind; VM не имеет доступа к production и production-секретам.
|
||
|
||
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` — должно быть 368 тестов
|
||
и 58 claims, 58/58 proven.
|
||
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/ci-dind.sh`,
|
||
`scripts/ci-build-image.sh`, `scripts/legacy-assets.sh`.
|
||
- `.woodpecker/{check,smoke,release}.yaml` — CI на Gitea + Woodpecker.
|
||
- `ci/Dockerfile` — прогретый образ `lms-ci:1` для изолированного dind.
|
||
- `deploy/backup.sh`, `deploy/deploy.sh`, `deploy/DEPLOY.md`,
|
||
`deploy/WOODPECKER.md`.
|