be4d43105e
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>
296 lines
14 KiB
JavaScript
296 lines
14 KiB
JavaScript
'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}`);
|