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:
@@ -53,8 +53,10 @@ flowchart TB
|
||||
в контейнерах это `http://identity:8081/api/v1/auth/jwks.json` (иначе 401).
|
||||
|
||||
Сроки сессии задаются переменными (значения по умолчанию подходят):
|
||||
`LMS_ACCESS_TOKEN_TTL=15m` и `LMS_REFRESH_TOKEN_TTL=7d` для identity,
|
||||
`LMS_SESSION_TIMEOUT=7d` для web.
|
||||
`LMS_ACCESS_TOKEN_TTL=15m`, `LMS_REFRESH_TOKEN_TTL=7d`,
|
||||
`LMS_SESSION_ABSOLUTE_TIMEOUT=30d`, `LMS_REFRESH_TOKEN_RETENTION=30d`,
|
||||
`LMS_REFRESH_TOKEN_CLEANUP_CRON` для identity; `LMS_SESSION_TIMEOUT=7d` и
|
||||
`LMS_SESSION_ABSOLUTE_TIMEOUT=30d` для web.
|
||||
|
||||
## 3. Сборка
|
||||
|
||||
@@ -105,7 +107,7 @@ sequenceDiagram
|
||||
DC->>DC: docker build (5 стадий)
|
||||
DC->>PG: старт postgres (volume lms_pgdata)
|
||||
DC->>DC: старт identity → academic → progress → gateway → web
|
||||
Note over DC: Flyway применяет миграции автоматически<br/>при старте сервисов (identity V1..V11, academic V1..V12, progress V1..V9 и V11..V13; у web миграций нет)
|
||||
Note over DC: Flyway применяет миграции автоматически<br/>при старте сервисов (identity V1..V13, academic V1..V12, progress V1..V9 и V11..V13; у web миграций нет)
|
||||
```
|
||||
|
||||
- Локально (`docker-compose.yml`) поднимается только `postgres`; прод-стек — `docker-compose.prod.yml`.
|
||||
|
||||
@@ -227,9 +227,10 @@ CREATE INDEX audit_events_action_idx ON identity.audit_events (action);
|
||||
```
|
||||
Внешних ключей нет: событие должно переживать удаление субъекта. Запись идёт
|
||||
из сервисов через `POST /api/v1/internal/audit` (`X-Internal-Token`), чтение —
|
||||
`GET /api/v1/audit?page=&limit=&action=` и `/admin/audit` только для админа.
|
||||
Страница аудита поддерживает `page` (с нуля) и `limit` (максимум 500), в UI —
|
||||
переход «Показать ещё».
|
||||
`GET /api/v1/audit?limit=&action=&cursor=` и `/admin/audit` только для админа.
|
||||
Страница аудита продолжается keyset-курсором `(occurred_at, id)`; offset `page`
|
||||
остаётся для совместимости, `limit` — максимум 500. В UI — «Показать ещё» и
|
||||
«В начало»; битый курсор даёт `400`.
|
||||
|
||||
**V11 — refresh-токены (добавлена при закрытии рисков аутентификации)**
|
||||
```sql
|
||||
@@ -422,7 +423,8 @@ V13 идемпотентна: если старая feature-миграция V10
|
||||
и защиты. Свой профиль студент открывает без записи в аудит, чтобы не
|
||||
засорять журнал и не нагружать identity на горячем пути. Админ смотрит
|
||||
журнал на `/admin/audit` с фильтром по действию; время отображается в МСК.
|
||||
Журнал читается страницами (`page`/`limit`, «Показать ещё»), а не только
|
||||
Журнал читается keyset-курсором (`cursor`/`limit`, «Показать ещё»; offset
|
||||
`page` — только совместимость), а не только
|
||||
последними N событиями. `membership_history` и `promotion_runs/items`
|
||||
остаются доменной историей и дополняют общий журнал.
|
||||
|
||||
@@ -433,19 +435,20 @@ V13 идемпотентна: если старая feature-миграция V10
|
||||
сервисами на каждом запросе).
|
||||
- **Refresh-токен** — 7 дней (`LMS_REFRESH_TOKEN_TTL`), хранится хешем в
|
||||
`identity.refresh_tokens` (`V11`), ротируется при каждом обновлении.
|
||||
Повторное использование старого токена отзывает все активные сессии
|
||||
аккаунта; выход, смена пароля, отчисление/выпуск/архив и закрытая группа
|
||||
обновление запрещают.
|
||||
Абсолютный потолок сессии — 30 дней (`LMS_SESSION_ABSOLUTE_TIMEOUT`, `V12`):
|
||||
ротация не продлевает `absolute_expires_at`. Повторное использование старого
|
||||
токена отзывает все активные сессии аккаунта; выход, смена пароля,
|
||||
отчисление/выпуск/архив и закрытая группа обновление запрещают. Истёкшие
|
||||
токены старше retention удаляет ночная задача.
|
||||
- **Прямой API-клиент** обязан обновлять пару через
|
||||
`POST /api/v1/auth/refresh`; `POST /api/v1/auth/logout` отзывает refresh.
|
||||
- **Web-сессия** — 7 дней sliding (`LMS_SESSION_TIMEOUT`), cookie `LMS_SESSION`
|
||||
(`HttpOnly`, `SameSite=Lax`, `Max-Age` обновляется на каждом ответе).
|
||||
Access-токен автоматически обновляется за 2 минуты до истечения, поэтому
|
||||
активная вкладка не требует повторного входа каждые 15 минут; при выходе
|
||||
refresh отзывается.
|
||||
- **Ограничение:** абсолютного лимита жизни сессии нет — при непрерывной
|
||||
активности она продлевается. Если политика потребует принудительный
|
||||
повторный вход раз в N дней, добавляется отдельный абсолютный `max-age`.
|
||||
(`HttpOnly`, `SameSite=Lax`, `Max-Age` обновляется на каждом ответе), но не
|
||||
дольше 30 дней с момента входа (`LMS_SESSION_ABSOLUTE_TIMEOUT`). После
|
||||
абсолютного срока cookie получает `Max-Age=0`, а пользователь — повторный
|
||||
вход. Access-токен автоматически обновляется за 2 минуты до истечения,
|
||||
поэтому активная вкладка не требует повторного входа каждые 15 минут; при
|
||||
выходе refresh отзывается.
|
||||
|
||||
---
|
||||
|
||||
@@ -457,8 +460,9 @@ V13 идемпотентна: если старая feature-миграция V10
|
||||
- **Активы (реализовано):** видимость актива наследуется от статьи → раздела →
|
||||
главы, маршрут `/api/v1/content/assets/{name}` проверяет доступ. Файл без
|
||||
связи со статьёй не публичен: аноним/студент/преподаватель получают `404`,
|
||||
админ может скачать файл и получить список таких файлов через
|
||||
`GET /api/v1/content/legacy-assets` для привязки или удаления. Старые
|
||||
админ может скачать файл, открыть страницу `/admin/legacy-assets`, вызвать
|
||||
`GET /api/v1/content/legacy-assets` или `scripts/legacy-assets.sh` для
|
||||
привязки или удаления. Старые
|
||||
внешние ссылки на несвязанные файлы у обычных пользователей перестают
|
||||
работать — это осознанное secure-by-default поведение.
|
||||
- Поисковая индексация: для `ALL` — по умолчанию разрешена; для остального — закрыть (robots/noindex).
|
||||
@@ -527,13 +531,16 @@ V13 идемпотентна: если старая feature-миграция V10
|
||||
|
||||
---
|
||||
|
||||
## 12. Интеграция «Контингент» / next.uniyar.ac.ru (задел)
|
||||
## 12. Интеграция «Контингент» / next.uniyar.ac.ru — исключена из плана
|
||||
|
||||
- Позже — **отдельный сервис-адаптер** (anti-corruption layer); внешняя система — источник истины по
|
||||
курсу/группе/статусу; LMS — зеркало; синхронизация односторонняя + отчёт дрифта.
|
||||
- Сейчас закладываем: `student_no` уникальный и стабильный (ключ склейки); внешние id добавим миграцией.
|
||||
- Обязательно: идемпотентность, журнал синхронизаций, отчёт расхождений, обработка академа/переводов/отчислений,
|
||||
устойчивость к недоступности внешней системы, ПДн.
|
||||
Решением от 2026-10-28 интеграция **не входит в объём работ**: нет API-спецификации
|
||||
и доступа к внешней системе, а без них можно сделать только непроверяемую заглушку.
|
||||
`student_no` остаётся уникальным и стабильным, поэтому при появлении нового ТЗ
|
||||
адаптер можно добавить без переделки модели.
|
||||
|
||||
Если интеграция понадобится позже: отдельный anti-corruption layer, идемпотентная
|
||||
синхронизация, журнал, отчёт дрифта, обработка академа/переводов/отчислений,
|
||||
устойчивость к недоступности внешней системы и ПДн.
|
||||
|
||||
---
|
||||
|
||||
@@ -573,6 +580,6 @@ V13 идемпотентна: если старая feature-миграция V10
|
||||
4. Запись/снятие групп на курс.
|
||||
5. Повышение курса: dry-run, журнал, идемпотентность, откат, выпуск.
|
||||
6. Академический отпуск/возврат, отчисление, выпуск, архив; усиленное удаление.
|
||||
7. (Позже) сервис-адаптер «Контингент»/`next.uniyar.ac.ru`.
|
||||
7. ~~Сервис-адаптер «Контингент»/`next.uniyar.ac.ru`~~ — исключён из плана.
|
||||
|
||||
Открытых вопросов нет: все решения зафиксированы в §1.
|
||||
|
||||
@@ -211,11 +211,11 @@
|
||||
|
||||
Номера уточняются при реализации, чтобы не конфликтовать с максимумами master
|
||||
(identity V3, **academic V5**, progress V12). Максимумы этой ветки:
|
||||
identity V11, **academic V12**, progress V13.
|
||||
identity V13, **academic V12**, progress V13.
|
||||
|
||||
| Сервис | Миграции | Содержание |
|
||||
|---|---|---|
|
||||
| identity | V4–V11 | роли, поля групп, статусы студентов, `membership_history`, `promotion_runs/items`, `accounts.status`; `V10` — `audit_events` без внешних ключей и индексы по `occurred_at`/`action`; `V11` — `refresh_tokens` (хеш, срок, отзыв, цепочка замен) |
|
||||
| identity | V4–V13 | роли, поля групп, статусы студентов, `membership_history`, `promotion_runs/items`, `accounts.status`; `V10` — `audit_events`; `V11` — `refresh_tokens` (хеш, срок, отзыв, цепочка замен); `V12` — `absolute_expires_at`; `V13` — keyset-индексы аудита |
|
||||
| academic | V6–V12 + post-bootstrap step | курсы, преподаватели, группы курса, `labs.course_id` (nullable), материалы курса/аудитории, частичные индексы, активы; `V10` — уникальный индекс `lower(title)` по курсам; `V11` — backfill `audience='ALL'` для глобальных глав; `V12` — backfill связей `article_assets` из markdown-ссылок; `SET NOT NULL` — post-bootstrap шаг вне Flyway |
|
||||
| progress | V13 | `grade_comments.author_account_id`, `lab_grades.updated_by_account_id`, индекс `lab_grades(lab_id)`; аудит реализован в identity `V10` |
|
||||
|
||||
@@ -297,13 +297,15 @@ identity V11, **academic V12**, progress V13.
|
||||
- **Legacy-активы.** Закрыто: связанные активы наследуют видимость статьи
|
||||
(миграция `V12`), а файлы без markdown-ссылки по умолчанию отдают `404`
|
||||
анониму/студенту/преподавателю. Админ перечисляет их через
|
||||
`GET /api/v1/content/legacy-assets` для привязки или удаления.
|
||||
`GET /api/v1/content/legacy-assets`, страницу `/admin/legacy-assets` или
|
||||
`scripts/legacy-assets.sh` для привязки или удаления.
|
||||
- **JWT и сессии.** Закрыто: access-токен 15 минут, refresh 7 дней с ротацией
|
||||
и отзывом, web-сессия 7 дней sliding. Прямые API-клиенты используют
|
||||
`POST /api/v1/auth/refresh`; logout и смена пароля отзывают refresh.
|
||||
- **Оставшаяся производительность.** Аудит постраничный и группы курса
|
||||
отдаются одним вызовом, но N+1 в отдельных лабораторных списках и
|
||||
offset-пагинация аудита остаются лабораторными деталями.
|
||||
и отзывом, web-сессия 7 дней sliding с абсолютным потолком 30 дней.
|
||||
Прямые API-клиенты используют `POST /api/v1/auth/refresh`; logout и смена
|
||||
пароля отзывают refresh; истёкшие токены чистит ночная задача.
|
||||
- **Производительность.** Аудит продолжается keyset-курсором, детали всех
|
||||
лабораторных курса и группы курса отдаются пакетно. Отдельный запрос сдач
|
||||
по выбранной лабораторной в ведомости остаётся, но это не N+1 по студентам.
|
||||
- **Комментарии без автора.** Старые записи останутся с «автор не указан».
|
||||
- **Архивные студенты.** Открывать ли профиль после отвязки группы — уточнить
|
||||
на этапе 3; по умолчанию история курса доступна, если студент был в курсе.
|
||||
@@ -334,25 +336,31 @@ identity V11, **academic V12**, progress V13.
|
||||
- **Этап 6 — выполнен.** Права преподавателя на запись (лабораторные,
|
||||
ведомость, группы, материалы), центральный аудит `identity.audit_events`
|
||||
с `/admin/audit`, CI merge gate и полный hardening. Проверены прогон
|
||||
тестов/линтеров (**319**, 0 failures), upgrade миграций с master-схемы
|
||||
(identity `V4–V11`, academic `V6–V12`, progress `V13`), security-матрица,
|
||||
gateway, видимость активов и deploy-runbook. LDD baseline: **46 claims
|
||||
(25 P0 / 21 P1), 46/46 proven**, browser E2E 4/4. Отчёт:
|
||||
тестов/линтеров (**346**, 0 failures), upgrade миграций с master-схемы
|
||||
(identity `V4–V13`, academic `V6–V12`, progress `V13`), security-матрица,
|
||||
gateway, видимость активов и deploy-runbook. LDD baseline: **51 claims
|
||||
(25 P0 / 26 P1), 51/51 proven**, browser E2E 18/18 (3 браузера). Отчёт:
|
||||
[16-stage6-report.md](16-stage6-report.md).
|
||||
|
||||
### 9.1. Известные пробелы после этапа 6
|
||||
|
||||
- **Импорт.** Старый формат не поддерживается, принимается только шаблон v2.
|
||||
Номер группы — числовой; отдельного требования к ведущим нулям нет.
|
||||
- **Пагинация и N+1.** `GET /api/v1/audit` поддерживает `page` + `limit`,
|
||||
в UI есть «Показать ещё»; `GET /api/v1/courses/{id}/groups` отдаёт
|
||||
привязанные группы с именами одним вызовом. Остаются offset-пагинация
|
||||
аудита и N+1 в отдельных лабораторных списках/ведомости на больших курсах.
|
||||
- **Абсолютный лимит сессии.** Web-сессия скользящая (7 дней) и отзывается
|
||||
при выходе/смене пароля/закрытии доступа, но при непрерывной активности
|
||||
может жить бессрочно; при необходимости нужен абсолютный `max-age`.
|
||||
- **Внешняя интеграция.** «Контингент»/`next.uniyar.ac.ru` остаётся заделом:
|
||||
отдельный anti-corruption слой и внешние идентификаторы — позже.
|
||||
- **Пагинация и N+1.** Аудит продолжается keyset-курсором `(occurred_at, id)`
|
||||
с offset-fallback для старых ссылок; `GET /api/v1/labs/details` отдаёт все
|
||||
лабораторные курса с уровнями и вариантами одним запросом; группы курса —
|
||||
одним вызовом. Ведомость по-прежнему запрашивает сдачи по выбранной
|
||||
лабораторной — это не N+1 по студентам.
|
||||
- **Legacy-активы.** Файлы без статьи уже не публичны; админская страница
|
||||
`/admin/legacy-assets`, API и `scripts/legacy-assets.sh` готовы. Остаётся
|
||||
прогнать инвентаризацию на production/staging и привязать или удалить файлы.
|
||||
- **Сессии.** Web-сессия скользящая 7 дней, абсолютный потолок 30 дней
|
||||
(`LMS_SESSION_ABSOLUTE_TIMEOUT`); истёкшие refresh-токены чистит ночная
|
||||
задача (`LMS_REFRESH_TOKEN_RETENTION`).
|
||||
- **CI.** Конфигурация и `ci-preflight.sh` готовы; первый реальный pipeline
|
||||
на GitLab shell-runners ещё нужно подтвердить.
|
||||
- **Внешняя интеграция «Контингент»/`next.uniyar.ac.ru` исключена из плана**
|
||||
до появления нового ТЗ и доступа к API.
|
||||
|
||||
## 10. Definition of Done
|
||||
|
||||
@@ -363,12 +371,13 @@ identity V11, **academic V12**, progress V13.
|
||||
- Профиль показывает статистику, комментарии и все попытки; файлы скачиваются.
|
||||
- Повышение, академотпуск, выпуск и архив работают по ТЗ.
|
||||
- Аудит чувствительных действий пишется в `identity.audit_events` и доступен
|
||||
админу на `/admin/audit` с пагинацией.
|
||||
админу на `/admin/audit` с keyset-пагинацией.
|
||||
- Активы наследуют видимость материала; файлы без привязки не публичны и
|
||||
доступны только админу через инвентаризацию `legacy-assets`.
|
||||
доступны только админу через страницу `/admin/legacy-assets`.
|
||||
- Access/refresh-токены и web-сессия работают по срокам 15 минут / 7 дней /
|
||||
7 дней; logout, смена пароля и закрытие доступа отзывают refresh.
|
||||
- Security-матрица и миграционный upgrade-тест зелёные (identity до `V11`).
|
||||
7 дней sliding с абсолютным потолком 30 дней; logout, смена пароля и
|
||||
закрытие доступа отзывают refresh.
|
||||
- Security-матрица и миграционный upgrade-тест зелёные (identity до `V13`).
|
||||
- CI на каждом push/MR прогоняет `test ktlintCheck detekt evidenceCheck` и
|
||||
smoke полного стека; полный release-evidence обязателен для тегов.
|
||||
- Backup перед деплоем обязателен; restore проверен на staging.
|
||||
|
||||
@@ -9,10 +9,11 @@
|
||||
- ADR с ключевыми решениями: [adr/0001-groups-courses-foundation.md](adr/0001-groups-courses-foundation.md).
|
||||
- Контракт API профиля студента: [09-teacher-student-profile.md](09-teacher-student-profile.md).
|
||||
- Миграции:
|
||||
- identity `V4–V11`: роли, группы-когорты, статусы студентов, повышение,
|
||||
- identity `V4–V13`: роли, группы-когорты, статусы студентов, повышение,
|
||||
история членства, статусы аккаунтов; `V10` (`audit_events`) добавлена
|
||||
позже, на этапе 6; `V11` (`refresh_tokens`) — при закрытии рисков
|
||||
аутентификации, тоже после этого отчёта;
|
||||
аутентификации, тоже после этого отчёта; `V12` (`absolute_expires_at`) и
|
||||
`V13` (keyset-индексы аудита) — при финальной доработке;
|
||||
- academic `V6–V12`: курсы, преподаватели, группы курса, `labs.course_id`,
|
||||
материалы курса/аудитории, индексы активов; `V10` (уникальный индекс
|
||||
`lower(title)` по курсам) добавлена на этапе 2, после этого отчёта;
|
||||
|
||||
@@ -54,12 +54,12 @@
|
||||
`V12` закрывает файлы, на которые есть ссылки в markdown. Если файл был
|
||||
загружен, но ни одна статья на него не ссылается, он больше **не публичен**:
|
||||
аноним, студент и преподаватель получают `404`, чтобы не раскрывать
|
||||
существование файла. Админ скачивает такой файл и получает его имя в
|
||||
инвентаризации `GET /api/v1/content/legacy-assets`, после чего привязывает
|
||||
файл к статье или удаляет. Старые внешние ссылки на несвязанные файлы у
|
||||
обычных пользователей перестают работать — это осознанное поведение
|
||||
secure-by-default, а не регрессия. Эндпоинт инвентаризации защищён
|
||||
`ROLE_ADMIN`.
|
||||
существование файла. Админ скачивает такой файл и получает его имя на
|
||||
странице `/admin/legacy-assets`, через `GET /api/v1/content/legacy-assets`
|
||||
или `scripts/legacy-assets.sh`, после чего привязывает файл к статье или
|
||||
удаляет. Старые внешние ссылки на несвязанные файлы у обычных пользователей
|
||||
перестают работать — это осознанное поведение secure-by-default, а не
|
||||
регрессия. Эндпоинт и страница инвентаризации защищены `ROLE_ADMIN`.
|
||||
|
||||
## Авторство материалов преподавателем (добавлено на этапе 6)
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
| Этап | Что реализовано | Отчёт |
|
||||
|---|---|---|
|
||||
| 0 | Схема и контракты: identity `V4–V11` (`V10` — аудит, `V11` — refresh-токены), academic `V6–V12`, progress `V13`; preflight-скрипты; ADR; контракт профиля студента курса | [10](10-stage0-report.md) |
|
||||
| 0 | Схема и контракты: identity `V4–V13` (`V10` — аудит, `V11` — refresh, `V12` — absolute, `V13` — keyset-индексы), academic `V6–V12`, progress `V13`; preflight-скрипты; ADR; контракт профиля студента курса | [10](10-stage0-report.md) |
|
||||
| 1 | Роли `ADMIN/TEACHER/STUDENT`, группы-когорты и парсер имени, статусы студентов и аккаунтов, `membership_history`, импорт v2 | [11](11-stage1-report.md) |
|
||||
| 2 | Курсы, `course_no` для браузерных URL, история преподавателей и групп, bootstrap курса C, привязка лабораторных к курсу, копирование лабораторных, админ-UI | [12](12-stage2-report.md) |
|
||||
| 3 | Аккаунты преподавателей, курсовая область видимости, внутренние API identity с `LMS_INTERNAL_TOKEN`, кабинет преподавателя, профиль студента курса и скачивание всех попыток | [13](13-stage3-report.md) |
|
||||
@@ -31,7 +31,10 @@
|
||||
недоступны (`404`) всем, кроме админа; админ перечисляет их через
|
||||
`GET /api/v1/content/legacy-assets`;
|
||||
- access-токен живёт 15 минут, refresh — 7 дней с ротацией и отзывом; web-сессия
|
||||
скользящая на 7 дней и сама обновляет access-токен;
|
||||
скользящая на 7 дней, но не дольше абсолютного потолка 30 дней, и сама
|
||||
обновляет access-токен; истёкшие refresh-токены очищаются ночью;
|
||||
- аудит продолжается keyset-курсором `(occurred_at, id)`, а детали всех
|
||||
лабораторных курса отдаются одним пакетным запросом без N+1;
|
||||
- преподаватель своего курса может писать: лабораторные/уровни/варианты,
|
||||
ведомость, привязка групп и авторские материалы курса; чужие курс, лаба,
|
||||
студент и группа отклоняются на сервисах;
|
||||
@@ -62,14 +65,15 @@
|
||||
|
||||
| Модуль | Тестов | Failures | Errors | Skipped |
|
||||
|---|---:|---:|---:|---:|
|
||||
| identity-service | 72 | 0 | 0 | 0 |
|
||||
| academic-service | 88 | 0 | 0 | 0 |
|
||||
| progress-service | 33 | 0 | 0 | 0 |
|
||||
| web | 69 | 0 | 0 | 0 |
|
||||
| identity-service | 98 | 0 | 0 | 0 |
|
||||
| academic-service | 99 | 0 | 0 | 0 |
|
||||
| progress-service | 36 | 0 | 0 | 0 |
|
||||
| web | 106 | 0 | 0 | 0 |
|
||||
| gateway | 2 | 0 | 0 | 0 |
|
||||
| markdown | 5 | 0 | 0 | 0 |
|
||||
| **Итого на момент этапа 6** | **267** | **0** | **0** | **0** |
|
||||
| **Итого на момент этапа 6** | **346** | **0** | **0** | **0** |
|
||||
|
||||
После закрытия рисков актуальный LDD-baseline — **319 тестов, 0 failures**
|
||||
После закрытия рисков актуальный LDD-baseline — **346 тестов, 0 failures**
|
||||
(см. раздел 7). `ktlintCheck` и `detekt` прошли без замечаний. У gateway теперь есть unit-тесты публичных auth-путей и 401; маршрутизация
|
||||
gateway дополнительно проверена конфигурацией и security-матрицей ниже.
|
||||
Тот же набор команд выполняет `.gitlab-ci.yml`; на каждый push/MR добавлен
|
||||
@@ -86,7 +90,7 @@ smoke-job, полный release-evidence обязателен для тегов.
|
||||
(`ПИ-11БО`, `ПИ-21БО`), зачисления, семестр, лабораторная, глава/раздел/
|
||||
статья/актив, оценка, комментарий и попытка сдачи.
|
||||
3. Поверх master применены миграции ветки:
|
||||
- identity `V4–V11`;
|
||||
- identity `V4–V13`;
|
||||
- academic `V6–V12`;
|
||||
- progress `V13`.
|
||||
4. Выполнены postflight-запросы и повторное применение `V13`.
|
||||
@@ -100,7 +104,10 @@ smoke-job, полный release-evidence обязателен для тегов.
|
||||
действием `ENROLL`; добавлен `accounts.status`; `V10` создаёт
|
||||
`identity.audit_events` без внешних ключей и индексы по `occurred_at`/`action`;
|
||||
`V11` создаёт `identity.refresh_tokens` (SHA-256-хеш, `expires_at`,
|
||||
`revoked_at`, `replaced_by`, индексы по аккаунту и сроку).
|
||||
`revoked_at`, `replaced_by`, индексы по аккаунту и сроку); `V12` добавляет
|
||||
`absolute_expires_at` с бэкфиллом и `NOT NULL`; `V13` создаёт композитные
|
||||
индексы `(occurred_at DESC, id DESC)` и `(action, occurred_at DESC, id DESC)`
|
||||
под keyset-пагинацию.
|
||||
- **academic:** добавлены `labs.course_id`, `chapters.course_id/audience`,
|
||||
индексы `labs_number_course_uidx`, `chapters_slug_global_uidx`,
|
||||
`chapters_slug_course_uidx`, `article_assets_article_idx`,
|
||||
@@ -126,7 +133,7 @@ smoke-job, полный release-evidence обязателен для тегов.
|
||||
|
||||
### 2.3. Security-проверки
|
||||
|
||||
Проверено автоматическими тестами (актуальный прогон — 319 тестов, 0 failures)
|
||||
Проверено автоматическими тестами (актуальный прогон — 346 тестов, 0 failures)
|
||||
и разбором конфигурации gateway. Запись преподавателя дополнительно покрыта
|
||||
negative-тестами «свой/чужой курс».
|
||||
|
||||
@@ -141,7 +148,7 @@ negative-тестами «свой/чужой курс».
|
||||
| Преподаватель своего курса → скрытый курсовой актив | доступ | `200`; админ видит любой актив |
|
||||
| Глобальный `STUDENTS`-актив → преподаватель | `403` | `403`; студент получает `200` |
|
||||
| Legacy-актив без связи со статьёй | `404` для анонима/студента/преподавателя, файл доступен только админу | `404` всем, кроме `ADMIN`; `GET /api/v1/content/legacy-assets` под `ROLE_ADMIN` перечисляет несвязанные файлы |
|
||||
| Access/refresh | короткий access + ротация refresh | access JWT 15 минут (`LMS_ACCESS_TOKEN_TTL`), refresh 7 дней (`LMS_REFRESH_TOKEN_TTL`); `POST /api/v1/auth/refresh` ротирует пару, повторное использование старого токена отзывает все сессии аккаунта |
|
||||
| Access/refresh | короткий access + ротация refresh | access JWT 15 минут (`LMS_ACCESS_TOKEN_TTL`), refresh 7 дней (`LMS_REFRESH_TOKEN_TTL`), абсолютный потолок сессии 30 дней (`LMS_SESSION_ABSOLUTE_TIMEOUT`); `POST /api/v1/auth/refresh` ротирует пару, не продлевая `absolute_expires_at`; повторное использование старого токена отзывает все сессии аккаунта; истёкшие токены чистит ночная задача |
|
||||
| Logout / смена пароля | сессии отзываются | `POST /api/v1/auth/logout` отзывает refresh; смена пароля — `revokeAll` |
|
||||
| Архив/отчисление/закрытая группа → refresh | обновление запрещено | `ensureSessionAllowed` при ротации: `EXPELLED`, `GRADUATED`, `ARCHIVED`, архивный аккаунт и `GRADUATED`/`ARCHIVED` группа дают `401` |
|
||||
| Web-сессия | 7 дней sliding, без повторного входа, быстрый отзыв | cookie `LMS_SESSION`, `Max-Age` 7 дней (`LMS_SESSION_TIMEOUT`), `HttpOnly`/`SameSite=Lax`; access обновляется за 2 минуты до истечения, cookie перевыпускается на каждом ответе; статус перепроверяется не реже чем раз в 45 секунд, `active=false` или `401/403` инвалидируют сессию, недоступность identity не разлогинивает всех; выход отзывает refresh |
|
||||
@@ -149,7 +156,7 @@ negative-тестами «свой/чужой курс».
|
||||
| Преподаватель своего курса → чтение | курс, группы, профиль, материалы | видит опубликованные и скрытые материалы, всех студентов активной группы курса, неопубликованные оценки и все попытки; `GET /api/v1/courses/{id}/groups` отдаёт привязанные группы с именами одним вызовом (без N+1) |
|
||||
| Преподаватель своего курса → запись | лабы/уровни/варианты, ведомость, группы, материалы | оценки/комментарии, публикация, уровни/варианты и раздача, защиты (код/reset), привязка групп, авторство материалов; проверки в progress `CourseScopeService` и academic course/content authoring |
|
||||
| Преподаватель чужого курса | `403` | `403` на чтение и запись: материалы, курс по `course_no`, состав группы, профиль, лабы, оценки, группы |
|
||||
| Аудит → админ | чтение журнала | `GET /api/v1/audit?page=&limit=&action=` и `/admin/audit` доступны только `ADMIN`; в UI — постраничный переход «Показать ещё» |
|
||||
| Аудит → админ | чтение журнала | `GET /api/v1/audit?limit=&action=&cursor=` (offset `page` — для совместимости) и `/admin/audit` доступны только `ADMIN`; в UI — keyset-переход «Показать ещё», битый курсор — `400` |
|
||||
| Аудит → запись из сервисов | только по токену | `POST /api/v1/internal/audit` с `X-Internal-Token`; без/неверный токен — `403` |
|
||||
| Студент → свой профиль | только своё и опубликованное | скрытые оценки/комментарии отсутствуют; чужой `studentId` — `403` |
|
||||
| Студент → чужой курс/группа | `403` | доступ только к курсам активной группы; архивный курс — `403` |
|
||||
@@ -158,10 +165,12 @@ negative-тестами «свой/чужой курс».
|
||||
| Gateway | внутренние пути не публикуются | маршрутов на `/api/v1/internal/**` нет; gateway требует JWT для всего, кроме `/actuator`, входа/setup/JWKS/refresh/logout и публичного `/api/v1/content/**` |
|
||||
| Gateway → заголовки личности | клиент не подменяет | `X-Account-Id/Login/Role` удаляются и проставляются только после проверки JWT |
|
||||
|
||||
Остаточные ограничения перечислены в разделе 4: offset-пагинация аудита,
|
||||
N+1 в отдельных лабораторных списках и отсутствие абсолютного лимита
|
||||
web-сессии. Продуктовые риски legacy-активов, 30-дневного JWT и ведущих
|
||||
нулей номера группы закрыты или сняты как не относящиеся к требованиям.
|
||||
Остаточные ограничения перечислены в разделе 4: ручная инвентаризация
|
||||
legacy-активов на production, сдачи по выбранной лабораторной в ведомости,
|
||||
непроверенный первый CI-прогон и отсутствие визуальной регрессии. Продуктовые
|
||||
риски legacy-активов, срока access-токена, пагинации, N+1 деталей лабораторных,
|
||||
абсолютного лимита сессии и ведущих нулей закрыты или сняты как не относящиеся
|
||||
к требованиям.
|
||||
|
||||
## 3. Чек-лист деплоя
|
||||
|
||||
@@ -185,16 +194,19 @@ web-сессии. Продуктовые риски legacy-активов, 30-д
|
||||
`smoke`/`release-evidence` — shell-runner с Java 21, Docker Compose v2,
|
||||
Python 3, Node и Chromium Playwright.
|
||||
- [ ] Заданы сроки сессии: `LMS_ACCESS_TOKEN_TTL=15m`,
|
||||
`LMS_REFRESH_TOKEN_TTL=7d`, `LMS_SESSION_TIMEOUT=7d` (значения по умолчанию
|
||||
подходят; менять только осознанно).
|
||||
`LMS_REFRESH_TOKEN_TTL=7d`, `LMS_SESSION_TIMEOUT=7d`,
|
||||
`LMS_SESSION_ABSOLUTE_TIMEOUT=30d`, `LMS_REFRESH_TOKEN_RETENTION=30d`
|
||||
(значения по умолчанию подходят; менять только осознанно). Очистка
|
||||
refresh запускается по `LMS_REFRESH_TOKEN_CLEANUP_CRON` (по умолчанию 03:30).
|
||||
|
||||
### 3.2. Порядок выкладки
|
||||
|
||||
1. **Flyway.** `./deploy/deploy.sh up` собирает образы и запускает
|
||||
`postgres → identity → academic → progress → gateway → web`. Миграции
|
||||
применяются автоматически при старте сервисов:
|
||||
identity `V4–V11` (`V10` — `identity.audit_events`, `V11` —
|
||||
`identity.refresh_tokens`), academic `V6–V12`, progress `V13`.
|
||||
identity `V4–V13` (`V10` — `identity.audit_events`, `V11` —
|
||||
`identity.refresh_tokens`, `V12` — `absolute_expires_at`, `V13` —
|
||||
keyset-индексы аудита), academic `V6–V12`, progress `V13`.
|
||||
Отдельный ручной запуск Flyway не нужен.
|
||||
2. **Bootstrap курса C.** После входа админа открыть `/admin/courses`.
|
||||
Пока курсов нет, показан блок первичной настройки. Bootstrap идемпотентно
|
||||
@@ -213,7 +225,9 @@ web-сессии. Продуктовые риски legacy-активов, 30-д
|
||||
- `comments_without_author` соответствует старым комментариям;
|
||||
- группы без полного разбора отсутствуют;
|
||||
- таблица `identity.audit_events` существует (`V10`);
|
||||
- таблица `identity.refresh_tokens` существует (`V11`).
|
||||
- таблица `identity.refresh_tokens` существует (`V11`);
|
||||
- у `refresh_tokens` есть `absolute_expires_at NOT NULL` (`V12`);
|
||||
- индекс `audit_events_occurred_id_idx` существует (`V13`).
|
||||
4. **V10 → V13 — только dev/staging.** На production/master записи
|
||||
`progress V10__profile_authors.sql` нет, ничего удалять не нужно. Не путать
|
||||
с academic `V10__course_title_unique.sql` — он относится к другой схеме и
|
||||
@@ -249,8 +263,8 @@ web-сессии. Продуктовые риски legacy-активов, 30-д
|
||||
- [ ] **Активы:** картинки курсовой статьи открываются у студента своего курса
|
||||
и преподавателя; чужой курс — `403`, скрытая картинка у студента — `404`,
|
||||
публичная `ALL` — без входа; файл без привязки к статье — `404` для всех,
|
||||
кроме админа. Админ проверяет инвентаризацию через
|
||||
`GET /api/v1/content/legacy-assets` (или админский UI/curl) и при
|
||||
кроме админа. Админ открывает `/admin/legacy-assets` (или
|
||||
`GET /api/v1/content/legacy-assets`, или `scripts/legacy-assets.sh`) и при
|
||||
необходимости привязывает или удаляет файлы.
|
||||
- [ ] **Копирование лабораторной:** админ копирует лабораторную в другой курс;
|
||||
создаётся черновик с уровнями и вариантами; оценки, сдачи, защиты и
|
||||
@@ -263,13 +277,13 @@ web-сессии. Продуктовые риски legacy-активов, 30-д
|
||||
- [ ] **Аудит:** `/admin/audit` открывается у админа, показывает события
|
||||
(например, `PROFILE_VIEW`, `SUBMISSION_DOWNLOAD`, `GRADE_SET`,
|
||||
`CONTENT_*`, `TEACHER_*`) с фильтром по действию и временем МСК;
|
||||
«Показать ещё» переходит на следующую страницу (`page`/`limit`);
|
||||
преподаватель/студент получают `403`.
|
||||
- [ ] **Сессия:** после входа cookie `LMS_SESSION` живёт 7 дней; при работе
|
||||
дольше 15 минут страница продолжает работать без повторного входа
|
||||
(access обновляется автоматически). Выход и смена пароля требуют повторного
|
||||
входа; вход отчисленного/архивного студента и refresh закрытой группы
|
||||
отклоняются.
|
||||
«Показать ещё» переходит по keyset-курсору (`cursor`), «В начало»
|
||||
возвращает к первой странице; преподаватель/студент получают `403`.
|
||||
- [ ] **Сессия:** после входа cookie `LMS_SESSION` живёт 7 дней, но не дольше
|
||||
30 дней с момента входа; при работе дольше 15 минут страница продолжает
|
||||
работать без повторного входа (access обновляется автоматически). Выход и
|
||||
смена пароля требуют повторного входа; вход отчисленного/архивного студента
|
||||
и refresh закрытой группы отклоняются.
|
||||
- [ ] Проверить логи `web`, `gateway`, `identity`, `academic`, `progress` на
|
||||
`ERROR`/`WARN` после smoke.
|
||||
|
||||
@@ -279,53 +293,65 @@ web-сессии. Продуктовые риски legacy-активов, 30-д
|
||||
шаблон v2 (поток/курс/номер/степень/год/ФИО/№/база). Ограничение
|
||||
зафиксировано в [11](11-stage1-report.md). Номер группы — числовой, поэтому
|
||||
отдельного требования к ведущим нулям нет.
|
||||
2. **Пагинация аудита — offset.** `GET /api/v1/audit` поддерживает `page` и
|
||||
`limit` (максимум 500), UI — «Показать ещё». Для очень больших журналов
|
||||
keyset-пагинация была бы устойчивее к вставкам между страницами.
|
||||
3. **N+1 в отдельных списках.** Имена групп курса отдаются одним вызовом
|
||||
(`GET /api/v1/courses/{id}/groups`), но часть лабораторных списков и
|
||||
ведомости на больших объёмах всё ещё собирается по частям. Полный нагрузочный
|
||||
профиль 100 групп / 1000 студентов проходит, однако пакетные выборки для
|
||||
оставшихся экранов — лабораторная деталь, а не блокер передачи.
|
||||
4. **Нет абсолютного лимита web-сессии.** Сессия скользящая (7 дней) и
|
||||
отзывается при выходе, смене пароля, отчислении/архиве и закрытии группы,
|
||||
но при непрерывной активности может жить бессрочно. Если политика требует
|
||||
принудительного повторного входа раз в N дней, нужен абсолютный `max-age`
|
||||
поверх sliding-сессии.
|
||||
2. **Инвентаризация legacy-активов на реальном хранилище.** UI
|
||||
`/admin/legacy-assets`, API и `scripts/legacy-assets.sh` готовы; остаётся
|
||||
прогнать их на production/staging и привязать или удалить найденные файлы.
|
||||
Это гигиена хранилища, а не открытый риск доступа: обычные пользователи
|
||||
уже получают `404`.
|
||||
3. **Ведомость запрашивает сдачи по выбранной лабораторной.** Детали всех
|
||||
лабораторных курса теперь пакетные (`GET /api/v1/labs/details`), но
|
||||
ведомость при открытии конкретной лабораторной делает отдельный запрос
|
||||
сдач. Это не N+1 по студентам и не блокер.
|
||||
4. **Browser E2E без визуальной регрессии.** 6 сценариев × Chromium/Firefox/
|
||||
WebKit = 18/18, но нет screenshot-diff и полного набора мобильных страниц.
|
||||
На Fedora WebKit запускается через Docker-образ Playwright, потому что
|
||||
системных Ubuntu-библиотек нет.
|
||||
5. **Первый реальный CI-прогон не подтверждён.** Конфигурация, smoke и
|
||||
`scripts/ci-preflight.sh` готовы, но наличие GitLab shell-runners с тегами
|
||||
`lms-shell`/`lms-release` и первый tag-job нужно проверить в GitLab.
|
||||
6. **Внешняя интеграция «Контингент» исключена из плана.** Отдельного
|
||||
anti-corruption слоя, синхронизации и внешних идентификаторов не будет без
|
||||
нового ТЗ и доступа к API.
|
||||
7. **Mutation testing не 100%.** Остаются эквивалентные мутанты и
|
||||
Kotlin/Spring-специфичные мутации; PIT harness — JUnit 4.
|
||||
8. **Нагрузочный smoke — не SLA.** Одна машина, синтетические данные;
|
||||
125 чтений и p95 < 1 с — ориентир, а не гарантия production.
|
||||
|
||||
## 5. Рекомендуемые следующие шаги
|
||||
|
||||
Приоритет — сверху вниз:
|
||||
|
||||
1. **Инвентаризация legacy-активов.** Несвязанные файлы уже не публичны; админ
|
||||
получает их список через `GET /api/v1/content/legacy-assets`, после чего
|
||||
привязывает к статьям или удаляет. Это гигиена хранилища, а не открытый
|
||||
риск доступа.
|
||||
2. **Keyset-пагинация и оставшиеся N+1.** Устойчивый курсор для аудита,
|
||||
пакетные выборки для лабораторных списков и ведомости на больших курсах.
|
||||
3. **Абсолютный лимит сессии.** Опционально: принудительный повторный вход
|
||||
раз в N дней поверх 7-дневной sliding-сессии.
|
||||
4. **Внешняя интеграция «Контингент»/next.uniyar.ac.ru.** Отдельный
|
||||
anti-corruption слой, идемпотентная синхронизация, журнал и отчёт дрифта;
|
||||
внешние идентификаторы добавляются миграцией.
|
||||
1. **Прогнать инвентаризацию legacy-активов на production/staging** через
|
||||
`/admin/legacy-assets` или `scripts/legacy-assets.sh`, привязать нужные
|
||||
файлы к статьям, остальные удалить.
|
||||
2. **Проверить CI на реальном runner.** Убедиться, что есть shell-runners
|
||||
`lms-shell`/`lms-release` с Java 21, Docker Compose v2, Python 3,
|
||||
Node/Chromium/Firefox/WebKit; дождаться зелёного push-MR pipeline и
|
||||
tag-job `release-evidence`.
|
||||
3. **Дальнейшее расширение E2E** — визуальная регрессия и остальные мобильные
|
||||
страницы, если UI продолжит расти.
|
||||
4. **Пакетные сдачи ведомости**, если появятся курсы, где открытие ведомости
|
||||
станет заметно медленным.
|
||||
|
||||
## 6. Итог
|
||||
|
||||
Этап 6 закрыт: права преподавателя на запись, аудит, CI, security-матрица,
|
||||
upgrade-тест миграций, видимость активов и deploy-runbook проверены.
|
||||
Продуктовые риски закрыты: legacy-активы secure-by-default, access/refresh и
|
||||
web-сессия с отзывом, пагинация аудита и bulk-выдача групп. Оставшиеся
|
||||
ограничения (offset-пагинация, отдельные N+1, отсутствие абсолютного лимита
|
||||
сессии) задокументированы в разделе 4 и не блокируют передачу.
|
||||
web-сессия с отзывом и абсолютным потолком, keyset-пагинация аудита,
|
||||
пакетные детали лабораторных и bulk-выдача групп. Оставшиеся ограничения
|
||||
(ручная инвентаризация на production, сдачи по выбранной лабораторной,
|
||||
непроверенный первый CI-прогон) задокументированы в разделе 4 и не блокируют
|
||||
передачу.
|
||||
|
||||
## 7. LDD baseline
|
||||
|
||||
После этапа 6 и закрытия рисков текущая функциональность проверена через
|
||||
LDD-контур: реестр `docs/evidence/claims.json` (**46 claims, 25 P0 / 21 P1**),
|
||||
LDD-контур: реестр `docs/evidence/claims.json` (**51 claims, 25 P0 / 26 P1**),
|
||||
gate `./gradlew evidenceCheck`, upgrade-скрипт, runtime-smoke, smoke-job,
|
||||
mutation testing и браузерный Playwright E2E. Результат:
|
||||
**46/46 claims proven, P0 gaps = 0, 319 тестов, 0 failures**; mutation strength
|
||||
`CourseAccessService` 75%, `PromotionPlanner` 69%; browser E2E 4/4;
|
||||
**51/51 claims proven, P0 gaps = 0, 346 тестов, 0 failures**; mutation strength
|
||||
`CourseAccessService` 75%, `PromotionPlanner` 69%; browser E2E 18/18 (6 сценариев × Chromium/Firefox/WebKit);
|
||||
конкурентные apply/rollback и масштабный нагрузочный smoke (100 групп /
|
||||
1000 студентов, 125 параллельных чтений, 0 ошибок, p95 < 1 с) зелёные.
|
||||
Smoke-job выполняется на каждом push/MR, полный `release-evidence` обязателен
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -14,7 +14,7 @@ Preview Mermaid Support) и в IntelliJ IDEA (плагин Mermaid).
|
||||
| [04-sequences.md](04-sequences.md) | Диаграммы последовательностей: вход, импорт ЛР, раздача вариантов, сдача, защита, оценки, материалы |
|
||||
| [05-states.md](05-states.md) | Диаграммы состояний: защита, публикация оценок, статус ЛР, публикация материалов |
|
||||
| [06-authoring.md](06-authoring.md) | Работа с учебными материалами: структура, markdown-редактор, импорт zip |
|
||||
| [07-groups-courses.md](07-groups-courses.md) | ТЗ: группы/потоки, курсы и преподаватели, повышение курса, академический отпуск, интеграция с «Контингент» |
|
||||
| [07-groups-courses.md](07-groups-courses.md) | ТЗ: группы/потоки, курсы и преподаватели, повышение курса, академический отпуск; «Контингент» исключён из плана |
|
||||
| [08-groups-courses-plan.md](08-groups-courses-plan.md) | План реализации ТЗ 07: решения, этапы 0–6, профиль студента курса, агенты и skills |
|
||||
| [09-teacher-student-profile.md](09-teacher-student-profile.md) | Контракт API профиля студента курса для преподавателя |
|
||||
| [10-stage0-report.md](10-stage0-report.md) | Отчёт этапа 0: миграции, preflight и проверка на копии БД |
|
||||
@@ -26,7 +26,7 @@ Preview Mermaid Support) и в IntelliJ IDEA (плагин Mermaid).
|
||||
| [16-stage6-report.md](16-stage6-report.md) | Отчёт этапа 6: права преподавателя на запись, аудит, CI, upgrade-тест миграций, security-матрица, чек-лист деплоя и остаточные ограничения |
|
||||
| [17-handoff.md](17-handoff.md) | Handoff: что уже внесено, что осталось, как восстановить контекст и прогнать проверки |
|
||||
| [evidence/README.md](evidence/README.md) | LDD: реестр утверждений, evidence gate, runtime- и migration-проверки |
|
||||
| [evidence/baseline-2026-10-27.md](evidence/baseline-2026-10-27.md) | LDD-baseline: 46 claims, 319 тестов, upgrade, runtime/scale, mutation и browser E2E |
|
||||
| [evidence/baseline-2026-10-27.md](evidence/baseline-2026-10-27.md) | LDD-baseline: 51 claims, 346 тестов, upgrade, runtime/scale, mutation и browser E2E |
|
||||
| [ADR 0001](adr/0001-groups-courses-foundation.md) | Ключевые архитектурные решения: курс C, миграции, доступ, статусы, повышение |
|
||||
|
||||
## Кратко о системе
|
||||
|
||||
@@ -92,8 +92,9 @@
|
||||
- `ACADEMIC_LEAVE` — вход в режиме только чтения.
|
||||
- Web перепроверяет статус с кэшем 30–60 секунд; сервисы проверяют статус
|
||||
студента и группы при доступе к курсу.
|
||||
- JWT остаётся 30-дневным; остаточный риск прямого API фиксируется в
|
||||
документации. Для браузера токен не доступен.
|
||||
- ~~JWT остаётся 30-дневным~~ — решение пересмотрено: access 15 минут,
|
||||
refresh 7 дней с ротацией и абсолютным потолком 30 дней (`V11`–`V12`).
|
||||
Для браузера токен не доступен.
|
||||
|
||||
### 6. Повышение курса
|
||||
|
||||
|
||||
@@ -52,14 +52,14 @@
|
||||
# 6. Mutation testing критичных P0-компонентов
|
||||
./scripts/ldd-mutation.sh
|
||||
|
||||
# 7. Браузерный E2E (Playwright + Chromium)
|
||||
# 7. Браузерный E2E (Playwright: Chromium/Firefox/WebKit)
|
||||
./scripts/ldd-browser.sh
|
||||
|
||||
# 8. Полный release gate
|
||||
./scripts/ldd-release.sh
|
||||
```
|
||||
|
||||
Для E2E один раз установите зависимости и браузер: `cd e2e && npm install && npx playwright install chromium`.
|
||||
Для E2E один раз установите зависимости и браузеры: `cd e2e && npm install && npx playwright install chromium firefox webkit`.
|
||||
Mutation harness использует JUnit 4: JUnit5-плагин PIT несовместим с JUnit
|
||||
Platform 6 из Spring Boot 4.
|
||||
|
||||
@@ -87,15 +87,17 @@ Platform 6 из Spring Boot 4.
|
||||
LDD-циклы выполнены 2026-10-27/28; последний — после закрытия продуктовых
|
||||
рисков:
|
||||
|
||||
- **319 автоматических тестов**, 0 failures/errors;
|
||||
- **46 claims**: 25 P0 + 21 P1, **46/46 proven, P0 gaps = 0**. P0 — доступы,
|
||||
- **346 автоматических тестов**, 0 failures/errors;
|
||||
- **51 claims**: 25 P0 + 26 P1, **51/51 proven, P0 gaps = 0**. P0 — доступы,
|
||||
повышение, жизненный цикл, профиль/скачивание, материалы/активы
|
||||
(включая secure-by-default legacy-файлы), миграция V13, аудит, refresh-токены
|
||||
и их отзыв; P1 — группы, авторство, UI, контракты, импорт, копирование лаб,
|
||||
пагинация аудита, bulk-группы курса, runtime, mutation и browser E2E;
|
||||
(включая secure-by-default legacy-файлы), миграции V11–V13, аудит,
|
||||
refresh-токены и их отзыв; P1 — группы, авторство, UI, контракты, импорт,
|
||||
копирование лаб, keyset-пагинация, пакетные детали лабораторных,
|
||||
абсолютный потолок сессии, runtime, mutation и browser E2E;
|
||||
- mutation testing: `CourseAccessService` 75% test strength,
|
||||
`PromotionPlanner` 69%;
|
||||
- Playwright: 4 сценария (admin, teacher, student/anonymous, сдача+mobile),
|
||||
- Playwright: 6 сценариев (admin, teacher, student/anonymous, сдача+mobile,
|
||||
cursor-аудит, инвентаризация активов) × Chromium/Firefox/WebKit = **18/18**,
|
||||
все зелёные;
|
||||
- масштабный load: 100 групп / 1000 студентов, 125 параллельных чтений,
|
||||
0 ошибок, p95 < 1 с (порог p95 < 2 с);
|
||||
@@ -107,7 +109,9 @@ LDD-циклы выполнены 2026-10-27/28; последний — посл
|
||||
Kotlin-модуль, поэтому DTO с default-значениями требовали все поля JSON
|
||||
(например, создание студента без `firstName`/`basis` давало 400).
|
||||
|
||||
Остаточные ограничения: offset-пагинация аудита, N+1 в отдельных лабораторных
|
||||
списках/ведомости, отсутствие абсолютного лимита web-сессии. Legacy-активы,
|
||||
срок жизни JWT и «ведущие нули» больше не являются открытыми рисками.
|
||||
Остаточные ограничения: ручная инвентаризация legacy-активов на production,
|
||||
сдачи по выбранной лабораторной в ведомости, непроверенный первый CI-прогон на
|
||||
реальных runner'ах и отсутствие визуальной регрессии. Legacy-активы, срок
|
||||
access-токена, пагинация, N+1 деталей лабораторных, абсолютный лимит сессии и
|
||||
«ведущие нули» больше не являются открытыми рисками.
|
||||
Результаты — в `baseline-2026-10-27.md`.
|
||||
|
||||
@@ -7,13 +7,13 @@ Playwright. В ходе проверки найден и исправлен ре
|
||||
|
||||
## 1. Что проверялось
|
||||
|
||||
Реестр: `docs/evidence/claims.json`, **44 утверждения**.
|
||||
Реестр: `docs/evidence/claims.json`, **51 утверждение**.
|
||||
|
||||
| Критичность | Claims | Proven | Gaps |
|
||||
|---|---:|---:|---:|
|
||||
| P0 | 25 | 25 | 0 |
|
||||
| P1 | 21 | 21 | 0 |
|
||||
| **Итого** | **46** | **46** | **0** |
|
||||
| P1 | 26 | 26 | 0 |
|
||||
| **Итого** | **51** | **51** | **0** |
|
||||
|
||||
Домены:
|
||||
|
||||
@@ -23,12 +23,13 @@ Playwright. В ходе проверки найден и исправлен ре
|
||||
- жизненный цикл: академ, возврат, отчисление, архив, восстановление, вход;
|
||||
- профиль студента и скачивание сдач;
|
||||
- материалы и активы, включая secure-by-default legacy-файлы;
|
||||
- миграции V11 и V13, полный upgrade с master-схемы;
|
||||
- миграции V11–V13, полный upgrade с master-схемы;
|
||||
- аудит: токен, admin-only, сохранность после удаления, representative-хуки,
|
||||
пагинация;
|
||||
- аутентификация: короткий access-токен, ротация/отзыв refresh, web-сессия;
|
||||
- группы курса без N+1, авторство материалов, ведомость, UI/контракты, импорт,
|
||||
копирование лабораторных;
|
||||
keyset-пагинация;
|
||||
- аутентификация: короткий access-токен, ротация/отзыв refresh, web-сессия,
|
||||
абсолютный потолок 30 дней и очистка истёкших токенов;
|
||||
- группы курса и детали лабораторных без N+1, авторство материалов, ведомость,
|
||||
UI/контракты, импорт, копирование лабораторных;
|
||||
- runtime-smoke, масштабный load, mutation testing, браузерный E2E и CI smoke.
|
||||
|
||||
## 2. Команды и результаты
|
||||
@@ -39,7 +40,7 @@ Playwright. В ходе проверки найден и исправлен ре
|
||||
./gradlew test --rerun-tasks
|
||||
```
|
||||
|
||||
Результат: **319 тестов, 0 failures, 0 errors, 0 skipped**.
|
||||
Результат: **346 тестов, 0 failures, 0 errors, 0 skipped**.
|
||||
`ktlintCheck` и `detekt` — зелёные.
|
||||
|
||||
### 2.2. LDD gate
|
||||
@@ -48,7 +49,7 @@ Playwright. В ходе проверки найден и исправлен ре
|
||||
./gradlew evidenceCheck
|
||||
```
|
||||
|
||||
Результат: **46/46 claims proven, P0 gaps = 0**.
|
||||
Результат: **51/51 claims proven, P0 gaps = 0**.
|
||||
Отчёт: `build/reports/evidence/index.md`.
|
||||
|
||||
Gate устроен так:
|
||||
@@ -67,11 +68,13 @@ Gate устроен так:
|
||||
|
||||
Сценарий: одноразовая PostgreSQL 18, master identity `V1–V3`, academic
|
||||
`V1–V5`, progress `V1–V9,V11,V12`, синтетические данные, затем ветка:
|
||||
identity `V4–V11`, academic `V6–V12`, progress `V13`.
|
||||
identity `V4–V13`, academic `V6–V12`, progress `V13`.
|
||||
|
||||
Проверено:
|
||||
|
||||
- `identity.audit_events` (`V10`) и `identity.refresh_tokens` (`V11`) созданы;
|
||||
- `identity.audit_events` (`V10`), `identity.refresh_tokens` (`V11`),
|
||||
`absolute_expires_at` (`V12`) и keyset-индекс `audit_events_occurred_id_idx`
|
||||
(`V13`) созданы;
|
||||
- `students`, `groups`, `membership_history` сохранились;
|
||||
- `labs.course_id`, `chapters.course_id`, `article_assets` созданы;
|
||||
- `grade_comments.author_account_id`, `lab_grades.updated_by_account_id` созданы;
|
||||
@@ -117,7 +120,8 @@ identity `V4–V11`, academic `V6–V12`, progress `V13`.
|
||||
преподавателю (существование не раскрывается);
|
||||
- админ может скачать такой файл;
|
||||
- `GET /api/v1/content/legacy-assets` защищён `ROLE_ADMIN` и перечисляет
|
||||
несвязанные файлы для привязки или удаления;
|
||||
несвязанные файлы; админская страница `/admin/legacy-assets` и
|
||||
`scripts/legacy-assets.sh` дают тот же список для привязки или удаления;
|
||||
- связанный актив по-прежнему наследует видимость статьи → раздела → главы:
|
||||
публичная `ALL` — без входа, курсовая — только участникам курса, скрытая
|
||||
для студента — `404`.
|
||||
@@ -138,30 +142,44 @@ identity `V4–V11`, academic `V6–V12`, progress `V13`.
|
||||
токена отзывает все сессии аккаунта;
|
||||
- logout и смена пароля отзывают refresh; отчисленный, выпущенный, архивный
|
||||
студент, архивный аккаунт и закрытая группа не могут обновить сессию;
|
||||
- web-сессия — 7 дней sliding (`LMS_SESSION_TIMEOUT`), cookie `LMS_SESSION`
|
||||
(`HttpOnly`, `SameSite=Lax`, `Max-Age` перевыпускается на каждом ответе);
|
||||
- `V12__refresh_absolute.sql` задаёт `absolute_expires_at`; ротация не
|
||||
продлевает абсолютный срок, после 30 дней refresh отклоняется;
|
||||
- web-сессия — 7 дней sliding (`LMS_SESSION_TIMEOUT`), но не дольше 30 дней
|
||||
(`LMS_SESSION_ABSOLUTE_TIMEOUT`); cookie `LMS_SESSION`
|
||||
(`HttpOnly`, `SameSite=Lax`, `Max-Age` перевыпускается на каждом ответе и
|
||||
ограничен остатком абсолютного срока);
|
||||
- access обновляется автоматически за 2 минуты до истечения, поэтому активная
|
||||
вкладка не требует входа каждые 15 минут; logout отзывает refresh.
|
||||
вкладка не требует входа каждые 15 минут; logout отзывает refresh;
|
||||
- истёкшие refresh-токены старше retention (30 дней) удаляет ночная задача
|
||||
(`LMS_REFRESH_TOKEN_CLEANUP_CRON`), свежие отозванные токены сохраняются для
|
||||
detection reuse.
|
||||
|
||||
Прямой API-клиент должен сам использовать `POST /api/v1/auth/refresh`;
|
||||
автоматического обновления вне web BFF нет.
|
||||
|
||||
Результат: **PASS**, claims `LDD-AUTH-001`, `LDD-AUTH-002`, `LDD-AUTH-003`.
|
||||
Результат: **PASS**, claims `LDD-AUTH-001`, `LDD-AUTH-002`, `LDD-AUTH-003`,
|
||||
`LDD-AUTH-006`, `LDD-AUTH-007`.
|
||||
|
||||
### 2.7. Пагинация, N+1 и масштаб
|
||||
|
||||
- `GET /api/v1/audit` принимает `page` и `limit` (максимум 500); в UI есть
|
||||
«Показать ещё». Это offset-пагинация, а не keyset.
|
||||
- `GET /api/v1/audit` принимает keyset-курсор `cursor` (opaque `occurredAt|id`),
|
||||
`limit` (максимум 500) и `action`; offset `page` остался для совместимости.
|
||||
Курсорные страницы не пересекаются, битый курсор — `400`; в UI «Показать ещё»
|
||||
идёт по курсору, «В начало» возвращает к первой странице.
|
||||
- `GET /api/v1/labs/details` отдаёт все лабораторные курса с уровнями и
|
||||
вариантами одним запросом; progress делает ровно один HTTP-вызов вместо
|
||||
запроса на каждую лабораторную.
|
||||
- `GET /api/v1/courses/{id}/groups` возвращает привязанные группы с именами
|
||||
одним вызовом; страница преподавателя не делает N+1 по именам групп.
|
||||
- Полный load использует seed на **100 групп / 1000 студентов** (SQL, только
|
||||
release-контур) и **125 параллельных чтений** курса, лаб, профиля, групп и
|
||||
аудита: **0 ошибок, p95 < 1 с** (порог p95 < 2 с).
|
||||
|
||||
Остаются offset-пагинация аудита и N+1 в отдельных лабораторных
|
||||
списках/ведомости — это лабораторные детали, не продуктовые риски доступа.
|
||||
Остаётся запрос сдач по выбранной лабораторной в ведомости — это не N+1 по
|
||||
студентам и не продуктовый риск доступа.
|
||||
|
||||
Результат: **PASS**, claims `LDD-AUDIT-004`, `LDD-PERF-001`, `LDD-RUNTIME-003`.
|
||||
Результат: **PASS**, claims `LDD-AUDIT-004`, `LDD-AUDIT-005`, `LDD-PERF-001`,
|
||||
`LDD-PERF-002`, `LDD-RUNTIME-003`.
|
||||
|
||||
### 2.8. Mutation testing
|
||||
|
||||
@@ -187,16 +205,20 @@ PIT запускается через CLI с JUnit 4 harness: JUnit5-плаги
|
||||
./scripts/ldd-browser.sh
|
||||
```
|
||||
|
||||
Playwright/Chromium на полном стеке, одноразовая БД:
|
||||
Playwright в Chromium, Firefox и WebKit на полном стеке, одноразовая БД:
|
||||
|
||||
- admin: вход, `/admin/courses` с курсом, `/admin/audit` с событием;
|
||||
- teacher: вход, курс, ведомость (группа/студент, без админских кнопок),
|
||||
профиль студента, создание главы курса;
|
||||
- anonymous: `/admin/audit` → `/login`;
|
||||
- student: вход, `/admin/audit` → `/student`, `/learn/course/{no}`.
|
||||
- student: вход, `/admin/audit` → `/student`, `/learn/course/{no}`;
|
||||
- admin: cursor-пагинация аудита («Показать ещё» → «Продолжение» → «В начало»);
|
||||
- admin: страница `/admin/legacy-assets` со списком или пустым состоянием.
|
||||
|
||||
Результат: **4/4 passed**, evidence: `build/evidence/browser.json`.
|
||||
Дополнительно проверены сдача файла студентом и mobile-размер teacher-страницы.
|
||||
Результат: **18/18 passed** (6 сценариев × 3 браузера), evidence:
|
||||
`build/evidence/browser.json`. Дополнительно проверены сдача файла студентом и
|
||||
mobile-размер teacher-страницы. На Fedora WebKit запускается через Docker-образ
|
||||
Playwright, потому что системных Ubuntu-библиотек нет.
|
||||
|
||||
### 2.10. Конкурентные apply/rollback
|
||||
|
||||
@@ -215,8 +237,12 @@ mutation, runtime + scale и browser E2E, затем финальный `evidenc
|
||||
- `check` — `test ktlintCheck detekt evidenceCheck` на push/MR;
|
||||
- `smoke` — `ldd-smoke.sh` (полный стек, health, вход админа, один курс)
|
||||
на каждом push/MR;
|
||||
- `release-evidence` — полный `ldd-release.sh` с Chromium/Node: обязателен для
|
||||
тегов, для веток запускается вручную.
|
||||
- `release-evidence` — полный `ldd-release.sh` с Chromium/Firefox/WebKit и
|
||||
Node: обязателен для тегов, для веток запускается вручную.
|
||||
- smoke/release начинаются с `scripts/ci-preflight.sh`, который проверяет
|
||||
Java 21, Docker Compose v2, Python 3, Node и Playwright.
|
||||
- Первый реальный прогон на GitLab shell-runners ещё не подтверждён; локальные
|
||||
скрипты и конфигурация проверены.
|
||||
|
||||
### 2.12. Найденный дефект
|
||||
|
||||
@@ -240,9 +266,12 @@ optional json fields`.
|
||||
- Legacy-актив без привязки не утекает обычным пользователям, а админ может
|
||||
провести инвентаризацию.
|
||||
- Refresh-токены ротируются и отзываются; закрытый статус блокирует продление
|
||||
сессии, а web-сессия переживает 15-минутный access-токен.
|
||||
- Аудит листается по страницам, а группы курса отдаются одним вызовом без N+1.
|
||||
- Аудит пишется и читается; upgrade не теряет синтетические данные.
|
||||
сессии, абсолютный потолок 30 дней ограничивает сессию, а web-сессия
|
||||
переживает 15-минутный access-токен.
|
||||
- Аудит листается keyset-курсором, детали лабораторных и группы курса
|
||||
отдаются пакетно без N+1.
|
||||
- Аудит пишется и читается; upgrade не теряет синтетические данные;
|
||||
истёкшие refresh-токены очищаются.
|
||||
- Мутационная прочность критичных компонентов измерена, а не заявлена.
|
||||
- Масштабный load на 100 группах / 1000 студентов проходит без ошибок.
|
||||
- Реальный дефект DTO/Jackson найден именно E2E-контуром и закрыт тестом.
|
||||
@@ -254,35 +283,36 @@ optional json fields`.
|
||||
2. **Property-based тесты покрывают не все ветки.** Есть генераторы для
|
||||
матрицы доступа, планировщика и 10 транзакционных apply/rollback-кейсов,
|
||||
но не для всех сочетаний полей и распределённой нагрузки.
|
||||
3. **Браузерный E2E — один Chromium и 4 сценария.** Нет визуальной регрессии,
|
||||
Safari/Firefox и полного мобильного набора страниц.
|
||||
3. **Браузерный E2E — 3 браузера, 6 сценариев.** Нет визуальной регрессии и
|
||||
полного мобильного набора страниц; WebKit на Fedora требует Docker-образа.
|
||||
4. **Нагрузочный smoke — не benchmark.** Одна машина, синтетические данные,
|
||||
без SLA production; 125 чтений и p95 < 1 с — ориентир, а не гарантия.
|
||||
5. **Offset-пагинация аудита.** Вставки между страницами могут сдвигать
|
||||
выдачу; keyset-курсор не реализован.
|
||||
6. **N+1 в отдельных лабораторных списках/ведомости.** Имена групп курса и
|
||||
аудит исправлены, но не все большие списки собраны пакетно.
|
||||
7. **Нет абсолютного лимита web-сессии.** Sliding-сессия может продлеваться
|
||||
бесконечно при непрерывной активности; отзыв по статусу при этом работает.
|
||||
5. **Сдачи по выбранной лабораторной в ведомости** запрашиваются отдельно;
|
||||
это не N+1 по студентам, но и не пакетная выборка.
|
||||
6. **Инвентаризация legacy-активов на production не выполнена.** UI, API и
|
||||
скрипт готовы, но реальные файлы нужно привязать или удалить вручную.
|
||||
7. **Первый CI-прогон не подтверждён.** Конфигурация и preflight готовы, но
|
||||
наличие shell-runners `lms-shell`/`lms-release` проверяется в GitLab.
|
||||
8. **Полный release-evidence не на каждом коммите.** На push/MR идёт smoke,
|
||||
а mutation/browser/scale — для тегов или вручную.
|
||||
9. **Внешняя интеграция «Контингент» не проверялась** — она остаётся заделом.
|
||||
9. **Интеграция «Контингент» исключена из плана** — без нового ТЗ и доступа к
|
||||
API она не проверялась и не реализуется.
|
||||
10. **Номер группы числовой.** Отдельного требования к ведущим нулям нет;
|
||||
`01` и `1` — одно значение, это не открытый риск.
|
||||
|
||||
## 5. Вывод
|
||||
|
||||
Текущая функциональность прошла LDD-контур: **46/46 claims proven, P0 gaps = 0**,
|
||||
**319 тестов**, upgrade, mutation baseline, browser E2E, smoke и масштабный load
|
||||
зелёные. Это достаточное основание продолжать доработки, но не «формальное
|
||||
доказательство всей системы».
|
||||
Текущая функциональность прошла LDD-контур: **51/51 claims proven, P0 gaps = 0**,
|
||||
**346 тестов**, upgrade, mutation baseline, 18/18 browser E2E, smoke и
|
||||
масштабный load зелёные. Это достаточное основание продолжать доработки, но не
|
||||
«формальное доказательство всей системы».
|
||||
|
||||
Следующий цикл LDD:
|
||||
|
||||
1. почистить инвентарь legacy-активов через `GET /api/v1/content/legacy-assets`
|
||||
(файлы уже не публичны, это гигиена хранилища);
|
||||
2. добавить keyset-пагинацию аудита и убрать оставшийся N+1 в лабораторных
|
||||
списках/ведомости;
|
||||
3. при необходимости ввести абсолютный лимит web-сессии;
|
||||
4. расширить browser E2E на публикацию оценки и остальные мобильные страницы;
|
||||
5. подготовить внешнюю интеграцию «Контингент»/`next.uniyar.ac.ru`.
|
||||
1. прогнать инвентаризацию legacy-активов на production/staging через
|
||||
`/admin/legacy-assets` или `scripts/legacy-assets.sh` и привязать/удалить
|
||||
найденные файлы;
|
||||
2. подтвердить первый реальный CI-прогон на shell-runners и tag-job;
|
||||
3. при необходимости расширить E2E на визуальную регрессию и остальные
|
||||
мобильные страницы;
|
||||
4. пакетно отдавать сдачи в ведомости, если появятся медленные курсы.
|
||||
|
||||
@@ -435,7 +435,7 @@
|
||||
"criticality": "P1",
|
||||
"status": "active",
|
||||
"requirement": "docs/16-stage6-report.md, §2.2",
|
||||
"claim": "Схема master + синтетические данные → миграции identity V4–V11, academic V6–V12, progress V13; V10 audit и V11 refresh создаются, V13 идемпотентна.",
|
||||
"claim": "Схема master + синтетические данные → миграции identity V4–V13, academic V6–V12, progress V13; V10 audit, V11 refresh, V12 absolute-expiry и V13 keyset-индексы создаются, V13 идемпотентна.",
|
||||
"oracle": "Успешное применение и postflight-наличие таблиц/колонок/индексов",
|
||||
"runtime": [
|
||||
"migration.master_upgrade"
|
||||
@@ -488,16 +488,16 @@
|
||||
},
|
||||
{
|
||||
"id": "LDD-BROWSER-001",
|
||||
"title": "Браузерный E2E админа, преподавателя и студента, включая сдачу и mobile",
|
||||
"title": "Браузерный E2E в Chromium, Firefox и WebKit",
|
||||
"criticality": "P1",
|
||||
"status": "active",
|
||||
"requirement": "docs/evidence/README.md",
|
||||
"claim": "Playwright на реальном стеке: admin видит курс и аудит; teacher открывает курс/ведомость/профиль, создаёт главу и видит mobile-вид; student сдаёт файл; аноним и студент не попадают в админку.",
|
||||
"claim": "6 сценариев (админ, преподаватель, студент/аноним, сдача+mobile, cursor-аудит, инвентаризация активов) проходят в Chromium, Firefox и WebKit: 18/18 без unexpected.",
|
||||
"oracle": "Playwright assertions на DOM/URL",
|
||||
"runtime": [
|
||||
"browser.e2e"
|
||||
],
|
||||
"limitations": "4 сценария, один Chromium; нет визуальной регрессии и Safari/Firefox."
|
||||
"limitations": "Нет визуальной регрессии и полного набора мобильных страниц; WebKit на Fedora запускается через Docker-образ Playwright."
|
||||
},
|
||||
{
|
||||
"id": "LDD-RUNTIME-002",
|
||||
@@ -613,7 +613,7 @@
|
||||
"ru.ysu.lms.web.session.SessionCookieRefreshFilterTest#refreshes cookie max age on every existing session()",
|
||||
"ru.ysu.lms.web.auth.AuthPageControllerTest#logout revokes refresh token and invalidates session()"
|
||||
],
|
||||
"limitations": "Cookie и серверный timeout оба 7 дней; абсолютного верхнего предела сессии нет."
|
||||
"limitations": "Cookie и серверный timeout 7 дней; поверх них действует абсолютный потолок 30 дней (LDD-AUTH-006)."
|
||||
},
|
||||
{
|
||||
"id": "LDD-AUDIT-004",
|
||||
@@ -621,13 +621,13 @@
|
||||
"criticality": "P1",
|
||||
"status": "active",
|
||||
"requirement": "docs/evidence/README.md",
|
||||
"claim": "Audit API отдаёт страницы limit/page без пересечений и с устойчивым порядком; UI показывает «Показать ещё».",
|
||||
"claim": "Audit API поддерживает offset-страницы `page`/`limit` для совместимости; UI по умолчанию продолжает журнал keyset-курсором.",
|
||||
"oracle": "Страницы AuditIntegrationTest и ссылки шаблона",
|
||||
"evidence": [
|
||||
"ru.ysu.lms.identity.audit.AuditIntegrationTest#pagination returns disjoint pages newest first()",
|
||||
"ru.ysu.lms.web.admin.AdminAuditTemplateTest#renders audit events with labels and filter()"
|
||||
],
|
||||
"limitations": "Offset-пагинация, не keyset; при частых вставках возможны сдвиги."
|
||||
"limitations": "Offset-режим остаётся только для старых ссылок; основной путь — курсор (LDD-AUDIT-005)."
|
||||
},
|
||||
{
|
||||
"id": "LDD-PERF-001",
|
||||
@@ -641,7 +641,7 @@
|
||||
"ru.ysu.lms.academic.course.CourseGroupTeacherAccessTest#teacher gets attached groups with names in one call()",
|
||||
"ru.ysu.lms.web.teacher.TeacherControllerCourseGroupsTest#loads group names with a single call instead of per-group requests()"
|
||||
],
|
||||
"limitations": "Детали лабораторных профиля всё ещё подтягиваются по одной; это следующий шаг."
|
||||
"limitations": "Детали лабораторных теперь пакетные (LDD-PERF-002); отдельные запросы на лабораторную в ведомости ещё остаются."
|
||||
},
|
||||
{
|
||||
"id": "LDD-AUTH-004",
|
||||
@@ -669,6 +669,84 @@
|
||||
"runtime.auth_refresh"
|
||||
],
|
||||
"limitations": "Проверено на одноразовом стеке; TTL не истекает в рамках smoke."
|
||||
},
|
||||
{
|
||||
"id": "LDD-AUTH-006",
|
||||
"title": "Абсолютный потолок сессии 30 дней",
|
||||
"criticality": "P1",
|
||||
"status": "active",
|
||||
"requirement": "docs/16-stage6-report.md, §4",
|
||||
"claim": "Ротация refresh не продлевает absolute_expires_at; после 30 дней refresh отклоняется, а web-сессия принудительно завершается и cookie не живёт дольше остатка.",
|
||||
"oracle": "absolute_expires_at в цепочке ротаций, 401 на refresh, redirect /login?expired=absolute, Max-Age cookie",
|
||||
"evidence": [
|
||||
"ru.ysu.lms.identity.RefreshTokenIntegrationTest#refresh past the absolute session expiry is rejected with unauthorized()",
|
||||
"ru.ysu.lms.identity.RefreshTokenIntegrationTest#rotation keeps the absolute session expiry unchanged()",
|
||||
"ru.ysu.lms.web.session.AuthInterceptorTest#absolute expired session redirects to absolute login without identity call()",
|
||||
"ru.ysu.lms.web.session.SessionCookieRefreshFilterTest#cookie max age is capped by remaining absolute lifetime()",
|
||||
"ru.ysu.lms.web.session.SessionCookieRefreshFilterTest#absolute expired session receives zero max age cookie()",
|
||||
"ru.ysu.lms.web.auth.AuthPageControllerTest#login stores authenticated at timestamp()"
|
||||
],
|
||||
"limitations": "Срок настраивается LMS_SESSION_ABSOLUTE_TIMEOUT; абсолютный отсчёт начинается с логина, а не с создания web-сессии."
|
||||
},
|
||||
{
|
||||
"id": "LDD-AUTH-007",
|
||||
"title": "Очистка истёкших refresh-токенов",
|
||||
"criticality": "P1",
|
||||
"status": "active",
|
||||
"requirement": "docs/16-stage6-report.md, §4",
|
||||
"claim": "Ночная очистка удаляет токены, у которых sliding- или absolute-срок истёк раньше retention (30 дней), и сохраняет свежие/недавно отозванные токены для detection reuse.",
|
||||
"oracle": "cleanupExpired(now) и количество удалённых записей",
|
||||
"evidence": [
|
||||
"ru.ysu.lms.identity.RefreshTokenIntegrationTest#cleanup deletes only tokens expired beyond retention()"
|
||||
],
|
||||
"limitations": "Расписание задаётся LMS_REFRESH_TOKEN_CLEANUP_CRON; в тесте вызывается метод напрямую, без ожидания cron."
|
||||
},
|
||||
{
|
||||
"id": "LDD-AUDIT-005",
|
||||
"title": "Keyset-пагинация аудита",
|
||||
"criticality": "P1",
|
||||
"status": "active",
|
||||
"requirement": "docs/16-stage6-report.md, §2.7",
|
||||
"claim": "Audit API принимает непрозрачный cursor (occurredAt|id), отдаёт строго следующие записи без пересечений и сохраняет фильтр по action; битый курсор — 400; UI ведёт «Показать ещё» по курсору.",
|
||||
"oracle": "Страницы AuditIntegrationTest и cursor-ссылка шаблона",
|
||||
"evidence": [
|
||||
"ru.ysu.lms.identity.audit.AuditIntegrationTest#cursor pagination walks all events newest first()",
|
||||
"ru.ysu.lms.identity.audit.AuditIntegrationTest#cursor pagination respects the action filter()",
|
||||
"ru.ysu.lms.identity.audit.AuditIntegrationTest#malformed audit cursor is rejected with bad request()",
|
||||
"ru.ysu.lms.identity.audit.AuditIntegrationTest#cursor from the newest event returns an empty page()",
|
||||
"ru.ysu.lms.web.admin.AdminAuditTemplateTest#cursor mode links next page with cursor and resets to start()"
|
||||
],
|
||||
"limitations": "Offset page остаётся для обратной совместимости; keyset-курсор не подписан, но не содержит секретов."
|
||||
},
|
||||
{
|
||||
"id": "LDD-PERF-002",
|
||||
"title": "Пакетные детали лабораторных без N+1",
|
||||
"criticality": "P1",
|
||||
"status": "active",
|
||||
"requirement": "docs/16-stage6-report.md, §2.7",
|
||||
"claim": "Academic отдаёт все лабораторные курса с уровнями и вариантами одним запросом /api/v1/labs/details; progress делает ровно один HTTP-вызов вместо запроса на каждую лабораторную.",
|
||||
"oracle": "LabDetailsIntegrationTest и счётчик запросов AcademicClientBatchLabsTest",
|
||||
"evidence": [
|
||||
"ru.ysu.lms.academic.lab.LabDetailsIntegrationTest#teacher gets all course labs with levels and variants in one call()",
|
||||
"ru.ysu.lms.academic.lab.LabDetailsIntegrationTest#student of the course receives lab details()",
|
||||
"ru.ysu.lms.academic.lab.LabDetailsIntegrationTest#foreign teacher is forbidden for lab details()",
|
||||
"ru.ysu.lms.progress.AcademicClientBatchLabsTest#getCourseLabs loads all labs with levels and variants in one request()"
|
||||
],
|
||||
"limitations": "Ведомость всё ещё запрашивает сдачи по выбранной лабораторной; это не N+1 по студентам."
|
||||
},
|
||||
{
|
||||
"id": "LDD-ASSET-003",
|
||||
"title": "Админская инвентаризация legacy-активов",
|
||||
"criticality": "P1",
|
||||
"status": "active",
|
||||
"requirement": "docs/15-stage5-course-materials.md",
|
||||
"claim": "Админ открывает /admin/legacy-assets, видит список файлов без статьи, ссылку на файл и пустое состояние; страница не показывает ошибку при пустом списке.",
|
||||
"oracle": "LegacyAssetsTemplateTest и HTTP-страница",
|
||||
"evidence": [
|
||||
"ru.ysu.lms.web.admin.LegacyAssetsTemplateTest#renders orphan assets with links and admin-only explanation()",
|
||||
"ru.ysu.lms.web.admin.LegacyAssetsTemplateTest#renders empty legacy assets state()"
|
||||
],
|
||||
"limitations": "Удаление файлов остаётся ручной операцией на стороне academic-service; UI только инвентаризирует."
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user