diff --git a/deploy/app.js b/deploy/app.js index 2e9d7f4..c6102e1 100644 --- a/deploy/app.js +++ b/deploy/app.js @@ -583,12 +583,12 @@ function closePromptModal(e) { } function loadPrompt(role) { - fetch('/prompt.cfm?action=get_active&role=' + role) + fetch(VM_API + '/api/prompts?role=' + role) .then(function(r) { return r.json(); }) .then(function(d) { - if (d.OK) { - promptEditor.value = d.BODY || ''; - activePromptId[role] = d.ID || ''; + if (d.ok) { + promptEditor.value = d.body || ''; + activePromptId[role] = d.id || ''; loadHistory(role); } }) @@ -596,52 +596,50 @@ function loadPrompt(role) { } function loadHistory(role) { - fetch('/prompt.cfm?action=list&role=' + role) + fetch(VM_API + '/api/prompts/list?role=' + role) .then(function(r) { return r.json(); }) .then(function(d) { - if (!d.OK || !d.VERSIONS || !d.VERSIONS.length) { + if (!d.ok || !d.versions || !d.versions.length) { promptHistory.innerHTML = '
нет версий
'; return; } - promptHistory.innerHTML = d.VERSIONS.map(function(v) { - var isActive = v.IS_ACTIVE ? '✓ ' : ''; - var style = v.IS_ACTIVE ? 'font-weight:600;' : ''; + promptHistory.innerHTML = d.versions.map(function(v) { + var isActive = v.is_active ? '✓ ' : ''; + var style = v.is_active ? 'font-weight:600;' : ''; return '
' - + '' + isActive + escHtml(v.NAME) + ' ' + fmtDate(v.CREATED_AT) + '' - + (!v.IS_ACTIVE ? '' : '') - + (!v.IS_ACTIVE ? '' : '') + + '' + isActive + escHtml(v.name) + ' ' + fmtDate(v.created_at) + '' + + (!v.is_active ? '' : '') + + (!v.is_active ? '' : '') + '
'; }).join(''); }) .catch(function(e) { console.error('loadHistory:', e); }); } -// escHtml/fmtDate — вынесены в app_utils.js - function activatePrompt(id) { - fetch('/prompt.cfm?action=activate', { + fetch(VM_API + '/api/prompts/activate', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({id: id}) }) .then(function(r) { return r.json(); }) .then(function(d) { - if (d.OK) { loadPrompt(promptRole); promptStatus.textContent = '✓ активирован'; } - else { promptStatus.textContent = '✕ ' + (d.ERROR || 'ошибка'); } + if (d.ok) { loadPrompt(promptRole); promptStatus.textContent = '✓ активирован'; } + else { promptStatus.textContent = '✕ ' + (d.error || 'ошибка'); } }); } function deletePrompt(id) { if (!confirm('Удалить эту версию промпта?')) return; - fetch('/prompt.cfm?action=delete', { + fetch(VM_API + '/api/prompts/delete', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({id: id}) }) .then(function(r) { return r.json(); }) .then(function(d) { - if (d.OK) { loadHistory(promptRole); promptStatus.textContent = '✓ удалено'; } - else { promptStatus.textContent = '✕ ' + (d.ERROR || 'ошибка'); } + if (d.ok) { loadHistory(promptRole); promptStatus.textContent = '✓ удалено'; } + else { promptStatus.textContent = '✕ ' + (d.error || 'ошибка'); } }); } @@ -649,23 +647,22 @@ document.getElementById('promptSave').addEventListener('click', function() { var body = promptEditor.value.trim(); if (!body) { promptStatus.textContent = '✕ тело промпта пустое'; return; } var name = promptName.value.trim() || ('v' + new Date().toISOString().replace(/[:.]/g,'-').substring(0,16)); - fetch('/prompt.cfm?action=save', { + fetch(VM_API + '/api/prompts/save', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ role: promptRole, name: name, body: body, - parent_id: activePromptId[promptRole] || '', notes: promptNotes.value.trim(), is_active: true }) }) .then(function(r) { return r.json(); }) .then(function(d) { - if (d.OK) { + if (d.ok) { promptStatus.textContent = '✓ сохранено (новая версия)'; promptName.value = ''; promptNotes.value = ''; loadPrompt(promptRole); } else { - promptStatus.textContent = '✕ ' + (d.ERROR || 'ошибка'); + promptStatus.textContent = '✕ ' + (d.error || 'ошибка'); } }); }); diff --git a/deploy/convert_server.py b/deploy/convert_server.py index d0db68a..386a8c3 100755 --- a/deploy/convert_server.py +++ b/deploy/convert_server.py @@ -48,6 +48,10 @@ class Handler(BaseHTTPRequestHandler): self._handle_api_groups(parsed) elif parsed.path == "/api/batch-progress": self._handle_api_batch_progress(parsed) + elif parsed.path == "/api/prompts": + self._handle_api_prompts_get(parsed) + elif parsed.path == "/api/prompts/list": + self._handle_api_prompts_list(parsed) elif parsed.path.startswith("/api/documents/"): self._handle_api_document(parsed) else: @@ -68,6 +72,12 @@ class Handler(BaseHTTPRequestHandler): self._handle_apply_groups() elif self.path == "/api/cleanup": self._handle_cleanup() + elif self.path == "/api/prompts/save": + self._handle_api_prompts_save() + elif self.path == "/api/prompts/activate": + self._handle_api_prompts_activate() + elif self.path == "/api/prompts/delete": + self._handle_api_prompts_delete() else: self.send_error(404) @@ -193,6 +203,83 @@ class Handler(BaseHTTPRequestHandler): execute("DELETE FROM contracts") self._json({"ok": True, "message": "all data cleaned"}) + # ── GET /api/cleanup ─────────────────────────────────────────────────── + + def _handle_cleanup(self): + """Полная очистка всех данных (кроме prompts).""" + from db.connection import execute + execute("DELETE FROM spec_current") + execute("DELETE FROM spec_events") + execute("DELETE FROM supplements") + execute("DELETE FROM upload_chunks") + execute("DELETE FROM documents") + execute("DELETE FROM contracts") + self._json({"ok": True, "message": "all data cleaned"}) + + # ── Prompts CRUD ────────────────────────────────────────────────────── + + def _handle_api_prompts_get(self, parsed): + from db import prompts as db_p + params = parse_qs(parsed.query) + role = params.get("role", [None])[0] + if not role: + self._json({"ok": False, "error": "role required"}, 400) + return + p = db_p.get_active(role) + if p: + self._json({"ok": True, "id": p["id"], "role": p["role"], "name": p["name"], + "body": p["body"], "is_active": p["is_active"], "notes": p.get("notes"), + "created_at": str(p.get("created_at", ""))}) + else: + self._json({"ok": False, "error": "not found"}, 404) + + def _handle_api_prompts_list(self, parsed): + from db import prompts as db_p + params = parse_qs(parsed.query) + role = params.get("role", [None])[0] + if not role: + self._json({"ok": False, "error": "role required"}, 400) + return + versions = db_p.list_by_role(role) + self._json({"ok": True, "versions": versions}) + + def _handle_api_prompts_save(self): + from db import prompts as db_p + length = int(self.headers.get("Content-Length", 0)) + body = json.loads(self.rfile.read(length)) + role = body.get("role", "") + name = body.get("name", "v" + __import__("datetime").datetime.now().isoformat()[:16]) + prompt_body = body.get("body", "") + notes = body.get("notes", "") + is_active = body.get("is_active", True) + if not role or not prompt_body: + self._json({"ok": False, "error": "role and body required"}, 400) + return + p = db_p.save_new_version(role, name, prompt_body, notes, is_active) + self._json({"ok": True, "id": p["id"]}) + + def _handle_api_prompts_activate(self): + from db import prompts as db_p + length = int(self.headers.get("Content-Length", 0)) + body = json.loads(self.rfile.read(length)) + pid = body.get("id", "") + if not pid: + self._json({"ok": False, "error": "id required"}, 400) + return + ok = db_p.activate(pid) + self._json({"ok": ok}) + + def _handle_api_prompts_delete(self): + from db import prompts as db_p + length = int(self.headers.get("Content-Length", 0)) + body = json.loads(self.rfile.read(length)) + pid = body.get("id", "") + if not pid: + self._json({"ok": False, "error": "id required"}, 400) + return + ok = db_p.delete_prompt(pid) + self._json({"ok": ok}) + # ── /app.js (static) ─────────────────────────────────────────────────── def _handle_app_js(self): diff --git a/deploy/db/prompts.py b/deploy/db/prompts.py index 5c9c77f..d693730 100644 --- a/deploy/db/prompts.py +++ b/deploy/db/prompts.py @@ -38,6 +38,47 @@ def seed_defaults(): _ensure_classify_prompt() +def list_by_role(role): + """List all versions for a role, newest first.""" + return query( + "SELECT id, role, name, is_active, created_by, notes, created_at FROM prompts WHERE role=%s ORDER BY created_at DESC", + (role,), + ) + + +def save_new_version(role, name, body, notes="", is_active=True): + """Save new prompt version. Deactivates all others for this role, inserts new one.""" + if is_active: + execute("UPDATE prompts SET is_active=false WHERE role=%s", (role,)) + return execute_returning( + """INSERT INTO prompts (role, name, body, is_active, notes) + VALUES (%s, %s, %s, %s, %s) RETURNING *""", + (role, name, body, is_active, notes), + ) + + +def activate(prompt_id): + """Activate a prompt version (deactivates others for same role).""" + row = query("SELECT role FROM prompts WHERE id=%s", (prompt_id,)) + if not row: + return False + role = row[0]["role"] + execute("UPDATE prompts SET is_active=false WHERE role=%s", (role,)) + execute("UPDATE prompts SET is_active=true WHERE id=%s", (prompt_id,)) + return True + + +def delete_prompt(prompt_id): + """Delete a prompt version (cannot delete active one).""" + row = query("SELECT is_active FROM prompts WHERE id=%s", (prompt_id,)) + if not row: + return False + if row[0]["is_active"]: + return False # cannot delete active + execute("DELETE FROM prompts WHERE id=%s", (prompt_id,)) + return True + + def _ensure_classify_prompt(): """Ensure classify prompt exists (idempotent).""" existing = query("SELECT id FROM prompts WHERE role = 'classify' AND is_active = true LIMIT 1") diff --git a/index.cfm b/index.cfm index deb5a83..cb85e0e 100644 --- a/index.cfm +++ b/index.cfm @@ -75,7 +75,7 @@
Nubes - Сверка договоров — LLM AI-driven Event Sourcing v1.0.168 — Lucee + Сверка договоров — LLM AI-driven Event Sourcing v1.0.169 — Lucee