const DIRTY_FIELDS = ['spamInterval', 'footerLeft', 'footerRight', 'aiPrompt', 'aiAutoCheck']; function fieldValue(el) { return el.type === 'checkbox' ? String(el.checked) : el.value; } function setBtnLoading(btn, on, spinnerClass) { if (on) { btn.dataset.label = btn.innerHTML; btn.disabled = true; btn.innerHTML = ` ${btn.dataset.loadingText || 'Обработка...'}`; } else { btn.disabled = false; btn.innerHTML = btn.dataset.label || btn.innerHTML; } } function updateDirty() { const dirty = DIRTY_FIELDS.some(id => { const el = document.getElementById(id); return String(el.dataset.original ?? '') !== fieldValue(el); }); document.getElementById('saveBar').classList.toggle('show', dirty); } function bindDirty() { DIRTY_FIELDS.forEach(id => { document.getElementById(id).addEventListener('input', updateDirty); }); } function snapshotDirty() { DIRTY_FIELDS.forEach(id => { document.getElementById(id).dataset.original = fieldValue(document.getElementById(id)); }); updateDirty(); } async function loadSettings() { const [sRes, stRes] = await Promise.all([ fetch(`${API}/api/settings`, { headers: hdr() }), fetch(`${API}/api/stats`, { headers: hdr() }), ]); const s = await sRes.json(); if (s.spam_interval_min !== undefined) document.getElementById('spamInterval').value = s.spam_interval_min; document.getElementById('footerLeft').value = s.footer_left || ''; document.getElementById('footerRight').value = s.footer_right || ''; document.getElementById('aiPrompt').value = s.ai_prompt || ''; document.getElementById('aiAutoCheck').checked = s.ai_autocheck_enabled !== 'false'; snapshotDirty(); loadAiQueue(); if (stRes.ok) { const st = await stRes.json(); document.getElementById('dataStats').innerHTML = ` Записей: ${st.entries} Учеников: ${st.students} Групп: ${st.groups} Сегодня: ${st.today} ${st.trash ? `В корзине: ${st.trash} →` : ''} `; } else { document.getElementById('dataStats').innerHTML = ''; } const siRes = await fetch(`${API}/api/system-info`, { headers: hdr() }); if (siRes.ok) { const si = await siRes.json(); renderSystemInfo(si); } else { document.getElementById('systemInfo').innerHTML = '
Ошибка загрузки
'; } } function renderSystemInfo(si) { const db = si.database || {}; const photos = si.photos || {}; const files = si.files || {}; const uploads = si.uploads || {}; const disk = si.disk; let tablesHtml = ''; if (db.tables && db.tables.length) { tablesHtml = '
Таблицы БД
' + db.tables.map(t => `
${t.name}${t.size}
`).join('') + '
'; } let diskHtml = ''; if (disk) { const pct = disk.used_pct; const color = pct > 90 ? '#ef4444' : pct > 70 ? '#f59e0b' : '#16a34a'; diskHtml = `
Диск ${disk.used} / ${disk.total}
Свободно: ${disk.free} (${100 - pct}%)
`; } document.getElementById('systemInfo').innerHTML = `
База данных
Размер${db.size || '—'}
${tablesHtml}
Фото
Фото групп${photos.group_photos?.count || 0} · ${photos.group_photos?.size || '0 B'}
Фото в записях${photos.entry_photos?.count || 0} · ${photos.entry_photos?.size || '0 B'}
Всего${photos.total_count || 0} · ${photos.total_size || '0 B'}
Файлы
Проектные файлы${files.project_files?.count || 0}
Загрузки
Файлов${uploads.count || 0}
Размер${uploads.size || '0 B'}
Диск
${diskHtml || '
Недоступно
'}
`; } async function loadAiQueue() { const el = document.getElementById('aiQueueInfo'); if (!el) return; try { const res = await fetch(`${API}/api/ai/queue`, { headers: hdr() }); if (!res.ok) { el.textContent = ''; return; } const q = await res.json(); const c = q.counts || {}; const state = q.enabled ? 'включена' : 'выключена'; el.innerHTML = `Автопроверка: ${state} · Модель: ${esc(q.model)} · В очереди: ${c.pending || 0} · Проверено: ${c.done || 0} · Ошибок: ${c.error || 0} · Пропущено: ${c.skipped || 0} · Монитор воркера →`; } catch { el.textContent = ''; } } async function saveSettings() { const v = document.getElementById('spamInterval').value; if (v === '' || Number(v) < 1) { alert('Введите число от 1'); return; } const btn = document.getElementById('saveBtn'); btn.dataset.loadingText = 'Сохранение...'; setBtnLoading(btn, true); try { const res = await fetch(`${API}/api/settings`, { method: 'PUT', headers: hdrJson(), body: JSON.stringify({ settings: { spam_interval_min: String(Math.round(Number(v))), footer_left: document.getElementById('footerLeft').value.trim(), footer_right: document.getElementById('footerRight').value.trim(), ai_prompt: document.getElementById('aiPrompt').value.trim(), ai_autocheck_enabled: document.getElementById('aiAutoCheck').checked ? 'true' : 'false', } }) }); if (res.ok) { showToast('Настройки сохранены'); snapshotDirty(); } else { const e = await res.json(); alert(e.error); } } finally { setBtnLoading(btn, false); } } async function downloadBackup() { const btn = document.getElementById('backupBtn'); btn.dataset.loadingText = 'Формирование...'; setBtnLoading(btn, true); try { 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('Бэкап скачан'); } finally { setBtnLoading(btn, false); } } 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'); const backupBtn = document.getElementById('backupBtn'); const restoreBtn = document.getElementById('restoreBtn'); st.classList.remove('err'); st.innerHTML = ' Восстановление...'; backupBtn.disabled = true; restoreBtn.disabled = true; 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; } finally { backupBtn.disabled = false; restoreBtn.disabled = false; } if (res.ok) { showToast('Данные восстановлены'); st.textContent = ''; loadSettings(); } else { st.classList.add('err'); st.innerHTML = 'Ошибка восстановления'; const t = await res.text(); alert('Ошибка: ' + t); } e.target.value = ''; }); document.getElementById('backupBtn').addEventListener('click', downloadBackup); document.getElementById('restoreBtn').addEventListener('click', () => document.getElementById('restoreFile').click()); document.getElementById('saveBtn').addEventListener('click', saveSettings); (async () => { if (await requireAdminPage()) { buildSidebar(document.body.dataset.page); bindDirty(); loadSettings(); } })();