diff --git a/deploy/app.js b/deploy/app.js
index 9d1c79d..3d5aa01 100644
--- a/deploy/app.js
+++ b/deploy/app.js
@@ -9,9 +9,10 @@ var SITE_URL = ''; // same origin for api calls
var fileInput = document.getElementById('fileInput');
var fileTable = document.getElementById('fileTable');
-var fileQueue = [];
-var contractId = null;
-var batchId = crypto.randomUUID(); // классификация: привязка всех файлов сессии
+// state.files — теперь в state.js (Фаза 0: state + render)
+// state.contractId — теперь в state.js (Фаза 0: state + render)
+// state.batchId — теперь в state.js (Фаза 0: state + render)
+// (batchId = crypto.randomUUID() — уникальный ID сессии для классификации)
// Автоочистка старых записей при загрузке страницы
(async function cleanup() {
@@ -20,9 +21,26 @@ var batchId = crypto.randomUUID(); // классификация: привяз
} catch(e) { /* ignore */ }
})();
-// Синхронизация БД с таблицей — удалить всё, чего нет в fileQueue
+/**
+ * render(state) — Главная функция рендеринга (Фаза 0, decoupling-final-plan.md).
+ *
+ * ПАТТЕРН: action → мутация state → render(state)
+ *
+ * Фаза 0: вызывает существующий renderTable().
+ * renderTable читает state.files напрямую (state — глобальный объект).
+ * Фаза 1+: будет вызывать renderFiles(state), renderGroups(state), renderStepper(state) и т.д.
+ *
+ * ПРАВИЛО: любое изменение state ДОЛЖНО завершаться вызовом render(state).
+ * Никакой прямой манипуляции DOM в обход render().
+ * console.log(state) показывает ВСЁ состояние в любой момент.
+ */
+function render(state) {
+ renderTable();
+}
+
+// Синхронизация БД с таблицей — удалить всё, чего нет в state.files
async function syncDB() {
- var keepIds = fileQueue.map(function(f) { return f.doc_id; }).filter(Boolean);
+ var keepIds = state.files.map(function(f) { return f.doc_id; }).filter(Boolean);
try {
await fetch(VM_API + '/api/sync', {
method: 'POST',
@@ -55,10 +73,10 @@ function resetStepper(fromId) {
// вынесены в app_utils.js для облегчения анализа и отладки.
function renderTable() {
- if (fileQueue.length === 0) {
+ if (state.files.length === 0) {
fileTable.innerHTML = '
' +
'| ' +
(f.doc_id ? '▸' : '') +
@@ -88,7 +106,7 @@ window.toggleClassifyDetail = async function(i) {
return;
}
- var f = fileQueue[i];
+ var f = state.files[i];
if (!f || !f.doc_id) return;
detailRow.style.display = '';
@@ -149,10 +167,10 @@ fileInput.addEventListener('change', async function() {
// Удалить из очереди файлы, которых нет в новом выборе
var newNames = new Set(newFiles.map(function(f) { return f.name; }));
- fileQueue = fileQueue.filter(function(f) {
+ state.files = state.files.filter(function(f) {
return newNames.has(f.name);
});
- renderTable();
+ render(state);
// Разблокировать кнопку классификации — состав файлов изменился
showClassifyBtn();
// Сбросить прогресс пайплайна
@@ -165,9 +183,9 @@ fileInput.addEventListener('change', async function() {
if (isZip) {
// ZIP: показать временную строку, распаковать, убрать
var zipEntry = { name: f.name, lastModified: f.lastModified, size: f.size, status: '⏳ распаковка...' };
- fileQueue.push(zipEntry);
- var zipIdx = fileQueue.length - 1;
- renderTable();
+ state.files.push(zipEntry);
+ var zipIdx = state.files.length - 1;
+ render(state);
try {
var zipResp = await new Promise(function(resolve, reject) {
@@ -184,8 +202,8 @@ fileInput.addEventListener('change', async function() {
if (!zipData.ok || !zipData.files) throw new Error('unzip failed');
// Убрать строку ZIP из таблицы
- fileQueue.splice(zipIdx, 1);
- renderTable();
+ state.files.splice(zipIdx, 1);
+ render(state);
// Добавить файлы из архива, пропуская дубликаты
for (var zi = 0; zi < zipData.files.length; zi++) {
@@ -194,8 +212,8 @@ fileInput.addEventListener('change', async function() {
// Проверить дубликат по имени
var dup = -1;
- for (var dj = 0; dj < fileQueue.length; dj++) {
- if (fileQueue[dj].name === zf.filename) { dup = dj; break; }
+ for (var dj = 0; dj < state.files.length; dj++) {
+ if (state.files[dj].name === zf.filename) { dup = dj; break; }
}
if (dup >= 0) continue; // уже есть — не добавляем
@@ -204,26 +222,26 @@ fileInput.addEventListener('change', async function() {
doc_id: zf.doc_id, uploaded: true,
status: '⏳ парсинг...'
};
- fileQueue.push(zEntry);
- var zIdx = fileQueue.length - 1;
- if (!contractId && zf.contract_id) contractId = zf.contract_id;
- renderTable();
+ state.files.push(zEntry);
+ var zIdx = state.files.length - 1;
+ if (!state.contractId && zf.contract_id) state.contractId = zf.contract_id;
+ render(state);
// Авто-парсинг: VM Python парсит ВСЁ
var zpr = zf.parsed;
if (zpr && zpr.status === 'parsed') {
- fileQueue[zIdx].parsed = true;
- fileQueue[zIdx].status = '✓ ' + zpr.element_count + ' эл.';
+ state.files[zIdx].parsed = true;
+ state.files[zIdx].status = '✓ ' + zpr.element_count + ' эл.';
} else if (zpr && zpr.status === 'error') {
- fileQueue[zIdx].status = '✗ ' + (zpr.error || 'ошибка парсинга') + '';
+ state.files[zIdx].status = '✗ ' + (zpr.error || 'ошибка парсинга') + '';
} else {
- fileQueue[zIdx].status = '✗ Неизвестная ошибка';
+ state.files[zIdx].status = '✗ Неизвестная ошибка';
}
- renderTable();
+ render(state);
}
} catch(ze) {
- fileQueue[zipIdx].status = '✗ ZIP: ' + ze.message + '';
- renderTable();
+ state.files[zipIdx].status = '✗ ZIP: ' + ze.message + '';
+ render(state);
}
continue; // ZIP обработан, переходим к следующему файлу
}
@@ -231,55 +249,55 @@ fileInput.addEventListener('change', async function() {
// Обычный файл (не ZIP)
var entry = { name: f.name, lastModified: f.lastModified, size: f.size, file: f, status: '↑ 0%' };
var existingIdx = -1;
- for (var j = 0; j < fileQueue.length; j++) {
- if (fileQueue[j].name === f.name) { existingIdx = j; break; }
+ for (var j = 0; j < state.files.length; j++) {
+ if (state.files[j].name === f.name) { existingIdx = j; break; }
}
var rowIdx;
if (existingIdx >= 0) {
- fileQueue[existingIdx] = entry;
+ state.files[existingIdx] = entry;
rowIdx = existingIdx;
} else {
- fileQueue.push(entry);
- rowIdx = fileQueue.length - 1;
+ state.files.push(entry);
+ rowIdx = state.files.length - 1;
}
- renderTable();
+ render(state);
try {
var resp = await uploadFile(f, function(pct) {
- fileQueue[rowIdx].status = '↑ ' + pct + '%';
- renderTable();
+ state.files[rowIdx].status = '↑ ' + pct + '%';
+ render(state);
});
// Python API returns lowercase keys (doc_id, contract_id, parsed)
- if (resp && resp.contract_id) contractId = resp.contract_id;
- fileQueue[rowIdx].doc_id = resp.doc_id;
- fileQueue[rowIdx].status = '✓';
- fileQueue[rowIdx].uploaded = true;
+ 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].uploaded = true;
// Авто-парсинг: VM Python парсит ВСЁ (PDF + DOCX + DOC)
var t0 = Date.now();
var pr = resp.parsed;
if (pr && pr.status === 'parsed') {
var elapsed = ((Date.now() - t0) / 1000).toFixed(1);
- fileQueue[rowIdx].parsed = true;
- fileQueue[rowIdx].parseInfo = pr;
- fileQueue[rowIdx].status = '✓ ' + pr.element_count + ' эл. (' + elapsed + 'с)';
+ state.files[rowIdx].parsed = true;
+ state.files[rowIdx].parseInfo = pr;
+ state.files[rowIdx].status = '✓ ' + pr.element_count + ' эл. (' + elapsed + 'с)';
} else if (pr && pr.status === 'error') {
- fileQueue[rowIdx].status = '✗ ' + (pr.error || 'ошибка парсинга') + '';
- fileQueue[rowIdx].parseInfo = pr;
+ state.files[rowIdx].status = '✗ ' + (pr.error || 'ошибка парсинга') + '';
+ state.files[rowIdx].parseInfo = pr;
} else {
- fileQueue[rowIdx].status = '✗ Неизвестная ошибка';
+ state.files[rowIdx].status = '✗ Неизвестная ошибка';
}
} catch(err) {
- fileQueue[rowIdx].status = '✗ ' + err.message + '';
+ state.files[rowIdx].status = '✗ ' + err.message + '';
}
- renderTable();
+ render(state);
}
await refreshSupps();
stepDone('stepUpload');
stepActive('stepClassify');
- // Синхронизировать БД с таблицей — удалить всё, чего нет в fileQueue
+ // Синхронизировать БД с таблицей — удалить всё, чего нет в state.files
syncDB();
fileInput.value = '';
@@ -322,7 +340,7 @@ async function runClassify() {
// Poll progress каждые 2с
var pollTimer = setInterval(async function() {
try {
- var r = await fetch(VM_API + '/api/batch-progress?batch=' + batchId);
+ var r = await fetch(VM_API + '/api/batch-progress?batch=' + state.batchId);
var d = await r.json();
if (d.ok && d.counts) {
var done = (d.counts.classified || 0) + (d.counts.failed || 0);
@@ -339,7 +357,7 @@ async function runClassify() {
var resp = await fetch(VM_API + '/api/classify-batch', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
- body: JSON.stringify({batch_id: batchId})
+ body: JSON.stringify({batch_id: state.batchId})
});
var data = await resp.json();
clearInterval(timer);
@@ -362,7 +380,7 @@ async function runClassify() {
}
async function loadGroups() {
- var resp = await fetch(VM_API + '/api/groups?batch=' + batchId);
+ var resp = await fetch(VM_API + '/api/groups?batch=' + state.batchId);
var data = await resp.json();
if (!data.ok || !data.groups) return;
@@ -375,7 +393,7 @@ async function loadGroups() {
stepDone('stepGroups');
// Сохраняем группы для compare
- window._groupsData = data.groups;
+ state.groups = data.groups;
data.groups.forEach(function(g, gi) {
var isUnresolved = g.contract_number === '__unresolved__';
@@ -442,7 +460,7 @@ function markGroupDone(gi, time) {
var bodyEl = document.getElementById('cmpBody_' + gi);
var bodyHTML = bodyEl ? bodyEl.innerHTML : '';
var card = bodyEl.parentNode;
- var g = window._groupsData[gi];
+ var g = state.groups[gi];
card.innerHTML = ' |