diff --git a/History/2026-08-19-ux-fixes.md b/History/2026-08-19-ux-fixes.md new file mode 100644 index 0000000..a54fd65 --- /dev/null +++ b/History/2026-08-19-ux-fixes.md @@ -0,0 +1,39 @@ +# v0.0.49 — индикатор распаковки, разделение фаз, таймауты SSE — 2026-08-19 + +**Дата:** 2026-08-19 +**Версия:** 0.0.48 → 0.0.49 + +--- + +## Контекст +Отзыв тестировщика: +1. «После Выбрать файлы долго не респондит — разбирает архивы» → нет индикатора. +2. «По Обфусцировать — двойной проход по файлам» → фазы загрузки и обработки не разделены. +3. «Ошибка: SSE connection failed» → долгое SSE рвалось шлюзом. + +## Изменения + +### Ingress (kubectl patch, не код) +- `proxy-read-timeout: 600 → 1800` +- `proxy-send-timeout: 600 → 1800` +— шлюз больше не рвёт долгое SSE (было ~10 мин, стало 30 мин). + +### `site/templates/index.html` +- **Индикатор распаковки**: при выборе файлов с `.zip` — `cursor: wait` + + статус «Разбираю архивы…», снимается после распаковки (try/finally). +- **Разделение фаз**: + - Фаза 1: статус «Загрузка (этап 1/2) i/N: имя». + - Фаза 2: статус «Обработка (этап 2/2)…», таймер «Обработка (этап 2/2)… Nс». + - При старте фазы 2 загрузочные статусы сбрасываются на «⏳». + +### `site/app.py` +- `VERSION = "0.0.49"`. + +## Проверка +- `node --check`, `py_compile` — OK. +- Тесты builder/replacer/scanner/zip — все зелёные. + +## Примечание +- Причина «SSE connection failed»: шлюз рвал долгое SSE (proxy-read-timeout 600с). + Таймауты подняты до 1800с. Если обработка > 30 мин — нужно ещё выше или + авто-реконнект EventSource (отложено: реконнект вызывает повторную обработку). diff --git a/site/app.py b/site/app.py index 6f9666a..bb730ab 100644 --- a/site/app.py +++ b/site/app.py @@ -20,7 +20,7 @@ if _sys_path_root not in sys.path: sys.path.insert(0, _sys_path_root) # Версия приложения (меняется при изменениях) -VERSION = "0.0.48" +VERSION = "0.0.49" def create_app(): diff --git a/site/templates/index.html b/site/templates/index.html index 1aa780e..6d8e7ce 100644 --- a/site/templates/index.html +++ b/site/templates/index.html @@ -343,16 +343,31 @@ function addFileWithDedup(file) { fi.addEventListener('change', async () => { const incoming = Array.from(fi.files); - for (const f of incoming) { - if (f.name.toLowerCase().endsWith('.zip')) { - try { - const nested = await listZipFiles(f); - nested.forEach(x => addFileWithDedup(x)); - } catch (err) { - addFileWithDedup(f); // не удалось распаковать — добавить zip как есть + const hasZip = incoming.some(f => f.name.toLowerCase().endsWith('.zip')); + if (hasZip) { + // Распаковка архивов может занять время — показать индикатор + document.body.style.cursor = 'wait'; + st.className = 'status progress'; + st.textContent = 'Разбираю архивы…'; + } + try { + for (const f of incoming) { + if (f.name.toLowerCase().endsWith('.zip')) { + try { + const nested = await listZipFiles(f); + nested.forEach(x => addFileWithDedup(x)); + } catch (err) { + addFileWithDedup(f); // не удалось распаковать — добавить zip как есть + } + } else { + addFileWithDedup(f); } - } else { - addFileWithDedup(f); + } + } finally { + if (hasZip) { + document.body.style.cursor = ''; + st.className = ''; + st.textContent = ''; } } const d = new DataTransfer(); @@ -383,7 +398,7 @@ async function uploadFiles() { for (let i = 0; i < total; i++) { const f = files[i], n = i + 1; st.className = 'status progress'; - st.textContent = 'Загрузка ' + n + '/' + total + ': ' + f.name; + st.textContent = 'Загрузка (этап 1/2) ' + n + '/' + total + ': ' + f.name; try { const t0 = performance.now(); await new Promise((resolve, reject) => { @@ -431,15 +446,16 @@ async function uploadFiles() { } // Фаза 2: обработка (SSE — прогресс по каждому файлу) + // Сброс загрузочных статусов — теперь этап обработки for (let i = 0; i < total; i++) ss(i, ''); const fileTimers = {}; // idx -> performance.now() const fileIntervals = {}; // idx -> setInterval id st.className = 'status progress'; - st.textContent = 'Обработка...'; + st.textContent = 'Обработка (этап 2/2)…'; const t0 = performance.now(); const ptimer = setInterval(() => { const sec = Math.round((performance.now() - t0) / 1000); - st.textContent = 'Обработка... ' + sec + 'с'; + st.textContent = 'Обработка (этап 2/2)… ' + sec + 'с'; }, 1000); // Крупный live-блок «Идёт обработка» с тикающим таймером