2fd7f6a463
- migrate.js → legacy-migrate.js (kept for rollback, delete 2026-07-01) - tests/setup.js now uses migrations-runner.run() on fresh temp DB - npm run migrate → versioned runner (was legacy init-every-start) - npm run migrate:legacy → legacy-migrate.js (emergency rollback only) After `npm run migrate:bootstrap` on prod: npm run migrate → "Nothing to apply — schema is up to date" All 32 previously-passing tests continue to pass. Pre-existing 3 auth.test.js failures (rate-limiter shared state) unchanged. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2989 lines
176 KiB
JavaScript
2989 lines
176 KiB
JavaScript
// LEGACY: monolithic init-on-every-start migration. Do not use directly.
|
||
// Schema is now managed via migrations-runner.js + migrations/NNN_*.sql
|
||
// New schema changes → add migrations/NNN_description.sql, run `npm run migrate`
|
||
// This file kept for rollback reference. Delete after 2026-07-01 if stable.
|
||
const db = require('./db');
|
||
|
||
db.exec(`
|
||
CREATE TABLE IF NOT EXISTS users (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
email TEXT UNIQUE NOT NULL,
|
||
password_hash TEXT NOT NULL,
|
||
name TEXT NOT NULL,
|
||
role TEXT NOT NULL DEFAULT 'student'
|
||
CHECK (role IN ('student', 'teacher', 'admin', 'free_student')),
|
||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||
last_login TEXT
|
||
);
|
||
|
||
CREATE TABLE IF NOT EXISTS subjects (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
slug TEXT UNIQUE NOT NULL,
|
||
name TEXT NOT NULL,
|
||
icon TEXT
|
||
);
|
||
|
||
INSERT OR IGNORE INTO subjects (slug, name, icon) VALUES
|
||
('bio', 'Биология', 'dna'),
|
||
('chem', 'Химия', 'atom'),
|
||
('math', 'Математика', 'compass'),
|
||
('phys', 'Физика', 'zap');
|
||
|
||
CREATE TABLE IF NOT EXISTS topics (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
subject_id INTEGER NOT NULL REFERENCES subjects(id) ON DELETE CASCADE,
|
||
name TEXT NOT NULL,
|
||
order_index INTEGER NOT NULL DEFAULT 0
|
||
);
|
||
|
||
CREATE TABLE IF NOT EXISTS questions (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
subject_id INTEGER NOT NULL REFERENCES subjects(id) ON DELETE CASCADE,
|
||
topic_id INTEGER REFERENCES topics(id) ON DELETE SET NULL,
|
||
text TEXT NOT NULL,
|
||
difficulty INTEGER NOT NULL DEFAULT 1 CHECK (difficulty BETWEEN 1 AND 3),
|
||
year INTEGER,
|
||
explanation TEXT,
|
||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||
);
|
||
|
||
CREATE TABLE IF NOT EXISTS options (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
question_id INTEGER NOT NULL REFERENCES questions(id) ON DELETE CASCADE,
|
||
text TEXT NOT NULL,
|
||
is_correct INTEGER NOT NULL DEFAULT 0,
|
||
order_index INTEGER NOT NULL DEFAULT 0
|
||
);
|
||
|
||
CREATE TABLE IF NOT EXISTS test_sessions (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||
subject_id INTEGER REFERENCES subjects(id) ON DELETE SET NULL,
|
||
mode TEXT NOT NULL DEFAULT 'exam'
|
||
CHECK (mode IN ('exam', 'practice', 'topic', 'random')),
|
||
total INTEGER NOT NULL,
|
||
score INTEGER,
|
||
status TEXT NOT NULL DEFAULT 'in_progress'
|
||
CHECK (status IN ('in_progress', 'completed', 'abandoned')),
|
||
started_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||
finished_at TEXT
|
||
);
|
||
|
||
CREATE TABLE IF NOT EXISTS session_questions (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
session_id INTEGER NOT NULL REFERENCES test_sessions(id) ON DELETE CASCADE,
|
||
question_id INTEGER NOT NULL REFERENCES questions(id),
|
||
order_index INTEGER NOT NULL
|
||
);
|
||
|
||
CREATE TABLE IF NOT EXISTS user_answers (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
session_id INTEGER NOT NULL REFERENCES test_sessions(id) ON DELETE CASCADE,
|
||
question_id INTEGER NOT NULL REFERENCES questions(id),
|
||
chosen_option_id INTEGER REFERENCES options(id),
|
||
is_correct INTEGER,
|
||
time_spent_sec INTEGER,
|
||
answered_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||
UNIQUE (session_id, question_id)
|
||
);
|
||
|
||
CREATE INDEX IF NOT EXISTS idx_questions_subject ON questions(subject_id);
|
||
CREATE INDEX IF NOT EXISTS idx_questions_topic ON questions(topic_id);
|
||
CREATE INDEX IF NOT EXISTS idx_sessions_user ON test_sessions(user_id);
|
||
CREATE INDEX IF NOT EXISTS idx_answers_session ON user_answers(session_id);
|
||
|
||
-- ── Классы ────────────────────────────────────────────────────────────
|
||
CREATE TABLE IF NOT EXISTS classes (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
name TEXT NOT NULL,
|
||
description TEXT,
|
||
teacher_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||
invite_code TEXT UNIQUE NOT NULL,
|
||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||
);
|
||
|
||
CREATE TABLE IF NOT EXISTS class_members (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
class_id INTEGER NOT NULL REFERENCES classes(id) ON DELETE CASCADE,
|
||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||
joined_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||
UNIQUE (class_id, user_id)
|
||
);
|
||
|
||
-- ── Задания ───────────────────────────────────────────────────────────
|
||
CREATE TABLE IF NOT EXISTS assignments (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
class_id INTEGER NOT NULL REFERENCES classes(id) ON DELETE CASCADE,
|
||
title TEXT NOT NULL,
|
||
subject_slug TEXT NOT NULL,
|
||
mode TEXT NOT NULL DEFAULT 'exam',
|
||
count INTEGER NOT NULL DEFAULT 25,
|
||
topic_id INTEGER REFERENCES topics(id) ON DELETE SET NULL,
|
||
deadline TEXT,
|
||
created_by INTEGER NOT NULL REFERENCES users(id),
|
||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||
);
|
||
|
||
CREATE TABLE IF NOT EXISTS assignment_sessions (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
assignment_id INTEGER NOT NULL REFERENCES assignments(id) ON DELETE CASCADE,
|
||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||
session_id INTEGER REFERENCES test_sessions(id) ON DELETE SET NULL,
|
||
UNIQUE (assignment_id, user_id)
|
||
);
|
||
|
||
CREATE INDEX IF NOT EXISTS idx_class_members_class ON class_members(class_id);
|
||
CREATE INDEX IF NOT EXISTS idx_class_members_user ON class_members(user_id);
|
||
CREATE INDEX IF NOT EXISTS idx_assignments_class ON assignments(class_id);
|
||
CREATE INDEX IF NOT EXISTS idx_assign_sess_assign ON assignment_sessions(assignment_id);
|
||
`);
|
||
|
||
console.log('✓ Migration complete. Database: learnspace.db');
|
||
|
||
/* ── Incremental columns (safe to run multiple times) ─────────────────── */
|
||
const incremental = [
|
||
"ALTER TABLE questions ADD COLUMN type TEXT NOT NULL DEFAULT 'single'",
|
||
"ALTER TABLE questions ADD COLUMN correct_text TEXT",
|
||
"ALTER TABLE user_answers ADD COLUMN answer_text TEXT",
|
||
`CREATE TABLE IF NOT EXISTS tests (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
title TEXT NOT NULL,
|
||
subject_slug TEXT NOT NULL,
|
||
description TEXT,
|
||
created_by INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||
)`,
|
||
`CREATE TABLE IF NOT EXISTS test_questions (
|
||
test_id INTEGER NOT NULL REFERENCES tests(id) ON DELETE CASCADE,
|
||
question_id INTEGER NOT NULL REFERENCES questions(id) ON DELETE CASCADE,
|
||
order_index INTEGER NOT NULL DEFAULT 0,
|
||
PRIMARY KEY (test_id, question_id)
|
||
)`,
|
||
'ALTER TABLE assignments ADD COLUMN test_id INTEGER REFERENCES tests(id) ON DELETE SET NULL',
|
||
"ALTER TABLE tests ADD COLUMN show_answers INTEGER NOT NULL DEFAULT 1",
|
||
`CREATE TABLE IF NOT EXISTS files (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
title TEXT NOT NULL,
|
||
description TEXT,
|
||
original_name TEXT NOT NULL,
|
||
stored_name TEXT NOT NULL UNIQUE,
|
||
mimetype TEXT,
|
||
size INTEGER,
|
||
subject_slug TEXT,
|
||
is_public INTEGER NOT NULL DEFAULT 1,
|
||
uploaded_by INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||
)`,
|
||
'CREATE INDEX IF NOT EXISTS idx_files_subject ON files(subject_slug)',
|
||
'CREATE INDEX IF NOT EXISTS idx_files_uploader ON files(uploaded_by)',
|
||
`CREATE TABLE IF NOT EXISTS file_access (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE,
|
||
type TEXT NOT NULL CHECK (type IN ('class', 'user')),
|
||
target_id INTEGER NOT NULL,
|
||
UNIQUE (file_id, type, target_id)
|
||
)`,
|
||
'CREATE INDEX IF NOT EXISTS idx_file_access_file ON file_access(file_id)',
|
||
'ALTER TABLE assignments ADD COLUMN file_id INTEGER REFERENCES files(id) ON DELETE SET NULL',
|
||
'ALTER TABLE questions ADD COLUMN image TEXT',
|
||
'ALTER TABLE assignment_sessions ADD COLUMN first_seen_at TEXT',
|
||
"ALTER TABLE subjects ADD COLUMN default_mode TEXT NOT NULL DEFAULT 'exam'",
|
||
"ALTER TABLE subjects ADD COLUMN default_count INTEGER NOT NULL DEFAULT 25",
|
||
"ALTER TABLE subjects ADD COLUMN default_test_id INTEGER REFERENCES tests(id) ON DELETE SET NULL",
|
||
"ALTER TABLE tests ADD COLUMN time_limit INTEGER",
|
||
`CREATE TABLE IF NOT EXISTS announcements (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
class_id INTEGER NOT NULL REFERENCES classes(id) ON DELETE CASCADE,
|
||
author_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||
text TEXT NOT NULL,
|
||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||
)`,
|
||
"CREATE INDEX IF NOT EXISTS idx_announcements_class ON announcements(class_id)",
|
||
`CREATE TABLE IF NOT EXISTS notifications (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||
type TEXT NOT NULL DEFAULT 'info',
|
||
message TEXT NOT NULL,
|
||
link TEXT,
|
||
is_read INTEGER NOT NULL DEFAULT 0,
|
||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||
)`,
|
||
"CREATE INDEX IF NOT EXISTS idx_notifications_user ON notifications(user_id, is_read)",
|
||
"ALTER TABLE options ADD COLUMN match_pair TEXT",
|
||
"ALTER TABLE assignments ADD COLUMN is_homework INTEGER NOT NULL DEFAULT 0",
|
||
`CREATE TABLE IF NOT EXISTS assignment_templates (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
created_by INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||
label TEXT NOT NULL,
|
||
subject_slug TEXT NOT NULL,
|
||
mode TEXT NOT NULL DEFAULT 'exam',
|
||
count INTEGER NOT NULL DEFAULT 25,
|
||
topic_id INTEGER REFERENCES topics(id) ON DELETE SET NULL,
|
||
test_id INTEGER REFERENCES tests(id) ON DELETE SET NULL,
|
||
file_id INTEGER REFERENCES files(id) ON DELETE SET NULL,
|
||
is_homework INTEGER NOT NULL DEFAULT 0,
|
||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||
)`,
|
||
"CREATE INDEX IF NOT EXISTS idx_tpl_creator ON assignment_templates(created_by)",
|
||
`CREATE TABLE IF NOT EXISTS folders (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
name TEXT NOT NULL,
|
||
created_by INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||
)`,
|
||
"ALTER TABLE files ADD COLUMN folder_id INTEGER",
|
||
`CREATE TABLE IF NOT EXISTS folder_access (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
folder_id INTEGER NOT NULL REFERENCES folders(id) ON DELETE CASCADE,
|
||
type TEXT NOT NULL CHECK (type IN ('class', 'user')),
|
||
target_id INTEGER NOT NULL,
|
||
UNIQUE (folder_id, type, target_id)
|
||
)`,
|
||
'CREATE INDEX IF NOT EXISTS idx_folder_access_folder ON folder_access(folder_id)',
|
||
`CREATE TABLE IF NOT EXISTS role_permissions (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
role TEXT NOT NULL CHECK (role IN ('teacher', 'student', 'free_student')),
|
||
permission TEXT NOT NULL,
|
||
enabled INTEGER NOT NULL DEFAULT 0,
|
||
UNIQUE (role, permission)
|
||
)`,
|
||
`CREATE TABLE IF NOT EXISTS user_permissions (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||
permission TEXT NOT NULL,
|
||
enabled INTEGER NOT NULL DEFAULT 0,
|
||
UNIQUE (user_id, permission)
|
||
)`,
|
||
'CREATE INDEX IF NOT EXISTS idx_user_permissions_user ON user_permissions(user_id)',
|
||
`CREATE TABLE IF NOT EXISTS submissions (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
class_id INTEGER NOT NULL REFERENCES classes(id) ON DELETE CASCADE,
|
||
assignment_id INTEGER REFERENCES assignments(id) ON DELETE SET NULL,
|
||
student_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||
original_name TEXT NOT NULL,
|
||
stored_name TEXT NOT NULL UNIQUE,
|
||
mimetype TEXT,
|
||
size INTEGER,
|
||
message TEXT,
|
||
status TEXT NOT NULL DEFAULT 'new' CHECK (status IN ('new','reviewed')),
|
||
teacher_note TEXT,
|
||
submitted_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||
)`,
|
||
'CREATE INDEX IF NOT EXISTS idx_submissions_class ON submissions(class_id)',
|
||
'CREATE INDEX IF NOT EXISTS idx_submissions_student ON submissions(student_id)',
|
||
'CREATE INDEX IF NOT EXISTS idx_submissions_assignment ON submissions(assignment_id)',
|
||
"ALTER TABLE questions ADD COLUMN source_type TEXT NOT NULL DEFAULT 'базовый'",
|
||
'CREATE INDEX IF NOT EXISTS idx_questions_source ON questions(source_type)',
|
||
];
|
||
for (const sql of incremental) {
|
||
try { db.exec(sql); }
|
||
catch (e) { if (!e.message.includes('duplicate column') && !e.message.includes('already exists')) console.warn(' incremental migration note:', e.message); }
|
||
}
|
||
|
||
/* ── Deduplicate topics (safe to run multiple times) ─────────────────── */
|
||
try {
|
||
const dupes = db.prepare(
|
||
'SELECT id FROM topics WHERE id NOT IN (SELECT MIN(id) FROM topics GROUP BY subject_id, name)'
|
||
).all();
|
||
if (dupes.length) {
|
||
const getDupesWithInfo = db.prepare(`
|
||
SELECT t.id, (SELECT MIN(id) FROM topics t2 WHERE t2.subject_id = t.subject_id AND t2.name = t.name) AS canonical_id
|
||
FROM topics t
|
||
WHERE t.id NOT IN (SELECT MIN(id) FROM topics GROUP BY subject_id, name)
|
||
`).all();
|
||
const updateQ = db.prepare('UPDATE questions SET topic_id = ? WHERE topic_id = ?');
|
||
db.exec('BEGIN');
|
||
for (const d of getDupesWithInfo) updateQ.run(d.canonical_id, d.id);
|
||
db.exec('COMMIT');
|
||
db.prepare('DELETE FROM topics WHERE id NOT IN (SELECT MIN(id) FROM topics GROUP BY subject_id, name)').run();
|
||
console.log(`✓ Deduplicated ${dupes.length} duplicate topics`);
|
||
}
|
||
db.exec('CREATE UNIQUE INDEX IF NOT EXISTS idx_topics_uniq ON topics(subject_id, name)');
|
||
} catch (e) {
|
||
try { db.exec('ROLLBACK'); } catch {}
|
||
console.warn('Topics dedup skipped:', e.message);
|
||
}
|
||
|
||
/* ── Recreate assignments table: make class_id nullable, add user_id ────── */
|
||
try {
|
||
const cols = db.prepare('PRAGMA table_info(assignments)').all();
|
||
const classIdCol = cols.find(c => c.name === 'class_id');
|
||
const hasUserId = cols.some(c => c.name === 'user_id');
|
||
if (classIdCol && classIdCol.notnull === 1 && !hasUserId) {
|
||
const hasTid = cols.some(c => c.name === 'test_id');
|
||
db.exec('PRAGMA foreign_keys = OFF');
|
||
const hasFileId = cols.some(c => c.name === 'file_id');
|
||
const hasHomework = cols.some(c => c.name === 'is_homework');
|
||
const hasMaxAtt = cols.some(c => c.name === 'max_attempts');
|
||
db.exec(`
|
||
CREATE TABLE assignments_v2 (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
class_id INTEGER REFERENCES classes(id) ON DELETE CASCADE,
|
||
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
||
title TEXT NOT NULL,
|
||
subject_slug TEXT NOT NULL,
|
||
mode TEXT NOT NULL DEFAULT 'exam',
|
||
count INTEGER NOT NULL DEFAULT 25,
|
||
topic_id INTEGER REFERENCES topics(id) ON DELETE SET NULL,
|
||
deadline TEXT,
|
||
created_by INTEGER NOT NULL REFERENCES users(id),
|
||
test_id INTEGER REFERENCES tests(id) ON DELETE SET NULL,
|
||
file_id INTEGER REFERENCES files(id) ON DELETE SET NULL,
|
||
is_homework INTEGER NOT NULL DEFAULT 0,
|
||
max_attempts INTEGER NOT NULL DEFAULT 0,
|
||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||
)
|
||
`);
|
||
// Copy only columns that exist in the old table
|
||
const copyCols = ['id','class_id','title','subject_slug','mode','count','topic_id','deadline','created_by','created_at'];
|
||
if (hasTid) copyCols.push('test_id');
|
||
if (hasFileId) copyCols.push('file_id');
|
||
if (hasHomework) copyCols.push('is_homework');
|
||
if (hasMaxAtt) copyCols.push('max_attempts');
|
||
const colList = copyCols.join(',');
|
||
db.exec(`INSERT INTO assignments_v2 (${colList}) SELECT ${colList} FROM assignments`);
|
||
db.exec('DROP TABLE assignments');
|
||
db.exec('ALTER TABLE assignments_v2 RENAME TO assignments');
|
||
db.exec('CREATE INDEX IF NOT EXISTS idx_assignments_class ON assignments(class_id)');
|
||
db.exec('CREATE INDEX IF NOT EXISTS idx_assignments_user ON assignments(user_id)');
|
||
db.exec('PRAGMA foreign_keys = ON');
|
||
console.log('✓ assignments migrated: class_id now nullable, user_id added');
|
||
}
|
||
} catch (e) {
|
||
try { db.exec('PRAGMA foreign_keys = ON'); } catch {}
|
||
console.warn('Assignment migration skipped:', e.message);
|
||
}
|
||
|
||
/* ── Theory / Lessons module ─────────────────────────────────────────── */
|
||
const theoryMigrations = [
|
||
`CREATE TABLE IF NOT EXISTS courses (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
subject_slug TEXT NOT NULL,
|
||
title TEXT NOT NULL,
|
||
description TEXT,
|
||
cover_emoji TEXT NOT NULL DEFAULT '',
|
||
order_index INTEGER NOT NULL DEFAULT 0,
|
||
is_published INTEGER NOT NULL DEFAULT 0,
|
||
created_by INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||
)`,
|
||
'CREATE INDEX IF NOT EXISTS idx_courses_subject ON courses(subject_slug)',
|
||
'CREATE INDEX IF NOT EXISTS idx_courses_creator ON courses(created_by)',
|
||
`CREATE TABLE IF NOT EXISTS lessons (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
course_id INTEGER NOT NULL REFERENCES courses(id) ON DELETE CASCADE,
|
||
title TEXT NOT NULL,
|
||
order_index INTEGER NOT NULL DEFAULT 0,
|
||
is_published INTEGER NOT NULL DEFAULT 0,
|
||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||
)`,
|
||
'CREATE INDEX IF NOT EXISTS idx_lessons_course ON lessons(course_id)',
|
||
`CREATE TABLE IF NOT EXISTS lesson_blocks (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
lesson_id INTEGER NOT NULL REFERENCES lessons(id) ON DELETE CASCADE,
|
||
type TEXT NOT NULL DEFAULT 'text'
|
||
CHECK (type IN ('heading','text','formula','image','quiz','sim','table','code','divider')),
|
||
order_index INTEGER NOT NULL DEFAULT 0,
|
||
data TEXT NOT NULL DEFAULT '{}'
|
||
)`,
|
||
'CREATE INDEX IF NOT EXISTS idx_blocks_lesson ON lesson_blocks(lesson_id)',
|
||
`CREATE TABLE IF NOT EXISTS lesson_progress (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||
lesson_id INTEGER NOT NULL REFERENCES lessons(id) ON DELETE CASCADE,
|
||
completed INTEGER NOT NULL DEFAULT 0,
|
||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||
UNIQUE (user_id, lesson_id)
|
||
)`,
|
||
'CREATE INDEX IF NOT EXISTS idx_lprogress_user ON lesson_progress(user_id)',
|
||
'CREATE INDEX IF NOT EXISTS idx_lprogress_lesson ON lesson_progress(lesson_id)',
|
||
];
|
||
for (const sql of theoryMigrations) {
|
||
try { db.exec(sql); }
|
||
catch (e) { if (!e.message.includes('duplicate column') && !e.message.includes('already exists')) console.warn(' theory migration note:', e.message); }
|
||
}
|
||
console.log('✓ Theory/Lessons tables ready');
|
||
|
||
/* ── Theory v2: sections, notes, class_courses ───────────────────────── */
|
||
const theoryV2 = [
|
||
`CREATE TABLE IF NOT EXISTS course_sections (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
course_id INTEGER NOT NULL REFERENCES courses(id) ON DELETE CASCADE,
|
||
title TEXT NOT NULL,
|
||
order_index INTEGER NOT NULL DEFAULT 0
|
||
)`,
|
||
'CREATE INDEX IF NOT EXISTS idx_sections_course ON course_sections(course_id)',
|
||
"ALTER TABLE lessons ADD COLUMN section_id INTEGER REFERENCES course_sections(id) ON DELETE SET NULL",
|
||
"ALTER TABLE lessons ADD COLUMN read_time INTEGER NOT NULL DEFAULT 0",
|
||
`CREATE TABLE IF NOT EXISTS lesson_notes (
|
||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||
lesson_id INTEGER NOT NULL REFERENCES lessons(id) ON DELETE CASCADE,
|
||
text TEXT NOT NULL DEFAULT '',
|
||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||
PRIMARY KEY (user_id, lesson_id)
|
||
)`,
|
||
'CREATE INDEX IF NOT EXISTS idx_notes_user ON lesson_notes(user_id)',
|
||
'CREATE INDEX IF NOT EXISTS idx_notes_lesson ON lesson_notes(lesson_id)',
|
||
`CREATE TABLE IF NOT EXISTS class_courses (
|
||
class_id INTEGER NOT NULL REFERENCES classes(id) ON DELETE CASCADE,
|
||
course_id INTEGER NOT NULL REFERENCES courses(id) ON DELETE CASCADE,
|
||
deadline TEXT,
|
||
assigned_by INTEGER REFERENCES users(id) ON DELETE SET NULL,
|
||
assigned_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||
PRIMARY KEY (class_id, course_id)
|
||
)`,
|
||
'CREATE INDEX IF NOT EXISTS idx_cc_class ON class_courses(class_id)',
|
||
'CREATE INDEX IF NOT EXISTS idx_cc_course ON class_courses(course_id)',
|
||
];
|
||
for (const sql of theoryV2) {
|
||
try { db.exec(sql); }
|
||
catch (e) { if (!e.message.includes('duplicate column') && !e.message.includes('already exists')) console.warn(' theoryV2 migration note:', e.message); }
|
||
}
|
||
console.log('✓ Theory v2 tables ready');
|
||
|
||
/* ── Grading: add grade column to submissions ────────────────────────── */
|
||
try { db.exec('ALTER TABLE submissions ADD COLUMN grade INTEGER'); } catch {}
|
||
console.log('✓ Grading column ready');
|
||
|
||
/* ── lesson_blocks v2: extend type constraint (callout, video, flashcard) */
|
||
try {
|
||
const tblDef = db.prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name='lesson_blocks'").get();
|
||
if (tblDef && !tblDef.sql.includes('callout')) {
|
||
db.exec('PRAGMA foreign_keys = OFF');
|
||
db.exec(`CREATE TABLE lesson_blocks_v2 (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
lesson_id INTEGER NOT NULL REFERENCES lessons(id) ON DELETE CASCADE,
|
||
type TEXT NOT NULL DEFAULT 'text'
|
||
CHECK (type IN ('heading','text','formula','image','quiz','sim',
|
||
'table','code','divider','callout','video','flashcard')),
|
||
order_index INTEGER NOT NULL DEFAULT 0,
|
||
data TEXT NOT NULL DEFAULT '{}'
|
||
)`);
|
||
db.exec('INSERT INTO lesson_blocks_v2 SELECT * FROM lesson_blocks');
|
||
db.exec('DROP TABLE lesson_blocks');
|
||
db.exec('ALTER TABLE lesson_blocks_v2 RENAME TO lesson_blocks');
|
||
db.exec('CREATE INDEX IF NOT EXISTS idx_blocks_lesson ON lesson_blocks(lesson_id)');
|
||
db.exec('PRAGMA foreign_keys = ON');
|
||
console.log('✓ lesson_blocks: extended type constraint (callout/video/flashcard)');
|
||
}
|
||
} catch (e) {
|
||
try { db.exec('PRAGMA foreign_keys = ON'); } catch {}
|
||
console.warn('lesson_blocks migration skipped:', e.message);
|
||
}
|
||
|
||
/* ── Gamification: XP, Levels, Streaks, Achievements, Daily Goals ────── */
|
||
const gamificationMigrations = [
|
||
"ALTER TABLE users ADD COLUMN xp INTEGER NOT NULL DEFAULT 0",
|
||
"ALTER TABLE users ADD COLUMN level INTEGER NOT NULL DEFAULT 1",
|
||
"ALTER TABLE users ADD COLUMN streak_current INTEGER NOT NULL DEFAULT 0",
|
||
"ALTER TABLE users ADD COLUMN streak_best INTEGER NOT NULL DEFAULT 0",
|
||
"ALTER TABLE users ADD COLUMN streak_date TEXT",
|
||
`CREATE TABLE IF NOT EXISTS xp_log (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||
amount INTEGER NOT NULL,
|
||
reason TEXT NOT NULL,
|
||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||
)`,
|
||
'CREATE INDEX IF NOT EXISTS idx_xp_log_user ON xp_log(user_id)',
|
||
'CREATE INDEX IF NOT EXISTS idx_xp_log_date ON xp_log(created_at)',
|
||
`CREATE TABLE IF NOT EXISTS achievements (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
slug TEXT UNIQUE NOT NULL,
|
||
title TEXT NOT NULL,
|
||
icon TEXT NOT NULL DEFAULT 'trophy',
|
||
category TEXT NOT NULL DEFAULT 'general',
|
||
description TEXT
|
||
)`,
|
||
`CREATE TABLE IF NOT EXISTS user_achievements (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||
achievement_id INTEGER NOT NULL REFERENCES achievements(id) ON DELETE CASCADE,
|
||
unlocked_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||
UNIQUE (user_id, achievement_id)
|
||
)`,
|
||
'CREATE INDEX IF NOT EXISTS idx_user_achievements_user ON user_achievements(user_id)',
|
||
`CREATE TABLE IF NOT EXISTS daily_goals (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||
date TEXT NOT NULL,
|
||
tests_target INTEGER NOT NULL DEFAULT 3,
|
||
tests_done INTEGER NOT NULL DEFAULT 0,
|
||
xp_target INTEGER NOT NULL DEFAULT 200,
|
||
xp_earned INTEGER NOT NULL DEFAULT 0,
|
||
UNIQUE (user_id, date)
|
||
)`,
|
||
'CREATE INDEX IF NOT EXISTS idx_daily_goals_user ON daily_goals(user_id, date)',
|
||
`CREATE TABLE IF NOT EXISTS challenges (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||
week TEXT NOT NULL,
|
||
title TEXT NOT NULL,
|
||
description TEXT,
|
||
type TEXT NOT NULL DEFAULT 'tests',
|
||
target INTEGER NOT NULL DEFAULT 3,
|
||
progress INTEGER NOT NULL DEFAULT 0,
|
||
xp_reward INTEGER NOT NULL DEFAULT 100,
|
||
subject_slug TEXT,
|
||
topic_id INTEGER,
|
||
completed INTEGER NOT NULL DEFAULT 0,
|
||
claimed INTEGER NOT NULL DEFAULT 0,
|
||
UNIQUE(user_id, week, title)
|
||
)`,
|
||
'CREATE INDEX IF NOT EXISTS idx_challenges_user_week ON challenges(user_id, week)',
|
||
];
|
||
for (const sql of gamificationMigrations) {
|
||
try { db.exec(sql); }
|
||
catch (e) { if (!e.message.includes('duplicate column') && !e.message.includes('already exists')) console.warn(' gamification migration note:', e.message); }
|
||
}
|
||
|
||
// Add goal_tier column to users if missing
|
||
try { db.exec("ALTER TABLE users ADD COLUMN goal_tier TEXT DEFAULT 'medium'"); } catch {}
|
||
// Add avatar_frame column to users if missing
|
||
try { db.exec("ALTER TABLE users ADD COLUMN avatar_frame TEXT DEFAULT 'default'"); } catch {}
|
||
|
||
// Seed achievements
|
||
try {
|
||
const { seedAchievements } = require('../controllers/gamificationController');
|
||
seedAchievements();
|
||
} catch (e) { console.warn(' seedAchievements note:', e.message); }
|
||
console.log('✓ Gamification tables ready');
|
||
|
||
/* ── lesson_blocks v3: add interactive types (matching, fill-blank, ordering) */
|
||
try {
|
||
const tblDef = db.prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name='lesson_blocks'").get();
|
||
if (tblDef && !tblDef.sql.includes('matching')) {
|
||
db.exec('PRAGMA foreign_keys = OFF');
|
||
db.exec(`CREATE TABLE lesson_blocks_v3 (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
lesson_id INTEGER NOT NULL REFERENCES lessons(id) ON DELETE CASCADE,
|
||
type TEXT NOT NULL DEFAULT 'text'
|
||
CHECK (type IN ('heading','text','formula','image','quiz','sim',
|
||
'table','code','divider','callout','video','flashcard',
|
||
'matching','fill-blank','ordering')),
|
||
order_index INTEGER NOT NULL DEFAULT 0,
|
||
data TEXT NOT NULL DEFAULT '{}'
|
||
)`);
|
||
db.exec('INSERT INTO lesson_blocks_v3 SELECT * FROM lesson_blocks');
|
||
db.exec('DROP TABLE lesson_blocks');
|
||
db.exec('ALTER TABLE lesson_blocks_v3 RENAME TO lesson_blocks');
|
||
db.exec('CREATE INDEX IF NOT EXISTS idx_blocks_lesson ON lesson_blocks(lesson_id)');
|
||
db.exec('PRAGMA foreign_keys = ON');
|
||
console.log('✓ lesson_blocks v3: interactive types added');
|
||
}
|
||
} catch (e) {
|
||
try { db.exec('PRAGMA foreign_keys = ON'); } catch {}
|
||
console.warn('lesson_blocks v3 migration skipped:', e.message);
|
||
}
|
||
|
||
/* ── lesson_blocks v4: full type set (accordion, timeline, diagram, alert, columns, geogebra, audio) */
|
||
try {
|
||
const tblDef4 = db.prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name='lesson_blocks'").get();
|
||
if (tblDef4 && !tblDef4.sql.includes('accordion')) {
|
||
db.exec('PRAGMA foreign_keys = OFF');
|
||
db.exec(`CREATE TABLE lesson_blocks_v4 (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
lesson_id INTEGER NOT NULL REFERENCES lessons(id) ON DELETE CASCADE,
|
||
type TEXT NOT NULL DEFAULT 'text'
|
||
CHECK (type IN ('heading','text','formula','image','quiz','sim',
|
||
'table','code','divider','callout','video','flashcard',
|
||
'matching','fill-blank','ordering',
|
||
'accordion','timeline','diagram','geogebra','audio','columns','alert')),
|
||
order_index INTEGER NOT NULL DEFAULT 0,
|
||
data TEXT NOT NULL DEFAULT '{}'
|
||
)`);
|
||
db.exec('INSERT INTO lesson_blocks_v4 SELECT * FROM lesson_blocks');
|
||
db.exec('DROP TABLE lesson_blocks');
|
||
db.exec('ALTER TABLE lesson_blocks_v4 RENAME TO lesson_blocks');
|
||
db.exec('CREATE INDEX IF NOT EXISTS idx_blocks_lesson ON lesson_blocks(lesson_id)');
|
||
db.exec('PRAGMA foreign_keys = ON');
|
||
console.log('✓ lesson_blocks v4: rich content types added');
|
||
}
|
||
} catch (e) {
|
||
try { db.exec('PRAGMA foreign_keys = ON'); } catch {}
|
||
console.warn('lesson_blocks v4 migration skipped:', e.message);
|
||
}
|
||
|
||
/* ── Templates: course & lesson templates ──────────────────────────── */
|
||
const templateMigrations = [
|
||
`CREATE TABLE IF NOT EXISTS course_templates (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
title TEXT NOT NULL,
|
||
description TEXT,
|
||
category TEXT NOT NULL DEFAULT 'general',
|
||
subject_slug TEXT,
|
||
structure TEXT NOT NULL DEFAULT '{}',
|
||
is_public INTEGER NOT NULL DEFAULT 0,
|
||
created_by INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||
)`,
|
||
'CREATE INDEX IF NOT EXISTS idx_ctpl_creator ON course_templates(created_by)',
|
||
'CREATE INDEX IF NOT EXISTS idx_ctpl_public ON course_templates(is_public)',
|
||
`CREATE TABLE IF NOT EXISTS lesson_templates (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
title TEXT NOT NULL,
|
||
category TEXT NOT NULL DEFAULT 'general',
|
||
subject_slug TEXT,
|
||
blocks TEXT NOT NULL DEFAULT '[]',
|
||
is_public INTEGER NOT NULL DEFAULT 0,
|
||
created_by INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||
)`,
|
||
'CREATE INDEX IF NOT EXISTS idx_ltpl_creator ON lesson_templates(created_by)',
|
||
'CREATE INDEX IF NOT EXISTS idx_ltpl_public ON lesson_templates(is_public)',
|
||
];
|
||
for (const sql of templateMigrations) {
|
||
try { db.exec(sql); }
|
||
catch (e) { if (!e.message.includes('duplicate column') && !e.message.includes('already exists')) console.warn(' template migration note:', e.message); }
|
||
}
|
||
console.log('✓ Templates tables ready');
|
||
|
||
/* ── Shop: coins, items, purchases ─────────────────────────────────── */
|
||
try { db.exec("ALTER TABLE users ADD COLUMN coins INTEGER NOT NULL DEFAULT 0"); } catch {}
|
||
const shopMigrations = [
|
||
`CREATE TABLE IF NOT EXISTS shop_items (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
name TEXT NOT NULL,
|
||
description TEXT,
|
||
type TEXT NOT NULL DEFAULT 'frame'
|
||
CHECK (type IN ('frame','theme','title','effect')),
|
||
category TEXT NOT NULL DEFAULT 'cosmetic',
|
||
price INTEGER NOT NULL DEFAULT 100,
|
||
data TEXT NOT NULL DEFAULT '{}',
|
||
icon TEXT NOT NULL DEFAULT 'star',
|
||
is_active INTEGER NOT NULL DEFAULT 1,
|
||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||
)`,
|
||
`CREATE TABLE IF NOT EXISTS user_purchases (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||
item_id INTEGER NOT NULL REFERENCES shop_items(id) ON DELETE CASCADE,
|
||
purchased_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||
UNIQUE(user_id, item_id)
|
||
)`,
|
||
'CREATE INDEX IF NOT EXISTS idx_purchases_user ON user_purchases(user_id)',
|
||
];
|
||
for (const sql of shopMigrations) {
|
||
try { db.exec(sql); }
|
||
catch (e) { if (!e.message.includes('duplicate column') && !e.message.includes('already exists')) console.warn(' shop migration note:', e.message); }
|
||
}
|
||
|
||
// Seed default shop items
|
||
try {
|
||
const cnt = db.prepare('SELECT COUNT(*) as c FROM shop_items').get().c;
|
||
if (cnt === 0) {
|
||
const ins = db.prepare('INSERT INTO shop_items (name, description, type, category, price, data, icon) VALUES (?,?,?,?,?,?,?)');
|
||
const items = [
|
||
['Неоновая рамка', 'Яркая неоновая рамка профиля', 'frame', 'cosmetic', 200, '{"css":"box-shadow:0 0 15px #0ff,inset 0 0 15px #0ff;border-color:#0ff"}', 'sparkles'],
|
||
['Огненная рамка', 'Рамка с эффектом пламени', 'frame', 'cosmetic', 300, '{"css":"box-shadow:0 0 15px #f60,inset 0 0 10px #f60;border-color:#f60"}', 'flame'],
|
||
['Радужная рамка', 'Переливающаяся радужная рамка', 'frame', 'cosmetic', 500, '{"css":"border-image:linear-gradient(135deg,#f06,#0cf,#0f6,#ff0,#f06) 1;box-shadow:0 0 12px rgba(255,0,100,.4)"}', 'star'],
|
||
['Титул: Знаток', 'Отображается под именем в профиле', 'title', 'cosmetic', 150, '{"text":"Знаток","color":"#4CC9F0"}', 'crown'],
|
||
['Титул: Гений', 'Отображается под именем в профиле', 'title', 'cosmetic', 400, '{"text":"Гений","color":"#FFD166"}', 'brain'],
|
||
['Титул: Легенда', 'Отображается под именем в профиле', 'title', 'cosmetic', 800, '{"text":"Легенда","color":"#EF476F"}', 'trophy'],
|
||
['Эффект: Искры', 'Частицы-искры вокруг аватара', 'effect', 'cosmetic', 350, '{"effect":"sparkle"}', 'sparkles'],
|
||
['Эффект: Пульсация', 'Пульсирующее свечение аватара', 'effect', 'cosmetic', 200, '{"effect":"pulse"}', 'zap'],
|
||
['Эффект: Снежинки', 'Снежинки вокруг аватара', 'effect', 'cosmetic', 300, '{"effect":"snow"}', 'star'],
|
||
];
|
||
for (const it of items) ins.run(...it);
|
||
console.log('✓ Shop items seeded');
|
||
}
|
||
} catch (e) { console.warn('Shop seed skipped:', e.message); }
|
||
// Active cosmetics columns
|
||
try { db.exec("ALTER TABLE users ADD COLUMN active_title TEXT DEFAULT NULL"); } catch {}
|
||
try { db.exec("ALTER TABLE users ADD COLUMN active_theme TEXT DEFAULT NULL"); } catch {}
|
||
try { db.exec("ALTER TABLE users ADD COLUMN active_effect TEXT DEFAULT NULL"); } catch {}
|
||
console.log('✓ Shop tables ready');
|
||
|
||
/* ── Token version for JWT invalidation ── */
|
||
try { db.exec("ALTER TABLE users ADD COLUMN token_version INTEGER NOT NULL DEFAULT 0"); } catch {}
|
||
console.log('✓ Token version column ready');
|
||
|
||
/* ── Lab experiments counter ── */
|
||
try { db.exec("ALTER TABLE users ADD COLUMN lab_experiments INTEGER NOT NULL DEFAULT 0"); } catch {}
|
||
try { db.exec("ALTER TABLE users ADD COLUMN lab_reactions INTEGER NOT NULL DEFAULT 0"); } catch {}
|
||
console.log('✓ Lab tracking columns ready');
|
||
|
||
/* ── Lesson comments ─────────────────────────────────────────────────── */
|
||
const commentMigrations = [
|
||
`CREATE TABLE IF NOT EXISTS lesson_comments (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
lesson_id INTEGER NOT NULL REFERENCES lessons(id) ON DELETE CASCADE,
|
||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||
parent_id INTEGER REFERENCES lesson_comments(id) ON DELETE CASCADE,
|
||
text TEXT NOT NULL,
|
||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||
)`,
|
||
'CREATE INDEX IF NOT EXISTS idx_lcomments_lesson ON lesson_comments(lesson_id)',
|
||
'CREATE INDEX IF NOT EXISTS idx_lcomments_user ON lesson_comments(user_id)',
|
||
'CREATE INDEX IF NOT EXISTS idx_lcomments_parent ON lesson_comments(parent_id)',
|
||
];
|
||
for (const sql of commentMigrations) {
|
||
try { db.exec(sql); }
|
||
catch (e) { if (!e.message.includes('duplicate column') && !e.message.includes('already exists')) console.warn(' comment migration note:', e.message); }
|
||
}
|
||
console.log('✓ Lesson comments table ready');
|
||
|
||
/* ── Bookmarks ───────────────────────────────────────────────────────── */
|
||
const bookmarkMigrations = [
|
||
`CREATE TABLE IF NOT EXISTS bookmarks (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||
entity_type TEXT NOT NULL CHECK (entity_type IN ('lesson','course','file','question')),
|
||
entity_id INTEGER NOT NULL,
|
||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||
UNIQUE (user_id, entity_type, entity_id)
|
||
)`,
|
||
'CREATE INDEX IF NOT EXISTS idx_bookmarks_user ON bookmarks(user_id)',
|
||
];
|
||
for (const sql of bookmarkMigrations) {
|
||
try { db.exec(sql); }
|
||
catch (e) { if (!e.message.includes('duplicate column') && !e.message.includes('already exists')) console.warn(' bookmark migration note:', e.message); }
|
||
}
|
||
console.log('✓ Bookmarks table ready');
|
||
|
||
/* ── Submissions v2: expand status CHECK + add reviewed_at ────────────── */
|
||
try {
|
||
const tblDef = db.prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name='submissions'").get();
|
||
if (tblDef && !tblDef.sql.includes('revision')) {
|
||
db.exec('PRAGMA foreign_keys = OFF');
|
||
db.exec(`CREATE TABLE submissions_v2 (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
class_id INTEGER NOT NULL REFERENCES classes(id) ON DELETE CASCADE,
|
||
assignment_id INTEGER REFERENCES assignments(id) ON DELETE SET NULL,
|
||
student_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||
original_name TEXT NOT NULL,
|
||
stored_name TEXT NOT NULL UNIQUE,
|
||
mimetype TEXT,
|
||
size INTEGER,
|
||
message TEXT,
|
||
status TEXT NOT NULL DEFAULT 'new' CHECK (status IN ('new','reviewed','revision','resubmitted','accepted')),
|
||
teacher_note TEXT,
|
||
grade INTEGER,
|
||
reviewed_at TEXT,
|
||
submitted_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||
)`);
|
||
db.exec(`INSERT INTO submissions_v2 (id,class_id,assignment_id,student_id,original_name,stored_name,mimetype,size,message,status,teacher_note,grade,submitted_at)
|
||
SELECT id,class_id,assignment_id,student_id,original_name,stored_name,mimetype,size,message,status,teacher_note,grade,submitted_at FROM submissions`);
|
||
db.exec('DROP TABLE submissions');
|
||
db.exec('ALTER TABLE submissions_v2 RENAME TO submissions');
|
||
db.exec('CREATE INDEX IF NOT EXISTS idx_submissions_class ON submissions(class_id)');
|
||
db.exec('CREATE INDEX IF NOT EXISTS idx_submissions_student ON submissions(student_id)');
|
||
db.exec('CREATE INDEX IF NOT EXISTS idx_submissions_assignment ON submissions(assignment_id)');
|
||
db.exec('PRAGMA foreign_keys = ON');
|
||
console.log('✓ Submissions v2: expanded statuses + reviewed_at');
|
||
}
|
||
} catch (e) {
|
||
try { db.exec('PRAGMA foreign_keys = ON'); } catch {}
|
||
console.warn('Submissions v2 migration skipped:', e.message);
|
||
}
|
||
|
||
/* ── Flashcards ─────────────────────────────────────────────────────────── */
|
||
db.exec(`
|
||
CREATE TABLE IF NOT EXISTS flashcard_decks (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||
title TEXT NOT NULL,
|
||
description TEXT NOT NULL DEFAULT '',
|
||
color TEXT NOT NULL DEFAULT '#9B5DE5',
|
||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||
);
|
||
CREATE TABLE IF NOT EXISTS flashcard_cards (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
deck_id INTEGER NOT NULL REFERENCES flashcard_decks(id) ON DELETE CASCADE,
|
||
front TEXT NOT NULL DEFAULT '',
|
||
back TEXT NOT NULL DEFAULT '',
|
||
order_idx INTEGER NOT NULL DEFAULT 0,
|
||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||
);
|
||
CREATE TABLE IF NOT EXISTS flashcard_reviews (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||
card_id INTEGER NOT NULL REFERENCES flashcard_cards(id) ON DELETE CASCADE,
|
||
ease_factor REAL NOT NULL DEFAULT 2.5,
|
||
interval_days INTEGER NOT NULL DEFAULT 1,
|
||
repetitions INTEGER NOT NULL DEFAULT 0,
|
||
due_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||
last_reviewed TEXT,
|
||
UNIQUE(user_id, card_id)
|
||
);
|
||
`);
|
||
|
||
/* ── Performance indexes ─────────────────────────────────────────────────── */
|
||
try { db.exec('CREATE INDEX IF NOT EXISTS idx_answers_sq ON user_answers(session_id, question_id)'); } catch {}
|
||
try { db.exec('CREATE INDEX IF NOT EXISTS idx_sq_session ON session_questions(session_id)'); } catch {}
|
||
try { db.exec('CREATE INDEX IF NOT EXISTS idx_sessions_user_status ON test_sessions(user_id, status)'); } catch {}
|
||
console.log('✓ Performance indexes ready');
|
||
|
||
/* ── Test attempts: max_attempts on assignments ──────────────────────────── */
|
||
try { db.exec('ALTER TABLE assignments ADD COLUMN max_attempts INTEGER NOT NULL DEFAULT 0'); } catch {}
|
||
console.log('✓ max_attempts column ready');
|
||
|
||
/* ── assignment_sessions v2: allow multiple attempts (remove UNIQUE) ──────── */
|
||
try {
|
||
const cols = db.prepare('PRAGMA table_info(assignment_sessions)').all();
|
||
const hasAttemptNum = cols.some(c => c.name === 'attempt_num');
|
||
if (!hasAttemptNum) {
|
||
db.exec('PRAGMA foreign_keys = OFF');
|
||
db.exec(`
|
||
CREATE TABLE assignment_sessions_v2 (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
assignment_id INTEGER NOT NULL REFERENCES assignments(id) ON DELETE CASCADE,
|
||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||
session_id INTEGER REFERENCES test_sessions(id) ON DELETE SET NULL,
|
||
attempt_num INTEGER NOT NULL DEFAULT 1,
|
||
first_seen_at TEXT
|
||
)
|
||
`);
|
||
db.exec(`INSERT INTO assignment_sessions_v2 (id, assignment_id, user_id, session_id, first_seen_at)
|
||
SELECT id, assignment_id, user_id, session_id, first_seen_at FROM assignment_sessions`);
|
||
db.exec('DROP TABLE assignment_sessions');
|
||
db.exec('ALTER TABLE assignment_sessions_v2 RENAME TO assignment_sessions');
|
||
db.exec('CREATE INDEX IF NOT EXISTS idx_assign_sess_assign ON assignment_sessions(assignment_id)');
|
||
db.exec('CREATE INDEX IF NOT EXISTS idx_assign_sess_user ON assignment_sessions(user_id)');
|
||
db.exec('PRAGMA foreign_keys = ON');
|
||
console.log('✓ assignment_sessions v2: multiple attempts enabled');
|
||
}
|
||
} catch (e) {
|
||
try { db.exec('PRAGMA foreign_keys = ON'); } catch {}
|
||
console.warn('assignment_sessions v2 migration skipped:', e.message);
|
||
}
|
||
|
||
/* ── App settings ────────────────────────────────────────────────────────── */
|
||
db.exec(`
|
||
CREATE TABLE IF NOT EXISTS app_settings (
|
||
key TEXT PRIMARY KEY,
|
||
value TEXT NOT NULL DEFAULT ''
|
||
);
|
||
INSERT OR IGNORE INTO app_settings (key, value) VALUES
|
||
('sim_module_disabled', '0'),
|
||
('sim_disabled_ids', '[]'),
|
||
('feature_crossword_enabled', '1'),
|
||
('feature_hangman_enabled', '1'),
|
||
('feature_pet_enabled', '1'),
|
||
('feature_red_book_enabled', '1'),
|
||
('feature_collection_enabled', '1'),
|
||
('feature_flashcards_enabled', '1'),
|
||
('feature_knowledge_map_enabled', '1'),
|
||
('feature_board_enabled', '1'),
|
||
('feature_biochem_enabled', '1'),
|
||
('feature_live_quiz_enabled', '1'),
|
||
('feature_classroom_enabled', '1');
|
||
`);
|
||
|
||
/* ── Performance indexes ───────────────────────────────────────────────── */
|
||
db.exec(`
|
||
CREATE INDEX IF NOT EXISTS idx_test_sessions_status ON test_sessions(status);
|
||
CREATE INDEX IF NOT EXISTS idx_test_sessions_user_status ON test_sessions(user_id, status);
|
||
CREATE INDEX IF NOT EXISTS idx_assignments_deadline ON assignments(deadline);
|
||
CREATE INDEX IF NOT EXISTS idx_xp_log_user_created ON xp_log(user_id, created_at);
|
||
`);
|
||
console.log('✓ Performance indexes ready');
|
||
|
||
/* ── User ban flag ─────────────────────────────────────────────────────── */
|
||
try { db.exec("ALTER TABLE users ADD COLUMN is_banned INTEGER NOT NULL DEFAULT 0"); } catch {}
|
||
console.log('✓ User ban column ready');
|
||
|
||
/* ── Class cover icon ─────────────────────────────────────────────────── */
|
||
try { db.exec("ALTER TABLE classes ADD COLUMN cover_emoji TEXT NOT NULL DEFAULT ''"); } catch {}
|
||
console.log('✓ Class cover_emoji column ready');
|
||
|
||
/* ── Live Quiz ──────────────────────────────────────────────────────────── */
|
||
db.exec(`
|
||
CREATE TABLE IF NOT EXISTS live_sessions (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
class_id INTEGER NOT NULL REFERENCES classes(id) ON DELETE CASCADE,
|
||
teacher_id INTEGER NOT NULL REFERENCES users(id),
|
||
question_id INTEGER REFERENCES questions(id),
|
||
status TEXT NOT NULL DEFAULT 'waiting',
|
||
show_results INTEGER NOT NULL DEFAULT 0,
|
||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||
ended_at TEXT
|
||
);
|
||
CREATE TABLE IF NOT EXISTS live_answers (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
live_session_id INTEGER NOT NULL REFERENCES live_sessions(id) ON DELETE CASCADE,
|
||
user_id INTEGER NOT NULL REFERENCES users(id),
|
||
option_id INTEGER REFERENCES options(id),
|
||
answer_text TEXT,
|
||
is_correct INTEGER,
|
||
answered_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||
UNIQUE(live_session_id, user_id)
|
||
);
|
||
CREATE INDEX IF NOT EXISTS idx_live_sessions_class ON live_sessions(class_id, status);
|
||
CREATE INDEX IF NOT EXISTS idx_live_answers_session ON live_answers(live_session_id);
|
||
`);
|
||
|
||
// Migration: add question_id to live_answers + recreate unique constraint
|
||
// (so history is preserved when teacher switches between questions in one session)
|
||
{
|
||
const cols = db.prepare('PRAGMA table_info(live_answers)').all();
|
||
const hasQid = cols.some(c => c.name === 'question_id');
|
||
if (!hasQid) {
|
||
db.exec('PRAGMA foreign_keys = OFF');
|
||
db.exec(`
|
||
CREATE TABLE live_answers_v2 (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
live_session_id INTEGER NOT NULL REFERENCES live_sessions(id) ON DELETE CASCADE,
|
||
user_id INTEGER NOT NULL REFERENCES users(id),
|
||
question_id INTEGER REFERENCES questions(id) ON DELETE SET NULL,
|
||
option_id INTEGER REFERENCES options(id),
|
||
answer_text TEXT,
|
||
is_correct INTEGER,
|
||
answered_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||
UNIQUE(live_session_id, user_id, question_id)
|
||
)
|
||
`);
|
||
// Migrate existing rows — old answers don't have question_id, set NULL
|
||
db.exec(`
|
||
INSERT INTO live_answers_v2 (id, live_session_id, user_id, option_id, answer_text, is_correct, answered_at)
|
||
SELECT id, live_session_id, user_id, option_id, answer_text, is_correct, answered_at FROM live_answers
|
||
`);
|
||
db.exec('DROP TABLE live_answers');
|
||
db.exec('ALTER TABLE live_answers_v2 RENAME TO live_answers');
|
||
db.exec('CREATE INDEX IF NOT EXISTS idx_live_answers_session ON live_answers(live_session_id)');
|
||
db.exec('CREATE INDEX IF NOT EXISTS idx_live_answers_question ON live_answers(live_session_id, question_id)');
|
||
db.exec('PRAGMA foreign_keys = ON');
|
||
console.log('✓ Migrated live_answers → added question_id');
|
||
}
|
||
}
|
||
console.log('✓ Live quiz tables ready');
|
||
|
||
/* ── Submission audit log ──────────────────────────────────────────────── */
|
||
db.exec(`
|
||
CREATE TABLE IF NOT EXISTS submission_log (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
submission_id INTEGER NOT NULL,
|
||
class_id INTEGER,
|
||
assignment_id INTEGER,
|
||
student_id INTEGER NOT NULL,
|
||
student_name TEXT,
|
||
original_name TEXT,
|
||
status TEXT,
|
||
grade INTEGER,
|
||
teacher_note TEXT,
|
||
submitted_at TEXT,
|
||
action TEXT NOT NULL DEFAULT 'deleted',
|
||
deleted_by INTEGER NOT NULL,
|
||
deleted_by_role TEXT,
|
||
deleted_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||
)
|
||
`);
|
||
console.log('✓ Submission audit log ready');
|
||
|
||
/* ── Красная книга РБ ───────────────────────────────────────────────────── */
|
||
db.exec(`
|
||
CREATE TABLE IF NOT EXISTS rb_groups (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
name_ru TEXT NOT NULL,
|
||
name_lat TEXT NOT NULL DEFAULT '',
|
||
icon TEXT NOT NULL DEFAULT '🌿',
|
||
color TEXT NOT NULL DEFAULT '#16a34a'
|
||
);
|
||
|
||
CREATE TABLE IF NOT EXISTS rb_habitats (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
name TEXT NOT NULL,
|
||
type TEXT NOT NULL DEFAULT 'forest',
|
||
description TEXT NOT NULL DEFAULT '',
|
||
sound_file TEXT NOT NULL DEFAULT ''
|
||
);
|
||
|
||
CREATE TABLE IF NOT EXISTS rb_species (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
group_id INTEGER NOT NULL REFERENCES rb_groups(id),
|
||
habitat_id INTEGER REFERENCES rb_habitats(id),
|
||
name_ru TEXT NOT NULL,
|
||
name_be TEXT NOT NULL DEFAULT '',
|
||
name_lat TEXT NOT NULL DEFAULT '',
|
||
category TEXT NOT NULL DEFAULT 'VU'
|
||
CHECK (category IN ('CR','EN','VU','NT','LC')),
|
||
by_category TEXT NOT NULL DEFAULT 'III',
|
||
description TEXT NOT NULL DEFAULT '',
|
||
interesting_fact TEXT NOT NULL DEFAULT '',
|
||
threats TEXT NOT NULL DEFAULT '[]',
|
||
conservation TEXT NOT NULL DEFAULT '',
|
||
where_to_see TEXT NOT NULL DEFAULT '',
|
||
photo_url TEXT NOT NULL DEFAULT '',
|
||
model_type TEXT NOT NULL DEFAULT 'silhouette'
|
||
CHECK (model_type IN ('procedural','silhouette','none')),
|
||
population_trend TEXT NOT NULL DEFAULT '[]',
|
||
biomass_kg REAL NOT NULL DEFAULT 0,
|
||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||
);
|
||
|
||
CREATE TABLE IF NOT EXISTS rb_species_regions (
|
||
species_id INTEGER NOT NULL REFERENCES rb_species(id) ON DELETE CASCADE,
|
||
region_code TEXT NOT NULL
|
||
CHECK (region_code IN ('brest','vitebsk','gomel','grodno','minsk','mogilev')),
|
||
PRIMARY KEY (species_id, region_code)
|
||
);
|
||
|
||
CREATE TABLE IF NOT EXISTS rb_food_web (
|
||
predator_id INTEGER NOT NULL REFERENCES rb_species(id) ON DELETE CASCADE,
|
||
prey_id INTEGER NOT NULL REFERENCES rb_species(id) ON DELETE CASCADE,
|
||
strength REAL NOT NULL DEFAULT 0.5,
|
||
PRIMARY KEY (predator_id, prey_id)
|
||
);
|
||
|
||
CREATE TABLE IF NOT EXISTS rb_population_data (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
species_id INTEGER NOT NULL REFERENCES rb_species(id) ON DELETE CASCADE,
|
||
year INTEGER NOT NULL,
|
||
count_estimate INTEGER NOT NULL DEFAULT 0,
|
||
source TEXT NOT NULL DEFAULT ''
|
||
);
|
||
|
||
CREATE TABLE IF NOT EXISTS rb_user_collection (
|
||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||
species_id INTEGER NOT NULL REFERENCES rb_species(id) ON DELETE CASCADE,
|
||
unlock_method TEXT NOT NULL DEFAULT 'explore',
|
||
notes TEXT NOT NULL DEFAULT '',
|
||
unlocked_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||
PRIMARY KEY (user_id, species_id)
|
||
);
|
||
|
||
CREATE TABLE IF NOT EXISTS rb_quests (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
title TEXT NOT NULL,
|
||
description TEXT NOT NULL DEFAULT '',
|
||
species_ids TEXT NOT NULL DEFAULT '[]',
|
||
xp_reward INTEGER NOT NULL DEFAULT 150,
|
||
badge_slug TEXT NOT NULL DEFAULT ''
|
||
);
|
||
|
||
CREATE TABLE IF NOT EXISTS rb_user_quests (
|
||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||
quest_id INTEGER NOT NULL REFERENCES rb_quests(id) ON DELETE CASCADE,
|
||
status TEXT NOT NULL DEFAULT 'active'
|
||
CHECK (status IN ('active','completed')),
|
||
progress TEXT NOT NULL DEFAULT '{}',
|
||
completed_at TEXT,
|
||
PRIMARY KEY (user_id, quest_id)
|
||
);
|
||
|
||
CREATE TABLE IF NOT EXISTS rb_sightings (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||
species_id INTEGER NOT NULL REFERENCES rb_species(id) ON DELETE CASCADE,
|
||
region_code TEXT NOT NULL DEFAULT '',
|
||
description TEXT NOT NULL DEFAULT '',
|
||
photo_url TEXT NOT NULL DEFAULT '',
|
||
confirmed_by_teacher INTEGER NOT NULL DEFAULT 0,
|
||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||
);
|
||
|
||
CREATE INDEX IF NOT EXISTS idx_rb_species_group ON rb_species(group_id);
|
||
CREATE INDEX IF NOT EXISTS idx_rb_species_category ON rb_species(category);
|
||
CREATE INDEX IF NOT EXISTS idx_rb_collection_user ON rb_user_collection(user_id);
|
||
CREATE INDEX IF NOT EXISTS idx_rb_sightings_user ON rb_sightings(user_id);
|
||
CREATE INDEX IF NOT EXISTS idx_rb_popdata_species ON rb_population_data(species_id);
|
||
`);
|
||
console.log('✓ Red Book tables ready');
|
||
|
||
// Red Book: add season_active column if not exists
|
||
try { db.exec("ALTER TABLE rb_species ADD COLUMN season_active TEXT NOT NULL DEFAULT ''"); } catch {}
|
||
// Red Book: add name_be_short column if not exists
|
||
try { db.exec("ALTER TABLE rb_species ADD COLUMN name_be_short TEXT NOT NULL DEFAULT ''"); } catch {}
|
||
|
||
// Classes: per-class feature toggles (JSON)
|
||
try { db.exec("ALTER TABLE classes ADD COLUMN features TEXT"); } catch {}
|
||
console.log('✓ Classes features column ready');
|
||
|
||
/* ── Biochemistry / Molecular Constructor ────────────────────────────────── */
|
||
db.exec(`
|
||
CREATE TABLE IF NOT EXISTS bio_elements (
|
||
symbol TEXT PRIMARY KEY,
|
||
name_ru TEXT NOT NULL,
|
||
valency_max INTEGER NOT NULL DEFAULT 1,
|
||
valency_opts TEXT NOT NULL DEFAULT '[1]',
|
||
color TEXT NOT NULL DEFAULT '#888888',
|
||
radius INTEGER NOT NULL DEFAULT 20,
|
||
mass REAL NOT NULL DEFAULT 1.0
|
||
);
|
||
CREATE TABLE IF NOT EXISTS bio_molecules (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
formula TEXT NOT NULL,
|
||
name_ru TEXT NOT NULL,
|
||
name_lat TEXT NOT NULL DEFAULT '',
|
||
category TEXT NOT NULL DEFAULT 'inorganic',
|
||
difficulty INTEGER NOT NULL DEFAULT 1,
|
||
description TEXT NOT NULL DEFAULT '',
|
||
atoms_json TEXT NOT NULL DEFAULT '[]',
|
||
bonds_json TEXT NOT NULL DEFAULT '[]',
|
||
is_library INTEGER NOT NULL DEFAULT 1,
|
||
topic_tags TEXT NOT NULL DEFAULT '[]',
|
||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||
);
|
||
CREATE TABLE IF NOT EXISTS bio_reactions (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
equation TEXT NOT NULL,
|
||
name_ru TEXT NOT NULL,
|
||
type TEXT NOT NULL DEFAULT 'synthesis',
|
||
description TEXT NOT NULL DEFAULT '',
|
||
reactant_ids TEXT NOT NULL DEFAULT '[]',
|
||
product_ids TEXT NOT NULL DEFAULT '[]',
|
||
conditions TEXT DEFAULT NULL,
|
||
energy_kj REAL DEFAULT NULL,
|
||
topic_tags TEXT NOT NULL DEFAULT '[]'
|
||
);
|
||
CREATE TABLE IF NOT EXISTS bio_challenges (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
title TEXT NOT NULL,
|
||
description TEXT NOT NULL DEFAULT '',
|
||
type TEXT NOT NULL DEFAULT 'build',
|
||
target_formula TEXT NOT NULL DEFAULT '',
|
||
hint TEXT DEFAULT NULL,
|
||
xp_reward INTEGER NOT NULL DEFAULT 50,
|
||
difficulty INTEGER NOT NULL DEFAULT 1,
|
||
topic_tag TEXT DEFAULT NULL,
|
||
order_n INTEGER NOT NULL DEFAULT 0
|
||
);
|
||
CREATE TABLE IF NOT EXISTS bio_user_molecules (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||
molecule_id INTEGER REFERENCES bio_molecules(id),
|
||
name TEXT DEFAULT NULL,
|
||
formula TEXT NOT NULL,
|
||
atoms_json TEXT NOT NULL,
|
||
bonds_json TEXT NOT NULL,
|
||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||
);
|
||
CREATE TABLE IF NOT EXISTS bio_user_challenges (
|
||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||
challenge_id INTEGER NOT NULL REFERENCES bio_challenges(id) ON DELETE CASCADE,
|
||
completed_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||
PRIMARY KEY (user_id, challenge_id)
|
||
);
|
||
CREATE INDEX IF NOT EXISTS idx_bio_mol_formula ON bio_molecules(formula);
|
||
CREATE INDEX IF NOT EXISTS idx_bio_mol_cat ON bio_molecules(category);
|
||
CREATE INDEX IF NOT EXISTS idx_bio_user_mol ON bio_user_molecules(user_id);
|
||
CREATE INDEX IF NOT EXISTS idx_bio_user_chal ON bio_user_challenges(user_id);
|
||
`);
|
||
console.log('✓ Biochemistry tables ready');
|
||
|
||
/* Seed elements */
|
||
if (!db.prepare('SELECT 1 FROM bio_elements LIMIT 1').get()) {
|
||
const ins = db.prepare('INSERT INTO bio_elements (symbol,name_ru,valency_max,valency_opts,color,radius,mass) VALUES (?,?,?,?,?,?,?)');
|
||
[
|
||
['H', 'Водород', 1, '[1]', '#D4D4D4', 18, 1.008],
|
||
['C', 'Углерод', 4, '[4]', '#555555', 20, 12.011],
|
||
['N', 'Азот', 3, '[3,5]', '#4060FF', 20, 14.007],
|
||
['O', 'Кислород', 2, '[2]', '#FF2020', 20, 15.999],
|
||
['P', 'Фосфор', 5, '[3,5]', '#FF8000', 22, 30.974],
|
||
['S', 'Сера', 6, '[2,4,6]', '#C8B400', 22, 32.060],
|
||
['Cl', 'Хлор', 1, '[1]', '#00A860', 22, 35.450],
|
||
['Na', 'Натрий', 1, '[1]', '#8040C0', 22, 22.990],
|
||
['Ca', 'Кальций', 2, '[2]', '#707070', 22, 40.078],
|
||
['Mg', 'Магний', 2, '[2]', '#1E8A1E', 22, 24.305],
|
||
['Fe', 'Железо', 3, '[2,3]', '#B03010', 22, 55.845],
|
||
].forEach(r => ins.run(...r));
|
||
console.log('✓ Bio elements seeded');
|
||
}
|
||
|
||
/* Seed molecules */
|
||
if (!db.prepare('SELECT 1 FROM bio_molecules LIMIT 1').get()) {
|
||
const ins = db.prepare(`INSERT INTO bio_molecules
|
||
(formula,name_ru,name_lat,category,difficulty,description,atoms_json,bonds_json,topic_tags)
|
||
VALUES (?,?,?,?,?,?,?,?,?)`);
|
||
const M = (f,r,l,cat,d,desc,a,b,t) =>
|
||
ins.run(f,r,l,cat,d,desc,JSON.stringify(a),JSON.stringify(b),JSON.stringify(t));
|
||
|
||
// ── Неорганика ──
|
||
M('H2O','Вода','Water','inorganic',1,'Основа жизни. Полярная молекула с угловой геометрией (104.5°). Универсальный растворитель.',
|
||
[{id:1,s:'O',x:0,y:0},{id:2,s:'H',x:-52,y:40},{id:3,s:'H',x:52,y:40}],
|
||
[{f:2,t:1,o:1},{f:3,t:1,o:1}],['вода','полярность','растворитель']);
|
||
|
||
M('CO2','Углекислый газ','Carbon dioxide','inorganic',1,'Продукт клеточного дыхания. Субстрат фотосинтеза. Линейная молекула.',
|
||
[{id:1,s:'C',x:0,y:0},{id:2,s:'O',x:-65,y:0},{id:3,s:'O',x:65,y:0}],
|
||
[{f:2,t:1,o:2},{f:3,t:1,o:2}],['фотосинтез','дыхание']);
|
||
|
||
M('O2','Молекулярный кислород','Oxygen','inorganic',1,'Продукт фотосинтеза. Необходим для аэробного дыхания. Двойная связь O=O.',
|
||
[{id:1,s:'O',x:-30,y:0},{id:2,s:'O',x:30,y:0}],
|
||
[{f:1,t:2,o:2}],['дыхание','фотосинтез']);
|
||
|
||
M('N2','Молекулярный азот','Nitrogen','inorganic',1,'78% атмосферы. Тройная связь N≡N — очень прочная. Химически инертен.',
|
||
[{id:1,s:'N',x:-30,y:0},{id:2,s:'N',x:30,y:0}],
|
||
[{f:1,t:2,o:3}],['атмосфера','азот']);
|
||
|
||
M('H2','Молекулярный водород','Hydrogen','inorganic',1,'Самый лёгкий газ. Восстановитель. Перспективное топливо будущего.',
|
||
[{id:1,s:'H',x:-25,y:0},{id:2,s:'H',x:25,y:0}],
|
||
[{f:1,t:2,o:1}],['водород']);
|
||
|
||
M('NH3','Аммиак','Ammonia','inorganic',1,'Тригонально-пирамидальная молекула. Участвует в синтезе аминокислот. Основание.',
|
||
[{id:1,s:'N',x:0,y:-5},{id:2,s:'H',x:-52,y:42},{id:3,s:'H',x:52,y:42},{id:4,s:'H',x:0,y:-60}],
|
||
[{f:2,t:1,o:1},{f:3,t:1,o:1},{f:4,t:1,o:1}],['азот','аминокислоты']);
|
||
|
||
M('HCl','Соляная кислота','Hydrochloric acid','inorganic',1,'Компонент желудочного сока. Сильная одноосновная кислота.',
|
||
[{id:1,s:'H',x:-35,y:0},{id:2,s:'Cl',x:35,y:0}],
|
||
[{f:1,t:2,o:1}],['кислота','пищеварение']);
|
||
|
||
M('H2O2','Пероксид водорода','Hydrogen peroxide','inorganic',2,'Сильный окислитель. Используется для дезинфекции ран.',
|
||
[{id:1,s:'H',x:-70,y:0},{id:2,s:'O',x:-20,y:0},{id:3,s:'O',x:30,y:0},{id:4,s:'H',x:80,y:0}],
|
||
[{f:1,t:2,o:1},{f:2,t:3,o:1},{f:3,t:4,o:1}],['перекись','дезинфекция']);
|
||
|
||
M('NaOH','Гидроксид натрия','Sodium hydroxide','inorganic',2,'Щёлочь. Мыльный раствор. Нейтрализует кислоты.',
|
||
[{id:1,s:'Na',x:-55,y:0},{id:2,s:'O',x:10,y:0},{id:3,s:'H',x:60,y:0}],
|
||
[{f:1,t:2,o:1},{f:2,t:3,o:1}],['основание','щёлочь']);
|
||
|
||
M('CO','Угарный газ','Carbon monoxide','inorganic',2,'Тройная связь C≡O. Смертельно токсичен. Связывается с гемоглобином.',
|
||
[{id:1,s:'C',x:-25,y:0},{id:2,s:'O',x:35,y:0}],
|
||
[{f:1,t:2,o:3}],['токсин','гемоглобин']);
|
||
|
||
// ── Органика ──
|
||
M('CH4','Метан','Methane','organic',1,'Простейший алкан. Болотный газ. Тетраэдрическая геометрия.',
|
||
[{id:1,s:'C',x:0,y:0},{id:2,s:'H',x:0,y:-58},{id:3,s:'H',x:55,y:30},{id:4,s:'H',x:-55,y:30},{id:5,s:'H',x:0,y:58}],
|
||
[{f:2,t:1,o:1},{f:3,t:1,o:1},{f:4,t:1,o:1},{f:5,t:1,o:1}],['алканы','органика']);
|
||
|
||
M('C2H4','Этилен','Ethylene','organic',2,'Алкен. Двойная связь C=C. Фитогормон — ускоряет созревание плодов.',
|
||
[{id:1,s:'C',x:-35,y:0},{id:2,s:'C',x:35,y:0},{id:3,s:'H',x:-72,y:-44},{id:4,s:'H',x:-72,y:44},{id:5,s:'H',x:72,y:-44},{id:6,s:'H',x:72,y:44}],
|
||
[{f:1,t:2,o:2},{f:3,t:1,o:1},{f:4,t:1,o:1},{f:5,t:2,o:1},{f:6,t:2,o:1}],['алкены','фитогормоны']);
|
||
|
||
M('C2H2','Ацетилен','Acetylene','organic',2,'Алкин. Тройная связь C≡C. Линейная молекула. Используется в сварке.',
|
||
[{id:1,s:'C',x:-30,y:0},{id:2,s:'C',x:30,y:0},{id:3,s:'H',x:-80,y:0},{id:4,s:'H',x:80,y:0}],
|
||
[{f:3,t:1,o:1},{f:1,t:2,o:3},{f:2,t:4,o:1}],['алкины','органика']);
|
||
|
||
M('C2H5OH','Этанол','Ethanol','organic',2,'Одноатомный спирт. Продукт брожения глюкозы. Растворитель. Нейротоксин.',
|
||
[{id:1,s:'C',x:-65,y:0},{id:2,s:'C',x:0,y:0},{id:3,s:'O',x:60,y:0},{id:4,s:'H',x:-65,y:-50},{id:5,s:'H',x:-65,y:50},{id:6,s:'H',x:-115,y:0},{id:7,s:'H',x:0,y:-50},{id:8,s:'H',x:0,y:50},{id:9,s:'H',x:95,y:-28}],
|
||
[{f:1,t:2,o:1},{f:2,t:3,o:1},{f:4,t:1,o:1},{f:5,t:1,o:1},{f:6,t:1,o:1},{f:7,t:2,o:1},{f:8,t:2,o:1},{f:9,t:3,o:1}],['спирты','брожение','органика']);
|
||
|
||
M('CH3COOH','Уксусная кислота','Acetic acid','organic',2,'Карбоновая кислота. Продукт окисления этанола. Компонент уксуса.',
|
||
[{id:1,s:'C',x:-55,y:0},{id:2,s:'C',x:10,y:0},{id:3,s:'O',x:65,y:-38},{id:4,s:'O',x:65,y:38},{id:5,s:'H',x:-55,y:-50},{id:6,s:'H',x:-55,y:50},{id:7,s:'H',x:-105,y:0},{id:8,s:'H',x:90,y:52}],
|
||
[{f:1,t:2,o:1},{f:2,t:3,o:2},{f:2,t:4,o:1},{f:5,t:1,o:1},{f:6,t:1,o:1},{f:7,t:1,o:1},{f:8,t:4,o:1}],['карбоновые кислоты','органика']);
|
||
|
||
console.log('✓ Bio molecules seeded');
|
||
}
|
||
|
||
/* Additional molecules (safe for existing DBs — inserts only if name absent) */
|
||
{
|
||
const addMol = db.prepare(`INSERT INTO bio_molecules
|
||
(formula,name_ru,name_lat,category,difficulty,description,atoms_json,bonds_json,topic_tags)
|
||
SELECT ?,?,?,?,?,?,?,?,? WHERE NOT EXISTS (SELECT 1 FROM bio_molecules WHERE name_ru=?)`);
|
||
const A = (f,r,l,cat,d,desc,a,b,t) =>
|
||
addMol.run(f,r,l,cat,d,desc,JSON.stringify(a),JSON.stringify(b),JSON.stringify(t),r);
|
||
|
||
A('C2H6','Этан','Ethane','organic',1,'Второй алкан. Одинарная C-C связь. Компонент природного газа.',
|
||
[{id:1,s:'C',x:-40,y:0},{id:2,s:'C',x:40,y:0},{id:3,s:'H',x:-40,y:-55},{id:4,s:'H',x:-40,y:55},{id:5,s:'H',x:-95,y:0},{id:6,s:'H',x:40,y:-55},{id:7,s:'H',x:40,y:55},{id:8,s:'H',x:95,y:0}],
|
||
[{f:1,t:2,o:1},{f:3,t:1,o:1},{f:4,t:1,o:1},{f:5,t:1,o:1},{f:6,t:2,o:1},{f:7,t:2,o:1},{f:8,t:2,o:1}],['алканы','органика']);
|
||
|
||
A('C3H8','Пропан','Propane','organic',2,'Третий алкан. Три атома углерода. Используется как топливо.',
|
||
[{id:1,s:'C',x:-80,y:0},{id:2,s:'C',x:0,y:0},{id:3,s:'C',x:80,y:0},{id:4,s:'H',x:-80,y:-52},{id:5,s:'H',x:-80,y:52},{id:6,s:'H',x:-130,y:0},{id:7,s:'H',x:0,y:-52},{id:8,s:'H',x:0,y:52},{id:9,s:'H',x:80,y:-52},{id:10,s:'H',x:80,y:52},{id:11,s:'H',x:130,y:0}],
|
||
[{f:1,t:2,o:1},{f:2,t:3,o:1},{f:4,t:1,o:1},{f:5,t:1,o:1},{f:6,t:1,o:1},{f:7,t:2,o:1},{f:8,t:2,o:1},{f:9,t:3,o:1},{f:10,t:3,o:1},{f:11,t:3,o:1}],['алканы','органика']);
|
||
|
||
A('HCHO','Формальдегид','Formaldehyde','organic',2,'Простейший альдегид. Двойная C=O. Консервант (формалин). Токсичен.',
|
||
[{id:1,s:'C',x:0,y:0},{id:2,s:'H',x:-52,y:-38},{id:3,s:'H',x:-52,y:38},{id:4,s:'O',x:60,y:0}],
|
||
[{f:2,t:1,o:1},{f:3,t:1,o:1},{f:1,t:4,o:2}],['альдегиды','органика']);
|
||
|
||
A('C3H6O','Ацетон','Acetone','organic',2,'Простейший кетон. C=O в центре цепи. Растворитель.',
|
||
[{id:1,s:'C',x:-65,y:0},{id:2,s:'C',x:0,y:0},{id:3,s:'C',x:65,y:0},{id:4,s:'O',x:0,y:-55},{id:5,s:'H',x:-65,y:-50},{id:6,s:'H',x:-65,y:50},{id:7,s:'H',x:-115,y:0},{id:8,s:'H',x:65,y:-50},{id:9,s:'H',x:65,y:50},{id:10,s:'H',x:115,y:0}],
|
||
[{f:1,t:2,o:1},{f:2,t:3,o:1},{f:2,t:4,o:2},{f:5,t:1,o:1},{f:6,t:1,o:1},{f:7,t:1,o:1},{f:8,t:3,o:1},{f:9,t:3,o:1},{f:10,t:3,o:1}],['кетоны','органика']);
|
||
|
||
A('HCOOH','Муравьиная кислота','Formic acid','organic',2,'Простейшая карбоновая кислота. В яде муравьёв.',
|
||
[{id:1,s:'H',x:-70,y:0},{id:2,s:'C',x:-10,y:0},{id:3,s:'O',x:45,y:-40},{id:4,s:'O',x:45,y:40},{id:5,s:'H',x:82,y:54}],
|
||
[{f:1,t:2,o:1},{f:2,t:3,o:2},{f:2,t:4,o:1},{f:4,t:5,o:1}],['карбоновые кислоты','органика']);
|
||
|
||
A('C2H5NO2','Глицин','Glycine','aminoacid',3,'Простейшая аминокислота. NH₂-CH₂-COOH. Нейромедиатор.',
|
||
[{id:1,s:'N',x:-90,y:0},{id:2,s:'C',x:-25,y:0},{id:3,s:'C',x:40,y:0},{id:4,s:'O',x:95,y:-35},{id:5,s:'O',x:95,y:35},{id:6,s:'H',x:-90,y:-48},{id:7,s:'H',x:-90,y:48},{id:8,s:'H',x:-25,y:-50},{id:9,s:'H',x:-25,y:50},{id:10,s:'H',x:118,y:48}],
|
||
[{f:1,t:2,o:1},{f:2,t:3,o:1},{f:3,t:4,o:2},{f:3,t:5,o:1},{f:6,t:1,o:1},{f:7,t:1,o:1},{f:8,t:2,o:1},{f:9,t:2,o:1},{f:10,t:5,o:1}],['аминокислоты','белки']);
|
||
|
||
A('NaCl','Хлорид натрия','Sodium chloride','inorganic',1,'Поваренная соль. Важнейший электролит крови.',
|
||
[{id:1,s:'Na',x:-35,y:0},{id:2,s:'Cl',x:35,y:0}],
|
||
[{f:1,t:2,o:1}],['соль','электролиты','кровь']);
|
||
|
||
A('H2SO4','Серная кислота','Sulfuric acid','inorganic',3,'Сильная двухосновная кислота. Дегидратирующий агент.',
|
||
[{id:1,s:'S',x:0,y:0},{id:2,s:'O',x:0,y:-58},{id:3,s:'O',x:0,y:58},{id:4,s:'O',x:-55,y:0},{id:5,s:'O',x:55,y:0},{id:6,s:'H',x:-92,y:0},{id:7,s:'H',x:92,y:0}],
|
||
[{f:1,t:2,o:2},{f:1,t:3,o:2},{f:1,t:4,o:1},{f:1,t:5,o:1},{f:4,t:6,o:1},{f:5,t:7,o:1}],['кислота','промышленность']);
|
||
|
||
A('HNO3','Азотная кислота','Nitric acid','inorganic',3,'Сильная кислота-окислитель. Синтез удобрений.',
|
||
[{id:1,s:'H',x:-90,y:30},{id:2,s:'O',x:-45,y:30},{id:3,s:'N',x:5,y:0},{id:4,s:'O',x:55,y:-35},{id:5,s:'O',x:55,y:35}],
|
||
[{f:1,t:2,o:1},{f:2,t:3,o:1},{f:3,t:4,o:2},{f:3,t:5,o:1}],['кислота','азот']);
|
||
|
||
console.log('✓ Additional bio molecules checked/added');
|
||
}
|
||
|
||
/* ── Phase 6: 30+ more molecules ─────────────────────────────────────────── */
|
||
{
|
||
const P6 = db.prepare(`INSERT INTO bio_molecules
|
||
(formula,name_ru,name_lat,category,difficulty,description,atoms_json,bonds_json,topic_tags,is_library)
|
||
SELECT ?,?,?,?,?,?,?,?,?,1 WHERE NOT EXISTS (SELECT 1 FROM bio_molecules WHERE name_ru=?)`);
|
||
const M = (f,r,l,cat,d,desc,a,b,t) =>
|
||
P6.run(f,r,l,cat,d,desc,JSON.stringify(a),JSON.stringify(b),JSON.stringify(t),r);
|
||
|
||
// ── Неорганика ──
|
||
M('H2S','Сероводород','Hydrogen sulfide','inorganic',1,
|
||
'Ядовитый газ с запахом тухлых яиц. Участвует в круговороте серы. Слабая кислота.',
|
||
[{id:1,s:'S',x:0,y:0},{id:2,s:'H',x:-48,y:36},{id:3,s:'H',x:48,y:36}],
|
||
[{f:2,t:1,o:1},{f:3,t:1,o:1}],['сера','кислота']);
|
||
|
||
M('SO2','Диоксид серы','Sulfur dioxide','inorganic',2,
|
||
'Бесцветный газ с резким запахом. Образуется при сжигании серосодержащего топлива. Кислотный оксид.',
|
||
[{id:1,s:'S',x:0,y:0},{id:2,s:'O',x:-65,y:-15},{id:3,s:'O',x:65,y:-15}],
|
||
[{f:2,t:1,o:2},{f:3,t:1,o:2}],['сера','кислотный оксид','атмосфера']);
|
||
|
||
M('SO3','Триоксид серы','Sulfur trioxide','inorganic',2,
|
||
'Реагирует с водой, образуя серную кислоту H₂SO₄. Тригональная плоская молекула.',
|
||
[{id:1,s:'S',x:0,y:0},{id:2,s:'O',x:-65,y:38},{id:3,s:'O',x:65,y:38},{id:4,s:'O',x:0,y:-65}],
|
||
[{f:2,t:1,o:2},{f:3,t:1,o:2},{f:4,t:1,o:2}],['сера','кислота']);
|
||
|
||
M('NO','Оксид азота (II)','Nitric oxide','inorganic',2,
|
||
'Нейромедиатор. Регулирует сосудистый тонус. Важная молекула-сигнал в организме.',
|
||
[{id:1,s:'N',x:-30,y:0},{id:2,s:'O',x:30,y:0}],
|
||
[{f:1,t:2,o:2}],['азот','нейромедиатор','сигнал']);
|
||
|
||
M('NO2','Диоксид азота','Nitrogen dioxide','inorganic',2,
|
||
'Бурый ядовитый газ. Компонент смога. Участвует в образовании кислотных дождей.',
|
||
[{id:1,s:'N',x:0,y:0},{id:2,s:'O',x:-60,y:-20},{id:3,s:'O',x:60,y:-20}],
|
||
[{f:2,t:1,o:2},{f:3,t:1,o:1}],['азот','смог','атмосфера']);
|
||
|
||
M('MgCl2','Хлорид магния','Magnesium chloride','inorganic',1,
|
||
'Ионное соединение. Содержится в морской воде. Применяется как противогололёдный реагент.',
|
||
[{id:1,s:'Cl',x:-80,y:0},{id:2,s:'Mg',x:0,y:0},{id:3,s:'Cl',x:80,y:0}],
|
||
[{f:1,t:2,o:1},{f:3,t:2,o:1}],['магний','ионная связь','соль']);
|
||
|
||
M('CaO','Оксид кальция','Calcium oxide','inorganic',1,
|
||
'Негашёная известь. Щелочной оксид. Реагирует с водой с выделением большого количества тепла.',
|
||
[{id:1,s:'Ca',x:-35,y:0},{id:2,s:'O',x:35,y:0}],
|
||
[{f:1,t:2,o:2}],['кальций','оксид','основной оксид']);
|
||
|
||
M('FeCl3','Хлорид железа (III)','Iron(III) chloride','inorganic',2,
|
||
'Качественный реагент на фенол. Катализатор хлорирования ароматических соединений.',
|
||
[{id:1,s:'Fe',x:0,y:0},{id:2,s:'Cl',x:-70,y:45},{id:3,s:'Cl',x:70,y:45},{id:4,s:'Cl',x:0,y:-75}],
|
||
[{f:1,t:2,o:1},{f:1,t:3,o:1},{f:1,t:4,o:1}],['железо','катализатор','галоген']);
|
||
|
||
M('H3PO4','Фосфорная кислота','Phosphoric acid','inorganic',3,
|
||
'Трёхосновная кислота. Применяется в производстве удобрений и пищевой промышленности (E338).',
|
||
[{id:1,s:'P',x:0,y:0},{id:2,s:'O',x:0,y:-65},{id:3,s:'O',x:-60,y:45},{id:4,s:'O',x:60,y:45},{id:5,s:'O',x:75,y:-45},
|
||
{id:6,s:'H',x:-95,y:70},{id:7,s:'H',x:95,y:70},{id:8,s:'H',x:110,y:-65}],
|
||
[{f:1,t:2,o:2},{f:1,t:3,o:1},{f:1,t:4,o:1},{f:1,t:5,o:1},
|
||
{f:6,t:3,o:1},{f:7,t:4,o:1},{f:8,t:5,o:1}],['фосфор','кислота','удобрения']);
|
||
|
||
// ── Органика ──
|
||
M('CH3OH','Метанол','Methanol','organic',2,
|
||
'Простейший спирт. Ядовит! Вызывает слепоту и смерть. Используется как топливо и растворитель.',
|
||
[{id:1,s:'C',x:-25,y:0},{id:2,s:'O',x:40,y:0},{id:3,s:'H',x:80,y:0},
|
||
{id:4,s:'H',x:-58,y:-35},{id:5,s:'H',x:-58,y:35},{id:6,s:'H',x:-58,y:0}],
|
||
[{f:1,t:2,o:1},{f:2,t:3,o:1},{f:4,t:1,o:1},{f:5,t:1,o:1},{f:6,t:1,o:1}],
|
||
['спирт','топливо','токсин']);
|
||
|
||
M('C3H7OH','Пропанол','Propanol','organic',2,
|
||
'Спирт с 3 углеродами. Растворитель. Компонент антисептических средств. CH₃CH₂CH₂OH.',
|
||
[{id:1,s:'C',x:-65,y:0},{id:2,s:'C',x:0,y:0},{id:3,s:'C',x:65,y:0},{id:4,s:'O',x:110,y:0},{id:5,s:'H',x:150,y:0},
|
||
{id:6,s:'H',x:-100,y:-35},{id:7,s:'H',x:-100,y:35},{id:8,s:'H',x:-100,y:0},
|
||
{id:9,s:'H',x:0,y:-45},{id:10,s:'H',x:0,y:45},
|
||
{id:11,s:'H',x:65,y:-45},{id:12,s:'H',x:65,y:45}],
|
||
[{f:1,t:2,o:1},{f:2,t:3,o:1},{f:3,t:4,o:1},{f:4,t:5,o:1},
|
||
{f:6,t:1,o:1},{f:7,t:1,o:1},{f:8,t:1,o:1},
|
||
{f:9,t:2,o:1},{f:10,t:2,o:1},{f:11,t:3,o:1},{f:12,t:3,o:1}],
|
||
['спирт','антисептик','растворитель']);
|
||
|
||
M('C2H4O','Ацетальдегид','Acetaldehyde','organic',2,
|
||
'Простейший альдегид. Промежуточный продукт окисления этанола. Образуется при брожении. CH₃CHO.',
|
||
[{id:1,s:'C',x:-35,y:0},{id:2,s:'C',x:35,y:0},{id:3,s:'O',x:80,y:-35},{id:4,s:'H',x:80,y:35},
|
||
{id:5,s:'H',x:-70,y:-38},{id:6,s:'H',x:-70,y:38},{id:7,s:'H',x:-70,y:0}],
|
||
[{f:1,t:2,o:1},{f:2,t:3,o:2},{f:2,t:4,o:1},{f:5,t:1,o:1},{f:6,t:1,o:1},{f:7,t:1,o:1}],
|
||
['альдегид','брожение','метаболизм']);
|
||
|
||
M('C6H6','Бензол','Benzene','organic',3,
|
||
'Ароматический углеводород с кольцом из 6 углеродов. Канцероген. Основа ароматической химии.',
|
||
[{id:1,s:'C',x:50,y:0},{id:2,s:'C',x:25,y:43},{id:3,s:'C',x:-25,y:43},
|
||
{id:4,s:'C',x:-50,y:0},{id:5,s:'C',x:-25,y:-43},{id:6,s:'C',x:25,y:-43},
|
||
{id:7,s:'H',x:90,y:0},{id:8,s:'H',x:45,y:78},{id:9,s:'H',x:-45,y:78},
|
||
{id:10,s:'H',x:-90,y:0},{id:11,s:'H',x:-45,y:-78},{id:12,s:'H',x:45,y:-78}],
|
||
[{f:1,t:2,o:2},{f:2,t:3,o:1},{f:3,t:4,o:2},{f:4,t:5,o:1},{f:5,t:6,o:2},{f:6,t:1,o:1},
|
||
{f:1,t:7,o:1},{f:2,t:8,o:1},{f:3,t:9,o:1},{f:4,t:10,o:1},{f:5,t:11,o:1},{f:6,t:12,o:1}],
|
||
['ароматика','кольцо','канцероген']);
|
||
|
||
M('C3H6','Пропилен','Propylene','organic',2,
|
||
'Простейший алкен с двойной связью. CH₂=CH-CH₃. Сырьё для производства полипропилена.',
|
||
[{id:1,s:'C',x:-65,y:0},{id:2,s:'C',x:0,y:0},{id:3,s:'C',x:65,y:0},
|
||
{id:4,s:'H',x:-100,y:-35},{id:5,s:'H',x:-100,y:35},{id:6,s:'H',x:0,y:-50},
|
||
{id:7,s:'H',x:100,y:-35},{id:8,s:'H',x:100,y:5},{id:9,s:'H',x:100,y:45}],
|
||
[{f:1,t:2,o:2},{f:2,t:3,o:1},
|
||
{f:4,t:1,o:1},{f:5,t:1,o:1},{f:6,t:2,o:1},
|
||
{f:7,t:3,o:1},{f:8,t:3,o:1},{f:9,t:3,o:1}],
|
||
['алкен','полимер','пластик']);
|
||
|
||
M('C4H10','Бутан','Butane','organic',2,
|
||
'Четырёхуглеродный алкан. Сжиженный газ в зажигалках и баллонах. CH₃CH₂CH₂CH₃.',
|
||
[{id:1,s:'C',x:-90,y:0},{id:2,s:'C',x:-30,y:0},{id:3,s:'C',x:30,y:0},{id:4,s:'C',x:90,y:0},
|
||
{id:5,s:'H',x:-125,y:-32},{id:6,s:'H',x:-125,y:32},{id:7,s:'H',x:-90,y:-45},
|
||
{id:8,s:'H',x:-30,y:-45},{id:9,s:'H',x:-30,y:45},
|
||
{id:10,s:'H',x:30,y:-45},{id:11,s:'H',x:30,y:45},
|
||
{id:12,s:'H',x:125,y:-32},{id:13,s:'H',x:125,y:32},{id:14,s:'H',x:90,y:-45}],
|
||
[{f:1,t:2,o:1},{f:2,t:3,o:1},{f:3,t:4,o:1},
|
||
{f:5,t:1,o:1},{f:6,t:1,o:1},{f:7,t:1,o:1},
|
||
{f:8,t:2,o:1},{f:9,t:2,o:1},{f:10,t:3,o:1},{f:11,t:3,o:1},
|
||
{f:12,t:4,o:1},{f:13,t:4,o:1},{f:14,t:4,o:1}],
|
||
['алкан','топливо','газ']);
|
||
|
||
M('CHCl3','Хлороформ','Chloroform','organic',2,
|
||
'Исторически использовался как анестетик. Гепатотоксичен. Растворитель. CHCl₃.',
|
||
[{id:1,s:'C',x:0,y:0},{id:2,s:'H',x:0,y:-65},
|
||
{id:3,s:'Cl',x:-60,y:45},{id:4,s:'Cl',x:60,y:45},{id:5,s:'Cl',x:0,y:65}],
|
||
[{f:1,t:2,o:1},{f:1,t:3,o:1},{f:1,t:4,o:1},{f:1,t:5,o:1}],
|
||
['галоген','растворитель','анестетик']);
|
||
|
||
M('CCl4','Тетрахлорметан','Carbon tetrachloride','organic',2,
|
||
'Тетраэдрическая молекула. Применялся как огнетушитель. Токсичен. Разрушает озоновый слой.',
|
||
[{id:1,s:'C',x:0,y:0},{id:2,s:'Cl',x:0,y:-75},{id:3,s:'Cl',x:75,y:0},{id:4,s:'Cl',x:0,y:75},{id:5,s:'Cl',x:-75,y:0}],
|
||
[{f:1,t:2,o:1},{f:1,t:3,o:1},{f:1,t:4,o:1},{f:1,t:5,o:1}],
|
||
['галоген','симметрия','растворитель']);
|
||
|
||
M('CH4N2O','Мочевина','Urea','organic',2,
|
||
'Конечный продукт азотистого обмена. Экскретируется почками. Используется как удобрение. O=C(NH₂)₂.',
|
||
[{id:1,s:'C',x:0,y:0},{id:2,s:'O',x:0,y:-65},{id:3,s:'N',x:-60,y:40},{id:4,s:'N',x:60,y:40},
|
||
{id:5,s:'H',x:-95,y:65},{id:6,s:'H',x:-30,y:75},{id:7,s:'H',x:30,y:75},{id:8,s:'H',x:95,y:65}],
|
||
[{f:1,t:2,o:2},{f:1,t:3,o:1},{f:1,t:4,o:1},
|
||
{f:5,t:3,o:1},{f:6,t:3,o:1},{f:7,t:4,o:1},{f:8,t:4,o:1}],
|
||
['белок','обмен','удобрение','почки']);
|
||
|
||
M('C2H4O2','Метилформиат','Methyl formate','organic',2,
|
||
'Простейший сложный эфир. Применяется как фумигант и растворитель. HCOOCH₃.',
|
||
[{id:1,s:'C',x:-35,y:0},{id:2,s:'O',x:25,y:0},{id:3,s:'C',x:90,y:0},{id:4,s:'O',x:-35,y:-65},
|
||
{id:5,s:'H',x:-70,y:0},{id:6,s:'H',x:125,y:-35},{id:7,s:'H',x:125,y:35},{id:8,s:'H',x:125,y:0}],
|
||
[{f:1,t:2,o:1},{f:2,t:3,o:1},{f:1,t:4,o:2},{f:5,t:1,o:1},
|
||
{f:6,t:3,o:1},{f:7,t:3,o:1},{f:8,t:3,o:1}],
|
||
['эфир','растворитель']);
|
||
|
||
// ── Биомолекулы ──
|
||
M('C3H7NO2','Аланин','Alanine','biomolecule',3,
|
||
'Простейшая хиральная аминокислота. Входит в состав белков. Заменимая АК. CH₃-CH(NH₂)-COOH.',
|
||
[{id:1,s:'C',x:-65,y:0},{id:2,s:'C',x:0,y:0},{id:3,s:'C',x:65,y:0},
|
||
{id:4,s:'N',x:0,y:-65},{id:5,s:'O',x:100,y:-35},{id:6,s:'O',x:100,y:35},
|
||
{id:7,s:'H',x:-100,y:-35},{id:8,s:'H',x:-100,y:35},{id:9,s:'H',x:-100,y:0},
|
||
{id:10,s:'H',x:0,y:35},{id:11,s:'H',x:-30,y:-90},{id:12,s:'H',x:30,y:-90},{id:13,s:'H',x:135,y:35}],
|
||
[{f:1,t:2,o:1},{f:2,t:3,o:1},{f:2,t:4,o:1},{f:3,t:5,o:2},{f:3,t:6,o:1},
|
||
{f:7,t:1,o:1},{f:8,t:1,o:1},{f:9,t:1,o:1},
|
||
{f:10,t:2,o:1},{f:11,t:4,o:1},{f:12,t:4,o:1},{f:13,t:6,o:1}],
|
||
['аминокислота','белок','хиральность']);
|
||
|
||
M('C3H6O3','Молочная кислота','Lactic acid','biomolecule',2,
|
||
'Образуется при анаэробном гликолизе. Причина усталости мышц. CH₃-CH(OH)-COOH.',
|
||
[{id:1,s:'C',x:-65,y:0},{id:2,s:'C',x:0,y:0},{id:3,s:'C',x:65,y:0},
|
||
{id:4,s:'O',x:0,y:-65},{id:5,s:'O',x:100,y:-35},{id:6,s:'O',x:100,y:35},
|
||
{id:7,s:'H',x:-100,y:-35},{id:8,s:'H',x:-100,y:35},{id:9,s:'H',x:-100,y:0},
|
||
{id:10,s:'H',x:0,y:35},{id:11,s:'H',x:-30,y:-90},{id:12,s:'H',x:135,y:35}],
|
||
[{f:1,t:2,o:1},{f:2,t:3,o:1},{f:2,t:4,o:1},{f:3,t:5,o:2},{f:3,t:6,o:1},
|
||
{f:7,t:1,o:1},{f:8,t:1,o:1},{f:9,t:1,o:1},
|
||
{f:10,t:2,o:1},{f:11,t:4,o:1},{f:12,t:6,o:1}],
|
||
['гликолиз','мышцы','анаэробный']);
|
||
|
||
M('C3H4O3','Пировиноградная кислота','Pyruvic acid','biomolecule',3,
|
||
'Ключевой метаболит. Конечный продукт гликолиза. Точка ветвления: ЦТК, брожение, синтез АК. CH₃-CO-COOH.',
|
||
[{id:1,s:'C',x:-65,y:0},{id:2,s:'C',x:0,y:0},{id:3,s:'C',x:65,y:0},
|
||
{id:4,s:'O',x:0,y:-65},{id:5,s:'O',x:100,y:-35},{id:6,s:'O',x:100,y:35},
|
||
{id:7,s:'H',x:-100,y:-35},{id:8,s:'H',x:-100,y:35},{id:9,s:'H',x:-100,y:0},{id:10,s:'H',x:135,y:35}],
|
||
[{f:1,t:2,o:1},{f:2,t:3,o:1},{f:2,t:4,o:2},{f:3,t:5,o:2},{f:3,t:6,o:1},
|
||
{f:7,t:1,o:1},{f:8,t:1,o:1},{f:9,t:1,o:1},{f:10,t:6,o:1}],
|
||
['гликолиз','Кребс','метаболизм','ЦТК']);
|
||
|
||
M('C6H12O6','Глюкоза','Glucose','biomolecule',3,
|
||
'Основной источник энергии клетки. Субстрат гликолиза. Моносахарид. Образуется при фотосинтезе.',
|
||
[],[],['углевод','гликолиз','энергия','фотосинтез']);
|
||
|
||
M('C12H22O11','Сахароза','Sucrose','biomolecule',3,
|
||
'Дисахарид: глюкоза + фруктоза. Обычный столовый сахар. Гидролизуется на моносахариды.',
|
||
[],[],['углевод','дисахарид','сахар']);
|
||
|
||
M('C5H5N5','Аденин','Adenine','biomolecule',3,
|
||
'Пуриновое основание. Входит в состав ДНК, РНК и АТФ. Комплементарен тимину в ДНК.',
|
||
[],[],['нуклеотид','ДНК','АТФ','пурин']);
|
||
|
||
M('C5H6N2O2','Тимин','Thymine','biomolecule',3,
|
||
'Пиримидиновое основание. Только в ДНК (в РНК заменяется урацилом). Пара аденину.',
|
||
[],[],['нуклеотид','ДНК','пиримидин']);
|
||
|
||
M('C4H5N3O','Цитозин','Cytosine','biomolecule',3,
|
||
'Пиримидиновое основание. Входит в ДНК и РНК. Пара гуанину (три водородные связи).',
|
||
[],[],['нуклеотид','ДНК','РНК','пиримидин']);
|
||
|
||
M('C5H5N5O','Гуанин','Guanine','biomolecule',3,
|
||
'Пуриновое основание. Входит в ДНК и РНК. Пара цитозину. Наибольшее содержание в ДНК термофилов.',
|
||
[],[],['нуклеотид','ДНК','РНК','пурин']);
|
||
|
||
M('C4H4N2O2','Урацил','Uracil','biomolecule',3,
|
||
'Пиримидиновое основание. Только в РНК (вместо тимина). Пара аденину.',
|
||
[],[],['нуклеотид','РНК','пиримидин']);
|
||
|
||
M('C5H10O5','Рибоза','Ribose','biomolecule',3,
|
||
'Пентоза. Входит в состав РНК, АТФ, НАД⁺. Структурный компонент нуклеотидов.',
|
||
[],[],['углевод','РНК','нуклеотид','пентоза']);
|
||
|
||
M('C5H10O4','Дезоксирибоза','Deoxyribose','biomolecule',3,
|
||
'Пятиуглеродный сахар. Структурный компонент ДНК. Отличается от рибозы отсутствием одного OH.',
|
||
[],[],['углевод','ДНК','нуклеотид']);
|
||
|
||
M('C16H32O2','Пальмитиновая кислота','Palmitic acid','biomolecule',2,
|
||
'Насыщенная жирная кислота С16. Входит в состав клеточных мембран и пальмового масла.',
|
||
[],[],['жирная кислота','липид','мембрана','насыщенная']);
|
||
|
||
M('C18H34O2','Олеиновая кислота','Oleic acid','biomolecule',2,
|
||
'Мononенасыщенная ω-9 жирная кислота. Основной компонент оливкового масла. Двойная связь Δ9.',
|
||
[],[],['жирная кислота','ненасыщенная','оливковое масло','омега']);
|
||
|
||
M('C3H5NO2','Серин','Serine','biomolecule',3,
|
||
'Полярная аминокислота с гидроксильной группой. Входит в активный центр многих ферментов.',
|
||
[],[],['аминокислота','белок','фермент']);
|
||
|
||
M('C3H7NO3','Треонин','Threonine','biomolecule',3,
|
||
'Незаменимая полярная аминокислота. Содержит гидроксильную группу. Участвует в сигнальных каскадах.',
|
||
[],[],['аминокислота','незаменимая','белок']);
|
||
|
||
M('C10H17N3O6S','Глутатион','Glutathione','biomolecule',3,
|
||
'Трипептид (γ-Glu-Cys-Gly). Важнейший клеточный антиоксидант. Обезвреживает перекисные радикалы.',
|
||
[],[],['антиоксидант','пептид','цистеин']);
|
||
|
||
M('C21H30O5','Кортизол','Cortisol','biomolecule',3,
|
||
'Стероидный гормон стресса. Вырабатывается корой надпочечников. Регулирует обмен веществ.',
|
||
[],[],['гормон','стероид','стресс','надпочечники']);
|
||
|
||
M('C18H21NO3','Морфин','Morphine','biomolecule',3,
|
||
'Алкалоид мака. Сильное обезболивающее. Взаимодействует с опиоидными рецепторами.',
|
||
[],[],['алкалоид','нейромедиатор','рецептор']);
|
||
|
||
console.log('✓ Phase 6: Additional molecules seeded');
|
||
}
|
||
|
||
/* Seed challenges */
|
||
if (!db.prepare('SELECT 1 FROM bio_challenges LIMIT 1').get()) {
|
||
const ins = db.prepare('INSERT INTO bio_challenges (title,description,type,target_formula,hint,xp_reward,difficulty,topic_tag,order_n) VALUES (?,?,?,?,?,?,?,?,?)');
|
||
[
|
||
['Построй воду', 'Соедини 2 атома водорода с кислородом', 'build','H2O','O — в центре, два H по бокам',30,1,'вода',1],
|
||
['Углекислый газ', 'Построй молекулу CO₂ — двойные связи!', 'build','CO2','C в центре, два O с двойными связями',40,1,'фотосинтез',2],
|
||
['Кислород O₂', 'Две O соединены двойной связью', 'build','O2','Двойная связь: перетащи от атома до второго О',30,1,'дыхание',3],
|
||
['Азот N₂', 'Тройная связь между двумя атомами N', 'build','N2','Тройная связь: кликни по линии 3 раза',50,2,'атмосфера',4],
|
||
['Аммиак NH₃', 'Три водорода связаны с азотом', 'build','NH3','N в центре, три H вокруг',50,2,'азот',5],
|
||
['Метан CH₄', 'Четыре водорода вокруг углерода', 'build','CH4','C имеет 4 свободные связи — к каждой прикрепи H',60,2,'алканы',6],
|
||
['Этилен C₂H₄', 'Двойная связь C=C — признак алкенов', 'build','C2H4','Сначала соедини два C двойной связью, затем добавь H',80,3,'алкены',7],
|
||
['Ацетилен C₂H₂', 'Тройная связь C≡C — самая прочная', 'build','C2H2','H-C≡C-H: два H по краям, тройная связь в центре',100,3,'алкины',8],
|
||
].forEach(r => ins.run(...r));
|
||
console.log('✓ Bio challenges seeded');
|
||
}
|
||
|
||
/* Seed reactions */
|
||
if (!db.prepare('SELECT 1 FROM bio_reactions LIMIT 1').get()) {
|
||
const ins = db.prepare('INSERT INTO bio_reactions (equation,name_ru,type,description,reactant_ids,product_ids,conditions,energy_kj,topic_tags) VALUES (?,?,?,?,?,?,?,?,?)');
|
||
const empty = '[]';
|
||
[
|
||
['2H₂ + O₂ → 2H₂O','Синтез воды','synthesis','Экзотермическая реакция. Основа водородной энергетики. ΔH = –483 кДж/моль.',empty,empty,'поджигание',-483.6,'["водород","кислород","вода"]'],
|
||
['CH₄ + 2O₂ → CO₂ + 2H₂O','Горение метана','combustion','Горение природного газа. Выделяет тепло. ΔH = –890 кДж/моль.',empty,empty,'t°',-890.0,'["метан","горение","углерод"]'],
|
||
['N₂ + 3H₂ ⇌ 2NH₃','Синтез аммиака (Хабер)','synthesis','Промышленный синтез. Высокое давление и температура. Обратимая реакция.',empty,empty,'450°C, Fe-кат, 200 атм',-92.4,'["азот","аммиак","промышленность"]'],
|
||
['6CO₂ + 6H₂O → C₆H₁₂O₆ + 6O₂','Фотосинтез','synthesis','Суммарное уравнение фотосинтеза. Световая + тёмная фазы.',empty,empty,'hν (свет)',2808,'["фотосинтез","глюкоза","хлоропласт"]'],
|
||
['C₆H₁₂O₆ + 6O₂ → 6CO₂ + 6H₂O','Клеточное дыхание','decomposition','Полное окисление глюкозы. Выделяется 38 АТФ. ΔH = –2808 кДж/моль.',empty,empty,'ферменты',-2808,'["дыхание","АТФ","митохондрия"]'],
|
||
].forEach(r => ins.run(...r));
|
||
console.log('✓ Bio reactions seeded');
|
||
}
|
||
|
||
/* Fix reaction molecule IDs (seeded with empty arrays) */
|
||
{
|
||
const firstRxn = db.prepare("SELECT reactant_ids FROM bio_reactions WHERE name_ru='Синтез воды'").get();
|
||
if (firstRxn && firstRxn.reactant_ids === '[]') {
|
||
const mid = f => db.prepare('SELECT id FROM bio_molecules WHERE formula=? LIMIT 1').get(f)?.id;
|
||
const upd = db.prepare('UPDATE bio_reactions SET reactant_ids=?, product_ids=? WHERE name_ru=?');
|
||
const rxnIds = [
|
||
{ name: 'Синтез воды', r: [mid('H2'), mid('O2')], p: [mid('H2O')] },
|
||
{ name: 'Горение метана', r: [mid('CH4'), mid('O2')], p: [mid('CO2'), mid('H2O')] },
|
||
{ name: 'Синтез аммиака (Хабер)', r: [mid('N2'), mid('H2')], p: [mid('NH3')] },
|
||
{ name: 'Фотосинтез', r: [mid('CO2'), mid('H2O')], p: [mid('O2')] },
|
||
{ name: 'Клеточное дыхание', r: [mid('O2')], p: [mid('CO2'), mid('H2O')] },
|
||
];
|
||
for (const u of rxnIds) {
|
||
const rIds = u.r.filter(Boolean), pIds = u.p.filter(Boolean);
|
||
if (rIds.length || pIds.length)
|
||
upd.run(JSON.stringify(rIds), JSON.stringify(pIds), u.name);
|
||
}
|
||
console.log('✓ Bio reaction molecule IDs fixed');
|
||
}
|
||
}
|
||
|
||
/* Add data_json column to bio_challenges (idempotent) */
|
||
{
|
||
const chalCols = db.prepare('PRAGMA table_info(bio_challenges)').all().map(c => c.name);
|
||
if (!chalCols.includes('data_json')) {
|
||
db.exec('ALTER TABLE bio_challenges ADD COLUMN data_json TEXT DEFAULT NULL');
|
||
console.log('✓ bio_challenges.data_json column added');
|
||
}
|
||
}
|
||
|
||
/* Seed identify + formula challenges */
|
||
{
|
||
const insChal = db.prepare(`
|
||
INSERT INTO bio_challenges (title,description,type,target_formula,hint,xp_reward,difficulty,topic_tag,order_n,data_json)
|
||
SELECT ?,?,?,?,?,?,?,?,?,? WHERE NOT EXISTS (SELECT 1 FROM bio_challenges WHERE title=?)`);
|
||
|
||
const mid = f => db.prepare('SELECT id FROM bio_molecules WHERE formula=? LIMIT 1').get(f)?.id;
|
||
const mname = f => db.prepare('SELECT name_ru FROM bio_molecules WHERE formula=? LIMIT 1').get(f)?.name_ru;
|
||
|
||
// identify: show molecule structure → pick correct name
|
||
[
|
||
{ title:'Узнай молекулу: вода', f:'H2O', choices:['Вода','Аммиак','Метан','Углекислый газ'], xp:30, d:1, t:'вода', n:9 },
|
||
{ title:'Узнай молекулу: CO₂', f:'CO2', choices:['Углекислый газ','Угарный газ','Кислород','Аммиак'], xp:30, d:1, t:'фотосинтез', n:10 },
|
||
{ title:'Узнай молекулу: аммиак', f:'NH3', choices:['Аммиак','Азот','Вода','Метан'], xp:40, d:2, t:'азот', n:11 },
|
||
{ title:'Узнай молекулу: метан', f:'CH4', choices:['Метан','Этан','Пропан','Этилен'], xp:40, d:2, t:'алканы', n:12 },
|
||
{ title:'Узнай молекулу: этилен', f:'C2H4', choices:['Этилен','Этан','Ацетилен','Метан'], xp:60, d:3, t:'алкены', n:13 },
|
||
].forEach(c => {
|
||
const mol_id = mid(c.f);
|
||
insChal.run(c.title, 'Определи молекулу по её структурной формуле', 'identify', c.f, null, c.xp, c.d, c.t, c.n,
|
||
JSON.stringify({ mol_id, choices: c.choices }), c.title);
|
||
});
|
||
|
||
// formula: shown a molecule name → pick correct formula
|
||
[
|
||
{ title:'Формула воды', name:'воды', f:'H2O', choices:['H2O','H2O2','HO','H3O'], xp:25, d:1, t:'вода', n:14 },
|
||
{ title:'Формула аммиака', name:'аммиака', f:'NH3', choices:['NH3','N2H4','NO2','HNO3'], xp:35, d:2, t:'азот', n:15 },
|
||
{ title:'Формула метана', name:'метана (простейший алкан)', f:'CH4',choices:['CH4','C2H6','C2H4','CH3'],xp:35, d:2, t:'алканы', n:16 },
|
||
{ title:'Формула кислорода', name:'молекулярного кислорода', f:'O2', choices:['O2','O3','CO','CO2'], xp:25, d:1, t:'дыхание',n:17 },
|
||
].forEach(c => {
|
||
const mol_name = mname(c.f) || c.name;
|
||
insChal.run(c.title, `Выбери правильную химическую формулу: ${mol_name}`, 'formula', c.f, null, c.xp, c.d, c.t, c.n,
|
||
JSON.stringify({ choices: c.choices, mol_name }), c.title);
|
||
});
|
||
|
||
console.log('✓ Bio identify/formula challenges seeded');
|
||
}
|
||
|
||
/* ── ПЛАН ФАЗА 1: Расширение контента ─────────────────────────────────── */
|
||
{
|
||
const am = db.prepare(`INSERT INTO bio_molecules (formula,name_ru,name_lat,category,difficulty,description,atoms_json,bonds_json,topic_tags,is_library)
|
||
SELECT ?,?,?,?,?,?,?,?,?,1 WHERE NOT EXISTS (SELECT 1 FROM bio_molecules WHERE name_ru=?)`);
|
||
const M = (f,r,l,cat,d,desc,a,b,t) => am.run(f,r,l,cat,d,desc,JSON.stringify(a),JSON.stringify(b),JSON.stringify(t),r);
|
||
|
||
// === INORGANIC MOLECULES ===
|
||
M('KOH','Гидроксид калия','Potassium hydroxide','inorganic',1,
|
||
'Сильное основание. Используется в промышленности и лабораторной практике.',
|
||
[{id:1,s:'K',x:-80,y:0},{id:2,s:'O',x:0,y:0},{id:3,s:'H',x:80,y:0}],
|
||
[{f:1,t:2,o:1},{f:2,t:3,o:1}],['кислоты-основания']);
|
||
|
||
M('Ca(OH)2','Гидроксид кальция','Calcium hydroxide','inorganic',1,
|
||
'Гашёная известь. Применяется в строительстве и сельском хозяйстве.',
|
||
[{id:1,s:'Ca',x:0,y:0},{id:2,s:'O',x:-80,y:60},{id:3,s:'H',x:-80,y:140},{id:4,s:'O',x:80,y:60},{id:5,s:'H',x:80,y:140}],
|
||
[{f:1,t:2,o:1},{f:2,t:3,o:1},{f:1,t:4,o:1},{f:4,t:5,o:1}],['кислоты-основания']);
|
||
|
||
M('Fe2O3','Оксид железа(III)','Iron(III) oxide','inorganic',2,
|
||
'Ржавчина. Красно-бурый порошок, встречается как минерал гематит.',
|
||
[{id:1,s:'Fe',x:-80,y:0},{id:2,s:'Fe',x:80,y:0},{id:3,s:'O',x:0,y:-80},{id:4,s:'O',x:-120,y:80},{id:5,s:'O',x:120,y:80}],
|
||
[{f:1,t:3,o:1},{f:2,t:3,o:1},{f:1,t:4,o:1},{f:2,t:5,o:1}],['металлы','оксиды']);
|
||
|
||
M('Al2O3','Оксид алюминия','Aluminium oxide','inorganic',2,
|
||
'Корунд, глинозём. Тугоплавкое вещество, основа алюминиевой промышленности.',
|
||
[{id:1,s:'Al',x:-80,y:0},{id:2,s:'Al',x:80,y:0},{id:3,s:'O',x:0,y:-80},{id:4,s:'O',x:-120,y:80},{id:5,s:'O',x:120,y:80}],
|
||
[{f:1,t:3,o:1},{f:2,t:3,o:1},{f:1,t:4,o:1},{f:2,t:5,o:1}],['металлы','оксиды']);
|
||
|
||
M('Na2CO3','Карбонат натрия','Sodium carbonate','inorganic',2,
|
||
'Сода кальцинированная. Применяется в стекольной и химической промышленности.',
|
||
[{id:1,s:'Na',x:-120,y:0},{id:2,s:'C',x:0,y:0},{id:3,s:'O',x:0,y:-85},{id:4,s:'O',x:-75,y:65},{id:5,s:'O',x:75,y:65},{id:6,s:'Na',x:120,y:0}],
|
||
[{f:2,t:3,o:2},{f:2,t:4,o:1},{f:2,t:5,o:1},{f:4,t:1,o:1},{f:5,t:6,o:1}],['соли']);
|
||
|
||
M('NaHCO3','Гидрокарбонат натрия','Sodium bicarbonate','inorganic',2,
|
||
'Питьевая сода. Разлагается при нагревании с выделением CO₂.',
|
||
[{id:1,s:'Na',x:-140,y:0},{id:2,s:'O',x:-50,y:0},{id:3,s:'H',x:-50,y:-80},{id:4,s:'C',x:40,y:0},{id:5,s:'O',x:40,y:-85},{id:6,s:'O',x:120,y:50}],
|
||
[{f:1,t:2,o:1},{f:2,t:3,o:1},{f:2,t:4,o:1},{f:4,t:5,o:2},{f:4,t:6,o:1}],['соли']);
|
||
|
||
M('KMnO4','Перманганат калия','Potassium permanganate','inorganic',2,
|
||
'Марганцовка. Сильный окислитель. Применяется как антисептик.',
|
||
[{id:1,s:'K',x:-130,y:0},{id:2,s:'Mn',x:0,y:0},{id:3,s:'O',x:0,y:-90},{id:4,s:'O',x:90,y:0},{id:5,s:'O',x:0,y:90},{id:6,s:'O',x:-90,y:0}],
|
||
[{f:1,t:6,o:1},{f:2,t:3,o:2},{f:2,t:4,o:2},{f:2,t:5,o:2},{f:2,t:6,o:1}],['окислители']);
|
||
|
||
M('HF','Фторид водорода','Hydrogen fluoride','inorganic',1,
|
||
'Плавиковая кислота в растворе. Используется для травления стекла.',
|
||
[{id:1,s:'H',x:-40,y:0},{id:2,s:'F',x:40,y:0}],
|
||
[{f:1,t:2,o:1}],['галогены']);
|
||
|
||
M('Br2','Бром','Bromine','inorganic',1,
|
||
'Единственный жидкий неметалл при комнатной температуре. Сильный окислитель.',
|
||
[{id:1,s:'Br',x:-50,y:0},{id:2,s:'Br',x:50,y:0}],
|
||
[{f:1,t:2,o:1}],['галогены']);
|
||
|
||
M('AgCl','Хлорид серебра','Silver chloride','inorganic',1,
|
||
'Белый осадок, нерастворимый в воде. Качественная реакция на ионы Cl⁻.',
|
||
[{id:1,s:'Ag',x:-50,y:0},{id:2,s:'Cl',x:50,y:0}],
|
||
[{f:1,t:2,o:1}],['соли','осадки']);
|
||
|
||
M('NH4Cl','Хлорид аммония','Ammonium chloride','inorganic',2,
|
||
'Нашатырь. Применяется в медицине, пайке металлов, как удобрение.',
|
||
[{id:1,s:'N',x:0,y:0},{id:2,s:'H',x:-80,y:0},{id:3,s:'H',x:0,y:-80},{id:4,s:'H',x:80,y:0},{id:5,s:'H',x:0,y:80},{id:6,s:'Cl',x:100,y:100}],
|
||
[{f:1,t:2,o:1},{f:1,t:3,o:1},{f:1,t:4,o:1},{f:1,t:5,o:1}],['соли','азот']);
|
||
|
||
M('BaSO4','Сульфат бария','Barium sulfate','inorganic',2,
|
||
'Нерастворимый белый осадок. Используется в рентгенодиагностике.',
|
||
[{id:1,s:'S',x:0,y:0},{id:2,s:'O',x:0,y:-85},{id:3,s:'O',x:85,y:0},{id:4,s:'O',x:0,y:85},{id:5,s:'O',x:-85,y:0},{id:6,s:'Ba',x:150,y:0}],
|
||
[{f:1,t:2,o:2},{f:1,t:3,o:1},{f:1,t:4,o:1},{f:1,t:5,o:2},{f:3,t:6,o:1}],['соли','осадки']);
|
||
|
||
M('CuSO4','Сульфат меди(II)','Copper(II) sulfate','inorganic',2,
|
||
'Медный купорос в гидратированной форме. Фунгицид, применяется в химическом анализе.',
|
||
[{id:1,s:'S',x:0,y:0},{id:2,s:'O',x:0,y:-85},{id:3,s:'O',x:85,y:0},{id:4,s:'O',x:0,y:85},{id:5,s:'O',x:-85,y:0},{id:6,s:'Cu',x:150,y:0}],
|
||
[{f:1,t:2,o:2},{f:1,t:3,o:1},{f:1,t:4,o:1},{f:1,t:5,o:2},{f:3,t:6,o:1}],['соли','металлы']);
|
||
|
||
M('Na2SO4','Сульфат натрия','Sodium sulfate','inorganic',2,
|
||
'Глауберова соль в гидратированной форме. Применяется в стекольной промышленности.',
|
||
[{id:1,s:'S',x:0,y:0},{id:2,s:'O',x:0,y:-80},{id:3,s:'O',x:80,y:0},{id:4,s:'O',x:0,y:80},{id:5,s:'O',x:-80,y:0},{id:6,s:'Na',x:-160,y:0},{id:7,s:'Na',x:160,y:0}],
|
||
[{f:1,t:2,o:2},{f:1,t:3,o:1},{f:1,t:4,o:1},{f:1,t:5,o:2},{f:5,t:6,o:1},{f:3,t:7,o:1}],['соли']);
|
||
|
||
M('FeCl3','Хлорид железа(III)','Iron(III) chloride','inorganic',2,
|
||
'Применяется как коагулянт при очистке воды, как катализатор.',
|
||
[{id:1,s:'Fe',x:0,y:0},{id:2,s:'Cl',x:-90,y:0},{id:3,s:'Cl',x:45,y:-78},{id:4,s:'Cl',x:45,y:78}],
|
||
[{f:1,t:2,o:1},{f:1,t:3,o:1},{f:1,t:4,o:1}],['металлы','соли']);
|
||
|
||
M('H3PO4','Фосфорная кислота','Phosphoric acid','inorganic',2,
|
||
'Трёхосновная кислота. Применяется в пищевой промышленности (Е338), удобрениях.',
|
||
[{id:1,s:'P',x:0,y:0},{id:2,s:'O',x:0,y:-90},{id:3,s:'O',x:90,y:45},{id:4,s:'O',x:-90,y:45},{id:5,s:'O',x:0,y:90},{id:6,s:'H',x:90,y:120},{id:7,s:'H',x:-90,y:120},{id:8,s:'H',x:80,y:-90}],
|
||
[{f:1,t:2,o:2},{f:1,t:3,o:1},{f:1,t:4,o:1},{f:1,t:5,o:1},{f:3,t:6,o:1},{f:4,t:7,o:1},{f:2,t:8,o:1}],['кислоты']);
|
||
|
||
M('CaCO3','Карбонат кальция','Calcium carbonate','inorganic',1,
|
||
'Мел, известняк, мрамор. Основа раковин и костей многих организмов.',
|
||
[{id:1,s:'C',x:0,y:0},{id:2,s:'O',x:0,y:-80},{id:3,s:'O',x:70,y:55},{id:4,s:'O',x:-70,y:55},{id:5,s:'Ca',x:130,y:55}],
|
||
[{f:1,t:2,o:2},{f:1,t:3,o:1},{f:1,t:4,o:1},{f:3,t:5,o:1}],['соли']);
|
||
|
||
M('K2SO4','Сульфат калия','Potassium sulfate','inorganic',2,
|
||
'Калийное удобрение. Применяется в сельском хозяйстве.',
|
||
[{id:1,s:'S',x:0,y:0},{id:2,s:'O',x:0,y:-80},{id:3,s:'O',x:80,y:0},{id:4,s:'O',x:0,y:80},{id:5,s:'O',x:-80,y:0},{id:6,s:'K',x:-160,y:0},{id:7,s:'K',x:160,y:0}],
|
||
[{f:1,t:2,o:2},{f:1,t:3,o:1},{f:1,t:4,o:1},{f:1,t:5,o:2},{f:5,t:6,o:1},{f:3,t:7,o:1}],['соли','удобрения']);
|
||
|
||
M('HNO2','Азотистая кислота','Nitrous acid','inorganic',2,
|
||
'Неустойчивая слабая кислота. Реагент в реакциях диазотирования.',
|
||
[{id:1,s:'H',x:-80,y:0},{id:2,s:'O',x:0,y:0},{id:3,s:'N',x:80,y:0},{id:4,s:'O',x:160,y:0}],
|
||
[{f:1,t:2,o:1},{f:2,t:3,o:1},{f:3,t:4,o:2}],['кислоты','азот']);
|
||
|
||
M('AlCl3','Хлорид алюминия','Aluminium chloride','inorganic',2,
|
||
'Катализатор реакций Фриделя-Крафтса. Применяется в органическом синтезе.',
|
||
[{id:1,s:'Al',x:0,y:0},{id:2,s:'Cl',x:-90,y:0},{id:3,s:'Cl',x:45,y:-78},{id:4,s:'Cl',x:45,y:78}],
|
||
[{f:1,t:2,o:1},{f:1,t:3,o:1},{f:1,t:4,o:1}],['металлы','соли']);
|
||
|
||
// === ORGANIC ===
|
||
M('C6H6','Бензол','Benzene','organic',2,
|
||
'Простейший арен. Ароматическое кольцо с делокализованными π-электронами.',
|
||
[{id:1,s:'C',x:80,y:0},{id:2,s:'C',x:40,y:-69},{id:3,s:'C',x:-40,y:-69},{id:4,s:'C',x:-80,y:0},{id:5,s:'C',x:-40,y:69},{id:6,s:'C',x:40,y:69}],
|
||
[{f:1,t:2,o:2},{f:2,t:3,o:1},{f:3,t:4,o:2},{f:4,t:5,o:1},{f:5,t:6,o:2},{f:6,t:1,o:1}],['арены','циклические']);
|
||
|
||
M('C7H8','Толуол','Toluene','organic',2,
|
||
'Бензол с метильной группой. Растворитель, сырьё для синтеза красителей.',
|
||
[{id:1,s:'C',x:80,y:0},{id:2,s:'C',x:40,y:-69},{id:3,s:'C',x:-40,y:-69},{id:4,s:'C',x:-80,y:0},{id:5,s:'C',x:-40,y:69},{id:6,s:'C',x:40,y:69},{id:7,s:'C',x:170,y:0}],
|
||
[{f:1,t:2,o:2},{f:2,t:3,o:1},{f:3,t:4,o:2},{f:4,t:5,o:1},{f:5,t:6,o:2},{f:6,t:1,o:1},{f:1,t:7,o:1}],['арены']);
|
||
|
||
M('C6H5OH','Фенол','Phenol','organic',2,
|
||
'Карболовая кислота. Применяется как антисептик и в производстве полимеров.',
|
||
[{id:1,s:'C',x:80,y:0},{id:2,s:'C',x:40,y:-69},{id:3,s:'C',x:-40,y:-69},{id:4,s:'C',x:-80,y:0},{id:5,s:'C',x:-40,y:69},{id:6,s:'C',x:40,y:69},{id:7,s:'O',x:170,y:0},{id:8,s:'H',x:240,y:0}],
|
||
[{f:1,t:2,o:2},{f:2,t:3,o:1},{f:3,t:4,o:2},{f:4,t:5,o:1},{f:5,t:6,o:2},{f:6,t:1,o:1},{f:1,t:7,o:1},{f:7,t:8,o:1}],['фенолы','арены']);
|
||
|
||
M('C6H5NH2','Анилин','Aniline','organic',2,
|
||
'Аминобензол. Исходное вещество для синтеза красителей и лекарств.',
|
||
[{id:1,s:'C',x:80,y:0},{id:2,s:'C',x:40,y:-69},{id:3,s:'C',x:-40,y:-69},{id:4,s:'C',x:-80,y:0},{id:5,s:'C',x:-40,y:69},{id:6,s:'C',x:40,y:69},{id:7,s:'N',x:170,y:0}],
|
||
[{f:1,t:2,o:2},{f:2,t:3,o:1},{f:3,t:4,o:2},{f:4,t:5,o:1},{f:5,t:6,o:2},{f:6,t:1,o:1},{f:1,t:7,o:1}],['амины','арены']);
|
||
|
||
M('C3H8O3','Глицерин','Glycerol','organic',2,
|
||
'Трёхатомный спирт. Компонент жиров, используется в косметике и фармацевтике.',
|
||
[{id:1,s:'C',x:-100,y:0},{id:2,s:'C',x:0,y:0},{id:3,s:'C',x:100,y:0},{id:4,s:'O',x:-100,y:80},{id:5,s:'O',x:0,y:80},{id:6,s:'O',x:100,y:80}],
|
||
[{f:1,t:2,o:1},{f:2,t:3,o:1},{f:1,t:4,o:1},{f:2,t:5,o:1},{f:3,t:6,o:1}],['спирты','липиды']);
|
||
|
||
M('CH2O','Формальдегид','Formaldehyde','organic',1,
|
||
'Простейший альдегид. Консервант, используется в производстве пластмасс.',
|
||
[{id:1,s:'C',x:0,y:0},{id:2,s:'O',x:0,y:-80},{id:3,s:'H',x:-70,y:50},{id:4,s:'H',x:70,y:50}],
|
||
[{f:1,t:2,o:2},{f:1,t:3,o:1},{f:1,t:4,o:1}],['альдегиды']);
|
||
|
||
M('CH3CHO','Уксусный альдегид','Acetaldehyde','organic',1,
|
||
'Этаналь. Промежуточный продукт обмена веществ, образуется при окислении этанола.',
|
||
[{id:1,s:'C',x:-60,y:0},{id:2,s:'C',x:60,y:0},{id:3,s:'O',x:60,y:-85}],
|
||
[{f:1,t:2,o:1},{f:2,t:3,o:2}],['альдегиды']);
|
||
|
||
M('C6H12O6','Глюкоза','Glucose','organic',3,
|
||
'Основной источник энергии клеток. Продукт фотосинтеза, субстрат гликолиза.',
|
||
[{id:1,s:'C',x:80,y:0},{id:2,s:'C',x:40,y:-69},{id:3,s:'C',x:-40,y:-69},{id:4,s:'C',x:-80,y:0},{id:5,s:'C',x:-40,y:69},{id:6,s:'O',x:40,y:69},{id:7,s:'O',x:155,y:0},{id:8,s:'O',x:40,y:-140},{id:9,s:'O',x:-40,y:-140},{id:10,s:'O',x:-155,y:0},{id:11,s:'C',x:-40,y:150},{id:12,s:'O',x:-40,y:230}],
|
||
[{f:1,t:2,o:1},{f:2,t:3,o:1},{f:3,t:4,o:1},{f:4,t:5,o:1},{f:5,t:6,o:1},{f:6,t:1,o:1},{f:1,t:7,o:1},{f:2,t:8,o:1},{f:3,t:9,o:1},{f:4,t:10,o:1},{f:5,t:11,o:1},{f:11,t:12,o:1}],['углеводы','гликолиз']);
|
||
|
||
M('C3H6O3','Молочная кислота','Lactic acid','organic',2,
|
||
'Продукт анаэробного гликолиза. Накапливается в мышцах при физической нагрузке.',
|
||
[{id:1,s:'C',x:-80,y:0},{id:2,s:'O',x:-80,y:80},{id:3,s:'C',x:0,y:0},{id:4,s:'C',x:80,y:0},{id:5,s:'O',x:80,y:-80},{id:6,s:'O',x:160,y:0}],
|
||
[{f:3,t:1,o:1},{f:1,t:2,o:1},{f:3,t:4,o:1},{f:4,t:5,o:2},{f:4,t:6,o:1}],['обмен веществ','органические кислоты']);
|
||
|
||
M('C3H4O3','Пировиноградная кислота','Pyruvic acid','organic',2,
|
||
'Пируват — ключевой метаболит. Связывает гликолиз и цикл Кребса.',
|
||
[{id:1,s:'C',x:-80,y:0},{id:2,s:'O',x:-80,y:-80},{id:3,s:'C',x:0,y:0},{id:4,s:'C',x:80,y:0},{id:5,s:'O',x:80,y:-80},{id:6,s:'O',x:160,y:0}],
|
||
[{f:1,t:2,o:2},{f:1,t:3,o:1},{f:3,t:4,o:1},{f:4,t:5,o:2},{f:4,t:6,o:1}],['гликолиз','обмен веществ']);
|
||
|
||
// === AMINO ACIDS (all 20) ===
|
||
// Standard backbone: N(1,-120,0) Cα(2,0,0) C(3,90,-55) O(4,135,-130) O(5,190,-10)
|
||
// bonds: N-Cα(1-2,1), Cα-C(2-3,1), C=O(3-4,2), C-OH(3-5,1)
|
||
// Side chains from Cα downward
|
||
|
||
M('C3H7NO2','Аланин','Alanine','biomolecule',2,
|
||
'Заменимая аминокислота. Входит в состав большинства белков.',
|
||
[{id:1,s:'N',x:-120,y:0},{id:2,s:'C',x:0,y:0},{id:3,s:'C',x:90,y:-55},{id:4,s:'O',x:135,y:-130},{id:5,s:'O',x:190,y:-10},{id:6,s:'C',x:0,y:90}],
|
||
[{f:1,t:2,o:1},{f:2,t:3,o:1},{f:3,t:4,o:2},{f:3,t:5,o:1},{f:2,t:6,o:1}],['аминокислоты']);
|
||
|
||
M('C5H11NO2','Валин','Valine','biomolecule',2,
|
||
'Незаменимая аминокислота. Необходима для синтеза белков мышечной ткани.',
|
||
[{id:1,s:'N',x:-120,y:0},{id:2,s:'C',x:0,y:0},{id:3,s:'C',x:90,y:-55},{id:4,s:'O',x:135,y:-130},{id:5,s:'O',x:190,y:-10},{id:6,s:'C',x:0,y:90},{id:7,s:'C',x:-60,y:165},{id:8,s:'C',x:60,y:165}],
|
||
[{f:1,t:2,o:1},{f:2,t:3,o:1},{f:3,t:4,o:2},{f:3,t:5,o:1},{f:2,t:6,o:1},{f:6,t:7,o:1},{f:6,t:8,o:1}],['аминокислоты','незаменимые']);
|
||
|
||
M('C6H13NO2','Лейцин','Leucine','biomolecule',2,
|
||
'Незаменимая аминокислота. Стимулирует синтез белка, особенно важна для мышц.',
|
||
[{id:1,s:'N',x:-120,y:0},{id:2,s:'C',x:0,y:0},{id:3,s:'C',x:90,y:-55},{id:4,s:'O',x:135,y:-130},{id:5,s:'O',x:190,y:-10},{id:6,s:'C',x:0,y:90},{id:7,s:'C',x:0,y:170},{id:8,s:'C',x:-65,y:240},{id:9,s:'C',x:65,y:240}],
|
||
[{f:1,t:2,o:1},{f:2,t:3,o:1},{f:3,t:4,o:2},{f:3,t:5,o:1},{f:2,t:6,o:1},{f:6,t:7,o:1},{f:7,t:8,o:1},{f:7,t:9,o:1}],['аминокислоты','незаменимые']);
|
||
|
||
M('C6H13NO2','Изолейцин','Isoleucine','biomolecule',3,
|
||
'Незаменимая аминокислота. Изомер лейцина, важен для синтеза гемоглобина.',
|
||
[{id:1,s:'N',x:-120,y:0},{id:2,s:'C',x:0,y:0},{id:3,s:'C',x:90,y:-55},{id:4,s:'O',x:135,y:-130},{id:5,s:'O',x:190,y:-10},{id:6,s:'C',x:0,y:90},{id:7,s:'C',x:-60,y:160},{id:8,s:'C',x:60,y:160},{id:9,s:'C',x:60,y:235}],
|
||
[{f:1,t:2,o:1},{f:2,t:3,o:1},{f:3,t:4,o:2},{f:3,t:5,o:1},{f:2,t:6,o:1},{f:6,t:7,o:1},{f:6,t:8,o:1},{f:8,t:9,o:1}],['аминокислоты','незаменимые']);
|
||
|
||
M('C5H9NO2','Пролин','Proline','biomolecule',3,
|
||
'Иминокислота. Создаёт жёсткие изгибы в белковой цепи (в коллагене ≈12%).',
|
||
[{id:1,s:'N',x:-80,y:0},{id:2,s:'C',x:0,y:0},{id:3,s:'C',x:80,y:-55},{id:4,s:'O',x:120,y:-130},{id:5,s:'O',x:175,y:-10},{id:6,s:'C',x:-10,y:90},{id:7,s:'C',x:-90,y:140},{id:8,s:'C',x:-150,y:60}],
|
||
[{f:1,t:2,o:1},{f:2,t:3,o:1},{f:3,t:4,o:2},{f:3,t:5,o:1},{f:2,t:6,o:1},{f:6,t:7,o:1},{f:7,t:8,o:1},{f:8,t:1,o:1}],['аминокислоты','иминокислоты']);
|
||
|
||
M('C3H7NO3','Серин','Serine','biomolecule',2,
|
||
'Заменимая аминокислота. Входит в активные центры многих ферментов.',
|
||
[{id:1,s:'N',x:-120,y:0},{id:2,s:'C',x:0,y:0},{id:3,s:'C',x:90,y:-55},{id:4,s:'O',x:135,y:-130},{id:5,s:'O',x:190,y:-10},{id:6,s:'C',x:0,y:90},{id:7,s:'O',x:0,y:170}],
|
||
[{f:1,t:2,o:1},{f:2,t:3,o:1},{f:3,t:4,o:2},{f:3,t:5,o:1},{f:2,t:6,o:1},{f:6,t:7,o:1}],['аминокислоты']);
|
||
|
||
M('C4H9NO3','Треонин','Threonine','biomolecule',2,
|
||
'Незаменимая аминокислота. Участвует в синтезе иммуноглобулинов.',
|
||
[{id:1,s:'N',x:-120,y:0},{id:2,s:'C',x:0,y:0},{id:3,s:'C',x:90,y:-55},{id:4,s:'O',x:135,y:-130},{id:5,s:'O',x:190,y:-10},{id:6,s:'C',x:0,y:90},{id:7,s:'O',x:-65,y:160},{id:8,s:'C',x:65,y:160}],
|
||
[{f:1,t:2,o:1},{f:2,t:3,o:1},{f:3,t:4,o:2},{f:3,t:5,o:1},{f:2,t:6,o:1},{f:6,t:7,o:1},{f:6,t:8,o:1}],['аминокислоты','незаменимые']);
|
||
|
||
M('C3H7NO2S','Цистеин','Cysteine','biomolecule',2,
|
||
'Серосодержащая аминокислота. Образует дисульфидные мостики, стабилизируя белки.',
|
||
[{id:1,s:'N',x:-120,y:0},{id:2,s:'C',x:0,y:0},{id:3,s:'C',x:90,y:-55},{id:4,s:'O',x:135,y:-130},{id:5,s:'O',x:190,y:-10},{id:6,s:'C',x:0,y:90},{id:7,s:'S',x:0,y:175}],
|
||
[{f:1,t:2,o:1},{f:2,t:3,o:1},{f:3,t:4,o:2},{f:3,t:5,o:1},{f:2,t:6,o:1},{f:6,t:7,o:1}],['аминокислоты','серосодержащие']);
|
||
|
||
M('C5H11NO2S','Метионин','Methionine','biomolecule',2,
|
||
'Незаменимая серосодержащая аминокислота. Донор метильных групп (метилирование).',
|
||
[{id:1,s:'N',x:-120,y:0},{id:2,s:'C',x:0,y:0},{id:3,s:'C',x:90,y:-55},{id:4,s:'O',x:135,y:-130},{id:5,s:'O',x:190,y:-10},{id:6,s:'C',x:0,y:90},{id:7,s:'C',x:0,y:165},{id:8,s:'S',x:0,y:245},{id:9,s:'C',x:80,y:300}],
|
||
[{f:1,t:2,o:1},{f:2,t:3,o:1},{f:3,t:4,o:2},{f:3,t:5,o:1},{f:2,t:6,o:1},{f:6,t:7,o:1},{f:7,t:8,o:1},{f:8,t:9,o:1}],['аминокислоты','незаменимые','серосодержащие']);
|
||
|
||
M('C4H7NO4','Аспарагиновая кислота','Aspartic acid','biomolecule',2,
|
||
'Кислая аминокислота. Участвует в цикле мочевины и синтезе пуринов.',
|
||
[{id:1,s:'N',x:-120,y:0},{id:2,s:'C',x:0,y:0},{id:3,s:'C',x:90,y:-55},{id:4,s:'O',x:135,y:-130},{id:5,s:'O',x:190,y:-10},{id:6,s:'C',x:0,y:90},{id:7,s:'C',x:0,y:170},{id:8,s:'O',x:-65,y:240},{id:9,s:'O',x:65,y:240}],
|
||
[{f:1,t:2,o:1},{f:2,t:3,o:1},{f:3,t:4,o:2},{f:3,t:5,o:1},{f:2,t:6,o:1},{f:6,t:7,o:1},{f:7,t:8,o:2},{f:7,t:9,o:1}],['аминокислоты','кислые']);
|
||
|
||
M('C5H9NO4','Глутаминовая кислота','Glutamic acid','biomolecule',2,
|
||
'Кислая аминокислота, нейромедиатор. Предшественник ГАМК и глутамина.',
|
||
[{id:1,s:'N',x:-120,y:0},{id:2,s:'C',x:0,y:0},{id:3,s:'C',x:90,y:-55},{id:4,s:'O',x:135,y:-130},{id:5,s:'O',x:190,y:-10},{id:6,s:'C',x:0,y:90},{id:7,s:'C',x:0,y:165},{id:8,s:'C',x:0,y:240},{id:9,s:'O',x:-65,y:310},{id:10,s:'O',x:65,y:310}],
|
||
[{f:1,t:2,o:1},{f:2,t:3,o:1},{f:3,t:4,o:2},{f:3,t:5,o:1},{f:2,t:6,o:1},{f:6,t:7,o:1},{f:7,t:8,o:1},{f:8,t:9,o:2},{f:8,t:10,o:1}],['аминокислоты','кислые','нейромедиаторы']);
|
||
|
||
M('C4H8N2O3','Аспарагин','Asparagine','biomolecule',2,
|
||
'Заменимая аминокислота. Амид аспарагиновой кислоты. Важен для нервной системы.',
|
||
[{id:1,s:'N',x:-120,y:0},{id:2,s:'C',x:0,y:0},{id:3,s:'C',x:90,y:-55},{id:4,s:'O',x:135,y:-130},{id:5,s:'O',x:190,y:-10},{id:6,s:'C',x:0,y:90},{id:7,s:'C',x:0,y:170},{id:8,s:'O',x:-70,y:240},{id:9,s:'N',x:70,y:240}],
|
||
[{f:1,t:2,o:1},{f:2,t:3,o:1},{f:3,t:4,o:2},{f:3,t:5,o:1},{f:2,t:6,o:1},{f:6,t:7,o:1},{f:7,t:8,o:2},{f:7,t:9,o:1}],['аминокислоты']);
|
||
|
||
M('C5H10N2O3','Глутамин','Glutamine','biomolecule',2,
|
||
'Наиболее распространённая свободная аминокислота в крови. Транспортёр азота.',
|
||
[{id:1,s:'N',x:-120,y:0},{id:2,s:'C',x:0,y:0},{id:3,s:'C',x:90,y:-55},{id:4,s:'O',x:135,y:-130},{id:5,s:'O',x:190,y:-10},{id:6,s:'C',x:0,y:90},{id:7,s:'C',x:0,y:165},{id:8,s:'C',x:0,y:240},{id:9,s:'O',x:-70,y:310},{id:10,s:'N',x:70,y:310}],
|
||
[{f:1,t:2,o:1},{f:2,t:3,o:1},{f:3,t:4,o:2},{f:3,t:5,o:1},{f:2,t:6,o:1},{f:6,t:7,o:1},{f:7,t:8,o:1},{f:8,t:9,o:2},{f:8,t:10,o:1}],['аминокислоты']);
|
||
|
||
M('C6H14N2O2','Лизин','Lysine','biomolecule',2,
|
||
'Незаменимая основная аминокислота. Необходима для роста и восстановления тканей.',
|
||
[{id:1,s:'N',x:-120,y:0},{id:2,s:'C',x:0,y:0},{id:3,s:'C',x:90,y:-55},{id:4,s:'O',x:135,y:-130},{id:5,s:'O',x:190,y:-10},{id:6,s:'C',x:0,y:90},{id:7,s:'C',x:0,y:165},{id:8,s:'C',x:0,y:240},{id:9,s:'C',x:0,y:315},{id:10,s:'N',x:0,y:390}],
|
||
[{f:1,t:2,o:1},{f:2,t:3,o:1},{f:3,t:4,o:2},{f:3,t:5,o:1},{f:2,t:6,o:1},{f:6,t:7,o:1},{f:7,t:8,o:1},{f:8,t:9,o:1},{f:9,t:10,o:1}],['аминокислоты','основные','незаменимые']);
|
||
|
||
M('C6H14N4O2','Аргинин','Arginine','biomolecule',3,
|
||
'Условно незаменимая аминокислота. Предшественник оксида азота NO. Входит в цикл мочевины.',
|
||
[{id:1,s:'N',x:-120,y:0},{id:2,s:'C',x:0,y:0},{id:3,s:'C',x:90,y:-55},{id:4,s:'O',x:135,y:-130},{id:5,s:'O',x:190,y:-10},{id:6,s:'C',x:0,y:90},{id:7,s:'C',x:0,y:165},{id:8,s:'C',x:0,y:240},{id:9,s:'N',x:0,y:315},{id:10,s:'C',x:0,y:390},{id:11,s:'N',x:-65,y:460},{id:12,s:'N',x:65,y:460}],
|
||
[{f:1,t:2,o:1},{f:2,t:3,o:1},{f:3,t:4,o:2},{f:3,t:5,o:1},{f:2,t:6,o:1},{f:6,t:7,o:1},{f:7,t:8,o:1},{f:8,t:9,o:1},{f:9,t:10,o:1},{f:10,t:11,o:2},{f:10,t:12,o:1}],['аминокислоты','основные','незаменимые']);
|
||
|
||
M('C6H9N3O2','Гистидин','Histidine','biomolecule',3,
|
||
'Незаменимая аминокислота с имидазольным кольцом. Предшественник гистамина.',
|
||
[{id:1,s:'N',x:-120,y:0},{id:2,s:'C',x:0,y:0},{id:3,s:'C',x:90,y:-55},{id:4,s:'O',x:135,y:-130},{id:5,s:'O',x:190,y:-10},{id:6,s:'C',x:0,y:90},{id:7,s:'C',x:0,y:165},{id:8,s:'C',x:50,y:230},{id:9,s:'N',x:90,y:310},{id:10,s:'C',x:0,y:360},{id:11,s:'N',x:-90,y:310},{id:12,s:'C',x:-50,y:230}],
|
||
[{f:1,t:2,o:1},{f:2,t:3,o:1},{f:3,t:4,o:2},{f:3,t:5,o:1},{f:2,t:6,o:1},{f:6,t:7,o:1},{f:7,t:8,o:1},{f:7,t:12,o:1},{f:8,t:9,o:2},{f:9,t:10,o:1},{f:10,t:11,o:1},{f:11,t:12,o:2}],['аминокислоты','основные','незаменимые']);
|
||
|
||
M('C9H11NO2','Фенилаланин','Phenylalanine','biomolecule',3,
|
||
'Незаменимая ароматическая аминокислота. Предшественник тирозина, дофамина.',
|
||
[{id:1,s:'N',x:-120,y:0},{id:2,s:'C',x:0,y:0},{id:3,s:'C',x:90,y:-55},{id:4,s:'O',x:135,y:-130},{id:5,s:'O',x:190,y:-10},{id:6,s:'C',x:0,y:90},{id:7,s:'C',x:0,y:165},{id:8,s:'C',x:50,y:225},{id:9,s:'C',x:90,y:305},{id:10,s:'C',x:50,y:385},{id:11,s:'C',x:-50,y:385},{id:12,s:'C',x:-90,y:305},{id:13,s:'C',x:-50,y:225}],
|
||
[{f:1,t:2,o:1},{f:2,t:3,o:1},{f:3,t:4,o:2},{f:3,t:5,o:1},{f:2,t:6,o:1},{f:6,t:7,o:1},{f:7,t:8,o:1},{f:7,t:13,o:1},{f:8,t:9,o:2},{f:9,t:10,o:1},{f:10,t:11,o:2},{f:11,t:12,o:1},{f:12,t:13,o:2}],['аминокислоты','ароматические','незаменимые']);
|
||
|
||
M('C9H11NO3','Тирозин','Tyrosine','biomolecule',3,
|
||
'Заменимая ароматическая аминокислота. Предшественник дофамина, адреналина, тироксина.',
|
||
[{id:1,s:'N',x:-120,y:0},{id:2,s:'C',x:0,y:0},{id:3,s:'C',x:90,y:-55},{id:4,s:'O',x:135,y:-130},{id:5,s:'O',x:190,y:-10},{id:6,s:'C',x:0,y:90},{id:7,s:'C',x:0,y:165},{id:8,s:'C',x:50,y:225},{id:9,s:'C',x:90,y:305},{id:10,s:'C',x:50,y:385},{id:11,s:'C',x:-50,y:385},{id:12,s:'C',x:-90,y:305},{id:13,s:'C',x:-50,y:225},{id:14,s:'O',x:130,y:385}],
|
||
[{f:1,t:2,o:1},{f:2,t:3,o:1},{f:3,t:4,o:2},{f:3,t:5,o:1},{f:2,t:6,o:1},{f:6,t:7,o:1},{f:7,t:8,o:1},{f:7,t:13,o:1},{f:8,t:9,o:2},{f:9,t:10,o:1},{f:10,t:11,o:2},{f:11,t:12,o:1},{f:12,t:13,o:2},{f:10,t:14,o:1}],['аминокислоты','ароматические']);
|
||
|
||
M('C11H12N2O2','Триптофан','Tryptophan','biomolecule',3,
|
||
'Незаменимая аминокислота. Предшественник серотонина и мелатонина.',
|
||
[{id:1,s:'N',x:-120,y:0},{id:2,s:'C',x:0,y:0},{id:3,s:'C',x:90,y:-55},{id:4,s:'O',x:135,y:-130},{id:5,s:'O',x:190,y:-10},{id:6,s:'C',x:0,y:90},{id:7,s:'C',x:0,y:165},{id:8,s:'N',x:-50,y:235},{id:9,s:'C',x:50,y:235},{id:10,s:'C',x:70,y:315},{id:11,s:'C',x:0,y:375},{id:12,s:'C',x:-70,y:315}],
|
||
[{f:1,t:2,o:1},{f:2,t:3,o:1},{f:3,t:4,o:2},{f:3,t:5,o:1},{f:2,t:6,o:1},{f:6,t:7,o:1},{f:7,t:8,o:1},{f:7,t:9,o:2},{f:8,t:12,o:1},{f:9,t:10,o:1},{f:10,t:11,o:2},{f:11,t:12,o:1}],['аминокислоты','ароматические','незаменимые']);
|
||
|
||
// Additional biomolecules
|
||
M('C21H28N7O14P2','АТФ','ATP','biomolecule',3,
|
||
'Аденозинтрифосфат — универсальный носитель энергии в клетке. «Энергетическая валюта».',
|
||
[],[],['энергетический обмен','нуклеотиды']);
|
||
|
||
M('C10H14N5O7P','АМФ','AMP','biomolecule',3,
|
||
'Аденозинмонофосфат. Образуется при гидролизе АТФ или АДФ.',
|
||
[],[],['нуклеотиды','энергетический обмен']);
|
||
|
||
M('C10H15N5O10P2','АДФ','ADP','biomolecule',3,
|
||
'Аденозиндифосфат. Промежуточный продукт между АТФ и АМФ.',
|
||
[],[],['энергетический обмен','нуклеотиды']);
|
||
|
||
M('C5H5N5','Аденин','Adenine','biomolecule',3,
|
||
'Пуриновое азотистое основание. Входит в состав ДНК, РНК, АТФ.',
|
||
[],[],['нуклеотиды','ДНК','РНК']);
|
||
|
||
M('C4H5N3O','Цитозин','Cytosine','biomolecule',3,
|
||
'Пиримидиновое азотистое основание. Входит в состав ДНК и РНК.',
|
||
[],[],['нуклеотиды','ДНК','РНК']);
|
||
|
||
M('C5H5N5O','Гуанин','Guanine','biomolecule',3,
|
||
'Пуриновое азотистое основание. Входит в состав ДНК и РНК.',
|
||
[],[],['нуклеотиды','ДНК','РНК']);
|
||
|
||
M('C5H6N2O2','Тимин','Thymine','biomolecule',3,
|
||
'Пиримидиновое основание, входящее только в ДНК. Комплементарно аденину.',
|
||
[],[],['нуклеотиды','ДНК']);
|
||
|
||
M('C4H4N2O2','Урацил','Uracil','biomolecule',3,
|
||
'Пиримидиновое основание РНК. Заменяет тимин. Комплементарно аденину.',
|
||
[],[],['нуклеотиды','РНК']);
|
||
|
||
M('C16H32O2','Пальмитиновая кислота','Palmitic acid','biomolecule',2,
|
||
'Насыщенная жирная кислота C16. Основной компонент животных жиров и пальмового масла.',
|
||
[{id:1,s:'C',x:0,y:0},{id:2,s:'O',x:0,y:-80},{id:3,s:'O',x:80,y:0},{id:4,s:'C',x:-80,y:0},{id:5,s:'C',x:-160,y:0},{id:6,s:'C',x:-240,y:0}],
|
||
[{f:1,t:2,o:2},{f:1,t:3,o:1},{f:1,t:4,o:1},{f:4,t:5,o:1},{f:5,t:6,o:1}],['жирные кислоты','липиды']);
|
||
|
||
M('C18H34O2','Олеиновая кислота','Oleic acid','biomolecule',3,
|
||
'Ненасыщенная жирная кислота C18:1. Основной компонент оливкового масла.',
|
||
[{id:1,s:'C',x:0,y:0},{id:2,s:'O',x:0,y:-80},{id:3,s:'O',x:80,y:0},{id:4,s:'C',x:-80,y:0},{id:5,s:'C',x:-160,y:0},{id:6,s:'C',x:-225,y:-45},{id:7,s:'C',x:-290,y:0}],
|
||
[{f:1,t:2,o:2},{f:1,t:3,o:1},{f:1,t:4,o:1},{f:4,t:5,o:1},{f:5,t:6,o:2},{f:6,t:7,o:1}],['жирные кислоты','ненасыщенные','липиды']);
|
||
|
||
console.log('✓ Plan Phase 1: Extended molecules seeded');
|
||
}
|
||
|
||
/* ── Plan Phase 1: Extended reactions ── */
|
||
{
|
||
const insR = db.prepare(`INSERT INTO bio_reactions (equation,name_ru,type,description,reactant_ids,product_ids,conditions,energy_kj,topic_tags)
|
||
SELECT ?,?,?,?,?,?,?,?,? WHERE NOT EXISTS (SELECT 1 FROM bio_reactions WHERE name_ru=?)`);
|
||
const mid = f => db.prepare('SELECT id FROM bio_molecules WHERE formula=? LIMIT 1').get(f)?.id;
|
||
const midN = n => db.prepare('SELECT id FROM bio_molecules WHERE name_ru=? LIMIT 1').get(n)?.id;
|
||
const R = (eq,name,type,desc,reacts,prods,cond,energy,tags) => {
|
||
const ri = JSON.stringify(reacts.map(f => mid(f) || midN(f)).filter(Boolean));
|
||
const pi = JSON.stringify(prods.map(f => mid(f) || midN(f)).filter(Boolean));
|
||
insR.run(eq,name,type,desc,ri,pi,cond,energy,JSON.stringify(tags),name);
|
||
};
|
||
|
||
R('2H2O → 2H2 + O2','Гидролиз воды','decomposition',
|
||
'Электролиз воды — разложение на водород и кислород',
|
||
['H2O'],['H2','O2'],'электролиз, 1500°C',285.8,['вода']);
|
||
|
||
R('Ca + 2H2O → Ca(OH)2 + H2↑','Взаимодействие кальция с водой','exchange',
|
||
'Реакция кальция с водой с образованием гашёной извести',
|
||
['H2O'],['Ca(OH)2','H2'],null,-635,['металлы']);
|
||
|
||
R('HCl + NaOH → NaCl + H2O','Нейтрализация HCl и NaOH','acid_base',
|
||
'Реакция нейтрализации сильной кислоты и сильного основания',
|
||
['HCl','NaOH'],['NaCl','H2O'],null,-57.3,['кислоты-основания']);
|
||
|
||
R('C2H5OH + 3O2 → 2CO2 + 3H2O','Горение этанола','combustion',
|
||
'Горение этилового спирта с выделением углекислого газа и воды',
|
||
['C2H5OH','O2'],['CO2','H2O'],null,-1366,['горение','спирты']);
|
||
|
||
R('C3H8 + 5O2 → 3CO2 + 4H2O','Горение пропана','combustion',
|
||
'Горение пропана — реакция сгорания в бытовых горелках',
|
||
['C3H8','O2'],['CO2','H2O'],null,-2220,['горение','алканы']);
|
||
|
||
R('2C6H6 + 15O2 → 12CO2 + 6H2O','Горение бензола','combustion',
|
||
'Горение бензола с образованием углекислого газа и воды',
|
||
['C6H6','O2'],['CO2','H2O'],null,-3267,['горение','арены']);
|
||
|
||
R('C12H22O11 + H2O → 2C6H12O6','Гидролиз сахарозы','hydrolysis',
|
||
'Инвертный сахар — расщепление сахарозы до глюкозы и фруктозы',
|
||
['C12H22O11','H2O'],['C6H12O6'],'H⁺, фермент',16.5,['углеводы']);
|
||
|
||
R('C6H12O6 → 2C3H6O3','Гликолиз (суммарная реакция)','exchange',
|
||
'Анаэробное расщепление глюкозы до молочной кислоты',
|
||
['C6H12O6'],['C3H6O3'],'ферменты',-219,['гликолиз','обмен веществ']);
|
||
|
||
R('АДФ + H3PO4 → АТФ + H2O','Синтез АТФ','synthesis',
|
||
'Фосфорилирование АДФ с образованием АТФ',
|
||
['C10H15N5O10P2','H3PO4'],['C21H28N7O14P2','H2O'],'АТФ-синтаза',-30.5,['энергетический обмен']);
|
||
|
||
R('АТФ + H2O → АДФ + H3PO4','Гидролиз АТФ','hydrolysis',
|
||
'Высвобождение энергии из АТФ путём гидролиза',
|
||
['C21H28N7O14P2','H2O'],['C10H15N5O10P2','H3PO4'],'АТФаза',-30.5,['энергетический обмен']);
|
||
|
||
R('C2H5OH → C2H4 + H2O','Дегидратация спирта','decomposition',
|
||
'Получение этилена из этанола путём отщепления воды',
|
||
['C2H5OH'],['C2H4','H2O'],'H₂SO₄, 170°C',null,['спирты','алкены']);
|
||
|
||
R('3CO2 + 3H2O → C3H6O3 + 3O2','Восстановление CO2 (цикл Кальвина)','synthesis',
|
||
'Цикл Кальвина — темновая фаза фотосинтеза',
|
||
['CO2','H2O'],['C3H6O3'],'свет, хлоропласты',480,['фотосинтез']);
|
||
|
||
R('2H2O2 → 2H2O + O2','Разложение пероксида водорода','decomposition',
|
||
'Каталитическое разложение пероксида водорода каталазой',
|
||
['H2O2'],['H2O','O2'],'каталаза',-98,['дыхание']);
|
||
|
||
R('C3H4O3 → CH3CHO + CO2','Декарбоксилирование пировиноградной кислоты','decomposition',
|
||
'Пируват → ацетальдегид + CO₂. Первый шаг спиртового брожения.',
|
||
['C3H4O3'],['CH3CHO','CO2'],'ПВК-декарбоксилаза',null,['гликолиз']);
|
||
|
||
R('C3H4O3 + H2O → C3H6O3','Образование молочной кислоты','synthesis',
|
||
'Молочнокислое брожение — восстановление пирувата до лактата',
|
||
['C3H4O3','H2O'],['C3H6O3'],'лактатдегидрогеназа',null,['брожение','гликолиз']);
|
||
|
||
R('2SO2 + O2 → 2SO3','Окисление серы до SO3','redox',
|
||
'Контактный метод получения триоксида серы',
|
||
['SO2','O2'],['SO3'],'V₂O₅, 450-500°C',-98,['неорганика']);
|
||
|
||
R('SO3 + H2O → H2SO4','Получение H2SO4','synthesis',
|
||
'Серная кислота из триоксида серы',
|
||
['SO3','H2O'],['H2SO4'],null,-130,['неорганика']);
|
||
|
||
R('2H2O → H3O+ + OH-','Диссоциация воды','decomposition',
|
||
'Аутопротолиз воды — равновесная диссоциация',
|
||
['H2O'],['H2O'],null,55.8,['вода','pH']);
|
||
|
||
R('(Крахмал) + H2O → C6H12O6','Гидролиз крахмала','hydrolysis',
|
||
'Расщепление полисахарида до глюкозы при пищеварении',
|
||
['H2O'],['C6H12O6'],'амилаза',null,['углеводы','пищеварение']);
|
||
|
||
R('C6H12O6 + 6O2 → 6CO2 + 6H2O','Клеточное дыхание','redox',
|
||
'Полное окисление глюкозы в митохондриях с синтезом АТФ',
|
||
['C6H12O6','O2'],['CO2','H2O'],'митохондрии',-2808,['дыхание','обмен веществ']);
|
||
|
||
R('2C2H5NO2 → C4H8N2O3 + H2O','Образование пептидной связи','synthesis',
|
||
'Конденсация двух молекул глицина с образованием дипептида и воды',
|
||
['C2H5NO2'],['C4H8N2O3','H2O'],'рибосома',17,['белки','синтез белка']);
|
||
|
||
R('(Белок) + H2O → аминокислоты','Гидролиз белка','hydrolysis',
|
||
'Расщепление пептидных связей белка до аминокислот',
|
||
['H2O'],['C2H5NO2'],'протеазы',null,['белки','пищеварение']);
|
||
|
||
R('C16H32O2 + 23O2 → 16CO2 + 16H2O','Β-окисление пальмитиновой кислоты','redox',
|
||
'Полное окисление пальмитиновой кислоты в митохондриях',
|
||
['C16H32O2','O2'],['CO2','H2O'],'митохондрии, β-оксидаза',-9790,['липиды','энергетический обмен']);
|
||
|
||
console.log('✓ Plan Phase 1: Extended reactions seeded');
|
||
}
|
||
|
||
/* ── Plan Phase 1: Extended challenges ── */
|
||
{
|
||
const insChal2 = db.prepare(`INSERT INTO bio_challenges (title,description,type,target_formula,hint,xp_reward,difficulty,topic_tag,order_n,data_json)
|
||
SELECT ?,?,?,?,?,?,?,?,?,? WHERE NOT EXISTS (SELECT 1 FROM bio_challenges WHERE title=?)`);
|
||
const mid2 = f => db.prepare('SELECT id FROM bio_molecules WHERE formula=? LIMIT 1').get(f)?.id;
|
||
const mname2 = f => db.prepare('SELECT name_ru FROM bio_molecules WHERE formula=? LIMIT 1').get(f)?.name_ru;
|
||
|
||
// BUILD challenges (10 new)
|
||
[
|
||
{ title:'Построй: HF', target:'HF', hint:'Водород + Фтор', xp:35, d:1, t:'галогены', n:19 },
|
||
{ title:'Построй: Br2', target:'Br2', hint:'Два атома брома', xp:35, d:1, t:'галогены', n:20 },
|
||
{ title:'Построй: NaOH', target:'NaOH', hint:'Натрий-кислород-водород', xp:40, d:1, t:'кислоты-основания',n:21 },
|
||
{ title:'Построй: NH4+', target:'NH4Cl', hint:'Азот + 4 водорода (катион аммония)', xp:40, d:1, t:'азот', n:22 },
|
||
{ title:'Построй: бензол C6H6', target:'C6H6', hint:'Шесть углеродов в кольце с чередующимися двойными связями', xp:80, d:3, t:'арены', n:23 },
|
||
{ title:'Построй: уксусный альдегид',target:'CH3CHO', hint:'Этаналь: CH₃-CHO', xp:50, d:2, t:'альдегиды', n:24 },
|
||
{ title:'Построй: глицерин', target:'C3H8O3', hint:'Трёхатомный спирт — C₃H₅(OH)₃', xp:60, d:2, t:'спирты', n:25 },
|
||
{ title:'Построй: формальдегид CH2O',target:'CH2O', hint:'Простейший альдегид — метаналь', xp:35, d:1, t:'альдегиды', n:26 },
|
||
{ title:'Построй: CaCO3', target:'CaCO3', hint:'Кальций + карбонат-ион', xp:45, d:2, t:'соли', n:27 },
|
||
{ title:'Построй: KOH', target:'KOH', hint:'Калий + гидроксил', xp:35, d:1, t:'кислоты-основания',n:28 },
|
||
].forEach(c => {
|
||
const mol_id = mid2(c.target);
|
||
insChal2.run(c.title, `Построй молекулу: ${mname2(c.target) || c.target}`, 'build', c.target, c.hint, c.xp, c.d, c.t, c.n,
|
||
JSON.stringify({ mol_id }), c.title);
|
||
});
|
||
|
||
// IDENTIFY challenges (15 new)
|
||
[
|
||
{ title:'Определи: этанол', f:'C2H5OH', choices:['Этанол','Этаналь','Метанол','Пропанол'], xp:40, d:2, t:'спирты', n:29 },
|
||
{ title:'Определи: этилен', f:'C2H4', choices:['Этилен','Этан','Ацетилен','Метан'], xp:40, d:2, t:'алкены', n:30 },
|
||
{ title:'Определи: уксусная кислота', f:'CH3COOH', choices:['Уксусная кислота','Молочная кислота','Пировиноградная к-та','Щавелевая к-та'], xp:50, d:2, t:'органические кислоты', n:31 },
|
||
{ title:'Определи: серная кислота', f:'H2SO4', choices:['Серная кислота','Соляная кислота','Азотная кислота','Фосфорная кислота'], xp:40, d:1, t:'кислоты', n:32 },
|
||
{ title:'Определи: NaOH', f:'NaOH', choices:['Гидроксид натрия','Карбонат натрия','Хлорид натрия','Гидрокарбонат натрия'], xp:30, d:1, t:'кислоты-основания', n:33 },
|
||
{ title:'Определи: глюкоза', f:'C6H12O6', choices:['Глюкоза','Фруктоза','Сахароза','Рибоза'], xp:60, d:2, t:'углеводы', n:34 },
|
||
{ title:'Определи: молочная кислота', f:'C3H6O3', choices:['Молочная кислота','Уксусная кислота','Глицерин','Пировиноградная к-та'], xp:55, d:2, t:'обмен веществ', n:35 },
|
||
{ title:'Определи: аланин', f:'C3H7NO2', choices:['Аланин','Глицин','Серин','Валин'], xp:50, d:2, t:'аминокислоты', n:36 },
|
||
{ title:'Определи: серин', f:'C3H7NO3', choices:['Серин','Аланин','Цистеин','Треонин'], xp:60, d:3, t:'аминокислоты', n:37 },
|
||
{ title:'Определи: бензол', f:'C6H6', choices:['Бензол','Гексан','Циклогексан','Толуол'], xp:60, d:2, t:'арены', n:38 },
|
||
{ title:'Определи: глицерин', f:'C3H8O3', choices:['Глицерин','Этанол','Пропанол','Сорбит'], xp:55, d:2, t:'липиды', n:39 },
|
||
{ title:'Определи: азотная кислота', f:'HNO3', choices:['Азотная кислота','Азотистая кислота','Серная кислота','Соляная кислота'], xp:35, d:1, t:'кислоты', n:40 },
|
||
{ title:'Определи: H2O2', f:'H2O2', choices:['Пероксид водорода','Вода','Озон','Гидроксид'], xp:35, d:1, t:'вода', n:41 },
|
||
{ title:'Определи: пировиноградная к-та', f:'C3H4O3', choices:['Пировиноградная кислота','Молочная кислота','Уксусная кислота','Янтарная кислота'], xp:65, d:3, t:'гликолиз', n:42 },
|
||
{ title:'Определи: пропан', f:'C3H8', choices:['Пропан','Этан','Метан','Бутан'], xp:35, d:1, t:'алканы', n:43 },
|
||
].forEach(c => {
|
||
const mol_id = mid2(c.f);
|
||
insChal2.run(c.title, `Определи молекулу по структурной формуле`, 'identify', c.f, null, c.xp, c.d, c.t, c.n,
|
||
JSON.stringify({ mol_id, choices: c.choices }), c.title);
|
||
});
|
||
|
||
// FORMULA challenges (15 new)
|
||
[
|
||
{ title:'Формула бензола', mol_name:'бензола', f:'C6H6', choices:['C6H6','C6H12','C6H5OH','C7H8'], xp:55, d:2, t:'арены', n:44 },
|
||
{ title:'Формула серной кислоты', mol_name:'серной кислоты', f:'H2SO4', choices:['H2SO4','H2SO3','H2S','HSO4'], xp:35, d:1, t:'кислоты', n:45 },
|
||
{ title:'Формула этилена', mol_name:'этилена', f:'C2H4', choices:['C2H4','C2H6','C2H2','CH4'], xp:40, d:2, t:'алкены', n:46 },
|
||
{ title:'Формула этанола', mol_name:'этанола', f:'C2H5OH', choices:['C2H5OH','CH3OH','C3H7OH','C2H4O'], xp:40, d:2, t:'спирты', n:47 },
|
||
{ title:'Формула пероксида водорода', mol_name:'пероксида водорода', f:'H2O2', choices:['H2O2','H2O','HO','HO2'], xp:35, d:1, t:'вода', n:48 },
|
||
{ title:'Формула уксусной кислоты', mol_name:'уксусной кислоты', f:'CH3COOH', choices:['CH3COOH','C2H5OH','HCOOH','C3H6O2'], xp:45, d:2, t:'органические кислоты',n:49 },
|
||
{ title:'Формула глюкозы', mol_name:'глюкозы', f:'C6H12O6', choices:['C6H12O6','C5H10O5','C12H22O11','C6H10O5'],xp:55, d:2, t:'углеводы', n:50 },
|
||
{ title:'Формула аланина', mol_name:'аланина', f:'C3H7NO2', choices:['C3H7NO2','C2H5NO2','C4H9NO2','C3H7NO3'], xp:50, d:2, t:'аминокислоты', n:51 },
|
||
{ title:'Формула азотной кислоты', mol_name:'азотной кислоты', f:'HNO3', choices:['HNO3','HNO2','N2O','NO2'], xp:35, d:1, t:'кислоты', n:52 },
|
||
{ title:'Формула NH3: аммиак', mol_name:'аммиака', f:'NH3', choices:['NH3','N2H4','NO','NO2'], xp:25, d:1, t:'азот', n:53 },
|
||
{ title:'Формула фосфорной кислоты', mol_name:'фосфорной кислоты', f:'H3PO4', choices:['H3PO4','H2PO4','HPO3','H3PO3'], xp:50, d:2, t:'кислоты', n:54 },
|
||
{ title:'Формула сероводорода', mol_name:'сероводорода', f:'H2S', choices:['H2S','HS','H2SO4','SO2'], xp:35, d:1, t:'неорганика', n:55 },
|
||
{ title:'Формула молочной кислоты', mol_name:'молочной кислоты', f:'C3H6O3', choices:['C3H6O3','C3H4O3','C2H4O2','C4H8O4'], xp:55, d:2, t:'обмен веществ', n:56 },
|
||
{ title:'Формула пропана', mol_name:'пропана', f:'C3H8', choices:['C3H8','C3H6','C2H6','C4H10'], xp:35, d:1, t:'алканы', n:57 },
|
||
{ title:'Формула пировиноградной кислоты', mol_name:'пировиноградной кислоты', f:'C3H4O3', choices:['C3H4O3','C3H6O3','C2H4O2','C4H6O4'], xp:65, d:3, t:'гликолиз', n:58 },
|
||
].forEach(c => {
|
||
const mol_name = mname2(c.f) || c.mol_name;
|
||
insChal2.run(c.title, `Выбери правильную химическую формулу: ${mol_name}`, 'formula', c.f, null, c.xp, c.d, c.t, c.n,
|
||
JSON.stringify({ choices: c.choices, mol_name }), c.title);
|
||
});
|
||
|
||
console.log('✓ Plan Phase 1: Extended challenges seeded');
|
||
}
|
||
|
||
/* ── users.role: expand CHECK to include 'free_student' ─────────────────── */
|
||
try {
|
||
const uInfo = db.prepare("PRAGMA table_info(users)").all();
|
||
// Detect if CHECK already allows free_student by attempting a dry-run (rollback)
|
||
const needsMigration = (() => {
|
||
try {
|
||
db.exec('SAVEPOINT chk_role_test');
|
||
db.exec("INSERT INTO users (email,password_hash,name,role) VALUES ('__chk__@x','x','x','free_student')");
|
||
db.exec('ROLLBACK TO chk_role_test');
|
||
db.exec('RELEASE chk_role_test');
|
||
return false; // already works
|
||
} catch {
|
||
try { db.exec('ROLLBACK TO chk_role_test'); db.exec('RELEASE chk_role_test'); } catch {}
|
||
return true;
|
||
}
|
||
})();
|
||
|
||
if (needsMigration) {
|
||
db.exec('PRAGMA foreign_keys = OFF');
|
||
db.exec(`
|
||
CREATE TABLE users_v2 (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
email TEXT UNIQUE NOT NULL,
|
||
password_hash TEXT NOT NULL,
|
||
name TEXT NOT NULL,
|
||
role TEXT NOT NULL DEFAULT 'student'
|
||
CHECK (role IN ('student','teacher','admin','free_student')),
|
||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||
last_login TEXT,
|
||
xp INTEGER NOT NULL DEFAULT 0,
|
||
level INTEGER NOT NULL DEFAULT 1,
|
||
streak_current INTEGER NOT NULL DEFAULT 0,
|
||
streak_best INTEGER NOT NULL DEFAULT 0,
|
||
streak_date TEXT,
|
||
goal_tier TEXT DEFAULT 'medium',
|
||
avatar_frame TEXT DEFAULT 'default',
|
||
coins INTEGER NOT NULL DEFAULT 0,
|
||
active_title TEXT DEFAULT NULL,
|
||
active_theme TEXT DEFAULT NULL,
|
||
active_effect TEXT DEFAULT NULL,
|
||
token_version INTEGER NOT NULL DEFAULT 0,
|
||
lab_experiments INTEGER NOT NULL DEFAULT 0,
|
||
lab_reactions INTEGER NOT NULL DEFAULT 0,
|
||
is_banned INTEGER NOT NULL DEFAULT 0
|
||
)
|
||
`);
|
||
// Copy only columns that exist in old table
|
||
const oldCols = uInfo.map(c => c.name);
|
||
const newCols = [
|
||
'id','email','password_hash','name','role','created_at','last_login',
|
||
'xp','level','streak_current','streak_best','streak_date',
|
||
'goal_tier','avatar_frame','coins',
|
||
'active_title','active_theme','active_effect',
|
||
'token_version','lab_experiments','lab_reactions','is_banned',
|
||
].filter(c => oldCols.includes(c));
|
||
db.exec(`INSERT INTO users_v2 (${newCols.join(',')}) SELECT ${newCols.join(',')} FROM users`);
|
||
db.exec('DROP TABLE users');
|
||
db.exec('ALTER TABLE users_v2 RENAME TO users');
|
||
db.exec('PRAGMA foreign_keys = ON');
|
||
console.log('✓ users.role CHECK updated to include free_student');
|
||
} else {
|
||
console.log('✓ users.role already supports free_student');
|
||
}
|
||
} catch (e) {
|
||
try { db.exec('PRAGMA foreign_keys = ON'); } catch {}
|
||
console.warn('users.role migration skipped:', e.message);
|
||
}
|
||
|
||
/* ── Phase 8.1: Add Z-coordinates to key molecules for 3D mode ─────────── */
|
||
{
|
||
const getByFormula = db.prepare(
|
||
"SELECT id, formula, atoms_json FROM bio_molecules WHERE formula = ? ORDER BY id LIMIT 1"
|
||
);
|
||
const updateAtoms = db.prepare("UPDATE bio_molecules SET atoms_json = ? WHERE id = ?");
|
||
|
||
function pj(s) { try { return JSON.parse(s); } catch { return []; } }
|
||
|
||
// H2O: O at z=0, H1 (left) at z=+30, H2 (right) at z=-30 — angular geometry
|
||
const h2o = getByFormula.get('H2O');
|
||
if (h2o) {
|
||
const a = pj(h2o.atoms_json);
|
||
if (!a.some(x => x.z !== undefined)) {
|
||
let hIdx = 0;
|
||
const updated = a.map(x =>
|
||
x.s === 'O' ? { ...x, z: 0 } :
|
||
x.s === 'H' ? { ...x, z: hIdx++ === 0 ? 30 : -30 } : x
|
||
);
|
||
updateAtoms.run(JSON.stringify(updated), h2o.id);
|
||
}
|
||
}
|
||
|
||
// NH3: N at z=+20 (apex, pyramidal), H atoms at z=-15 (base)
|
||
const nh3 = getByFormula.get('NH3');
|
||
if (nh3) {
|
||
const a = pj(nh3.atoms_json);
|
||
if (!a.some(x => x.z !== undefined)) {
|
||
const updated = a.map(x =>
|
||
x.s === 'N' ? { ...x, z: 20 } :
|
||
x.s === 'H' ? { ...x, z: -15 } : x
|
||
);
|
||
updateAtoms.run(JSON.stringify(updated), nh3.id);
|
||
}
|
||
}
|
||
|
||
// CH4: C at z=0, tetrahedral H — alternate +35/-35
|
||
// 2D layout: H1 top, H2 right, H3 left, H4 bottom → z: +35,-35,+35,-35
|
||
const ch4 = getByFormula.get('CH4');
|
||
if (ch4) {
|
||
const a = pj(ch4.atoms_json);
|
||
if (!a.some(x => x.z !== undefined)) {
|
||
let hIdx = 0;
|
||
const hZ = [35, -35, 35, -35];
|
||
const updated = a.map(x =>
|
||
x.s === 'C' ? { ...x, z: 0 } :
|
||
x.s === 'H' ? { ...x, z: hZ[hIdx++] ?? 0 } : x
|
||
);
|
||
updateAtoms.run(JSON.stringify(updated), ch4.id);
|
||
}
|
||
}
|
||
|
||
// C6H6 benzene: planar — all z=0 (explicit)
|
||
const c6h6 = getByFormula.get('C6H6');
|
||
if (c6h6) {
|
||
const a = pj(c6h6.atoms_json);
|
||
if (!a.some(x => x.z !== undefined)) {
|
||
updateAtoms.run(JSON.stringify(a.map(x => ({ ...x, z: 0 }))), c6h6.id);
|
||
}
|
||
}
|
||
|
||
// Amino acids: N (amino group) at z=+30, O atoms at z=-20, C/H at z=0
|
||
const aminoAcids = db.prepare(
|
||
"SELECT id, atoms_json FROM bio_molecules WHERE category = 'amino_acid'"
|
||
).all();
|
||
for (const mol of aminoAcids) {
|
||
const a = pj(mol.atoms_json);
|
||
if (a.some(x => x.z !== undefined)) continue;
|
||
let nIdx = 0;
|
||
const updated = a.map(x =>
|
||
x.s === 'N' ? { ...x, z: nIdx++ === 0 ? 30 : 10 } :
|
||
x.s === 'O' ? { ...x, z: -20 } :
|
||
{ ...x, z: 0 }
|
||
);
|
||
updateAtoms.run(JSON.stringify(updated), mol.id);
|
||
}
|
||
|
||
console.log('✓ Phase 8.1: Z-coordinates added to key molecules');
|
||
}
|
||
|
||
/* ── Phase 8.2: Add 2D atom coordinates to molecules with empty atoms_json ── */
|
||
{
|
||
const getEmpty = db.prepare("SELECT id, formula, name_ru FROM bio_molecules WHERE atoms_json='[]' OR atoms_json IS NULL");
|
||
const setAtoms = db.prepare("UPDATE bio_molecules SET atoms_json=?, bonds_json=? WHERE id=?");
|
||
|
||
// Purine ring scaffold (hexagon fused with pentagon sharing C4-C5 edge)
|
||
// Hexagon center ~(0,0), R=56; pentagon below, all heavy-atom ring positions
|
||
const PURINE_RING = {
|
||
atoms: [
|
||
{id:1,s:'N',x:56, y:0}, // N3
|
||
{id:2,s:'C',x:28, y:50}, // C4 (junction)
|
||
{id:3,s:'C',x:-28, y:50}, // C5 (junction)
|
||
{id:4,s:'C',x:-56, y:0}, // C6
|
||
{id:5,s:'N',x:-28, y:-50}, // N1
|
||
{id:6,s:'C',x:28, y:-50}, // C2
|
||
{id:7,s:'N',x:45, y:103}, // N9
|
||
{id:8,s:'C',x:0, y:136}, // C8
|
||
{id:9,s:'N',x:-45, y:103}, // N7
|
||
],
|
||
bonds: [
|
||
{f:1,t:2,o:2},{f:2,t:3,o:1},{f:3,t:4,o:2},{f:4,t:5,o:1},{f:5,t:6,o:2},{f:6,t:1,o:1}, // 6-ring
|
||
{f:2,t:7,o:1},{f:7,t:8,o:2},{f:8,t:9,o:1},{f:9,t:3,o:2}, // 5-ring
|
||
],
|
||
};
|
||
|
||
// Pyrimidine ring scaffold (6-membered flat-bottom hexagon, R=50)
|
||
const PYRIM_RING = {
|
||
atoms: [
|
||
{id:1,s:'C',x:0, y:-50}, // C2 (top)
|
||
{id:2,s:'N',x:43, y:-25}, // N3
|
||
{id:3,s:'C',x:43, y:25}, // C4
|
||
{id:4,s:'C',x:0, y:50}, // C5
|
||
{id:5,s:'C',x:-43, y:25}, // C6
|
||
{id:6,s:'N',x:-43, y:-25}, // N1
|
||
],
|
||
bonds: [
|
||
{f:6,t:1,o:1},{f:1,t:2,o:1},{f:2,t:3,o:2},{f:3,t:4,o:1},{f:4,t:5,o:2},{f:5,t:6,o:1},
|
||
],
|
||
};
|
||
|
||
const STRUCTURES = {
|
||
// ── Nucleobases ──────────────────────────────────────────────────
|
||
'Аденин': {
|
||
// Purine: C6 has -NH2
|
||
atoms: [
|
||
...PURINE_RING.atoms,
|
||
{id:10,s:'N',x:-100,y:0}, // exo NH2
|
||
],
|
||
bonds: [
|
||
...PURINE_RING.bonds,
|
||
{f:4,t:10,o:1},
|
||
],
|
||
},
|
||
'Гуанин': {
|
||
// Purine: C6 has =O (keto), C2 has -NH2, N1 has H
|
||
atoms: [
|
||
...PURINE_RING.atoms,
|
||
{id:10,s:'O',x:-100,y:0}, // C6=O
|
||
{id:11,s:'N',x:28, y:-95}, // C2-NH2
|
||
],
|
||
bonds: [
|
||
...PURINE_RING.bonds,
|
||
{f:4,t:10,o:2}, // C6=O
|
||
{f:6,t:11,o:1}, // C2-NH2
|
||
],
|
||
},
|
||
'Цитозин': {
|
||
// Pyrimidine: C2=O, C4-NH2
|
||
atoms: [
|
||
...PYRIM_RING.atoms,
|
||
{id:7,s:'O',x:0, y:-95}, // C2=O
|
||
{id:8,s:'N',x:88, y:42}, // C4-NH2
|
||
],
|
||
bonds: [
|
||
...PYRIM_RING.bonds,
|
||
{f:1,t:7,o:2},
|
||
{f:3,t:8,o:1},
|
||
],
|
||
},
|
||
'Тимин': {
|
||
// Pyrimidine: C2=O, C4=O, C5-CH3
|
||
atoms: [
|
||
...PYRIM_RING.atoms,
|
||
{id:7,s:'O',x:0, y:-95}, // C2=O
|
||
{id:8,s:'O',x:88, y:42}, // C4=O
|
||
{id:9,s:'C',x:0, y:95}, // C5-CH3
|
||
],
|
||
bonds: [
|
||
...PYRIM_RING.bonds,
|
||
{f:1,t:7,o:2},
|
||
{f:3,t:8,o:2},
|
||
{f:4,t:9,o:1},
|
||
],
|
||
},
|
||
'Урацил': {
|
||
// Pyrimidine: C2=O, C4=O
|
||
atoms: [
|
||
...PYRIM_RING.atoms,
|
||
{id:7,s:'O',x:0, y:-95},
|
||
{id:8,s:'O',x:88, y:42},
|
||
],
|
||
bonds: [
|
||
...PYRIM_RING.bonds,
|
||
{f:1,t:7,o:2},
|
||
{f:3,t:8,o:2},
|
||
],
|
||
},
|
||
|
||
// ── Sugars ───────────────────────────────────────────────────────
|
||
'Глюкоза': {
|
||
// Pyranose ring (Haworth): O at top, C1-C5 clockwise, C6 branch on C5
|
||
atoms: [
|
||
{id:1,s:'O',x:0, y:-48}, // ring O
|
||
{id:2,s:'C',x:46, y:-24}, // C1 (anomeric)
|
||
{id:3,s:'C',x:46, y:24}, // C2
|
||
{id:4,s:'C',x:0, y:48}, // C3
|
||
{id:5,s:'C',x:-46,y:24}, // C4
|
||
{id:6,s:'C',x:-46,y:-24}, // C5
|
||
{id:7,s:'C',x:-85,y:-52}, // C6 (CH2OH)
|
||
{id:8,s:'O',x:85, y:-48}, // C1-OH
|
||
],
|
||
bonds: [
|
||
{f:1,t:2,o:1},{f:2,t:3,o:1},{f:3,t:4,o:1},{f:4,t:5,o:1},{f:5,t:6,o:1},{f:6,t:1,o:1},
|
||
{f:6,t:7,o:1},{f:2,t:8,o:1},
|
||
],
|
||
},
|
||
'Рибоза': {
|
||
// Furanose ring: O at top, C1-C4 clockwise, C5 branch
|
||
atoms: [
|
||
{id:1,s:'O',x:0, y:-42}, // ring O
|
||
{id:2,s:'C',x:40, y:-13}, // C1
|
||
{id:3,s:'C',x:25, y:35}, // C2
|
||
{id:4,s:'C',x:-25,y:35}, // C3
|
||
{id:5,s:'C',x:-40,y:-13}, // C4
|
||
{id:6,s:'C',x:-70,y:-48}, // C5 (CH2OH)
|
||
{id:7,s:'O',x:72, y:-42}, // C1-OH
|
||
],
|
||
bonds: [
|
||
{f:1,t:2,o:1},{f:2,t:3,o:1},{f:3,t:4,o:1},{f:4,t:5,o:1},{f:5,t:1,o:1},
|
||
{f:5,t:6,o:1},{f:2,t:7,o:1},
|
||
],
|
||
},
|
||
'Дезоксирибоза': {
|
||
// Same as ribose but C2 lacks OH
|
||
atoms: [
|
||
{id:1,s:'O',x:0, y:-42},
|
||
{id:2,s:'C',x:40, y:-13}, // C1
|
||
{id:3,s:'C',x:25, y:35}, // C2 (no OH — deoxygenated)
|
||
{id:4,s:'C',x:-25,y:35}, // C3
|
||
{id:5,s:'C',x:-40,y:-13}, // C4
|
||
{id:6,s:'C',x:-70,y:-48}, // C5
|
||
{id:7,s:'O',x:72, y:-42}, // C1-OH
|
||
],
|
||
bonds: [
|
||
{f:1,t:2,o:1},{f:2,t:3,o:1},{f:3,t:4,o:1},{f:4,t:5,o:1},{f:5,t:1,o:1},
|
||
{f:5,t:6,o:1},{f:2,t:7,o:1},
|
||
],
|
||
},
|
||
'Сахароза': {
|
||
// Two linked rings (simplified): glucose + fructose, joined via O bridge
|
||
atoms: [
|
||
// Glucose ring (left)
|
||
{id:1, s:'O',x:-60, y:-42}, // ring O
|
||
{id:2, s:'C',x:-20, y:-62},
|
||
{id:3, s:'C',x:10, y:-28},
|
||
{id:4, s:'C',x:-5, y:18},
|
||
{id:5, s:'C',x:-55, y:20},
|
||
{id:6, s:'C',x:-80, y:-20},
|
||
// Bridge O
|
||
{id:7, s:'O',x:50, y:-28}, // glycosidic O
|
||
// Fructose ring (right, furanose)
|
||
{id:8, s:'O',x:82, y:20}, // ring O
|
||
{id:9, s:'C',x:115,y:-5},
|
||
{id:10,s:'C',x:115,y:40},
|
||
{id:11,s:'C',x:75, y:55},
|
||
{id:12,s:'C',x:55, y:20},
|
||
],
|
||
bonds: [
|
||
{f:1,t:2,o:1},{f:2,t:3,o:1},{f:3,t:4,o:1},{f:4,t:5,o:1},{f:5,t:6,o:1},{f:6,t:1,o:1},
|
||
{f:3,t:7,o:1},
|
||
{f:7,t:12,o:1},
|
||
{f:8,t:9,o:1},{f:9,t:10,o:1},{f:10,t:11,o:1},{f:11,t:12,o:1},{f:12,t:8,o:1},
|
||
],
|
||
},
|
||
|
||
// ── Lipids ───────────────────────────────────────────────────────
|
||
'Олеиновая кислота': {
|
||
// C18 chain with cis double bond at C9-C10; show carboxyl + kink + chain
|
||
atoms: [
|
||
{id:1,s:'O',x:-85,y:0}, {id:2,s:'C',x:-55,y:0}, {id:3,s:'O',x:-55,y:-42},
|
||
{id:4,s:'C',x:-20,y:20}, {id:5,s:'C',x:15, y:0},
|
||
{id:6,s:'C',x:50, y:20}, {id:7,s:'C',x:85, y:0},
|
||
// cis double bond kink
|
||
{id:8,s:'C',x:115,y:10}, {id:9,s:'C',x:150,y:-10},
|
||
{id:10,s:'C',x:180,y:10},{id:11,s:'C',x:210,y:-10},
|
||
{id:12,s:'C',x:240,y:10},
|
||
],
|
||
bonds: [
|
||
{f:1,t:2,o:2},{f:2,t:3,o:1}, // COOH
|
||
{f:2,t:4,o:1},{f:4,t:5,o:1},{f:5,t:6,o:1},{f:6,t:7,o:1},
|
||
{f:7,t:8,o:1},{f:8,t:9,o:2}, // C=C double bond
|
||
{f:9,t:10,o:1},{f:10,t:11,o:1},{f:11,t:12,o:1},
|
||
],
|
||
},
|
||
'Пальмитиновая кислота': {
|
||
// C16 saturated chain; show carboxyl + zigzag
|
||
atoms: [
|
||
{id:1,s:'O',x:-85,y:0}, {id:2,s:'C',x:-55,y:0}, {id:3,s:'O',x:-55,y:-42},
|
||
{id:4,s:'C',x:-20,y:20}, {id:5,s:'C',x:15, y:0},
|
||
{id:6,s:'C',x:50, y:20}, {id:7,s:'C',x:85, y:0},
|
||
{id:8,s:'C',x:120,y:20}, {id:9,s:'C',x:155,y:0},
|
||
{id:10,s:'C',x:190,y:20},{id:11,s:'C',x:225,y:0},
|
||
],
|
||
bonds: [
|
||
{f:1,t:2,o:2},{f:2,t:3,o:1},
|
||
{f:2,t:4,o:1},{f:4,t:5,o:1},{f:5,t:6,o:1},{f:6,t:7,o:1},
|
||
{f:7,t:8,o:1},{f:8,t:9,o:1},{f:9,t:10,o:1},{f:10,t:11,o:1},
|
||
],
|
||
},
|
||
|
||
// ── Nucleotides ──────────────────────────────────────────────────
|
||
'АТФ': {
|
||
// Adenine + ribose + triphosphate (simplified)
|
||
atoms: [
|
||
// Adenine ring (purine, scaled down a bit)
|
||
{id:1,s:'N',x:42, y:0}, {id:2,s:'C',x:21, y:37}, {id:3,s:'C',x:-21,y:37},
|
||
{id:4,s:'C',x:-42,y:0}, {id:5,s:'N',x:-21,y:-37}, {id:6,s:'C',x:21, y:-37},
|
||
{id:7,s:'N',x:34, y:77}, {id:8,s:'C',x:0, y:100}, {id:9,s:'N',x:-34,y:77},
|
||
{id:10,s:'N',x:-75,y:0}, // NH2
|
||
// Ribose
|
||
{id:11,s:'O',x:21, y:140},
|
||
{id:12,s:'C',x:55, y:155},{id:13,s:'C',x:65,y:195},
|
||
{id:14,s:'C',x:25,y:215},{id:15,s:'C',x:-5,y:185},
|
||
// Triphosphate tail
|
||
{id:16,s:'O',x:-25,y:215},
|
||
{id:17,s:'P',x:-60,y:220},{id:18,s:'O',x:-75,y:180},{id:19,s:'O',x:-90,y:250},
|
||
{id:20,s:'P',x:-115,y:218},{id:21,s:'O',x:-115,y:175},
|
||
{id:22,s:'P',x:-155,y:218},{id:23,s:'O',x:-155,y:175},
|
||
],
|
||
bonds: [
|
||
// purine 6-ring
|
||
{f:1,t:2,o:2},{f:2,t:3,o:1},{f:3,t:4,o:2},{f:4,t:5,o:1},{f:5,t:6,o:2},{f:6,t:1,o:1},
|
||
// purine 5-ring
|
||
{f:2,t:7,o:1},{f:7,t:8,o:2},{f:8,t:9,o:1},{f:9,t:3,o:2},
|
||
// NH2
|
||
{f:4,t:10,o:1},
|
||
// glycosidic bond N9-ribose
|
||
{f:8,t:11,o:1},
|
||
// ribose ring
|
||
{f:11,t:12,o:1},{f:12,t:13,o:1},{f:13,t:14,o:1},{f:14,t:15,o:1},{f:15,t:11,o:1},
|
||
// 5'-O to phosphate
|
||
{f:15,t:16,o:1},
|
||
// triphosphate chain
|
||
{f:16,t:17,o:1},{f:17,t:18,o:2},{f:17,t:19,o:1},
|
||
{f:19,t:20,o:1},{f:20,t:21,o:2},
|
||
{f:20,t:22,o:1},{f:22,t:23,o:2},
|
||
],
|
||
},
|
||
'АДФ': {
|
||
// Adenine + ribose + diphosphate (ATP minus one P)
|
||
atoms: [
|
||
{id:1,s:'N',x:42, y:0}, {id:2,s:'C',x:21, y:37}, {id:3,s:'C',x:-21,y:37},
|
||
{id:4,s:'C',x:-42,y:0}, {id:5,s:'N',x:-21,y:-37}, {id:6,s:'C',x:21, y:-37},
|
||
{id:7,s:'N',x:34, y:77}, {id:8,s:'C',x:0, y:100}, {id:9,s:'N',x:-34,y:77},
|
||
{id:10,s:'N',x:-75,y:0},
|
||
{id:11,s:'O',x:21, y:140},
|
||
{id:12,s:'C',x:55, y:155},{id:13,s:'C',x:65,y:195},
|
||
{id:14,s:'C',x:25,y:215},{id:15,s:'C',x:-5,y:185},
|
||
{id:16,s:'O',x:-25,y:215},
|
||
{id:17,s:'P',x:-60,y:220},{id:18,s:'O',x:-75,y:180},{id:19,s:'O',x:-90,y:250},
|
||
{id:20,s:'P',x:-115,y:218},{id:21,s:'O',x:-115,y:175},
|
||
],
|
||
bonds: [
|
||
{f:1,t:2,o:2},{f:2,t:3,o:1},{f:3,t:4,o:2},{f:4,t:5,o:1},{f:5,t:6,o:2},{f:6,t:1,o:1},
|
||
{f:2,t:7,o:1},{f:7,t:8,o:2},{f:8,t:9,o:1},{f:9,t:3,o:2},
|
||
{f:4,t:10,o:1},
|
||
{f:8,t:11,o:1},
|
||
{f:11,t:12,o:1},{f:12,t:13,o:1},{f:13,t:14,o:1},{f:14,t:15,o:1},{f:15,t:11,o:1},
|
||
{f:15,t:16,o:1},
|
||
{f:16,t:17,o:1},{f:17,t:18,o:2},{f:17,t:19,o:1},
|
||
{f:19,t:20,o:1},{f:20,t:21,o:2},
|
||
],
|
||
},
|
||
'АМФ': {
|
||
// Adenine + ribose + monophosphate
|
||
atoms: [
|
||
{id:1,s:'N',x:42, y:0}, {id:2,s:'C',x:21, y:37}, {id:3,s:'C',x:-21,y:37},
|
||
{id:4,s:'C',x:-42,y:0}, {id:5,s:'N',x:-21,y:-37}, {id:6,s:'C',x:21, y:-37},
|
||
{id:7,s:'N',x:34, y:77}, {id:8,s:'C',x:0, y:100}, {id:9,s:'N',x:-34,y:77},
|
||
{id:10,s:'N',x:-75,y:0},
|
||
{id:11,s:'O',x:21, y:140},
|
||
{id:12,s:'C',x:55, y:155},{id:13,s:'C',x:65,y:195},
|
||
{id:14,s:'C',x:25,y:215},{id:15,s:'C',x:-5,y:185},
|
||
{id:16,s:'O',x:-25,y:215},
|
||
{id:17,s:'P',x:-60,y:220},{id:18,s:'O',x:-75,y:180},{id:19,s:'O',x:-90,y:250},
|
||
],
|
||
bonds: [
|
||
{f:1,t:2,o:2},{f:2,t:3,o:1},{f:3,t:4,o:2},{f:4,t:5,o:1},{f:5,t:6,o:2},{f:6,t:1,o:1},
|
||
{f:2,t:7,o:1},{f:7,t:8,o:2},{f:8,t:9,o:1},{f:9,t:3,o:2},
|
||
{f:4,t:10,o:1},
|
||
{f:8,t:11,o:1},
|
||
{f:11,t:12,o:1},{f:12,t:13,o:1},{f:13,t:14,o:1},{f:14,t:15,o:1},{f:15,t:11,o:1},
|
||
{f:15,t:16,o:1},
|
||
{f:16,t:17,o:1},{f:17,t:18,o:2},{f:17,t:19,o:1},
|
||
],
|
||
},
|
||
|
||
// ── Small amino acids ────────────────────────────────────────────
|
||
'Серин': {
|
||
// HO-CH2-CH(NH2)-COOH → serine
|
||
atoms: [
|
||
{id:1,s:'O',x:-55,y:0}, {id:2,s:'C',x:-25,y:0}, {id:3,s:'O',x:-25,y:-45}, // COOH
|
||
{id:4,s:'C',x:10, y:20}, // Cα (NH2)
|
||
{id:5,s:'N',x:10, y:65}, // NH2
|
||
{id:6,s:'C',x:45, y:0}, // Cβ (CH2OH)
|
||
{id:7,s:'O',x:80, y:20}, // OH
|
||
],
|
||
bonds: [
|
||
{f:1,t:2,o:2},{f:2,t:3,o:1},{f:2,t:4,o:1},
|
||
{f:4,t:5,o:1},{f:4,t:6,o:1},{f:6,t:7,o:1},
|
||
],
|
||
},
|
||
'Треонин': {
|
||
// CH3-CH(OH)-CH(NH2)-COOH
|
||
atoms: [
|
||
{id:1,s:'O',x:-55,y:0}, {id:2,s:'C',x:-25,y:0}, {id:3,s:'O',x:-25,y:-45},
|
||
{id:4,s:'C',x:10, y:20},
|
||
{id:5,s:'N',x:10, y:65},
|
||
{id:6,s:'C',x:45, y:0},
|
||
{id:7,s:'O',x:80, y:20},
|
||
{id:8,s:'C',x:45, y:-45}, // methyl
|
||
],
|
||
bonds: [
|
||
{f:1,t:2,o:2},{f:2,t:3,o:1},{f:2,t:4,o:1},
|
||
{f:4,t:5,o:1},{f:4,t:6,o:1},{f:6,t:7,o:1},{f:6,t:8,o:1},
|
||
],
|
||
},
|
||
|
||
// ── Other ────────────────────────────────────────────────────────
|
||
'Глутатион': {
|
||
// γ-Glu-Cys-Gly tripeptide (simplified backbone)
|
||
atoms: [
|
||
{id:1,s:'N',x:-90,y:0},{id:2,s:'C',x:-55,y:0},{id:3,s:'C',x:-30,y:-35},{id:4,s:'O',x:-30,y:-75},
|
||
{id:5,s:'C',x:-20,y:30},{id:6,s:'C',x:10, y:0},{id:7,s:'O',x:10, y:-45},
|
||
{id:8,s:'N',x:40, y:20},{id:9,s:'C',x:70, y:0},{id:10,s:'S',x:110,y:20},
|
||
{id:11,s:'C',x:70, y:-45},{id:12,s:'O',x:70,y:-90},
|
||
],
|
||
bonds: [
|
||
{f:1,t:2,o:1},{f:2,t:3,o:1},{f:3,t:4,o:2},{f:2,t:5,o:1},{f:5,t:6,o:1},
|
||
{f:6,t:7,o:2},{f:6,t:8,o:1},{f:8,t:9,o:1},{f:9,t:10,o:1},{f:9,t:11,o:1},{f:11,t:12,o:2},
|
||
],
|
||
},
|
||
'Кортизол': {
|
||
// Steroid (4 fused rings, simplified with 4-ring skeleton)
|
||
atoms: [
|
||
// Ring A (left 6-ring)
|
||
{id:1,s:'C',x:-80,y:10},{id:2,s:'C',x:-55,y:-30},{id:3,s:'C',x:-15,y:-30},
|
||
{id:4,s:'C',x:-5, y:10},{id:5,s:'C',x:-30,y:45}, {id:6,s:'C',x:-65,y:45},
|
||
// Ring B (middle 6-ring, shares C4-C5 with A)
|
||
{id:7,s:'C',x:35, y:-30},{id:8,s:'C',x:55, y:10},{id:9,s:'C',x:30, y:45},
|
||
// Ring C (right 6-ring, shares C8-C9 with B)
|
||
{id:10,s:'C',x:95, y:-30},{id:11,s:'C',x:105,y:10},{id:12,s:'C',x:80, y:45},
|
||
// Ring D (5-ring, right)
|
||
{id:13,s:'C',x:130,y:20},{id:14,s:'C',x:125,y:60},
|
||
// Functional groups
|
||
{id:15,s:'O',x:-15,y:-68},{id:16,s:'O',x:130,y:-15},
|
||
],
|
||
bonds: [
|
||
// Ring A
|
||
{f:1,t:2,o:1},{f:2,t:3,o:2},{f:3,t:4,o:1},{f:4,t:5,o:1},{f:5,t:6,o:1},{f:6,t:1,o:1},
|
||
// Ring B (C4-C8, C5-C9)
|
||
{f:4,t:7,o:1},{f:7,t:10,o:1},{f:10,t:11,o:1},{f:11,t:8,o:1},{f:8,t:9,o:1},{f:9,t:5,o:1},
|
||
// Ring C (C8-C11, C9-C12)
|
||
{f:8,t:13,o:1},{f:13,t:14,o:1},{f:14,t:12,o:1},{f:12,t:9,o:1},
|
||
// Ring D
|
||
{f:11,t:13,o:1},
|
||
// C3=O and C11-OH
|
||
{f:3,t:15,o:2},{f:10,t:16,o:1},
|
||
],
|
||
},
|
||
'Морфин': {
|
||
// Complex alkaloid (simplified pentacyclic skeleton)
|
||
atoms: [
|
||
{id:1,s:'C',x:-30,y:-55},{id:2,s:'C',x:15, y:-55},{id:3,s:'C',x:42, y:-15},
|
||
{id:4,s:'C',x:25, y:30}, {id:5,s:'C',x:-20,y:30}, {id:6,s:'C',x:-48,y:-10},
|
||
{id:7,s:'O',x:-85,y:-10},{id:8,s:'C',x:65, y:20},{id:9,s:'N',x:50, y:65},
|
||
{id:10,s:'C',x:-5, y:75},{id:11,s:'C',x:-42,y:55},{id:12,s:'O',x:95, y:0},
|
||
{id:13,s:'C',x:50, y:-45},
|
||
],
|
||
bonds: [
|
||
{f:1,t:2,o:2},{f:2,t:3,o:1},{f:3,t:4,o:2},{f:4,t:5,o:1},{f:5,t:6,o:2},{f:6,t:1,o:1},
|
||
{f:6,t:7,o:1},{f:3,t:8,o:1},{f:8,t:9,o:1},{f:9,t:10,o:1},{f:10,t:11,o:1},{f:11,t:5,o:1},
|
||
{f:8,t:12,o:1},{f:2,t:13,o:1},{f:13,t:8,o:1},
|
||
],
|
||
},
|
||
};
|
||
|
||
const emptyMols = getEmpty.all();
|
||
let fixed = 0;
|
||
for (const mol of emptyMols) {
|
||
const s = STRUCTURES[mol.name_ru];
|
||
if (!s) continue;
|
||
setAtoms.run(JSON.stringify(s.atoms), JSON.stringify(s.bonds), mol.id);
|
||
fixed++;
|
||
}
|
||
console.log(`✓ Phase 8.2: 2D structures added to ${fixed} molecules`);
|
||
}
|
||
|
||
/* ── Fix role_permissions CHECK to include free_student ────────────────── */
|
||
try {
|
||
const rpNeedsFix = (() => {
|
||
try {
|
||
db.exec('SAVEPOINT chk_rp_test');
|
||
db.exec("INSERT INTO role_permissions (role,permission,enabled) VALUES ('free_student','test',0)");
|
||
db.exec('ROLLBACK TO chk_rp_test');
|
||
db.exec('RELEASE chk_rp_test');
|
||
return false;
|
||
} catch {
|
||
try { db.exec('ROLLBACK TO chk_rp_test'); db.exec('RELEASE chk_rp_test'); } catch {}
|
||
return true;
|
||
}
|
||
})();
|
||
if (rpNeedsFix) {
|
||
db.exec('PRAGMA foreign_keys = OFF');
|
||
db.exec(`CREATE TABLE role_permissions_v2 (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
role TEXT NOT NULL CHECK (role IN ('teacher', 'student', 'free_student')),
|
||
permission TEXT NOT NULL,
|
||
enabled INTEGER NOT NULL DEFAULT 0,
|
||
UNIQUE (role, permission)
|
||
)`);
|
||
db.exec('INSERT INTO role_permissions_v2 (id,role,permission,enabled) SELECT id,role,permission,enabled FROM role_permissions');
|
||
db.exec('DROP TABLE role_permissions');
|
||
db.exec('ALTER TABLE role_permissions_v2 RENAME TO role_permissions');
|
||
db.exec('PRAGMA foreign_keys = ON');
|
||
console.log('✓ role_permissions CHECK updated to include free_student');
|
||
}
|
||
} catch (e) {
|
||
try { db.exec('PRAGMA foreign_keys = ON'); } catch {}
|
||
console.warn('role_permissions migration skipped:', e.message);
|
||
}
|
||
|
||
/* ── Parent links (parent dashboard access) ────────────────────────────── */
|
||
db.exec(`
|
||
CREATE TABLE IF NOT EXISTS parent_links (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
student_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||
token TEXT UNIQUE NOT NULL,
|
||
label TEXT NOT NULL DEFAULT '',
|
||
is_active INTEGER NOT NULL DEFAULT 1,
|
||
last_used TEXT,
|
||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||
expires_at TEXT
|
||
)
|
||
`);
|
||
db.exec('CREATE INDEX IF NOT EXISTS idx_parent_links_student ON parent_links(student_id)');
|
||
db.exec('CREATE INDEX IF NOT EXISTS idx_parent_links_token ON parent_links(token)');
|
||
|
||
db.exec(`
|
||
CREATE TABLE IF NOT EXISTS parent_notifications (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
parent_link_id INTEGER NOT NULL REFERENCES parent_links(id) ON DELETE CASCADE,
|
||
type TEXT NOT NULL DEFAULT 'info',
|
||
message TEXT NOT NULL,
|
||
is_read INTEGER NOT NULL DEFAULT 0,
|
||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||
)
|
||
`);
|
||
db.exec('CREATE INDEX IF NOT EXISTS idx_parent_notif_link ON parent_notifications(parent_link_id, is_read)');
|
||
/* Composite index for parent notification queries ordered by date */
|
||
db.exec('CREATE INDEX IF NOT EXISTS idx_parent_notif_link_date ON parent_notifications(parent_link_id, created_at DESC)');
|
||
/* Composite index for parent dashboard session queries */
|
||
db.exec('CREATE INDEX IF NOT EXISTS idx_sessions_user_status_date ON test_sessions(user_id, status, started_at DESC)');
|
||
|
||
/* ── Admin audit log ───────────────────────────────────────────────────── */
|
||
db.exec(`
|
||
CREATE TABLE IF NOT EXISTS admin_audit_log (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
admin_id INTEGER NOT NULL REFERENCES users(id),
|
||
action TEXT NOT NULL,
|
||
target TEXT,
|
||
detail TEXT,
|
||
ip TEXT,
|
||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||
)
|
||
`);
|
||
db.exec('CREATE INDEX IF NOT EXISTS idx_audit_log_date ON admin_audit_log(created_at DESC)');
|
||
db.exec('CREATE INDEX IF NOT EXISTS idx_audit_log_admin ON admin_audit_log(admin_id)');
|
||
|
||
/* ── Error log ─────────────────────────────────────────────────────────── */
|
||
db.exec(`
|
||
CREATE TABLE IF NOT EXISTS error_log (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
level TEXT NOT NULL DEFAULT 'error',
|
||
message TEXT NOT NULL,
|
||
stack TEXT,
|
||
route TEXT,
|
||
method TEXT,
|
||
user_id INTEGER,
|
||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||
)
|
||
`);
|
||
db.exec('CREATE INDEX IF NOT EXISTS idx_error_log_date ON error_log(created_at DESC)');
|
||
|
||
/* ── Online Classroom ───────────────────────────────────────────────────── */
|
||
db.exec(`
|
||
CREATE TABLE IF NOT EXISTS classroom_sessions (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
class_id INTEGER REFERENCES classes(id) ON DELETE CASCADE,
|
||
teacher_id INTEGER NOT NULL REFERENCES users(id),
|
||
title TEXT NOT NULL DEFAULT '',
|
||
status TEXT NOT NULL DEFAULT 'active',
|
||
current_page INTEGER NOT NULL DEFAULT 1,
|
||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||
ended_at TEXT
|
||
)
|
||
`);
|
||
db.exec('CREATE INDEX IF NOT EXISTS idx_cr_sessions_class ON classroom_sessions(class_id, status)');
|
||
|
||
db.exec(`
|
||
CREATE TABLE IF NOT EXISTS classroom_invites (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
session_id INTEGER NOT NULL REFERENCES classroom_sessions(id) ON DELETE CASCADE,
|
||
user_id INTEGER NOT NULL REFERENCES users(id),
|
||
UNIQUE(session_id, user_id)
|
||
)
|
||
`);
|
||
|
||
db.exec(`
|
||
CREATE TABLE IF NOT EXISTS classroom_pages (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
session_id INTEGER NOT NULL REFERENCES classroom_sessions(id) ON DELETE CASCADE,
|
||
page_num INTEGER NOT NULL DEFAULT 1,
|
||
bg_color TEXT NOT NULL DEFAULT '#ffffff',
|
||
template TEXT NOT NULL DEFAULT 'blank',
|
||
UNIQUE(session_id, page_num)
|
||
)
|
||
`);
|
||
// Add template + name columns to existing classroom_pages tables (idempotent)
|
||
try { db.exec(`ALTER TABLE classroom_pages ADD COLUMN template TEXT NOT NULL DEFAULT 'blank'`); } catch (_) { /* already exists */ }
|
||
try { db.exec(`ALTER TABLE classroom_pages ADD COLUMN name TEXT`); } catch (_) { /* already exists */ }
|
||
|
||
db.exec(`
|
||
CREATE TABLE IF NOT EXISTS classroom_strokes (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
session_id INTEGER NOT NULL REFERENCES classroom_sessions(id) ON DELETE CASCADE,
|
||
page_num INTEGER NOT NULL DEFAULT 1,
|
||
user_id INTEGER NOT NULL REFERENCES users(id),
|
||
tool TEXT NOT NULL DEFAULT 'pencil',
|
||
data TEXT NOT NULL,
|
||
seq INTEGER NOT NULL,
|
||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||
)
|
||
`);
|
||
db.exec('CREATE INDEX IF NOT EXISTS idx_cr_strokes ON classroom_strokes(session_id, page_num, seq)');
|
||
|
||
db.exec(`
|
||
CREATE TABLE IF NOT EXISTS classroom_chat (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
session_id INTEGER NOT NULL REFERENCES classroom_sessions(id) ON DELETE CASCADE,
|
||
user_id INTEGER NOT NULL REFERENCES users(id),
|
||
message TEXT NOT NULL,
|
||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||
)
|
||
`);
|
||
db.exec('CREATE INDEX IF NOT EXISTS idx_cr_chat ON classroom_chat(session_id)');
|
||
|
||
db.exec(`
|
||
CREATE TABLE IF NOT EXISTS classroom_attendance (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
session_id INTEGER NOT NULL REFERENCES classroom_sessions(id) ON DELETE CASCADE,
|
||
user_id INTEGER NOT NULL REFERENCES users(id),
|
||
joined_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||
left_at TEXT,
|
||
UNIQUE(session_id, user_id)
|
||
)
|
||
`);
|
||
|
||
// Add pinned column to classroom_chat if not exists (safe ALTER TABLE)
|
||
try { db.exec(`ALTER TABLE classroom_chat ADD COLUMN pinned INTEGER NOT NULL DEFAULT 0`); } catch {}
|
||
try { db.exec(`ALTER TABLE classroom_chat ADD COLUMN attachment_url TEXT`); } catch {}
|
||
try { db.exec(`ALTER TABLE classroom_chat ADD COLUMN attachment_type TEXT`); } catch {}
|
||
|
||
// Chat reactions
|
||
db.exec(`
|
||
CREATE TABLE IF NOT EXISTS classroom_chat_reactions (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
chat_id INTEGER NOT NULL REFERENCES classroom_chat(id) ON DELETE CASCADE,
|
||
user_id INTEGER NOT NULL REFERENCES users(id),
|
||
reaction TEXT NOT NULL,
|
||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||
UNIQUE(chat_id, user_id, reaction)
|
||
)
|
||
`);
|
||
db.exec('CREATE INDEX IF NOT EXISTS idx_cr_reactions ON classroom_chat_reactions(chat_id)');
|
||
|
||
// Per-user session notes
|
||
db.exec(`
|
||
CREATE TABLE IF NOT EXISTS classroom_notes (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
session_id INTEGER NOT NULL REFERENCES classroom_sessions(id) ON DELETE CASCADE,
|
||
user_id INTEGER NOT NULL REFERENCES users(id),
|
||
content TEXT NOT NULL DEFAULT '',
|
||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||
UNIQUE(session_id, user_id)
|
||
)
|
||
`);
|
||
|
||
// Lesson templates (saved whiteboard state)
|
||
db.exec(`
|
||
CREATE TABLE IF NOT EXISTS classroom_templates (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
teacher_id INTEGER NOT NULL REFERENCES users(id),
|
||
title TEXT NOT NULL,
|
||
description TEXT DEFAULT '',
|
||
pages_data TEXT NOT NULL DEFAULT '[]',
|
||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||
)
|
||
`);
|
||
|
||
// Guest token for classroom sessions (public read-only whiteboard access)
|
||
try { db.exec("ALTER TABLE classroom_sessions ADD COLUMN guest_token TEXT UNIQUE"); } catch {}
|
||
// Board theme per session (synced to all participants)
|
||
try { db.exec("ALTER TABLE classroom_sessions ADD COLUMN board_theme TEXT NOT NULL DEFAULT 'chalkboard'"); } catch {}
|
||
|
||
// Persistent draw permissions (survives server restart)
|
||
db.exec(`
|
||
CREATE TABLE IF NOT EXISTS classroom_draw_permissions (
|
||
session_id INTEGER NOT NULL REFERENCES classroom_sessions(id) ON DELETE CASCADE,
|
||
user_id INTEGER NOT NULL REFERENCES users(id),
|
||
PRIMARY KEY (session_id, user_id)
|
||
)
|
||
`);
|
||
|
||
// Raised hands (persisted — survives server restart)
|
||
db.exec(`
|
||
CREATE TABLE IF NOT EXISTS classroom_hands (
|
||
session_id INTEGER NOT NULL REFERENCES classroom_sessions(id) ON DELETE CASCADE,
|
||
user_id INTEGER NOT NULL REFERENCES users(id),
|
||
PRIMARY KEY (session_id, user_id)
|
||
)
|
||
`);
|
||
|
||
// ── Geometry (Planimetry) ────────────────────────────────────────────────────
|
||
// Saved geometry constructions (teacher-created tasks/templates)
|
||
db.exec(`
|
||
CREATE TABLE IF NOT EXISTS geometry_tasks (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
teacher_id INTEGER NOT NULL REFERENCES users(id),
|
||
class_id INTEGER REFERENCES classes(id) ON DELETE SET NULL,
|
||
title TEXT NOT NULL DEFAULT 'Без названия',
|
||
description TEXT DEFAULT '',
|
||
state_json TEXT NOT NULL DEFAULT '{}',
|
||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||
)
|
||
`);
|
||
db.exec('CREATE INDEX IF NOT EXISTS idx_geo_tasks_teacher ON geometry_tasks(teacher_id)');
|
||
|
||
// Student submissions for geometry tasks
|
||
db.exec(`
|
||
CREATE TABLE IF NOT EXISTS geometry_submissions (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
task_id INTEGER NOT NULL REFERENCES geometry_tasks(id) ON DELETE CASCADE,
|
||
student_id INTEGER NOT NULL REFERENCES users(id),
|
||
state_json TEXT NOT NULL DEFAULT '{}',
|
||
score REAL DEFAULT NULL,
|
||
feedback TEXT DEFAULT '',
|
||
submitted_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||
UNIQUE(task_id, student_id)
|
||
)
|
||
`);
|
||
db.exec('CREATE INDEX IF NOT EXISTS idx_geo_subs_task ON geometry_submissions(task_id)');
|
||
|
||
// Avatar photo (approved URL, NULL = use initials)
|
||
try { db.exec("ALTER TABLE users ADD COLUMN avatar_url TEXT DEFAULT NULL"); } catch {}
|
||
|
||
// Avatar moderation queue
|
||
db.exec(`
|
||
CREATE TABLE IF NOT EXISTS avatar_requests (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||
filename TEXT NOT NULL,
|
||
status TEXT NOT NULL DEFAULT 'pending',
|
||
reviewer_id INTEGER REFERENCES users(id),
|
||
reject_msg TEXT,
|
||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||
reviewed_at TEXT
|
||
)
|
||
`);
|
||
db.exec('CREATE INDEX IF NOT EXISTS idx_avatar_req_user ON avatar_requests(user_id)');
|
||
db.exec('CREATE INDEX IF NOT EXISTS idx_avatar_req_status ON avatar_requests(status)');
|
||
|
||
// User preferences (server-synced: whiteboard defaults, dashboard visibility, etc.)
|
||
db.exec(`
|
||
CREATE TABLE IF NOT EXISTS user_preferences (
|
||
user_id INTEGER PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
|
||
data TEXT NOT NULL DEFAULT '{}',
|
||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||
)
|
||
`);
|
||
|
||
// Pet columns on users table (ALTER TABLE is idempotent via try/catch)
|
||
try { db.exec("ALTER TABLE users ADD COLUMN pet_name TEXT"); } catch {}
|
||
try { db.exec("ALTER TABLE users ADD COLUMN pet_color TEXT DEFAULT 'purple'"); } catch {}
|
||
try { db.exec("ALTER TABLE users ADD COLUMN pet_last_petted TEXT"); } catch {}
|
||
try { db.exec("ALTER TABLE users ADD COLUMN pet_petting_streak INT DEFAULT 0"); } catch {}
|
||
try { db.exec("ALTER TABLE users ADD COLUMN pet_last_star TEXT"); } catch {}
|
||
try { db.exec("ALTER TABLE users ADD COLUMN pet_bg TEXT DEFAULT 'default'"); } catch {}
|
||
try { db.exec("ALTER TABLE users ADD COLUMN pet_bg_owned TEXT DEFAULT '[]'"); } catch {}
|
||
try { db.exec("ALTER TABLE users ADD COLUMN pet_last_fed TEXT"); } catch {}
|
||
db.exec('CREATE INDEX IF NOT EXISTS idx_geo_subs_student ON geometry_submissions(student_id)');
|