feat(control-panel): сброс системы «чистый запуск» (с бэкапом и подтверждением)
Пункт [Z] в control-panel.ps1: предпросмотр → бэкап БД → подтверждение вводом «СБРОС» → очистка. Скрипт backend/scripts/reset-system.js (dry-run по умолчанию, выполнение только с --apply --confirm=RESET): • сохраняет одного админа (min id), переназначает ему авторский контент (courses/tests/flashcard_decks/custom_sims/шаблоны/библиотека/lab-ссылки/board); • стирает всех остальных пользователей + классы/задания/сессии/геймификацию/ уведомления/прогресс/историю классрума/доступы/логи; • сохраняет контент: учебники, вопросы, темы, уроки, exam-prep, симуляции, биохимия, красная книга, магазин/достижения-определения, роли/права, app_settings; • обнуляет игровые счётчики админа; классифицирует ВСЕ 119 таблиц, неизвестные не трогает; • FK off + транзакция + foreign_key_check + VACUUM. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,174 @@
|
||||
'use strict';
|
||||
/* ───────────────────────────────────────────────────────────────────────────
|
||||
reset-system.js — «ЧИСТЫЙ ЗАПУСК»: убрать всех пользователей и их активность,
|
||||
СОХРАНИВ весь контент, настройки/права и одного админа.
|
||||
|
||||
⚠️ ДЕСТРУКТИВНО. Перед запуском СДЕЛАЙТЕ БЭКАП (control-panel «Бэкап БД» делает
|
||||
это автоматически перед вызовом). По умолчанию — DRY-RUN (только показывает план).
|
||||
|
||||
Что делает (--apply --confirm=RESET):
|
||||
• Сохраняет ОДНОГО админа (min id среди role='admin') — чтобы не залочиться.
|
||||
• REASSIGN: авторский контент (courses/tests/flashcard_decks/custom_sims/шаблоны/
|
||||
библиотека/lab-ссылки/board-шаблоны) переписывается на сохранённого админа —
|
||||
иначе при удалении автора он бы каскадно удалился.
|
||||
• WIPE: все аккаунты (кроме админа), классы, задания, сессии, сдачи, геймификация,
|
||||
уведомления, прогресс, материалы, история классрума/викторин, доступы, логи.
|
||||
• KEEP: учебники, вопросы, темы, уроки, exam-prep, симуляции, биохимия, красная
|
||||
книга, магазин-товары, достижения-определения, роли/права, app_settings.
|
||||
• Сбрасывает игровые счётчики у сохранённого админа (xp/coins/стрик/…).
|
||||
• Неизвестные (не классифицированные) таблицы НЕ трогает + предупреждает.
|
||||
|
||||
Запуск:
|
||||
node backend/scripts/reset-system.js # DRY-RUN (план)
|
||||
node backend/scripts/reset-system.js --apply --confirm=RESET # выполнить
|
||||
─────────────────────────────────────────────────────────────────────────── */
|
||||
|
||||
const { DatabaseSync } = require('node:sqlite');
|
||||
const path = require('path');
|
||||
|
||||
const APPLY = process.argv.includes('--apply');
|
||||
const CONFIRM = process.argv.includes('--confirm=RESET');
|
||||
|
||||
/* Контент-таблицы: владелец переписывается на сохранённого админа (колонка у каждой своя). */
|
||||
const REASSIGN = {
|
||||
courses: 'created_by', tests: 'created_by',
|
||||
flashcard_decks: 'user_id', custom_sims: 'owner_id',
|
||||
course_templates: 'created_by', lesson_templates: 'created_by',
|
||||
assignment_templates: 'created_by', lab_sim_links: 'created_by',
|
||||
classroom_templates: 'teacher_id', folders: 'created_by', files: 'uploaded_by',
|
||||
};
|
||||
|
||||
/* Активность/организация — полностью очищается. */
|
||||
const WIPE = new Set([
|
||||
'test_sessions', 'session_questions', 'user_answers',
|
||||
'exam_attempts', 'exam_mock_sessions', 'exam_user_plan',
|
||||
'assignments', 'assignment_sessions', 'assignment_completion',
|
||||
'submissions', 'submission_log',
|
||||
'classes', 'class_members', 'class_courses',
|
||||
'classroom_sessions', 'classroom_attendance', 'classroom_chat', 'classroom_chat_reactions',
|
||||
'classroom_draw_permissions', 'classroom_hands', 'classroom_invites', 'classroom_muted',
|
||||
'classroom_notes', 'classroom_pages', 'classroom_strokes',
|
||||
'live_sessions', 'live_answers',
|
||||
'content_access',
|
||||
'xp_log', 'coin_log', 'user_achievements', 'daily_goals', 'challenges', 'user_purchases',
|
||||
'notifications', 'parent_notifications', 'parent_links',
|
||||
'student_materials', 'material_collections',
|
||||
'game_progress',
|
||||
'lesson_progress', 'lesson_comments', 'lesson_notes',
|
||||
'textbook_progress', 'textbook_bookmarks', 'bookmarks',
|
||||
'flashcard_reviews', 'flashcard_deck_access',
|
||||
'bio_user_challenges', 'bio_user_molecules', 'bio_user_pathway',
|
||||
'rb_user_collection', 'rb_user_quests', 'rb_sightings',
|
||||
'assistant_seen', 'assistant_memory', 'assistant_feedback', 'assistant_usage', 'assistant_cache',
|
||||
'imggen_usage',
|
||||
'folder_access', 'file_access',
|
||||
'avatar_requests',
|
||||
'geometry_submissions', 'geometry_tasks',
|
||||
'security_events', 'error_log', 'admin_audit_log',
|
||||
'student_prep',
|
||||
'announcements', 'teacher_students', 'user_permissions', 'user_preferences',
|
||||
]);
|
||||
|
||||
/* Контент/конфиг — НЕ трогаем (явный список для контроля «неизвестных» таблиц). */
|
||||
const KEEP = new Set([
|
||||
'subjects', 'questions', 'options', 'topics',
|
||||
'textbooks', 'textbook_chunks',
|
||||
'lessons', 'lesson_blocks', 'course_sections',
|
||||
'exam_tasks', 'exam_topics', 'exam_tracks', 'exam9_variant_tests',
|
||||
'test_questions', 'flashcard_cards', 'lab_sims',
|
||||
'bio_challenges', 'bio_elements', 'bio_molecules', 'bio_pathways', 'bio_reactions',
|
||||
'rb_food_web', 'rb_groups', 'rb_habitats', 'rb_population_data', 'rb_quests', 'rb_species', 'rb_species_regions',
|
||||
'shop_items', 'achievements',
|
||||
'roles', 'role_permissions', 'app_settings', '_migrations',
|
||||
]);
|
||||
|
||||
/* Игровые счётчики сохранённого админа — обнуляем (чистый старт). */
|
||||
const ADMIN_RESET_SQL =
|
||||
`UPDATE users SET xp = 0, level = 1, coins = 0, streak_current = 0, streak_best = 0,
|
||||
streak_date = NULL, goal_tier = 0, lab_experiments = 0, lab_reactions = 0,
|
||||
pet_petting_streak = 0 WHERE id = ?`;
|
||||
|
||||
const DB = path.join(__dirname, '..', 'data', 'learnspace.db');
|
||||
const db = new DatabaseSync(DB);
|
||||
|
||||
/* Сохраняемый админ — наименьший id среди админов. */
|
||||
const keptAdmin = db.prepare("SELECT id, email, name FROM users WHERE role = 'admin' ORDER BY id LIMIT 1").get();
|
||||
if (!keptAdmin) {
|
||||
console.error('✗ В системе нет ни одного админа — сброс отменён (иначе залочитесь). Создайте админа сначала.');
|
||||
db.close();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const allTables = db.prepare(
|
||||
"SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name"
|
||||
).all().map(r => r.name);
|
||||
|
||||
const cnt = t => { try { return db.prepare(`SELECT COUNT(*) c FROM "${t}"`).get().c; } catch { return '?'; } };
|
||||
|
||||
/* Классификация + предупреждение о неизвестных. */
|
||||
const unknown = allTables.filter(t => t !== 'users' && !REASSIGN[t] && !WIPE.has(t) && !KEEP.has(t));
|
||||
|
||||
console.log(`\n=== reset-system «ЧИСТЫЙ ЗАПУСК» (${APPLY ? (CONFIRM ? 'APPLY' : 'APPLY без --confirm=RESET → отказ') : 'DRY-RUN'}) ===`);
|
||||
console.log(`Сохраняемый админ: id=${keptAdmin.id} ${keptAdmin.email} «${keptAdmin.name}»`);
|
||||
const totalUsers = cnt('users');
|
||||
console.log(`Пользователей сейчас: ${totalUsers} → останется 1 (админ), удалится ${totalUsers - 1}\n`);
|
||||
|
||||
console.log('REASSIGN (контент → админу):');
|
||||
for (const [t, col] of Object.entries(REASSIGN)) console.log(` ${t.padEnd(22)} ${col.padEnd(12)} строк: ${cnt(t)}`);
|
||||
console.log('\nWIPE (полная очистка):');
|
||||
let wipeTotal = 0;
|
||||
for (const t of WIPE) { const c = cnt(t); if (typeof c === 'number') wipeTotal += c; console.log(` ${t.padEnd(28)} строк: ${c}`); }
|
||||
console.log(` — всего строк к удалению (без каскада users): ~${wipeTotal}`);
|
||||
console.log(`\nKEEP (контент/конфиг сохраняется): ${KEEP.size} таблиц.`);
|
||||
if (unknown.length) {
|
||||
console.log(`\n⚠️ НЕИЗВЕСТНЫЕ таблицы (НЕ трогаем — проверьте вручную): ${unknown.join(', ')}`);
|
||||
}
|
||||
|
||||
if (!APPLY) {
|
||||
console.log('\nDRY-RUN: ничего не изменено. Для выполнения: node backend/scripts/reset-system.js --apply --confirm=RESET\n');
|
||||
db.close();
|
||||
process.exit(0);
|
||||
}
|
||||
if (!CONFIRM) {
|
||||
console.error('\n✗ Нужен флаг --confirm=RESET (защита от случайного запуска). Отмена.');
|
||||
db.close();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
/* ── Выполнение ── */
|
||||
db.exec('PRAGMA foreign_keys = OFF'); // снимаем каскады — управляем удалением вручную, детерминированно
|
||||
db.exec('BEGIN');
|
||||
try {
|
||||
// 1) Переназначить контент на сохранённого админа
|
||||
for (const [t, col] of Object.entries(REASSIGN)) {
|
||||
try { db.prepare(`UPDATE "${t}" SET "${col}" = ? WHERE "${col}" IS NOT NULL AND "${col}" != ?`).run(keptAdmin.id, keptAdmin.id); }
|
||||
catch (e) { console.error(` ! reassign ${t}: ${e.message}`); }
|
||||
}
|
||||
// 2) Очистить активность/организацию
|
||||
for (const t of WIPE) {
|
||||
try { db.prepare(`DELETE FROM "${t}"`).run(); }
|
||||
catch (e) { console.error(` ! wipe ${t}: ${e.message}`); }
|
||||
}
|
||||
// 3) Удалить всех пользователей, кроме сохранённого админа
|
||||
db.prepare('DELETE FROM users WHERE id != ?').run(keptAdmin.id);
|
||||
// 4) Обнулить игровые счётчики у админа
|
||||
db.prepare(ADMIN_RESET_SQL).run(keptAdmin.id);
|
||||
db.exec('COMMIT');
|
||||
} catch (e) {
|
||||
db.exec('ROLLBACK');
|
||||
db.exec('PRAGMA foreign_keys = ON');
|
||||
console.error('\n✗ Ошибка — откат, изменений нет:', e.message);
|
||||
db.close();
|
||||
process.exit(1);
|
||||
}
|
||||
db.exec('PRAGMA foreign_keys = ON');
|
||||
|
||||
/* Проверка целостности + сжатие файла. */
|
||||
const fkBad = db.prepare('PRAGMA foreign_key_check').all();
|
||||
if (fkBad.length) console.log(`⚠️ foreign_key_check: ${fkBad.length} висячих ссылок (в неизвестных таблицах?) — проверьте.`);
|
||||
try { db.exec('VACUUM'); } catch {}
|
||||
|
||||
console.log(`\n✓ ЧИСТЫЙ ЗАПУСК выполнен. Осталось пользователей: ${cnt('users')} (только админ id=${keptAdmin.id}).`);
|
||||
console.log(`✓ Контент сохранён: учебники ${cnt('textbooks')}, вопросы ${cnt('questions')}, тесты ${cnt('tests')}, курсы ${cnt('courses')}, exam-prep ${cnt('exam_tasks')}.`);
|
||||
console.log(`\nВойдите под админом ${keptAdmin.email}. Не забудьте перезапустить сервер.\n`);
|
||||
db.close();
|
||||
@@ -183,6 +183,30 @@ function Backup-Db {
|
||||
Prune-Backups
|
||||
}
|
||||
|
||||
function Reset-System {
|
||||
Write-Host ''
|
||||
Write-Host ' ============================================================' -ForegroundColor Red
|
||||
Write-Host ' ВНИМАНИЕ: ЧИСТЫЙ ЗАПУСК - НЕОБРАТИМАЯ ОЧИСТКА' -ForegroundColor Red
|
||||
Write-Host ' ============================================================' -ForegroundColor Red
|
||||
Write-Host ' УДАЛЯТСЯ: все пользователи (кроме одного админа), классы,' -ForegroundColor Yellow
|
||||
Write-Host ' задания, сессии, геймификация, уведомления, прогресс, история.' -ForegroundColor Yellow
|
||||
Write-Host ' СОХРАНЯТСЯ: учебники, вопросы, тесты, курсы, уроки, exam-prep,' -ForegroundColor Gray
|
||||
Write-Host ' симуляции, настройки/права и один админ (контент переходит ему).' -ForegroundColor Gray
|
||||
Write-Host ''
|
||||
Write-Host ' План (предпросмотр, без изменений):' -ForegroundColor Cyan
|
||||
try { & node scripts/reset-system.js | Out-Host } catch { Write-Host (' Ошибка плана: ' + $_.Exception.Message) -ForegroundColor Red; return }
|
||||
Write-Host ''
|
||||
$ans = (Read-Host ' Для подтверждения введите СБРОС (иначе отмена)').Trim().ToUpper()
|
||||
if ($ans -ne 'СБРОС' -and $ans -ne 'RESET') { Write-Host ' Отменено.' -ForegroundColor Yellow; return }
|
||||
if (Server-Proc) { Write-Host ' Сервер работает - остановите его ([2]) перед сбросом для надёжности.' -ForegroundColor Yellow }
|
||||
Write-Host ' Шаг 1/2: бэкап БД...' -ForegroundColor Cyan
|
||||
Backup-Db
|
||||
Write-Host ' Шаг 2/2: очистка...' -ForegroundColor Cyan
|
||||
try { & node scripts/reset-system.js --apply --confirm=RESET | Out-Host }
|
||||
catch { Write-Host (' Ошибка сброса: ' + $_.Exception.Message) -ForegroundColor Red; return }
|
||||
Write-Host ' Готово. Перезапустите сервер ([3]). Бэкап сохранён в data\backups.' -ForegroundColor Green
|
||||
}
|
||||
|
||||
function Restore-Db {
|
||||
if (-not (Test-Path $script:BackupDir)) { Write-Host ' Папки бэкапов нет — сначала сделайте бэкап ([B]).' -ForegroundColor Yellow; return }
|
||||
$files = @(Get-ChildItem $script:BackupDir -Filter 'learnspace-*.db' -ErrorAction SilentlyContinue | Sort-Object LastWriteTime -Descending)
|
||||
@@ -389,6 +413,7 @@ while ($run) {
|
||||
Menu-Row '[5]' 'Применить миграции' '[A]' 'Создать админа'
|
||||
Menu-Row ' ' '' '[W]' 'Сторож (авто-рестарт)'
|
||||
Menu-Row ' ' '' '[E]' 'Ошибки в логах'
|
||||
Menu-Row ' ' '' '[Z]' 'Сброс системы (чистый запуск)'
|
||||
Write-Host ''
|
||||
Menu-Head 'ДИАГНОСТИКА И ПРОЧЕЕ' ''
|
||||
Write-Host ' ' -NoNewline
|
||||
@@ -417,6 +442,7 @@ while ($run) {
|
||||
'^(U|Г)$' { Run-Cmd 'Обновление из репозитория' { Update-FromRepo }; Refresh-Status }
|
||||
'^(W|Ц)$' { Watchdog; Refresh-Status }
|
||||
'^(E|У)$' { Run-Cmd 'Ошибки в логах' { Show-Errors } }
|
||||
'^(Z|Я)$' { Reset-System; Refresh-Status; Start-Sleep 1 }
|
||||
'^0$' { $run = $false }
|
||||
default { }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user