Files
drhider/History/ux-frontend/2026-08-24-interrupt-eta-design.md
T

172 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Дизайн: 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/<sid>`: `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/<sid>`.
- Во время добора: счётчик «готово 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/<sid>`, событие `cancelled`, ETA в heartbeat, частичный результат.
- `drhider/obfuscator.py` — `cancel_event`, `CancelRequested`, передача объёма текста, сборка частичного результата.
- `site/templates/index.html` — сообщение, кнопка, диалог, счётчик, ETA.
- `drhider/config.py` (опц.) — коэффициент оценки времени.
## 8. Открытые вопросы
- Точное место сбора «готовых файлов» в `obfuscate_files` (где хранить заменённые байты для частичного ZIP).
- Формат события `cancelled` (поля `saved/total` + причина).
- Значение коэффициента оценки (уточнить по ещё паре замеров).