'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();