diff --git a/site/config.py b/site/config.py
index e927d6e..4a249cd 100644
--- a/site/config.py
+++ b/site/config.py
@@ -1,7 +1,7 @@
"""Конфигурация приложения — все настройки в одном месте."""
import os
-VERSION = "2.0.4"
+VERSION = "2.0.5"
LLM_URL = os.getenv("LLM_API_URL", "https://api.aillm.ru/v1/chat/completions")
LLM_KEY = os.getenv("LLM_API_KEY", "")
diff --git a/site/static/files.js b/site/static/files.js
index 6f5151a..60fa382 100644
--- a/site/static/files.js
+++ b/site/static/files.js
@@ -3,7 +3,7 @@
*
* ⛔⛔⛔ НЕ МЕНЯТЬ БЕЗ РАЗРЕШЕНИЯ НАЕЛЯ ⛔⛔⛔
*
- * v2.0.2: fetch() + прогресс + таблица сразу
+ * v2.0.3: честный счётчик ⏳ соединение... Nс вместо фейкового ↑N%
* Проверено в бою 2026-07-16. Любое изменение = риск сломать загрузку.
*
* КЛЮЧЕВЫЕ ФУНКЦИИ (не трогать):
@@ -16,7 +16,7 @@
* statusToHTML(status) — Чистая функция: структура → HTML (Фаза 1).
*
* Вход: { kind, pct?, text?, count?, elapsed? } — ни одного HTML-тега.
- * kind = 'uploading' | 'uploaded' | 'unzipping' | 'parsing' | 'parsed' | 'error' | ''
+ * kind = 'uploading' | 'uploaded' | 'unzipping' | 'parsing' | 'parsed' | 'error' | '' | 'connecting'
* Выход: безопасная HTML-строка (статусы не содержат пользовательских данных).
*
* ПАТТЕРН (decoupling-final-plan.md): отделяем данные от представления.
@@ -25,7 +25,8 @@
function statusToHTML(st) {
if (!st || !st.kind) return '';
switch (st.kind) {
- case 'uploading': return '↑ ' + (st.pct || 0) + '%';
+ case 'connecting': return '⏳ соединение... ' + (st.elapsed || 0) + 'с';
+ case 'uploading': return '⏳ отправка... ' + (st.elapsed || 0) + 'с';
case 'uploaded': return '✓';
case 'unzipping': return '⏳ распаковка...';
case 'parsing': return '⏳ парсинг...';
@@ -203,26 +204,27 @@ window.toggleClassifyDetail = async function(i) {
};
/**
- * ⛔ НЕ МЕНЯТЬ ⛔ uploadFile — fetch-загрузка с имитацией прогресса.
+ * ⛔ НЕ МЕНЯТЬ ⛔ uploadFile — fetch-загрузка с честным счётчиком времени.
*
- * v2.0.2: fetch() вместо XHR (XHR давал ERR_CONNECTION_RESET).
- * Прогресс: setInterval 200ms, кап 90%, затем 100% при ответе.
+ * v2.0.3: вместо фейкового ↑N% — честный счётчик ⏳ соединение... Nс → ⏳ отправка... Nс.
+ * fetch() не даёт реальный upload progress, поэтому считаем секунды.
* Кэш-бастинг: ?_=Date.now()
*/
function uploadFile(file, onProgress, zipSource) {
- if (onProgress) onProgress(0);
+ var startTime = Date.now();
+ var phase = 'connecting'; // connecting → uploading
+ if (onProgress) onProgress({ kind: 'connecting', elapsed: 0 });
var fd = new FormData();
fd.append('files', file, file.name);
if (state.contractId) fd.append('contract_id', state.contractId);
fd.append('batch_id', state.batchId);
if (zipSource) fd.append('zip_source', zipSource);
- // Имитация прогресса (fetch не даёт реальный upload progress)
- var pct = 0;
+ // Честный счётчик: каждую секунду обновляем elapsed
var timer = setInterval(function() {
- pct = Math.min(pct + 5, 90); // до 90% ползём, остальное — по факту
- if (onProgress) onProgress(pct);
- }, 200);
+ var elapsed = Math.floor((Date.now() - startTime) / 1000);
+ if (onProgress) onProgress({ kind: phase, elapsed: elapsed });
+ }, 1000);
return fetch(UPLOAD_URL + '?_=' + Date.now(), { method: 'POST', body: fd })
.then(function(r) {
@@ -232,7 +234,8 @@ function uploadFile(file, onProgress, zipSource) {
.then(function(data) {
clearInterval(timer);
if (data.ok) {
- if (onProgress) onProgress(100);
+ var elapsed = Math.floor((Date.now() - startTime) / 1000);
+ if (onProgress) onProgress({ kind: 'uploading', elapsed: elapsed });
return data;
}
throw new Error(data.error || 'Неизвестная ошибка');
@@ -403,7 +406,7 @@ async function addZipFile(file) {
*/
async function addRegularFile(file, zipSource) {
// Создать запись с начальным статусом
- var entry = { name: file.name, lastModified: file.lastModified, size: file.size, file: file, status: { kind: 'uploading', pct: 0 }, zip_source: zipSource || null };
+ var entry = { name: file.name, lastModified: file.lastModified, size: file.size, file: file, status: { kind: 'connecting', elapsed: 0 }, zip_source: zipSource || null };
// ДЕДУПЛИКАЦИЯ: ключ = (zip_source, name), чтобы одноимённые файлы из разных ZIP не затирались
var dupKey = (zipSource || '') + '/' + file.name;
@@ -427,9 +430,9 @@ async function addRegularFile(file, zipSource) {
render(state);
try {
- // fetch-загрузка с имитацией прогресса (XHR → fetch из-за RST)
- var resp = await uploadFile(file, function(pct) {
- state.files[rowIdx].status = { kind: 'uploading', pct: pct };
+ // fetch-загрузка с честным счётчиком времени
+ var resp = await uploadFile(file, function(st) {
+ state.files[rowIdx].status = st;
render(state);
}, zipSource);
// Бэкенд возвращает doc_id и contract_id (нижний регистр — Python keys)
@@ -507,7 +510,7 @@ async function onFilesSelected(newFiles) {
await addZipFile(f);
continue;
}
- state.files.push({ name: f.name, lastModified: f.lastModified, size: f.size, file: f, status: { kind: 'uploading', pct: 0 }, zip_source: null });
+ state.files.push({ name: f.name, lastModified: f.lastModified, size: f.size, file: f, status: { kind: 'connecting', elapsed: 0 }, zip_source: null });
state.files[state.files.length - 1]._pendingFile = f;
}
render(state);
@@ -520,8 +523,8 @@ async function onFilesSelected(newFiles) {
delete entry._pendingFile;
var f = pendingFile;
try {
- var resp = await uploadFile(f, function(pct) {
- entry.status = { kind: 'uploading', pct: pct };
+ var resp = await uploadFile(f, function(st) {
+ entry.status = st;
render(state);
});
if (resp && resp.contract_id) state.contractId = resp.contract_id;
diff --git a/site/templates/index.html b/site/templates/index.html
index b1ab725..f06d847 100644
--- a/site/templates/index.html
+++ b/site/templates/index.html
@@ -216,11 +216,11 @@
-
-
-
-
-
-
+
+
+
+
+
+