diff --git a/site/static/app.js b/site/static/app.js
index 08c2b63..36a8b0a 100644
--- a/site/static/app.js
+++ b/site/static/app.js
@@ -180,7 +180,10 @@ async function runClassify() {
clearInterval(timer);
clearInterval(pollTimer);
classifyDone = true;
- btn.innerHTML = '✓ ' + data.done + '/' + data.total + ' классифицировано (' + Math.round((Date.now() - start) / 1000) + 'с)';
+ var summary = data.summary || (data.done + '/' + data.total + ' классифицировано');
+ if (data.garbage) summary += ' | 🗑️ ' + data.garbage + ' мусор';
+ if (data.types) summary += ' | ' + Object.entries(data.types).map(function(e){return e[1]+'×'+e[0];}).join(', ');
+ btn.innerHTML = '✓ ' + summary + ' (' + Math.round((Date.now() - start) / 1000) + 'с)';
stepDone('stepClassify');
stepActive('stepGroups');
loadGroupsAction();
diff --git a/site/static/files.js b/site/static/files.js
index a025050..2f050f2 100644
--- a/site/static/files.js
+++ b/site/static/files.js
@@ -35,7 +35,10 @@ function statusToHTML(st) {
case 'unzipping': return '⏳ распаковка...';
case 'parsing': return '⏳ парсинг...';
case 'parsed': return '✓ ' + (st.count || 0) + ' эл.' + (st.elapsed ? ' (' + st.elapsed + 'с)' : '') + '';
- case 'error': return '✗ ' + (st.text || 'Неизвестная ошибка') + '';
+ case 'error': return '✗ ' + escHtml(st.text || 'Неизвестная ошибка') + '';
+ case 'duplicate': return '⤿ дубликат';
+ case 'warning': return '⚠ ' + escHtml(st.text || 'пустой') + '';
+ case 'no_format': return '✗ формат не поддерживается';
default: return '';
}
}
@@ -367,6 +370,16 @@ async function addZipFile(file) {
});
if (!zipResp.ok || !zipResp.files) throw new Error('unzip failed');
+ // Подтверждение: показать список файлов перед загрузкой
+ var fileList = zipResp.files.map(function(f) { return ' • ' + f.filename + ' (' + (f.size/1024).toFixed(1) + ' KB)'; }).join('\n');
+ if (!confirm('Архив «' + file.name + '» — ' + total + ' файлов:\n\n' + fileList + '\n\nЗагрузить эти файлы?')) {
+ // Отмена — убрать строку ZIP
+ var zipPos2 = state.files.indexOf(zipEntry);
+ if (zipPos2 >= 0) state.files.splice(zipPos2, 1);
+ render(state);
+ return;
+ }
+
// Шаг 2: обработать каждый файл из архива
// Временная строка ZIP остаётся в таблице — обновляем её статус
var total = zipResp.files.length;
@@ -443,7 +456,20 @@ async function addRegularFile(file, zipSource) {
// Бэкенд возвращает doc_id и contract_id (нижний регистр — Python keys)
if (resp && resp.contract_id) state.contractId = resp.contract_id;
state.files[rowIdx].doc_id = resp.doc_id;
- state.files[rowIdx].status = { kind: 'uploaded' };
+
+ // Дубликат?
+ if (resp && resp.duplicate_of) {
+ state.files[rowIdx].status = { kind: 'duplicate' };
+ render(state);
+ return;
+ }
+ // Warning от парсинга?
+ if (resp && resp.warning) {
+ state.files[rowIdx].status = { kind: 'warning', text: resp.warning };
+ render(state);
+ } else {
+ state.files[rowIdx].status = { kind: 'uploaded' };
+ }
state.files[rowIdx].uploaded = true;
// Авто-парсинг: бэкенд парсит всё (PDF + DOCX + DOC)