Files
Maxim Dolgolyov be4d43105e LearnSpace: full-stack educational whiteboard platform
Node.js/Express backend + vanilla JS frontend.
Features: real-time collaborative whiteboard (SSE), multi-page support,
LaTeX formulas, shapes/connectors, coordinate systems, number lines,
compass, zoom/pan, Catmull-Rom pencil smoothing, ruler/protractor with
rotation & resize controls, minimap navigation overlay, auto-measurements,
multi-page thumbnails sidebar, PNG export, page templates.
Student/teacher workflows: classes, assignments, library, dashboard.
Mobile responsive. SQLite (better-sqlite3).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-12 10:10:37 +03:00

296 lines
14 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use strict';
const db = require('./src/db/db');
const bcrypt = require('bcryptjs');
/* ─────────────────────────── helpers ────────────────────────────────── */
function daysAgo(d, h = 0, m = 0) {
const dt = new Date();
dt.setDate(dt.getDate() - d);
dt.setHours(dt.getHours() - h, dt.getMinutes() - m, 0, 0);
return dt.toISOString().replace('T', ' ').slice(0, 19);
}
function pick(arr) { return arr[Math.floor(Math.random() * arr.length)]; }
function shuffle(arr) {
const a = [...arr];
for (let i = a.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[a[i], a[j]] = [a[j], a[i]];
}
return a;
}
function rand(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; }
/* ─────────────────────────── data ───────────────────────────────────── */
const PASS = bcrypt.hashSync('pass1234', 10);
const TEACHER = { name: 'Наталья Смирнова', email: 'teacher@school.by', role: 'teacher' };
const STUDENTS = [
{ name: 'Алексей Петров', email: 'petrov@school.by', skill: 0.85 },
{ name: 'Мария Иванова', email: 'ivanova@school.by', skill: 0.78 },
{ name: 'Дмитрий Соколов', email: 'sokolov@school.by', skill: 0.62 },
{ name: 'Анна Козлова', email: 'kozlova@school.by', skill: 0.91 },
{ name: 'Иван Новиков', email: 'novikov@school.by', skill: 0.55 },
{ name: 'Екатерина Морозова', email: 'morozova@school.by', skill: 0.73 },
{ name: 'Сергей Волков', email: 'volkov@school.by', skill: 0.40 },
{ name: 'Ольга Лебедева', email: 'lebedeva@school.by', skill: 0.82 },
{ name: 'Никита Зайцев', email: 'zajcev@school.by', skill: 0.48 },
{ name: 'Виктория Семёнова', email: 'semenova@school.by', skill: 0.69 },
{ name: 'Артём Павлов', email: 'pavlov@school.by', skill: 0.30, lazy: true },
{ name: 'Елена Фёдорова', email: 'fedorova@school.by', skill: 0.95 },
{ name: 'Максим Орлов', email: 'orlov@school.by', skill: 0.58 },
{ name: 'Юлия Попова', email: 'popova@school.by', skill: 0.76, lazy: true },
];
/* ─────────────────────────── insert users ───────────────────────────── */
const insertUser = db.prepare(
'INSERT OR IGNORE INTO users (email, password_hash, name, role) VALUES (?, ?, ?, ?)'
);
const getUser = db.prepare('SELECT id FROM users WHERE email = ?');
insertUser.run(TEACHER.email, PASS, TEACHER.name, TEACHER.role);
const teacherId = getUser.get(TEACHER.email).id;
console.log(`✓ Teacher: ${TEACHER.name} (id=${teacherId})`);
const studentIds = [];
for (const s of STUDENTS) {
insertUser.run(s.email, PASS, s.name, 'student');
const id = getUser.get(s.email).id;
studentIds.push({ id, skill: s.skill, lazy: !!s.lazy, name: s.name });
}
console.log(`✓ ${STUDENTS.length} students inserted`);
/* ─────────────────────────── classes ────────────────────────────────── */
function genCode() {
return Math.random().toString(36).slice(2, 9).toUpperCase();
}
const insertClass = db.prepare(
'INSERT OR IGNORE INTO classes (name, description, teacher_id, invite_code) VALUES (?, ?, ?, ?)'
);
const getClassByName = db.prepare('SELECT id FROM classes WHERE name = ? AND teacher_id = ?');
const classData = [
{ name: '11А · Биология', desc: 'Подготовка к ЦТ 2026 по биологии', subject: 'bio', subjectId: 1 },
{ name: '10Б · Математика', desc: 'Алгебра и начала анализа', subject: 'math', subjectId: 3 },
];
const classes = [];
for (const c of classData) {
insertClass.run(c.name, c.desc, teacherId, genCode());
const row = getClassByName.get(c.name, teacherId);
classes.push({ ...c, id: row.id });
console.log(`✓ Class: "${c.name}" (id=${row.id})`);
}
/* ─────────────────────────── enroll students ────────────────────────── */
const insertMember = db.prepare(
'INSERT OR IGNORE INTO class_members (class_id, user_id, joined_at) VALUES (?, ?, ?)'
);
// Класс 1 (bio) — все студенты
for (const s of studentIds) {
insertMember.run(classes[0].id, s.id, daysAgo(rand(20, 40)));
}
// Класс 2 (math) — первые 10 студентов
for (const s of studentIds.slice(0, 10)) {
insertMember.run(classes[1].id, s.id, daysAgo(rand(15, 35)));
}
console.log('✓ Students enrolled in classes');
/* ─────────────────────────── load questions ─────────────────────────── */
const allQuestions = db.prepare(
'SELECT id, subject_id, type FROM questions WHERE type = ? OR type IS NULL'
).all('single');
const questionsBySubject = {};
for (const q of allQuestions) {
if (!questionsBySubject[q.subject_id]) questionsBySubject[q.subject_id] = [];
questionsBySubject[q.subject_id].push(q);
}
// Load all options grouped by question
const allOptions = db.prepare('SELECT id, question_id, is_correct FROM options').all();
const optionsByQuestion = {};
for (const o of allOptions) {
if (!optionsByQuestion[o.question_id]) optionsByQuestion[o.question_id] = [];
optionsByQuestion[o.question_id].push(o);
}
/* ─────────────────────────── simulate session ────────────────────────── */
const insertSession = db.prepare(
"INSERT INTO test_sessions (user_id, subject_id, mode, total, score, status, started_at, finished_at) VALUES (?, ?, ?, ?, ?, 'completed', ?, ?)"
);
const insertSQ = db.prepare(
'INSERT OR IGNORE INTO session_questions (session_id, question_id, order_index) VALUES (?, ?, ?)'
);
const insertAnswer = db.prepare(
'INSERT OR IGNORE INTO user_answers (session_id, question_id, chosen_option_id, is_correct, time_spent_sec, answered_at) VALUES (?, ?, ?, ?, ?, ?)'
);
const insertAssignSes = db.prepare(
'INSERT OR IGNORE INTO assignment_sessions (assignment_id, user_id, session_id) VALUES (?, ?, ?)'
);
function simulateSession(userId, subjectId, mode, count, skill, daysAgoN) {
const pool = shuffle(questionsBySubject[subjectId] || []).slice(0, count);
if (!pool.length) return null;
const startedAt = daysAgo(daysAgoN, rand(0, 6));
const finishedAt = daysAgo(daysAgoN, rand(0, 5), rand(5, 40));
let score = 0;
const info = insertSession.run(userId, subjectId, mode, pool.length, 0, startedAt, finishedAt);
const sessionId = info.lastInsertRowid;
pool.forEach((q, idx) => {
insertSQ.run(sessionId, q.id, idx);
const opts = optionsByQuestion[q.id] || [];
if (!opts.length) return;
const correct = opts.find(o => o.is_correct);
const wrong = opts.filter(o => !o.is_correct);
// skill = probability of answering correctly
const isCorrect = Math.random() < skill;
let chosen;
if (isCorrect && correct) {
chosen = correct;
} else {
chosen = wrong.length ? pick(wrong) : pick(opts);
}
const wasCorrect = chosen.is_correct ? 1 : 0;
if (wasCorrect) score++;
insertAnswer.run(sessionId, q.id, chosen.id, wasCorrect, rand(8, 95), finishedAt);
});
// Update score
db.prepare('UPDATE test_sessions SET score = ? WHERE id = ?').run(score, sessionId);
return sessionId;
}
/* ─────────────────────────── assignments ────────────────────────────── */
const insertAssign = db.prepare(`
INSERT OR IGNORE INTO assignments (class_id, title, subject_slug, mode, count, deadline, created_by, created_at)
VALUES (?, ?, ?, 'exam', ?, ?, ?, ?)
`);
const getAssign = db.prepare('SELECT id FROM assignments WHERE class_id = ? AND title = ?');
const assignmentDefs = [
// Bio class
{
classIdx: 0, title: 'Клетка и её строение', subjectId: 1, count: 15,
createdAgo: 28, deadlineAgo: 21, studentSkipChance: 0.0
},
{
classIdx: 0, title: 'Генетика — базовый уровень', subjectId: 1, count: 20,
createdAgo: 18, deadlineAgo: 11, studentSkipChance: 0.05
},
{
classIdx: 0, title: 'Контрольная: Митоз и мейоз', subjectId: 1, count: 25,
createdAgo: 10, deadlineAgo: 5, studentSkipChance: 0.10
},
{
classIdx: 0, title: 'Фотосинтез и дыхание', subjectId: 1, count: 15,
createdAgo: 3, deadlineAgo: null, studentSkipChance: 0.20
},
// Math class
{
classIdx: 1, title: 'Квадратные уравнения', subjectId: 3, count: 10,
createdAgo: 22, deadlineAgo: 16, studentSkipChance: 0.0
},
{
classIdx: 1, title: 'Тригонометрия — тест', subjectId: 3, count: 15,
createdAgo: 12, deadlineAgo: 7, studentSkipChance: 0.08
},
{
classIdx: 1, title: 'Логарифмы и степени', subjectId: 3, count: 15,
createdAgo: 4, deadlineAgo: null, studentSkipChance: 0.25
},
];
for (const a of assignmentDefs) {
const cls = classes[a.classIdx];
const deadline = a.deadlineAgo ? daysAgo(a.deadlineAgo) : null;
insertAssign.run(cls.id, a.title, cls.subject, a.count, deadline, teacherId, daysAgo(a.createdAgo));
const row = getAssign.get(cls.id, a.title);
if (!row) { console.warn(` ! Could not get assignment id for "${a.title}"`); continue; }
const assignId = row.id;
// Determine enrolled students for this class
const enrolled = a.classIdx === 0 ? studentIds : studentIds.slice(0, 10);
let done = 0;
for (const s of enrolled) {
// Lazy students skip recent assignments
if (s.lazy && a.studentSkipChance > 0.15) continue;
// Random skip by skip chance
if (Math.random() < a.studentSkipChance) continue;
const sessionId = simulateSession(
s.id, a.subjectId, 'exam', a.count, s.skill,
rand(a.deadlineAgo ? a.deadlineAgo : 1, a.createdAgo)
);
if (sessionId) {
insertAssignSes.run(assignId, s.id, sessionId);
done++;
}
}
console.log(`✓ Assignment "${a.title}" — ${done}/${enrolled.length} completed`);
}
/* ─────────────────────────── free-form sessions ─────────────────────── */
// Some students did extra practice on their own
const practiceSubjects = [[1, 'bio'], [3, 'math']];
for (const s of studentIds) {
if (s.lazy) continue;
const extraCount = rand(1, 4);
for (let i = 0; i < extraCount; i++) {
const [subId] = pick(practiceSubjects);
simulateSession(s.id, subId, 'practice', rand(10, 25), s.skill + 0.05, rand(1, 30));
}
}
console.log('✓ Extra practice sessions simulated');
/* ─────────────────────────── announcements ──────────────────────────── */
const insertAnn = db.prepare(
'INSERT INTO announcements (class_id, author_id, text, created_at) VALUES (?, ?, ?, ?)'
);
const annTexts = [
[0, '📅 Напоминаю: контрольная по митозу пройдёт в следующую пятницу. Повторите темы деления клетки.', 9],
[0, '✅ Результаты по теме «Клетка» проверены. Молодцы! Средний балл — 74%. Слабее всего — органоиды.', 20],
[0, '📚 Для подготовки к ЦТ рекомендую дополнительно пройти тест по фотосинтезу в системе.', 5],
[1, '🧮 Разбор ошибок по квадратным уравнениям будет в среду, 14:00, кабинет 204.', 11],
[1, '⚠️ Дедлайн по логарифмам — в пятницу. Кто не сдал тригонометрию — сдайте до конца недели.', 3],
];
for (const [classIdx, text, ago] of annTexts) {
insertAnn.run(classes[classIdx].id, teacherId, text, daysAgo(ago));
}
console.log('✓ Announcements created');
/* ─────────────────────────── notifications ──────────────────────────── */
const insertNotif = db.prepare(
'INSERT INTO notifications (user_id, type, message, link, is_read, created_at) VALUES (?, ?, ?, ?, ?, ?)'
);
// Teacher gets notifs about students completing assignments
for (const s of studentIds.slice(0, 6)) {
insertNotif.run(teacherId, 'session',
${s.name}» сдал «Генетика — базовый уровень» — ${rand(55, 95)}%`,
'/classes.html', 1, daysAgo(rand(5, 15)));
}
// Students get assignment notifs
for (const s of studentIds) {
insertNotif.run(s.id, 'assignment', '📋 Для вас задание: «Контрольная: Митоз и мейоз»', '/dashboard.html', 1, daysAgo(10));
insertNotif.run(s.id, 'assignment', '📋 Для вас задание: «Фотосинтез и дыхание»', '/dashboard.html', rand(0,1), daysAgo(3));
}
console.log('✓ Notifications created');
/* ─────────────────────────── summary ────────────────────────────────── */
const stats = {
users: db.prepare('SELECT COUNT(*) as n FROM users').get().n,
sessions: db.prepare('SELECT COUNT(*) as n FROM test_sessions').get().n,
answers: db.prepare('SELECT COUNT(*) as n FROM user_answers').get().n,
assigns: db.prepare('SELECT COUNT(*) as n FROM assignments').get().n,
};
console.log('\n═══ Seed complete ═══');
console.log(` Users: ${stats.users}`);
console.log(` Sessions: ${stats.sessions}`);
console.log(` Answers: ${stats.answers}`);
console.log(` Assignments: ${stats.assigns}`);
console.log('\n Password for all accounts: pass1234');
console.log(` Teacher: ${TEACHER.email}`);
console.log(` Students: ${STUDENTS[0].email}${STUDENTS[STUDENTS.length-1].email}`);