diff --git a/README.md b/README.md
index b35d1b7..19a94fb 100644
--- a/README.md
+++ b/README.md
@@ -2,6 +2,16 @@
Микросервисная система для проведения практических занятий (Kotlin + Spring Boot + PostgreSQL 18).
+## Документация
+
+UML- и архитектурные диаграммы (Mermaid) — в каталоге [`docs/`](docs/README.md):
+
+- [Архитектура](docs/01-architecture.md) — контекст, компоненты, модули, аутентификация;
+- [Развёртывание](docs/02-deployment.md) — Docker, nginx, тома, обновление;
+- [Модель данных и классов](docs/03-domain-model.md) — диаграммы классов по сервисам и ER-модель;
+- [Диаграммы последовательностей](docs/04-sequences.md) — вход, импорт ЛР, раздача вариантов, сдача, защита, оценки, материалы;
+- [Диаграммы состояний](docs/05-states.md) — защита, публикация оценок и материалов, сдача, пароль.
+
## Модули
- `libs/common-http` — общий формат ошибок и обработчик исключений.
diff --git a/docs/01-architecture.md b/docs/01-architecture.md
new file mode 100644
index 0000000..a7cc3b9
--- /dev/null
+++ b/docs/01-architecture.md
@@ -0,0 +1,160 @@
+# Архитектура системы
+
+## 1. Контекст (какие внешние участники и системы)
+
+```mermaid
+flowchart LR
+ user["Преподаватель / студент
(браузер)"]
+ nginx["nginx
lms.our-place.ru
TLS, reverse proxy"]
+ web["web BFF
Thymeleaf + htmx
:8090"]
+ gw["gateway
Spring Cloud Gateway
:8080"]
+ id["identity-service
:8081"]
+ ac["academic-service
:8082"]
+ pr["progress-service
:8083"]
+ db[("PostgreSQL 18
схемы identity / academic / progress")]
+ fs[("Файловое хранилище
content-assets, submissions")]
+
+ user -->|HTTPS| nginx
+ nginx -->|HTTP 127.0.0.1:8090| web
+ web -->|Bearer JWT| gw
+ gw --> id
+ gw --> ac
+ gw --> pr
+ id --> db
+ ac --> db
+ pr --> db
+ ac --> fs
+ pr --> fs
+ pr -->|REST, внутренняя сеть| ac
+ pr -->|REST, внутренняя сеть| id
+ id -.->|JWKS: /api/v1/auth/jwks.json| gw
+ id -.->|JWKS| ac
+ id -.->|JWKS| pr
+```
+
+- Браузер никогда не получает JWT: токен живёт в HTTP-сессии web-сервиса
+ (`lms.token`), а в браузер уходит только cookie сессии (`HttpOnly`, 30 дней).
+- Все REST-запросы web проксирует на gateway, добавляя `Authorization: Bearer`.
+- Gateway проверяет подпись токена по JWKS identity и передаёт вниз заголовки
+ `X-Account-Id`, `X-Account-Login`, `X-Account-Role`.
+- Каждый сервис дополнительно валидирует JWT самостоятельно (resource server).
+
+## 2. Компоненты по сервисам
+
+```mermaid
+flowchart TB
+ subgraph web["web (BFF, :8090)"]
+ wc["@Controller
AuthPage, Learn, Student, Admin, ContentAdmin"]
+ wi["Interceptor'ы:
Csrf, Auth, Admin, MustChangePassword"]
+ wapi["ApiClient (WebClient)
+ DTO"]
+ wmd["MarkdownRenderer
(libs:markdown)"]
+ wc --> wapi
+ wc --> wmd
+ wi --> wc
+ end
+
+ subgraph gw["gateway (:8080)"]
+ routes["Маршруты
identity / academic / progress"]
+ filter["JwtAuthenticationFilter
order -100"]
+ filter --> routes
+ end
+
+ subgraph id["identity-service (:8081)"]
+ idc["AuthController, GroupController, StudentController"]
+ ids["AuthService, GroupService, Passwords,
StudentImportParser, CredentialsExporter"]
+ idr["AccountRepository, GroupRepository,
EnrollmentRepository, StudentRepository, SettingRepository"]
+ idc --> ids --> idr
+ end
+
+ subgraph ac["academic-service (:8082)"]
+ acc["LabController, ContentPublicController,
ContentAdminController"]
+ acs["LabService, ContentService,
LabImportParser, AssetStorage"]
+ acr["LabRepository, LabLevelRepository, VariantRepository,
SemesterRepository, Content*Repository"]
+ acc --> acs --> acr
+ end
+
+ subgraph pr["progress-service (:8083)"]
+ prc["ProgressController, DefenseAdminController,
DefenseMyController"]
+ prs["ProgressService, SubmissionService,
DefenseService, GradePublicationService"]
+ prr["VariantAssignmentRepository, StudentLabLevelRepository,
LabGradeRepository, GradeCommentRepository,
CommentTemplateRepository, SubmissionRepository,
DefenseSessionRepository, GradePublicationRepository"]
+ prcl["AcademicClient, IdentityClient"]
+ prc --> prs
+ prs --> prr
+ prs --> prcl
+ end
+
+ wapi -->|HTTP| routes
+ routes --> idc
+ routes --> acc
+ routes --> prc
+ prcl -->|GET /api/v1/labs/{id}| acc
+ prcl -->|GET /api/v1/groups и /{id}| idc
+```
+
+## 3. Модули Gradle
+
+```mermaid
+flowchart LR
+ web[":web"] --> common[":libs:common-http"]
+ web --> md[":libs:markdown"]
+ gw[":services:gateway"] --> sec[":libs:security-jwt"]
+ gw --> common
+ id[":services:identity-service"] --> common
+ id --> sec
+ ac[":services:academic-service"] --> common
+ ac --> sec
+ ac --> md
+ pr[":services:progress-service"] --> common
+ pr --> sec
+ tests[":libs:test-support"] -.-> id
+ tests -.-> ac
+ tests -.-> pr
+```
+
+| Модуль | Назначение |
+|---|---|
+| `:libs:common-http` | единый формат ошибок (`ApiError`), иерархия `LmsException`, `LmsExceptionHandler`, генератор UUIDv7 (`Ids`) |
+| `:libs:security-jwt` | свойства JWT, парсинг RSA-ключей, автоконфигурация `JwtEncoder`/`JwtDecoder`/`PasswordEncoder`, enum `LmsRole` |
+| `:libs:markdown` | markdown → безопасный HTML (OWASP-санитайзер), таблицы, спойлеры `:::spoiler` |
+| `:libs:test-support` | константы для интеграционных тестов с PostgreSQL |
+
+## 4. Аутентификация и авторизация
+
+```mermaid
+flowchart TD
+ a["Клиент: POST /api/v1/auth/login"] --> b["gateway: путь публичный
(login/setup/health/jwks)"]
+ b --> c["identity: AuthService.login"]
+ c --> d{"BCrypt совпал?"}
+ d -->|нет| e["401 + ApiError"]
+ d -->|да| f["JwtEncoder (RSA-2048, kid=lms)
claims: sub, login, role, exp"]
+ f --> g["AuthResponse{token,...}"]
+
+ h["Клиент: запрос к защищённому API
Authorization: Bearer ..."] --> i["gateway JwtAuthenticationFilter"]
+ i --> j{"Токен валиден по JWKS?"}
+ j -->|нет| k["401 Unauthorized"]
+ j -->|да| l["Добавляет X-Account-Id/Login/Role"]
+ l --> m["Сервис: SecurityConfig
oauth2ResourceServer(jwt)"]
+ m --> n{"@PreAuthorize
ROLE_ADMIN / isAuthenticated"}
+ n -->|нет| o["403 Forbidden"]
+ n -->|да| p["Контроллер → сервис → БД"]
+```
+
+- Роли: `ADMIN`, `STUDENT`; конвертер ролей читает claim `role` как `ROLE_`.
+- Публичные пути: `/actuator/**`, `/api/v1/auth/{health,setup,login,jwks.json}`,
+ `/api/v1/content/tree`, `/api/v1/content/assets/**`,
+ `/api/v1/content/{chapter}/{section}/{article}`.
+- RSA-ключи identity хранятся в таблице `settings` и публикуются в JWKS.
+
+## 5. Рендеринг страниц и защита форм (web)
+
+```mermaid
+flowchart LR
+ req["HTTP-запрос"] --> csrf["CsrfInterceptor /**
GET: выдаёт token в сессию
POST: сверяет _csrf"]
+ csrf --> must["MustChangePasswordInterceptor /**
(кроме /change-password, /logout, /login, статики)"]
+ must --> auth["AuthInterceptor
/admin/**, /student/**, /change-password"]
+ auth --> adm["AdminInterceptor /admin/**"]
+ adm --> ctrl["Контроллер → ApiClient → gateway"]
+```
+
+- `GlobalModelAdvice` кладёт `currentUser` (SessionUser) во все шаблоны.
+- Скрытые материалы и полное дерево материалов доступны только администратору.
diff --git a/docs/02-deployment.md b/docs/02-deployment.md
new file mode 100644
index 0000000..2eeab89
--- /dev/null
+++ b/docs/02-deployment.md
@@ -0,0 +1,108 @@
+# Развёртывание
+
+## 1. Диаграмма развёртывания
+
+```mermaid
+flowchart TB
+ internet["Интернет"]
+ subgraph host["Сервер lms.our-place.ru (Ubuntu, Docker)"]
+ nginx["nginx (host)
80/443, TLS (certbot)
proxy → 127.0.0.1:8090"]
+ subgraph net["docker network: lms"]
+ web["container: web
lms-web:latest
:8090"]
+ gw["container: gateway
lms-gateway:latest
:8080"]
+ id["container: identity
lms-identity:latest
:8081"]
+ ac["container: academic
lms-academic:latest
:8082"]
+ pr["container: progress
lms-progress:latest
:8083"]
+ pg["container: postgres
postgres:18
:5432"]
+ vol1[("volume lms_pgdata")]
+ vol2[("volume lms_assets
/data/content-assets")]
+ vol3[("volume lms_submissions
/data/submissions")]
+ end
+ end
+
+ internet -->|HTTPS| nginx
+ nginx -->|HTTP| web
+ web --> gw
+ gw --> id
+ gw --> ac
+ gw --> pr
+ id --> pg
+ ac --> pg
+ pr --> pg
+ ac --> vol2
+ pr --> vol3
+ pg --> vol1
+```
+
+Наружу публикуется только web на `127.0.0.1:8090`; доступ снаружи — через nginx и TLS.
+
+## 2. Контейнеры и переменные окружения
+
+| Сервис | Образ / стадия Dockerfile | Порт | Ключевые переменные | Тома |
+|---|---|---|---|---|
+| postgres | `postgres:18` | 5432 (внутр.) | `POSTGRES_DB`, `POSTGRES_USER`, `POSTGRES_PASSWORD` | `lms_pgdata` |
+| identity | `target: identity` | 8081 | `LMS_DB_URL` (схема `identity`), `LMS_DB_USER`, `LMS_DB_PASSWORD` | — |
+| academic | `target: academic` | 8082 | `LMS_DB_URL` (`academic`), `LMS_CONTENT_ASSETS_DIR`, `LMS_JWT_JWK_SET_URI` | `lms_assets` |
+| progress | `target: progress` | 8083 | `LMS_DB_URL` (`progress`), `LMS_SUBMISSIONS_DIR`, `LMS_IDENTITY_URI`, `LMS_ACADEMIC_URI`, `LMS_JWT_JWK_SET_URI` | `lms_submissions` |
+| gateway | `target: gateway` | 8080 | `LMS_IDENTITY_URI`, `LMS_ACADEMIC_URI`, `LMS_PROGRESS_URI`, `LMS_JWT_JWK_SET_URI` | — |
+| web | `target: web` | 8090 | `LMS_GATEWAY_URL`, `SERVER_PORT` | — |
+
+`LMS_JWT_JWK_SET_URI` обязателен для gateway, academic и progress —
+в контейнерах это `http://identity:8081/api/v1/auth/jwks.json` (иначе 401).
+
+## 3. Сборка
+
+```mermaid
+flowchart LR
+ src["Исходники
services/, libs/, web/, buildSrc/"]
+ subgraph build["Dockerfile, стадия build (eclipse-temurin:21-jdk)"]
+ g["./gradlew bootJar -x test
(Gradle-кэш в cache-mount)"]
+ end
+ jar["bootJar-артефакты"]
+ subgraph run["Runtime-стадии (eclipse-temurin:21-jre)"]
+ r["java -jar app.jar
TZ=Europe/Moscow"]
+ end
+ src --> g --> jar --> r
+```
+
+Java на сервере не нужна: сборка и запуск — внутри контейнеров.
+`deploy/deploy.sh up` собирает образы и запускает стек из `docker-compose.prod.yml`.
+
+## 4. Nginx
+
+`deploy/nginx/lms.our-place.ru.conf`:
+
+- `listen 80` (TLS добавляет `certbot --nginx`), `server_name lms.our-place.ru`;
+- ACME-челлендж через `/.well-known/acme-challenge/`;
+- `client_max_body_size 120m` — загрузка ZIP-архивов лабораторных и решений;
+- `proxy_pass http://127.0.0.1:8090` c заголовками
+ `Host`, `X-Real-IP`, `X-Forwarded-For`, `X-Forwarded-Proto`, `X-Forwarded-Host`;
+- таймауты 300 с на чтение/отправку.
+
+Web-сервис запускается с `server.forward-headers-strategy: framework`,
+поэтому корректно формирует https-редиректы за прокси.
+
+## 5. Обновление и данные
+
+```mermaid
+sequenceDiagram
+ autonumber
+ participant Dev as Разработчик
+ participant Git as Gitea
+ participant Srv as Сервер /opt/LMS
+ participant DC as docker compose
+ participant PG as postgres
+
+ Dev->>Git: git push origin master
+ Srv->>Git: git pull
+ Srv->>DC: ./deploy/deploy.sh up
+ DC->>DC: docker build (5 стадий)
+ DC->>PG: старт postgres (volume lms_pgdata)
+ DC->>DC: старт identity → academic → progress → gateway → web
+ Note over DC: Flyway применяет миграции
V1..V5 автоматически при старте сервисов
+```
+
+- Резервные копии: `pg_dump` (том `lms_pgdata`) и `tar` томов `lms_assets`,
+ `lms_submissions` — команды в [deploy/DEPLOY.md](../deploy/DEPLOY.md).
+- Порядок наполнения данными (админ → группы → лабораторная → варианты →
+ оценки/защита → материалы) описан в [deploy/IMPORT.md](../deploy/IMPORT.md).
diff --git a/docs/03-domain-model.md b/docs/03-domain-model.md
new file mode 100644
index 0000000..86096c2
--- /dev/null
+++ b/docs/03-domain-model.md
@@ -0,0 +1,639 @@
+# Модель данных и классов
+
+Ниже — диаграммы классов по сервисам (JPA-сущности + репозитории + сервисы),
+ER-модель БД и модель web-слоя.
+
+## 1. identity-service
+
+```mermaid
+classDiagram
+ class Account {
+ +UUID id
+ +String login
+ +String passwordHash
+ +LmsRole role
+ +Boolean mustChangePassword
+ +String tempPassword
+ +Instant createdAt
+ +Instant updatedAt
+ }
+ class Student {
+ +UUID accountId
+ +String studentNo
+ +String lastName
+ +String firstName
+ +String middleName
+ +String basis
+ +String note
+ +String fullName
+ }
+ class GroupEntity {
+ +UUID id
+ +String name
+ +Int taskDurationMin
+ +Instant createdAt
+ }
+ class Enrollment {
+ +UUID id
+ +Instant enrolledAt
+ }
+ class Setting {
+ +String key
+ +String value
+ }
+ class LmsRole {
+ <>
+ ADMIN
+ STUDENT
+ }
+
+ Account "1" --> "1" Student : @MapsId account_id
+ GroupEntity "1" --> "*" Enrollment : group_id
+ Student "1" --> "*" Enrollment : student_id
+ Account --> LmsRole : role
+```
+
+```mermaid
+classDiagram
+ class AuthController
+ class GroupController
+ class StudentController
+ class AuthService {
+ +adminExists() Boolean
+ +setup(SetupRequest) AuthResponse
+ +login(LoginRequest) AuthResponse
+ +me(UUID) MeResponse
+ +changePassword(UUID, ChangePasswordRequest)
+ }
+ class GroupService {
+ +list() List~GroupSummary~
+ +create(CreateGroupRequest) GroupSummary
+ +createStudent(UUID, CreateStudentRequest) Credential
+ +candidates(UUID) List~StudentCandidate~
+ +enroll(UUID, UUID) GroupDetail
+ +unenroll(UUID, UUID) GroupDetail
+ +import(String, String, ByteArray) ImportResult
+ +groupOfStudent(UUID) GroupSummary
+ +detail(UUID) GroupDetail
+ +resetPassword(UUID) ResetPasswordResponse
+ +credentials(UUID, String) ExportFile
+ }
+ class AccountRepository
+ class GroupRepository
+ class EnrollmentRepository
+ class StudentRepository
+ class SettingRepository
+ class StudentImportParser {
+ +parse(String, ByteArray) ParsedGroup
+ }
+ class CredentialsExporter {
+ +csv(List~StudentRow~) ByteArray
+ +xlsx(List~StudentRow~) ByteArray
+ }
+ class Passwords {
+ +generate(Int) String
+ }
+
+ AuthController --> AuthService
+ GroupController --> GroupService
+ StudentController --> GroupService
+ AuthService --> AccountRepository
+ AuthService --> StudentRepository
+ GroupService --> GroupRepository
+ GroupService --> EnrollmentRepository
+ GroupService --> StudentRepository
+ GroupService --> AccountRepository
+ GroupService --> StudentImportParser
+ GroupService --> CredentialsExporter
+ GroupService --> Passwords
+```
+
+## 2. academic-service
+
+```mermaid
+classDiagram
+ class Semester {
+ +UUID id
+ +String title
+ +Int year
+ +Boolean active
+ }
+ class Lab {
+ +UUID id
+ +Int number
+ +String title
+ +String descriptionMd
+ +Instant deadlineAt
+ +Instant defenseAt
+ +BigDecimal pointsMax
+ +String status
+ +Instant createdAt
+ }
+ class LabLevel {
+ +UUID id
+ +String code
+ +String title
+ +String description
+ +Int position
+ +BigDecimal pointsMax
+ }
+ class Variant {
+ +UUID id
+ +String number
+ +String title
+ +String statementMd
+ +Int position
+ }
+ class ContentChapter {
+ +UUID id
+ +String title
+ +String slug
+ +Int position
+ +Boolean published
+ }
+ class ContentSection {
+ +UUID id
+ +UUID chapterId
+ +String title
+ +String slug
+ +Int position
+ +Boolean published
+ }
+ class ContentArticle {
+ +UUID id
+ +UUID sectionId
+ +String title
+ +String slug
+ +String bodyMd
+ +Int position
+ +Boolean published
+ }
+
+ Semester "1" --> "*" Lab : semester_id
+ Lab "1" --> "*" LabLevel : lab_id
+ LabLevel "1" --> "*" Variant : lab_level_id
+ Lab "1" --> "*" Variant : lab_id
+ ContentChapter "1" --> "*" ContentSection : chapter_id
+ ContentSection "1" --> "*" ContentArticle : section_id
+```
+
+```mermaid
+classDiagram
+ class LabController
+ class ContentPublicController
+ class ContentAdminController
+ class LabService {
+ +importFromFile(String, ByteArray) LabImportResult
+ +list() List~LabSummary~
+ +detail(UUID) LabDetail
+ +create(LabCreateRequest) LabDetail
+ +update(UUID, LabUpdateRequest) LabDetail
+ +delete(UUID)
+ +toggleStatus(UUID)
+ +addLevel(UUID, LevelSaveRequest)
+ +renameLevel(UUID, UUID, LevelRenameRequest)
+ +deleteLevel(UUID, UUID)
+ +addVariant(UUID, VariantSaveRequest)
+ +updateVariant(UUID, UUID, VariantSaveRequest)
+ +deleteVariant(UUID, UUID)
+ }
+ class ContentService {
+ +tree(Boolean) ContentTree
+ +treeAll() ContentTree
+ +findArticle(String, String, String, Boolean) ContentArticleView
+ +setChapterPublished(UUID, Boolean)
+ +setSectionPublished(UUID, Boolean)
+ +setArticlePublished(UUID, Boolean)
+ +listAll() List~ContentArticleView~
+ +createChapter(String, String) ChapterTreeNode
+ +createSection(UUID, String, String) SectionTreeNode
+ +createArticlePlaced(ArticleCreatePayload) ContentArticleView
+ +updateArticle(UUID, String, String)
+ +moveArticle(UUID, UUID) ContentArticleView
+ +deleteArticle(UUID)
+ +importZip(String, ByteArray) ContentImportResult
+ }
+ class LabImportParser {
+ +parse(String, ByteArray) LabImportBundle
+ }
+ class AssetStorage {
+ +store(String, ByteArray) StoredAsset
+ +read(String) Pair
+ }
+ class LabRepository
+ class LabLevelRepository
+ class VariantRepository
+ class SemesterRepository
+ class ContentChapterRepository
+ class ContentSectionRepository
+ class ContentArticleRepository
+
+ LabController --> LabService
+ ContentPublicController --> ContentService
+ ContentAdminController --> ContentService
+ LabService --> LabRepository
+ LabService --> LabLevelRepository
+ LabService --> VariantRepository
+ LabService --> SemesterRepository
+ LabService --> LabImportParser
+ ContentService --> ContentChapterRepository
+ ContentService --> ContentSectionRepository
+ ContentService --> ContentArticleRepository
+ ContentService --> AssetStorage
+```
+
+## 3. progress-service
+
+```mermaid
+classDiagram
+ class VariantAssignment {
+ +UUID id
+ +UUID studentId
+ +UUID labId
+ +UUID variantId
+ +Boolean manual
+ +Instant assignedAt
+ }
+ class StudentLabLevel {
+ +UUID studentId
+ +UUID labId
+ +UUID labLevelId
+ +Boolean manual
+ }
+ class LabGrade {
+ +UUID studentId
+ +UUID labId
+ +BigDecimal score
+ +Instant updatedAt
+ }
+ class GradePublication {
+ +UUID groupId
+ +UUID labId
+ +Instant publishedAt
+ }
+ class GradeComment {
+ +UUID id
+ +UUID studentId
+ +UUID labId
+ +String text
+ +String tone
+ +Instant createdAt
+ }
+ class CommentTemplate {
+ +UUID id
+ +String text
+ +String tone
+ +Int useCount
+ }
+ class Submission {
+ +UUID id
+ +UUID studentId
+ +UUID labId
+ +Int seq
+ +Instant createdAt
+ }
+ class DefenseSession {
+ +UUID id
+ +UUID studentId
+ +UUID labId
+ +String assignmentMd
+ +String status
+ +String code
+ +Instant startedAt
+ +Instant readyAt
+ +String readyReason
+ }
+ class DefenseStatus {
+ <>
+ waiting
+ started
+ ready
+ }
+
+ CommentTemplate "1" --> "*" GradeComment : подсказки (text/tone)
+ GradeComment "*" --> "1" LabGrade : поясняет оценку
+ LabGrade "1" --> "0..1" GradePublication : видна студенту после публикации
+ DefenseSession --> DefenseStatus : status
+ Note for VariantAssignment : studentId / labId / variantId — идентификаторы других сервисов (FK на уровне БД нет)
+ Note for LabGrade : groupId для публикации берётся из identity
+```
+
+```mermaid
+classDiagram
+ class ProgressController
+ class DefenseAdminController
+ class DefenseMyController
+ class ProgressService {
+ +setLevel(LevelAssignmentRequest, String) LevelAssignmentResponse
+ +getLevel(UUID, UUID) StudentLabLevel
+ +getLevelsForLab(UUID) List~StudentLabLevel~
+ +distribute(DistributeRequest, String) DistributeResult
+ +assignWithInfo(AssignRequest, String) VariantAssignmentResponse
+ +assignmentsForLabEnriched(UUID, String) List~VariantAssignmentResponse~
+ +setGrade(GradeSetRequest) GradeResponse
+ +suggestComments(String) List~CommentOut~
+ +myComments(UUID) List~GradeComment~
+ +getStudentAssignment(UUID, UUID, String) MeAssignmentResponse
+ }
+ class SubmissionService {
+ +submit(UUID, UUID, List~MultipartFile~, String) SubmissionDto
+ +mySubmissions(UUID, UUID) List~SubmissionDto~
+ +download(UUID, UUID, UUID, Boolean) Pair
+ +latestPerStudent(UUID) List~SubmissionDto~
+ }
+ class DefenseService {
+ +syncTimeouts() Int
+ +assign(UUID, UUID, String) DefenseDto
+ +issueCode(UUID) String
+ +reset(UUID)
+ +listForLab(UUID) List~DefenseDto~
+ +startByCode(UUID, String) DefenseDto
+ +finish(UUID, UUID) DefenseDto
+ +mySessions(UUID) List~DefenseDto~
+ +resetByStudentLab(UUID, UUID)
+ }
+ class GradePublicationService {
+ +setPublished(UUID, UUID, Boolean)
+ +isPublished(UUID, UUID) Boolean
+ +publishedLabIdsForGroup(UUID) List~String~
+ +publishedGroupIdsForLab(UUID) List~String~
+ }
+ class AcademicClient {
+ +getLab(String, String) LabInfo
+ +getLabSummary(String, String) LabInfo
+ }
+ class IdentityClient {
+ +getGroup(String, String) GroupInfo
+ +listGroups(String) List~Map~
+ }
+ class SubmissionStorage {
+ +store(UUID, UUID, Int, List~MultipartFile~) StoredSubmission
+ +zipOf(Path) ByteArray
+ +listFiles(Path) List~String~
+ +dirFor(UUID, UUID, Int) Path
+ }
+ class VariantAssignmentRepository
+ class StudentLabLevelRepository
+ class LabGradeRepository
+ class GradeCommentRepository
+ class CommentTemplateRepository
+ class SubmissionRepository
+ class DefenseSessionRepository
+ class GradePublicationRepository
+
+ ProgressController --> ProgressService
+ DefenseAdminController --> DefenseService
+ DefenseMyController --> DefenseService
+ ProgressController --> SubmissionService
+ ProgressController --> GradePublicationService
+ ProgressService --> VariantAssignmentRepository
+ ProgressService --> StudentLabLevelRepository
+ ProgressService --> LabGradeRepository
+ ProgressService --> GradeCommentRepository
+ ProgressService --> CommentTemplateRepository
+ ProgressService --> AcademicClient
+ ProgressService --> IdentityClient
+ SubmissionService --> SubmissionRepository
+ SubmissionService --> SubmissionStorage
+ SubmissionService --> AcademicClient
+ DefenseService --> DefenseSessionRepository
+ GradePublicationService --> GradePublicationRepository
+```
+
+## 4.web (BFF)
+
+```mermaid
+classDiagram
+ class AuthPageController
+ class LearnController
+ class StudentController
+ class AdminController
+ class ContentAdminController
+ class ApiClient {
+ +login(String, String) AuthResponseDto
+ +contentTree(String, Boolean) ContentTreeNodeDto
+ +contentArticle(String, String, String, String, Boolean) ContentArticleDto
+ +setContentPublished(String, String, String, Boolean)
+ +groupDetail(String, String) GroupDetailDto
+ +distribute(String, DistributeRequestPayload) DistributeResultDto
+ +setGrade(String, String, String, String, String) GradeResponseDto
+ +submit(String, String, List~MultipartFile~, String) List~SubmissionDto~
+ +myGrades(String, String) List~GradeResponseDto~
+ }
+ class SessionUser {
+ +String id
+ +String login
+ +String role
+ +String name
+ +Boolean mustChangePassword
+ +isAdmin() Boolean
+ +isStudent() Boolean
+ }
+ class SessionKeys {
+ +String TOKEN
+ +String USER
+ }
+ class CsrfInterceptor
+ class AuthInterceptor
+ class AdminInterceptor
+ class MustChangePasswordInterceptor
+ class MarkdownRenderer {
+ +render(String) String
+ }
+
+ AuthPageController --> ApiClient
+ LearnController --> ApiClient
+ StudentController --> ApiClient
+ AdminController --> ApiClient
+ ContentAdminController --> ApiClient
+ LearnController --> MarkdownRenderer
+ AuthPageController --> SessionUser
+ AuthPageController --> SessionKeys
+ CsrfInterceptor ..> SessionKeys
+ AuthInterceptor ..> SessionUser
+ AdminInterceptor ..> SessionUser
+ MustChangePasswordInterceptor ..> SessionUser
+```
+
+## 5. ER-модель БД
+
+Схемы `identity`, `academic`, `progress` живут в одной БД `lms`.
+Между схемами внешних ключей нет: progress хранит `studentId`/`labId`/`groupId`
+как UUID и получает связанные данные по REST.
+
+```mermaid
+erDiagram
+ ACCOUNTS {
+ uuid id PK
+ varchar login UK
+ varchar password_hash
+ varchar role
+ boolean must_change_password
+ varchar temp_password
+ timestamptz created_at
+ timestamptz updated_at
+ }
+ STUDENTS {
+ uuid account_id PK
+ varchar student_no UK
+ varchar last_name
+ varchar first_name
+ varchar middle_name
+ varchar basis
+ text note
+ }
+ GROUPS {
+ uuid id PK
+ varchar name UK
+ int task_duration_min
+ timestamptz created_at
+ }
+ ENROLLMENTS {
+ uuid id PK
+ uuid group_id FK
+ uuid student_id FK
+ timestamptz enrolled_at
+ }
+ SETTINGS {
+ varchar key PK
+ varchar value
+ }
+
+ ACCOUNTS ||--|| STUDENTS : "account_id"
+ GROUPS ||--o{ ENROLLMENTS : "group_id"
+ STUDENTS ||--o{ ENROLLMENTS : "student_id"
+
+ SEMESTERS {
+ uuid id PK
+ varchar title
+ int year
+ boolean active
+ }
+ LABS {
+ uuid id PK
+ int number
+ varchar title
+ uuid semester_id FK
+ text description_md
+ timestamptz deadline_at
+ timestamptz defense_at
+ numeric points_max
+ varchar status
+ timestamptz created_at
+ }
+ LAB_LEVELS {
+ uuid id PK
+ uuid lab_id FK
+ varchar code
+ varchar title
+ text description
+ int position
+ numeric points_max
+ }
+ VARIANTS {
+ uuid id PK
+ uuid lab_id FK
+ uuid lab_level_id FK
+ varchar number
+ varchar title
+ text statement_md
+ int position
+ }
+ CHAPTERS {
+ uuid id PK
+ varchar title UK
+ varchar slug UK
+ int position
+ boolean published
+ }
+ SECTIONS {
+ uuid id PK
+ uuid chapter_id FK
+ varchar title
+ varchar slug
+ int position
+ boolean published
+ }
+ ARTICLES {
+ uuid id PK
+ uuid section_id FK
+ varchar title
+ varchar slug
+ text body_md
+ int position
+ boolean published
+ }
+
+ SEMESTERS ||--o{ LABS : "semester_id"
+ LABS ||--o{ LAB_LEVELS : "lab_id"
+ LAB_LEVELS ||--o{ VARIANTS : "lab_level_id"
+ CHAPTERS ||--o{ SECTIONS : "chapter_id"
+ SECTIONS ||--o{ ARTICLES : "section_id"
+
+ VARIANT_ASSIGNMENTS {
+ uuid id PK
+ uuid student_id
+ uuid lab_id
+ uuid variant_id
+ boolean manual
+ timestamptz assigned_at
+ }
+ STUDENT_LAB_LEVELS {
+ uuid student_id PK
+ uuid lab_id PK
+ uuid lab_level_id
+ boolean manual
+ }
+ LAB_GRADES {
+ uuid student_id PK
+ uuid lab_id PK
+ numeric score
+ timestamptz updated_at
+ }
+ GRADE_PUBLICATIONS {
+ uuid group_id PK
+ uuid lab_id PK
+ timestamptz published_at
+ }
+ GRADE_COMMENTS {
+ uuid id PK
+ uuid student_id
+ uuid lab_id
+ text text
+ varchar tone
+ timestamptz created_at
+ }
+ COMMENT_TEMPLATES {
+ uuid id PK
+ varchar text UK
+ varchar tone
+ int use_count
+ }
+ SUBMISSIONS {
+ uuid id PK
+ uuid student_id
+ uuid lab_id
+ int seq
+ timestamptz created_at
+ }
+ DEFENSE_SESSIONS {
+ uuid id PK
+ uuid student_id
+ uuid lab_id
+ text assignment_md
+ varchar status
+ varchar code
+ timestamptz started_at
+ timestamptz ready_at
+ varchar ready_reason
+ }
+
+ VARIANT_ASSIGNMENTS }o--|| VARIANTS : "variant_id (не FK)"
+ VARIANT_ASSIGNMENTS }o--|| LABS : "lab_id (не FK)"
+ STUDENT_LAB_LEVELS }o--|| LABS : "lab_id (не FK)"
+ LAB_GRADES }o--|| LABS : "lab_id (не FK)"
+ GRADE_PUBLICATIONS }o--|| LABS : "lab_id (не FK)"
+ GRADE_COMMENTS }o--|| LABS : "lab_id (не FK)"
+ SUBMISSIONS }o--|| LABS : "lab_id (не FK)"
+ DEFENSE_SESSIONS }o--|| LABS : "lab_id (не FK)"
+```
diff --git a/docs/04-sequences.md b/docs/04-sequences.md
new file mode 100644
index 0000000..84fd381
--- /dev/null
+++ b/docs/04-sequences.md
@@ -0,0 +1,324 @@
+# Диаграммы последовательностей
+
+## 1. Вход в систему и выдача JWT
+
+```mermaid
+sequenceDiagram
+ autonumber
+ actor U as Пользователь
+ participant W as "web (BFF)"
+ participant G as gateway
+ participant I as identity
+ participant DB as PostgreSQL
+
+ U->>W: POST /login (login, password, _csrf)
+ W->>G: POST /api/v1/auth/login
+ Note over G: путь публичный (без токена)
+ G->>I: POST /api/v1/auth/login
+ I->>DB: SELECT accounts WHERE login = ?
+ DB-->>I: Account(password_hash, role, must_change_password)
+ I->>I: BCrypt.matches(password, hash)
+ alt пароль верный
+ I->>I: JwtEncoder: RS256, kid=lms, claims sub/login/role/exp
+ I-->>G: AuthResponse(token, role, name, mustChangePassword)
+ G-->>W: 200 AuthResponse
+ W->>W: session.invalidate(); новая сессия
+ W->>W: session[token]=JWT, session[user]=SessionUser
+ alt mustChangePassword
+ W-->>U: 302 /change-password
+ else
+ W-->>U: 302 /admin | /student
+ end
+ else пароль неверный
+ I-->>G: 401 UnauthorizedException
+ G-->>W: 401 ApiError
+ W-->>U: страница входа с ошибкой
+ end
+```
+
+## 2. Защищённый запрос: проверка JWT
+
+```mermaid
+sequenceDiagram
+ autonumber
+ participant W as "web (BFF)"
+ participant G as gateway
+ participant S as "сервис identity/academic/progress"
+ participant ID as identity /jwks.json
+
+ W->>G: GET /api/v1/... (Authorization: Bearer JWT)
+ G->>G: JwtAuthenticationFilter (order -100)
+ alt путь публичный
+ G->>S: запрос без проверки токена
+ else требуется токен
+ G->>ID: JWKS (кэшируется)
+ ID-->>G: открытые RSA-ключи
+ G->>G: проверка подписи и срока действия
+ alt токен валиден
+ G->>S: + X-Account-Id, X-Account-Login, X-Account-Role
+ else
+ G-->>W: 401 {"status":401,"detail":"..."}
+ end
+ end
+ S->>S: SecurityConfig: oauth2ResourceServer(jwt), @PreAuthorize
+ S-->>W: 200 данные / 403 при нехватке прав
+```
+
+## 3. Первый вход: принудительная смена пароля
+
+```mermaid
+sequenceDiagram
+ autonumber
+ actor S as Студент
+ participant W as "web (BFF)"
+ participant I as identity
+ participant DB as PostgreSQL
+
+ S->>W: POST /login (временный пароль)
+ W->>I: POST /api/v1/auth/login
+ I-->>W: token, mustChangePassword = true
+ W->>W: сессия: SessionUser(mustChangePassword=true)
+ W-->>S: 302 /change-password
+ S->>W: GET /learn (попытка уйти)
+ W->>W: MustChangePasswordInterceptor (/**)
+ W-->>S: 302 /change-password
+ S->>W: POST /change-password (old, new, new2)
+ W->>I: POST /api/v1/auth/change-password (Bearer)
+ I->>DB: UPDATE accounts SET password_hash=?, must_change_password=false, temp_password=NULL
+ I-->>W: 200 ok
+ W->>W: SessionUser.copy(mustChangePassword=false)
+ W-->>S: 302 /student
+ Note over S,W: до смены пароля доступны только /change-password, /logout и статика
+```
+
+## 4. Импорт лабораторной (ZIP)
+
+```mermaid
+sequenceDiagram
+ autonumber
+ actor A as Администратор
+ participant W as web
+ participant G as gateway
+ participant AC as academic
+ participant DB as PostgreSQL
+
+ A->>W: POST /admin/labs/import (file=lab1.zip, _csrf)
+ W->>G: POST /api/v1/labs/import (multipart, Bearer)
+ G->>AC: POST /api/v1/labs/import
+ AC->>AC: LabImportParser.parse(zip)
+ Note over AC: lab.yaml (метаданные, уровни) +
README.md (условие) + variants/*.md + assets/
+ AC->>DB: проверка уникальности номера ЛР в семестре
+ alt номер занят
+ AC-->>W: 409 Conflict "Лабораторная уже существует"
+ else
+ AC->>DB: INSERT labs, lab_levels, variants
+ AC-->>W: LabImportResult(levelCount, variantCount)
+ end
+ W-->>A: редирект на /admin/labs с уведомлением
+ A->>W: POST /admin/labs/{id}/edit (даты, статус published, уровни, варианты)
+ W->>AC: PUT /api/v1/labs/{id}, POST/PUT/DELETE уровней и вариантов
+ AC->>DB: UPDATE labs / INSERT|UPDATE|DELETE lab_levels, variants
+ W-->>A: «Все изменения сохранены»
+```
+
+## 5. Раздача вариантов
+
+```mermaid
+sequenceDiagram
+ autonumber
+ actor A as Администратор
+ participant W as web
+ participant G as gateway
+ participant P as progress
+ participant AC as academic
+ participant ID as identity
+ participant DB as PostgreSQL
+
+ A->>W: POST /admin/labs/{id}/distribute (groupIds[], force)
+ W->>G: POST /api/v1/distribute (Bearer)
+ G->>P: DistributeRequest(labId, groupIds, force)
+ P->>DB: SELECT student_lab_levels, variant_assignments (labId)
+ P->>ID: GET /api/v1/groups, GET /api/v1/groups/{id}
+ ID-->>P: студенты выбранных групп
+ P->>AC: GET /api/v1/labs/{id}
+ AC-->>P: уровни и варианты лабораторной
+ Note over P: Алгоритм:
1) пул = варианты уровня студента (без уровня — первый уровень)
2) очередь студентов случайна и общая для выбранных групп
3) выбирается наименее занятый вариант уровня,
пока есть невыданные — повторов нет
4) ручные закрепления и force не удаляет
+ P->>DB: INSERT/UPDATE variant_assignments (manual=false)
+ P-->>W: DistributeResult(assigned, total)
+ W-->>A: «Распределено: N из M»
+```
+
+## 6. Сдача решения студентом
+
+```mermaid
+sequenceDiagram
+ autonumber
+ actor S as Студент
+ participant W as web
+ participant G as gateway
+ participant P as progress
+ participant AC as academic
+ participant FS as файловое хранилище
+ participant DB as PostgreSQL
+
+ S->>W: POST /student/labs/{id}/submit (files, _csrf)
+ W->>G: POST /api/v1/me/submissions/{labId} (multipart, Bearer)
+ G->>P: submit(studentId, labId, files, token)
+ P->>AC: GET /api/v1/labs/{id}
+ alt дедлайн прошёл
+ AC-->>P: deadlineAt < now
+ P-->>W: 400 "Приём решений закрыт"
+ W-->>S: ошибка на странице лабы
+ else приём открыт
+ P->>DB: SELECT max(seq) по студенту и ЛР
+ P->>FS: сохранить файлы в submissions/{studentId}/{labId}/{seq}
+ P->>DB: INSERT submissions
+ P-->>W: List
+ W-->>S: 302 обратно на страницу лабы (история сдач)
+ end
+```
+
+## 7. Защита лабораторной
+
+```mermaid
+sequenceDiagram
+ autonumber
+ actor A as Преподаватель
+ actor S as Студент
+ participant W as web
+ participant P as progress
+ participant DB as PostgreSQL
+
+ A->>W: «Назначить задание» → POST /admin/defenses/save
+ W->>P: POST /api/v1/defenses/assign
+ P->>DB: INSERT/UPDATE defense_sessions (status=waiting, assignment_md)
+ A->>W: «выдать код» → POST /admin/defenses/{id}/issue-code
+ P->>DB: UPDATE code = 6-значный, status=waiting
+ P-->>W: код (flash-уведомление)
+ W-->>A: код показан преподавателю
+
+ S->>W: POST /student/defenses/start (labId, code)
+ W->>P: POST /api/v1/me/defenses/start
+ P->>DB: SELECT ... WHERE code=? AND student_id=?
+ alt код верный
+ P->>DB: UPDATE status=started, started_at=now
+ P-->>W: DefenseDto(secondsLeft)
+ W-->>S: страница защиты с таймером
+ else
+ P-->>W: 400 "Неверный код"
+ end
+
+ S->>W: POST /student/defenses/{id}/finish
+ W->>P: POST /api/v1/me/defenses/{id}/finish
+ P->>DB: UPDATE status=ready, ready_at=now
+ P-->>W: DefenseDto
+ W-->>S: «Работа сохранена»
+
+ Note over P,DB: DefenseService.syncTimeouts() переводит просроченные
сессии в ready с ready_reason=timeout
+ A->>W: «доп. задание: начать заново» → POST /admin/defenses/{id}/reset
+ P->>DB: UPDATE status=waiting, code=NULL, started_at=NULL
+ W-->>A: сессия снова ждёт кода
+```
+
+## 8. Оценка, комментарии и публикация
+
+```mermaid
+sequenceDiagram
+ autonumber
+ actor A as Преподаватель
+ actor S as Студент
+ participant W as web
+ participant P as progress
+ participant DB as PostgreSQL
+
+ A->>W: POST /admin/groups/{gid}/grades (studentId, labId, score, commentsJson)
+ W->>P: POST /api/v1/grades (GradeSetRequest)
+ P->>DB: UPSERT lab_grades (student_id, lab_id, score)
+ P->>DB: DELETE grade_comments по паре, затем INSERT новых
+ P->>DB: UPSERT comment_templates (use_count++ для автоподсказок)
+ P-->>W: GradeResponse
+ Note over DB: Студент оценку пока НЕ видит
+
+ A->>W: «опубликовать оценки» → POST /admin/groups/{gid}/publish
+ W->>P: POST /api/v1/grades/publish (labId, groupId, published=true)
+ P->>DB: INSERT/DELETE grade_publications (group_id, lab_id)
+ P-->>W: 200
+
+ S->>W: GET /student/labs/{labId}
+ W->>P: GET /api/v1/me/grades?groupId=... (Bearer)
+ P->>DB: SELECT lab_grades + grade_comments + grade_publications
+ P-->>W: только опубликованные оценки группы
+ W-->>S: оценка и комментарии списками: позитивный / негативный / нейтральный
+```
+
+## 9. Учебные материалы: публикация и просмотр
+
+```mermaid
+sequenceDiagram
+ autonumber
+ actor A as Администратор
+ actor G as Гость/студент
+ participant W as web
+ participant G2 as gateway
+ participant AC as academic
+ participant DB as PostgreSQL
+
+ A->>W: GET /learn
+ W->>G2: GET /api/v1/content/tree/all (Bearer admin)
+ AC->>DB: SELECT chapters, sections, articles (включая скрытые)
+ W-->>A: дерево с бейджами «скрыто» и кнопками публикации
+
+ A->>W: POST /admin/content/articles/toggle-published (published=false)
+ W->>G2: PUT /api/v1/content/articles/{id}/published
+ AC->>DB: UPDATE articles SET published=false
+ W-->>A: «Материал скрыт от студентов»
+
+ G->>W: GET /learn
+ W->>G2: GET /api/v1/content/tree
+ AC->>DB: SELECT ... WHERE published = true
+ AC-->>W: только опубликованное
+ W-->>G: материалы без скрытых глав, разделов и статей
+
+ G->>W: GET /learn/{chapter}/{section}/{article}
+ W->>G2: GET /api/v1/content/{c}/{s}/{a}
+ alt статья (или её глава/раздел) скрыта
+ AC-->>W: 404 NotFoundException
+ W-->>G: 302 /learn
+ else опубликована
+ AC-->>W: ContentArticleView(bodyMd, published)
+ W->>W: MarkdownRenderer.render (таблицы, код, :::spoiler)
+ W-->>G: статья с подсветкой кода
+ end
+ Note over A,AC: админ открывает скрытую статью через /api/v1/content/all/{c}/{s}/{a}
+```
+
+## 10. Страница группы у администратора (агрегация данных)
+
+```mermaid
+sequenceDiagram
+ autonumber
+ actor A as Администратор
+ participant W as "web (AdminController.groupDetail)"
+ participant G as gateway
+ participant ID as identity
+ participant AC as academic
+ participant P as progress
+
+ A->>W: GET /admin/groups/{gid}
+ W->>G: GET /api/v1/groups/{gid}
+ G->>ID: GroupService.detail (студенты по ФИО)
+ W->>G: GET /api/v1/labs
+ G->>AC: LabService.list (опубликованные ЛР)
+ loop по каждой лабораторной
+ W->>G: GET /api/v1/assignments?labId
+ G->>P: варианты студентов
+ W->>G: GET /api/v1/submissions?labId
+ G->>P: последние сдачи
+ W->>G: GET /api/v1/defenses?labId
+ G->>P: сессии защиты
+ end
+ W->>G: GET /api/v1/grades + /grades/publications?groupId
+ G->>P: оценки
+ W->>W: сборка матрицы «студент × лабораторная»
+ W-->>A: страница группы (оценки, защиты, поиск, фильтры, сортировка)
+```
diff --git a/docs/05-states.md b/docs/05-states.md
new file mode 100644
index 0000000..e1851f4
--- /dev/null
+++ b/docs/05-states.md
@@ -0,0 +1,127 @@
+# Диаграммы состояний
+
+## 1. Сессия защиты (`defense_sessions.status`)
+
+```mermaid
+stateDiagram-v2
+ [*] --> waiting : преподаватель назначил задание
+ waiting --> waiting : выдан/обновлён код
+ waiting --> started : студент ввёл верный код
+ started --> ready : студент завершил работу
+ started --> ready : истекло время (syncTimeouts, readyReason=timeout)
+ ready --> waiting : «доп. задание: начать заново» (reset)
+ ready --> [*] : задание принято
+ note right of waiting
+ code = NULL — ждёт кода
+ code != NULL — код выдан
+ end note
+```
+
+## 2. Публикация оценок (пара «группа + лабораторная»)
+
+```mermaid
+stateDiagram-v2
+ [*] --> hidden : оценка выставлена (lab_grades)
+ hidden --> published : «опубликовать оценки» (grade_publications)
+ published --> hidden : «скрыть оценки»
+ note right of hidden
+ студент не видит ни балл,
+ ни комментарии
+ end note
+ note right of published
+ студенты группы видят балл и
+ комментарии, сгруппированные по тону
+ end note
+```
+
+## 3. Статус лабораторной (`labs.status`)
+
+```mermaid
+stateDiagram-v2
+ [*] --> draft : импорт/создание
+ draft --> published : публикация
+ published --> draft : снятие с публикации
+ note right of draft
+ студентам не видна;
+ доступна для настройки
+ end note
+ note right of published
+ видна студентам: условие, вариант,
+ сдача; оценки — только после публикации
+ end note
+```
+
+## 4. Публикация материалов (главы, разделы, статьи)
+
+```mermaid
+stateDiagram-v2
+ state "опубликовано" as pub
+ state "скрыто" as hid
+ [*] --> pub : импорт/создание (published=true)
+ pub --> hid : «Скрыть» (published=false)
+ hid --> pub : «Опубликовать»
+ note right of hid
+ Не видно гостям и студентам:
+ глава целиком скрывает разделы и статьи,
+ раздел — свои статьи.
+ Администратор видит их с бейджем «скрыто»
+ и может открыть для предпросмотра.
+ end note
+```
+
+## 5. Жизненный цикл сдачи решения
+
+```mermaid
+stateDiagram-v2
+ [*] --> notSubmitted : лабораторная выдана
+ notSubmitted --> submitted : сдача (seq = 1, файлы на диске)
+ submitted --> submitted : переотправка (seq + 1, хранится вся история)
+ submitted --> closed : наступил дедлайн (deadlineAt)
+ submitted --> closed : выдано индивидуальное задание на защиту
+ closed --> [*]
+ note right of closed
+ Форма отправки скрыта,
+ доступно только скачивание
+ ранее отправленных решений
+ end note
+ note right of submitted
+ Скачать можно любую попытку;
+ преподаватель видит последнюю
+ end note
+```
+
+## 6. Вариант студента
+
+```mermaid
+stateDiagram-v2
+ [*] --> none : студент в группе, ЛР опубликована
+ none --> auto : распределение («Раздать»)
+ auto --> manual : «Закрепить» конкретный вариант
+ auto --> auto : повторное распределение без force (не трогает занятые)
+ manual --> manual : force не перезаписывает ручные закрепления
+ auto --> none : force (сброс авто-назначений)
+ note right of auto
+ подбирается наименее занятый вариант
+ своего уровня; пока есть невыданные —
+ повторов нет
+ end note
+```
+
+## 7. Пароль студента
+
+```mermaid
+stateDiagram-v2
+ [*] --> temporary : аккаунт создан / «Сбросить пароль»
+ temporary --> own : смена на /change-password
+ own --> temporary : «Сбросить пароль» преподавателем
+ note right of temporary
+ must_change_password = true,
+ temp_password хранится;
+ пока пароль не сменён, доступны только
+ /change-password, /logout и статика
+ end note
+ note right of own
+ must_change_password = false,
+ temp_password = NULL
+ end note
+```
diff --git a/docs/README.md b/docs/README.md
new file mode 100644
index 0000000..12819d3
--- /dev/null
+++ b/docs/README.md
@@ -0,0 +1,30 @@
+# Документация LMS
+
+UML- и архитектурные диаграммы проекта. Все диаграммы — в формате **Mermaid**,
+поэтому они рендерятся прямо в Gitea/GitHub, в VS Code (расширение Markdown
+Preview Mermaid Support) и в IntelliJ IDEA (плагин Mermaid).
+
+## Содержание
+
+| Документ | Что описывает |
+|---|---|
+| [01-architecture.md](01-architecture.md) | Контекст системы, компоненты, модули Gradle, схема аутентификации |
+| [02-deployment.md](02-deployment.md) | Диаграмма развёртывания: Docker, nginx, тома, порты, переменные окружения |
+| [03-domain-model.md](03-domain-model.md) | Диаграммы классов по сервисам, ER-модель БД, модель web-слоя |
+| [04-sequences.md](04-sequences.md) | Диаграммы последовательностей: вход, импорт ЛР, раздача вариантов, сдача, защита, оценки, материалы |
+| [05-states.md](05-states.md) | Диаграммы состояний: защита, публикация оценок, статус ЛР, публикация материалов |
+
+## Кратко о системе
+
+LMS — микросервисное приложение на Kotlin/Spring Boot 3 (Java 21) с
+server-side-rendered UI на Thymeleaf + htmx:
+
+- **gateway** (`:8080`) — единая точка входа для REST API: маршрутизация и проверка JWT;
+- **identity-service** (`:8081`) — аккаунты, группы, студенты, аутентификация (JWT + JWKS);
+- **academic-service** (`:8082`) — семестры, лабораторные (уровни, варианты), учебные материалы;
+- **progress-service** (`:8083`) — варианты/уровни студентов, сдачи, оценки и комментарии, защиты;
+- **web** (`:8090`) — BFF: сессии, CSRF, страницы админа и студента, markdown-рендер;
+- **PostgreSQL 18** — одна БД `lms` со схемами `identity`, `academic`, `progress`.
+
+Поток запроса: браузер → nginx → web → gateway → сервис → PostgreSQL.
+Подробности — в [01-architecture.md](01-architecture.md).