Phase 7: deployment, data import, UI hardening and security fixes

Docker/prod deployment:
- multi-stage Dockerfile (JDK21 build, JRE21 run), docker-compose.prod.yml,
  nginx config for lms.our-place.ru, deploy.sh; DEPLOY/IMPORT guides (UI flow)
- Fix Docker 401: LMS_JWT_JWK_SET_URI for academic/progress; forward-headers

Variant distribution (lab page only):
- round-robin by student level, shared random queue across selected groups,
  least-used variants, manual pins always preserved; occupied/dup labels
- setup page: single form + one «Сохранить всё», batch level/variant for
  selected students (checkboxes)

Grades, defense, materials:
- publication gate + comment lists; defense CSV import (2/3 cols, quotes)
- additional task after defense ("начать заново"); enroll/unenroll students
  from group page; temporary-password lock (no navigation until changed) and
  persistent "Смена пароля"; forced change once
- chapter page /learn/{c}; header: LMS home, Войти, user -> dashboard,
  compact theme toggle; responsive mobile header
- markdown: single newlines are soft breaks; highlight.js with light/dark themes
- lab/content imports ZIP-only (lab.yaml+README+variants/assets,
  chapters tree+assets); duplicate lab numbers rejected; variant ordering
  and date defaults honored

Fixes:
- 500 on student create (@MapsId set before persist); broken dialogs/JS refs in group page
This commit is contained in:
2026-09-14 22:01:46 +03:00
parent 5e60cc3819
commit cdf4fb0ee0
58 changed files with 3967 additions and 884 deletions

View File

@@ -0,0 +1,116 @@
function $id(id) { return document.getElementById(id); }
/* ---------- Оценка + комментарии ---------- */
function openGradeEditor(btn) {
var all = [];
try { all = JSON.parse(btn.getAttribute('data-comments') || '[]'); } catch (e) { all = []; }
var idx = parseInt(btn.getAttribute('data-ci') || '0', 10);
var list = Array.isArray(all[idx]) ? all[idx] : [];
$id('grade-student').value = btn.getAttribute('data-student') || '';
$id('grade-lab').value = btn.getAttribute('data-lab') || '';
$id('grade-who').value = btn.getAttribute('data-student-name') || '';
var title = $id('grade-title');
if (title) title.textContent = (btn.getAttribute('data-lab-name') || '') + ' — ' + (btn.getAttribute('data-student-name') || '');
$id('grade-score').value = btn.getAttribute('data-score') || '';
var box = $id('cs-rows');
box.innerHTML = '';
list.forEach(function (c) { csRow(c.text, c.tone); });
if (!list.length) csRow('', 'neutral');
var d = $id('modal-grade');
if (d && d.showModal) d.showModal();
}
function csRow(text, tone) {
var box = $id('cs-rows');
var row = document.createElement('div');
row.className = 'cs-row';
var inp = document.createElement('input');
inp.className = 'input cs-text';
inp.placeholder = 'Комментарий';
inp.value = text || '';
var sel = document.createElement('select');
sel.className = 'input cs-tone';
[['positive', '+ позитивный'], ['negative', '— негативный'], ['neutral', '○ нейтральный']].forEach(function (o) {
var op = document.createElement('option');
op.value = o[0];
op.textContent = o[1];
sel.appendChild(op);
});
sel.value = tone || 'neutral';
var rm = document.createElement('button');
rm.type = 'button';
rm.className = 'btn btn-secondary btn-sm';
rm.textContent = 'удалить';
rm.onclick = function () { row.remove(); };
row.appendChild(inp);
row.appendChild(sel);
row.appendChild(rm);
box.appendChild(row);
}
function csSerialize() {
var out = [];
document.querySelectorAll('#cs-rows .cs-row').forEach(function (r) {
var t = r.querySelector('.cs-text').value.trim();
if (t) out.push({ text: t, tone: r.querySelector('.cs-tone').value });
});
$id('grade-comments').value = JSON.stringify(out);
}
/* ---------- Индивидуальные задания (защита) ---------- */
function openDefenseForm(btn) {
var form = $id('defense-form');
if (!form) return;
form.reset();
var student = btn.getAttribute('data-student') || '';
var lab = btn.getAttribute('data-lab') || '';
if (student) $id('defense-student-select').value = student;
if (lab) $id('defense-lab-select').value = lab;
$id('defense-md').value = btn.getAttribute('data-md') || '';
var d = $id('modal-defense');
if (d && d.showModal) d.showModal();
}
function showDefenseText(btn) {
var el = $id('defense-text-body');
if (el) el.textContent = btn.getAttribute('data-md') || '(пусто)';
var title = $id('defense-text-title');
if (title) title.textContent = (btn.getAttribute('data-student-name') || '') + ' — ' + (btn.getAttribute('data-lab-name') || '');
var d = $id('modal-defense-text');
if (d && d.showModal) d.showModal();
}
function filterDefense() {
var s = $id('defense-lab-filter');
if (!s) return;
var labId = s.value;
document.querySelectorAll('#defense-table tr[data-lab]').forEach(function (tr) {
tr.style.display = (!labId || tr.getAttribute('data-lab') === labId) ? '' : 'none';
});
}
/* ---------- Матрица: фокус на лабораторной и закрепление варианта ---------- */
function focusLab() {
var s = $id('lab-focus');
if (!s) return;
var labId = s.value;
document.querySelectorAll('th.lab-col[data-lab], td.cell-lab[data-lab]').forEach(function (el) {
el.style.display = (!labId || el.getAttribute('data-lab') === labId) ? '' : 'none';
});
}
document.addEventListener('submit', function (ev) {
if (ev.target && ev.target.id === 'grade-form') csSerialize();
}, true);
window.addEventListener('DOMContentLoaded', function () { filterDefense(); focusLab(); });