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 @@

-
Сверка договоров — LLM AI-driven Event Sourcing v1.0.168 — Lucee
+
Сверка договоров — LLM AI-driven Event Sourcing v1.0.169 — Lucee