docs: сортировка History по темам (sonnet, opus, llm, sse, ux-frontend, upload, infra, plans, tests, base, code-fixes)
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
# v0.0.41 — время на конкретный файл в колонке Статус — 2026-08-19
|
||||
|
||||
**Дата:** 2026-08-19
|
||||
**Версия:** 0.0.40 → 0.0.41
|
||||
|
||||
---
|
||||
|
||||
## Суть
|
||||
|
||||
В колонке «Статус» таблицы у всех файлов показывалось ~одинаковое ОБЩЕЕ время.
|
||||
Причина: фронт считал время как разницу `start`→`done`, а LLM — общий для всех
|
||||
файлов (в проходе 1) — попадал в интервал КАЖДОГО файла.
|
||||
|
||||
Теперь бэк считает время на КОНКРЕТНЫЙ файл (extract_text + regex + замена,
|
||||
БЕЗ общего LLM) и передаёт его в `done`-событии.
|
||||
|
||||
## Изменения
|
||||
|
||||
### `drhider/obfuscator.py`
|
||||
- `import time` добавлен.
|
||||
- Заведён `file_times = [0.0] * total`.
|
||||
- Проход 1: `file_times[i] += time.time() - t0` вокруг extract_text + regex.
|
||||
- Проход 2: `file_times[i] += time.time() - t0` вокруг замены.
|
||||
- `progress_cb` расширен до `(phase, idx, total, fname, elapsed)`; в `done`
|
||||
передаётся `round(file_times[i], 2)`.
|
||||
|
||||
### `site/routes/api_bp.py`
|
||||
- `progress(phase, idx, total_, name, elapsed)`; в SSE `done` добавлен `elapsed`.
|
||||
|
||||
### `site/templates/index.html`
|
||||
- В `done`-обработчике: если `d.elapsed > 0` — показать `d.elapsed.toFixed(1)`
|
||||
(время файла от бэка), иначе fallback на старое вычисление.
|
||||
|
||||
### `site/app.py`
|
||||
- `VERSION = "0.0.41"`.
|
||||
|
||||
## Проверка
|
||||
- События: `[start(0,0.0), start(1,0.0), done(0,<время>), done(1,<время>)]`.
|
||||
- `py_compile`, `node --check` — OK.
|
||||
- Тесты: builder 20/20, replacer 10/10, scanner 20/20, zip 28/28.
|
||||
@@ -0,0 +1,39 @@
|
||||
# v0.0.40 — имя файла в live-блоке + сброс итогов — 2026-08-19
|
||||
|
||||
**Дата:** 2026-08-19
|
||||
**Версия:** 0.0.39 → 0.0.40
|
||||
|
||||
---
|
||||
|
||||
## Суть
|
||||
|
||||
Два UX-фикса во время обработки:
|
||||
|
||||
1. **Имя текущего файла в live-блоке.** Раньше `start`-событие (с именем файла)
|
||||
слалось только в **проходе 2** (замена), который идёт ПОСЛЕ LLM. А LLM — в
|
||||
проходе 1 и самый долгий. Поэтому во время LLM висело «Подготовка…» вместо
|
||||
имени файла.
|
||||
|
||||
2. **Сброс старых «Итогов» при повторной обфускации.** Раньше блок
|
||||
«📊 Итоги обработки» не скрывался при новом запуске — старые итоги висели
|
||||
во время новой обработки.
|
||||
|
||||
## Изменения
|
||||
|
||||
### `drhider/obfuscator.py`
|
||||
- `progress_cb("start", i, total, name)` перенесён из прохода 2 в **проход 1**
|
||||
(перед `extract_text` каждого файла).
|
||||
- В проходе 2 остался только `progress_cb("done", ...)`.
|
||||
- Порядок событий: `start`×N (extract_text) → `done`×N (замена).
|
||||
|
||||
### `site/templates/index.html`
|
||||
- В начале `uploadFiles()` добавлено скрытие `#statsBlock` и `#liveBlock`
|
||||
(как уже скрывается `db`).
|
||||
|
||||
### `site/app.py`
|
||||
- `VERSION = "0.0.40"`.
|
||||
|
||||
## Проверка
|
||||
- Порядок событий: `[start(0), start(1), done(0), done(1)]` — корректно.
|
||||
- `node --check`, `py_compile` — OK.
|
||||
- Тесты: builder 20/20, replacer 10/10, scanner 20/20, zip 28/28.
|
||||
@@ -0,0 +1,55 @@
|
||||
# v0.0.38 — живой таймер обработки + индикация ИИ — 2026-08-19
|
||||
|
||||
**Дата:** 2026-08-19
|
||||
**Версия:** 0.0.37 → 0.0.38
|
||||
|
||||
---
|
||||
|
||||
## Суть
|
||||
|
||||
Юзер должен видеть, что обработка идёт (не зависла), особенно пока LLM
|
||||
«переваривает» большой файл. Добавлены:
|
||||
- крупный live-блок «⏱ Обработка…» с тикающим таймером общего времени;
|
||||
- строка текущего файла с размером («Файл 3/5: имя (5.0 МБ)»);
|
||||
- индикация ИИ: «🤖 ИИ обрабатывает… Nс» (от реальных данных с бэка).
|
||||
|
||||
Итоговая формулировка времени стала ясной: «…общее Xс, из них ИИ Yс».
|
||||
|
||||
## Изменения
|
||||
|
||||
### `drhider/llm_client.py`
|
||||
- В `__init__` добавлены `llm_active` (bool) и `llm_started` (float).
|
||||
- Метод `llm_elapsed_now()` — секунд с начала текущего LLM-вызова (0 если не активен).
|
||||
- `complete()`: ставит `llm_active=True`/`llm_started=time.time()` перед HTTP,
|
||||
сбрасывает `llm_active=False` в `finally`. `import time` добавлен.
|
||||
|
||||
### `site/routes/api_bp.py`
|
||||
- В ветке `except queue.Empty` (пока воркер занят) — heartbeat-событие SSE:
|
||||
`event: llm` → `{active, elapsed, tokens}` (раз в ~1с, timeout=1).
|
||||
|
||||
### `site/templates/index.html`
|
||||
- CSS для `.live-block`, `.live-title`, `.live-file`, `.live-timer` (40px), `.live-llm`.
|
||||
- HTML: блок `#liveBlock` с `#liveFile`, `#liveTimer`, `#liveLlm`/`#liveLlmTime`;
|
||||
в итоговом блоке подпись «Время ИИ» → «Из них ИИ».
|
||||
- JS (фаза 2):
|
||||
- показ `#liveBlock`, интервал `liveRefresh` (200мс) — тикает `#liveTimer`;
|
||||
- обработчик `start` — в `#liveFile` имя текущего файла + размер из `sf[idx]`;
|
||||
- обработчик `llm` — показывает/скрывает `#liveLlm` и обновляет `#liveLlmTime`;
|
||||
- `complete`/`onerror`/`catch` — скрыть `#liveBlock`, очистить интервалы;
|
||||
- итоговый текст: «общее Xс, из них ИИ Yс».
|
||||
- `resetAll` — скрывает `#liveBlock`.
|
||||
|
||||
### `site/app.py`
|
||||
- `VERSION = "0.0.38"`.
|
||||
|
||||
## Проверка
|
||||
- `node --check`, `py_compile` — OK.
|
||||
- `LLMClient`: во время вызова `llm_active=True`, `llm_elapsed_now()≈0.3с`; после —
|
||||
`False`; накопление токенов и времени — ок.
|
||||
- SSE end-to-end: `complete` содержит `{total, tokens, llm_sec}`. Heartbeat `llm`
|
||||
генерируется в ветке `except queue.Empty` при долгой обработке (локально без
|
||||
LLM обработка мгновенная, поэтому heartbeat не успевает — в проде при долгой
|
||||
LLM-обработке будет слаться раз в сек).
|
||||
|
||||
## Примечание
|
||||
Бэкенд метрики не менял (только флаг/метод для live-таймера + heartbeat).
|
||||
@@ -0,0 +1,39 @@
|
||||
# v0.0.49 — индикатор распаковки, разделение фаз, таймауты SSE — 2026-08-19
|
||||
|
||||
**Дата:** 2026-08-19
|
||||
**Версия:** 0.0.48 → 0.0.49
|
||||
|
||||
---
|
||||
|
||||
## Контекст
|
||||
Отзыв тестировщика:
|
||||
1. «После Выбрать файлы долго не респондит — разбирает архивы» → нет индикатора.
|
||||
2. «По Обфусцировать — двойной проход по файлам» → фазы загрузки и обработки не разделены.
|
||||
3. «Ошибка: SSE connection failed» → долгое SSE рвалось шлюзом.
|
||||
|
||||
## Изменения
|
||||
|
||||
### Ingress (kubectl patch, не код)
|
||||
- `proxy-read-timeout: 600 → 1800`
|
||||
- `proxy-send-timeout: 600 → 1800`
|
||||
— шлюз больше не рвёт долгое SSE (было ~10 мин, стало 30 мин).
|
||||
|
||||
### `site/templates/index.html`
|
||||
- **Индикатор распаковки**: при выборе файлов с `.zip` — `cursor: wait` +
|
||||
статус «Разбираю архивы…», снимается после распаковки (try/finally).
|
||||
- **Разделение фаз**:
|
||||
- Фаза 1: статус «Загрузка (этап 1/2) i/N: имя».
|
||||
- Фаза 2: статус «Обработка (этап 2/2)…», таймер «Обработка (этап 2/2)… Nс».
|
||||
- При старте фазы 2 загрузочные статусы сбрасываются на «⏳».
|
||||
|
||||
### `site/app.py`
|
||||
- `VERSION = "0.0.49"`.
|
||||
|
||||
## Проверка
|
||||
- `node --check`, `py_compile` — OK.
|
||||
- Тесты builder/replacer/scanner/zip — все зелёные.
|
||||
|
||||
## Примечание
|
||||
- Причина «SSE connection failed»: шлюз рвал долгое SSE (proxy-read-timeout 600с).
|
||||
Таймауты подняты до 1800с. Если обработка > 30 мин — нужно ещё выше или
|
||||
авто-реконнект EventSource (отложено: реконнект вызывает повторную обработку).
|
||||
@@ -0,0 +1,52 @@
|
||||
# v0.0.33 — раскрытие ZIP в таблицу (фронтенд) — 2026-08-19
|
||||
|
||||
**Дата:** 2026-08-19
|
||||
**Версия:** 0.0.32 → 0.0.33
|
||||
**Файл:** `site/templates/index.html` (фронтенд), `site/app.py` (версия)
|
||||
|
||||
---
|
||||
|
||||
## Суть
|
||||
|
||||
Раньше выбранный `.zip` попадал в таблицу одной строкой и не раскрывался.
|
||||
Теперь при выборе файлов ZIP **распаковывается на клиенте**, файлы из архива
|
||||
попадают в таблицу **по отдельности** (полный относительный путь).
|
||||
|
||||
## Что добавлено (`index.html`, в тот же `<script>`)
|
||||
|
||||
1. **Нативный распаковщик ZIP** без внешних библиотек:
|
||||
- `DecompressionStream('deflate-raw')` — поддержка сжатых (DEFLATE) архивов
|
||||
(WhatsApp/1С архивы сжаты; старый STORE-only `unzip` из v5 не подходил).
|
||||
- `parseZip()` — разбор EOCD → Central Directory → local headers.
|
||||
- `dosToMs()` — DOS date/time (4 байта) → timestamp (мс).
|
||||
- `decodeZipName()` — имя: UTF-8 (если флаг 0x800) иначе CP437→CP866
|
||||
(кириллица из 1С, зеркалит серверный `_decode_name`).
|
||||
|
||||
2. **Рекурсия** `listZipFiles()`:
|
||||
- директории (`name` оканчивается на `/`) пропускаются;
|
||||
- вложенный `.zip` распаковывается рекурсивно;
|
||||
- имя сохраняется **полным путём** (`dir1/sub/file.pdf`).
|
||||
|
||||
3. **Дедуп + суффикс** `addFileWithDedup()`:
|
||||
- структура `fileMeta` (Map: имя → `{size, mtime}`);
|
||||
- имя есть + тот же размер И та же дата → точный дубль → **пропустить**;
|
||||
- имя есть + разный размер ИЛИ дата → **суффикс** `name_2.ext`, `name_3.ext`…;
|
||||
- дата: для обычных файлов `File.lastModified` (мс), для извлечённых из ZIP —
|
||||
DOS date/time → мс.
|
||||
|
||||
4. `fi.addEventListener('change')` переписан в `async`:
|
||||
- `.zip` → `listZipFiles` → каждый файл через `addFileWithDedup`;
|
||||
- при ошибке распаковки (неподдерживаемый метод/не-ZIP) — zip добавляется как есть.
|
||||
|
||||
## Проверка
|
||||
|
||||
- JS-синтаксис: `node --check` — OK.
|
||||
- Рекурсия вложенного ZIP (`nested.zip` → `sub/a.txt`) — извлечён, проверено в Node.
|
||||
- Дедуп/суффикс: `dup.txt` (3) → `dup_2.txt` (5) → `dup_3.txt` (2); точный дубль пропущен.
|
||||
- Backend не менялся (кроме версии в `app.py`).
|
||||
|
||||
## Открытый вопрос
|
||||
|
||||
`DecompressionStream` требует современных браузеров (Chrome 103+, Safari 16.4+,
|
||||
Firefox 113+). AES-шифрованные ZIP (метод 99) не поддерживаются — при этом архив
|
||||
добавляется в таблицу как есть (fallback).
|
||||
@@ -0,0 +1,38 @@
|
||||
# v0.0.35 — фильтр документов при раскрытии ZIP — 2026-08-19
|
||||
|
||||
**Дата:** 2026-08-19
|
||||
**Версия:** 0.0.34 → 0.0.35
|
||||
**Файл:** `site/templates/index.html` (фронт `listZipFiles`), `site/app.py` (версия)
|
||||
|
||||
---
|
||||
|
||||
## Суть
|
||||
|
||||
При раскрытии ZIP в таблицу ранее вытаскивались ВСЕ файлы, включая изображения
|
||||
(WhatsApp jpeg и т.п.). Теперь из ZIP извлекаются **только документы**:
|
||||
|
||||
Разрешённые расширения: `.pdf`, `.doc`, `.docx`, `.txt`, `.md`.
|
||||
|
||||
Всё прочее (jpeg, png, xls, exe и т.д.) — пропускается. Вложенные ZIP
|
||||
раскрываются рекурсивно, и фильтр применяется и к их содержимому.
|
||||
|
||||
## Изменение
|
||||
|
||||
`listZipFiles()` в `index.html`:
|
||||
```js
|
||||
const allowedExt = ['.pdf', '.doc', '.docx', '.txt', '.md'];
|
||||
...
|
||||
else if (allowedExt.some(ext => low.endsWith(ext))) {
|
||||
out.push(new File([e.data], e.name, { lastModified: e.dosMs }));
|
||||
}
|
||||
// иначе — не документ, пропускаем
|
||||
```
|
||||
|
||||
## Проверка
|
||||
- `node --check` — OK.
|
||||
- Тест на архиве с `doc.pdf, old.doc, n.docx, a.txt, r.md, pic.jpeg, pic2.png`:
|
||||
извлечены только 5 документов, jpeg/png пропущены — OK.
|
||||
|
||||
## Вопрос (отложен)
|
||||
- Нужно также упомянуть в подсказке на странице требование современного
|
||||
браузера (`DecompressionStream`: Chrome 103+, Safari 16.4+, Firefox 113+).
|
||||
@@ -0,0 +1,36 @@
|
||||
# 2026-08-20 — Лимиты загрузки + предупреждение в таблице (v0.0.50)
|
||||
|
||||
## Проблема
|
||||
Юзер грузил всё подряд (сотни МБ, включая >200МБ) → HTTP 413 / SSE-обрыв.
|
||||
Требование: понятная политика лимитов, но файлы сверх лимита НЕ молча скрывать —
|
||||
показывать в таблице, чтобы юзер не повторял загрузку в недоумении.
|
||||
|
||||
## Решение (фронт, index.html)
|
||||
Лимиты:
|
||||
- 1 файл > 100 МБ → «не учитывается»
|
||||
- сумма учитываемых > 1 ГБ → следующий файл тоже «не учитывается»
|
||||
|
||||
Логика:
|
||||
- `MAX_FILE_BYTES = 100МБ`, `MAX_SESSION_BYTES = 1ГБ` (константы).
|
||||
- `addFileWithDedup`: файл сверх лимита добавляется в `sf` (виден в таблице),
|
||||
но имя кладётся в `overNames` (Set). Учитываемость считаем по не-overNames.
|
||||
- `rr()`: строки с `overNames` получают класс `row-over` (красный фон/текст),
|
||||
статус «🔥 не учитывается»; счётчик: «N учитываются + M свыше лимита · СУММА».
|
||||
- `uploadFiles()`: строит `toSend` ТОЛЬКО из учитываемых, `sendIdx` маппит
|
||||
idx-отправленного → idx в `sf` (прогресс/таймеры/start/done пишут в верную строку).
|
||||
Если все переборные → «Нет файлов для обфускации (все превышают лимит)».
|
||||
- Шапка: добавлена строка «Ограничения: файл не более 100 МБ, суммарно не более
|
||||
1 ГБ. Файлы сверх лимита помечаются красным и не участвуют в обфускации.»
|
||||
- CSS: `.row-over td { background: rgba(220,38,38,.10) !important; color:#c0392b; }`.
|
||||
- `rm`/`resetAll` — чистят `overNames`.
|
||||
|
||||
Бэк НЕ менялся (MAX_CONTENT_LENGTH=200МБ, MAX_SESSION_BYTES=500МБ остались как защита
|
||||
на сервере). Внимание: бэк-лимит 200МБ/запрос и 500МБ/сессия всё ещё ниже фронтовых
|
||||
1ГБ — для файлов 100МБ-200МБ фронт пустит, но одиночный запрос >200МБ упадёт 413.
|
||||
Это можно согласовать позже, если нужно.
|
||||
|
||||
## Проверка
|
||||
- node --check секции <script> index.html — OK.
|
||||
- py_compile app.py — OK.
|
||||
- Лог-тест лимитов через node: 120МБ→перебор, 13 учитываемых ~995МБ, >1ГБ→перебор — верно.
|
||||
- VERSION поднята до 0.0.50.
|
||||
@@ -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 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` + причина).
|
||||
- Значение коэффициента оценки (уточнить по ещё паре замеров).
|
||||
@@ -0,0 +1,49 @@
|
||||
# Реализовано: TTL-фикс + прерывание с сохранением + ETA + UI-таблица (v0.0.63)
|
||||
|
||||
_2026-08-24. По плану `2026-08-24-implementation-plan-for-flash.md`. Код написан (роль Flash)._
|
||||
|
||||
## Что сделано
|
||||
|
||||
### session.py — TTL-фикс + отмена
|
||||
- `touch(sid)`, `pause_ttl(sid)`, `resume_ttl(sid)` — продление/пауза/возобновление TTL.
|
||||
- В сессии `"cancel": threading.Event()`; `request_cancel(sid)`, `get_cancel_event(sid)`.
|
||||
|
||||
### scanner.py — прогресс и мягкая остановка LLM
|
||||
- `class CancelRequested`.
|
||||
- `scan_llm_ner(..., cancel_event, file_progress)`: отмена ТОЛЬКО между файлами (текущий добирается);
|
||||
события `file_start{chars,chunks}`, `file_chunk{chunks_done,chunks_total}`, `file_done{elapsed}`.
|
||||
|
||||
### obfuscator.py — частичный результат
|
||||
- `obfuscate()` возвращает `(zip, csv, meta)`: `meta=None` (штатно) или
|
||||
`{"cancelled": True, "processed", "total"}` (прерывание).
|
||||
- Трекинг `llm_done` (файлы с завершённым LLM). При отмене в LLM — в результат идут
|
||||
ТОЛЬКО файлы из `llm_done` (их обфускация корректна); при отмене в фазе замены —
|
||||
стоп после текущего файла. Отмена в replace НЕ проверяется, если LLM уже прерван
|
||||
(файлы из llm_done добираются).
|
||||
- Событие `extract_done{total_chars, per_file}` (объём текста для ETA).
|
||||
|
||||
### api_bp.py — API
|
||||
- `POST /api/cancel/<sid>`.
|
||||
- `process_stream`: `pause_ttl` в начале / `resume_ttl` в `finally`; передача `cancel_event`
|
||||
и `file_progress` в воркер; события `extract_done/file_start/file_chunk/file_done/cancelled`;
|
||||
heartbeat `llm` расширен: `eta_sec, done_chars, total_chars`; per-file ETA в `file_chunk`.
|
||||
- При закрытии вкладки (`disconnect`) ставится и `cancel`, и `cancel_event` (воркер останавливается).
|
||||
- legacy `process()` — фикс 3-значного возврата + TTL.
|
||||
|
||||
### index.html — UI
|
||||
- Кнопка «⏹ Прервать» + модал-подтверждение (объясняет, что сохранится).
|
||||
- Таблица 3 секций: «✓ Обработанные» (факт время) / «▶ Текущий файл» (прошло / ~осталось) /
|
||||
«○ Ожидают» (~оценка из скорости текущего файла).
|
||||
- Live-блок: «осталось ~X» (глобальная ETA), текущий файл с per-file ETA.
|
||||
- SSE-обработчики: extract_done/file_start/file_chunk/file_done/cancelled; done различает
|
||||
пропущенные (до extract_done) и готовые (после).
|
||||
|
||||
## Проверка
|
||||
- `py_compile` всех .py — OK; `node --check` (JS из index.html) — OK; `get_errors` — нет.
|
||||
- Локальные тесты логики отмены (FakeLLM): штатно=4 файла; отмена в LLM=3 сохранено;
|
||||
отмена до LLM=0..1 (текущий добирается); отмена после завершения=полный.
|
||||
- Смоук-тест: приложение создаётся, все роуты включая `/api/cancel/<sid>` зарегистрированы.
|
||||
|
||||
## ВАЖНО
|
||||
- Версия 0.0.63. **Код не задеплоен** — нужен редеплой на кластере и прогон реальных тестов
|
||||
(долгий прогон + прерывание).
|
||||
Reference in New Issue
Block a user