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

12 KiB
Raw Blame History

Дизайн: 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 молча Falsedownload/csv = 404, результат потерян.
  • Нет UX для долгих прогонов: юзер не знает, сколько ждать, и не может прервать с сохранением.

1. Требования (от пользователя)

  1. Показывать, что обработка долгая «потому что много данных».
  2. Возможность прерывания с сохранением уже обработанного в выходном ZIP + CSV.
  3. Прерывание не мгновенное — «пусть завершает, сколько требуется» (мягкий добор).
  4. Юзер точно знает, что будет сохранено (до нажатия и во время).
  5. В начале — ориентировочное время обработки всего пакета, чтобы знать, чего ожидать.

2. Блок A — TTL-фикс (фундамент, обязателен первым)

Файл: site/session.py

Текущее поведение

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.pyPOST /api/cancel/<sid>, событие cancelled, ETA в heartbeat, частичный результат.
  • drhider/obfuscator.pycancel_event, CancelRequested, передача объёма текста, сборка частичного результата.
  • site/templates/index.html — сообщение, кнопка, диалог, счётчик, ETA.
  • drhider/config.py (опц.) — коэффициент оценки времени.

8. Открытые вопросы

  • Точное место сбора «готовых файлов» в obfuscate_files (где хранить заменённые байты для частичного ZIP).
  • Формат события cancelled (поля saved/total + причина).
  • Значение коэффициента оценки (уточнить по ещё паре замеров).