diff --git a/README.md b/README.md index 8267943..433ea26 100644 --- a/README.md +++ b/README.md @@ -111,6 +111,19 @@ cp .env.prod.example .env.prod # заполнить LMS_DB_PASSWORD существующие лабораторные, главы и активные группы в курс «Промышленное программирование на языке C». Лабораторные привязаны к курсу: создание и импорт требуют выбора курса, лабу можно скопировать в другой курс. +- **Преподаватели и доступ**: `/admin/teachers` создаёт аккаунты `TEACHER` + с одноразовым паролем; преподаватель видит только назначенные курсы + (`/teacher`), их группы, студентов и профили. Курсы и лабораторные + фильтруются по роли, внутренние вызовы identity защищены `LMS_INTERNAL_TOKEN`. +- **Профиль студента курса**: `/teacher/courses/{id}/students/{id}` и админский + маршрут показывают сводку, оценки, комментарии с авторами, защиты и все + попытки сдачи со скачиванием ZIP; студенту API отдаёт только его профиль + и опубликованные данные. +- **Повышение и жизненный цикл**: `/admin/promotion` — dry-run, применение + и откат повышения (автоматический dry-run 31 августа МСК); на странице + группы — академотпуск, возврат, отчисление, архив и восстановление; + архив группы и подтверждение удаления логином/ФИО. Вход закрыт для + выпущенных и архивных групп. - **Лабораторные**: импорт ZIP (`lab.yaml` + `README.md` + `variants/*.md` + `assets/`); создание через UI с произвольным числом уровней; единая форма настроек с одной кнопкой «Сохранить всё»; статусы draft/published diff --git a/deploy/DEPLOY.md b/deploy/DEPLOY.md index 6393485..50242c7 100644 --- a/deploy/DEPLOY.md +++ b/deploy/DEPLOY.md @@ -47,7 +47,15 @@ cp .env.prod.example .env.prod nano .env.prod ``` -Обязательно задайте надёжный `LMS_DB_PASSWORD`. Остальные значения по умолчанию подходят. +Обязательно задайте `LMS_DB_PASSWORD` и `LMS_INTERNAL_TOKEN`. Остальные +значения по умолчанию подходят. + +`LMS_INTERNAL_TOKEN` — общий секрет для внутренних вызовов identity из +academic/progress (профиль студента, состав группы). Сгенерируйте длинную +случайную строку (например, `openssl rand -hex 32`) и укажите её в `.env.prod`. +Compose передаёт одно и то же значение identity, academic и progress. +Значение по умолчанию `dev-internal-token` для production не используйте; +`.env.prod` не коммитьте. ## 4. Собрать и запустить @@ -148,6 +156,9 @@ Backup на том же сервере — не единственная защ JWKS: должно быть `LMS_JWT_JWK_SET_URI=http://identity:8081/api/v1/auth/jwks.json` (уже прописано в `docker-compose.prod.yml` для academic/progress/gateway). Внутри контейнера `localhost:8081` недоступен, поэтому токен не проверяется. +- **403/502 в профиле студента или составе группы** — проверьте, что + `LMS_INTERNAL_TOKEN` задан в `.env.prod` и одинаков у identity, academic + и progress. При неверном внутреннем токене identity отвечает `403`. - **Не проходят загрузки больших ZIP** — увеличьте `client_max_body_size` в nginx (сейчас 120 МБ) и `spring.servlet.multipart.max-request-size`. diff --git a/docs/08-groups-courses-plan.md b/docs/08-groups-courses-plan.md index 089c69f..b3fd3eb 100644 --- a/docs/08-groups-courses-plan.md +++ b/docs/08-groups-courses-plan.md @@ -1,6 +1,6 @@ # 08. План реализации: группы, курсы, преподаватели -**Статус:** план согласован, реализация по этапам 0–6, один релиз. +**Статус:** план согласован, этапы 0–4 выполнены, этапы 5–6 — в работе. **Основа:** [07-groups-courses.md](07-groups-courses.md). **Дата решений:** 2026-10-23. @@ -303,10 +303,20 @@ identity V9, **academic V10**, progress V13 (+V14 при необходимос - **Этап 2 — выполнен.** Курсы и `course_no`, история преподавателей и групп, bootstrap курса C, привязка лабораторных к курсу, копирование лаб, админ-UI курсов и лабораторных. Отчёт: [12-stage2-report.md](12-stage2-report.md). -- **Этап 3 — следующий.** Доступ и профиль: `TEACHER` в security-конфигах, - внутренние access-эндпоинты, проверки курса/группы/студента, профиль студента - курса, материалы курса и аудитории, активы, кабинет преподавателя. -- Этапы 4–6 — по плану. +- **Этап 3 — выполнен.** Аккаунты преподавателей, курсовая область видимости, + внутренние API identity с `LMS_INTERNAL_TOKEN`, кабинет и состав групп + преподавателя, профиль студента курса и скачивание всех попыток. + Отчёт: [13-stage3-report.md](13-stage3-report.md). +- **Этап 4 — выполнен.** Повышение (dry-run/apply/rollback, планировщик + 31 августа МСК и догон), жизненный цикл студента, архив группы, + усиленное подтверждение удаления, блокировка входа для архивных + и выпущенных групп. Отчёт: [14-stage4-report.md](14-stage4-report.md). +- **Этап 5 — остаётся.** UX и контент: студенческие страницы курсов, + материалы по `/learn/course/{course_no}/...` и аудитории, наследуемая + видимость активов, публичные материалы `ALL`. +- **Этап 6 — остаётся.** Hardening: полная security-матрица, + upgrade-тест миграций, аудит, индексы/пагинация, backup/restore, + документация и smoke. ## 10. Definition of Done diff --git a/docs/13-stage3-report.md b/docs/13-stage3-report.md new file mode 100644 index 0000000..e29a203 --- /dev/null +++ b/docs/13-stage3-report.md @@ -0,0 +1,172 @@ +# 13. Отчёт этапа 3: доступ и профиль студента курса + +**Дата:** 2026-10-27. +**Статус:** этап 3 выполнен; этап 4 (повышение и жизненный цикл) — следующий. + +## 1. Что сделано + +### Аккаунты преподавателей + +- Добавлен административный API `/api/v1/teachers`: список, создание, + сброс пароля. Создаётся только аккаунт роли `TEACHER`. +- Логин проверяется на длину (минимум 3 символа) и уникальность; пустой пароль + заменяется сгенерированным. Новый пароль помечается как временный + (`mustChangePassword`), показывается один раз. +- В админке появилась страница `/admin/teachers` с созданием преподавателя + и сбросом пароля; одноразовый пароль выводится во всплывающем уведомлении. + +### Курсовая область видимости + +- `CourseAccessService` в academic-service определяет доступные курсы по роли: + - `ADMIN` — все курсы, включая архивные; + - `TEACHER` — только активные курсы с активным назначением + `course_teachers` (`removed_at IS NULL`); + - `STUDENT` — только активные курсы активных групп, в которых студент + состоит сейчас (группы запрашиваются у identity-service); + - анонимный пользователь до сервиса не доходит — gateway требует JWT. +- `GET /api/v1/courses/mine` возвращает курсы текущего пользователя. +- `GET /api/v1/courses/{id}` проверяет доступ к курсу. Админские операции + создания, переименования, архивирования, назначения преподавателей + и привязки групп остались только для `ADMIN`. +- Список лабораторных без `courseId` фильтруется по доступным курсам; + с `courseId` доступ проверяется явно. Карточка лабораторной тоже проверяет + доступ через её курс. Лабораторная без курса доступна только админу. +- Недоступный курс/лабораторная дают `403`, несуществующая — `404`. + Недоступность identity при проверке групп даёт `502`, а не `403`. + +### Внутренние API identity + +Добавлены read-only эндпоинты для межсервисных вызовов: + +| Метод | Путь | Назначение | +|---|---|---| +| `GET` | `/api/v1/internal/students/{studentId}/context` | карточка студента: ФИО, №, статус, год набора, текущая группа | +| `POST` | `/api/v1/internal/accounts/lookup` | пакетные логины авторов комментариев и оценок | +| `GET` | `/api/v1/internal/groups/{groupId}` | группа и текущий состав без учётных данных | + +Эндпоинты защищены заголовком `X-Internal-Token`; сравнение токена — +constant-time. Gateway не маршрутизирует `/api/v1/internal/**`, поэтому снаружи +эти пути недоступны. + +### Кабинет преподавателя + +- `/teacher` — список назначенных курсов с количеством групп и лабораторных. +- `/teacher/courses/{id}` — карточка курса: привязанные группы и лабораторные + в режиме просмотра. Редактирование курса и лабораторных остаётся у админа. +- `/teacher/courses/{courseId}/groups/{groupId}` — состав группы, привязанной + к курсу, со ссылками на профиль студента. +- Доступ к `/teacher/**` разрешён роли `TEACHER` и админу; студент + перенаправляется в свой кабинет, аноним — на вход. После входа преподаватель + попадает на `/teacher`. + +### Профиль студента курса + +- Progress-service собирает агрегированный профиль одним запросом: + `GET /api/v1/courses/{courseId}/students/{studentId}/profile`. +- В ответе: курс, карточка студента, сводка и полный список лабораторных курса. +- Для каждой лабораторной: вариант и уровень, число попыток, дата последней + сдачи, оценка и её публикация, общий комментарий, комментарии преподавателей, + защита (статус, код, задание, остаток времени), все попытки с файлами + и ссылкой на скачивание. +- Скачивание любой попытки: + `GET /api/v1/courses/{courseId}/submissions/{submissionId}/download` + (ZIP). Проверяется, что сдача относится к лабораторной этого курса; + чужой `submissionId` и чужой курс отклоняются. +- BFF-страницы профиля: + - `/teacher/courses/{courseId}/students/{studentId}`; + - `/admin/courses/{courseId}/students/{studentId}`. +- Авторство: миграция `V13__profile_authors.sql` добавляет + `grade_comments.author_account_id` и `lab_grades.updated_by_account_id`; + новые комментарии и оценки сохраняют автора. Старые записи показываются + без автора. Индекс `lab_grades(lab_id)` ускоряет выборку по списку лаб. +- В ведомости группы ФИО студента и кнопка «Профиль» ведут на профиль, если + группа открыта из карточки курса. На странице группы преподавателя те же + ссылки доступны из состава группы. + +## 2. Правила доступа + +| Объект | ADMIN | TEACHER | STUDENT | +|---|---|---|---| +| Курс | любой, включая архивный | только активный назначенный | только активный курс текущей группы | +| Лабораторные курса | все | все курса, включая черновики | все лабораторные доступного курса | +| Состав группы курса | любая привязанная группа | группа должна быть активной привязкой курса | доступ к курсу по своей группе | +| Профиль студента | любой студент любого курса | студент должен состоять в активной группе курса | только свой профиль | +| Оценки и комментарии | все | все, включая неопубликованные | только опубликованные | +| Скачивание попытки | любая попытка курса | только студент из активной группы курса | отдельный download-эндпоинт недоступен | +| Внутренние API identity | по сервисному токену | по сервисному токену | по сервисному токену | + +Дополнительно: + +- Преподаватель чужого курса получает `403`; студент чужого профиля — `403`. +- Студент видит только опубликованные оценки и комментарии. Черновики + и неопубликованные комментарии скрыты. +- Профиль не даёт доступа к другим курсам студента. +- Автор комментария/оценки показывается логином; старые записи без автора — + «—». Логины подтягиваются одним пакетным запросом. + +## 3. Токен и конфигурация + +- Общий секрет: `LMS_INTERNAL_TOKEN`. +- Где задаётся: `lms.internal.token` в `application.yml` identity, academic + и progress. Локально допускается значение по умолчанию + `dev-internal-token`; в production оно обязательно заменяется. +- `docker-compose.prod.yml` передаёт токен в identity, academic и progress; + `.env.prod.example` содержит заготовку без секрета. +- Заголовок: `X-Internal-Token`. Пустой или неверный токен — `403`. +- Gateway не публикует внутренние пути; вызовы идут напрямую между сервисами + по внутренней сети Docker. + +## 4. UI + +- `/admin/teachers` — создание преподавателя, список, сброс пароля, + отображение временного пароля один раз. +- `/teacher` — «Мои курсы». +- `/teacher/courses/{id}` — группы и лабораторные курса, переход к составу + группы. +- `/teacher/courses/{courseId}/groups/{groupId}` — студенты группы, ФИО + и кнопка «Открыть» ведут в профиль. +- `/teacher/courses/{courseId}/students/{studentId}` и + `/admin/courses/{courseId}/students/{studentId}` — единый шаблон профиля + с разными маршрутами скачивания и возврата. +- В навигации: «Преподаватели» у админа, «Мои курсы» у преподавателя. +- Профиль показывает сводку (лабораторных, сдано, оценено, средний балл, + защиты), каждую лабораторную, комментарии, защиты и все попытки. + +## 5. Тесты + +Добавлены и расширены наборы: + +- identity: `InternalControllerIntegrationTest` (12) — контекст студента, + пакетные логины, состав группы, неверный/пустой токен, неизвестные id; + `TeacherIntegrationTest` (6) — создание, генерация пароля, список, + сброс пароля, запрет для не-админа. +- academic: `CourseAccessServiceTest` (16) — роли, чужие/архивные курсы, + снятые назначения, студенты без групп, `mine`, доступ к лабораторным; + `CourseRosterServiceTest` (6) — состав группы, чужая группа/преподаватель, + недоступность identity. +- progress: `StudentProfileIntegrationTest` (5) — неопубликованные данные + для преподавателя, только опубликованное для студента, чужой курс/студент, + скачивание чужой сдачи. +- web: `StudentProfileTemplateTest`, `ProfileDtoJsonTest`, + `TeacherGroupTemplateTest`, `TeacherInterceptorTest`, + `CourseGroupRosterDtoTest` — контракты, шаблоны, роли и ссылки. + +По результатам последнего прогона (2026-10-27) перечисленные наборы прошли +с `0 failures / 0 errors`. + +## 6. Известные пробелы + +- **Контент курса.** URL материалов вида `/learn/course/{course_no}/...`, + проверка `chapters.course_id/audience`, наследуемая видимость активов + и выдача файлов по курсу ещё не реализованы. Схема подготовлена на этапе 2. +- **Права преподавателя.** Кабинет преподавателя — просмотр: курс, группы, + состав, лабораторные и профили. Редактирование лабораторных, оценок + и раздачи вариантов из кабинета не добавлялось. +- **Названия групп в кабинете.** На странице курса преподавателя группа + показывается UUID: academic отдаёт `groupId`, а имя группы не входит + в контракт карточки курса. Нужен batch-запрос имён или расширение DTO. +- **Собственный профиль студента.** API допускает `STUDENT` только для своего + профиля, но отдельной студенческой BFF-страницы профиля нет; текущие + страницы профиля — преподавательская и админская. +- **Аудит скачиваний.** Скачивание попыток пока не пишется в аудит; + это этап 6. diff --git a/docs/14-stage4-report.md b/docs/14-stage4-report.md new file mode 100644 index 0000000..1915693 --- /dev/null +++ b/docs/14-stage4-report.md @@ -0,0 +1,174 @@ +# 14. Отчёт этапа 4: повышение и жизненный цикл + +**Дата:** 2026-10-27. +**Статус:** этап 4 выполнен; этап 5 (UX и контент курса) — следующий. + +## 1. Повышение + +### Алгоритм + +План строится по активным группам и студентам: + +- Активная группа с курсом меньше предела (`БО` — 4, `МО` — 2) переводится + на следующий курс: меняются `course` и имя по шаблону + `поток-курсномерстепень`. +- Активная группа на последнем курсе получает статус `GRADUATED`. +- Активные студенты выпускаемой группы получают `GRADUATED`. +- Студенты в академическом отпуске переводятся в параллель следующего набора + того же курса: `enrollmentYear = год повышения - (курс - 1)`. + Если такой группы нет, она создаётся в плане; при применении — в БД. +- Студенты в статусах `EXPELLED`, `GRADUATED`, `ARCHIVED` в план не попадают. +- Группы без курса/степени/потока/номера и студенты без зачисления + пропускаются с предупреждением; план при этом не блокируется. + +Порядок применения: сначала группы (от старших курсов), затем студенты. +Каждый элемент хранит `before`/`after` в JSONB. Перед применением все `before` +сверяются с текущим состоянием; при расхождении вся транзакция отменяется +с `409`, частичного применения нет. + +### API + +Все эндпоинты требуют роль `ADMIN`: + +| Метод | Путь | Назначение | +|---|---|---| +| `GET` | `/api/v1/promotion` | список запусков | +| `GET` | `/api/v1/promotion/{id}` | план/результат с элементами | +| `POST` | `/api/v1/promotion/dry-run` | построить план; тело `{ "year": 2026 }` необязательно | +| `POST` | `/api/v1/promotion/{id}/apply` | применить запланированный dry-run | +| `POST` | `/api/v1/promotion/{id}/rollback` | откатить завершённое применение | + +Правила: + +- `year` — 2000–2100; без года берётся текущий. +- Повторный dry-run за год возвращает существующий план в статусе `PLANNED`. +- Применять можно только `DRY_RUN` в статусе `PLANNED`. Запуск переходит + в `APPLY/RUNNING`, затем `COMPLETED`. +- Один успешный `APPLY` на год защищён частичным уникальным индексом + `promotion_runs(year) WHERE mode = 'APPLY' AND status IN ('RUNNING','COMPLETED')`; + конкурентное применение даёт `409`. +- Повторное применение уже завершённого запуска идемпотентно возвращает + результат и не создаёт новых записей истории. +- Откат доступен только для `COMPLETED`. Перед откатом проверяется, что + текущее состояние совпадает с `after`; ручные изменения дают `409`. + Восстанавливаются курс, имя и статус групп, статусы/группы студентов, + удаляются созданные планом пустые целевые группы. История не стирается: + добавляются записи `RETURN` с причиной «Откат повышения». + +### Планировщик + +- `PromotionScheduler` строит dry-run 31 августа в `00:00` по + `Europe/Moscow` (`cron = "0 0 0 31 8 *"`). +- При старте сервиса выполняется догон: если сегодня 31 августа или позже + и за год ещё нет запусков, dry-run строится сразу. +- Догон идемпотентен: advisory lock не даёт двум инстансам создать два плана. +- Планировщик включён по умолчанию; отключается + `lms.promotion.scheduler-enabled=false` (используется в тестах). +- Автоматически применяется только dry-run. Применение — всегда вручную + админом после проверки плана. + +## 2. Жизненный цикл студента + +Эндпоинты требуют роль `ADMIN`: + +| Метод | Путь | Тело | Результат | +|---|---|---|---| +| `POST` | `/api/v1/students/{id}/academic-leave` | `reason?` | перевод в параллель следующего набора, статус `ACADEMIC_LEAVE` | +| `POST` | `/api/v1/students/{id}/return` | `groupId`, `reason?` | возврат в указанную активную группу, статус `ACTIVE` | +| `POST` | `/api/v1/students/{id}/expel` | `reason?` | статус `EXPELLED`, зачисление сохраняется | +| `POST` | `/api/v1/students/{id}/archive` | — | статус студента `ARCHIVED`, аккаунт `ARCHIVED` | +| `POST` | `/api/v1/students/{id}/restore` | `groupId`, `reason?` | восстановление из архива/отчисления в активную группу | + +Правила: + +- Академотпуск доступен только `ACTIVE`; группа должна иметь заполненные + поток, курс, номер и степень. Целевая параллель ищется по когорте + и создаётся при отсутствии. +- Возврат доступен только из `ACADEMIC_LEAVE`; целевая группа должна быть + активной. +- Отчислить нельзя уже отчисленного, выпущенного или архивного студента. + Зачисление остаётся для истории, вход закрывает статус. +- Архив закрывает и студента, и аккаунт (`archived_at`); зачисление + сохраняется. Восстановление возвращает аккаунт в `ACTIVE` и переводит + студента в выбранную группу. +- Каждое действие пишет `membership_history` с типом, причиной и актором. + +## 3. Архив группы, удаление и вход + +- `POST /api/v1/groups/{id}/archive` переводит группу в `ARCHIVED`, + не меняя статусы студентов. +- Вход студента блокируется, если его текущая группа `ARCHIVED` или + `GRADUATED` (`AuthService.ensureGroupOpen`). Студент без группы может + войти, если его собственный статус и аккаунт активны. +- Удаление пользователя требует параметр `confirm`, совпадающий с логином + или ФИО студента (trim, точное совпадение). Иначе — `400`, данные + не удаляются. В админской форме появилось обязательное поле подтверждения. +- «Убрать из группы» осталось отдельным действием: оно удаляет зачисление, + но не является отчислением. Для отчисления с историей используется + «Отчислить». + +## 4. UI + +- `/admin/promotion` — список запусков, построение dry-run за выбранный год. +- `/admin/promotion/{id}` — сводка, предупреждения, таблица элементов + «было/стало», кнопки «Применить план» и «Откатить» по статусу. +- На странице группы: + - «Академ» — для активного студента; + - «В архив» — для активного студента; + - «Отчислить» — для активного студента; + - «Вернуть в группу» — для `ACADEMIC_LEAVE`; + - «Восстановить в группу» — для `ARCHIVED`/`EXPELLED`; + - «Убрать из группы» — отдельно от отчисления. +- «В архив» для группы — на странице группы; кнопка скрыта для уже + архивной группы. +- В навигацию админа добавлен пункт «Повышение». +- Шаблоны и представления покрыты тестами, включая подписи статусов, + счётчики и состояния before/after. + +## 5. Тесты + +Добавлены наборы: + +- `PromotionServiceIntegrationTest` (6): dry-run не меняет данные, + применение переводит группы/выпускает студентов/двигает академ, + повторное применение идемпотентно, откат восстанавливает данные + и удаляет созданную группу, конфликт ручных изменений, guard даты + и дедупликация планировщика. +- `StudentLifecycleIntegrationTest` (11): академотпуск с созданием + и переиспользованием параллели, возврат, отчисление с блокировкой входа, + вход из архивной группы, активной группы и без группы, восстановление + после отчисления и архива, архив группы без смены статусов студентов, + подтверждение удаления. +- `AdminStage4TemplateTest` (5) и `PromotionDtoTest` (2): журнал повышения, + кнопки apply/rollback, lifecycle-кнопки по статусам, поле подтверждения + удаления, JSON-контракты. +- `AuthServiceLoginBackoffTest` обновлён под новую зависимость проверки + группы. + +По результатам последнего прогона (2026-10-27) перечисленные наборы прошли +с `0 failures / 0 errors`. + +## 6. Известные пробелы и forward-only + +- **Только ручное применение.** Планировщик создаёт только dry-run; + автоматического применения и уведомлений нет. +- **Один план на год.** Повторный dry-run за год возвращает уже сохранённый + `PLANNED`-план; пересборки или удаления плана в UI нет. После завершённого + применения новый dry-run за тот же год запрещён до отката. +- **Откат целиком.** Поэлементного отката нет; при конфликте откатывается + весь запуск. После ручных изменений сначала нужно вернуть данные + к состоянию `after`. +- **История не удаляется.** Откат добавляет `RETURN`, а не стирает + исходные записи; журнал остаётся аудиторским следом. +- **Групповой архив односторонний.** Отдельного endpoint/UI для возврата + группы из архива нет; студенты архивной группы не входят. +- **Lifecycle из ведомости.** Возврат и восстановление в UI выполняются + в текущую группу; выбор другой группы доступен только через API. + Массовых операций по студентам нет. +- **JWT и блокировка.** Вход закрывается при следующей аутентификации; + уже выданный JWT действует до истечения (до 30 дней). Это остаточный + риск, зафиксированный в плане; аудит и hardening — этап 6. +- **Нет новых миграций.** Используются таблицы `promotion_runs`, + `promotion_run_items`, `membership_history`, `accounts.status` + из миграций `V7`–`V9` этапа 1. JSONB пишется через `JdbcTemplate` + из-за несовместимости Jackson 2/3 в Hibernate. diff --git a/docs/README.md b/docs/README.md index 359a584..dc5d8f5 100644 --- a/docs/README.md +++ b/docs/README.md @@ -20,6 +20,8 @@ Preview Mermaid Support) и в IntelliJ IDEA (плагин Mermaid). | [10-stage0-report.md](10-stage0-report.md) | Отчёт этапа 0: миграции, preflight и проверка на копии БД | | [11-stage1-report.md](11-stage1-report.md) | Отчёт этапа 1: роли, группы-когорты, статусы, история, импорт v2 | | [12-stage2-report.md](12-stage2-report.md) | Отчёт этапа 2: курсы, bootstrap курса C, привязка и копирование лабораторных, миграция V10 | +| [13-stage3-report.md](13-stage3-report.md) | Отчёт этапа 3: преподаватели, доступ к курсам, внутренние API, кабинет и профиль студента курса | +| [14-stage4-report.md](14-stage4-report.md) | Отчёт этапа 4: повышение, академотпуск, отчисление, архив, восстановление и удаление | | [ADR 0001](adr/0001-groups-courses-foundation.md) | Ключевые архитектурные решения: курс C, миграции, доступ, статусы, повышение | ## Кратко о системе