diff --git a/deploy/app.js b/deploy/app.js
index 3d5aa01..bec846d 100644
--- a/deploy/app.js
+++ b/deploy/app.js
@@ -72,6 +72,29 @@ function resetStepper(fromId) {
// Утилиты (formatSize, formatDate, moveUp, moveDown, escHtml, fmtDate) —
// вынесены в app_utils.js для облегчения анализа и отладки.
+/**
+ * statusToHTML(status) — Чистая функция: структура → HTML (Фаза 1).
+ *
+ * Вход: { kind, pct?, text?, count?, elapsed? } — ни одного HTML-тега.
+ * kind = 'uploading' | 'uploaded' | 'unzipping' | 'parsing' | 'parsed' | 'error' | ''
+ * Выход: безопасная HTML-строка (статусы не содержат пользовательских данных).
+ *
+ * ПАТТЕРН (decoupling-final-plan.md): отделяем данные от представления.
+ * state хранит чистые данные, statusToHTML — чистая функция рендеринга.
+ */
+function statusToHTML(st) {
+ if (!st || !st.kind) return '';
+ switch (st.kind) {
+ case 'uploading': return '↑ ' + (st.pct || 0) + '%';
+ case 'uploaded': return '✓';
+ case 'unzipping': return '⏳ распаковка...';
+ case 'parsing': return '⏳ парсинг...';
+ case 'parsed': return '✓ ' + (st.count || 0) + ' эл.' + (st.elapsed ? ' (' + st.elapsed + 'с)' : '') + '';
+ case 'error': return '✗ ' + (st.text || 'Неизвестная ошибка') + '';
+ default: return '';
+ }
+}
+
function renderTable() {
if (state.files.length === 0) {
fileTable.innerHTML = '
| Нет файлов — выберите .docx / .pdf |
';
@@ -83,7 +106,7 @@ function renderTable() {
escHtml(f.name) + '' +
'' + formatDate(f.lastModified) + ' | ' +
'' + formatSize(f.size) + ' | ' +
- '' + (f.status || '') + ' | ' +
+ '' + statusToHTML(f.status) + ' | ' +
' | ' +
' | ' +
'' +
@@ -182,7 +205,7 @@ fileInput.addEventListener('change', async function() {
if (isZip) {
// ZIP: показать временную строку, распаковать, убрать
- var zipEntry = { name: f.name, lastModified: f.lastModified, size: f.size, status: '⏳ распаковка...' };
+ var zipEntry = { name: f.name, lastModified: f.lastModified, size: f.size, status: { kind: 'unzipping' } };
state.files.push(zipEntry);
var zipIdx = state.files.length - 1;
render(state);
@@ -220,7 +243,7 @@ fileInput.addEventListener('change', async function() {
var zEntry = {
name: zf.filename, size: zf.size,
doc_id: zf.doc_id, uploaded: true,
- status: '⏳ парсинг...'
+ status: { kind: 'parsing' }
};
state.files.push(zEntry);
var zIdx = state.files.length - 1;
@@ -231,23 +254,23 @@ fileInput.addEventListener('change', async function() {
var zpr = zf.parsed;
if (zpr && zpr.status === 'parsed') {
state.files[zIdx].parsed = true;
- state.files[zIdx].status = '✓ ' + zpr.element_count + ' эл.';
+ state.files[zIdx].status = { kind: 'parsed', count: zpr.element_count };
} else if (zpr && zpr.status === 'error') {
- state.files[zIdx].status = '✗ ' + (zpr.error || 'ошибка парсинга') + '';
+ state.files[zIdx].status = { kind: 'error', text: zpr.error || 'ошибка парсинга' };
} else {
- state.files[zIdx].status = '✗ Неизвестная ошибка';
+ state.files[zIdx].status = { kind: 'error', text: 'Неизвестная ошибка' };
}
render(state);
}
} catch(ze) {
- state.files[zipIdx].status = '✗ ZIP: ' + ze.message + '';
+ state.files[zipIdx].status = { kind: 'error', text: 'ZIP: ' + ze.message };
render(state);
}
continue; // ZIP обработан, переходим к следующему файлу
}
// Обычный файл (не ZIP)
- var entry = { name: f.name, lastModified: f.lastModified, size: f.size, file: f, status: '↑ 0%' };
+ var entry = { name: f.name, lastModified: f.lastModified, size: f.size, file: f, status: { kind: 'uploading', pct: 0 } };
var existingIdx = -1;
for (var j = 0; j < state.files.length; j++) {
if (state.files[j].name === f.name) { existingIdx = j; break; }
@@ -264,13 +287,13 @@ fileInput.addEventListener('change', async function() {
try {
var resp = await uploadFile(f, function(pct) {
- state.files[rowIdx].status = '↑ ' + pct + '%';
+ state.files[rowIdx].status = { kind: 'uploading', pct: pct };
render(state);
});
// Python API returns lowercase keys (doc_id, contract_id, parsed)
if (resp && resp.contract_id) state.contractId = resp.contract_id;
state.files[rowIdx].doc_id = resp.doc_id;
- state.files[rowIdx].status = '✓';
+ state.files[rowIdx].status = { kind: 'uploaded' };
state.files[rowIdx].uploaded = true;
// Авто-парсинг: VM Python парсит ВСЁ (PDF + DOCX + DOC)
@@ -280,15 +303,15 @@ fileInput.addEventListener('change', async function() {
var elapsed = ((Date.now() - t0) / 1000).toFixed(1);
state.files[rowIdx].parsed = true;
state.files[rowIdx].parseInfo = pr;
- state.files[rowIdx].status = '✓ ' + pr.element_count + ' эл. (' + elapsed + 'с)';
+ state.files[rowIdx].status = { kind: 'parsed', count: pr.element_count, elapsed: elapsed };
} else if (pr && pr.status === 'error') {
- state.files[rowIdx].status = '✗ ' + (pr.error || 'ошибка парсинга') + '';
+ state.files[rowIdx].status = { kind: 'error', text: pr.error || 'ошибка парсинга' };
state.files[rowIdx].parseInfo = pr;
} else {
- state.files[rowIdx].status = '✗ Неизвестная ошибка';
+ state.files[rowIdx].status = { kind: 'error', text: 'Неизвестная ошибка' };
}
} catch(err) {
- state.files[rowIdx].status = '✗ ' + err.message + '';
+ state.files[rowIdx].status = { kind: 'error', text: err.message };
}
render(state);
}