Files
LMS/docs/17-handoff.md
Vladislav Kopnin 65ed08f5c2 feat(web): перевести браузерные URL курсов на courseNo
- teacher/admin course pages, content, gradebook, profile, downloads
- courseNo -> UUID только на границе REST API
- обновлены e2e, шаблоны, тесты и docs
- LDD-UI-002; baseline 59 claims / 385 тестов
2026-10-29 19:51:03 +03:00

312 lines
19 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`; последние изменения — 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`.
- **59 claims**: 27 P0 + 32 P1; **59/59 proven, P0 gaps = 0**.
- **385 автотестов**, 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
```
Последний подтверждённый результат:
- `385` тестов, 0 failures;
- `59/59` 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` — должно быть 385 тестов
и 59 claims, 59/59 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`.