UI rework: modal dialogs, inline lab editing, manual creation, content management

- Dashboard: real stats (groups, students, labs, average grade), labs/groups
  summary lists, no placeholder tile
- Lab page: view + edit combined — metadata form in modal, condition rendered
  as markdown, per-level variant tables with add/edit/delete dialogs,
  level rename/delete, student level/variant selectors with group filter
- Labs list: create-lab dialog with manual level entry (code; title lines)
- Groups: create-group dialog, add-student dialog (auto password, toast card)
- Content: public /learn reader is the single place; admin buttons (new
  article with chapter/section create-or-select, rename/delete chapters and
  sections, move articles between sections, import zip) appear for admins;
  removed separate 'Контент' admin section
- Buttons use btn classes instead of nav-link
- Favicon: generated SVG book icon
- Security: CSRF synchronizer token interceptor (replaced spring-security in
  web which broke logback under nested jars), session regenerated on login,
  hardened session cookie, gateway actuator limited to health,info
- Jackson 3: nullable-safe request DTOs for labs (FAIL_ON_NULL_FOR_PRIMITIVES)
- academic: POST /labs (manual create with levels), level add/rename/delete,
  content chapters/sections CRUD, article create/move/delete endpoints
This commit is contained in:
2026-09-11 16:00:56 +03:00
parent cca20b5416
commit 08890875e5
45 changed files with 2435 additions and 86 deletions

View File

@@ -10,6 +10,7 @@
<p class="text-sm text-muted mt-1">Студентов: <span th:text="${#lists.size(group.students)}">0</span></p>
</div>
<div class="flex gap-2">
<button class="btn btn-primary" type="button" onclick="openModal('modal-add-student')">+ Студент</button>
<a class="btn btn-secondary" th:href="@{/admin/groups/{id}/credentials(id=${group.id}, format='xlsx')}">Выгрузить XLSX</a>
<a class="btn btn-secondary" th:href="@{/admin/groups/{id}/credentials(id=${group.id}, format='csv')}">CSV</a>
</div>
@@ -67,6 +68,7 @@
<input class="input input-sm input-score" name="score" type="number" step="0.1" min="0" max="5"
th:value="${score}">
<button class="btn btn-secondary btn-sm" type="submit">OK</button>
<input type="hidden" name="_csrf" th:value="${csrfToken}">
</form>
</td>
<td class="text-center font-semibold">
@@ -95,7 +97,8 @@
<span th:unless="${student.mustChangePassword}" class="badge badge-ok">изменён</span></td>
<td class="text-center">
<form th:action="@{/admin/groups/{gid}/students/{sid}/reset-password(gid=${group.id}, sid=${student.id})}" method="post">
<button class="nav-link text-sm" type="submit">Сбросить</button>
<button class="btn btn-danger btn-sm" type="submit">Сбросить</button>
<input type="hidden" name="_csrf" th:value="${csrfToken}">
</form>
</td>
</tr>
@@ -104,5 +107,42 @@
</table>
</div>
</div>
<div th:if="${createdStudent}" class="card import-card toast">
<div class="card-body">
<h2 class="section-title">Студент добавлен</h2>
<p class="text-sm" th:text="${createdStudent.fullName}">—</p>
<p class="text-sm">Логин: <code class="code font-mono" th:text="${createdStudent.studentNo}">—</code></p>
<p class="text-sm">Пароль: <code class="code font-mono font-semibold" th:text="${createdStudent.password}">—</code></p>
</div>
</div>
<dialog id="modal-add-student" class="modal">
<div class="modal-header">
<h2 class="modal-title">Добавить студента</h2>
<button class="modal-close" type="button" onclick="closeModal('modal-add-student')">✕</button>
</div>
<div class="modal-body">
<form class="form-stack" th:action="@{/admin/groups/{gid}/students/create(gid=${group.id})}" method="post">
<input type="hidden" name="_csrf" th:value="${csrfToken}">
<div class="grid grid-2">
<div><label class="label" for="stNo">Студенческий номер (логин)</label>
<input class="input" id="stNo" name="studentNo" required></div>
<div><label class="label" for="stBasis">Основа обучения</label>
<input class="input" id="stBasis" name="basis" placeholder="бюджет"></div>
</div>
<div><label class="label" for="stLast">Фамилия</label>
<input class="input" id="stLast" name="lastName" required></div>
<div class="grid grid-2">
<div><label class="label" for="stFirst">Имя</label>
<input class="input" id="stFirst" name="firstName"></div>
<div><label class="label" for="stMiddle">Отчество</label>
<input class="input" id="stMiddle" name="middleName"></div>
</div>
<p class="hint">Пароль будет сгенерирован автоматически.</p>
<button class="btn btn-primary" type="submit">Добавить</button>
</form>
</div>
</dialog>
</body>
</html>