feat(admin): сброс системы «чистый запуск» в веб-панели
Добавлено такое же действие, как [Z] в control-panel: POST /api/admin/reset-system (+ /reset-system/plan для предпросмотра), только admin. Общая логика вынесена в src/services/systemReset.js (classify/pickKeptAdmin/runReset) — реюзится CLI и эндпоинтом. Веб-эндпоинт безопаснее CLI: сохраняет ТЕКУЩЕГО админа (оператор остаётся залогинен), делает бэкап БД ДО сброса (wal_checkpoint + копия в data/backups/), требует body.confirm='СБРОС'. UI — «Опасная зона» в overview-секции: предпросмотр плана + ввод «СБРОС» + результат с именем бэкапа. db.js: добавлен db._path (нужен бэкапу при сбросе). Логика проверена смоуком на копии живой БД (16 юзеров удалено, контент сохранён, REASSIGN на админа, гейм-счётчики обнулены, 0 висячих FK). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -452,6 +452,24 @@
|
||||
<i data-lucide="file-text"></i> Audit log
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="ov-section-title" style="margin-top:32px;color:var(--pink)">Опасная зона</div>
|
||||
<div class="ov-card danger" style="padding-bottom:16px">
|
||||
<div class="ov-card-icon"><i data-lucide="alert-octagon" style="width:18px;height:18px"></i></div>
|
||||
<div class="ov-card-label" style="margin-bottom:10px;font-weight:700;color:#0F172A">
|
||||
Сброс системы «чистый запуск»
|
||||
</div>
|
||||
<div style="font-size:.82rem;color:#56687A;line-height:1.5;margin-bottom:14px;max-width:560px">
|
||||
Удаляет всех пользователей (кроме вас), классы, сессии, задания, прогресс, уведомления и
|
||||
историю. Учебники, вопросы, тесты, курсы и настройки сохраняются — авторский контент
|
||||
переназначается на ваш аккаунт. Перед сбросом автоматически создаётся резервная копия БД.
|
||||
Действие необратимо.
|
||||
</div>
|
||||
<button class="ov-quick-btn" id="ov-reset-system-btn"
|
||||
style="border-color:rgba(241,91,181,0.5);color:var(--pink);max-width:280px">
|
||||
<i data-lucide="trash-2"></i> Сбросить систему…
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
/* ── wire quick-links via event delegation ───────────────── */
|
||||
@@ -459,9 +477,119 @@
|
||||
btn.addEventListener('click', function () { navigateTo(btn.dataset.go); });
|
||||
});
|
||||
|
||||
const resetBtn = el.querySelector('#ov-reset-system-btn');
|
||||
if (resetBtn) resetBtn.addEventListener('click', openResetModal);
|
||||
|
||||
if (window.lucide) lucide.createIcons({ nodes: [el] });
|
||||
}
|
||||
|
||||
/* ── Сброс системы «чистый запуск» — модалка с предпросмотром + вводом «СБРОС» ── */
|
||||
async function openResetModal() {
|
||||
const e = LS.esc;
|
||||
const m = LS.modal({
|
||||
title: 'Сброс системы — чистый запуск',
|
||||
size: 'md',
|
||||
content: '<div style="padding:8px 0;color:#56687A">Загрузка плана…</div>',
|
||||
actions: [{ label: 'Отмена' }],
|
||||
});
|
||||
|
||||
let plan;
|
||||
try {
|
||||
plan = await LS.api('/api/admin/reset-system/plan');
|
||||
} catch (err) {
|
||||
m.setBody('<div style="color:#F94144">Не удалось загрузить план: ' + e(err.message) + '</div>');
|
||||
return;
|
||||
}
|
||||
|
||||
const kept = plan.keptAdmin || {};
|
||||
const delUsers = Math.max(0, (plan.totalUsers || 0) - 1);
|
||||
const wipeRows = plan.wipeRows || 0;
|
||||
const reassignRows = (plan.reassign || []).reduce(function (a, r) {
|
||||
return a + (typeof r.rows === 'number' ? r.rows : 0);
|
||||
}, 0);
|
||||
const unknownNote = (plan.unknown && plan.unknown.length)
|
||||
? '<div style="margin-top:10px;padding:8px 11px;border-radius:8px;background:rgba(255,179,71,.12);' +
|
||||
'border:1px solid rgba(255,179,71,.35);font-size:.8rem;color:#9a6a10">' +
|
||||
'Неизвестные таблицы (не трогаются): ' + e(plan.unknown.join(', ')) + '</div>'
|
||||
: '';
|
||||
|
||||
m.setBody(
|
||||
'<div style="font-size:.88rem;line-height:1.6;color:#0F172A">' +
|
||||
'<div style="padding:10px 13px;border-radius:10px;background:rgba(241,91,68,.08);' +
|
||||
'border:1px solid rgba(241,91,68,.3);margin-bottom:14px">' +
|
||||
'<strong>Это действие необратимо.</strong> Перед сбросом будет создан бэкап БД.' +
|
||||
'</div>' +
|
||||
'<div style="margin-bottom:6px">Останется один администратор:</div>' +
|
||||
'<div style="padding:8px 12px;border-radius:8px;background:rgba(15,23,42,.04);margin-bottom:14px">' +
|
||||
'<strong>' + e(kept.name || '—') + '</strong> · ' + e(kept.email || '') +
|
||||
' <span style="color:#56687A">(вы)</span></div>' +
|
||||
'<ul style="margin:0 0 14px;padding-left:18px;color:#334155">' +
|
||||
'<li>Удалится пользователей: <strong>' + delUsers + '</strong></li>' +
|
||||
'<li>Очистится записей активности/организации: <strong>~' + wipeRows + '</strong></li>' +
|
||||
'<li>Контента переназначится на вас: <strong>' + reassignRows + '</strong> записей</li>' +
|
||||
'<li>Сохранится контент-таблиц: <strong>' + (plan.keepCount || 0) + '</strong></li>' +
|
||||
'</ul>' +
|
||||
unknownNote +
|
||||
'<div style="margin:16px 0 6px">Для подтверждения введите <strong>СБРОС</strong>:</div>' +
|
||||
'<input id="ov-reset-confirm-inp" type="text" autocomplete="off" ' +
|
||||
'style="width:100%;padding:9px 12px;border:1.5px solid rgba(15,23,42,.18);border-radius:10px;' +
|
||||
'font-size:.95rem;font-family:inherit" placeholder="СБРОС">' +
|
||||
'</div>'
|
||||
);
|
||||
|
||||
const inp = m.body.querySelector('#ov-reset-confirm-inp');
|
||||
function syncBtn() {
|
||||
const ok = inp && inp.value.trim() === 'СБРОС';
|
||||
const btn = document.getElementById('ov-reset-go');
|
||||
if (btn) btn.disabled = !ok;
|
||||
}
|
||||
|
||||
function setReadyActions() {
|
||||
m.setActions([
|
||||
{ label: 'Отмена' },
|
||||
{
|
||||
label: 'Сбросить систему', danger: true, id: 'ov-reset-go', close: false,
|
||||
onClick: doReset,
|
||||
},
|
||||
]);
|
||||
const btn = document.getElementById('ov-reset-go');
|
||||
if (btn) btn.disabled = true;
|
||||
}
|
||||
|
||||
async function doReset() {
|
||||
const btn = document.getElementById('ov-reset-go');
|
||||
if (!inp || inp.value.trim() !== 'СБРОС') return;
|
||||
if (btn) { btn.disabled = true; btn.textContent = 'Выполняется…'; }
|
||||
m.setError('');
|
||||
let res;
|
||||
try {
|
||||
res = await LS.api('/api/admin/reset-system', { method: 'POST', body: { confirm: 'СБРОС' } });
|
||||
} catch (err) {
|
||||
m.setError('Ошибка: ' + (err.message || 'сброс не выполнен'));
|
||||
if (btn) { btn.disabled = false; btn.textContent = 'Сбросить систему'; }
|
||||
return;
|
||||
}
|
||||
m.setBody(
|
||||
'<div style="text-align:center;padding:14px 0">' +
|
||||
'<div style="font-size:2rem;margin-bottom:6px;color:var(--green)">' +
|
||||
'<i data-lucide="check-circle-2" style="width:40px;height:40px"></i></div>' +
|
||||
'<div style="font-size:1rem;font-weight:800;color:#0F172A;margin-bottom:10px">Система сброшена</div>' +
|
||||
'<div style="font-size:.86rem;color:#56687A;line-height:1.6">' +
|
||||
'Удалено пользователей: <strong>' + (res.deletedUsers || 0) + '</strong>, осталось: <strong>' +
|
||||
(res.remainingUsers || 1) + '</strong>.<br>' +
|
||||
'Бэкап сохранён: <code style="font-size:.8rem">' + LS.esc(res.backup || '—') + '</code>' +
|
||||
(res.fkDangling ? '<br><span style="color:#F94144">Висячих ссылок: ' + res.fkDangling + '</span>' : '') +
|
||||
'</div>' +
|
||||
'</div>'
|
||||
);
|
||||
m.setActions([{ label: 'Перезагрузить', primary: true, close: false, onClick: function () { location.reload(); } }]);
|
||||
if (window.lucide) lucide.createIcons({ nodes: [m.body] });
|
||||
}
|
||||
|
||||
setReadyActions();
|
||||
if (inp) { inp.addEventListener('input', syncBtn); setTimeout(function () { inp.focus(); }, 60); }
|
||||
}
|
||||
|
||||
async function load() {
|
||||
const el = document.getElementById('overview-content');
|
||||
if (!el) return;
|
||||
|
||||
Reference in New Issue
Block a user