feat: add branches feature, security audit, and multi-branch support

This commit is contained in:
dev
2026-09-09 09:41:07 +03:00
parent 7a003e5df6
commit d6e589d2f5
19 changed files with 1377 additions and 82 deletions
+79 -23
View File
@@ -36,25 +36,40 @@
</div>
<div class="modal-overlay" id="editModal" onclick="if(event.target===this)closeEdit()">
<div class="edit-modal">
<div class="edit-modal" style="max-width:480px">
<h3>Редактирование записи</h3>
<div>
<label>Фамилия и имя</label>
<input type="text" id="editName" list="studentEditList">
<datalist id="studentEditList"></datalist>
</div>
<div>
<label>Группа</label>
<select id="editGroup"></select>
</div>
<div>
<label>Что делала(а)</label>
<textarea id="editDesc"></textarea>
</div>
<div class="actions">
<button class="cancel" onclick="closeEdit()">Отмена</button>
<button class="save" onclick="saveEdit()">Сохранить</button>
</div>
<form id="editForm" class="settings-stack" style="gap:12px">
<div class="settings-row">
<div class="settings-field" style="flex:1">
<label>Фамилия и имя <span style="color:#ef4444">*</span></label>
<input type="text" id="editName" class="settings-input" list="studentEditList" required autocomplete="off">
<datalist id="studentEditList"></datalist>
</div>
</div>
<div class="settings-row">
<div class="settings-field" style="flex:1">
<label>Группа <span style="color:#ef4444">*</span></label>
<select id="editGroup" class="settings-input" required></select>
</div>
</div>
<div class="settings-row">
<div class="settings-field" style="flex:1">
<label>Что делала(а) <span style="color:#ef4444">*</span></label>
<textarea id="editDesc" class="settings-input" required></textarea>
</div>
</div>
<div class="settings-row">
<div class="settings-field" style="flex:1">
<label>Прикрепить файлы</label>
<input type="file" id="editFiles" multiple accept=".pdf,.doc,.docx,.txt,.md,.html,.htm,.zip,.rar,.7z,.jpg,.jpeg,.png,.gif,.webp">
<div class="files-preview" id="editFilesPreview"></div>
</div>
</div>
<div class="card-foot" style="justify-content:flex-end;gap:8px">
<button type="button" class="btn-primary ghost" onclick="closeEdit()">Отмена</button>
<button type="submit" class="btn-primary">Сохранить</button>
</div>
</form>
</div>
</div>
@@ -297,6 +312,8 @@ async function openEdit(id) {
function closeEdit() {
document.getElementById('editModal').classList.remove('open');
document.getElementById('editFiles').value = '';
document.getElementById('editFilesPreview').innerHTML = '';
editId = null;
}
@@ -311,11 +328,27 @@ async function saveEdit() {
headers: hdrJson(),
body: JSON.stringify({ student_name: name, group_id, description })
});
if (res.ok) {
closeEdit();
loadEntries();
showToast('Запись обновлена');
} else { const err = await res.json(); alert(err.error); }
if (!res.ok) { const err = await res.json(); alert(err.error); return; }
const files = document.getElementById('editFiles').files;
if (files.length) {
const formData = new FormData();
for (const f of files) formData.append('files', f);
const uploadRes = await fetch(`${API}/api/entries/${editId}/files`, {
method: 'POST',
headers: { 'X-Admin-Token': token },
body: formData
});
if (!uploadRes.ok) {
const err = await uploadRes.json().catch(() => ({}));
alert(err.error || 'Ошибка загрузки файлов');
return;
}
}
closeEdit();
loadEntries();
showToast('Запись обновлена');
}
async function delEntry(id) {
@@ -333,6 +366,29 @@ document.getElementById('searchInput').addEventListener('input', () => {
clearTimeout(searchTimer);
searchTimer = setTimeout(() => { page = 1; loadEntries(); }, 300);
});
document.getElementById('editForm').addEventListener('submit', (e) => {
e.preventDefault();
saveEdit();
});
document.getElementById('editFiles').addEventListener('change', (e) => {
const preview = document.getElementById('editFilesPreview');
preview.innerHTML = '';
Array.from(e.target.files).forEach((f, i) => {
const div = document.createElement('div');
div.className = 'file-preview-item';
div.style.cssText = 'display:flex;align-items:center;gap:8px;padding:6px 8px;background:var(--bg);border:1px solid var(--border);border-radius:6px;font-size:.8rem';
div.innerHTML = `<span>${esc(f.name)}</span> <span style="color:var(--muted)">${(f.size/1024).toFixed(1)} KB</span> <button type="button" onclick="removeEditFile(${i})" style="background:none;border:none;color:var(--muted);cursor:pointer;font-size:1.1rem;line-height:1">✕</button>`;
preview.appendChild(div);
});
});
function removeEditFile(index) {
const input = document.getElementById('editFiles');
const dt = new DataTransfer();
Array.from(input.files).forEach((f, i) => { if (i !== index) dt.items.add(f); });
input.files = dt.files;
input.dispatchEvent(new Event('change'));
}
async function exportCSV() {
const p = new URLSearchParams();