diff --git a/docs/MIGRATION-GUIDE.md b/docs/MIGRATION-GUIDE.md index 458a365..22b2ede 100644 --- a/docs/MIGRATION-GUIDE.md +++ b/docs/MIGRATION-GUIDE.md @@ -57,13 +57,13 @@ Flask → ZIP + CSV → браузеру | `ss(idx, html)` | Обновление ячейки статуса в таблице | | `downloadZip()` / `downloadCsv()` | Скачивание результатов | -**Критичные исправления (v0.0.26):** +**Критичные исправления (v0.0.26 — v0.0.29):** ```javascript // 1. Прогрев upstream при загрузке страницы — обязательно! window.addEventListener('load', () => { sf = []; fi.value = ''; rr(); - fetch('/health').catch(() => {}); // ← вот это + fetch('/health').catch(() => {}); // ← обязательно! греет upstream }); // 2. Abort предыдущих запросов перед новым @@ -76,23 +76,56 @@ window.addEventListener('beforeunload', () => resetAll()); // 4. Сохранение списка файлов до очистки const files = sf.slice(); // копия перед resetAll() -// 5. Цикл загрузки (по одному файлу) +// 5. Выбор файлов — ДОБАВЛЯЕТ в таблицу (не стирает, с дедупом) +fi.addEventListener('change', () => { + const incoming = Array.from(fi.files); + const seen = new Set(sf.map(f => f.name + '|' + f.size)); + for (const f of incoming) { + if (!seen.has(f.name + '|' + f.size)) sf.push(f); + } + const d = new DataTransfer(); + sf.forEach(f => d.items.add(f)); + fi.files = d.files; + rr(); +}); + +// 6. Цикл загрузки (fetch, не XHR — XHR давал RST на первом запросе) for (let i = 0; i < total; i++) { const fd = new FormData(); fd.append('files', f, f.name); if (currentSid) fd.append('session', currentSid); - const resp = await fetch('/api/upload?_=' + Date.now(), { + const resp = await fetch('/api/upload?_=' + Date.now(), { // ← fetch, не XHR method: 'POST', body: fd }); const data = await resp.json(); currentSid = data.session; } -// 6. SSE-обработка +// 7. SSE-обработка activeES = new EventSource('/api/process_stream/' + currentSid); activeES.addEventListener('start', ...); // файл начат activeES.addEventListener('done', ...); // файл готов activeES.addEventListener('complete', ...); // всё готово + +// 8. Скачивание ZIP/CSV — через fetch+Blob (не window.location!) +function downloadZip() { + if (!currentSid) return; + fetch('/api/download/' + currentSid) + .then(r => { + const disp = r.headers.get('Content-Disposition'); + const m = disp && disp.match(/filename="?(.+?)"?$/); + return Promise.all([r.blob(), m ? m[1] : 'result.zip']); + }) + .then(([blob, fname]) => { + const a = document.createElement('a'); + a.href = URL.createObjectURL(blob); + a.download = fname; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(a.href); + }); +} ``` ### 2. `site/routes/api_bp.py` — API эндпоинты @@ -236,6 +269,8 @@ def твоя_функция(files, **kwargs): - [ ] `fetch('/health')` при загрузке страницы - [ ] `beforeunload` → `resetAll()` - [ ] `activeXHR` и `activeES` очищаются перед новым запуском +- [ ] `fetch` вместо XHR (XHR на первом запросе даёт RST) +- [ ] Выбор файлов — добавляет, не стирает (с дедупом) - [ ] `Connection: close` — **НЕ ставить** (Waitress/PEP 3333 запрещает hop-by-hop) ---