From 4db02d75bc55c6fb6869ce8cd3ae8efbc20dca89 Mon Sep 17 00:00:00 2001 From: dev Date: Fri, 11 Sep 2026 16:26:30 +0300 Subject: [PATCH] Add AI-check details modal on worker page and raise backup upload limit --- .env.example | 2 ++ docker-compose.yml | 1 + public/admin.css | 6 ++++++ public/js/worker.js | 22 +++++++++++++++++++++- public/worker.html | 18 +++++++++++++++++- server.js | 5 ++++- 6 files changed, 51 insertions(+), 3 deletions(-) diff --git a/.env.example b/.env.example index 0437563..21f156e 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,8 @@ ADMIN_USERNAME=admin ADMIN_PASSWORD=сложный-пароль-админки DB_PASSWORD=случайная-длинная-строка +# Лимит размера загружаемого на восстановление бэкапа, МБ (по умолчанию 500) +BACKUP_UPLOAD_LIMIT_MB=500 # Hugging Face token (нужен для закрытых/приватных репозиториев моделей) HUGGINGFACE_TOKEN= diff --git a/docker-compose.yml b/docker-compose.yml index 8bb4dad..61a80c6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -29,6 +29,7 @@ services: DATABASE_URL: postgres://app:${DB_PASSWORD}@db:5432/whereldo ADMIN_PASSWORD: ${ADMIN_PASSWORD} ADMIN_USERNAME: ${ADMIN_USERNAME:-admin} + BACKUP_UPLOAD_LIMIT_MB: ${BACKUP_UPLOAD_LIMIT_MB:-500} AI_MODEL: ${AI_MODEL:-qwen2.5-1.5b-instruct-q4_k_m.gguf} AI_PROMPT: ${AI_PROMPT:-} NODE_ENV: production diff --git a/public/admin.css b/public/admin.css index feff405..b3e3575 100644 --- a/public/admin.css +++ b/public/admin.css @@ -373,6 +373,12 @@ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;b .ai-badge-skipped,.ai-badge-reverted{background:rgba(148,163,184,.18);color:var(--muted)} .ai-badge-ic.ai-badge-processing .lucide-loader{animation:spin 1s linear infinite} @keyframes spin{to{transform:rotate(360deg)}} +.ai-details{background:none;border:none;padding:0;margin:0;color:var(--accent);font-size:.8rem;cursor:pointer;white-space:nowrap} +.ai-details:hover{text-decoration:underline} +.ai-detail-block{margin-top:4px} +.ai-detail-block + .ai-detail-block{margin-top:14px} +.ai-detail-label{font-size:.75rem;color:var(--muted);margin-bottom:6px;font-weight:600} +.ai-detail-block pre{margin:0;background:var(--bg);border:1px solid var(--border);border-radius:8px;padding:12px;white-space:pre-wrap;word-break:break-word;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:.78rem;line-height:1.45;color:var(--text);max-height:34vh;overflow:auto} .ai-status{margin-top:6px;font-size:.78rem;color:var(--muted)} .ai-queue{margin-top:8px;font-size:.78rem;color:var(--muted)} diff --git a/public/js/worker.js b/public/js/worker.js index 0a74edb..fa50f6c 100644 --- a/public/js/worker.js +++ b/public/js/worker.js @@ -17,6 +17,7 @@ const STATUS_BADGE = { let state = null; let refreshTimer = null; let busy = false; +let recentRows = []; function fmtDur(ms) { if (ms === null || ms === undefined || ms < 0) return '—'; @@ -141,13 +142,15 @@ function renderRecent() { const empty = document.getElementById('recentEmpty'); if (!rows.length) { tbody.innerHTML = ''; empty.style.display = 'block'; return; } empty.style.display = 'none'; - tbody.innerHTML = rows.map(r => ` + recentRows = rows; + tbody.innerHTML = rows.map((r, i) => ` ${fmtTime(r.ai_checked_at)} ${esc(r.student_name)} ${esc(r.group_name || '')} ${statusBadge(r.ai_status)} ${r.changed ? '✏️ исправлен' : '—'} + `).join(''); } @@ -225,6 +228,23 @@ document.getElementById('requeueBtn').addEventListener('click', requeueFailed); document.getElementById('recentFilter').addEventListener('change', renderRecent); document.getElementById('autoRefresh').addEventListener('change', e => setAutoRefresh(e.target.checked)); +document.getElementById('recentTable').addEventListener('click', e => { + const btn = e.target.closest('.ai-details'); + if (!btn) return; + const r = recentRows[parseInt(btn.dataset.aiIdx, 10)]; + if (!r) return; + document.getElementById('aiDetailTitle').textContent = `ИИ-проверка #${r.id} — ${r.student_name}`; + document.getElementById('aiDetailInput').textContent = r.description_original || '—'; + document.getElementById('aiDetailOutput').textContent = r.description_ai || '—'; + document.getElementById('aiDetailModal').classList.add('open'); +}); +document.getElementById('aiDetailCloseBtn').addEventListener('click', () => { + document.getElementById('aiDetailModal').classList.remove('open'); +}); +document.getElementById('aiDetailModal').addEventListener('click', e => { + if (e.target === e.currentTarget) e.currentTarget.classList.remove('open'); +}); + (async () => { if (await requireAdminPage()) { buildSidebar(document.body.dataset.page); diff --git a/public/worker.html b/public/worker.html index 0a86d82..27910d5 100644 --- a/public/worker.html +++ b/public/worker.html @@ -69,7 +69,7 @@
- +
ПровереноУченикГруппаСтатусИзменения
ПровереноУченикГруппаСтатусИзмененияДетали
@@ -95,6 +95,22 @@
+
diff --git a/server.js b/server.js index 98234fd..4e222ca 100644 --- a/server.js +++ b/server.js @@ -749,6 +749,8 @@ async function aiCorrectText(text) { } } +const BACKUP_UPLOAD_LIMIT_MB = parseInt(process.env.BACKUP_UPLOAD_LIMIT_MB || '500', 10); + const uploadBackup = multer({ storage: multer.diskStorage({ destination: (_, __, cb) => { @@ -760,7 +762,7 @@ const uploadBackup = multer({ cb(null, `backup-${Date.now()}${ext}`); }, }), - limits: { fileSize: 50 * 1024 * 1024 }, + limits: { fileSize: BACKUP_UPLOAD_LIMIT_MB * 1024 * 1024 }, }); const SAFE_NAME = /^[\w,.()-]+$/; @@ -2633,6 +2635,7 @@ app.get('/api/ai/status', requireAdmin, async (_, res) => { ), pool.query( `SELECT e.id, e.student_name, e.ai_status, e.ai_checked_at, e.ai_error, g.name AS group_name, + e.description_original, e.description_ai, (e.description_ai IS NOT NULL AND e.description_original IS NOT NULL AND e.description_ai <> e.description_original) AS changed FROM entries e JOIN groups g ON g.id = e.group_id WHERE e.ai_checked_at IS NOT NULL AND e.deleted_at IS NULL