docs: дизайн TTL-фикса, прерывания с сохранением, оценки времени и UI (план, код не менялся)

This commit is contained in:
“Naeel”
2026-08-24 10:48:16 +03:00
parent 6542d51375
commit 7a860d645d
+171
View File
@@ -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/<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 841985с.
- Коэффициент задать константой на фронте/бэке (можно в `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` + причина).
- Значение коэффициента оценки (уточнить по ещё паре замеров).