107 lines
4.5 KiB
HTML
107 lines
4.5 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="ru">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
<title>Настройки — Админ-панель</title>
|
|
<link rel="stylesheet" href="admin.css">
|
|
</head>
|
|
<body data-page="settings">
|
|
<div class="layout">
|
|
<div class="sidebar" id="sidebar"></div>
|
|
<div class="main">
|
|
<h2>Настройки</h2>
|
|
<div class="settings-card">
|
|
<div class="settings-row">
|
|
<div>
|
|
<label for="spamInterval">Защита от повторов (минут)</label>
|
|
<p class="hint">Если ученик уже отправил ответ, следующая отправка от него будет отклонена в течение этого времени. 0 — отключить.</p>
|
|
</div>
|
|
<input type="number" id="spamInterval" min="0" max="1440" style="width:120px;padding:10px 14px;border:1px solid var(--border);border-radius:8px;background:var(--card);color:var(--text);font-size:.9rem">
|
|
</div>
|
|
<div style="display:flex;gap:10px;margin-top:24px">
|
|
<button class="btn-link" onclick="saveSettings()">Сохранить</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="settings-card">
|
|
<h3 style="margin:0 0 8px">Резервная копия</h3>
|
|
<p class="hint">Скачайте архив со всеми данными (ученики, группы, журнал, настройки) и фотографиями. Восстановление полностью заменит текущие данные.</p>
|
|
<div style="display:flex;gap:10px;flex-wrap:wrap;margin-top:16px">
|
|
<button class="btn-link" onclick="downloadBackup()">Скачать бэкап</button>
|
|
<button class="btn-link" onclick="document.getElementById('restoreFile').click()">Восстановить из файла</button>
|
|
<input type="file" id="restoreFile" accept=".tar.gz,.gz,application/gzip" style="display:none">
|
|
</div>
|
|
<div id="restoreStatus" class="hint" style="margin-top:12px"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="toast" id="toast"></div>
|
|
<script src="admin.js"></script>
|
|
<script>
|
|
async function loadSettings() {
|
|
const res = await fetch(`${API}/api/settings`, { headers: hdr() });
|
|
const s = await res.json();
|
|
if (s.spam_interval_min !== undefined) document.getElementById('spamInterval').value = s.spam_interval_min;
|
|
}
|
|
|
|
async function saveSettings() {
|
|
const v = document.getElementById('spamInterval').value;
|
|
if (v === '' || Number(v) < 0) { alert('Введите число от 0'); return; }
|
|
const res = await fetch(`${API}/api/settings`, {
|
|
method: 'PUT',
|
|
headers: hdrJson(),
|
|
body: JSON.stringify({ settings: { spam_interval_min: String(Math.round(Number(v))) } })
|
|
});
|
|
if (res.ok) showToast('Настройки сохранены');
|
|
else { const e = await res.json(); alert(e.error); }
|
|
}
|
|
|
|
async function downloadBackup() {
|
|
const res = await fetch(`${API}/api/backup`, { headers: hdr() });
|
|
if (!res.ok) { const t = await res.text(); alert('Ошибка: ' + t); return; }
|
|
const blob = await res.blob();
|
|
const cd = res.headers.get('Content-Disposition') || '';
|
|
const m = cd.match(/filename="([^"]+)"/);
|
|
const name = m ? m[1] : `whatido-backup-${new Date().toISOString().slice(0, 10)}.json.gz`;
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = name;
|
|
a.click();
|
|
URL.revokeObjectURL(url);
|
|
showToast('Бэкап скачан');
|
|
}
|
|
|
|
document.getElementById('restoreFile').addEventListener('change', async (e) => {
|
|
const file = e.target.files[0];
|
|
if (!file) return;
|
|
if (!confirm('Восстановление заменит все текущие данные. Продолжить?')) { e.target.value = ''; return; }
|
|
const st = document.getElementById('restoreStatus');
|
|
st.textContent = 'Восстановление...';
|
|
const fd = new FormData();
|
|
fd.append('backup', file);
|
|
let res;
|
|
try {
|
|
res = await fetch(`${API}/api/restore`, { method: 'POST', headers: hdr(), body: fd });
|
|
} catch (err) {
|
|
st.textContent = '';
|
|
alert('Ошибка сети: ' + err.message);
|
|
e.target.value = '';
|
|
return;
|
|
}
|
|
st.textContent = '';
|
|
if (res.ok) showToast('Данные восстановлены');
|
|
else { const t = await res.text(); alert('Ошибка: ' + t); }
|
|
e.target.value = '';
|
|
});
|
|
|
|
(async () => {
|
|
if (await checkAuth()) {
|
|
buildSidebar(document.body.dataset.page);
|
|
loadSettings();
|
|
}
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html> |