v2.0.5: честный счётчик соединение... Nс вместо фейкового ↑N% при upload
Deploy contracts-flask / validate (push) Successful in 0s

This commit is contained in:
2026-07-16 09:02:56 +04:00
parent 4f9bee4c1e
commit f4bae5a6b9
3 changed files with 30 additions and 27 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
"""Конфигурация приложения — все настройки в одном месте.""" """Конфигурация приложения — все настройки в одном месте."""
import os 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_URL = os.getenv("LLM_API_URL", "https://api.aillm.ru/v1/chat/completions")
LLM_KEY = os.getenv("LLM_API_KEY", "") LLM_KEY = os.getenv("LLM_API_KEY", "")
+23 -20
View File
@@ -3,7 +3,7 @@
* *
* ⛔⛔⛔ НЕ МЕНЯТЬ БЕЗ РАЗРЕШЕНИЯ НАЕЛЯ ⛔⛔⛔ * ⛔⛔⛔ НЕ МЕНЯТЬ БЕЗ РАЗРЕШЕНИЯ НАЕЛЯ ⛔⛔⛔
* *
* v2.0.2: fetch() + прогресс + таблица сразу * v2.0.3: честный счётчик ⏳ соединение... Nс вместо фейкового ↑N%
* Проверено в бою 2026-07-16. Любое изменение = риск сломать загрузку. * Проверено в бою 2026-07-16. Любое изменение = риск сломать загрузку.
* *
* КЛЮЧЕВЫЕ ФУНКЦИИ (не трогать): * КЛЮЧЕВЫЕ ФУНКЦИИ (не трогать):
@@ -16,7 +16,7 @@
* statusToHTML(status) — Чистая функция: структура → HTML (Фаза 1). * statusToHTML(status) — Чистая функция: структура → HTML (Фаза 1).
* *
* Вход: { kind, pct?, text?, count?, elapsed? } — ни одного HTML-тега. * Вход: { kind, pct?, text?, count?, elapsed? } — ни одного HTML-тега.
* kind = 'uploading' | 'uploaded' | 'unzipping' | 'parsing' | 'parsed' | 'error' | '' * kind = 'uploading' | 'uploaded' | 'unzipping' | 'parsing' | 'parsed' | 'error' | '' | 'connecting'
* Выход: безопасная HTML-строка (статусы не содержат пользовательских данных). * Выход: безопасная HTML-строка (статусы не содержат пользовательских данных).
* *
* ПАТТЕРН (decoupling-final-plan.md): отделяем данные от представления. * ПАТТЕРН (decoupling-final-plan.md): отделяем данные от представления.
@@ -25,7 +25,8 @@
function statusToHTML(st) { function statusToHTML(st) {
if (!st || !st.kind) return ''; if (!st || !st.kind) return '';
switch (st.kind) { switch (st.kind) {
case 'uploading': return ' ' + (st.pct || 0) + '%'; case 'connecting': return '⏳ соединение... ' + (st.elapsed || 0) + 'с';
case 'uploading': return '⏳ отправка... ' + (st.elapsed || 0) + 'с';
case 'uploaded': return '<span class="status-ok">✓</span>'; case 'uploaded': return '<span class="status-ok">✓</span>';
case 'unzipping': return '⏳ распаковка...'; case 'unzipping': return '⏳ распаковка...';
case 'parsing': 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). * v2.0.3: вместо фейкового ↑N% — честный счётчик ⏳ соединение... Nс → ⏳ отправка... Nс.
* Прогресс: setInterval 200ms, кап 90%, затем 100% при ответе. * fetch() не даёт реальный upload progress, поэтому считаем секунды.
* Кэш-бастинг: ?_=Date.now() * Кэш-бастинг: ?_=Date.now()
*/ */
function uploadFile(file, onProgress, zipSource) { 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(); var fd = new FormData();
fd.append('files', file, file.name); fd.append('files', file, file.name);
if (state.contractId) fd.append('contract_id', state.contractId); if (state.contractId) fd.append('contract_id', state.contractId);
fd.append('batch_id', state.batchId); fd.append('batch_id', state.batchId);
if (zipSource) fd.append('zip_source', zipSource); if (zipSource) fd.append('zip_source', zipSource);
// Имитация прогресса (fetch не даёт реальный upload progress) // Честный счётчик: каждую секунду обновляем elapsed
var pct = 0;
var timer = setInterval(function() { var timer = setInterval(function() {
pct = Math.min(pct + 5, 90); // до 90% ползём, остальное — по факту var elapsed = Math.floor((Date.now() - startTime) / 1000);
if (onProgress) onProgress(pct); if (onProgress) onProgress({ kind: phase, elapsed: elapsed });
}, 200); }, 1000);
return fetch(UPLOAD_URL + '?_=' + Date.now(), { method: 'POST', body: fd }) return fetch(UPLOAD_URL + '?_=' + Date.now(), { method: 'POST', body: fd })
.then(function(r) { .then(function(r) {
@@ -232,7 +234,8 @@ function uploadFile(file, onProgress, zipSource) {
.then(function(data) { .then(function(data) {
clearInterval(timer); clearInterval(timer);
if (data.ok) { if (data.ok) {
if (onProgress) onProgress(100); var elapsed = Math.floor((Date.now() - startTime) / 1000);
if (onProgress) onProgress({ kind: 'uploading', elapsed: elapsed });
return data; return data;
} }
throw new Error(data.error || 'Неизвестная ошибка'); throw new Error(data.error || 'Неизвестная ошибка');
@@ -403,7 +406,7 @@ async function addZipFile(file) {
*/ */
async function addRegularFile(file, zipSource) { 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 не затирались // ДЕДУПЛИКАЦИЯ: ключ = (zip_source, name), чтобы одноимённые файлы из разных ZIP не затирались
var dupKey = (zipSource || '') + '/' + file.name; var dupKey = (zipSource || '') + '/' + file.name;
@@ -427,9 +430,9 @@ async function addRegularFile(file, zipSource) {
render(state); render(state);
try { try {
// fetch-загрузка с имитацией прогресса (XHR → fetch из-за RST) // fetch-загрузка с честным счётчиком времени
var resp = await uploadFile(file, function(pct) { var resp = await uploadFile(file, function(st) {
state.files[rowIdx].status = { kind: 'uploading', pct: pct }; state.files[rowIdx].status = st;
render(state); render(state);
}, zipSource); }, zipSource);
// Бэкенд возвращает doc_id и contract_id (нижний регистр — Python keys) // Бэкенд возвращает doc_id и contract_id (нижний регистр — Python keys)
@@ -507,7 +510,7 @@ async function onFilesSelected(newFiles) {
await addZipFile(f); await addZipFile(f);
continue; 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; state.files[state.files.length - 1]._pendingFile = f;
} }
render(state); render(state);
@@ -520,8 +523,8 @@ async function onFilesSelected(newFiles) {
delete entry._pendingFile; delete entry._pendingFile;
var f = pendingFile; var f = pendingFile;
try { try {
var resp = await uploadFile(f, function(pct) { var resp = await uploadFile(f, function(st) {
entry.status = { kind: 'uploading', pct: pct }; entry.status = st;
render(state); render(state);
}); });
if (resp && resp.contract_id) state.contractId = resp.contract_id; if (resp && resp.contract_id) state.contractId = resp.contract_id;
+6 -6
View File
@@ -216,11 +216,11 @@
</div> </div>
</div> </div>
<script src="/static/state.js?v=2.0.4"></script> <script src="/static/state.js?v=2.0.5"></script>
<script src="/static/app_utils.js?v=2.0.4"></script> <script src="/static/app_utils.js?v=2.0.5"></script>
<script src="/static/files.js?v=2.0.4"></script> <script src="/static/files.js?v=2.0.5"></script>
<script src="/static/groups.js?v=2.0.4"></script> <script src="/static/groups.js?v=2.0.5"></script>
<script src="/static/compare.js?v=2.0.4"></script> <script src="/static/compare.js?v=2.0.5"></script>
<script src="/static/app.js?v=2.0.4"></script> <script src="/static/app.js?v=2.0.5"></script>
</body> </body>
</html> </html>