diff --git a/History/2026-08-24-interrupt-eta-design.md b/History/2026-08-24-interrupt-eta-design.md new file mode 100644 index 0000000..3493990 --- /dev/null +++ b/History/2026-08-24-interrupt-eta-design.md @@ -0,0 +1,171 @@ +# Дизайн: TTL-фикс + прерывание с сохранением + оценка времени + UI (2026-08-24) + +> План реализации фичи по итогам тестов 24.08 (см. `2026-08-24-tests-upload-and-obfuscation.md`). +> Статус: **ПЛАН, код не менялся**. Порядок: TTL → прерывание → ETA → UI. + +--- + +## 0. Исходная проблема (что тесты вскрыли) + +- **TTL-баг**: сессия живёт 30 мин (`TTL_SECONDS = 30*60`), таймер запускается при создании + и **не продлевается** во время обработки. Обфускация 101 файла заняла 1995с (>30 мин) → + сессия удалена TTL → `store_result` молча `False` → `download`/`csv` = **404**, результат потерян. +- **Нет UX для долгих прогонов**: юзер не знает, сколько ждать, и не может прервать с сохранением. + +## 1. Требования (от пользователя) + +1. Показывать, что обработка долгая «потому что много данных». +2. Возможность **прерывания с сохранением** уже обработанного в выходном ZIP + CSV. +3. Прерывание **не мгновенное** — «пусть завершает, сколько требуется» (мягкий добор). +4. Юзер **точно знает, что будет сохранено** (до нажатия и во время). +5. В начале — **ориентировочное время** обработки всего пакета, чтобы знать, чего ожидать. + +--- + +## 2. Блок A — TTL-фикс (фундамент, обязателен первым) + +**Файл:** `site/session.py` + +### Текущее поведение +```python +TTL_SECONDS = 30 * 60 +def _start_timer(sid): + def _clean(): _sessions.pop(sid, None) + timer = threading.Timer(TTL_SECONDS, _clean); timer.daemon=True; timer.start(); return timer +``` +Таймер одноразовый, не продлевается → долгий прогон убивает сессию. + +### Новое поведение +- Добавить `def touch(sid)`: отменяет старый таймер и запускает новый (продлевает жизнь). +- При старте обработки (`process_stream`) — **отменить** таймер сессии (во время обработки + сессия живёт «вечно», пока идёт воркер). +- При завершении (`complete` / `error` / `cancelled`) — **запустить** таймер заново + (результат доступен ещё 30 мин после окончания). +- `add_file`, `get_files` — можно тоже `touch()` для единообразия (не обязательно). + +**Итог:** долгий прогон больше не теряет сессию; результат доступен 30 мин после завершения. + +--- + +## 3. Блок B — прерывание с сохранением (мягкая остановка) + +### 3.1. Модель поведения (согласовано с пользователем) + +| Где нажата «Прервать» | Поведение | +|---|---| +| Во время LLM-анализа | Анализ **добарается до конца** (без полного mapping нельзя сохранить ни одного документа). Прерывание срабатывает на границе фазы замены. | +| Во время фазы замены | Дорабатывается текущий файл → стоп. В ZIP+CSV попадают все готовые файлы + полная таблица замен. | +| После завершения | Обычный полный результат (прерывание не актуально). | + +- Флаг отмены проверяется **только на границах файлов фазы замены** → результат всегда осмысленный. +- LLM-фаза не прерывается (добор), что соответствует «пусть завершает, сколько требуется». + +### 3.2. Изменения по файлам + +**`site/session.py`** +- В словаре сессии добавить `"cancel": threading.Event()` при создании. +- Хелперы: `request_cancel(sid)`, `cancel_requested(sid)`. + +**`site/routes/api_bp.py`** +- Новый эндпоинт `POST /api/cancel/`: `request_cancel(sid)` → `{ok:true}` (или 404 если нет сессии). +- `process_stream`: + - При старте — `cancel_event` = событие из сессии, таймер TTL отменяется. + - Воркер передаёт `cancel_event` в `obfuscate_files`. + - Обработка исключения отмены → сборка **частичного результата**: + - ZIP из файлов, прошедших замену (исключая `mapping.csv`) + полный `mapping.csv`. + - `store_result` + `store_csv` (сейчас `store_result` сохраняет только полный zip). + - Новое SSE-событие `cancelled`: `data: {"saved": <кол-во готовых>, "total": <всего>, ...}`. + - После завершения/отмены — перезапуск TTL-таймера. + +**`drhider/obfuscator.py`** +- `obfuscate_files(..., cancel_event=None)`. +- В цикле фазы замены (по файлам): `if cancel_event and cancel_event.is_set(): raise CancelRequested()`. +- В LLM-цикле отмену **НЕ** проверяем (добор до конца). +- `CancelRequested` — новое исключение (в `drhider/obfuscator.py` или общий модуль). +- Для сборки частичного ZIP: функция должна уметь вернуть уже обработанные файлы + (список `(имя, bytes)` заменённых) — либо прогресс-колбеком, либо исключение с данными. + +### 3.3. Частичный результат — детали +- Каждый файл, прошедший замену, гарантированно консистентен: глобальный mapping один для всех. +- Частичный ZIP = готовые файлы + `mapping.csv` (полная таблица — она собрана к концу LLM). +- Если прервано до конца LLM — прерывание не сработает (добор), значит частичный результат + будет как минимум с теми файлами, которые успели замениться после LLM. + +--- + +## 4. Блок C — оценка времени (ETA) + +### 4.1. Уровень 1 — статическая оценка в начале +- Фронт знает `N` файлов и объём `S` (МБ) до обработки. +- Эмпирический коэффициент из замеров: 24.08 — 101 файл / ~150 КБ текста → LLM ≈ 1924с + (~12.8 с/КБ текста); 23.08 — 100 файлов → LLM 841–985с. +- Коэффициент задать константой на фронте/бэке (можно в `config.py`), формула: + `оценка ≈ S_текста(КБ) × K + константа`. Для бинарных/сканов текста нет — оценка грубая, + поэтому это «ориентировочно», с оговоркой в UI. + +### 4.2. Уровень 2 — после извлечения (бэк знает объём текста) +- `obfuscate_files` после фазы извлечения знает суммарный объём текста (`total_chars`). +- Через новый колбек (или расширенный `progress_cb`) отдать `{phase:"extract_done", chars:N}`. +- Тогда оценка точнее: `tokens ≈ chars × фактор`, `время ≈ tokens / скорость(истор.)`. + +### 4.3. Уровень 3 — адаптивный ETA во время LLM +- В LLM-цикле меряем фактическую скорость: обработано символов / прошедшее время. +- Оставшийся объём = `total_chars − processed_chars`. +- `ETA = оставшиеся символы / скорость`. +- Отдавать в существующем heartbeat `llm`: + `data: {"active":..., "elapsed":..., "tokens":..., "eta_sec": N, "done_chars":..., "total_chars":...}`. +- Фронт обновляет «осталось ~X мин» в live-блоке. + +### 4.4. Ограничения (честно) +- Оценка **ориентировочная** (LLM-вывод варьируется). Показывать как «≈», обновлять адаптивно. +- Для сканов/PDF без текстового слоя — извлечённый текст мал, LLM-этап быстрый, оценка завышена. + +--- + +## 5. Блок D — UI (index.html) + +### 5.1. Сообщение «много данных» +- После загрузки (перед обработкой): «Загружено N файлов (X МБ). Идёт обработка — + это может занять ~M мин.» +- Держать в статус-строке и в live-блоке. + +### 5.2. Кнопка «Прервать» +- Появляется/активна на этапе обработки (Фаза 2). +- По нажатию — **диалог-подтверждение**: + > «Остановить обработку? Будет сохранено: документы, уже прошедшие обработку + > (сейчас готово X из N), и таблица замен. Текущий анализ будет доведён до конца. + > [Остановить] [Отмена]» +- После подтверждения — `POST /api/cancel/`. +- Во время добора: счётчик «готово X/N» + «завершаем текущий этап…». + +### 5.3. Показ частичного результата +- SSE-событие `cancelled` → статус «Сохранено X из N документов + таблица замен». +- Кнопки «Скачать ZIP» / «Скачать CSV» работают как обычно (ведут на download/csv). + +### 5.4. ETA в live-блоке +- «Обработка… осталось ~X мин» — из `eta_sec` хартбита. +- Статическая оценка — сразу после старта обработки. + +--- + +## 6. Порядок реализации и проверка + +1. **TTL-фикс** (`session.py` + `api_bp.py`): тест — долгий прогон >30 мин, скачивание после. +2. **Прерывание** (`obfuscator.py` + `api_bp.py` + `session.py`): тест — прервать в фазе замены, + проверить частичный ZIP+CSV. +3. **ETA** (`obfuscator.py`/`api_bp.py` + фронт): тест — сверка оценки с фактом. +4. **UI** (кнопка, диалог, счётчик): ручной тест в браузере. + +Версия: бамп до **0.0.63** после реализации (всех блоков). + +## 7. Затронутые файлы +- `site/session.py` — TTL `touch`, флаг отмены. +- `site/routes/api_bp.py` — `POST /api/cancel/`, событие `cancelled`, ETA в heartbeat, частичный результат. +- `drhider/obfuscator.py` — `cancel_event`, `CancelRequested`, передача объёма текста, сборка частичного результата. +- `site/templates/index.html` — сообщение, кнопка, диалог, счётчик, ETA. +- `drhider/config.py` (опц.) — коэффициент оценки времени. + +## 8. Открытые вопросы +- Точное место сбора «готовых файлов» в `obfuscate_files` (где хранить заменённые байты для частичного ZIP). +- Формат события `cancelled` (поля `saved/total` + причина). +- Значение коэффициента оценки (уточнить по ещё паре замеров).