CREATE TABLE IF NOT EXISTS branches ( id SERIAL PRIMARY KEY, name VARCHAR(200) NOT NULL UNIQUE, address TEXT, phone VARCHAR(50), created_at TIMESTAMPTZ DEFAULT now() ); CREATE TABLE IF NOT EXISTS banned_ips ( ip VARCHAR(64) PRIMARY KEY, reason VARCHAR(100) NOT NULL, banned_until TIMESTAMPTZ NOT NULL, created_at TIMESTAMPTZ DEFAULT now() ); CREATE TABLE IF NOT EXISTS groups ( id SERIAL PRIMARY KEY, name VARCHAR(100) NOT NULL UNIQUE, branch_id INT REFERENCES branches(id) ON DELETE SET NULL, created_at TIMESTAMPTZ DEFAULT now(), day_of_week INT, time_start TIME, time_end TIME, cover_path VARCHAR(255), deleted_at TIMESTAMPTZ, purge_at TIMESTAMPTZ ); ALTER TABLE groups ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ; ALTER TABLE groups ADD COLUMN IF NOT EXISTS purge_at TIMESTAMPTZ; CREATE TABLE IF NOT EXISTS modules ( id SERIAL PRIMARY KEY, name VARCHAR(200) NOT NULL UNIQUE, lessons_count INT NOT NULL DEFAULT 0, is_active BOOLEAN NOT NULL DEFAULT true, photo_path VARCHAR(255), created_at TIMESTAMPTZ DEFAULT now() ); ALTER TABLE modules ADD COLUMN IF NOT EXISTS is_active BOOLEAN NOT NULL DEFAULT true; ALTER TABLE modules ADD COLUMN IF NOT EXISTS photo_path VARCHAR(255); CREATE TABLE IF NOT EXISTS students ( id SERIAL PRIMARY KEY, name VARCHAR(150) NOT NULL UNIQUE, group_id INT REFERENCES groups(id), created_at TIMESTAMPTZ DEFAULT now(), photo_path VARCHAR(255), profile JSONB ); ALTER TABLE students ADD COLUMN IF NOT EXISTS photo_path VARCHAR(255); ALTER TABLE students ADD COLUMN IF NOT EXISTS profile JSONB; CREATE TABLE IF NOT EXISTS student_photos ( id SERIAL PRIMARY KEY, student_id INT NOT NULL REFERENCES students(id) ON DELETE CASCADE, photo_path VARCHAR(255) NOT NULL, created_at TIMESTAMPTZ DEFAULT now() ); CREATE INDEX IF NOT EXISTS idx_student_photos_student_id ON student_photos(student_id); CREATE TABLE IF NOT EXISTS entries ( id SERIAL PRIMARY KEY, student_name VARCHAR(150) NOT NULL, group_id INT NOT NULL REFERENCES groups(id), module_id INT REFERENCES modules(id) ON DELETE SET NULL, description TEXT NOT NULL, description_original TEXT, description_ai TEXT, ai_status VARCHAR(20) NOT NULL DEFAULT 'pending', ai_checked_at TIMESTAMPTZ, ai_error TEXT, photo_path VARCHAR(255), photo_original_path VARCHAR(255), deleted_at TIMESTAMPTZ, purge_at TIMESTAMPTZ, created_at TIMESTAMPTZ DEFAULT now() ); ALTER TABLE entries ADD COLUMN IF NOT EXISTS description_original TEXT; ALTER TABLE entries ADD COLUMN IF NOT EXISTS description_ai TEXT; ALTER TABLE entries ADD COLUMN IF NOT EXISTS ai_status VARCHAR(20) NOT NULL DEFAULT 'pending'; ALTER TABLE entries ADD COLUMN IF NOT EXISTS ai_checked_at TIMESTAMPTZ; ALTER TABLE entries ADD COLUMN IF NOT EXISTS ai_error TEXT; ALTER TABLE entries ADD COLUMN IF NOT EXISTS module_id INT REFERENCES modules(id) ON DELETE SET NULL; ALTER TABLE entries ADD COLUMN IF NOT EXISTS purge_at TIMESTAMPTZ; CREATE INDEX IF NOT EXISTS idx_entries_ai_pending ON entries(id) WHERE ai_status = 'pending' AND deleted_at IS NULL; CREATE INDEX IF NOT EXISTS idx_entries_module_id ON entries(module_id); CREATE OR REPLACE FUNCTION notify_entries_changed() RETURNS trigger AS $$ BEGIN IF (TG_OP = 'INSERT') THEN PERFORM pg_notify('entries_changed', json_build_object('type', 'entry_created', 'id', NEW.id)::text); ELSIF (TG_OP = 'UPDATE' AND OLD.ai_status IS DISTINCT FROM NEW.ai_status) THEN PERFORM pg_notify('entries_changed', json_build_object('type', 'ai_status', 'id', NEW.id, 'status', NEW.ai_status, 'error', NEW.ai_error, 'description', NEW.description, 'description_ai', NEW.description_ai, 'description_original', NEW.description_original)::text); END IF; RETURN NULL; END; $$ LANGUAGE plpgsql; DROP TRIGGER IF EXISTS trg_entries_notify ON entries; CREATE TRIGGER trg_entries_notify AFTER INSERT OR UPDATE OF ai_status ON entries FOR EACH ROW EXECUTE FUNCTION notify_entries_changed(); CREATE TABLE IF NOT EXISTS settings ( key TEXT PRIMARY KEY, value TEXT ); INSERT INTO settings (key, value) VALUES ('spam_interval_min', '30') ON CONFLICT (key) DO NOTHING; INSERT INTO settings (key, value) VALUES ('ai_prompt', 'Ты — редактор текстов. Исправь ТОЛЬКО грамматические, орфографические и пунктуационные ошибки в тексте. Приведи к правильному регистру буквы. НЕ меняй слова, структуру предложений, стиль или смысл текста. Верни ТОЛЬКО исправленный текст без пояснений.') ON CONFLICT (key) DO NOTHING; INSERT INTO settings (key, value) VALUES ('ai_autocheck_enabled', 'true') ON CONFLICT (key) DO NOTHING; INSERT INTO settings (key, value) VALUES ('share_show_student_message', 'false') ON CONFLICT (key) DO NOTHING; INSERT INTO settings (key, value) VALUES ('share_show_entry_date', 'false') ON CONFLICT (key) DO NOTHING; INSERT INTO settings (key, value) VALUES ('share_show_student_names', 'true') ON CONFLICT (key) DO NOTHING; INSERT INTO settings (key, value) VALUES ('share_show_group_photos', 'true') ON CONFLICT (key) DO NOTHING; INSERT INTO settings (key, value) VALUES ('cookie_notice_text', 'Этот сайт использует cookie-файлы для корректной работы. Продолжая просмотр, вы соглашаетесь с их использованием.') ON CONFLICT (key) DO NOTHING; INSERT INTO settings (key, value) VALUES ('camera_enabled', 'true') ON CONFLICT (key) DO NOTHING; INSERT INTO settings (key, value) VALUES ('trash_purge_days', '30') ON CONFLICT (key) DO NOTHING; INSERT INTO settings (key, value) VALUES ('timezone', 'Europe/Moscow') ON CONFLICT (key) DO NOTHING; INSERT INTO settings (key, value) VALUES ('time_format', '24h') ON CONFLICT (key) DO NOTHING; CREATE TABLE IF NOT EXISTS share_links ( id SERIAL PRIMARY KEY, token VARCHAR(40) UNIQUE NOT NULL, name VARCHAR(200) NOT NULL, group_id INT REFERENCES groups(id), student_name VARCHAR(150), date_from DATE, date_to DATE, show_student_names BOOLEAN, expires_at TIMESTAMPTZ, access_password_hash VARCHAR(255), message TEXT, link_url VARCHAR(500), show_student_message BOOLEAN, show_entry_date BOOLEAN, show_group_photos BOOLEAN, created_at TIMESTAMPTZ DEFAULT now() ); CREATE INDEX IF NOT EXISTS idx_share_links_expires_at ON share_links(expires_at); INSERT INTO groups (name) VALUES ('Beginner'), ('Intermediate'), ('Advanced') ON CONFLICT (name) DO NOTHING; CREATE TABLE IF NOT EXISTS group_photos ( id SERIAL PRIMARY KEY, group_id INT NOT NULL REFERENCES groups(id) ON DELETE CASCADE, photo_path VARCHAR(255) NOT NULL, caption TEXT, taken_at DATE, sort_order INT DEFAULT 0, created_at TIMESTAMPTZ DEFAULT now() ); CREATE INDEX IF NOT EXISTS idx_group_photos_group_id ON group_photos(group_id); CREATE TABLE IF NOT EXISTS project_files ( id SERIAL PRIMARY KEY, entry_id INT REFERENCES entries(id) ON DELETE CASCADE, token VARCHAR(64) UNIQUE NOT NULL, path VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL, created_at TIMESTAMPTZ DEFAULT now(), detached_at TIMESTAMPTZ ); CREATE TABLE IF NOT EXISTS entry_photos ( id SERIAL PRIMARY KEY, entry_id INT NOT NULL REFERENCES entries(id) ON DELETE CASCADE, photo_path VARCHAR(255) NOT NULL, caption TEXT, sort_order INT DEFAULT 0, created_at TIMESTAMPTZ DEFAULT now() ); CREATE INDEX IF NOT EXISTS idx_entry_photos_entry_id ON entry_photos(entry_id); CREATE TABLE IF NOT EXISTS users ( id SERIAL PRIMARY KEY, username VARCHAR(100) NOT NULL UNIQUE, password_hash VARCHAR(255) NOT NULL, name VARCHAR(150), role VARCHAR(20) NOT NULL DEFAULT 'tutor' CHECK (role IN ('admin','tutor')), is_active BOOLEAN DEFAULT true, created_at TIMESTAMPTZ DEFAULT now() ); ALTER TABLE groups ADD COLUMN IF NOT EXISTS tutor_id INT REFERENCES users(id) ON DELETE SET NULL; CREATE TABLE IF NOT EXISTS user_branches ( user_id INT NOT NULL REFERENCES users(id) ON DELETE CASCADE, branch_id INT NOT NULL REFERENCES branches(id) ON DELETE CASCADE, PRIMARY KEY (user_id, branch_id) ); CREATE TABLE IF NOT EXISTS sessions ( id SERIAL PRIMARY KEY, user_id INT NOT NULL REFERENCES users(id) ON DELETE CASCADE, token VARCHAR(64) NOT NULL UNIQUE, created_at TIMESTAMPTZ DEFAULT now(), expires_at TIMESTAMPTZ NOT NULL ); CREATE INDEX IF NOT EXISTS idx_sessions_token ON sessions(token); CREATE INDEX IF NOT EXISTS idx_sessions_expires_at ON sessions(expires_at); CREATE TABLE IF NOT EXISTS lesson_reports ( id SERIAL PRIMARY KEY, group_id INT NOT NULL REFERENCES groups(id) ON DELETE CASCADE, lesson_date DATE NOT NULL, lesson_time TIME, topic TEXT, text TEXT NOT NULL, text_original TEXT, text_ai TEXT, ai_status VARCHAR(20) NOT NULL DEFAULT 'none', ai_checked_at TIMESTAMPTZ, ai_error TEXT, author_id INT REFERENCES users(id) ON DELETE SET NULL, branch_id INT REFERENCES branches(id) ON DELETE SET NULL, created_at TIMESTAMPTZ DEFAULT now(), updated_at TIMESTAMPTZ DEFAULT now() ); CREATE UNIQUE INDEX IF NOT EXISTS idx_lesson_reports_group_date ON lesson_reports(group_id, lesson_date); CREATE INDEX IF NOT EXISTS idx_lesson_reports_date ON lesson_reports(lesson_date DESC); CREATE TABLE IF NOT EXISTS lesson_report_versions ( id SERIAL PRIMARY KEY, lesson_report_id INT NOT NULL REFERENCES lesson_reports(id) ON DELETE CASCADE, text TEXT NOT NULL, source VARCHAR(20) NOT NULL DEFAULT 'manual', author_id INT REFERENCES users(id) ON DELETE SET NULL, created_at TIMESTAMPTZ DEFAULT now() ); CREATE INDEX IF NOT EXISTS idx_lesson_report_versions_report ON lesson_report_versions(lesson_report_id, id DESC); CREATE TABLE IF NOT EXISTS photo_jobs ( id SERIAL PRIMARY KEY, entry_id INT NOT NULL REFERENCES entries(id) ON DELETE CASCADE, action VARCHAR(20) NOT NULL DEFAULT 'ai', params JSONB, before_path VARCHAR(255), after_path VARCHAR(255), status VARCHAR(20) NOT NULL DEFAULT 'pending', applied BOOLEAN NOT NULL DEFAULT false, attempts INT NOT NULL DEFAULT 0, error TEXT, created_at TIMESTAMPTZ DEFAULT now(), finished_at TIMESTAMPTZ ); CREATE INDEX IF NOT EXISTS idx_photo_jobs_pending ON photo_jobs(id) WHERE status = 'pending'; CREATE INDEX IF NOT EXISTS idx_photo_jobs_entry_id ON photo_jobs(entry_id); INSERT INTO settings (key, value) VALUES ('photo_worker_enabled', 'true') ON CONFLICT (key) DO NOTHING; INSERT INTO settings (key, value) VALUES ('photo_ai_face_mode', 'off') ON CONFLICT (key) DO NOTHING; INSERT INTO settings (key, value) VALUES ('photo_ai_face_model', 'gfpgan') ON CONFLICT (key) DO NOTHING; INSERT INTO settings (key, value) VALUES ('photo_ai_device_pref', 'auto') ON CONFLICT (key) DO NOTHING; CREATE TABLE IF NOT EXISTS audit_log ( id SERIAL PRIMARY KEY, user_id INT REFERENCES users(id) ON DELETE SET NULL, action VARCHAR(100) NOT NULL, target JSONB, ip VARCHAR(45), created_at TIMESTAMPTZ DEFAULT now() ); CREATE INDEX IF NOT EXISTS idx_audit_log_created_at ON audit_log(created_at DESC); CREATE TABLE IF NOT EXISTS notifications ( id SERIAL PRIMARY KEY, type VARCHAR(50) NOT NULL, level VARCHAR(20) NOT NULL DEFAULT 'info', title VARCHAR(200) NOT NULL, body TEXT, link VARCHAR(255), target JSONB, admin_only BOOLEAN NOT NULL DEFAULT false, branch_id INT REFERENCES branches(id) ON DELETE SET NULL, created_at TIMESTAMPTZ DEFAULT now() ); CREATE INDEX IF NOT EXISTS idx_notifications_created_at ON notifications(created_at DESC); CREATE INDEX IF NOT EXISTS idx_notifications_branch_id ON notifications(branch_id); CREATE TABLE IF NOT EXISTS notification_reads ( user_id INT NOT NULL REFERENCES users(id) ON DELETE CASCADE, notification_id INT NOT NULL REFERENCES notifications(id) ON DELETE CASCADE, read_at TIMESTAMPTZ DEFAULT now(), PRIMARY KEY (user_id, notification_id) ); CREATE INDEX IF NOT EXISTS idx_notification_reads_user ON notification_reads(user_id); INSERT INTO settings (key, value) VALUES ('notify_enabled', 'true') ON CONFLICT (key) DO NOTHING; INSERT INTO settings (key, value) VALUES ('notify_retention_days', '30') ON CONFLICT (key) DO NOTHING; INSERT INTO settings (key, value) VALUES ('notify_entry_new', 'true') ON CONFLICT (key) DO NOTHING; INSERT INTO settings (key, value) VALUES ('notify_entry_ai_corrected', 'false') ON CONFLICT (key) DO NOTHING; INSERT INTO settings (key, value) VALUES ('notify_entry_ai_error', 'true') ON CONFLICT (key) DO NOTHING; INSERT INTO settings (key, value) VALUES ('notify_photo_job_done', 'true') ON CONFLICT (key) DO NOTHING; INSERT INTO settings (key, value) VALUES ('notify_photo_job_error', 'true') ON CONFLICT (key) DO NOTHING; INSERT INTO settings (key, value) VALUES ('notify_ip_ban', 'true') ON CONFLICT (key) DO NOTHING; INSERT INTO settings (key, value) VALUES ('notify_backup_restore', 'true') ON CONFLICT (key) DO NOTHING; INSERT INTO settings (key, value) VALUES ('notify_backup_create', 'false') ON CONFLICT (key) DO NOTHING; INSERT INTO settings (key, value) VALUES ('notify_system_test', 'true') ON CONFLICT (key) DO NOTHING; INSERT INTO settings (key, value) VALUES ('lesson_ai_enabled', 'true') ON CONFLICT (key) DO NOTHING; INSERT INTO settings (key, value) VALUES ('lesson_ai_prompt', 'Ты — редактор сообщений тьютора детской IT-школы. Переработай исходный текст занятия так, чтобы он звучал естественно и грамотно, как будто его написал живой тьютор родителю, а не нейросеть. ГЛАВНОЕ: - Не добавляй информацию, которой нет в исходном тексте. Сохрани все факты и смысл. - Исправь ошибки, повторы и неудачные формулировки. Убери канцелярит, шаблонные фразы и «ИИ-язык». - Не используй чрезмерную похвалу и не превращай обычное занятие в достижение мирового масштала. СТИЛЬ И СТРУКТУРА: - Один цельный абзац, 2–4 предложения, от третьего лица. - Первое предложение начинай со слов «На занятии ребята …». - Если в исходном тексте описана практическая часть, начни её со слов «В конце занятия …» или «Затем …». Нет практики в исходнике — не придумывай её. - Вместо «они» пиши «каждый». Обращение на «вы» не используй. - Коротко скажи, с чем познакомились или что изучали; затем — что конкретно делали; в конце — что сделал самостоятельно. - Сократи перечисления, объединяй их через «и», «а также», не повторяй одно и то же разными словами. - Убери разговорные и оценочные обороты: «было весело», «очень», «классно». ФОРМАТ: - Без заголовков, списков, markdown, подписей и пояснений. - Не начинай со слов «Сегодня», «Вчера», «Дата», «Группа», «Время» и вообще не упоминай группу, дату и время занятия. - Не начинай с «На данном занятии» или «В рамках занятия». - Не оборачивай ответ в кавычки. - Не добавляй лишних предложений: если исходный текст уже написан нормально, не переписывай его ради переписывания. ПРИМЕР ПРЕОБРАЗОВАНИЯ (бери отсюда только формулировки, тему и факты примера в свой текст не переноси): Исходный текст: «Ребята познакомились с программой Scratch Jr, научились выбирать фон, добавлять, изменять и создавать своих персонажей. В завершении занятия они выполнили практическое индивидуальное задание по созданию собственной анимации и небольшой программы». Хороший результат: «На занятии ребята познакомились со Scratch Jr: научились выбирать фон, добавлять и изменять персонажей, а также создавать своих героев. В конце занятия каждый самостоятельно выполнил небольшое практическое задание — придумал свою анимацию и собрал простую программу». ГЛАВНОЕ ПРАВИЛО: Отрабатывай ровно по этому исходному тексту. Ничего из примера выше в свой текст не переноси: тема, программа, персонажи и детали из примера не твои.') ON CONFLICT (key) DO NOTHING; INSERT INTO settings (key, value) VALUES ('notify_lesson_ai_formatted', 'false') ON CONFLICT (key) DO NOTHING;