docs: актуализация под текущее состояние (Bootstrap вместо htmx, RestClient, интерцепторы, контроллеры, публикация по студентам, watch-опрос, автосейв без перезагрузки)

This commit is contained in:
2026-10-13 15:59:37 +03:00
parent 4fa400ddb1
commit b1f00fd776
7 changed files with 152 additions and 45 deletions

View File

@@ -6,7 +6,7 @@
flowchart LR flowchart LR
user["Преподаватель / студент<br/>(браузер)"] user["Преподаватель / студент<br/>(браузер)"]
nginx["nginx<br/>lms.our-place.ru<br/>TLS, reverse proxy"] nginx["nginx<br/>lms.our-place.ru<br/>TLS, reverse proxy"]
web["web BFF<br/>Thymeleaf + htmx<br/>:8090"] web["web BFF<br/>Thymeleaf + Bootstrap<br/>:8090"]
gw["gateway<br/>Spring Cloud Gateway<br/>:8080"] gw["gateway<br/>Spring Cloud Gateway<br/>:8080"]
id["identity-service<br/>:8081"] id["identity-service<br/>:8081"]
ac["academic-service<br/>:8082"] ac["academic-service<br/>:8082"]
@@ -44,9 +44,9 @@ flowchart LR
```mermaid ```mermaid
flowchart TB flowchart TB
subgraph web["web (BFF, :8090)"] subgraph web["web (BFF, :8090)"]
wc["@Controller<br/>AuthPage, Learn, Student, Admin, ContentAdmin"] wc["@Controller: AuthPage, Learn, Student,<br/>AdminGroup/Lab/Student/Distribute/Defense/Users, ContentAdmin<br/>@RestController: ContentAssetProxy"]
wi["Interceptor'ы:<br/>Csrf, Auth, Admin, MustChangePassword"] wi["Interceptor'ы:<br/>Csrf, NoCachePage, MustChangePassword, Auth, Admin"]
wapi["ApiClient (WebClient)<br/>+ DTO"] wapi["ApiClient (RestClient)<br/>+ DTO"]
wmd["MarkdownRenderer<br/>(libs:markdown)"] wmd["MarkdownRenderer<br/>(libs:markdown)"]
wc --> wapi wc --> wapi
wc --> wmd wc --> wmd
@@ -67,16 +67,16 @@ flowchart TB
end end
subgraph ac["academic-service (:8082)"] subgraph ac["academic-service (:8082)"]
acc["LabController, ContentPublicController,<br/>ContentAdminController"] acc["LabController, ContentPublicController,<br/>ContentAdminController, ContentOrderController"]
acs["LabService, ContentService,<br/>LabImportParser, AssetStorage"] acs["LabService, ContentService,<br/>LabImportParser, AssetStorage"]
acr["LabRepository, LabLevelRepository, VariantRepository,<br/>SemesterRepository, Content*Repository"] acr["LabRepository, LabLevelRepository, VariantRepository,<br/>SemesterRepository, Content*Repository"]
acc --> acs --> acr acc --> acs --> acr
end end
subgraph pr["progress-service (:8083)"] subgraph pr["progress-service (:8083)"]
prc["ProgressController, DefenseAdminController,<br/>DefenseMyController"] prc["ProgressController, SubmissionController, DefenseAdminController,<br/>DefenseMyController, StudentPurgeController, GroupLabDataController"]
prs["ProgressService, SubmissionService,<br/>DefenseService, GradePublicationService"] prs["ProgressService, SubmissionService, DefenseService,<br/>GradePublicationService, StudentPurgeService"]
prr["VariantAssignmentRepository, StudentLabLevelRepository,<br/>LabGradeRepository, GradeCommentRepository,<br/>CommentTemplateRepository, SubmissionRepository,<br/>DefenseSessionRepository, GradePublicationRepository"] prr["VariantAssignmentRepository, StudentLabLevelRepository,<br/>LabGradeRepository, GradeCommentRepository, CommentTemplateRepository,<br/>SubmissionRepository, DefenseSessionRepository,<br/>GradePublicationRepository, GradePublicationStudentRepository"]
prcl["AcademicClient, IdentityClient"] prcl["AcademicClient, IdentityClient"]
prc --> prs prc --> prs
prs --> prr prs --> prr
@@ -150,7 +150,8 @@ flowchart TD
```mermaid ```mermaid
flowchart LR flowchart LR
req["HTTP-запрос"] --> csrf["CsrfInterceptor /**<br/>GET: выдаёт token в сессию<br/>POST: сверяет _csrf"] req["HTTP-запрос"] --> csrf["CsrfInterceptor /**<br/>GET: выдаёт token в сессию<br/>POST: сверяет _csrf"]
csrf --> must["MustChangePasswordInterceptor /**<br/>(кроме /change-password, /logout, /login, статики)"] csrf --> nocahe["NoCachePageInterceptor /**<br/>(страницы без кэша; /css, /js исключены)"]
nocahe --> must["MustChangePasswordInterceptor /**<br/>(кроме /change-password, /logout, /login, статики)"]
must --> auth["AuthInterceptor<br/>/admin/**, /student/**, /change-password"] must --> auth["AuthInterceptor<br/>/admin/**, /student/**, /change-password"]
auth --> adm["AdminInterceptor /admin/**"] auth --> adm["AdminInterceptor /admin/**"]
adm --> ctrl["Контроллер → ApiClient → gateway"] adm --> ctrl["Контроллер → ApiClient → gateway"]

