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 095e337136
commit 006951adae
44 changed files with 2435 additions and 85 deletions

View File

@@ -4,11 +4,14 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title th:replace="${title}">LMS</title>
<link rel="icon" type="image/svg+xml" th:href="@{/favicon.svg}">
<link rel="stylesheet" th:href="@{/css/app.css}">
<script th:src="@{/js/htmx.min.js}" defer></script>
<script>
(function(){ var t = localStorage.getItem('lms-theme') || 'light'; document.documentElement.setAttribute('data-theme', t); })();
function toggleTheme(){ var e=document.documentElement; var t=e.getAttribute('data-theme')==='dark'?'light':'dark'; e.setAttribute('data-theme',t); localStorage.setItem('lms-theme',t); }
function openModal(id){ var d=document.getElementById(id); if(d) d.showModal(); }
function closeModal(id){ var d=document.getElementById(id); if(d) d.close(); }
</script>
</head>
<body>
@@ -16,13 +19,17 @@
<header class="page-header">
<div class="page-header-inner">
<a th:href="@{/}" class="logo">LMS</a>
<nav class="nav" th:if="${currentUser != null}">
<a th:if="${currentUser.admin}" th:href="@{/admin/groups}" class="nav-link">Группы</a>
<a th:if="${currentUser.admin}" th:href="@{/admin/labs}" class="nav-link">Лабораторные</a>
<span class="nav-sep" th:text="${currentUser.name}">—</span>
<nav class="nav">
<a th:href="@{/learn}" class="nav-link">Материалы</a>
<th:block th:if="${currentUser != null}">
<a th:if="${currentUser.admin}" th:href="@{/admin/groups}" class="nav-link">Группы</a>
<a th:if="${currentUser.admin}" th:href="@{/admin/labs}" class="nav-link">Лабораторные</a>
<span class="nav-sep" th:text="${currentUser.name}">—</span>
</th:block>
<button onclick="toggleTheme()" class="btn btn-secondary btn-sm">☀/🌙</button>
<form th:action="@{/logout}" method="post">
<form th:if="${currentUser != null}" th:action="@{/logout}" method="post">
<button class="btn btn-secondary" type="submit">Выйти</button>
<input type="hidden" name="_csrf" th:value="${csrfToken}">
</form>
</nav>
</div>