diff --git a/docs/01-architecture.md b/docs/01-architecture.md
index a7cc3b9..4e59b30 100644
--- a/docs/01-architecture.md
+++ b/docs/01-architecture.md
@@ -6,7 +6,7 @@
flowchart LR
user["Преподаватель / студент
(браузер)"]
nginx["nginx
lms.our-place.ru
TLS, reverse proxy"]
- web["web BFF
Thymeleaf + htmx
:8090"]
+ web["web BFF
Thymeleaf + Bootstrap
:8090"]
gw["gateway
Spring Cloud Gateway
:8080"]
id["identity-service
:8081"]
ac["academic-service
:8082"]
@@ -44,9 +44,9 @@ flowchart LR
```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"]
+ wc["@Controller: AuthPage, Learn, Student,
AdminGroup/Lab/Student/Distribute/Defense/Users, ContentAdmin
@RestController: ContentAssetProxy"]
+ wi["Interceptor'ы:
Csrf, NoCachePage, MustChangePassword, Auth, Admin"]
+ wapi["ApiClient (RestClient)
+ DTO"]
wmd["MarkdownRenderer
(libs:markdown)"]
wc --> wapi
wc --> wmd
@@ -67,16 +67,16 @@ flowchart TB
end
subgraph ac["academic-service (:8082)"]
- acc["LabController, ContentPublicController,
ContentAdminController"]
+ acc["LabController, ContentPublicController,
ContentAdminController, ContentOrderController"]
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"]
+ prc["ProgressController, SubmissionController, DefenseAdminController,
DefenseMyController, StudentPurgeController, GroupLabDataController"]
+ prs["ProgressService, SubmissionService, DefenseService,
GradePublicationService, StudentPurgeService"]
+ prr["VariantAssignmentRepository, StudentLabLevelRepository,
LabGradeRepository, GradeCommentRepository, CommentTemplateRepository,
SubmissionRepository, DefenseSessionRepository,
GradePublicationRepository, GradePublicationStudentRepository"]
prcl["AcademicClient, IdentityClient"]
prc --> prs
prs --> prr
@@ -150,7 +150,8 @@ flowchart TD
```mermaid
flowchart LR
req["HTTP-запрос"] --> csrf["CsrfInterceptor /**
GET: выдаёт token в сессию
POST: сверяет _csrf"]
- csrf --> must["MustChangePasswordInterceptor /**
(кроме /change-password, /logout, /login, статики)"]
+ csrf --> nocahe["NoCachePageInterceptor /**
(страницы без кэша; /css, /js исключены)"]
+ nocahe --> must["MustChangePasswordInterceptor /**
(кроме /change-password, /logout, /login, статики)"]
must --> auth["AuthInterceptor
/admin/**, /student/**, /change-password"]
auth --> adm["AdminInterceptor /admin/**"]
adm --> ctrl["Контроллер → ApiClient → gateway"]
diff --git a/docs/02-deployment.md b/docs/02-deployment.md
index 2eeab89..e17e665 100644
--- a/docs/02-deployment.md
+++ b/docs/02-deployment.md
@@ -47,6 +47,8 @@ flowchart TB
| 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_DB_USER`, `LMS_DB_PASSWORD`, `LMS_DB_NAME`.
+
`LMS_JWT_JWK_SET_URI` обязателен для gateway, academic и progress —
в контейнерах это `http://identity:8081/api/v1/auth/jwks.json` (иначе 401).
@@ -99,9 +101,10 @@ sequenceDiagram
DC->>DC: docker build (5 стадий)
DC->>PG: старт postgres (volume lms_pgdata)
DC->>DC: старт identity → academic → progress → gateway → web
- Note over DC: Flyway применяет миграции
V1..V5 автоматически при старте сервисов
+ Note over DC: Flyway применяет миграции автоматически
при старте сервисов (identity V1..V3, academic V1..V5, progress V1..V8; у web миграций нет)
```
+- Локально (`docker-compose.yml`) поднимается только `postgres`; прод-стек — `docker-compose.prod.yml`.
- Резервные копии: `pg_dump` (том `lms_pgdata`) и `tar` томов `lms_assets`,
`lms_submissions` — команды в [deploy/DEPLOY.md](../deploy/DEPLOY.md).
- Порядок наполнения данными (админ → группы → лабораторная → варианты →
diff --git a/docs/03-domain-model.md b/docs/03-domain-model.md
index c435d31..8c4878d 100644
--- a/docs/03-domain-model.md
+++ b/docs/03-domain-model.md
@@ -182,6 +182,7 @@ classDiagram
class LabController
class ContentPublicController
class ContentAdminController
+ class ContentOrderController
class LabService {
+importFromFile(String, ByteArray) LabImportResult
+list() List~LabSummary~
@@ -234,6 +235,7 @@ classDiagram
LabController --> LabService
ContentPublicController --> ContentService
ContentAdminController --> ContentService
+ ContentOrderController --> ContentService
LabService --> LabRepository
LabService --> LabLevelRepository
LabService --> VariantRepository
@@ -274,6 +276,12 @@ classDiagram
+UUID labId
+Instant publishedAt
}
+ class GradePublicationStudent {
+ +UUID groupId
+ +UUID labId
+ +UUID studentId
+ +Instant publishedAt
+ }
class GradeComment {
+UUID id
+UUID studentId
@@ -313,6 +321,8 @@ classDiagram
ready
}
+ GradePublication "1" --> "*" GradePublicationStudent : легаси «вся группа» и персональные
+ Note for GradePublicationStudent : персональная публикация оценки/комментариев студенту
CommentTemplate "1" --> "*" GradeComment : подсказки (text/tone)
GradeComment "*" --> "1" LabGrade : поясняет оценку
LabGrade "1" --> "0..1" GradePublication : видна студенту после публикации
@@ -324,8 +334,11 @@ classDiagram
```mermaid
classDiagram
class ProgressController
+ class SubmissionController
class DefenseAdminController
class DefenseMyController
+ class StudentPurgeController
+ class GroupLabDataController
class ProgressService {
+setLevel(LevelAssignmentRequest, String) LevelAssignmentResponse
+getLevel(UUID, UUID) StudentLabLevel
@@ -334,7 +347,7 @@ classDiagram
+assignWithInfo(AssignRequest, String) VariantAssignmentResponse
+assignmentsForLabEnriched(UUID, String) List~VariantAssignmentResponse~
+setGrade(GradeSetRequest) GradeResponse
- +suggestComments(String) List~CommentOut~
+ +suggestComments(String) List~CommentSuggestion~
+myComments(UUID) List~GradeComment~
+getStudentAssignment(UUID, UUID, String) MeAssignmentResponse
}
@@ -356,10 +369,12 @@ classDiagram
+resetByStudentLab(UUID, UUID)
}
class GradePublicationService {
- +setPublished(UUID, UUID, Boolean)
- +isPublished(UUID, UUID) Boolean
- +publishedLabIdsForGroup(UUID) List~String~
- +publishedGroupIdsForLab(UUID) List~String~
+ +state(UUID, UUID) PublicationState
+ +setStudents(UUID, UUID, List~UUID~)
+ +isPublished(UUID, UUID, UUID) Boolean
+ }
+ class StudentPurgeService {
+ +purge(UUID)
}
class AcademicClient {
+getLab(String, String) LabInfo
@@ -383,12 +398,17 @@ classDiagram
class SubmissionRepository
class DefenseSessionRepository
class GradePublicationRepository
+ class GradePublicationStudentRepository
ProgressController --> ProgressService
+ SubmissionController --> SubmissionService
DefenseAdminController --> DefenseService
DefenseMyController --> DefenseService
+ StudentPurgeController --> StudentPurgeService
+ GroupLabDataController --> ProgressService
ProgressController --> SubmissionService
ProgressController --> GradePublicationService
+ StudentPurgeService --> SubmissionStorage
ProgressService --> VariantAssignmentRepository
ProgressService --> StudentLabLevelRepository
ProgressService --> LabGradeRepository
@@ -401,6 +421,7 @@ classDiagram
SubmissionService --> AcademicClient
DefenseService --> DefenseSessionRepository
GradePublicationService --> GradePublicationRepository
+ GradePublicationService --> GradePublicationStudentRepository
```
## 4.web (BFF)
@@ -410,13 +431,29 @@ classDiagram
class AuthPageController
class LearnController
class StudentController
- class AdminController
+ class AdminGroupController
+ class AdminLabController
+ class AdminStudentController
+ class AdminDistributeController
+ class AdminDefenseController
+ class AdminUsersController
class ContentAdminController
+ class ContentAssetProxy
class ApiClient {
+login(String, String) AuthResponseDto
+contentTree(String, Boolean) ContentTreeNodeDto
+contentArticle(String, String, String, String, Boolean) ContentArticleDto
+setContentPublished(String, String, String, Boolean)
+ +suggestComments(String, String) List~CommentSuggestionDto~
+ +deleteCommentTemplate(String, String)
+ +publicationState(String, String, String) PublicationStateDto
+ +setPublication(String, PublicationSetPayload)
+ +purgeStudent(String, String)
+ +searchStudents(String, String) List~AdminUserDto~
+ +deleteStudent(String, String) DeleteUserDto
+ +defenseWatch(String, List~String~) List~DefenseDto~
+ +myDefenseStatus(String, String) Map
+ +groupLabData(String, String) List~Map~
+groupDetail(String, String) GroupDetailDto
+distribute(String, DistributeRequestPayload) DistributeResultDto
+setGrade(String, String, String, String, String) GradeResponseDto
@@ -437,6 +474,7 @@ classDiagram
+String USER
}
class CsrfInterceptor
+ class NoCachePageInterceptor
class AuthInterceptor
class AdminInterceptor
class MustChangePasswordInterceptor
@@ -447,8 +485,14 @@ classDiagram
AuthPageController --> ApiClient
LearnController --> ApiClient
StudentController --> ApiClient
- AdminController --> ApiClient
+ AdminGroupController --> ApiClient
+ AdminLabController --> ApiClient
+ AdminStudentController --> ApiClient
+ AdminDistributeController --> ApiClient
+ AdminDefenseController --> ApiClient
+ AdminUsersController --> ApiClient
ContentAdminController --> ApiClient
+ ContentAssetProxy --> ApiClient
LearnController --> MarkdownRenderer
AuthPageController --> SessionUser
AuthPageController --> SessionKeys
@@ -544,7 +588,7 @@ erDiagram
}
CHAPTERS {
uuid id PK
- varchar title UK
+ varchar title
varchar slug UK
int position
boolean published
@@ -598,6 +642,12 @@ erDiagram
uuid lab_id PK
timestamptz published_at
}
+ GRADE_PUBLICATION_STUDENTS {
+ uuid group_id PK
+ uuid lab_id PK
+ uuid student_id PK
+ timestamptz published_at
+ }
GRADE_COMMENTS {
uuid id PK
uuid student_id
@@ -636,6 +686,7 @@ erDiagram
STUDENT_LAB_LEVELS }o--|| LABS : "lab_id (не FK)"
LAB_GRADES }o--|| LABS : "lab_id (не FK)"
GRADE_PUBLICATIONS }o--|| LABS : "lab_id (не FK)"
+ GRADE_PUBLICATION_STUDENTS }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
index f616d2e..2b1189a 100644
--- a/docs/04-sequences.md
+++ b/docs/04-sequences.md
@@ -146,6 +146,7 @@ sequenceDiagram
P->>DB: INSERT/UPDATE variant_assignments (manual=false)
P-->>W: DistributeResult(assigned, total)
W-->>A: «Распределено: N из M»
+ Note over A,W: UI — большая модалка «Выдача вариантов» на странице группы
(фрагмент по fetch). Эндпоинт отвечает редиректом — это норма;
модалка обновляется на месте, тост показывается поверх неё
```
## 6. Сдача решения студентом
@@ -214,13 +215,13 @@ sequenceDiagram
P-->>W: DefenseDto
W-->>S: «Работа сохранена»
- Note over P,DB: DefenseService.syncTimeouts() переводит просроченные
сессии в ready с ready_reason=timeout
+ Note over P,DB: DefenseService.syncTimeouts() переводит просроченные
сессии в ready с ready_reason=timeout; кнопка «Готов» даёт ready_reason=button.
В UI: «Готов к сдаче» (button) / «Вышло время» (timeout)
A->>W: «доп. задание: начать заново» → POST /admin/defenses/{id}/reset
P->>DB: UPDATE status=waiting, code=NULL, started_at=NULL
W-->>A: сессия снова ждёт кода
```
-## 8. Оценка, комментарии и публикация
+## 8. Оценка, комментарии и публикация (по студенту)
```mermaid
sequenceDiagram
@@ -235,19 +236,28 @@ sequenceDiagram
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++ для автоподсказок)
+ Note over DB: comment_templates — подсказки; частота считается
по актуальным grade_comments (не по 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
+ A->>W: чекбокс публикации в ячейке (student×lab) → POST /admin/groups/{gid}/publish-student
+ W->>P: POST /api/v1/grades/publication {groupId, labId, studentIds[]} — заменяет набор
+ P->>DB: DELETE/INSERT grade_publication_students
(легаси grade_publications снимается)
+ W-->>A: тост «Опубликовано: N из M», счётчик «глаза» обновлён
+
+ A->>W: «глаз» в шапке ЛР (всем / скрыть у всех) → POST /admin/groups/{gid}/publish (published=true|false)
+ W->>P: POST /api/v1/grades/publication {studentIds = все | []}
+
+ A->>W: поиск подсказок → GET /admin/comment-templates?q=
+ W->>P: GET /api/v1/comment-templates?q=
+ P-->>W: [{text, tone, count}] — частота по текущим оценкам
+ A->>W: удалить подсказку с частотой 0 → POST /admin/comment-templates/delete (text)
+ W->>P: DELETE /api/v1/comment-templates?text= (400, если где-то используется)
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: только опубликованные оценки группы
+ P->>DB: SELECT lab_grades + grade_comments; фильтр isPublished(group, lab, student)
+ P-->>W: только опубликованные этому студенту оценки
W-->>S: оценка и комментарии списками: позитивный / негативный / нейтральный
```
@@ -314,16 +324,18 @@ sequenceDiagram
G->>ID: GroupService.detail (студенты по ФИО)
W->>G: GET /api/v1/labs
G->>AC: LabService.list (опубликованные ЛР)
+ W->>G: GET /api/v1/assignments/group/{gid}
+ G->>P: уровни и варианты всех студентов группы (bulk)
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: сессии защиты
+ W->>G: GET /api/v1/grades/publication?groupId&labId
+ G->>P: состояние публикации {all, studentIds}
end
- W->>G: GET /api/v1/grades + /grades/publications?groupId
- G->>P: оценки
+ W->>G: GET /api/v1/grades
+ G->>P: все оценки (матрица)
W->>W: сборка матрицы «студент × лабораторная»
W-->>A: страница группы (оценки, защиты, поиск, фильтры, сортировка)
```
@@ -351,6 +363,32 @@ sequenceDiagram
AC->>DB: UPDATE chapters.position,
sections.chapter_id/position,
articles.section_id/position
AC-->>W: 200
W-->>UI: {"ok": true}
- UI->>UI: location.reload() — страница из серверного состояния
+ UI->>UI: без перезагрузки — DOM уже перестроен, статус «сохранено»
Note over AC,DB: один запрос применяет и порядок, и перенос между родителями
+ Note over UI: раскрытые разделы и позиция скролла сохраняются (localStorage / ui-state.js)
+```
+
+## 12. Синхронизация таймеров защиты (опрос раз в 10 секунд)
+
+```mermaid
+sequenceDiagram
+ autonumber
+ actor A as Преподаватель
+ actor S as Студент
+ participant W as web
+ participant P as progress
+
+ loop каждые 10 c (страница группы)
+ A->>W: GET /admin/defense-watch?labs=...
+ W->>P: GET /api/v1/defenses/watch?labs=...
+ P-->>W: статусы, secondsLeft, readyReason
+ W-->>A: обновление ячеек; переход в ready → тост «ФИО — ЛР: Готов к сдаче / Вышло время»
+ end
+
+ loop каждые 10 c (страница лабы студента)
+ S->>W: GET /student/labs/{id}/defense-status
+ W->>P: GET /api/v1/me/defenses/{labId}/status
+ P-->>W: status / secondsLeft / readyReason
+ W-->>S: подстройка таймера; при status != started — перезагрузка страницы
+ end
```
diff --git a/docs/05-states.md b/docs/05-states.md
index e1851f4..fed403e 100644
--- a/docs/05-states.md
+++ b/docs/05-states.md
@@ -17,23 +17,32 @@ stateDiagram-v2
end note
```
-## 2. Публикация оценок (пара «группа + лабораторная»)
+## 2. Публикация оценок (пара «студент + лабораторная»)
```mermaid
stateDiagram-v2
[*] --> hidden : оценка выставлена (lab_grades)
- hidden --> published : «опубликовать оценки» (grade_publications)
- published --> hidden : «скрыть оценки»
+ hidden --> published : чекбокс публикации в ячейке студент×ЛР (grade_publication_students)
+ published --> hidden : снять чекбокс
+ hidden --> published : «опубликовать всем» (глаз в шапке ЛР)
+ published --> hidden : «скрыть у всех»
note right of hidden
- студент не видит ни балл,
- ни комментарии
+ студент не видит ни балл, ни комментарии.
+ Проверка доступа — по (student, lab):
+ групповая публикация (легаси, grade_publications)
+ открывает всем, иначе — по списку студентов.
end note
note right of published
- студенты группы видят балл и
- комментарии, сгруппированные по тону
+ видно только выбранным студентам; подсказки
+ комментариев показывают частоту и позволяют
+ удалять шаблоны с частотой 0
end note
```
+Кнопки: чекбокс в ячейке публикует/скрывает оценку конкретному студенту (AJAX, без перезагрузки,
+тост с «N из M»); «глаз» в шапке лабораторной — массово «всем/скрыть у всех» (частичное состояние —
+жёлтый со счётчиком).
+
## 3. Статус лабораторной (`labs.status`)
```mermaid
@@ -113,7 +122,7 @@ stateDiagram-v2
stateDiagram-v2
[*] --> temporary : аккаунт создан / «Сбросить пароль»
temporary --> own : смена на /change-password
- own --> temporary : «Сбросить пароль» преподавателем
+ own --> temporary : «Сбросить пароль» администратором
note right of temporary
must_change_password = true,
temp_password хранится;
diff --git a/docs/06-authoring.md b/docs/06-authoring.md
index a1a549a..77101eb 100644
--- a/docs/06-authoring.md
+++ b/docs/06-authoring.md
@@ -89,16 +89,20 @@ printf("Ответ: 42\n");
Администратору доступны действия не только на главной `/learn`, но и на каждой странице:
-* **статья** — «Править» (правка прямо на странице: текст статьи открывается в редакторе
+* **статья** — иконка-карандаш (правка прямо на странице: текст статьи открывается в редакторе
с панелью форматирования — жирный, курсив, заголовки, списки, ссылки, код, таблицы,
картинки, видео, спойлер), «Скрыть/Опубликовать», «Удалить», а также навигация
**← предыдущая / следующая →** (работает и между разделами) и **боковое содержание**
со всеми главами, разделами и статьями;
* **раздел** — «+ Статья», «Переименовать раздел», «Скрыть/Опубликовать»,
- «Перенести статьи и удалить», «Удалить раздел»; у каждой статьи в списке — «Править»
+ «Перенести статьи и удалить», «Удалить раздел»; у каждой статьи в списке — иконка-карандаш
(открывает статью с готовым окном правки), скрытие и удаление;
* **глава** — «+ Раздел», «Переименовать главу», «Скрыть/Опубликовать», «Удалить главу»;
- у каждого раздела — переименование, скрытие, перенос статей и удаление, у каждой статьи — «Править».
+ у каждого раздела — переименование, скрытие, перенос статей и удаление, у каждой статьи — иконка-карандаш.
+
+На странице статьи боковое содержание: на десктопе закреплено (своя прокрутка), на мобильных
+открывается кнопкой «Содержание» справа. На главной `/learn` на мобильных три кнопки
+(«+ Статья», «+ Глава», «Импорт zip») идут под заголовком в один ряд.
Порядок меняется перетаскиванием за значок **⠿** (сохраняется сразу, без перезагрузки страницы):
на главной `/learn` — главы, разделы и статьи (в том числе перенос раздела в другую главу и
diff --git a/docs/README.md b/docs/README.md
index 562104c..bcaf253 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -19,13 +19,14 @@ Preview Mermaid Support) и в IntelliJ IDEA (плагин Mermaid).
## Кратко о системе
LMS — микросервисное приложение на Kotlin/Spring Boot 3 (Java 21) с
-server-side-rendered UI на Thymeleaf + htmx:
+server-side-rendered UI: Thymeleaf + Bootstrap 5 (CSS и иконки вендорены локально)
+и собственный JS (htmx не используется):
- **gateway** (`:8080`) — единая точка входа для REST API: маршрутизация и проверка JWT;
- **identity-service** (`:8081`) — аккаунты, группы, студенты, аутентификация (JWT + JWKS);
- **academic-service** (`:8082`) — семестры, лабораторные (уровни, варианты), учебные материалы;
- **progress-service** (`:8083`) — варианты/уровни студентов, сдачи, оценки и комментарии, защиты;
-- **web** (`:8090`) — BFF: сессии, CSRF, страницы админа и студента, markdown-рендер;
+- **web** (`:8090`) — BFF: сессии, CSRF, страницы админа и студента, markdown-рендер, Static (Bootstrap/Icons);
- **PostgreSQL 18** — одна БД `lms` со схемами `identity`, `academic`, `progress`.
Поток запроса: браузер → nginx → web → gateway → сервис → PostgreSQL.