View File

@@ -47,6 +47,8 @@ flowchart TB
| gateway | `target: gateway` | 8080 | `LMS_IDENTITY_URI`, `LMS_ACADEMIC_URI`, `LMS_PROGRESS_URI`, `LMS_JWT_JWK_SET_URI` | — | | 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` | — | | 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 — `LMS_JWT_JWK_SET_URI` обязателен для gateway, academic и progress —
в контейнерах это `http://identity:8081/api/v1/auth/jwks.json` (иначе 401). в контейнерах это `http://identity:8081/api/v1/auth/jwks.json` (иначе 401).
@@ -99,9 +101,10 @@ sequenceDiagram
DC->>DC: docker build (5 стадий) DC->>DC: docker build (5 стадий)
DC->>PG: старт postgres (volume lms_pgdata) DC->>PG: старт postgres (volume lms_pgdata)
DC->>DC: старт identity → academic → progress → gateway → web DC->>DC: старт identity → academic → progress → gateway → web
Note over DC: Flyway применяет миграции<br/>V1..V5 автоматически при старте сервисов Note over DC: Flyway применяет миграции автоматически<br/>при старте сервисов (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`, - Резервные копии: `pg_dump` (том `lms_pgdata`) и `tar` томов `lms_assets`,
`lms_submissions` — команды в [deploy/DEPLOY.md](../deploy/DEPLOY.md). `lms_submissions` — команды в [deploy/DEPLOY.md](../deploy/DEPLOY.md).
- Порядок наполнения данными (админ → группы → лабораторная → варианты → - Порядок наполнения данными (админ → группы → лабораторная → варианты →

View File

@@ -182,6 +182,7 @@ classDiagram
class LabController class LabController
class ContentPublicController class ContentPublicController
class ContentAdminController class ContentAdminController
class ContentOrderController
class LabService { class LabService {
+importFromFile(String, ByteArray) LabImportResult +importFromFile(String, ByteArray) LabImportResult
+list() List~LabSummary~ +list() List~LabSummary~
@@ -234,6 +235,7 @@ classDiagram
LabController --> LabService LabController --> LabService
ContentPublicController --> ContentService ContentPublicController --> ContentService
ContentAdminController --> ContentService ContentAdminController --> ContentService
ContentOrderController --> ContentService
LabService --> LabRepository LabService --> LabRepository
LabService --> LabLevelRepository LabService --> LabLevelRepository
LabService --> VariantRepository LabService --> VariantRepository
@@ -274,6 +276,12 @@ classDiagram
+UUID labId +UUID labId
+Instant publishedAt +Instant publishedAt
} }
class GradePublicationStudent {
+UUID groupId
+UUID labId
+UUID studentId
+Instant publishedAt
}
class GradeComment { class GradeComment {
+UUID id +UUID id
+UUID studentId +UUID studentId
@@ -313,6 +321,8 @@ classDiagram
ready ready
} }
GradePublication "1" --> "*" GradePublicationStudent : легаси «вся группа» и персональные
Note for GradePublicationStudent : персональная публикация оценки/комментариев студенту
CommentTemplate "1" --> "*" GradeComment : подсказки (text/tone) CommentTemplate "1" --> "*" GradeComment : подсказки (text/tone)
GradeComment "*" --> "1" LabGrade : поясняет оценку GradeComment "*" --> "1" LabGrade : поясняет оценку
LabGrade "1" --> "0..1" GradePublication : видна студенту после публикации LabGrade "1" --> "0..1" GradePublication : видна студенту после публикации
@@ -324,8 +334,11 @@ classDiagram
```mermaid ```mermaid
classDiagram classDiagram
class ProgressController class ProgressController
class SubmissionController
class DefenseAdminController class DefenseAdminController
class DefenseMyController class DefenseMyController
class StudentPurgeController
class GroupLabDataController
class ProgressService { class ProgressService {
+setLevel(LevelAssignmentRequest, String) LevelAssignmentResponse +setLevel(LevelAssignmentRequest, String) LevelAssignmentResponse
+getLevel(UUID, UUID) StudentLabLevel +getLevel(UUID, UUID) StudentLabLevel
@@ -334,7 +347,7 @@ classDiagram
+assignWithInfo(AssignRequest, String) VariantAssignmentResponse +assignWithInfo(AssignRequest, String) VariantAssignmentResponse
+assignmentsForLabEnriched(UUID, String) List~VariantAssignmentResponse~ +assignmentsForLabEnriched(UUID, String) List~VariantAssignmentResponse~
+setGrade(GradeSetRequest) GradeResponse +setGrade(GradeSetRequest) GradeResponse
+suggestComments(String) List~CommentOut~ +suggestComments(String) List~CommentSuggestion~
+myComments(UUID) List~GradeComment~ +myComments(UUID) List~GradeComment~
+getStudentAssignment(UUID, UUID, String) MeAssignmentResponse +getStudentAssignment(UUID, UUID, String) MeAssignmentResponse
} }
@@ -356,10 +369,12 @@ classDiagram
+resetByStudentLab(UUID, UUID) +resetByStudentLab(UUID, UUID)
} }
class GradePublicationService { class GradePublicationService {
+setPublished(UUID, UUID, Boolean) +state(UUID, UUID) PublicationState
+isPublished(UUID, UUID) Boolean +setStudents(UUID, UUID, List~UUID~)
+publishedLabIdsForGroup(UUID) List~String~ +isPublished(UUID, UUID, UUID) Boolean
+publishedGroupIdsForLab(UUID) List~String~ }
class StudentPurgeService {
+purge(UUID)
} }
class AcademicClient { class AcademicClient {
+getLab(String, String) LabInfo +getLab(String, String) LabInfo
@@ -383,12 +398,17 @@ classDiagram
class SubmissionRepository class SubmissionRepository
class DefenseSessionRepository class DefenseSessionRepository
class GradePublicationRepository class GradePublicationRepository
class GradePublicationStudentRepository
ProgressController --> ProgressService ProgressController --> ProgressService
SubmissionController --> SubmissionService
DefenseAdminController --> DefenseService DefenseAdminController --> DefenseService
DefenseMyController --> DefenseService DefenseMyController --> DefenseService
StudentPurgeController --> StudentPurgeService
GroupLabDataController --> ProgressService
ProgressController --> SubmissionService ProgressController --> SubmissionService
ProgressController --> GradePublicationService ProgressController --> GradePublicationService
StudentPurgeService --> SubmissionStorage
ProgressService --> VariantAssignmentRepository ProgressService --> VariantAssignmentRepository
ProgressService --> StudentLabLevelRepository ProgressService --> StudentLabLevelRepository
ProgressService --> LabGradeRepository ProgressService --> LabGradeRepository
@@ -401,6 +421,7 @@ classDiagram
SubmissionService --> AcademicClient SubmissionService --> AcademicClient
DefenseService --> DefenseSessionRepository DefenseService --> DefenseSessionRepository
GradePublicationService --> GradePublicationRepository GradePublicationService --> GradePublicationRepository
GradePublicationService --> GradePublicationStudentRepository
``` ```
## 4.web (BFF) ## 4.web (BFF)
@@ -410,13 +431,29 @@ classDiagram
class AuthPageController class AuthPageController
class LearnController class LearnController
class StudentController class StudentController
class AdminController class AdminGroupController
class AdminLabController
class AdminStudentController
class AdminDistributeController
class AdminDefenseController
class AdminUsersController
class ContentAdminController class ContentAdminController
class ContentAssetProxy
class ApiClient { class ApiClient {
+login(String, String) AuthResponseDto +login(String, String) AuthResponseDto
+contentTree(String, Boolean) ContentTreeNodeDto +contentTree(String, Boolean) ContentTreeNodeDto
+contentArticle(String, String, String, String, Boolean) ContentArticleDto +contentArticle(String, String, String, String, Boolean) ContentArticleDto
+setContentPublished(String, String, String, Boolean) +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 +groupDetail(String, String) GroupDetailDto
+distribute(String, DistributeRequestPayload) DistributeResultDto +distribute(String, DistributeRequestPayload) DistributeResultDto
+setGrade(String, String, String, String, String) GradeResponseDto +setGrade(String, String, String, String, String) GradeResponseDto
@@ -437,6 +474,7 @@ classDiagram
+String USER +String USER
} }
class CsrfInterceptor class CsrfInterceptor
class NoCachePageInterceptor
class AuthInterceptor class AuthInterceptor
class AdminInterceptor class AdminInterceptor
class MustChangePasswordInterceptor class MustChangePasswordInterceptor
@@ -447,8 +485,14 @@ classDiagram
AuthPageController --> ApiClient AuthPageController --> ApiClient
LearnController --> ApiClient LearnController --> ApiClient
StudentController --> ApiClient StudentController --> ApiClient
AdminController --> ApiClient AdminGroupController --> ApiClient
AdminLabController --> ApiClient
AdminStudentController --> ApiClient
AdminDistributeController --> ApiClient
AdminDefenseController --> ApiClient
AdminUsersController --> ApiClient
ContentAdminController --> ApiClient ContentAdminController --> ApiClient
ContentAssetProxy --> ApiClient
LearnController --> MarkdownRenderer LearnController --> MarkdownRenderer
AuthPageController --> SessionUser AuthPageController --> SessionUser
AuthPageController --> SessionKeys AuthPageController --> SessionKeys
@@ -544,7 +588,7 @@ erDiagram
} }
CHAPTERS { CHAPTERS {
uuid id PK uuid id PK
varchar title UK varchar title
varchar slug UK varchar slug UK
int position int position
boolean published boolean published
@@ -598,6 +642,12 @@ erDiagram
uuid lab_id PK uuid lab_id PK
timestamptz published_at timestamptz published_at
} }
GRADE_PUBLICATION_STUDENTS {
uuid group_id PK
uuid lab_id PK
uuid student_id PK
timestamptz published_at
}
GRADE_COMMENTS { GRADE_COMMENTS {
uuid id PK uuid id PK
uuid student_id uuid student_id
@@ -636,6 +686,7 @@ erDiagram
STUDENT_LAB_LEVELS }o--|| LABS : "lab_id (не FK)" STUDENT_LAB_LEVELS }o--|| LABS : "lab_id (не FK)"
LAB_GRADES }o--|| LABS : "lab_id (не FK)" LAB_GRADES }o--|| LABS : "lab_id (не FK)"
GRADE_PUBLICATIONS }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)" GRADE_COMMENTS }o--|| LABS : "lab_id (не FK)"
SUBMISSIONS }o--|| LABS : "lab_id (не FK)" SUBMISSIONS }o--|| LABS : "lab_id (не FK)"
DEFENSE_SESSIONS }o--|| LABS : "lab_id (не FK)" DEFENSE_SESSIONS }o--|| LABS : "lab_id (не FK)"

View File

@@ -146,6 +146,7 @@ sequenceDiagram
P->>DB: INSERT/UPDATE variant_assignments (manual=false) P->>DB: INSERT/UPDATE variant_assignments (manual=false)
P-->>W: DistributeResult(assigned, total) P-->>W: DistributeResult(assigned, total)
W-->>A: «Распределено: N из M» W-->>A: «Распределено: N из M»
Note over A,W: UI — большая модалка «Выдача вариантов» на странице группы<br/>(фрагмент по fetch). Эндпоинт отвечает редиректом — это норма;<br/>модалка обновляется на месте, тост показывается поверх неё
``` ```
## 6. Сдача решения студентом ## 6. Сдача решения студентом
@@ -214,13 +215,13 @@ sequenceDiagram
P-->>W: DefenseDto P-->>W: DefenseDto
W-->>S: «Работа сохранена» W-->>S: «Работа сохранена»
Note over P,DB: DefenseService.syncTimeouts() переводит просроченные<br/>сессии в ready с ready_reason=timeout Note over P,DB: DefenseService.syncTimeouts() переводит просроченные<br/>сессии в ready с ready_reason=timeout; кнопка «Готов» даёт ready_reason=button.<br/>В UI: «Готов к сдаче» (button) / «Вышло время» (timeout)
A->>W: «доп. задание: начать заново» → POST /admin/defenses/{id}/reset A->>W: «доп. задание: начать заново» → POST /admin/defenses/{id}/reset
P->>DB: UPDATE status=waiting, code=NULL, started_at=NULL P->>DB: UPDATE status=waiting, code=NULL, started_at=NULL
W-->>A: сессия снова ждёт кода W-->>A: сессия снова ждёт кода
``` ```
## 8. Оценка, комментарии и публикация ## 8. Оценка, комментарии и публикация (по студенту)
```mermaid ```mermaid
sequenceDiagram sequenceDiagram
@@ -235,19 +236,28 @@ sequenceDiagram
W->>P: POST /api/v1/grades (GradeSetRequest) W->>P: POST /api/v1/grades (GradeSetRequest)
P->>DB: UPSERT lab_grades (student_id, lab_id, score) P->>DB: UPSERT lab_grades (student_id, lab_id, score)
P->>DB: DELETE grade_comments по паре, затем INSERT новых P->>DB: DELETE grade_comments по паре, затем INSERT новых
P->>DB: UPSERT comment_templates (use_count++ для автоподсказок) Note over DB: comment_templates — подсказки; частота считается<br/>по актуальным grade_comments (не по use_count)
P-->>W: GradeResponse P-->>W: GradeResponse
Note over DB: Студент оценку пока НЕ видит Note over DB: Студент оценку пока НЕ видит
A->>W: «опубликовать оценки» → POST /admin/groups/{gid}/publish A->>W: чекбокс публикации в ячейке (student×lab) → POST /admin/groups/{gid}/publish-student
W->>P: POST /api/v1/grades/publish (labId, groupId, published=true) W->>P: POST /api/v1/grades/publication {groupId, labId, studentIds[]} — заменяет набор
P->>DB: INSERT/DELETE grade_publications (group_id, lab_id) P->>DB: DELETE/INSERT grade_publication_students<br/>(легаси grade_publications снимается)
P-->>W: 200 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} S->>W: GET /student/labs/{labId}
W->>P: GET /api/v1/me/grades?groupId=... (Bearer) W->>P: GET /api/v1/me/grades?groupId=... (Bearer)
P->>DB: SELECT lab_grades + grade_comments + grade_publications P->>DB: SELECT lab_grades + grade_comments; фильтр isPublished(group, lab, student)
P-->>W: только опубликованные оценки группы P-->>W: только опубликованные этому студенту оценки
W-->>S: оценка и комментарии списками: позитивный / негативный / нейтральный W-->>S: оценка и комментарии списками: позитивный / негативный / нейтральный
``` ```
@@ -314,16 +324,18 @@ sequenceDiagram
G->>ID: GroupService.detail (студенты по ФИО) G->>ID: GroupService.detail (студенты по ФИО)
W->>G: GET /api/v1/labs W->>G: GET /api/v1/labs
G->>AC: LabService.list (опубликованные ЛР) G->>AC: LabService.list (опубликованные ЛР)
W->>G: GET /api/v1/assignments/group/{gid}
G->>P: уровни и варианты всех студентов группы (bulk)
loop по каждой лабораторной loop по каждой лабораторной
W->>G: GET /api/v1/assignments?labId
G->>P: варианты студентов
W->>G: GET /api/v1/submissions?labId W->>G: GET /api/v1/submissions?labId
G->>P: последние сдачи G->>P: последние сдачи
W->>G: GET /api/v1/defenses?labId W->>G: GET /api/v1/defenses?labId
G->>P: сессии защиты G->>P: сессии защиты
W->>G: GET /api/v1/grades/publication?groupId&labId
G->>P: состояние публикации {all, studentIds}
end end
W->>G: GET /api/v1/grades + /grades/publications?groupId W->>G: GET /api/v1/grades
G->>P: оценки G->>P: все оценки (матрица)
W->>W: сборка матрицы «студент × лабораторная» W->>W: сборка матрицы «студент × лабораторная»
W-->>A: страница группы (оценки, защиты, поиск, фильтры, сортировка) W-->>A: страница группы (оценки, защиты, поиск, фильтры, сортировка)
``` ```
@@ -351,6 +363,32 @@ sequenceDiagram
AC->>DB: UPDATE chapters.position,<br/>sections.chapter_id/position,<br/>articles.section_id/position AC->>DB: UPDATE chapters.position,<br/>sections.chapter_id/position,<br/>articles.section_id/position
AC-->>W: 200 AC-->>W: 200
W-->>UI: {"ok": true} W-->>UI: {"ok": true}
UI->>UI: location.reload() — страница из серверного состояния UI->>UI: без перезагрузки — DOM уже перестроен, статус «сохранено»
Note over AC,DB: один запрос применяет и порядок, и перенос между родителями 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
``` ```

View File

@@ -17,23 +17,32 @@ stateDiagram-v2
end note end note
``` ```
## 2. Публикация оценок (пара «группа + лабораторная») ## 2. Публикация оценок (пара «студент + лабораторная»)
```mermaid ```mermaid
stateDiagram-v2 stateDiagram-v2
[*] --> hidden : оценка выставлена (lab_grades) [*] --> hidden : оценка выставлена (lab_grades)
hidden --> published : «опубликовать оценки» (grade_publications) hidden --> published : чекбокс публикации в ячейке студент×ЛР (grade_publication_students)
published --> hidden : «скрыть оценки» published --> hidden : снять чекбокс
hidden --> published : «опубликовать всем» (глаз в шапке ЛР)
published --> hidden : «скрыть у всех»
note right of hidden note right of hidden
студент не видит ни балл, студент не видит ни балл, ни комментарии.
ни комментарии Проверка доступа — по (student, lab):
групповая публикация (легаси, grade_publications)
открывает всем, иначе — по списку студентов.
end note end note
note right of published note right of published
студенты группы видят балл и видно только выбранным студентам; подсказки
комментарии, сгруппированные по тону комментариев показывают частоту и позволяют
удалять шаблоны с частотой 0
end note end note
``` ```
Кнопки: чекбокс в ячейке публикует/скрывает оценку конкретному студенту (AJAX, без перезагрузки,
тост с «N из M»); «глаз» в шапке лабораторной — массово «всем/скрыть у всех» (частичное состояние —
жёлтый со счётчиком).
## 3. Статус лабораторной (`labs.status`) ## 3. Статус лабораторной (`labs.status`)
```mermaid ```mermaid
@@ -113,7 +122,7 @@ stateDiagram-v2
stateDiagram-v2 stateDiagram-v2
[*] --> temporary : аккаунт создан / «Сбросить пароль» [*] --> temporary : аккаунт создан / «Сбросить пароль»
temporary --> own : смена на /change-password temporary --> own : смена на /change-password
own --> temporary : «Сбросить пароль» преподавателем own --> temporary : «Сбросить пароль» администратором
note right of temporary note right of temporary
must_change_password = true, must_change_password = true,
temp_password хранится; temp_password хранится;

View File

@@ -89,16 +89,20 @@ printf("Ответ: 42\n");
Администратору доступны действия не только на главной `/learn`, но и на каждой странице: Администратору доступны действия не только на главной `/learn`, но и на каждой странице:
* **статья** — «Править» (правка прямо на странице: текст статьи открывается в редакторе * **статья** — иконка-карандаш (правка прямо на странице: текст статьи открывается в редакторе
с панелью форматирования — жирный, курсив, заголовки, списки, ссылки, код, таблицы, с панелью форматирования — жирный, курсив, заголовки, списки, ссылки, код, таблицы,
картинки, видео, спойлер), «Скрыть/Опубликовать», «Удалить», а также навигация картинки, видео, спойлер), «Скрыть/Опубликовать», «Удалить», а также навигация
**← предыдущая / следующая →** (работает и между разделами) и **боковое содержание** **← предыдущая / следующая →** (работает и между разделами) и **боковое содержание**
со всеми главами, разделами и статьями; со всеми главами, разделами и статьями;
* **раздел** — «+ Статья», «Переименовать раздел», «Скрыть/Опубликовать», * **раздел** — «+ Статья», «Переименовать раздел», «Скрыть/Опубликовать»,
«Перенести статьи и удалить», «Удалить раздел»; у каждой статьи в списке — «Править» «Перенести статьи и удалить», «Удалить раздел»; у каждой статьи в списке — иконка-карандаш
(открывает статью с готовым окном правки), скрытие и удаление; (открывает статью с готовым окном правки), скрытие и удаление;
* **глава** — «+ Раздел», «Переименовать главу», «Скрыть/Опубликовать», «Удалить главу»; * **глава** — «+ Раздел», «Переименовать главу», «Скрыть/Опубликовать», «Удалить главу»;
у каждого раздела — переименование, скрытие, перенос статей и удаление, у каждой статьи — «Править». у каждого раздела — переименование, скрытие, перенос статей и удаление, у каждой статьи — иконка-карандаш.
На странице статьи боковое содержание: на десктопе закреплено (своя прокрутка), на мобильных
открывается кнопкой «Содержание» справа. На главной `/learn` на мобильных три кнопки
(«+ Статья», «+ Глава», «Импорт zip») идут под заголовком в один ряд.
Порядок меняется перетаскиванием за значок **⠿** (сохраняется сразу, без перезагрузки страницы): Порядок меняется перетаскиванием за значок **⠿** (сохраняется сразу, без перезагрузки страницы):
на главной `/learn` — главы, разделы и статьи (в том числе перенос раздела в другую главу и на главной `/learn` — главы, разделы и статьи (в том числе перенос раздела в другую главу и

View File

@@ -19,13 +19,14 @@ Preview Mermaid Support) и в IntelliJ IDEA (плагин Mermaid).
## Кратко о системе ## Кратко о системе
LMS — микросервисное приложение на Kotlin/Spring Boot 3 (Java 21) с 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; - **gateway** (`:8080`) — единая точка входа для REST API: маршрутизация и проверка JWT;
- **identity-service** (`:8081`) — аккаунты, группы, студенты, аутентификация (JWT + JWKS); - **identity-service** (`:8081`) — аккаунты, группы, студенты, аутентификация (JWT + JWKS);
- **academic-service** (`:8082`) — семестры, лабораторные (уровни, варианты), учебные материалы; - **academic-service** (`:8082`) — семестры, лабораторные (уровни, варианты), учебные материалы;
- **progress-service** (`:8083`) — варианты/уровни студентов, сдачи, оценки и комментарии, защиты; - **progress-service** (`:8083`) — варианты/уровни студентов, сдачи, оценки и комментарии, защиты;
- **web** (`:8090`) — BFF: сессии, CSRF, страницы админа и студента, markdown-рендер; - **web** (`:8090`) — BFF: сессии, CSRF, страницы админа и студента, markdown-рендер, Static (Bootstrap/Icons);
- **PostgreSQL 18** — одна БД `lms` со схемами `identity`, `academic`, `progress`. - **PostgreSQL 18** — одна БД `lms` со схемами `identity`, `academic`, `progress`.
Поток запроса: браузер → nginx → web → gateway → сервис → PostgreSQL. Поток запроса: браузер → nginx → web → gateway → сервис → PostgreSQL.