Воркер ИИ
+Отслеживание автоматической проверки сообщений записей
+Очередь (0)
+Записи, ожидающие проверки моделью. Обновляются автоматически.
+| ID | Ученик | Группа | Создана |
|---|
Последние проверки
+| Проверено | Ученик | Группа | Статус | Изменения |
|---|
diff --git a/AGENTS.md b/AGENTS.md index d3d17c3..3b2eb3d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -141,6 +141,7 @@ curl -H "X-Admin-Token: $ADMIN_PASSWORD" http://localhost:3003/api/groups | File | Purpose | |------|---------| | `server.js` | Entire backend (Express, routes, DB, uploads, backup) | +| `worker.js` | Background AI auto-check worker for entry messages | | `db/init.sql` | Initial schema (runs on fresh DB) | | `db/migration.sql` | Idempotent migrations for existing DBs | | `docker-compose.yml` | Service definitions (app, db, tailscale) | diff --git a/db/init.sql b/db/init.sql index 016552b..932dd0f 100644 --- a/db/init.sql +++ b/db/init.sql @@ -29,11 +29,24 @@ CREATE TABLE IF NOT EXISTS entries ( student_name VARCHAR(150) NOT NULL, group_id INT NOT NULL REFERENCES groups(id), 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), deleted_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; + +CREATE INDEX IF NOT EXISTS idx_entries_ai_pending ON entries(id) WHERE ai_status = 'pending' AND deleted_at IS NULL; + CREATE TABLE IF NOT EXISTS settings ( key TEXT PRIMARY KEY, value TEXT @@ -41,6 +54,10 @@ CREATE TABLE IF NOT EXISTS settings ( 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; CREATE TABLE IF NOT EXISTS share_links ( id SERIAL PRIMARY KEY, @@ -53,6 +70,8 @@ CREATE TABLE IF NOT EXISTS share_links ( anonymize_names BOOLEAN DEFAULT false, expires_at TIMESTAMPTZ, access_password_hash VARCHAR(255), + message TEXT, + link_url VARCHAR(500), created_at TIMESTAMPTZ DEFAULT now() ); @@ -86,6 +105,17 @@ CREATE TABLE IF NOT EXISTS project_files ( 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, diff --git a/db/migration.sql b/db/migration.sql index 5a65df1..45d1852 100644 --- a/db/migration.sql +++ b/db/migration.sql @@ -13,6 +13,8 @@ CREATE TABLE IF NOT EXISTS settings ( 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; CREATE TABLE IF NOT EXISTS share_links ( id SERIAL PRIMARY KEY, @@ -25,6 +27,9 @@ CREATE TABLE IF NOT EXISTS share_links ( created_at TIMESTAMPTZ DEFAULT now() ); +ALTER TABLE share_links ADD COLUMN IF NOT EXISTS message TEXT; +ALTER TABLE share_links ADD COLUMN IF NOT EXISTS link_url VARCHAR(500); + DO $$ BEGIN INSERT INTO students (name) @@ -48,7 +53,37 @@ CREATE UNIQUE INDEX IF NOT EXISTS project_files_token_key ON project_files(token ALTER TABLE project_files ALTER COLUMN entry_id DROP NOT NULL; ALTER TABLE project_files ADD COLUMN IF NOT EXISTS 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); + ALTER TABLE entries ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ; +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; + +CREATE INDEX IF NOT EXISTS idx_entries_ai_pending ON entries(id) WHERE ai_status = 'pending' AND deleted_at IS NULL; + +INSERT INTO settings (key, value) VALUES ('ai_autocheck_enabled', 'true') +ON CONFLICT (key) DO NOTHING; + +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM settings WHERE key = 'ai_autocheck_migrated') THEN + UPDATE entries SET description_original = description WHERE description_original IS NULL; + UPDATE entries SET ai_status = 'skipped' WHERE ai_status = 'pending'; + INSERT INTO settings (key, value) VALUES ('ai_autocheck_migrated', '1'); + END IF; +END $$; CREATE TABLE IF NOT EXISTS branches ( id SERIAL PRIMARY KEY, @@ -105,3 +140,10 @@ CREATE TABLE IF NOT EXISTS group_photos ( ALTER TABLE group_photos ADD COLUMN IF NOT EXISTS sort_order INT DEFAULT 0; CREATE INDEX IF NOT EXISTS idx_group_photos_group_id ON group_photos(group_id); + +UPDATE entries e SET photo_path = p.photo_path +FROM ( + SELECT DISTINCT ON (entry_id) entry_id, photo_path + FROM entry_photos ORDER BY entry_id, sort_order, id +) p +WHERE e.photo_path IS NULL AND p.entry_id = e.id; diff --git a/db/migration_share_links.sql b/db/migration_share_links.sql index 7cf8028..269dae1 100644 --- a/db/migration_share_links.sql +++ b/db/migration_share_links.sql @@ -14,4 +14,8 @@ ALTER TABLE share_links ADD COLUMN IF NOT EXISTS access_password_hash VARCHAR(25 -- UPDATE share_links SET expires_at = created_at + interval '7 days' WHERE expires_at IS NULL; -- 5. Add index for cleanup of expired links -CREATE INDEX IF NOT EXISTS idx_share_links_expires_at ON share_links(expires_at); \ No newline at end of file +CREATE INDEX IF NOT EXISTS idx_share_links_expires_at ON share_links(expires_at); + +-- 6. Add public message and external link shown on the share page +ALTER TABLE share_links ADD COLUMN IF NOT EXISTS message TEXT; +ALTER TABLE share_links ADD COLUMN IF NOT EXISTS link_url VARCHAR(500); \ No newline at end of file diff --git a/public/admin.css b/public/admin.css index 908407e..d2ef58b 100644 --- a/public/admin.css +++ b/public/admin.css @@ -232,6 +232,9 @@ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;b .page-head h2{font-size:1.3rem;font-weight:600;margin:0} .page-sub{color:var(--muted);font-size:.85rem} .settings-stack{display:flex;flex-direction:column;gap:20px;max-width:680px} +.link-fields{display:grid;grid-template-columns:1fr 1fr;gap:12px;align-items:start} +.link-fields > .full{grid-column:1/-1} +@media(max-width:600px){.link-fields{grid-template-columns:1fr}} .settings-stack-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(320px,1fr));gap:20px;max-width:1200px} .settings-stack-grid > .settings-card{max-width:none;height:100%} .settings-stack-grid > .settings-card.wide{grid-column:1/-1} @@ -352,3 +355,29 @@ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;b .ai-btn:hover{color:var(--accent);background:rgba(37,99,235,.1)} .ai-btn:disabled{opacity:.5;cursor:not-allowed} #aiSuggestion{font-size:.9rem;line-height:1.5} + +/* --- AI auto-check --- */ +.ai-badge{display:inline-block;font-size:.68rem;font-weight:600;padding:2px 7px;border-radius:999px;margin-left:6px;vertical-align:middle;background:rgba(37,99,235,.1);color:var(--accent);white-space:nowrap} +.ai-badge-done{background:rgba(22,163,74,.12);color:#16a34a} +.ai-badge-error{background:rgba(239,68,68,.12);color:#ef4444} +.ai-badge-skipped,.ai-badge-reverted{background:rgba(148,163,184,.18);color:var(--muted)} +.ai-status{margin-top:6px;font-size:.78rem;color:var(--muted)} +.ai-queue{margin-top:8px;font-size:.78rem;color:var(--muted)} + +/* --- AI worker monitor --- */ +.worker-dot{width:9px;height:9px;border-radius:50%;display:inline-block;flex-shrink:0;background:#94a3b8} +.worker-dot.on{background:#16a34a;animation:wpulse 1.6s infinite} +.worker-dot.busy{background:#f59e0b;animation:wpulse 1.2s infinite} +.worker-dot.err{background:#ef4444} +@keyframes wpulse{0%{box-shadow:0 0 0 0 rgba(22,163,74,.45)}70%{box-shadow:0 0 0 7px rgba(22,163,74,0)}100%{box-shadow:0 0 0 0 rgba(22,163,74,0)}} +.worker-state{display:inline-flex;align-items:center;gap:8px;font-size:.85rem;font-weight:600} +.worker-counts{display:flex;flex-wrap:wrap;gap:10px;margin-top:16px} +.worker-counts .chip{background:var(--card);border:1px solid var(--border);border-radius:999px;padding:7px 14px;font-size:.8rem;color:var(--text);display:inline-flex;align-items:center;gap:6px} +.worker-counts .chip b{font-weight:700} +.worker-counts .chip.pending b{color:#f59e0b} +.worker-counts .chip.done b{color:#16a34a} +.worker-counts .chip.error b{color:#ef4444} +.worker-counts .chip.skipped b{color:var(--muted)} +.worker-counts .chip.reverted b{color:var(--accent)} + + diff --git a/public/admin.js b/public/admin.js index 003cca1..932dcb9 100644 --- a/public/admin.js +++ b/public/admin.js @@ -53,6 +53,7 @@ function buildSidebar(active) { { page: 'branches', label: 'Филиалы' }, { page: 'users', label: 'Пользователи' }, { page: 'audit', label: 'Аудит' }, + { page: 'worker', label: 'Воркер ИИ' }, { page: 'settings', label: 'Настройки' }, ]; const nav = [...base, ...(isAdmin() ? adminNav : [])]; diff --git a/public/dashboard.html b/public/dashboard.html index 614f937..7ce8cf8 100644 --- a/public/dashboard.html +++ b/public/dashboard.html @@ -44,6 +44,7 @@ 👥Группы 🔗Ссылки 🗑️Корзина + 🤖Воркер ИИ 🛡️Бэкап diff --git a/public/journal.html b/public/journal.html index be7c796..fe26598 100644 --- a/public/journal.html +++ b/public/journal.html @@ -58,6 +58,14 @@ +
+ + +Инструкция для нейросети при исправлении ошибок в записи «Что делала(а)».
+Отслеживание автоматической проверки сообщений записей
+Записи, ожидающие проверки моделью. Обновляются автоматически.
+| ID | Ученик | Группа | Создана |
|---|
| Проверено | Ученик | Группа | Статус | Изменения |
|---|
Записи, которые не удалось обработать после всех попыток. Нажмите «Перепроверить ошибки», чтобы запустить их снова.
+| Время | ID | Ученик | Ошибка |
|---|