v1.0.176: XSS ×5 fix + syncDB await + escHtml(null)

This commit is contained in:
2026-06-24 18:03:12 +04:00
parent 666f85b743
commit 780e52d072
2 changed files with 24 additions and 21 deletions
+23 -21
View File
@@ -21,13 +21,15 @@ var batchId = crypto.randomUUID(); // классификация: привяз
})();
// Синхронизация БД с таблицей — удалить всё, чего нет в fileQueue
function syncDB() {
async function syncDB() {
var keepIds = fileQueue.map(function(f) { return f.doc_id; }).filter(Boolean);
fetch(VM_API + '/api/sync', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({keep_ids: keepIds})
}).catch(function(){});
try {
await fetch(VM_API + '/api/sync', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({keep_ids: keepIds})
});
} catch(e) { /* сервер недоступен — не критично */ }
}
// ── Pipeline stepper ─────────────────────────────────────────
function stepDone(id) {
@@ -58,7 +60,7 @@ function renderTable() {
} else {
fileTable.innerHTML = fileQueue.map(function(f, i) {
return '<tr id="row_' + i + '">' +
'<td class="name-cell">' + f.name + '</td>' +
'<td class="name-cell">' + escHtml(f.name) + '</td>' +
'<td style="font-size:12px;color:var(--muted);">' + formatDate(f.lastModified) + '</td>' +
'<td class="num-cell" style="font-size:12px;color:var(--muted);">' + formatSize(f.size) + '</td>' +
'<td class="status-cell">' + (f.status || '') + '</td>' +
@@ -318,18 +320,18 @@ async function loadGroups() {
var docsHtml = g.documents.map(function(d) {
var reason = '';
if (d.parent_number) {
reason = ' — нет базового договора №' + d.parent_number;
reason = ' — нет базового договора №' + escHtml(d.parent_number);
} else if (d.classify_status === 'failed') {
reason = ' — ошибка классификации: ' + (d.error_message || '?');
reason = ' — ошибка классификации: ' + escHtml(d.error_message || '?');
} else if (!d.doc_type || d.doc_type === 'other') {
reason = ' — тип не определён';
} else {
reason = ' — нет matching-договора';
}
return '<div style="padding:2px 0;">' +
'<span style="color:var(--muted);">[' + (d.doc_type || '?') + ']</span> ' +
d.filename +
(d.doc_date ? ' <span style="color:var(--muted);">' + d.doc_date + '</span>' : '') +
'<span style="color:var(--muted);">[' + escHtml(d.doc_type || '?') + ']</span> ' +
escHtml(d.filename) +
(d.doc_date ? ' <span style="color:var(--muted);">' + escHtml(d.doc_date) + '</span>' : '') +
'<span style="color:var(--destructive);font-size:11px;">' + reason + '</span>' +
'</div>';
}).join('');
@@ -337,14 +339,14 @@ async function loadGroups() {
'<div style="font-size:12px;">' + docsHtml + '</div>' +
'<div style="font-size:11px;color:var(--muted);margin-top:4px;">Загрузите недостающие базовые договоры для этих номеров.</div>';
} else {
card.innerHTML = '<div style="font-weight:600;margin-bottom:6px;">📄 Договор №' + (g.contract_number || '?') + ' — ' + (g.counterparty || 'контрагент не определён') + '</div>' +
card.innerHTML = '<div style="font-weight:600;margin-bottom:6px;">📄 Договор №' + escHtml(g.contract_number || '?') + ' — ' + escHtml(g.counterparty || 'контрагент не определён') + '</div>' +
'<div style="font-size:12px;">' +
g.documents.map(function(d) {
var typeLabel = {contract: 'договор', supplement: 'допсоглашение', specification: 'спецификация'}[d.doc_type] || d.doc_type;
return '<div style="padding:2px 0;">' +
'<span style="color:var(--muted);">' + (typeLabel || '?') + '</span> ' +
d.filename +
(d.doc_date ? ' <span style="color:var(--muted);">' + d.doc_date + '</span>' : '') +
'<span style="color:var(--muted);">' + escHtml(typeLabel || '?') + '</span> ' +
escHtml(d.filename) +
(d.doc_date ? ' <span style="color:var(--muted);">' + escHtml(d.doc_date) + '</span>' : '') +
'</div>';
}).join('') + '</div>';
card.innerHTML += '<button class="btn btn-primary" style="margin-top:6px;font-size:12px;height:28px;" onclick="runCompareForGroup(' + gi + ')">▶ Сравнить эту группу</button>';
@@ -723,11 +725,11 @@ async function showText(i) {
// ── Результаты классификации ──────────────────────────
if (qData.classify_status === 'classified' || qData.classify_raw) {
rightHtml += '<div style="margin-top:12px;font-weight:600;font-size:12px;border-top:1px solid var(--brand-gray);padding-top:8px;">🏷️ Классификация LLM</div>';
if (qData.doc_type) rightHtml += '<div class="kv"><span class="k">Тип</span><span class="v">' + qData.doc_type + '</span></div>';
if (qData.own_number) rightHtml += '<div class="kv"><span class="k">Номер</span><span class="v">' + qData.own_number + '</span></div>';
if (qData.parent_number) rightHtml += '<div class="kv"><span class="k">Родительский</span><span class="v">' + qData.parent_number + '</span></div>';
if (qData.doc_date) rightHtml += '<div class="kv"><span class="k">Дата</span><span class="v">' + qData.doc_date + '</span></div>';
if (qData.counterparty) rightHtml += '<div class="kv"><span class="k">Контрагент</span><span class="v">' + qData.counterparty + '</span></div>';
if (qData.doc_type) rightHtml += '<div class="kv"><span class="k">Тип</span><span class="v">' + escHtml(qData.doc_type) + '</span></div>';
if (qData.own_number) rightHtml += '<div class="kv"><span class="k">Номер</span><span class="v">' + escHtml(qData.own_number) + '</span></div>';
if (qData.parent_number) rightHtml += '<div class="kv"><span class="k">Родительский</span><span class="v">' + escHtml(qData.parent_number) + '</span></div>';
if (qData.doc_date) rightHtml += '<div class="kv"><span class="k">Дата</span><span class="v">' + escHtml(qData.doc_date) + '</span></div>';
if (qData.counterparty) rightHtml += '<div class="kv"><span class="k">Контрагент</span><span class="v">' + escHtml(qData.counterparty) + '</span></div>';
if (qData.classify_raw) {
var rawId = 'raw_' + docId.replace(/-/g,'');
rightHtml += '<details style="margin-top:8px;"><summary style="cursor:pointer;font-size:11px;color:var(--brand-primary);">Сырой ответ LLM</summary>';
+1
View File
@@ -86,6 +86,7 @@ window.closeAbout = closeAbout;
* Используется в истории промптов.
*/
function escHtml(s) {
if (s == null) return '';
var d = document.createElement('div');
d.textContent = s;
return d.innerHTML;