# Дизайн: 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` + причина). - Значение коэффициента оценки (уточнить по ещё паре замеров).