Compare commits

248 Commits
Author SHA1 Message Date
Repinoid 3fd6f1cffc 1
Deploy drhider / validate (push) Canceled after 0s
2026-09-14 18:40:13 +03:00
Repinoid 24bd89b511 Merge origin/master into master 2026-09-14 18:33:38 +03:00
Repinoid f90d97b276 chore: ignore agent temporary files 2026-09-14 18:25:27 +03:00
Repinoid 1ff305f38e chore: ignore nested upload-platform repository 2026-09-12 19:53:59 +03:00
Repinoid 741edafc4a chore: sync local VM mirror state 2026-09-12 19:48:37 +03:00
“Naeel” 04affca363 Merge remote-tracking branch 'origin/master'
Deploy drhider / validate (push) Canceled after 0s
2026-09-06 14:09:58 +03:00
“Naeel” d0128392e3 feat: add upload platform 2026-09-06 14:09:22 +03:00
“Naeel” 11716259a1 1
Deploy drhider / validate (push) Canceled after 0s
2026-09-05 13:27:35 +03:00
“Naeel” 4b33593a82 docs: document VM rsync sync
Deploy drhider / validate (push) Canceled after 0s
2026-08-31 20:59:26 +03:00
“Naeel” e7d771e00f fix: remove api blueprint syntax error
Deploy drhider / validate (push) Canceled after 0s
2026-08-31 20:55:26 +03:00
“Naeel” ce234132dd docs: README модуля — раздача в Flask, onXHR, слой 3
Deploy drhider / validate (push) Canceled after 0s
2026-08-26 07:12:27 +03:00
“Naeel” 3abcc6c1fc test: покрыть zip deflate (метод 8), skip в Node 18 (нет deflate-raw)
Deploy drhider / validate (push) Canceled after 0s
2026-08-25 19:17:52 +03:00
“Naeel” 0ad9e0047f fix: вернуть регистрацию activeXHR (onXHR) для отмены загрузки + тесты чистых функций
Deploy drhider / validate (push) Canceled after 0s
2026-08-25 19:14:30 +03:00
“Naeel” 52c3eb53f1 test: добавить нагрузочные/долгие тесты слоёв 1 и 2 (37 тестов всего)
Deploy drhider / validate (push) Canceled after 0s
2026-08-25 19:11:29 +03:00
“Naeel” 17e6dbd065 fix: resetAll — syncProcCtx до table.clear (неверный рендер «Готово 0/0» после новой сессии)
Deploy drhider / validate (push) Canceled after 0s
2026-08-25 17:29:17 +03:00
“Naeel” 53751a1f7b chore: удалить неиспользуемый site/session.py, bump v0.0.76
Deploy drhider / validate (push) Canceled after 0s
2026-08-25 08:56:45 +03:00
“Naeel” a33dff2f47 feat: интегрировать модуль upload в drhider (бэк blueprint + фронт ES-модули) 2026-08-25 08:52:50 +03:00
“Naeel” af172115b0 fix(upload): параметризация pullRetries/pullRetryDelay/pullTimeout, configure лимитов/TTL, onStatus в init, логирование sid/name 2026-08-25 08:37:09 +03:00
“Naeel” 5711e97fe5 feat: модуль upload — вынос слоёв 1 и 2 (выбор файлов + закачка через ВМ), файл-на-функцию, тесты 2026-08-25 08:08:14 +03:00
“Naeel” 5f5ed000b8 1
Deploy drhider / validate (push) Canceled after 0s
2026-08-25 07:58:51 +03:00
“Naeel” a18877415d docs: детализировать PLAN-upload-layers (2 слоя, файл-на-функцию, README для агента) 2026-08-25 07:57:55 +03:00
“Naeel” 7a4f642452 docs: тест стабильности v0.0.75 — состав, цикл, CSV/ZIP, кнопка, фильтр zip OK 2026-08-24 20:40:27 +03:00
“Naeel” 090f5e8993 docs: подтверждение v0.0.75 на проде — кнопка при 0 файлов, фильтр zip, регресс OK 2026-08-24 20:23:46 +03:00
“Naeel” d8dee114a3 docs: решение — историю git не переписываем, ключ убран из файлов, рекомендована ротация 2026-08-24 20:22:40 +03:00
“Naeel” f2141ec731 секрет-гигиена: убран LLM_API_KEY из README/DEPLOY/History (попал в коммите 0707d53 при копировании env из Штурвала)
Deploy drhider / validate (push) Canceled after 0s
2026-08-24 20:21:02 +03:00
“Naeel” eaa064a721 v0.0.75: кнопка disabled при 0 файлов, фильтр документов в expand_zips, README-риски
Deploy drhider / validate (push) Canceled after 0s
2026-08-24 20:18:44 +03:00
“Naeel” 6c51df96d9 docs: подтверждение v0.0.74 на проде 2026-08-24 20:12:49 +03:00
“Naeel” ba4546ecc9 v0.0.74: отмена в extract-фазе — прерывание не ждёт все .doc-конвертации
Deploy drhider / validate (push) Canceled after 0s
2026-08-24 20:07:23 +03:00
“Naeel” dd1e285c8c docs: подтверждение v0.0.73 на проде — SSRF/path traversal защита + регресс подпапок OK 2026-08-24 20:04:10 +03:00
“Naeel” ff4895b63b v0.0.73: фиксы по код-ревью Соннета — SSRF, path traversal, слабый SID, proc_error, zip-бомба, self-XSS
Deploy drhider / validate (push) Canceled after 0s
2026-08-24 19:58:27 +03:00
“Naeel” 4b73657e4c docs: код-ревью Соннета — 15 находок (SSRF, path traversal, слабый SID, zip-бомба, idx-мисматч и др.) 2026-08-24 19:50:50 +03:00
“Naeel” 82acc84311 docs: промпт для код-ревью Соннетом — строго ограниченный список файлов + 8 вопросов 2026-08-24 19:37:36 +03:00
“Naeel” 933ee79e3c docs: TODO — баг кнопки «Обфусцировать» активна при 0 файлов после «Новой сессии» (исправить при следующей правке, v0.0.73) 2026-08-24 19:35:42 +03:00
“Naeel” b0c055f926 docs: массовое тестирование v0.0.72 — 4 блока (папка/цикл/прерывание/UI) все OK; найден мелкий баг кнопки при 0 файлов 2026-08-24 19:34:49 +03:00
“Naeel” 44e12db886 docs: тест v0.0.72 на проде — статусы «не извлечён»/«пропущен (лимит)», счётчик дедупа, полный цикл OK 2026-08-24 19:28:05 +03:00
“Naeel” 08e4788a35 docs: временный сбой сертификата gitea — push починился сам, диагностика 2026-08-24 19:23:37 +03:00
“Naeel” e31476ebab v0.0.72: ретраи pull (3 попытки), корректная ошибка лимита сессии, счётчик дедупа
Deploy drhider / validate (push) Canceled after 0s
2026-08-24 19:14:53 +03:00
“Naeel” 0479afb44a v0.0.71: разделение статусов — «пропущен (лимит)» vs «не извлечён»; группа «Пропущены (сверх лимита)»; схема логики загрузки + найденные баги
Deploy drhider / validate (push) Canceled after 0s
2026-08-24 19:04:55 +03:00
“Naeel” cee18046c2 v0.0.70: заметная кнопка HELP в шапке (вместо «?») + пояснение статуса «пропущен» в ограничениях
Deploy drhider / validate (push) Canceled after 0s
2026-08-24 18:53:11 +03:00
“Naeel” ec660ddd1f docs: диагноз разового DNS-сбоя pull (gaierror -5) — единичная ошибка, работает 2026-08-24 18:47:28 +03:00
“Naeel” a901c4f023 docs: полный тест выбора папки v0.0.69 на проде — все сценарии OK, найден баг счётчика при дедупе 2026-08-24 18:42:21 +03:00
“Naeel” 89bb8a3442 docs: подтверждение v0.0.69 на проде — оба сценария папки с архивами работают 2026-08-24 18:39:28 +03:00
“Naeel” 2a49155b05 v0.0.69: архивы из папки не теряются — zip без документов добавляется как есть; раскрытие zip с документами; склонение счётчика
Deploy drhider / validate (push) Canceled after 0s
2026-08-24 17:10:21 +03:00
“Naeel” f45ecb603d v0.0.68: кнопка «Выбрать папку» — рекурсивный выбор папки с подпапками (webkitdirectory), относительный путь сохраняется
Deploy drhider / validate (push) Canceled after 0s
2026-08-24 16:47:48 +03:00
“Naeel” cfe6b9e2bf docs: план для Flash — кнопка «Выбрать папку» (webkitdirectory, рекурсивно, относительный путь) 2026-08-24 16:45:56 +03:00
“Naeel” d5b3ed3f68 docs: тесты v0.0.67 (загрузка/лимиты/ZIP-кириллица/ETA/прерывание) + разбор двухкластерного деплоя 2026-08-24 16:38:32 +03:00
“Naeel” 45c6f3b7f0 docs: v0.0.64-0.0.67 (тикеры/оценки, блокировки UI, фикс имён ZIP) + README History 2026-08-24 15:55:55 +03:00
“Naeel” 023dba3daa v0.0.67: фикс кириллических имён из ZIP (UTF-8 без флага -> мусор CP866) — строгая UTF-8 проверка в decodeZipName
Deploy drhider / validate (push) Canceled after 0s
2026-08-24 15:46:24 +03:00
“Naeel” c346be5add v0.0.66: блокировка выбора/удаления файлов на время загрузки и обработки, заморозка сессии после результата + кнопка Новая сессия
Deploy drhider / validate (push) Canceled after 0s
2026-08-24 15:42:37 +03:00
“Naeel” dd9785747d v0.0.65: мгновенные ориентировочные оценки времени обработки по каждому файлу и суммарно (сразу при выборе файлов)
Deploy drhider / validate (push) Canceled after 0s
2026-08-24 15:25:36 +03:00
“Naeel” efa18795db docs: README для History — описание каждой папки и каждого файла 2026-08-24 15:24:57 +03:00
“Naeel” a645493c32 v0.0.64: тикер текущего файла и оценки времени на всех этапах обработки (в т.ч. извлечение), самокалибровка скорости
Deploy drhider / validate (push) Canceled after 0s
2026-08-24 15:23:38 +03:00
“Naeel” db7cdbdd84 docs: сортировка History по темам (sonnet, opus, llm, sse, ux-frontend, upload, infra, plans, tests, base, code-fixes) 2026-08-24 15:23:31 +03:00
“Naeel” 25e4b46e76 v0.0.63: TTL-фикс, прерывание с сохранением, трекинг файлов/чанков, ETA (global+per-file), UI-таблица 3 секции + кнопка Прервать
Deploy drhider / validate (push) Canceled after 0s
2026-08-24 14:59:03 +03:00
“Naeel” 0e5f3ec9a1 docs: подробный план реализации для Flash (TTL+трекинг+прерывание+ETA+UI, v0.0.63) 2026-08-24 14:16:03 +03:00
“Naeel” 7a860d645d docs: дизайн TTL-фикса, прерывания с сохранением, оценки времени и UI (план, код не менялся) 2026-08-24 10:48:16 +03:00
“Naeel” 6542d51375 docs: тесты загрузки и обфускации 24.08 + найденный баг TTL<долгой обфускации (download 404) 2026-08-24 09:28:02 +03:00
“Naeel” 0dc5d8673d v0.0.62: лимиты 50МБ/файл, 500МБ/сессия (фронт+бэк), лимиты в UI
Deploy drhider / validate (push) Canceled after 0s
2026-08-24 08:22:54 +03:00
“Naeel” 2fd4afa907 docs: тесты реальной обфускации и фикс .doc/liberta (v0.0.61, без параллельности, 2×100 файлов — 0 ошибок конверсии) 2026-08-23 12:56:43 +03:00
“Naeel” 1509eeb989 v0.0.61: параллельность убрана — .doc через liberta последовательно (liberta не тянет конкурентность, javaldx), LLM _LLM_CONCURRENCY=1
Deploy drhider / validate (push) Canceled after 0s
2026-08-23 12:02:32 +03:00
“Naeel” c17fca792c docs: результаты edge-case и стресс-теста загрузки (v0.0.59–0.0.60, OOM на 4Gi, дедуп, лимиты) 2026-08-23 11:11:47 +03:00
“Naeel” 4087f10345 v0.0.60: дедуп по имя+размер (дату игнорируем) — при совпадении оставляем более свежий файл, дубль из архива/повтора не создаёт _2
Deploy drhider / validate (push) Canceled after 0s
2026-08-23 10:39:16 +03:00
“Naeel” 6dcbd2bade v0.0.59: убран лимит 100 МБ/файл — учитываются все файлы (остался только суммарный 1 ГБ)
Deploy drhider / validate (push) Canceled after 0s
2026-08-23 08:40:34 +03:00
“Naeel” 5ddee6b4bd 1
Deploy drhider / validate (push) Canceled after 0s
2026-08-23 07:11:06 +03:00
“Naeel” 82b597e1c2 drhider: тест-режим 'только загрузка' (?upload-only=1) — ранний выход до Фазы 2 (без обработки) + debug-endpoint /api/session_files 2026-08-23 07:05:52 +03:00
“Naeel” 2a735e34f8 docs: план тест-режима 'только загрузка' (?upload-only=1) для проверки ВМ-загрузки без обработки
Deploy drhider / validate (push) Canceled after 0s
2026-08-23 07:04:48 +03:00
“Naeel” 7be1a1f649 docs: план универсального файлового сервиса — статус реализовано/задеплоено (v1.0.0, ВМ) 2026-08-21 16:23:49 +03:00
“Naeel” 7537e7ffe1 chore: убрать file_list.csv из репозитория (data-артефакт, не относится к коду)
Deploy drhider / validate (push) Canceled after 0s
2026-08-21 15:40:32 +03:00
“Naeel” 04f6d7f81a v0.0.58: ВМ-загрузка (паттерн ВМ-буфер+pull) — PUT на ВМ + /api/upload_refs (egress), nginx /drhider-upload/ + CORS + TTL
Deploy drhider / validate (push) Canceled after 0s
2026-08-21 15:40:24 +03:00
“Naeel” 705b39ab7d docs: актуализация README/ARCHITECTURE + план паттерна ВМ-буфер+pull (снимок перед ВМ-инкапсуляцией загрузки)
Deploy drhider / validate (push) Canceled after 0s
2026-08-21 15:35:45 +03:00
“Naeel” a9d4139afb v0.0.57: LLM обрабатывает ВСЕ данные — пофайловый чанкинг (6000/overlap 500), параллельность 4, дедуп+верификация против полного текста
Deploy drhider / validate (push) Canceled after 0s
2026-08-20 10:01:01 +03:00
“Naeel” da0ea5e964 docs: Q&A Sonnet по LLM-чанкингу (схема + 6 уточнений, решение к внедрению)
Deploy drhider / validate (push) Canceled after 0s
2026-08-20 09:58:56 +03:00
“Naeel” 670f533f48 v0.0.56: КРИТИЧНО — mapping.csv (ключ расшифровки) убран из выходного ZIP, CSV только отдельно
Deploy drhider / validate (push) Canceled after 0s
2026-08-20 09:38:28 +03:00
“Naeel” dfa325a5c5 v0.0.55: A1 threading для .doc (liberta), A2 кэш compiled regex — безопасное ускорение
Deploy drhider / validate (push) Canceled after 0s
2026-08-20 09:20:30 +03:00
“Naeel” 59a1924bd0 v0.0.54: В2 фикс кракозябр имён zip (utf-8+валидация→cp866), В1 фильтр таблиц (безопасен, эффект≈0)
Deploy drhider / validate (push) Canceled after 0s
2026-08-20 09:10:16 +03:00
“Naeel” 62067bc99f docs: ревью Sonnet — промпт, follow-up вопросы, Q&A с ответами 2026-08-20 09:04:03 +03:00
“Naeel” fe7c238c0b v0.0.53: устойчивость к битым файлам — воркер не падает, битые пропускаются, остальные обрабатываются
Deploy drhider / validate (push) Canceled after 0s
2026-08-20 08:30:30 +03:00
“Naeel” 3ae7d325e2 v0.0.52: файловый лог /tmp/drhider.log (LOG_FILE env) — логи видны в поде через kubectl exec
Deploy drhider / validate (push) Canceled after 0s
2026-08-20 08:12:23 +03:00
“Naeel” 59a66278a3 v0.0.51: максимальное логирование (LOG_LEVEL env) + детальные логи SSE/воркера для диагностики обрыва
Deploy drhider / validate (push) Canceled after 0s
2026-08-20 07:55:11 +03:00
“Naeel” 551ed8cf42 v0.0.50: лимиты загрузки (>100МБ/файл, >1ГБ сумма) — красная подсветка в таблице, исключение из обфускации
Deploy drhider / validate (push) Canceled after 0s
2026-08-20 07:25:02 +03:00
“Naeel” 0a94d3f449 docs: бенчмарк UI 5 прогонов DownLoads.zip (v0.0.49) 2026-08-19 19:59:45 +04:00
“Naeel” b30dd09e1f chore: .gitignore — добавить files/ (тестовые PDF)
Deploy drhider / validate (push) Canceled after 0s
2026-08-19 19:28:15 +04:00
“Naeel” 7389e1009a v0.0.49: индикатор распаковки архивов, разделение фаз (загрузка/обработка), таймауты SSE на ingress 1800с
Deploy drhider / validate (push) Canceled after 0s
2026-08-19 18:49:55 +04:00
“Naeel” 4bdcde86b7 v0.0.48: версия (без понижения, откат фичи выбора папки остаётся в коде)
Deploy drhider / validate (push) Canceled after 0s
2026-08-19 18:43:03 +04:00
“Naeel” f5ca8c3eb5 Revert "v0.0.47: выбор целой папки (webkitdirectory) — рекурсивно, как раскрытие ZIP"
Deploy drhider / validate (push) Canceled after 0s
This reverts commit b5a14cfc3d.
2026-08-19 18:41:43 +04:00
“Naeel” b5a14cfc3d v0.0.47: выбор целой папки (webkitdirectory) — рекурсивно, как раскрытие ZIP
Deploy drhider / validate (push) Canceled after 0s
2026-08-19 18:29:34 +04:00
“Naeel” 612979ed6e docs: удаление process_names + git push HTTP/2 проблема (решение HTTP/1.1)
Deploy drhider / validate (push) Canceled after 0s
2026-08-19 18:23:23 +04:00
“Naeel” d2f2bd39f4 v0.0.46: удалён тестовый API process_names (нельзя указывать локальные файлы — сервер не видит машину клиента)
Deploy drhider / validate (push) Canceled after 0s
2026-08-19 14:54:17 +04:00
“Naeel” 8d7a7dfaf6 v0.0.45: тестовый API process_names — обфускация файлов по именам (только при валидном TEST_INPUT_DIR)
Deploy drhider / validate (push) Canceled after 0s
2026-08-19 14:47:16 +04:00
“Naeel” 2224e20bde v0.0.44: во время LLM live-блок показывает 'ИИ анализирует все файлы', а не имя последнего файла
Deploy drhider / validate (push) Canceled after 0s
2026-08-19 14:43:01 +04:00
“Naeel” 2f5a94bf3f docs: диагностика нестабильной загрузки + аннотации ingress + таймаут 300с
Deploy drhider / validate (push) Canceled after 0s
2026-08-19 14:33:26 +04:00
“Naeel” 70a47ac905 v0.0.43: таймаут загрузки 30с → 300с (загрузка больших файлов нестабильна через шлюз) 2026-08-19 14:33:08 +04:00
“Naeel” c67fffbc56 v0.0.42: таймер в статусе тикает только у текущего файла (остальные останавливаются при start)
Deploy drhider / validate (push) Canceled after 0s
2026-08-19 14:00:10 +04:00
“Naeel” 0453827b8b v0.0.41: время на конкретный файл в колонке Статус (бэк считает, без общего LLM)
Deploy drhider / validate (push) Canceled after 0s
2026-08-19 13:46:00 +04:00
“Naeel” 020f9d43c6 v0.0.40: имя файла в live-блоке + сброс старых итогов при повторной обфускации
Deploy drhider / validate (push) Canceled after 0s
2026-08-19 13:36:12 +04:00
“Naeel” b46198d74a dr
Deploy drhider / validate (push) Canceled after 0s
2026-08-19 12:05:27 +04:00
“Naeel” bc09ada90e v0.0.39: оптимизация apply_replacements — однопроходная замена вместо N×re.sub (ускорение в разы)
Deploy drhider / validate (push) Canceled after 0s
2026-08-19 09:47:16 +04:00
“Naeel” e548c89880 v0.0.38: живой таймер обработки + индикация ИИ (live-блок, heartbeat llm в SSE)
Deploy drhider / validate (push) Canceled after 0s
2026-08-19 09:25:01 +04:00
“Naeel” defbc10004 v0.0.37: крупный заметный блок статистики (общее время, время ИИ, токены) в UI
Deploy drhider / validate (push) Canceled after 0s
2026-08-19 09:16:18 +04:00
“Naeel” af9faba3fc v0.0.36: подсказка форматов на странице — добавлены .doc и .md
Deploy drhider / validate (push) Canceled after 0s
2026-08-19 09:10:33 +04:00
“Naeel” 5677c59261 v0.0.35: при раскрытии ZIP — только документы (pdf, doc, docx, txt, md), изображения пропускаются
Deploy drhider / validate (push) Canceled after 0s
2026-08-19 09:08:31 +04:00
“Naeel” 02d9592eb5 v0.0.34: вывод токенов и времени LLM в UI (суммарно за прогон)
Deploy drhider / validate (push) Canceled after 0s
2026-08-19 09:00:40 +04:00
“Naeel” 53a1fb40d2 v0.0.33: раскрытие ZIP в таблицу (фронт) — нативный unzip, рекурсия, дедуп по размер+дате
Deploy drhider / validate (push) Canceled after 0s
2026-08-19 08:36:41 +04:00
“Naeel” 0ad5a5fabd test: .doc заглушка — актуальный формат ошибки сервиса; docs: итоги v0.0.32
Deploy drhider / validate (push) Canceled after 0s
2026-08-18 23:08:43 +04:00
“Naeel” 94e588aaba v0.0.32: фиксы по ревью — единый обфускатор, рекурсия ZIP, кириллица 1С, дедуп, SSE-дисконнект
Deploy drhider / validate (push) Canceled after 0s
2026-08-18 23:03:31 +04:00
Repinoid c3c6e5b584 Добавлены списки поступающих в магистратуру 45.04.02 Лингвистика (очная, бюджет) и общие коды: Сеченов, МГУ ФИЯР, МГЛУ (2026-08-04) 2026-08-04 21:06:50 +04:00
“Naeel” 301ceecaa8 docs: v0.0.30-31 — .doc поддержка, обновление версии 2026-07-16 12:24:47 +04:00
“Naeel” cb6274a887 fix: .doc теперь обрабатывается (обфускация + md в архиве) 2026-07-16 12:16:54 +04:00
“Naeel” 87a6df45c5 v0.0.30: .doc поддержка — accept + liberta API вместо LibreOffice 2026-07-16 11:28:16 +04:00
“Naeel” c102d54887 chore: gitignore + clean 2026-07-16 06:54:06 +04:00
“Naeel” a182a2f38a docs: MIGRATION-GUIDE — дополнен фиксами из v0.0.27-29 (fetch, dedup, Blob download) 2026-07-16 06:53:19 +04:00
“Naeel” 5f0ca8e463 docs: PROBLEM-AND-SOLUTION.md — переписан, только факты, без ложных следов 2026-07-16 06:50:29 +04:00
“Naeel” 8484197eec docs: финальный диагноз — keep-alive 10 единственная причина RST 2026-07-15 19:15:17 +04:00
“Naeel” 6e8f79ac56 docs: итоговая диагностика RST — hostPort vs cloud LB, корневая причина 2026-07-15 19:09:24 +04:00
Repinoid 2aea7bf2f6 fix: move cluster dump TMP → History 2026-07-15 18:46:35 +04:00
Repinoid 0ff904ceb6 cluster dump 2026-07-15: ingress, kube-vip, drhider state 2026-07-15 18:34:43 +04:00
“Naeel” fd4f152fc8 chore: убрал большие файлы из git 2026-07-15 15:18:25 +04:00
“Naeel” d205118cd5 chore: всё закоммичено — TMP, gitignore, chrome log 2026-07-15 15:15:06 +04:00
“Naeel” 4af247938f docs: полное состояние проекта на 2026-07-15 2026-07-15 15:14:20 +04:00
“Naeel” 0640e7d47f docs: формат имён скачиваемых файлов в архитектуру 2026-07-15 15:11:11 +04:00
“Naeel” 5efed1c577 fix: имя файла из Content-Disposition а не хардкод 2026-07-15 15:06:43 +04:00
“Naeel” 40372fe03f fix: Выбрать файлы добавляет а не стирает таблицу + dedup 2026-07-15 15:01:35 +04:00
“Naeel” e66dff23b8 fix: download ZIP/CSV через fetch+Blob вместо window.location — не сбрасывает страницу 2026-07-15 14:57:03 +04:00
“Naeel” 68fada075e chore: финальная документация — всё закоммичено 2026-07-14 22:02:25 +04:00
“Naeel” 338cb2f372 docs: инструкция миграции UI и загрузки с ВМ на Flask (для Сверки Контрактов) 2026-07-14 22:01:43 +04:00
“Naeel” 9b4060d8c5 docs: требования к Flask-приложению для Штурвала — health, no_cache, SSE, BOM, etc 2026-07-14 21:58:56 +04:00
“Naeel” 8158a8bc01 docs: полная конфигурация кластера — MTU + keep-alive + HTTP/2 + RST fix 2026-07-14 21:54:23 +04:00
“Naeel” 26572abb3b fix: JS warmup GET /health на загрузке + keep-alive 75 + bump v0.0.26 2026-07-14 20:40:26 +04:00
“Naeel” cd29e727f3 fix: uploadFiles не теряет файлы после resetAll — сохраняет sf.slice() 2026-07-14 19:31:44 +04:00
“Naeel” e513bf3c9e fix: es → activeES во всех местах (был не заменён на строке 262) 2026-07-14 19:29:29 +04:00
“Naeel” 17bc4aebf8 fix: сброс состояния при F5/закрытии — beforeunload, abort XHR, close EventSource 2026-07-14 19:22:36 +04:00
“Naeel” b3298b415e docs: PROBLEM-AND-SOLUTION, правила, история Connection:close, gitignore 2026-07-14 19:19:12 +04:00
“Naeel” 5e7319cd42 fix: убрать Connection: close — Waitress/WSGI запрещает hop-by-hop заголовки 2026-07-14 13:28:00 +04:00
“Naeel” 204e865aff docs: исправлен PROBLEM-AND-SOLUTION.md — mtu 1400 а не 1450, убран раздел Опуса, обновлены результаты 2026-07-14 13:27:06 +04:00
“Naeel” 6fe1ba2b42 fix: Connection: close на всех ответах — nginx не кэширует upstream keepalive 2026-07-14 13:21:31 +04:00
“Naeel” 0ceaee1089 fix: XHR + cache-busting ?_=Date.now() чтобы исключить connection reuse 2026-07-14 10:31:26 +04:00
“Naeel” b6dac6f2bd fix: XHR timeout 30с + ontimeout — не висеть бесконечно на загрузке 2026-07-14 10:24:55 +04:00
“Naeel” 79cd25a762 fix: SSE — отлов GeneratorExit при отключении клиента
- api_bp.py: try/except GeneratorExit после каждого yield в generate()
- при F5 во время обработки поток освобождается, остальные файлы не обрабатываются
- bump: 0.0.17 → 0.0.18
2026-07-14 09:15:44 +04:00
“Naeel” bbffb00eb8 fix: убрать .doc из UI и документации (LibreOffice недоступен на Штурвале) 2026-07-14 08:30:37 +04:00
“Naeel” ddbf67924b docs: добавить раздел Kubernetes-кластер в DEPLOY.md (kubectl, ingress, cilium) 2026-07-14 07:42:32 +04:00
“Naeel” 00d54dbfb6 feat: поддержка .doc через LibreOffice headless
- extractor.py: добавлена doc_to_markdown() — .doc → .docx (soffice) → docx_to_markdown()
- Dockerfile: apt install libreoffice-writer
- ARCHITECTURE.md: обновлён статус .doc
- bump: 0.0.16 → 0.0.17
2026-07-14 07:21:39 +04:00
“Naeel” 444ef4b732 docs: добавить раздел «Поддерживаемые форматы» в ARCHITECTURE.md 2026-07-14 07:04:42 +04:00
“Naeel” f072fc9fdb docs: уточнение — реконсил по событию, не по таймеру 2026-07-13 22:18:03 +04:00
“Naeel” f38ba4ca37 docs: финальный обзор Опуса + правки (Ethernet, 51с) 2026-07-13 21:57:25 +04:00
“Naeel” 82b5cb4402 docs: схема провайдерской инкапсуляции + таблица параметров 2026-07-13 21:53:15 +04:00
“Naeel” ec9107cff4 docs: eth0=1500, bottleneck в сети провайдера, 1450 empirically 2026-07-13 21:49:36 +04:00
“Naeel” fe45b07c4f docs: ping DF блокирован файрволлом, 1450 подтверждён эмпирически 2026-07-13 21:48:00 +04:00
“Naeel” cd22fd62a6 docs: исправлены ошибки (TCP backoff, DSR, ping) + ответ Опуса 2026-07-13 21:45:44 +04:00
“Naeel” 5e4418f648 docs: убраны домыслы — причина 1450 неизвестна, факт установлен замером 2026-07-13 21:38:02 +04:00
“Naeel” 2949d0ef09 docs: пояснение — почему Cilium ожидает 1500 2026-07-13 21:36:21 +04:00
“Naeel” 1ad35dada7 docs: flowchart — Geneve над данными через --- 2026-07-13 21:34:55 +04:00
“Naeel” ae09c23d81 docs: Geneve над данными, без жаргона 2026-07-13 21:33:04 +04:00
“Naeel” d0f12ef4a3 docs: горизонт + 50 над 1450 в subgraph 2026-07-13 21:30:59 +04:00
“Naeel” a34a78d8b9 docs: вертикальная схема — шапка сверху, тело снизу, дверь не пускает 2026-07-13 21:29:38 +04:00
“Naeel” ab08acd60d docs: Mermaid — пакет + шапка = не влазит в дверь 2026-07-13 21:28:12 +04:00
“Naeel” f005e8cf29 docs: простая ASCII-схема — рост пакета vs дверь 2026-07-13 21:26:41 +04:00
“Naeel” 1da496c2e5 docs: схема яснее — один путь от А к Б через Geneve 2026-07-13 21:25:13 +04:00
“Naeel” 17fc2670a5 docs: Mermaid flowchart вместо block-beta 2026-07-13 21:22:28 +04:00
“Naeel” 7ed1f5d650 docs: Mermaid-диаграмма вместо ASCII 2026-07-13 21:21:19 +04:00
“Naeel” 3df63df0b0 docs: ASCII-схема пакета Geneve и MTU 2026-07-13 21:20:28 +04:00
“Naeel” 75568a2586 docs: пояснение Cilium и Geneve курсивом перед списком 2026-07-13 21:18:02 +04:00
“Naeel” 9ea3c2991a docs: причина расписана по шагам с жирными числами 2026-07-13 21:15:48 +04:00
“Naeel” cbf7de27d8 docs: результаты в тестовом кластере + рекомендации для production 2026-07-13 21:06:19 +04:00
“Naeel” d94fa3f556 v0.0.16: Nubes логотип, favicon, версия слева, модальное окно Help 2026-07-13 18:11:52 +04:00
“Naeel” 7d39885d90 docs: проблема описана обще — HTTP POST на managed Flask через ingress 2026-07-13 18:02:38 +04:00
“Naeel” e7da82538f docs: безличный стиль — «были применены» вместо «мы» 2026-07-13 18:00:27 +04:00
“Naeel” a91a5e9538 docs: уточнено — что именно сделано сейчас (Local + 4 реплики) 2026-07-13 17:55:21 +04:00
“Naeel” c25b22cfe6 docs: PROBLEM-AND-SOLUTION v2 — полный анализ Опуса 4.8 2026-07-13 17:49:42 +04:00
“Naeel” 68571fbbcd docs: PROBLEM-AND-SOLUTION — корневой .md для девопса 2026-07-13 17:45:36 +04:00
“Naeel” eaef850421 docs: замер MTU — confirmed underlay=1450, tunnel=1400 2026-07-13 17:42:03 +04:00
“Naeel” 0950b68649 docs: MSS-CLAMPING-PLAN v2 — исправлено после валидации Opus (underlay MTU, замер, рестарт) 2026-07-13 17:29:12 +04:00
“Naeel” 322626a372 docs: запрос Opus — валидация MSS Clamping плана для девопса 2026-07-13 17:22:54 +04:00
“Naeel” f9b88cbf93 v0.0.15: fix _md_tolerant_pattern — всегда, не только при промахе 2026-07-13 17:18:00 +04:00
“Naeel” a56a819329 v0.0.14: MSK time (UTC+3) — ZIP entries + download filenames 2026-07-13 17:05:16 +04:00
“Naeel” f3c854c5fb v0.0.13: fix Markdown-bold splitting + \xa0 in PERSON_PATTERN 2026-07-13 13:18:10 +04:00
“Naeel” 23b4ad9889 docs: ответ Sonnet — фикс Markdown-жирности и \xa0 в PERSON_PATTERN 2026-07-13 13:14:42 +04:00
“Naeel” 29e2d08cdb docs: MSS-CLAMPING-PLAN — корневое решение вместо Local+4реплики 2026-07-13 13:12:55 +04:00
“Naeel” 0cdef67c1f docs: ответ Sonnet — TMP-файлы от старого кода, _next_token() чист 2026-07-13 12:41:08 +04:00
“Naeel” 8c59c4b7e0 docs: запрос Sonnet — «токены» заменены на «коды замены» 2026-07-13 12:35:16 +04:00
“Naeel” 70226d8c39 docs: запрос Sonnet — анализ токенов обфускации 2026-07-13 12:33:03 +04:00
“Naeel” 4a4de0f8e2 docs: INGRESS-SOLUTIONS — добавлен Opus анализ + helm upgrade команды 2026-07-13 11:55:36 +04:00
“Naeel” 38109ba6ec docs: INGRESS-SOLUTIONS — Sonnet анализ вариантов DaemonSet/MTU/DSR 2026-07-13 11:53:47 +04:00
“Naeel” adab2c624f v0.0.12: fix download — window.location вместо a.click() 2026-07-13 11:44:43 +04:00
“Naeel” a380c0e70c docs: KUBEVIP — Helm auto-reset root cause, updated pod list 2026-07-13 11:39:50 +04:00
“Naeel” bbc45e155f v0.0.11: fix download — a.appendTo(body) перед click 2026-07-13 11:13:28 +04:00
“Naeel” 7bdcb5b7b4 v0.0.10: fix download filename — timestamp локальное время 2026-07-13 11:09:20 +04:00
“Naeel” c878b3db1a v0.0.9: per-file timer — 3.2с → ✓ 12.5с 2026-07-13 11:06:52 +04:00
“Naeel” bf202cdfa4 v0.0.8: fix SSE format — event: строка отдельно от data: 2026-07-13 10:59:03 +04:00
“Naeel” e3405556b5 v0.0.7: SSE per-file progress — таблица обновляется по каждому файлу 2026-07-13 10:55:43 +04:00
“Naeel” 46d8ced0dd v0.0.6: прогресс загрузки в строке (XMLHttpRequest onprogress) 2026-07-13 10:52:09 +04:00
“Naeel” e4975fb37e v0.0.5: CSV отдельно, timestamp имена, прогресс в таблице 2026-07-13 10:40:00 +04:00
“Naeel” 613b645456 v0.0.4: fix — restore index.html (accidentally deleted) 2026-07-13 10:33:56 +04:00
“Naeel” b7075dd8bf docs: KUBEVIP-EXTERNAL-TRAFFIC-POLICY — ingress replicas fix 2026-07-13 10:33:30 +04:00
“Naeel” 75c7424963 v0.0.3: таблица + кнопка, без JS 2026-07-13 10:14:22 +04:00
“Naeel” 0d16bb6520 v0.0.2: minimal Hello World — только шапка Nubes + версия 2026-07-13 10:10:06 +04:00
“Naeel” 491b49f633 v0.0.1: revert to v7.0.0 code — api_bp unified, no Connection close, version reset 2026-07-13 10:04:15 +04:00
“Naeel” 76a2d4b7ed v0.0.3: fix Connection header banned by WSGI PEP 3333 2026-07-13 09:25:07 +04:00
“Naeel” c35e8da357 v0.0.2: split api_bp into upload/process/download modules, Connection: close, upload tests (13 new, 106 total) 2026-07-13 09:15:38 +04:00
“Naeel” 345448caa4 reset version 0.0.1 2026-07-13 08:45:52 +04:00
“Naeel” 18922b486b v7.0.0: API upload/process/download, session.py, фронт без обработки — всё на бэкенде 2026-07-13 08:12:20 +04:00
“Naeel” e590d8ab9a v6.0.0: вся обработка на бэкенде — zip.js удалён, фронт только XHR+скачивание 2026-07-13 07:53:08 +04:00
“Naeel” cbf619aaba docs: принципы 6-7 — вся обработка на бэкенде, пофайлово, фронт только UI 2026-07-13 07:48:19 +04:00
“Naeel” 0998892d7f v5.2.1: Dockerfile как у шаблона Baldurs-Gate (CMD python site/app.py) — без него ingress дропает соединения 2026-07-13 07:31:28 +04:00
“Naeel” d0e874d87e v5.2.0: waitress вместо app.run() — production WSGI, 8 потоков, исправляет ERR_TIMED_OUT 2026-07-12 16:05:44 +04:00
“Naeel” 81bdee6203 v5.1.0: тесты builder(20), extractor(15), scanner(20), replacer(10) — 65 тестов, 0 ошибок 2026-07-12 15:31:42 +04:00
“Naeel” efaa42dd2e v5.0.9: ZIP модуль вынесен в site/static/zip.js, тесты test_zip.py (28 тестов), исправлен баг смещения start+18 2026-07-12 15:25:39 +04:00
“Naeel” 9145345882 fix: баг в unzip() — неверное смещение compressed size (+14 вместо start+18) 2026-07-12 15:01:33 +04:00
“Naeel” ee42865fcd fix: убран Connection: close — каждый статик-файл требовал новый TCP handshake 2026-07-12 14:55:16 +04:00
“Naeel” 3ba201748c минимизация copilot-instructions.md (3 строки) + CODERULES.md отдельно 2026-07-12 14:53:38 +04:00
“Naeel” f98b4ba7a5 fix: убрал правило из copilot-instructions.md (экономия токенов), оставил в ARCHITECTURE.md 2026-07-12 14:51:40 +04:00
“Naeel” a1574677a5 docs: правило «ноль внешних зависимостей» в ARCHITECTURE.md и copilot-instructions.md 2026-07-12 14:50:42 +04:00
“Naeel” 1a84770e82 v5.0.2: нативный ZIP (unzip + buildZip) — 0 внешних зависимостей, страница грузится мгновенно 2026-07-12 14:48:59 +04:00
“Naeel” 75b8476703 fix: JSZip локально вместо CDN — убирает задержку 10с 2026-07-12 14:42:25 +04:00
“Naeel” 370a869aef v5.0.0: гибридные токены (Иванов_0001, ООО_Технология_0034), удалены generators/, checksum, random_utils 2026-07-12 14:30:12 +04:00
“Naeel” 62c7ea0162 v4.0.2: бегущий секундомер в колонке статуса ( 5с, 6с...) 2026-07-12 14:19:44 +04:00
“Naeel” df15f48ecc fix: Connection: close в after_request — XHR не держат keep-alive после запросов 2026-07-12 14:12:43 +04:00
“Naeel” adf3002e3d v4.0.0: дизайн Nubes, логотип, ZIP без mapping.csv внутри, CSV отдельно с датой-временем 2026-07-12 13:52:24 +04:00
“Naeel” 59f7601266 fix: no-cache заголовки (Cache-Control, Pragma, Expires) — F5 больше не кэширует 2026-07-12 13:11:48 +04:00
“Naeel” f15af2866d fix: дата/время файлов в ZIP (time.localtime), bump 3.2.3 2026-07-12 13:07:07 +04:00
“Naeel” 1d91d5f92e bump 3.2.1 → 3.2.2 2026-07-12 12:59:16 +04:00
“Naeel” fd5e1613a5 fix: ссылки в copilot-instructions.md (../ вместо ./) 2026-07-12 12:58:29 +04:00
“Naeel” 303830528f bump 3.2.0 → 3.2.1 2026-07-12 12:56:39 +04:00
“Naeel” 3d55562f02 fix: безопасный re.match в генераторах + сброс fileInput при загрузке страницы 2026-07-12 12:54:40 +04:00
“Naeel” dacf0d584f v3.2.0: новый LLM-промпт (STEP 1/2, фикс. типы, NOT PII, already_found), убран хардкод НУБЕС 2026-07-12 11:14:26 +04:00
“Naeel” 5c426cb037 v3.2.0: новый LLM-промпт (STEP 1/2, фикс. типы, NOT PII, already_found), убран хардкод НУБЕС 2026-07-12 11:11:43 +04:00
“Naeel” 0c2b636d31 v3.1.0: пофайловая обработка, прогресс в таблице, JSZip — один ZIP на выходе 2026-07-12 10:50:51 +04:00
“Naeel” a87361c159 bump version 3.0.1 → 3.0.2 2026-07-12 10:33:54 +04:00
“Naeel” 7fc41aabce frontend: пофайловая загрузка с прогрессом, убран gunicorn 2026-07-12 10:33:09 +04:00
“Naeel” 2731bc7f09 fix: вернул gunicorn+gevent — без них Flask dev server не держит долгие LLM-запросы 2026-07-12 10:18:04 +04:00
“Naeel” 724124b3fc fix: убрал тестовый файл из репы, добавил в .gitignore 2026-07-12 10:17:01 +04:00
“Naeel” 72ae915935 fix: app.run(threaded=True) для долгих запросов 2026-07-12 10:16:52 +04:00
“Naeel” 3e5aef70a0 fix: добавлен app.run() — Штурвал запускает python app.py, нужен listen на :5000 2026-07-12 09:43:34 +04:00
“Naeel” 34f9a6f74f fix: импорт routes в app.py (site конфликтует со stdlib, нельзя from .routes) 2026-07-12 09:29:59 +04:00
“Naeel” 84ae7439f5 History: v3.0.0 завершён 2026-07-12 09:15:13 +04:00
“Naeel” 2a2cc1467c v3.0.0: универсальная архитектура — MD-пайплайн, LLM сама определяет типы, fallback-генератор 2026-07-12 09:14:45 +04:00
“Naeel” f5427b0db7 replacer.py: удалён replace_in_text (заменён на apply_replacements напрямую) 2026-07-12 09:14:22 +04:00
“Naeel” af2f2a9f71 extractor.py → MD-пайплайн (docx_to_markdown, pdf_to_markdown), obfuscator.py — новый пайплайн без docx-объектов 2026-07-12 09:13:47 +04:00
“Naeel” 2ceeb05a8b scanner.py: универсальный LLM-промпт (LLM сама определяет типы) + fallback-генератор 2026-07-12 09:11:29 +04:00
“Naeel” 579f9c8334 generators: fallback.py — character-class preserving для неизвестных типов 2026-07-12 09:09:54 +04:00
“Naeel” d607d7d306 Организация: .md файлы → docs/, обновлены ссылки 2026-07-12 08:46:12 +04:00
“Naeel” 4c3f9d4e49 Разделение: copilot-instructions.md (правила) + DEVELOPMENT.md (порядок правок) 2026-07-12 08:43:42 +04:00
“Naeel” cc2f627de2 History: документация 2026-07-12 2026-07-12 08:38:07 +04:00
“Naeel” 0707d53b37 Документация: README.md, ARCHITECTURE.md, DEPLOY.md, .github/copilot-instructions.md 2026-07-12 08:37:51 +04:00
“Naeel” 7a005f7b06 History: результаты рефакторинга 2026-07-12 08:34:40 +04:00
“Naeel” 51bc1a79e7 Фаза 2: site/app.py → blueprint'ы (main, health, api), старый drhider.py удалён 2026-07-12 08:34:06 +04:00
“Naeel” 2cf4e08100 Фаза 2: llm_client.py, extractor.py, scanner.py, replacer.py, builder.py, obfuscator.py, __init__.py 2026-07-12 08:24:33 +04:00
“Naeel” 37581eb601 Фаза 2: config.py, checksum.py, random_utils.py, generators/ (11 модулей) 2026-07-12 08:22:20 +04:00
“Naeel” a2f754ab4f Фаза 1: удалён Dockerfile, drhider_server.py, gunicorn из requirements, __main__ блок 2026-07-12 08:19:45 +04:00
204 changed files with 20222 additions and 988 deletions
+24
View File
@@ -0,0 +1,24 @@
# ⛔ ЖЁСТКИЕ ПРАВИЛА
- НЕЛЬЗЯ менять код без «делай» — показать план → ждать
- НЕЛЬЗЯ менять ЧТО-ЛИБО при вопросе (?, почему, как, где, что) — только ответ словами
- НЕЛЬЗЯ спешить — обдумать, проверить, потом отвечать
- НЕЛЬЗЯ лезть в /home/naeel/nubes/contracts/ (кроме loadtest)
- НЕЛЬЗЯ предполагать — сомневаешься = спроси
- НЕЛЬЗЯ грузить внешние CDN/библиотеки на фронтенд — всё в коде
# ✅ ПОРЯДОК ПРАВКИ
1. План → «делай»
2. Правим → проверяем syntax → проверяем import
3. git add -A && git commit -m "..." && git push
4. Bump VERSION в site/app.py перед push
# 📋 ДОКИ
- Порядок правок: docs/DEVELOPMENT.md
- Архитектура: docs/ARCHITECTURE.md
- Деплой: docs/DEPLOY.md
- История: History/
# 📋 ПРОЕКТ
Штурвал Managed Flask, без Dockerfile/gunicorn, без БД
URL: https://drhider.pythonk8s.dev.nubes.ru/
ВМ legacy: ssh -i ~/.ssh/naeel_vm_id_ed25519 naeel@5.172.178.213
+7
View File
@@ -0,0 +1,7 @@
# ⛔⛔⛔ STOP: без «делай» ничего не делать
# ⛔⛔⛔ ВОПРОС (?, почему, как, где, что) — ТОЛЬКО ОТВЕТ. Читать/узнавать МОЖНО, менять НЕЛЬЗЯ.
# ⛔ Не спешить. Не лезть в contracts/. Не гадать — спросить.
НИКОГДА не действовать по догадкам !!!
ВСЕГДА всё перепроверять перед действием или ответом !!!
ВСЕГДА КРАТКО без воды отвечать, чётко по делу, без лишних слов.
# 📋 Правила: .github/CODERULES.md
+26
View File
@@ -1,3 +1,29 @@
__pycache__/
*.pyc
.env
.env.*
!.env.example
venv/
.venv/
env/
ENV/
instance/
.flaskenv
.pytest_cache/
.mypy_cache/
.ruff_cache/
.coverage
coverage.xml
htmlcov/
*.log
build/
dist/
*.egg-info/
# TMP/
# about1500.md
*.har
chrome-net-export-log.json
TSTFILES/
files/
upload-platform/
.agent-tmp/
+3
View File
@@ -0,0 +1,3 @@
{
"python-envs.defaultEnvManager": "ms-python.python:system"
}
Binary file not shown.
+5 -2
View File
@@ -1,8 +1,11 @@
FROM python:3.12-slim
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends \
libreoffice-writer \
&& rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY drhider.py drhider_server.py /app/
COPY site /app/site
COPY drhider /app/drhider
EXPOSE 5000
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--timeout", "300", "--workers", "1", "--worker-class", "gevent", "--limit-request-field_size", "0", "--limit-request-body", "0", "site.app:app"]
CMD ["python", "site/app.py"]
+152
View File
@@ -0,0 +1,152 @@
# History — журнал проекта DrHider
Хронологический журнал: планы, вопросы/ответы ревьюеров (Sonnet, Opus),
реализации фич (по версиям), диагностика, тесты и бенчмарки.
**Правило:** любая находка/решение/факт записывается сюда сразу, в момент обнаружения.
## Структура (папки по темам)
| Папка | Что внутри |
|-------|-----------|
| [base/](base/) | Ранняя документация и первоначальная настройка проекта |
| [plans/](plans/) | Планы и проектирование (аудит, архитектуры v3/v3.1, планы реализации) |
| [sonnet/](sonnet/) | Все вопросы к Sonnet и его ответы (обсуждения, ревью, Q&A) |
| [opus/](opus/) | Запросы к Opus и его ответы (валидация MSS-clamping, ревью документации) |
| [llm/](llm/) | Всё про LLM-часть: метрики, статистика в UI, чанкинг для полного покрытия |
| [sse/](sse/) | SSE-события: отлов отключения клиента, hop-by-hop заголовки |
| [infra/](infra/) | Инфраструктура: кластер, ingress, RST-диагностика, HTTP/2, ВМ-буфер |
| [ux-frontend/](ux-frontend/) | Фичи фронтенда: таймеры, имена файлов, ZIP-раскрытие, лимиты, прерывание/ETA |
| [upload/](upload/) | Загрузка файлов: диагностика, лимиты, стресс-тесты |
| [code-fixes/](code-fixes/) | Исправления кода по версиям (баги, оптимизация, безопасность, логирование) |
| [tests/](tests/) | Тесты и бенчмарки: замеры UI, обфускация, нагрузка |
---
## base/ — первоначальная настройка
- **2026-07-11-initial-setup.md** — создание проекта DrHider на платформе Штурвал (Managed Flask, URL, входные точки).
- **2026-07-12-documentation.md** — создание полной документации после рефакторинга (список документируемых файлов).
## plans/ — планы и проектирование
- **2026-07-12-audit-and-plan.md** — полный аудит проекта и план рефакторинга под Managed Flask.
- **2026-07-12-v3-plan.md** — план v3: универсальная архитектура (убрать жёсткий список типов сущностей).
- **2026-07-12-v3-done.md** — итоги v3: переход на Markdown-пайплайн + универсальное LLM-обнаружение.
- **2026-07-12-v3.1-plan.md** — план v3.1: пофайловая обработка + прогресс в таблице (решение таймаута).
- **2026-08-24-implementation-plan-for-flash.md** — план для агента-кодера (Flash): TTL-фикс, трекинг файлов, прерывание, ETA, UI.
- **2026-08-24-folder-select-plan.md** — план для Flash: кнопка «Выбрать папку» (webkitdirectory, рекурсивно, относительный путь сохраняется).
## sonnet/ — обсуждения с Sonnet (вопросы и ответы)
**12–13 июля — проектирование:**
- **2026-07-12-sonnet-query.md** + **2026-07-12-sonnet-response.md** — вопрос/ответ: универсальная архитектура DrHider (6 вопросов → 6 ответов → карта изменений).
- **2026-07-12-sonnet-query-timeout.md** + **2026-07-12-sonnet-response-timeout.md** — диагностика ERR_TIMED_OUT в новом окне браузера (dev-сервер: 6 потоков заняты keep-alive).
- **2026-07-12-sonnet-query-v2.md** + **2026-07-12-sonnet-response-v2.md** — улучшение LLM-промпта (двухфазная инструкция).
- **2026-07-12-sonnet-query-v3-numbering.md** + **2026-07-12-sonnet-response-v3-numbering.md** — нумерация вместо генерации фейков (прозрачный аудит).
- **2026-07-12-sonnet-query-v4-hybrid.md** + **2026-07-12-sonnet-response-v4-hybrid.md** — гибридный формат замен «шаблон + номер» (вариант В).
- **2026-07-13-sonnet-query-architecture.md** + **2026-07-13-sonnet-response-architecture.md** — архитектура пофайловой обработки с одним ZIP (вариант А + блокирующий /process).
- **2026-07-13-sonnet-query-obfuscation-tokens.md** — запрос: анализ обфускации.
- **2026-07-13-sonnet-response-obfuscation-analysis.md** — анализ обфускации (TMP-файлы vs код).
- **2026-07-13-sonnet-response-obfuscation-bugs.md** — анализ багов обфускации и их фикс.
**20 августа — ревью скорости и сбоев:**
- **2026-08-20-sonnet-query-review-speed.md** — промпт: код-ревью drhider (скорость + объяснение сбоев, ограничение файлов).
- **2026-08-20-sonnet-query-review-speed-followup.md** — follow-up вопросы по спорным пунктам ревью (В1–В4).
- **2026-08-20-sonnet-review-qa.md** — Q&A: вопросы → ответы Sonnet → решения по ревью.
**20 августа — LLM-чанкинг:**
- **2026-08-20-sonnet-query-llm-pattern.md** — вопрос: как лучше организовать LLM-покрытие всех документов.
- **2026-08-20-sonnet-query-llm-followup.md** — 6 уточнений перед внедрением (чанкинг, дедуп, верификация, промпт).
- **2026-08-20-sonnet-llm-qa.md** — сводный Q&A по LLM-чанкингу (схема + ответы У.1–У.6 + решение).
**20 августа — ускорение:**
- **2026-08-20-sonnet-query-honest-speed.md** — честный вопрос: можно ли ускорить без риска устойчивости.
## opus/ — обсуждения с Opus
- **2026-07-13-opus-query-mss-clamping.md** — запрос: валидация плана MSS Clamping.
- **2026-07-13-opus-response-mss-clamping.md** — ответ: валидация плана MSS Clamping.
- **2026-07-13-opus-response-document-review.md** — проверка PROBLEM-AND-SOLUTION.md на ошибки.
- **2026-07-13-opus-response-final-review.md** — финальная проверка PROBLEM-AND-SOLUTION.md.
## llm/ — LLM-часть
- **2026-08-19-llm-metrics.md** — v0.0.34: вывод токенов и времени LLM.
- **2026-08-19-llm-stats-block.md** — v0.0.37: крупный блок статистики LLM в UI.
- **2026-08-20-llm-full-chunking.md** — v0.0.57: LLM обрабатывает ВСЕ данные (пофайловый чанкинг 6000/overlap 500, параллельность 4, дедуп + верификация).
## sse/ — SSE-события
- **2026-07-14-sse-generator-exit.md** — v0.0.18: отлов отключения клиента SSE (GeneratorExit/BrokenPipe).
- **2026-07-14-hop-by-hop-connection.md** — v0.0.21→0.0.22: Waitress запрещает hop-by-hop заголовок `Connection: close`.
## infra/ — инфраструктура
- **2026-07-14-browser-rst-debug.md** — диагностика ERR_CONNECTION_RESET в браузере.
- **2026-07-15-cluster-dump.md** — снимок кластера (4 ноды, taints, состояние).
- **2026-07-15-rst-root-cause.md** — итоговая диагностика ERR_CONNECTION_RESET (v0.0.29).
- **2026-08-19-remove-process-names-http2.md** — v0.0.46: удалён process_names + проблема git push через HTTP/2.
- **2026-08-21-pattern-vm-buffer-pull-plan.md** — план: перенос паттерна «ВМ-буфер + pull» в drhider.
- **2026-08-21-vm-files-api-plan.md** — план: универсальный файловый сервис на ВМ (API).
- **2026-08-21-vm-upload-implemented.md** — v0.0.58: ВМ-загрузка реализована (паттерн «ВМ-буфер + pull»).
- **2026-08-24-two-clusters-deploy.md** — деплой на двух кластерах: iot-naeel (актуальный, DNS → .151) и naeel-test-3 (не погашен).
## ux-frontend/ — фичи фронтенда
- **2026-08-19-live-timer-llm.md** — v0.0.38: живой таймер обработки + индикация ИИ.
- **2026-08-19-live-filename.md** — v0.0.40: имя файла в live-блоке + сброс итогов.
- **2026-08-19-file-time.md** — v0.0.41: время на конкретный файл в колонке «Статус».
- **2026-08-19-zip-expand-frontend.md** — v0.0.33: раскрытие ZIP в таблицу (фронтенд).
- **2026-08-19-zip-filter-docs.md** — v0.0.35: фильтр документов при раскрытии ZIP.
- **2026-08-19-ux-fixes.md** — v0.0.49: индикатор распаковки, разделение фаз (1/2, 2/2), таймауты SSE.
- **2026-08-20-upload-limits.md** — v0.0.50: лимиты загрузки (>100МБ/файл, >1ГБ сумма) + красная подсветка в таблице.
- **2026-08-24-interrupt-eta-design.md** — дизайн: TTL-фикс + прерывание с сохранением + оценка времени + UI.
- **2026-08-24-interrupt-eta-implemented.md** — v0.0.63: реализовано прерывание с сохранением + ETA + UI-таблица.
- **2026-08-24-time-tickers-estimates.md** — v0.0.640.0.65: тикер текущего файла на всех этапах, мгновенные оценки времени по файлам и суммарно.
- **2026-08-24-folder-select-implemented.md** — v0.0.68: кнопка «Выбрать папку» (webkitdirectory, рекурсивно, относительный путь).
- **2026-08-24-folder-zip-not-lost.md** — v0.0.69: архивы из папки без документов не теряются (добавляются как есть); раскрытие zip с документами; склонение счётчика.
- **2026-08-24-v075-small-fixes.md** — v0.0.75: кнопка при 0 файлов, фильтр zip (idx-мисматч), README-риски; замечен закоммиченный LLM_API_KEY.
- **2026-08-24-v074-cancel-extract.md** — v0.0.74: отмена в extract-фазе (не ждём все .doc).
- **2026-08-24-v073-security-fixes.md** — v0.0.73: 6 фиксов по ревью Соннета (SSRF, path traversal, слабый SID, proc_error, zip-бомба, self-XSS); 1 отклонено (cleanup), 5 отложено.
- **2026-08-24-pull-retry-limit-counter.md** — v0.0.72: ретраи pull из ВМ, корректная ошибка лимита сессии, счётчик дедупа.
- **2026-08-24-code-review-sonnet.md** — код-ревью Соннета: 15 багов (SSRF, path traversal, слабый SID, zip-бомба и др.), промпт в docs/code-review-sonnet.md.
- **2026-08-24-upload-logic-schema.md** — подробная схема логики загрузки + найденные баги/несоответствия.
- **2026-08-24-help-button-limits-text.md** — v0.0.70: заметная кнопка HELP (вместо «?») + пояснение статуса «пропущен» в ограничениях.
- **2026-08-24-ui-locks-session-freeze.md** — v0.0.66: блокировки UI (выбор/удаление) на время работы, заморозка сессии + кнопка «Новая сессия».
## upload/ — загрузка файлов
- **2026-08-19-upload-delay.md** — диагностика медленной/нестабильной загрузки + v0.0.43 (таймаут 300с).
- **2026-08-23-test-upload-only-plan.md** — план: тест-режим «только загрузка» (для Флаша).
- **2026-08-23-upload-stress-test.md** — тесты загрузки и стресс (v0.0.590.0.60, кластер naeel-test-3).
- **2026-08-24-upload-limits.md** — v0.0.62: лимиты 50 МБ на файл, 500 МБ на сессию.
## code-fixes/ — исправления кода (по версиям)
- **2026-08-31-vm-sync-readme.md** — документирован `rsync` текущего проекта на ВМ
в `/home/naeel/drhider`; legacy-каталог исключён.
- **2026-08-31-api-bp-syntax-error.md** — v0.0.77: удалена случайная строка из `site/routes/api_bp.py`,
вызывавшая `SyntaxError` при импорте приложения; задокументирован ответ Fable по архитектуре.
- **2026-08-18-fix-review-bugs.md** — v0.0.32: фиксы по ревью обфускатора.
- **2026-08-19-replacer-optimization.md** — v0.0.39: оптимизация apply_replacements (однопроходная замена, один regex).
- **2026-08-20-logging.md** — v0.0.51: максимальное логирование (LOG_LEVEL env) + детальные логи SSE/воркера.
- **2026-08-20-logging-file.md** — v0.0.52: файловый лог /tmp/drhider.log (для kubectl exec).
- **2026-08-20-broken-files.md** — v0.0.53: устойчивость к битым файлам (воркер не падает, битые пропускаются).
- **2026-08-20-v1-v2-implemented.md** — v0.0.54: В2 (фикс кракозябр имён zip) + В1 (фильтр таблиц, эффект≈0).
- **2026-08-20-honest-answer-and-A-implemented.md** — v0.0.55: честный ответ Sonnet + реализация А (threading .doc, кэш regex).
- **2026-08-20-csv-out-of-zip.md** — v0.0.56: КРИТИЧНО — mapping.csv (ключ расшифровки) убран из ZIP.
- **2026-08-24-zip-name-cyrillic-frontend.md** — v0.0.67: фикс кириллических имён из ZIP на фронте (UTF-8 без флага → CP866-мусор), согласовано с бэком.
## tests/ — тесты и бенчмарки
- **2026-08-19-ui-5runs-benchmark.md** — v0.0.49: бенчмарк UI, 5 прогонов DownLoads.zip (18 файлов, замеры времени).
- **2026-08-23-obfuscation-tests.md** — тесты реальной обфускации и фикс .doc (v0.0.600.0.61, корпус /mnt/y/T).
- **2026-08-24-tests-upload-and-obfuscation.md** — тесты загрузки и обфускации (v0.0.62, лимиты, ВМ-буфер).
- **2026-08-24-test-batch-v067.md** — тесты v0.0.67: загрузка, лимит 50МБ, ZIP-кириллица, ETA, прерывание с частичным сохранением.
---
## Соглашение об именах
`ГГГГ-ММ-ДД-тема.md` (иногда с версией). В начале каждого файла — дата и версия/статус.
При сортировке файл относится в папку по главной теме (имя файла/содержание), не по дате.
+24
View File
@@ -0,0 +1,24 @@
# Документация — 2026-07-12
**После рефакторинга** создана полная документация:
### Созданные файлы
| Файл | Назначение |
|---|---|
| `README.md` | Обзор проекта, правила для агентов, структура, API, env vars |
| `ARCHITECTURE.md` | Архитектура: схема, двухпроходная обфускация, поток данных, описание каждого модуля |
| `DEPLOY.md` | Деплой, ВМ, переменные окружения, локальная разработка, как добавить генератор |
| `.github/copilot-instructions.md` | Правила для Copilot (не менять без «делай», не смотреть локальные папки, коммитить после каждой правки) |
### Ключевые факты зафиксированы
- Платформа: Штурвал (Managed Flask), без Dockerfile/gunicorn
- URL: https://drhider.pythonk8s.dev.nubes.ru/
- Дизайн фронтенда: `/home/naeel/nubes/design/`
- ВМ (legacy): `5.172.178.213`, ssh-ключ `~/.ssh/naeel_vm_id_ed25519`
- Переменные окружения: LLM_API_KEY, LLM_URL, LLM_MODEL
### Коммит
`0707d53` — Документация: README.md, ARCHITECTURE.md, DEPLOY.md, .github/copilot-instructions.md
@@ -0,0 +1,84 @@
# v0.0.32 — фиксы по ревью обфускатора — 2026-08-18
**Дата:** 2026-08-18
**Версия:** 0.0.31 → 0.0.32
**Ветка:** master (коммит `94e588a`, после rebase над `c3c6e5b`)
**Источник:** ревью `History/2026-08-18-review-sonnet-bugs.md`
---
## Внесённые изменения
### 1. Единый обфускатор на все файлы (ТОП-1 — согласованность токенов)
- **Было:** `api_bp.py` обрабатывал каждый файл отдельным `obfuscate_files()`
новая инстанс `TwoPassObfuscator` на файл → одна сущность получала РАЗНЫЕ токены
в разных файлах, `all_mapping` (ручной разбор CSV) был неверен.
- **Стало:** все файлы сессии → один `obfuscate_files(...)` с общим mapping.
Проверено: «ООО Ромашка» в двух файлах → один токен «Сфера_0001».
- **Побочно:** убран хрупкий ручной разбор CSV `split(",", 2)` (ломавшийся на
запятых в значениях) — больше не нужен при едином вызове.
### 2. Рекурсивный `expand_zips` (ТОП-2)
- `extractor.py:expand_zips` переписан: очередь + распаковка вложенных ZIP.
ZIP внутри ZIP теперь раскрывается. Проверено: вложенный `doc1.txt` извлекается.
### 3. SSE-дисконнект (ТОП-3)
- `api_bp.py:generate()`: ловится `(GeneratorExit, BrokenPipeError,
ConnectionResetError)` вместо одного `GeneratorExit`.
- `obfuscate_files` выполняется в отдельном потоке; прогресс через очередь;
флаг отмены (`threading.Event`) при разрыве соединения.
- Коллбек прогресса `(phase, idx, total, fname)` где phase ∈ {start, done} —
согласуется с фронт-протоколом (`start`/`done` по `idx`).
### 4. Кириллица 1С (CP437→CP866)
- `extractor.py._decode_name`: если имя не-UTF-8 (флаг 0x800 снят) и содержит
не-ASCII → `name.encode("cp437").decode("cp866")`. Проверено изолированно.
### 5. Дедупликация имён
- `builder.py:build_zip`: одинаковое имя+контент → пропуск; имя+разный контент →
суффикс `_2`, `_3`... Проверено: `['a.md', 'a_2.md']`.
- `obfuscator.py:_dedupe_file_names`: уникализация имён на входе до прохода 1
(защита `all_texts[fname]` от перезаписи).
### 6. upload(): цикл по всем файлам
- `api_bp.py:upload()`: обрабатывает все `request.files.getlist("files")`
(поддержка загрузки целых папок через `webkitdirectory`).
- Сохранено поведение ошибок: файл без имени → «No filename» (совместимо с тестом).
### 7. Лимит объёма сессии
- `session.py`: `MAX_SESSION_BYTES = 500 MB`; в `add_file` — скип при превышении.
### 8. Защита от ZIP-бомб
- `extractor.py`: проверка `total + file_size > LIMIT` ДО `zf.read`; скип архива
при превышении ratio/лимита/числа файлов; лимиты глобально на вызов.
---
## Тесты
| Набор | Результат |
|-------|-----------|
| test_builder | 20/20 OK |
| test_zip | 28/28 OK |
| test_replacer | 10/10 OK |
| test_scanner | 20/20 OK |
| test_upload | 13/13 OK |
| test_extractor | **14/15** — падает `.doc` (внешний сервис liberta) |
`test_extractor` `.doc`: отправляется мусорный OLE2-магик
`b"\xD0\xCF\x11\xE0\x00"*10`, сервис `liberta.containerk8s.dev.nubes.ru/convert`
возвращает «conversion produced no .docx». НЕ регрессия от v0.0.32 — ветка
`.doc`/`doc_to_markdown` не менялась. Вероятно, сервис изменил поведение или
не принимает невалидный контент. Требует отдельного разбора.
---
## Git
- Remote был впереди (посторонний коммит `c3c6e5b` про магистратуру) → `git pull --rebase`.
- Push успешен: `c3c6e5b..94e588a`.
- Ветка-сохранение документации: `save-docs-2026-08-18` (`de072d8`).
## TODO / открытые вопросы
- [ ] Разобраться с тестом `.doc` (возможно, обновить тест на актуальный сервис).
- [ ] Визуально проверить SSE-прогресс во фронте после рефакторинга.
@@ -0,0 +1,45 @@
# v0.0.39 — оптимизация apply_replacements (однопроходная замена) — 2026-08-19
**Дата:** 2026-08-19
**Версия:** 0.0.38 → 0.0.39
**Файл:** `drhider/replacer.py`, `site/app.py` (версия)
---
## Суть
Обфускация PDF 19 МБ шла 132.7с, из них ИИ (LLM) всего 7с — остальное (~125с)
тратил основной код. Замер выявил узкое место: `apply_replacements`.
## Причина
`apply_replacements` для **каждой** сущности из mapping делала **2 полных
прохода `re.sub` по всему тексту** (основной + фоллбэк `_md_tolerant_pattern`).
Сложность O(N_сущностей × размер_текста × 2) — квадратичная по числу сущностей.
При 300+ сущностей и тексте в несколько МБ — десятки секунд.
Замер (0.9 МБ, 304 сущности): **17.3 с**.
## Решение
Однопроходная замена:
- Все ключи собираются в **один** regex-паттерн через альтернацию `(?:...|...)`
(по убыванию длины, с `re.escape`, границы `(?<!\w)...(?!\w)` для alnum-сущностей).
- Один `re.sub` с callback (lookup в mapping), O(text).
- Фоллбэк `_md_tolerant_pattern` применяется **только к ключам, не найденным**
основной заменой (отслеживание через `matched_keys`), а не ко всем.
## Результат замера
- 300 сущностей реально в тексте (1.3 МБ): **1.17 с** (было бы десятки сек).
- Реалистичный сценарий (10 сущностей): **0.29 с**.
- Согласованность токенов и mapping.csv сохранены (проверено через `obfuscate`).
## Проверка
- `test_replacer` 10/10, `test_builder` 20/20, `test_scanner` 20/20, `test_zip` 28/28.
- Пайплайн `obfuscate`: «ООО Ромашка»→«Вектор_0001» в двух файлах согласовано,
mapping.csv корректен.
## Примечание
- `scan_regex` (0.4с) и upload (сеть ~1.7 МБ/с) — не узкие места, не трогали.
- `pdf_to_markdown` (pdfplumber) локально не замерен (нет библиотеки); остаётся
потенциальным вторым узким местом для будущей оптимизации (pypdf/pdfminer).
@@ -0,0 +1,22 @@
# 2026-08-20 — Устойчивость к битым файлам (v0.0.53)
## Root cause (найден по логу /tmp/drhider.log)
SSE "connection failed" при обработке НЕ был обрывом шлюза. Воркер падал целиком:
```
worker: exception: PdfminerException(PDFSyntaxError('No /Root object! - Is this really a PDF?'))
```
на битом PDF (миниатюра 142 байта). Исключение в `extract_text` не было обёрнуто
по-файлово → воркер ронялся → SSE слал `error` → фронт показывал "SSE connection failed".
## Фикс (drhider/obfuscator.py)
Проход 1: `extract_text` обёрнут в try/except. При ошибке файл добавляется в
`skipped`, логируется warning, файл пропускается, остальные обрабатываются.
Проход 2: файлы из `skipped` пропускаются (не попадают в результат).
Проверено на flat/ (10 файлов: 5 валидных + 5 битых миниатюр):
- битые PDF/docx пропущены (PDFSyntaxError/BadZipFile), валидные 5 → в zip.
- воркер не падает, complete доходит.
## Примечание
Миниатюры 54-142 B в /mnt/y/T/flat — НЕ валидные PDF/docx (мусор из корзины).
Их можно удалить из тестового набора (или оставить как кейс устойчивости).
@@ -0,0 +1,24 @@
# 2026-08-20 — КРИТИЧНО: mapping.csv (ключ расшифровки) убран из выходного ZIP (v0.0.56)
## Проблема (утечка секретных данных)
Выходной ZIP содержал `mapping.csv` — таблицу соответствия «оригинал → замена»,
т.е. ключ расшифровки (реальные ФИО/телефоны/ИНН ↔ токены). Если ZIP передать
третьему лицу — тот получал и обфусцированные файлы, и таблицу расшифровки.
CSV должен храниться/скачиваться ОТДЕЛЬНО от обфусцированных файлов.
## Фикс (drhider/builder.py `build_zip`)
- Убрана запись `mapping.csv` в архив. Теперь в ZIP — ТОЛЬКО обфусцированные файлы.
- Параметр `mapping_csv` сохранён в сигнатуре (для совместимости вызовов), но в
архив не пишется.
- CSV продолжает генерироваться (`build_mapping_csv`) и отдаваться отдельно
через `/api/csv/<sid>` (кнопка «Скачать CSV»), как написано на странице.
## Тесты (tests/test_builder.py)
- Блок «ZIP с mapping.csv» переписан: теперь проверяет, что mapping.csv НЕ в архиве.
- Интеграционный блок: ZIP содержит только doc.md, CSV проверяется отдельной строкой.
- Все 106 тестов OK.
## Проверка
- python3 tests/test_builder.py — 20/20 OK (включая новый сценарий «mapping не в zip»).
- Остальные тесты — без изменений поведения, OK.
- VERSION 0.0.56.
@@ -0,0 +1,44 @@
# 2026-08-20 — Честный ответ Sonnet об ускорении + реализация А (v0.0.55)
## Честный ответ Sonnet (History/2026-08-20-sonnet-query-honest-speed.md)
Вопрос: можно ли значительно ускорить обработку, не рискуя устойчивостью/таблицами?
Ответ Sonnet (принято, согласуется с замерами):
- **Безопасно ускорить extract_text() на pdfplumber НЕЛЬЗЯ** — pdfminer pure Python,
CPU-bound, GIL полностью блокирует threading. (Подтверждено: extract_text 64.4с на Spartan10.)
- A) Безопасно-просто:
- A1. Threading для .doc (liberta, IO-bound) — N×~120с → ~120с при нескольких .doc.
- A2. Кэш compiled regex между файлами (проход 2).
- B) Заметный выигрыш, но с рисками (ОТЛОЖЕНО):
- B1. ProcessPool по файлам (spawn) — 3 PDF × 25с → ~30с (только 2+ PDF, CPU=2).
- B2. ProcessPool по страницам (spawn) — Spartan10 64с → ~35с (сложно: content, порядок).
- C) Не стоит:
- fork ProcessPool при session в памяти (COW-раздутие, 4Gi на пределе) — только spawn.
- Threading по страницам/файлам PDF — GIL, ноль.
- Итог Sonnet: безопасного значительного ускорения ОДНОГО PDF нет. Для набора 2+ PDF —
B1. При CPU=2 потолок 2x.
## Решение пользователя
«Делай по А, безопасно» — реализованы ТОЛЬКО A1 и A2. B — НЕ отложено (не делаем).
## Реализация (v0.0.55)
### A1 — threading для .doc (obfuscator.py, проход 1)
- .doc файлы (конвертация через HTTP liberta, IO-bound) запускаются в
ThreadPoolExecutor(max_workers=min(4, N_doc)).
- Основной цикл сохраняет порядок: для .doc берёт future.result(), остальные —
последовательно. progress_cb/порядок start/done не меняются.
- Выигрыш: HTTP-конвертация .doc перекрывается с CPU-обработкой PDF/docx.
### A2 — кэш compiled regex (replacer.py + obfuscator.py)
- replacer.py: вынесен `_build_combined_re(sorted_keys)`, `apply_replacements` получил
параметр `compiled_re=None` (если None — компилирует сам).
- obfuscator.py: после формирования `self._sorted_keys` компилируется один раз
`self._compiled_re`, передаётся во все вызовы apply_replacements (проход 2).
- __init__/finally: _compiled_re инициализируется/очищается.
## Проверка
- Все 106 тестов OK (test_replacer 10/10 — apply_replacements с compiled_re).
- Интеграция: flat/ (10 файлов) → 5 обработано (EB.md, EB1.md, Spartan10, TKM, mini_78b),
битые пропущены, .doc через liberta в потоках. 78.3с (в основном Spartan10 64с extract_text).
- VERSION 0.0.55.
@@ -0,0 +1,24 @@
# 2026-08-20 — Файловый лог в поде (v0.0.52)
## Проблема
В v0.0.51 добавили логирование в stderr, но на платформе Штурвал
`kubectl logs` НЕ показывает stdout/stderr приложения (fd 1/2 python -> pipe,
который читает супервизор платформы, а не kubelet). Логи приложения недоступны.
## Решение
`setup_logging()` в site/app.py:
- логи в stderr (для локального запуска)
- + `FileHandler` в `/tmp/drhider.log` (путь через env `LOG_FILE`)
- уровень через env `LOG_LEVEL` (DEBUG/INFO/WARNING)
- root.handlers.clear() + свои handler'ы (не полагаемся на basicConfig)
- если файл не открылся — не падаем, пишем в stderr
## Как читать логи в поде
```
kubectl exec -n 20a75175-a58c-49cb-b8fa-e86367b1a8dc <pod> -c app -- cat /tmp/drhider.log
```
(или tail -f). Версия 0.0.52.
## Проверено локально
Файл /tmp/drhider.log создаётся, пишет "Logging configured, version=0.0.52"
и сообщения логгеров.
+32
View File
@@ -0,0 +1,32 @@
# 2026-08-20 — Максимальное логирование (v0.0.51)
## Проблема
SSE рвётся на проде (пару минут), обработка при этом продолжается (воркер жив).
Логов в поде не было — приложение не писало в stdout, нельзя было диагностировать
обрыв (внешний шлюз vs генератор).
## Решение
1. `site/app.py``setup_logging()`:
- уровень из env `LOG_LEVEL` (DEBUG/INFO/WARNING), default INFO
- `logging.basicConfig(..., stream=sys.stderr, force=True)` — в stdout/stderr пода
- формат с таймстампом; уровни для логгеров drhider/app/routes/session
- `LOG_LEVEL` задаётся через env-переменные приложения на платформе
2. `site/routes/api_bp.py` — подробные логи:
- upload: каждый файл (имя, размер), итог, ошибки
- process_stream: start, worker start/done (время, токены, zip_len),
каждое событие progress (start/done) с idx/name/elapsed,
disconnect на heartbeat/progress/complete/error (с причиной),
result/complete, error event
3. VERSION поднята до 0.0.51
## Проверка (локально, DEBUG)
Upload 1 файла + SSE: видны все события от upload до complete.
`LLM NER failed: Illegal header value b'Bearer '` — ожидаемо без ключа (локально).
## Как читать логи пода после деплоя
```
kubectl logs -n 20a75175-a58c-49cb-b8fa-e86367b1a8dc <pod> --tail=500 --timestamps
```
- Если `process_stream: disconnect on progress` — клиент/шлюз оборвал.
- Если worker дошёл до `complete`, а клиент не получил — рвёт шлюз/браузер.
- Если `worker: exception` — ошибка обработки.
@@ -0,0 +1,37 @@
# 2026-08-20 — В1+В2 из ревью Sonnet (v0.0.54)
## В2 — фикс кракозябр имён zip (extractor.py `_decode_name`)
Добавлена попытка decode("utf-8") ПЕРЕД decode("cp866"), с валидацией диапазона:
- результат UTF-8 принимается, только если все символы — ASCII или кириллица (U+0400U+04FF)
(отсекает случайную коллизию CP866→UTF-8, напр. «Т»+«г» = U+04A3);
- при ошибке или выходе из диапазона — фоллбэк decode("cp866") (реальные 1С).
Проверено:
- CP866-зип (zip_subfolders_cp866.zip) → имена корректны (1_Металлургия.pdf…)
- UTF-8 с флагом (zip_subfolders_utf8.zip) → корректны
- test_zip 28/28 OK
## В1 — фильтр перед extract_tables() (extractor.py `pdf_to_markdown`)
Добавлен: `if page.lines or page.curves or page.rects:` перед `extract_tables()`.
Задумывалось как ускорение сканов (extract_tables на страницах без линий впустую).
### ⚠️ ФАКТ замера (опровергает гипотезу Sonnet)
На Spartan10Manual.pdf (14.8 МБ, 619 стр):
- extract_text суммарно 64.4с (104мс/стр) — УЗКОЕ МЕСТО
- extract_tables суммарно 0.2с (0мс/стр) — ничтожно
- на 272 страницах без линий extract_tables суммарно 0.0с — почти мгновенно
ВЫВОД: фильтр В1 НЕ даёт ускорения (extract_tables и так дешёвый). Реальное узкое
место — extract_text (pdfminer). Фильтр оставлен как безопасная защита (таблицы
не теряет: на 0144-03-2023_отчет об оценке.pdf 94=94 таблицы, потеряно страниц 0),
но эффект ускорения ≈ 0.
### Реальный путь ускорения (отложено)
Узкое место — extract_text (104мс/стр × 619 стр = 64с). Ускорение только через:
- распараллеливание extract_text по страницам/файлам (ProcessPool) — отложено,
требует замера рисков (fork/память, CPU=2);
- или смену извлечения текста без потери таблиц (риск: таблицы критичны).
## Тесты
Все 106 тестов OK (test_zip 28, test_extractor 15, test_builder 20, test_replacer 10,
test_scanner 20, test_upload 13). VERSION 0.0.54.
@@ -0,0 +1,26 @@
# v0.0.72 — Ретраи pull, корректная ошибка лимита, счётчик дедупа (2026-08-24)
_code-fixes. По итогам код-ревью (см. `infra/2026-08-24-upload-logic-schema.md`)._
## 1. Ретраи pull (site/routes/api_bp.py, `upload_refs`)
- `PULL_RETRIES = 3`, `PULL_RETRY_DELAY = 2` (сек).
- Pull из ВМ-буфера теперь в цикле: при любой ошибке (DNS `gaierror -5`, сеть) — до 3 попыток
с паузой 2с. После исчерпания — проброс исходной ошибки (502 «Pull failed»).
- Закрывает инцидент 18:42 (разовый DNS-сбой ронял всю загрузку).
## 2. Корректная ошибка лимита сессии (`upload_refs`)
- `add_file` возвращает `False` и для «сессия исчезла», и для «превышен лимит 500МБ».
- Теперь: при `False` — если `get_files(sid) is None` → 404 «Session not found»;
иначе (лимит) → файл пропускается (`delete` с ВМ) и загрузка продолжается.
Раньше оба случая давали ложное «Session not found».
## 3. Счётчик «Добавлено из папки» (site/templates/index.html)
- `addFileWithDedup` теперь возвращает `true` (файл добавлен) / `false` (дедуп).
- `added++` только при `true` — дубли больше не завышают счётчик.
- Проверено: папка [Договор.txt, Договор.txt (дубль), Акт.txt] → «Добавлено из папки: 2 файла».
## Проверка
- `node --check` — OK, `py_compile` (app.py, api_bp.py) — OK.
- Локально: счётчик дедупа работает (2 файла при одном дубле).
- Ретраи/лимит — на проде после деплоя.
- Версия 0.0.71 → 0.0.72.
@@ -0,0 +1,46 @@
# v0.0.73 — фиксы по код-ревью Соннета (безопасность + явные баги) (2026-08-24)
_code-fixes. По ревью `History/sonnet/2026-08-24-code-review-sonnet.md` (промпт `docs/code-review-sonnet.md`)._
_Критично перепроверены в коде; спорные пункты — отклонены/отложены (см. ниже)._
## Исправлено (6 фиксов)
1. **SSRF** (`api_bp.py::upload_refs`) — добавлена валидация `url.startswith(VM_UPLOAD_PREFIX)`;
недоверенный URL пропускается. Константа `VM_UPLOAD_PREFIX = "https://contracts.kube5s.ru/drhider-upload/"`.
2. **Path traversal / zip slip** (`api_bp.py`) — функция `_safe_name()`: нормализует слэши,
отбрасывает `..` и абсолютные пути, сохраняя подпапки (`Подпапка/Акт.txt` → как есть,
`../../evil.pdf` → ""). Применена в `upload` и `upload_refs`.
Unit-тест 8 кейсов — все OK.
3. **Слабый SID** (`session.py`) — `uuid.uuid4().hex[:12]``uuid.uuid4().hex` (128 бит).
4. **Серверная ошибка не отображалась** — серверное событие `event: error` (конфликт с встроенным
EventSource) → `event: proc_error`; добавлен клиентский `addEventListener('proc_error', …)`
с показом сообщения.
5. **ZIP-бомба: обход через поддельный `file_size`** (`extractor.py`) — добавлена проверка
`total_uncompressed > MAX_UNCOMPRESSED` ПОСЛЕ `zf.read()` с `break` (ранний выход).
6. **Self-XSS через имя файла** (`index.html`) — добавлена `esc()` и применена к `f.name`
в `rr()` и `procRow()`.
## Отклонено (критично к Соннету)
- **«cleanup(sid) после /download»** — НЕ сделано: ZIP и CSV скачиваются РАЗДЕЛЬНЫМИ запросами;
удаление сессии после отдачи ZIP сломало бы скачивание CSV. Сессия и так чистится по TTL (30 мин).
## Отложено (требуют решения/риск)
- idx-мисматч при `expand_zips` (фильтр расширений на бэке) — связано с фичей v0.0.69
(zip без документов «как есть»), требует решения по поведению.
- Отмена не проверяется в extract-фазе (.doc liberta 120с) — отдельный фикс.
- LLM-таймаут тихо обнуляет чанк — логирование/статус.
- Debug-эндпоинт `session_files` — ограничить/закрыть.
- LLM prompt injection — задокументировать (класс риска, не фикс кода).
## Проверка
- `py_compile` (app.py, api_bp.py, session.py, extractor.py) — OK; `node --check` — OK.
- `_safe_name` unit-тест — 8/8 OK.
- `create_app()` стартует (VERSION 0.0.73).
- Версия 0.0.72 → 0.0.73.
## Проверка на проде (редеплой 20:00, v0.0.73)
- SSRF: `POST /api/upload_refs` с `url=http://169.254.169.254/...``{"count":0,"ok":true}` (URL отклонён).
- Path traversal: `name="../../evil.txt"``{"count":0,"ok":true}` (имя отклонено).
- SID теперь полный 32 hex (128 бит) — виден в ответе.
- Регресс: папка с подпапкой + кириллицей → «Обработано 2 файлов: 4.3с»,
в ZIP `Подпапка/Договор_1.md` + `Акт.md` (пути сохранены, `_safe_name` не сломал подпапки).
@@ -0,0 +1,30 @@
# v0.0.74 — отмена в extract-фазе (2026-08-24)
_code-fixes. Доработка по код-ревью Соннета (пункт «отмена не работает в extract-фазе»)._
## Что сделано (`drhider/obfuscator.py`)
- В проходе 1 (извлечение) добавлена проверка `cancel_event.is_set()` между файлами:
при отмене цикл извлечения прерывается (`break`), LLM-сканирование пропускается,
результат помечается `cancelled` (processed=0, пустой ZIP + mapping.csv только заголовок).
- Раньше отмена ждала завершения ВСЕХ извлечений, включая медленные `.doc` (liberta, до 120с/файл).
## Проверка
- `py_compile` — OK, `import` drhider — OK.
- Unit-тест: `cancel_event` установлен до старта → `meta={'cancelled': True, 'processed': 0, 'total': 2}`,
пустой ZIP (22 Б), CSV только заголовок.
## Критично к Соннету (по итогам проверки)
- **«LLM-таймаут тихо обнуляет чанк»** — ОТКЛОНЕНО: `_call_llm` уже логирует
(`log.warning("LLM NER failed: %s", e)`) в `except Exception`. Таймаут не «тихий».
- **«cleanup после /download»** — ОТКЛОНЕНО ранее (ZIP и CSV скачиваются раздельно, см. v0.0.73).
## Осталось (не трогаем, задокументировано)
- idx-мисматч `expand_zips` (фильтр расширений) — краевой случай, спорное поведение.
- LLM prompt injection — класс риска, не фикс кода.
- debug-эндпоинт `session_files` — мелочь, может сломать тесты.
Версия 0.0.73 → 0.0.74.
## Проверка на проде (редеплой 20:07, v0.0.74)
- Версия v0.0.74 — на проде.
- Регресс: обычный цикл «Обработано 2 файлов: 1.9с» — фикс отмены не сломал обработку.
@@ -0,0 +1,31 @@
# v0.0.75 — кнопка при 0 файлов, фильтр zip, README-риски (2026-08-24)
_code-fixes. Закрытие отложенных пунктов списка «что далее»._
## Что сделано
1. **Кнопка «Обфусцировать» активна при 0 файлов** (`index.html::resetAll`) —
убрано перекрытие `ub.disabled = false` после `rr()` (rr() уже ставит disabled при пустом списке).
Из TODO (`docs/WhatTODO.md`, пункт 1).
2. **idx-мисматч `expand_zips`** (`drhider/extractor.py`) — синхронизирован фильтр расширений
с фронтом `listZipFiles`: из zip берутся ТОЛЬКО документы (`.pdf .doc .docx .txt .md`)
и вложенные `.zip`. Unit-тест: zip [txt,pdf,xlsx,ini] → раскрыты только txt,pdf.
3. **README** — добавлена секция «Известные ограничения / риски»: LLM prompt injection,
кейс «zip без документов».
## Не сделано (решение)
- **`session_files` оставлен** — используется для диагностики (тесты),
SID теперь 128-бит (не угадывается), риск мал.
## ⚠️ Замечено (требует решения)
- В `README.md` в открытом виде закоммичен `LLM_API_KEY` (секрет!). Желательно вынести
в переменные окружения Штурвала и убрать из README.
## Проверка
- `py_compile` (app.py, extractor.py) — OK; `node --check` — OK.
- `expand_zips` unit-тест — OK (только документы).
- `resetAll` больше не содержит `ub.disabled = false` (5 оставшихся мест — нужные ветки).
- Версия 0.0.74 → 0.0.75.
## Проверка на проде (редеплой 20:20, v0.0.75)
- Кнопка «Обфусцировать»: disabled при 0 файлов → активна с файлом → disabled после «Новой сессии».
- Регресс: zip с документом раскрыт (справка.txt), «Обработано 2 файлов: 4.4с» — фильтр zip не сломал.
@@ -0,0 +1,29 @@
# v0.0.67 — Фикс кириллических имён из ZIP на фронтенде (2026-08-24)
_code-fixes. Имя `TKM_6й_Семестр_ЭКЗАМЕН.docx` из архива отображалось как
`TKM_6╨╣_╨б╨╡╨╝╨╡╤Б╤В╤А_...` (мусор)._
## Причина
Архиватор записал имя **в UTF-8, но без UTF-8-флага** (bit 11). Фронт (`decodeZipName`)
видел «флага нет», шёл в legacy-ветку и декодировал **UTF-8-байты как CP866**
`D0 99` («й») → `╨╣` (псевдографика CP866).
## Фикс (index.html, `decodeZipName`)
- Первым делом — **строгая проверка UTF-8** (`TextDecoder('utf-8', {fatal:true})`):
если байты — валидный UTF-8 с кириллицей или печатаемым текстом → берём как есть.
- Только если строгий UTF-8 не проходит (реальные CP437/CP866 из 1С) — legacy-путь
(CP437 → CP866) не меняется.
- CP866-кириллица (0x80–0xAF) — это продолжения UTF-8 без ведущих байтов, поэтому
строгий UTF-8 для них честно падает и legacy-путь работает как раньше.
## Согласованность с бэком
Бэк (`extractor.py`, `_decode_name`) это уже умел (v0.0.54, «В2 — фикс кракозябр имён zip»).
Теперь фронт и бэк обрабатывают имена ZIP одинаково.
## Проверка (node)
- `TKM_6й_Семестр_ЭКЗАМЕН.docx` (UTF-8 без флага) — декодируется верно ✅.
- ASCII — ✅. Legacy CP437/CP866-путь не тронут.
- `node --check` — OK.
## Версия
- 0.0.66 → 0.0.67.
@@ -0,0 +1,26 @@
# 2026-08-31 — удалена случайная строка из `api_bp.py`
## Ответ Fable: анализ архитектуры DrHider
Сервис предназначен для обезличивания документов `.docx`, `.pdf`, `.doc`, `.txt` и `.zip`.
Основной пайплайн: `extractor``scanner` (regex + LLM) → `replacer``builder`,
оркестрация выполняется через `TwoPassObfuscator`. Веб-часть использует Flask,
blueprint'ы `main`, `health` и `api`, а загрузка файлов реализована через переиспользуемый
модуль с ВМ-буфером и pull.
## Найденная проблема
В `site/routes/api_bp.py` между инициализацией blueprint и определением первой функции
находилась строка:
```text
TMP/примеры_договоров_для_ИИ
```
Это невалидный Python-код и причина `SyntaxError` при импорте приложения. Строка удалена
без изменения остального API.
## Версия и проверка
- Исправление выпущено как версия `0.0.77` вместо `0.0.76`.
- Рабочая копия до исправления не содержала незакоммиченных изменений.
@@ -0,0 +1,13 @@
# 2026-08-31 — документирован sync проекта на ВМ
В README добавлен рабочий порядок синхронизации текущего checkout на ВМ:
- источник: `/home/naeel/nubes/drhider`;
- цель: `/home/naeel/drhider`;
- инструмент: `rsync` через SSH;
- исключены `.git`, кэши Python, `.venv` и `.pytest_cache`;
- `--delete` не используется;
- после копирования выполняются проверка версии и `py_compile`.
Legacy-каталог `/home/naeel/contracts/drhider` явно отмечен как недопустимая цель:
там работает старый standalone-сервис `contracts-drhider`.
@@ -0,0 +1,237 @@
# Диагностика ERR_CONNECTION_RESET в браузере
**Дата:** 2026-07-14
---
## Хронология
### Фаза 1: MTU (решено)
Проблема: 63KB POST из браузера → `ERR_CONNECTION_RESET`. Причина: MTU 1500 + Geneve 50 = 1550 > underlay 1450.
Решение: Cilium MTU = 1400. Проверено curl с ВМ (30/30).
### Фаза 2: HTTP/2
HTTP/2 → `ERR_HTTP2_PROTOCOL_ERROR`. HTTP/1.1 → `ERR_CONNECTION_RESET`.
Решение: `use-http2: "false"`.
### Фаза 3: Поиск корневой причины RST
Баг: браузер → свежая страница → POST 63KB → `ERR_CONNECTION_RESET`.
## Полная матрица тестов
| # | Сценарий | Результат |
|---|----------|-----------|
| 1 | Браузер: свежая страница → POST 63KB | ❌ RST / `⏳ 100%` hang |
| 2 | Браузер: свежая страница → GET (до Flask) → POST 63KB | ✅ 200 OK |
| 3 | Браузер: свежая страница → POST 10B → POST 63KB | ✅ 200 OK |
| 4 | Браузер: свежая страница → GET (nginx сам, 405) → POST 63KB | ❌ RST |
| 5 | `curl` с ВМ (5.172.178.213) → POST 63KB | ✅ всегда <100ms |
| 6 | Сразу после `kubectl rollout restart` ingress → браузер | ✅ работает |
| 7 | Через 5-10 мин после рестарта → браузер | ❌ снова RST |
## Что исключено
| Гипотеза | Статус | Почему |
|----------|--------|--------|
| MTU | ❌ исключено | 1400, curl с ВМ всегда работает |
| HTTP/2 | ❌ исключено | `use-http2: false`, та же проблема |
| XHR vs fetch | ❌ исключено | оба метода падают одинаково |
| `proxy_request_buffering: off` | ❌ исключено | `on` для `location /` |
| `client-body-timeout: 10` | ❌ исключено | 63KB < 1 сек |
| FD/worker-лимиты | ❌ исключено | 1M FD, 16K connections, 62MB mem |
| OOM/Restart | ❌ исключено | 0 рестартов, нет OOMKilled |
| Stale upstream keepalive | ❌ исключено | `keepalive=0` не помогло |
| nginx→Flask (upstream) | ❌ исключено | curl с ВМ доказывает что работает |
## Эксперименты
### Перезапуск ingress временно чинит
После `kubectl rollout restart` — браузер работает. Через 5-10 мин — снова RST. Накопление состояния.
### Ресурсы в норме
```
worker_rlimit_nofile: 1047552 (1M FD)
worker_connections: 16384
worker_processes: 4
Memory: 62 MiB, CPU: 3-7m
Restarts: 0, OOMKilled: нет
```
### `upstream-keepalive-connections: 0` — НЕ фикс
Баг вернулся через 5-10 минут. Keepalive pool не при чём.
## Критическое противоречие
- `curl` с ВМ (тот же датацентр) → POST 63KB → **всегда 200 OK <100ms**
- Браузер через 5-10 мин после рестарта ingress → **ERR_CONNECTION_RESET**
curl и браузер идут через один VIP (185.247.187.151), на одни ingress-поды.
Разница: клиент (curl vs Chrome) и сетевой путь (локальная сеть ДЦ vs интернет/VPN).
## Текущая конфигурация (2026-07-14)
| Параметр | Значение |
|----------|----------|
| `use-http2` | `"false"` |
| `upstream-keepalive-connections` | `"0"` |
| `error-log-level` | `debug` |
| `proxy-body-size` | `1024m` |
| `client-body-timeout` | `"10"` |
| `keep-alive` | `"10"` |
| Cilium MTU | `1400` |
| `externalTrafficPolicy` | `Cluster` |
| Ingress controller | `1.12.6` (Штурвал) |
---
## Вопрос к Claude (Sonnet/Opus)
### Симптом
Браузер (Chrome/Electron, Windows) → POST 63KB multipart/form-data → `ERR_CONNECTION_RESET` или зависание на `⏳ 100%`. Тело уходит полностью, ответ не приходит — TCP RST.
### Окружение
```
Кластер: bare-metal K8s 1.34.1, 4 ноды (3 worker + 1 control-plane)
CNI: Cilium Geneve, MTU подов = 1400
VIP: kube-vip 185.247.187.151 (ARP на все 4 ноды)
Ingress: nginx-ingress (Штурвал Helm), контроллер 1.12.6
2 реплики, externalTrafficPolicy: Cluster
Приложение: Flask/Waitress, 1 реплика (Штурвал managed)
```
Конфиг ingress:
```yaml
use-http2: "false"
upstream-keepalive-connections: "0"
client-body-timeout: "10"
keep-alive: "10"
worker-processes: 4
worker-rlimit-nofile: 1047552
worker-connections: 16384
proxy-body-size: 1024m
proxy-request-buffering: on # для location /
error-log-level: debug
```
### Критическое противоречие
- `curl` с ВМ (5.172.178.213, тот же датацентр) → POST 63KB → **всегда 200 OK <100ms**
- Браузер через 5-10 мин после рестарта ingress → **ERR_CONNECTION_RESET**
- Сразу после рестарта ingress → браузер работает
curl и браузер идут через один VIP (185.247.187.151), на одни ingress-поды. Разница: клиент (curl vs Chrome) и сетевой путь до VIP (локальная сеть ДЦ vs интернет/VPN).
### Вопросы
1. Почему рестарт ingress помогает на 5-10 минут, а потом браузерные запросы снова получают RST, при том что curl с ВМ продолжает работать?
2. Может ли `externalTrafficPolicy: Cluster` + kube-vip создавать conntrack-записи, которые со временем ломают соединения из интернета (с другими TCP options/MSS) но не из локальной сети?
3. Стоит ли попробовать `externalTrafficPolicy: Local`? Какие риски?
4. Какие ещё эксперименты можно провести без tcpdump (нет доступа к нодам по SSH, нет tcpdump в подах)?
---
## TODO
- [ ] Ответ от Claude
- [ ] `externalTrafficPolicy: Local` — эксперимент
- [ ] Воспроизвести с `curl` из WSL на машине пользователя
- [ ] tcpdump через `kubectl debug` (требует координации)
- [ ] Обновить ingress controller 1.12.6 → актуальный
- [ ] Проверить `client-body-timeout: 60` + `keep-alive: 75`
---
## Ответ Claude (Sonnet) — 2026-07-14
### Два независимых механизма
Симптом объясняется наложением двух механизмов:
### Механизм 1: kube-vip ARP flapping + conntrack asymmetry (основной)
Именно это объясняет 5-10-минутное окно и разницу между browser и curl.
**Как работает с `externalTrafficPolicy: Cluster`:**
```
Browser → VIP → Node N1 (kube-vip leader) → iptables DNAT+SNAT → nginx pod
└── conntrack entry создаётся на N1:
(browser_ip:srcport → 185.247.187.151:443)
↔ (N1_ip:eph → nginx_pod:443)
```
Проблема возникает когда kube-vip меняет лидера:
```
kube-vip: N1 → N2 (истёк lease или GARP от N2)
Коммутатор: обновляет ARP-кэш, VIP теперь на N2
Browser (старое HTTP keep-alive соединение):
→ PSH/ACK 63KB → попадает на N2
N2: нет conntrack записи → TCP RST
curl (новое соединение):
→ SYN → попадает на N2
N2: создаёт новую conntrack → всё ОК
```
**Почему 5-10 минут:** время жизни ARP-записи на коммутаторе или интервал переизбрания kube-vip лидера. "ARP на все 4 ноды" — периодические GARP-анонсы могут уходить с разных нод.
**Почему GET-to-Flask чинит последующий POST (тесты #2 vs #4):**
- GET → Flask: создаётся *новое* TCP-соединение (новый SYN) → правильный conntrack на текущей ноде. POST идёт по *тому же* соединению → conntrack жив → OK.
- GET → 405: nginx отвечает сам и закрывает соединение с `Connection: close`. POST создаёт *новое* соединение, но VIP уже на другой ноде → RST.
### Механизм 2: `keepalive_timeout: 10s` — слишком агрессивно
`keep-alive: "10"` в ConfigMap = `keepalive_timeout 10s` в nginx.
```
T=0: Browser загружает страницу (GET /)
T=10: nginx закрывает idle соединение (отправляет FIN)
T=?: Пользователь заполняет форму (>10 сек → почти всегда)
T=X: Browser отправляет POST по "живому" с его точки зрения соединению
→ nginx уже закрыл socket → RST
```
Браузер не всегда замечает FIN немедленно (TCP буферизация ОС). curl — каждый раз новое соединение.
### Почему curl всегда работает
| | Browser | curl |
|---|---|---|
| Соединение | HTTP keep-alive, долгоживущее | Новое для каждого запроса |
| conntrack | Создано при SYN, может устареть | Новый SYN на каждый вызов |
| keepalive | Держит idle >10s | Сразу POST → не ждёт |
| VIP flap | Пакет идёт на новую ноду без conntrack | SYN на новой ноде создаёт conntrack |
### Ключевой инсайт
> Разница не в TCP-опциях (MSS, window size). Разница в **типе пакета**: curl всегда начинает с SYN → conntrack создаётся на правильной ноде. Browser держит соединение между запросами → когда VIP мигрирует, браузерные пакеты (ACK/PSH) приходят на новую ноду без предшествующего SYN → нет conntrack-записи → RST.
### Итоговая таблица причин
| Причина | Механизм | Почему не curl |
|---|---|---|
| kube-vip ARP lease renewal | Смена ноды → conntrack orphan | Новый SYN всегда |
| `keepalive_timeout: 10s` | nginx закрывает idle conn → browser не замечает | Нет idle |
Оба фактора вместе: рестарт сбрасывает состояние, 5-10 минут → kube-vip переизбирается / браузер держит соединение дольше 10s → RST.
### Рекомендации
1. **`keep-alive: "75"`** — исключить механизм #2 (самое простое, безопасное)
2. **`externalTrafficPolicy: Local`** — исключить механизм #1 (риск: потеря source IP для других сервисов)
3. **JS: Connection warmer** — GET на Flask при загрузке страницы (уже доказано тестом #2)
4. **JS: пересоздавать соединение** — использовать `fetch()` без keep-alive (credentials: 'omit' или отдельный subdomain)
+158
View File
@@ -0,0 +1,158 @@
# Cluster Dump — 2026-07-15 17:00 MSK
## 1. НОДЫ (4 шт)
| Нода | Роль | Taints |
|------|------|--------|
| iot-naeel-control-plane-xb699 | control-plane | `NoSchedule:control-plane` |
| iot-naeel-workers-vqphm-6f74n | workers | нет |
| iot-naeel-workers-vqphm-bhbvs | workers | нет |
| iot-naeel-workers-vqphm-v8zq4 | workers | нет |
## 2. INGRESS (shturval-ingress-controller)
### Поды (2 реплики)
| Под | Нода | Возраст |
|-----|------|---------|
| ...86jsp | control-plane-xb699 | ~6ч |
| ...jm4kl | workers-vqphm-v8zq4 | ~6ч |
### Сервис
- Тип: LoadBalancer
- External IP: 185.247.187.151
- `externalTrafficPolicy: Cluster`
- NodePorts: 80:30739, 443:32391
### Ingress ConfigMap
| Параметр | Значение |
|----------|----------|
| keep-alive | **10** |
| client-body-timeout | **10** |
| client-header-timeout | **10** |
| proxy-body-size | **8m** |
| upstream-keepalive-connections | **0** |
| use-http2 | false |
| worker-processes | 4 |
| error-log-level | info |
### Helm
- Chart: shturval-ingress-controller-2.12.1
- Ревизия 9 (сегодня 11:53) — `replicaCount: 2`, hostPort enabled
- Ревизия 8 (сегодня 02:00) — то же самое
- Более старых ревизий нет (почищены)
### Affinity (ingress deploy)
```yaml
preferredDuringScheduling:
- weight: 10 → избегать control-plane
- weight: 80 → предпочитать node-role: ingress
- weight: 50 → предпочитать node-role: infra
```
### События (каждые 51 сек!)
```
Warning SyncLoadBalancerFailed failed to ensure load balancer: no address pools could be found
Normal EnsuringLoadBalancer Ensuring load balancer
```
## 3. KUBE-VIP
### DaemonSet: shturval-vip (4 пода, на ВСЕХ 4 нодах)
- Image: r.shturval.tech/kube-vip:v1.0.0
- ARP mode: `vip_arp: true`
- Election: `svc_election: true`
- Leaderelection: `vip_leaderelection: false`
- Lease: `sht-uservip-cp-lock`
### Cloud Provider: shturval-vip-provider (1 под, на bhbvs)
- Image: r.shturval.tech/kubevip/kube-vip-cloud-provider:v0.0.12
- Ищет ConfigMap: `kubevip` в неймспейсе `kube-system`
### ❌ ConfigMap `kubevip` в `kube-system` — **ПУСТОЙ!**
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: kubevip
namespace: kube-system
data: {} # <-- НЕТ ДАННЫХ
```
**Это причина ошибки «no address pools could be found»!**
## 4. DRHIDER (pythonk8s)
### Деплой
- 1 реплика
- Ревизия: 49
- Инит-контейнер: клонирует из Git, потом pip install + python app.py
- Ресурсы: 500m CPU, 1Gi RAM
- Проба: TCP :5000
### ❌ PodAffinity (добавлен сегодня 16:46, непрошеный)
```yaml
requiredDuringScheduling:
podAffinity → ingress pod на той же ноде
```
### Сервис
- ClusterIP: 10.102.171.63
- Port 80 → targetPort 5000
- `internalTrafficPolicy: Cluster`
### Ingress (drhider)
- Host: drhider.pythonk8s.dev.nubes.ru
- Аннотации: proxy-body-size=1024m, connect=120s, read/send=600s
- TLS: letsencrypt-prod
### Поды
| Под | Нода | Возраст |
|-----|------|---------|
| pythonk8s-6698c6c78-dkwsq | workers-vqphm-v8zq4 | ~15 мин |
Drhider на одной ноде с ingress-подом jm4kl.
## 5. ВСЕ INGRESS-РЕСУРСЫ
| Хост | Неймспейс | Возраст |
|------|-----------|---------|
| drhider.pythonk8s.dev.nubes.ru | 20a75175-... | 4д |
| loadtest.pythonk8s.dev.nubes.ru | 9039a501-... | 5д |
| contractor.pythonk8s.dev.nubes.ru | b4523aba-... | 7ч |
| capire.kube5s.ru | default | 54д |
| grafana.kube5s.ru | grafana | 66д |
| keycloak.k8c.ru | keycloak | 96д |
| qu.kube5s.ru | shared-sqs | 93д |
| iot.kube5s.ru | sless | 94д |
| terra.k8c.ru | terra | 92д |
## 6. CILIUM
4 пода (по одному на ноду), все Running. MTU был изменён на 1400 (по STATE).
## 7. КЛЮЧЕВЫЕ НАХОДКИ
### 🔴 ConfigMap `kubevip` пустой
kube-vip-cloud-provider не может найти address pools → каждые 51 сек ошибка SyncLoadBalancerFailed. При этом `185.247.187.151` работает через ARP-mode kube-vip DaemonSet (не через cloud provider). Вероятно, это не fatal, но указывает на недонастроенную интеграцию.
### 🔴 Ingress ConfigMap отличается от ожидаемого
- keep-alive: 10 (должен быть 75)
- client-body-timeout: 10 (должен быть 60)
- client-header-timeout: 10 (должен быть 30)
- proxy-body-size: 8m (должен быть 1024m)
Но для drhider это переопределено в аннотациях ingress-ресурса.
### 🟡 Ingress 2 реплики на 4 нодах с kube-vip
kube-vip анонсирует 185.247.187.151 на ВСЕХ 4 нодах, но ingress-поды только на 2. При `externalTrafficPolicy: Cluster` трафик на ноды без ingress: SNAT → кросс-нода → риск conntrack RST.
### 🟡 Helm: 2 апгрейда сегодня
Ревизии 8 (02:00) и 9 (11:53) сегодня. Кто-то обновлял ingress. `replicaCount: 2` в обеих.
### 🟢 Аннотации drhider ingress — хорошие
proxy-body-size=1024m, connect=120s, read/send=600s. Переопределяют дефолты ConfigMap.
### 🟢 Cilium — стабильный
4 пода, все Running, MTU 1400.
+204
View File
@@ -0,0 +1,204 @@
# DrHider — итоговая диагностика ERR_CONNECTION_RESET
**Дата:** 2026-07-15 (финал)
**Версия:** v0.0.29
---
## Резюме (моё мнение)
**`keep-alive: 10` — единственная причина ERR_CONNECTION_RESET.** Всё остальное (conntrack, hostPort, ARP flapping, svc_election) — шум, который мы искали 2 дня.
### Почему это так
VIP `185.247.187.151` жёстко привязан к control-plane-xb699 через аннотацию `kube-vip.io/vipHost`. Никакого flapping нет — трафик всегда приходит на одну ноду. Второй ingress-под на v8zq4 — избыточен, но не мешает.
Сценарий:
1. Браузер загружает страницу — opens TCP
2. Пользователь выбирает файлы — проходит >10 сек
3. Nginx (keep-alive=10) закрывает idle соединение
4. Браузер не замечает FIN (буферизация ОС)
5. POST 63KB летит в мёртвый сокет → RST
### Почему предыдущие гипотезы неверны
| Гипотеза | Опровержение |
|----------|-------------|
| kube-vip ARP flapping | `vipHost` фиксирует VIP на одной ноде. Lease мёртв — плевать |
| hostPort на 2 из 4 нод | Трафик не на все 4 ноды, а только на control-plane |
| upstream keepalive stale | `keepalive=0` не помог |
| MTU | Решено ещё 14-го, не при чём |
| conntrack | ETPolicy Cluster не при чём — трафик на одну ноду |
### Что подтверждает keep-alive гипотезу
| Тест | Объяснение |
|------|-----------|
| Сразу после рестарта ingress → OK | Все соединения свежие |
| Через 5-10 мин → RST | Соединения постарели >10с |
| GET/health → POST → OK | Новое живое соединение |
| curl с ВМ → всегда OK | curl открывает новый SYN каждый раз |
| `keep-alive: 75` + JS warmup → OK на 10+ мин | Увеличили окно, warmup греет |
## Хронология
| Дата | Событие |
|------|---------|
| 2026-07-11 | Исходный деплой |
| 2026-07-14 (день) | MTU fix (Cilium 1400) — решило 51с задержку |
| 2026-07-14 (вечер) | RST диагностика: keep-alive 75 + JS warmup — частично |
| 2026-07-15 (11:53) | Helm ревизия 9 — сброс ConfigMap на дефолты (keep-alive:10) |
| 2026-07-15 | podAffinity `required` на drhider (ручной kubectl edit) |
| 2026-07-15 (16:00) | Полная диагностика: hostPort vs cloud LB |
---
## Две независимые проблемы
### Проблема #1: MTU (решена 2026-07-14)
**Симптом:** 51с задержка при POST любого размера через внешний VIP.
**Причина:** Geneve +50, underlay 1450, дефолт 1500 → фрагментация/дроп.
**Решение:** Cilium `mtu: 1400`.
### Проблема #2: ERR_CONNECTION_RESET (хост порт, решена 2026-07-15)
**Симптом:** Браузер получает TCP RST при POST 63KB через 5-10 мин после рестарта ingress.
**Ранее считалось:** keep-alive 10с или kube-vip conntrack.
**Реальная причина:** Штурвал балансирует трафик на **все 4 ноды**, но ingress стоит `replicas: 2` с **hostPort** — порт 80/443 открыт только на 2 нодах.
```
Browser → cloud LB → любая из 4 нод
├── нода с ingress-подом → hostPort 80 → nginx → ✅
└── нода БЕЗ ingress-пода → порт 80 не слушается → RST ❌
```
**50% запросов попадает на пустую ноду → RST.**
---
## Подтверждающие данные
### Helm ревизии (дамп)
| Ревизия | Время | replicaCount | Другие изменения |
|---------|-------|-------------|------------------|
| 1 (деплой) | 2026-07-11 | 2 | — |
| 8 | 2026-07-15 02:00 | **2** | — |
| 9 | 2026-07-15 11:53 | **2** | — |
Helm **не менял** replicaCount. Всегда 2. Но ConfigMap сбрасывал на дефолты (keep-alive: 10, proxy-body-size: 8m и т.д.).
### Ingress ConfigMap (последствия Helm upgrade)
```yaml
# Было (ручные правки 2026-07-14) → Стало (после ревизии 9)
keep-alive: "75" → "10" # вернулось на дефолт Штурвала
proxy-body-size: "1024m" → "8m" # тоже сброшено
error-log-level: debug → info # сброшено
upstream-keepalive-connections: "0" → сохранилось
```
### kube-vip svc_election — мёртв
```yaml
# Lease ingress/kubevip-shturval-ingress-controller-controller
holderIdentity: "" # пусто — никто не держит
leaseDurationSeconds: 1 # аномально короткий (норма 15-30)
renewTime: 2026-07-11T14:55:28Z # 4 дня назад не обновлялся
leaseTransitions: 17 # но было 17 переходов
```
svc_election никогда не работал. VIP назначен через `ipMode: VIP` в статусе сервиса, работает на уровне облака (BGP/маршрутизация).
### hostPort
```yaml
# В шаблоне пода ingress (Helm template)
hostPort: 80
hostPort: 443
```
Включён в ревизиях 8 и 9. hostPort = порт слушается ТОЛЬКО на нодах где стоит под.
### podAffinity на drhider — временный костыль
```json
{
"podAffinity": {
"requiredDuringSchedulingIgnoredDuringExecution": [{
"labelSelector": {"matchLabels": {"app.kubernetes.io/name": "shturval-ingress-controller"}},
"namespaceSelector": {"matchLabels": {"name": "ingress"}},
"topologyKey": "kubernetes.io/hostname"
}]
}
}
```
Добавлен через `kubectl edit`, НЕ через Helm. Привязывает drhider к ноде с ingress-подом. Это НЕ лечит RST (RST на уровне VIP→нода, не на уровне нода→drhider). При следующем `helm upgrade` — исчезнет.
### Kube-vip DaemonSet — не участвует
```yaml
vip_arp: true # ARP включён
vip_leaderelection: false # нет CP election
svc_election: true # должен быть — но lease мёртв
```
Даемоны не логгируют VIP `185.247.187.151`. Трафик распределяет облако, не kube-vip.
---
## Решение
**Единственное полное решение:** ingress-под на каждой ноде куда приходит трафик.
### Вариант 1 (рекомендуемый): увеличить replicas
```yaml
# Helm values
shturval-ingress-controller:
replicaCount: 4
```
Или выяснить у DevOps сколько нод в LB-пуле Штурвала и поставить `replicaCount = число нод`.
### Вариант 2 (если балансировка на все worker'ы): DaemonSet
```yaml
# Helm values
shturval-ingress-controller:
kind: DaemonSet
```
---
## Что делать сейчас (ручной воркараунд)
### 1. Починить ConfigMap (срочно)
```bash
kubectl patch configmap -n ingress shturval-ingress-controller-controller --type merge \
-p '{"data":{"keep-alive":"75","proxy-body-size":"1024m","client-body-timeout":"60","client-header-timeout":"30"}}'
```
### 2. Масштабировать ingress (временный фикс)
```bash
kubectl scale deploy -n ingress shturval-ingress-controller-controller --replicas=4
```
Поды раскидаются по всем 4 нодам → hostPort на всех → 0% RST.
⚠️ **Предупреждение:** после следующего `helm upgrade`:
- ConfigMap сбросится (нужно править Helm values)
- replicas вернётся на 2
- podAffinity на drhider исчезнет
---
## Вопросы к DevOps
1. **Сколько нод в LB-пуле Штурвала?** (на какие ноды облако направляет трафик ingress?)
2. **Можно ли увеличить `replicaCount` до числа нод?**
3. **Как правильно изменить Helm values для ingress?** (чтобы ConfigMap не сбрасывался при upgrade)
@@ -0,0 +1,37 @@
# v0.0.46 — удалён process_names + git push HTTP/2 проблема — 2026-08-19
**Дата:** 2026-08-19
**Версия:** 0.0.45 → 0.0.46
---
## Удалён тестовый API process_names
- `process_names` (обфускация файлов по именам из TEST_INPUT_DIR) **удалён**.
- Причина: сервер не может читать локальные файлы клиента (нет доступа к его
машине). Файлы в поде — неудобно. API бесполезен.
- Удалены: endpoint, `TEST_INPUT_DIR`/`_TEST_API_ENABLED`, `import os`, History-файл.
- Версия 0.0.46.
## Находка: git push к gitea зависал (HTTP/2)
**Симптом:** `git push origin master` висел бесконечно (таймауты), хотя
`git ls-remote` и `curl` к gitea работали (200, быстро).
**Причина:** git использовал **HTTP/2** для отправки pack; шлюз
`gitea.services.ngcloud.ru` блокировал передачу данных по HTTP/2
(та же проблема, что была в drhider с HTTP/2 — ERR_HTTP2_PROTOCOL_ERROR).
**Решение:** push через HTTP/1.1 мгновенно прошёл:
```bash
git -c http.version=HTTP/1.1 push origin master
# и закреплено в конфиге:
git config http.version HTTP/1.1
```
**Проверка:** `8d7a7df..d2f2bd3 master -> master`, синхронизировано.
## Полезно
- На этой машине/шлюзе для git (и возможно других HTTP/2 клиентов) использовать
HTTP/1.1.
- git config `http.version HTTP/1.1` уже установлен.
@@ -0,0 +1,124 @@
# ПЛАН: перенос паттерна «ВМ-буфер + pull» в drhider
_Дата: 2026-08-21. Основа: `contracts/loadtest/History/2026-08-21-pattern-vm-buffer-pull.md`._
_Статус: РЕАЛИЗОВАНО 2026-08-21 (v0.0.58), см. `2026-08-21-vm-upload-implemented.md`._
---
## Цель
drhider (Managed Flask, Штурвал, `drhider.pythonk8s.dev.nubes.ru`) получает файлы через
`POST /api/upload` (multipart). Входной шлюз managed-кластера обрывает тела >~64КБ
(HTTP 000 ~10с). Решение — загружать файл на ВМ напрямую, Flask тянет сам (egress).
## Директива (обязательно)
**ВСЕ файловые операции загрузки в Flask (из браузера или вообще снаружи) — ТОЛЬКО через ВМ.**
Никаких прямых `POST /api/upload` с телом файла во Flask.
## Среда (уточнено 2026-08-21)
- `iot-naeel` — СОБСТВЕННЫЙ кластер, параметры/аннотации правились через kubectl. Пока
тестируем/смотрим поведение на нём (лимит 64КБ там может не воспроизводиться).
- В дальнейшем деплой — на ОБЫЧНЫЙ managed-кластер (лимит 64КБ будет реальным).
- ВМ — ОДНА (`5.172.178.213`, `contracts.kube5s.ru`), та же, что для loadtest. Location
`/drhider-upload/` добавляется в существующий nginx рядом с `/lt-serve/`.
- Вывод: VM-путь строим СЕЙЧАС, независимо от поведения iot-naeel — прод всё равно на обычном.
## Текущий поток
`uploadFiles()` (index.html ~413): по одному файлу в цикле → `FormData``POST /api/upload`.
Бэк: `MAX_CONTENT_LENGTH=200МБ`, сессия 500МБ. HTTP-клиент — **httpx** (не requests).
## Целевой поток
```
Браузер ──(PUT, файл целиком)──▶ ВМ nginx (без лимита 64КБ)
│ └─ получил URL + токен
└──▶ Flask POST /api/upload_refs (JSON: [{name,size,url}], <64КБ)
Flask egress GET с ВМ ──▶ читает файл ──▶ add_file() ──▶ дальше как сейчас
```
## Изменения (4 места)
### 1. ВМ `5.172.178.213`, `nginx-contracts.conf` — приём загрузки
Новый location (рядом с `/lt-serve/`), приём через PUT + статическая отдача:
```nginx
location /drhider-upload/ {
alias /var/www/drhider-upload/;
dav_methods PUT;
create_full_put_path on;
client_max_body_size 1024m;
# CORS: браузер с drhider.pythonk8s.dev.nubes.ru шлёт PUT (не simple → preflight)
add_header Access-Control-Allow-Origin https://drhider.pythonk8s.dev.nubes.ru always;
add_header Access-Control-Allow-Methods 'PUT, GET, OPTIONS, DELETE' always;
add_header Access-Control-Allow-Headers 'Content-Type' always;
if ($request_method = OPTIONS) { return 204; }
}
```
- Каталог `/var/www/drhider-upload/` (владелец `www-data`), НЕ `/home/naeel` (403).
- Порядок: backup → правка → `nginx -t``systemctl reload nginx`.
### 2. Flask — новый endpoint pull (`site/routes/api_bp.py`)
```python
@api_bp.route("/upload_refs", methods=["POST"])
def upload_refs():
data = request.get_json(silent=True) or {}
sid = data.get("session") or create_session()
refs = data.get("files") or []
if not refs:
return jsonify({"ok": False, "error": "No files"}), 400
added = 0
with httpx.Client(timeout=120, follow_redirects=True) as client:
for ref in refs:
name, url = ref.get("name"), ref.get("url")
if not name or not url:
continue
content = b"".join(client.stream("GET", url).iter_bytes()) # egress, по частям
if not add_file(sid, name, content):
return jsonify({"ok": False, "error": "Session not found"}), 404
added += 1
return jsonify({"ok": True, "session": sid, "count": file_count(sid)})
```
- Импорт `httpx` (уже в requirements.txt) и `file_count` из session.
- `stream(...).iter_bytes()` — НЕ `content` целиком (OOM при 100МБ+).
- Обработка ошибок egress (таймаут/403/404) → вернуть 502 с именем файла.
### 3. Фронт (`site/templates/index.html`, `uploadFiles()`)
Разбить «Фазу 1» на два шага:
- **Шаг 1a:** каждый файл → `fetch(<ВМ>/drhider-upload/<token>/<name>, {method:'PUT', body:file})`,
собирать `[{name, size, url}]`. Прогресс — `xhr.upload.onprogress` заменить на `fetch` +
`ReadableStream` (или оставить XHR, но на PUT к ВМ).
- **Шаг 1b:** один `POST /api/upload_refs` с JSON `{session, files:[...]}` (<64КБ).
- Токен: `crypto.randomUUID()`. URL ВМ — константа/`data-атрибут`.
- Показывать прогресс «загрузка на ВМ» отдельно от «загрузка в Flask».
### 4. `requirements.txt` — без изменений (httpx уже есть).
## Безопасность и жизненный цикл
- Токен в пути URL — только `crypto.randomUUID()`, не переиспользуется.
- Flask после `add_file` делает `DELETE` на URL ВМ (или ВМ чистит по TTL — cron/systemd-timer
`find /var/www/drhider-upload -mmin +30 -delete`).
- ВМ отдаёт файл только по валидному URL с токеном (нет открытого листинга: `autoindex off`).
## Грабли (из pattern-дока + drhider)
- CORS: PUT — не simple-метод → браузер шлёт OPTIONS preflight; nginx обязан отвечать 204.
- `site/` конфликтует со stdlib `site.py` (для loadtest; у drhider — Штурвал, без gunicorn).
- egress через `httpx` с `stream`, timeout 120; большие ОТВЕТЫ проходят (проверено 50МБ).
- OOM: не читать файл в память целиком; сессия 500МБ уже ограничивает.
- Локальные curl на Krupski идут через прокси `172.17.192.1:10808` → для теста ВМ `--noproxy '*'`.
## Порядок работ + проверка
1. ВМ nginx (location + каталог + CORS) → проверить `curl --noproxy '*' -X PUT`.
2. Flask `/api/upload_refs` → проверить `python3 -c "import py_compile; py_compile.compile(...)"`.
3. Фронт `uploadFiles()``node -c` нет (это EJS/HTML+JS внутри шаблона) — ручная проверка.
4. Сквозной тест: файл 10МБ → ВМ → Flask → обфускация → ZIP.
5. Commit + push (правило: после правки + bump версии `VERSION` в `site/app.py`).
## Решения (закрыты)
- Кластер: сейчас `iot-naeel` (тест), прод — обычный managed. VM-путь обязателен.
- ВМ: одна, `contracts.kube5s.ru` (`5.172.178.213`), общая с loadtest — добавляем location.
@@ -0,0 +1,75 @@
# ПЛАН: универсальный файловый сервис на ВМ (API)
_Дата: 2026-08-21. Статус: РЕАЛИЗОВАНО И ЗАДЕПЛОЕНО (v1.0.0), см. `README.md` сервиса `/home/naeel/nubes/vmfiles/` и `2026-08-21-vm-upload-implemented.md`._
## Цель
Один внутренний (НЕ публичный) сервис на ВМ `5.172.178.213` (`contracts.kube5s.ru`)
для приёма/временного хранения/выдачи файлов. Потребители: **drhider**, **contracts**,
**SQS IoT**. Заменяет сырой nginx-dav `/drhider-upload/` на задокументированный,
версионированный API, чтобы любому сервису было ясно, как им пользоваться.
## Потребители и режимы
- **drhider** — браузер (PUT файла) + сервер (pull). Нужен CORS для браузера.
- **contracts** — сервер-к-серверу, много файлов из локали.
- **SQS IoT** — сервер-к-серверу.
## Технология
**FastAPI + uvicorn** (Python уже есть на ВМ).
Почему FastAPI: автогенерация OpenAPI/Swagger (`/docs`) → «как юзать» видно из коробки;
нативный streaming для больших тел.
## API v1 (контракт)
| Метод | Путь | Вход | Ответ |
|---|---|---|---|
| `POST` | `/api/v1/files` | тело=файл (raw или multipart `file`), header `X-App-Key` | `201 {"id","url","size","expires_at"}` |
| `GET` | `/api/v1/files/{id}` | header `X-App-Key` | байты файла |
| `DELETE` | `/api/v1/files/{id}` | header `X-App-Key` | `204` |
| `GET` | `/api/v1/health` | — | `{"ok":true,"version":...}` |
| `GET` | `/api/v1/docs` | — | Swagger UI (авто) |
Единые ошибки: `{"error": "...", "code": "unauthorized|not_found|too_large|expired|quota_exceeded"}`.
## Авторизация
Заголовок `X-App-Key`, по одному секрету на приложение. Ключи — в конфиге на ВМ
(файл `apps.yml`/`.env`): `drhider`, `contracts`, `sqs-iot`. Без валидного ключа — `401`.
## Хранилище и метаданные
- Файлы: `/var/lib/vmfiles/` (или `/var/www/vmfiles/`, владелец — пользователь сервиса).
- Метаданные: SQLite (`id, app, name, size, created_at, expires_at`).
- id — серверный UUID (или клиентский токен — см. «открытые вопросы»).
## Лимиты и TTL
- `max_file_size` (по умолчанию 1024m) и `quota` на приложение.
- TTL по умолчанию 30 мин (на приложение можно переопределить).
- Чистка — встроенная фоновая задача сервиса (вместо текущего cron-`find`).
## Деплой на ВМ
1. Код сервиса — в отдельной папке на ВМ (или отдельный репозиторий).
2. systemd-юнит `vmfiles.service``uvicorn app:app --host 127.0.0.1 --port 8769`.
3. nginx: новый `location /api/v1/ { proxy_pass http://127.0.0.1:8769; ... }`
+ `proxy_request_buffering off` (стримить большие тела) + `client_max_body_size 1024m`.
CORS — отдать сервису (FastAPI), не nginx.
4. Проверка: `nginx -t` → reload.
## Миграция drhider
- Пока сервис поднимается — `/drhider-upload/` оставить как есть (не ломать текущее).
- После готовности — перевести `uploadFiles()` и `/api/upload_refs` на `/api/v1/files`,
убрать dav-location и cron-`find`.
## Порядок работ
1. Скелет FastAPI + health + `/docs`.
2. `POST/GET/DELETE /files` + SQLite-метаданные + TTL-чистка.
3. `X-App-Key` авторизация + квоты.
4. systemd + nginx `proxy_pass`.
5. Тесты: curl (raw + multipart), большие файлы, expiry, quota.
6. Перевести drhider на новый API, выпилить старый dav.
7. Документ-спека (`README`/`API.md`) + примеры для contracts/SQS.
## Открытые вопросы (до «делай»)
1. Язык: FastAPI (рекомендую) или Flask?
2. id файла: сервер генерирует (POST) или клиент приносит токен (PUT)? Влияет на браузерный поток drhider.
3. Загрузка: только raw-тело или поддерживать multipart тоже?
4. Где живёт код сервиса — отдельный репозиторий на gitea или папка на ВМ?
5. Ключи приложений — придумать сейчас или сервис сгенерирует при первом старте?
@@ -0,0 +1,43 @@
# ВМ-загрузка в drhider — реализовано (v0.0.58, 2026-08-21)
_Реализация паттерна «ВМ-буфер + pull» по плану `2026-08-21-pattern-vm-buffer-pull-plan.md`._
_Снимок до изменений: ветка `save-pre-vm-upload-2026-08-21`._
## Что сделано
1. **Flask** (`site/routes/api_bp.py`) — новый `POST /api/upload_refs`:
принимает JSON `{session, files:[{name,size,url}]}`, тянет каждый файл с ВМ
ИСХОДЯЩИМ GET (httpx, stream по частям), кладёт в сессию `add_file()`,
после успешного pull делает DELETE с ВМ. Ошибки egress → 502.
2. **Фронт** (`site/templates/index.html`, `uploadFiles()`) — Фаза 1 переделана:
- 1a: каждый файл → `PUT` на ВМ `https://contracts.kube5s.ru/drhider-upload/<токен>_<k>`
(XHR, прогресс по-прежнему в строке), собирает refs.
- 1b: один маленький `POST /api/upload_refs` (<64КБ).
- Токен — `crypto.randomUUID()`. Константа `VM_UPLOAD_URL`.
3. **ВМ** (`5.172.178.213`) — nginx `nginx-contracts.conf`:
- `location /drhider-upload/``alias /var/www/drhider-upload/`,
`dav_methods PUT DELETE`, `create_full_put_path on`, `client_max_body_size 1024m`,
CORS-заголовки для `drhider.pythonk8s.dev.nubes.ru`, OPTIONS → 204.
- Каталог `/var/www/drhider-upload/` (www-data).
- Backup: `/tmp/nginx-contracts.conf.bak.20260821`.
- TTL-чистка в root cron: `*/5 * * * * find /var/www/drhider-upload -type f -mmin +30 -delete`.
4. **Версия**`site/app.py`: 0.0.57 → 0.0.58.
## Отклонение от плана
- URL файла на ВМ — БЕЗ имени: `.../drhider-upload/<токен>_<k>` (имя — только в метаданных).
Иначе кириллица/пробелы в имени ломали бы URL.
## Тесты (2026-08-21)
- ВМ: PUT 10МБ → 201 (1.0с); GET → 200, 10485760B; OPTIONS preflight → 204 с CORS;
DELETE → 204; GET после DELETE → 404.
- Сквозной: PUT на ВМ → `POST /api/upload_refs` → 200, файл в сессии (10485760B),
файл с ВМ удалён (404).
- `py_compile` api_bp/app — OK; `node --check` JS из index.html — OK.
## Замечания
- ГРАБЛИ: на Krupski `dd` алиасится на `docker compose down` — для генерации файлов
использовать `head -c N /dev/urandom > file`.
- Локальные curl/httpx к contracts.kube5s.ru — через прокси (`172.17.192.1:10808`):
для тестов `--noproxy '*'` / `NO_PROXY=contracts.kube5s.ru`.
- Прод-безопасность (позже): токены без TTL-времени в самом URL, авторизация на ВМ
(сейчас ключ — случайный UUID, файл живёт ≤30 мин по cron).
@@ -0,0 +1,29 @@
# Временный сбой сертификата gitea и push (2026-08-24)
_infra. Прецедент, чтобы не терять время в следующий раз._
## Симптом
`git push origin master` к `gitea.services.ngcloud.ru` падал:
```
SSL: certificate subject name (*.ngcloud.ru) does not match target host name 'gitea.services.ngcloud.ru'
```
При этом в браузере `https://gitea.services.ngcloud.ru/Nail/drhider.git` открывался.
## Диагностика
- `gitea.services.ngcloud.ru``194.31.9.41` (стабильно, и с машины, и с ВМ).
- На `194.31.9.41:443` отдавался **wildcard `*.ngcloud.ru`** (SAN: `*.ngcloud.ru, ngcloud.ru`),
который НЕ покрывает `gitea.services.ngcloud.ru` (два уровня: `services.ngcloud.ru`).
- Внутренний ClusterIP `10.96.52.11` (gitea.mgmt.nubes.ru, hosts ВМ) недоступен ни с машины, ни с ВМ.
- На ВМ git-настроек нет (нет sslVerify=false), `credential.helper=store`.
## Итог
Проблема была **на стороне gitea/ngcloud.ru** — временно отдавался неверный (wildcard) сертификат.
Через ~10-20 минут сертификат вернулся корректным (`CN = gitea.services.ngcloud.ru`,
SAN `DNS:gitea.services.ngcloud.ru`) → `git push` прошёл (`0479afb..e31476e`).
## Урок
- Если push к gitea падает по SSL «*.ngcloud.ru doesn't match» — это временный сбой сертификата
на стороне gitea (не код). Проверить: `echo | openssl s_client -connect gitea.services.ngcloud.ru:443 -servername gitea.services.ngcloud.ru | openssl x509 -noout -subject -ext subjectAltName`.
- Не менять git config (`sslVerify=false`) и не синкать на ВМ — подождать и повторить push.
- Аналогичный сбой DNS был у `contracts.kube5s.ru` в 18:42 (см. `2026-08-24-pull-dns-transient.md`) —
вероятно, общее инфраструктурное происшествие ngcloud.ru в этот день.
@@ -0,0 +1,20 @@
# Секрет-гигиена: LLM_API_KEY в git (2026-08-24)
_infra/заметка. Решение пользователя: историю git НЕ переписывать._
## Что было
- `LLM_API_KEY` (sk-ucI5Yv…) попал в git при написании документации (коммит `0707d53`,
2026-07-12) — переменные окружения Штурвала скопированы в README/DEPLOY дословно, секрет не вычищен.
## Что сделано
- Ключ убран из текущих файлов: `README.md`, `docs/DEPLOY.md`, `History/plans/2026-07-12-audit-and-plan.md`
(коммит `f2141ec`, запушен).
- Проверено: в текущих файлах ключа нет.
## Решение пользователя
- **Историю git не переписываем** (filter-repo/filter-branch — нет, «хуй с ним»).
- Ключ остаётся в git-истории (коммиты `0707d53`, `a2f754a`, `4c3f9d4` и др.), репозиторий публичный.
## Рекомендация (не выполнено)
- Ротация `LLM_API_KEY` в Штурвале (старый считается скомпрометированным). Решение за пользователем.
- На будущее: секреты в env Штурвала, в git — только заглушки «(секрет)».
@@ -0,0 +1,28 @@
# Разовый DNS-сбой pull: «Pull failed: [Errno -5] No address associated with hostname» (2026-08-24)
_infra. Вопрос пользователя «ЭТО ЧТО ????» при загрузке папки CNC (220 файлов, отобрано 8)._
## Симптом
Фронт: «Ошибка передачи ссылок: Pull failed: [Errno -5] No address associated with hostname»
при нажатии «Обфусцировать».
## Диагностика (факты)
- ВМ-буфер: `https://contracts.kube5s.ru/drhider-upload/` (nginx dav), host → 5.172.178.213.
- DNS снаружи (8.8.8.8) и на ВМ: `contracts.kube5s.ru` → 5.172.178.213 — ок; буфер отвечает 403.
- Под drhider: ns `20a75175-a58c-49cb-b8fa-e86367b1a8dc`, pod `pythonk8s-85d8f4dcc8-vjjts`
(создан ~18:36 МСК, редеплой 18:31-18:33). `getent`/`socket` из пода → 5.172.178.213 — ок.
- Логи пода: ЕДИНСТВЕННАЯ ошибка `upload_refs: pull error sid=66ce16c1cda9` в **15:42:43 UTC (18:42 МСК)**:
`connect_tcp.started host='contracts.kube5s.ru'``gaierror(-5)`.
- До неё (15:41:41 UTC / 18:41 МСК) сессия пользователя `3ec718dafa5b` с теми же 8 файлами
(info.txt, out1.txt, fatD.pdf, fatihaDownEdited.pdf, fatihaUPEdited.pdf, fatU.pdf, FT0.pdf, sha0.pdf)
УСПЕШНО обработана (worker done, 29.8с, tokens 2138).
- CoreDNS: 2 пода, 0 рестартов, 44d — стабильны.
- Живой тест сейчас: папка 2 txt → «✅ Обработано 2 файлов: 6.6с» — pull работает.
## Вывод
**Разовый DNS-сбой** CoreDNS/upstream на `contracts.kube5s.ru` в узкий момент 18:42 МСК
(одна ошибка за весь лог). К выбору папки отношения нет; повторный запуск работает.
## Рекомендация (опция)
Добавить ретраи pull в `upload_refs` (2-3 повтора с паузой при `ConnectError`/DNS-ошибке) —
v0.0.70. Решение за пользователем.
@@ -0,0 +1,27 @@
# Деплой: два кластера, iot-naeel актуальный, naeel-test-3 — не погашен (2026-08-24)
## Симптом
- health/HTML внешнего URL отдавали **0.0.67**, а под в ns 20a75175 на naeel-test-3 был **0.0.61**
(внутренний health 0.0.61, нет MAX_FILE_BYTES/cancel/ETA/UI).
- Часть запросов (upload_refs) попадала на старый под → тесты вели себя непредсказуемо
(60МБ «принимался»).
## Причина (подтверждена kubeconfig'ами обоих кластеров)
- drhider развёрнут на **двух кластерах** в одном namespace-ID `20a75175-...`:
- **iot-naeel**: под `pythonk8s-cf686f894-96l7m` (возраст ~42м — свежий редеплой), **v0.0.67**;
ingress `drhider.pythonk8s.dev.nubes.ru`**185.247.187.151**.
- **naeel-test-3**: под `pythonk8s-797ddd69df-dmdg6`**старый, не погашенный** (был 0.0.61);
ingress → 185.247.187.147.
- **DNS `drhider.pythonk8s.dev.nubes.ru` → 185.247.187.151 = iot-naeel** → внешний URL обслуживает
iot-naeel (0.0.67). naeel-test-3 в DNS не участвует, но остаётся живым инстансом.
## Что сделано
- kubeconfig'и на ВМ обновлены на оба кластера (`~/.kube/config` iot-naeel, `~/.kube/config-naeel-test-3`).
- Проверены оба пода: и iot-naeel, и naeel-test-3 теперь **v0.0.67** (naeel-test-3 обновлён
пересозданием пода — `scale 0 → 1` заставил git-clone master@0.0.67).
- Внешний бэкенд — iot-naeel (0.0.67), все тесты прогнаны на нём (см. tests/2026-08-24-test-batch-v067.md).
## Осталось (рекомендация)
- В Штурвале **погасить/удалить инстанс drhider на naeel-test-3** (ns 20a75175) — он не нужен,
DNS ведёт на iot-naeel. Оставить один актуальный (iot-naeel, 0.0.67).
- Проверить прочие `pythonk8s` на iot-naeel (contractor/loadtest/polygon/atest) — это другие приложения, не drhider.
@@ -0,0 +1,43 @@
# Схема логики загрузки + найденные баги/несоответствия (2026-08-24)
_infra. Подробная схема по коду: index.html (фронт), api_bp.py, session.py, obfuscator.py, extractor.py._
## Участники
- **Браузер** — index.html: выбор файлов/папки, PUT на ВМ, SSE-прогресс, статусы.
- **ВМ-буфер** — nginx dav `https://contracts.kube5s.ru/drhider-upload/` (CORS разрешён только для origin `https://drhider.pythonk8s.dev.nubes.ru`).
- **Бэк** — Flask (api_bp.py): upload_refs (pull), process_stream (SSE, воркер-поток), session.py (лимиты), drhider (extractor→scanner→replacer→builder).
## Этап 0 — выбор файлов (браузер)
1. `fileInput` (multiple, accept) — для `.zip``listZipFiles` (раскрытие в браузере, allowedExt = .pdf .doc .docx .txt .md) → `addFileWithDedup`; иначе — как есть.
2. `folderInput` (`webkitdirectory`) — рекурсивно: `webkitRelativePath.slice(1)` (отбрасываем верхнюю папку), zip раскрывается с префиксом пути, документы — с относительным путём, прочее — пропуск.
3. `addFileWithDedup`: дедуп (имя+размер → пропуск; коллизия имени → суффикс `_2`); лимиты 50МБ/файл и 500МБ/сессия → `overNames` («🔥 не учитывается» в обычной таблице).
## Этап 1 — загрузка (uploadFiles, фаза 1)
4. `toSend` = файлы БЕЗ `overNames`; over-файлы сразу получают статус «пропущен (лимит)» (v0.0.71).
5. Для каждого файла: **PUT** на `VM_UPLOAD_URL + token + '_' + k` (имя в URL не несётся) → прогресс % в ячейке → `✓ N KB/s`. `refs.push({name, size, url})`. Таймаут 300с.
6. **POST /api/upload_refs** {session, files: refs}:
- бэк: для каждого ref: `size > 50МБ` → delete+skip; `GET url` (pull egress, timeout 120с) → `content`; `content > 50МБ` → delete+skip; `add_file(sid, name, content)` (лимит сессии 500МБ); `delete url` с ВМ.
- ошибка сети/DNS → **502 «Pull failed»** (весь запрос падает).
7. Тест-режим `?upload-only=1` — стоп после фазы 1.
## Этап 2 — обработка (SSE, фаза 2)
8. `EventSource /api/process_stream/<sid>`; воркер-поток → `obfuscate_files(all_files)`.
9. **obfuscator (проход 1)**: `expand_zips` (повторная распаковка zip, basename, защита от бомб) → `_dedupe_file_names` → для каждого: `extract_text` (исключение → `skipped`, событие `done` на этапе извлечения) → `scan_regex``extract_done` (total_chars, per_file) → `scan_llm_ner` (чанки: file_start/file_chunk/file_done; отмена → CancelRequested).
10. **проход 2**: `apply_replacements` для каждого (битые/скан → пропуск; при отмене — только `llm_done`) → `done``build_zip` + `build_mapping_csv` → событие `result`/`cancelled`.
11. Фронт: 3-секционная таблица (Обработанные / Текущий / Ожидают); статусы: `done` (до extract_done = битый → **skipped «не извлечён»** v0.0.71), `current`, `analyzed`, `pending`; `complete`/`cancelled` → статистика, кнопки ZIP/CSV, заморозка сессии.
## Найденные баги и несоответствия
1. **[исправлено v0.0.71]** Статус «пропущен» использовался для ДВУХ разных причин: лимит и «не извлёкся». Теперь: «пропущен (лимит)» и «не извлечён» (пустой/битый/скан).
2. **[исправлено v0.0.71]** over-файлы во время обработки попадали в группу «Ожидают обработки» (нет procState → pending) с оценкой времени. Теперь — отдельная группа «⛔ Пропущены (сверх лимита)».
3. **[открыто]** `upload_refs` неатомарен: при ошибке pull (DNS/сеть) часть файлов уже добавлена в сессию и удалена с ВМ, но фронт получает 502 и бросает — сессия-сирота, файлы на ВМ частично остаются (инцидент 18:42). Нужны ретраи/атомарность.
4. **[открыто]** `upload_refs` при превышении суммарного лимита сессии отвечает «Session not found» (404) — add_file возвращает False и для отсутствия сессии, и для лимита; сообщение неверное.
5. **[открыто, риск]** Повторный `expand_zips` на бэке (с `os.path.basename` — обрезает пути) при отправке zip как есть (v0.0.69) ломает соответствие idx: количество файлов бэка ≠ фронта → `sendIdx[d.idx]` = undefined. Обычно не срабатывает (фронт сам раскрывает), но риск при расхождении allowedExt.
6. **[открыто]** allowedExt не совпадают: фронт раскрывает из zip только документы, бэк `expand_zips` берёт ВСЕ файлы (без фильтра) — расхождение поведения при zip «как есть».
7. **[открыто, косметика]** Счётчик «Добавлено из папки: N» завышается при дедупе (`added++` безусловно после `addFileWithDedup`).
8. **[открыто]** Три разных отображения одной причины (лимит) до обработки / при старте / в обработке — унифицировано в v0.0.71 (группа over).
## Проверка
- `node --check` — OK, `py_compile` — OK.
- Локально (v0.0.71): группа «⛔ Пропущены (сверх лимита)» + «пропущен (лимит)» — работает.
Полный цикл локально невозможен из-за CORS ВМ-буфера (разрешён только origin прода).
- Версия 0.0.70 → 0.0.71.
+45
View File
@@ -0,0 +1,45 @@
# v0.0.34 — вывод токенов и времени LLM — 2026-08-19
**Дата:** 2026-08-19
**Версия:** 0.0.33 → 0.0.34
---
## Суть
Пользователь замечает, что обфускация долгая. Добавлен вывод в UI:
- суммарных токенов LLM за прогон;
- суммарного времени работы LLM за прогон.
Строка статуса после завершения: «✅ Обработано N файлов за Xс · LLM: Yс · Z токенов».
## Изменения
### `drhider/llm_client.py`
- В `LLMClient.__init__` добавлены кумулятивные счётчики:
- `tokens_prompt`, `tokens_completion`, `llm_sec` (float);
- свойство `tokens_total = prompt + completion`.
- В `complete()` после ответа:
- `self.llm_sec += r.elapsed.total_seconds()` — реальное время HTTP-вызова;
- `usage` (из `r.json()`) → `self.tokens_prompt` / `self.tokens_completion`
(через `.get(..., 0)` — устойчиво к отсутствию `usage`).
- Возврат `complete()` не изменён (по-прежнему `content`) — `scan_llm_ner` не трогали.
### `site/routes/api_bp.py`
- В `worker()` после `obfuscate_files` в очередь `result` добавляется
`stats = {tokens: llm.tokens_total, llm_sec: round(llm.llm_sec, 1)}`.
- В `event: complete` данные: `{total, tokens, llm_sec}`.
### `site/templates/index.html`
- В обработчике `complete`: если `d.llm_sec > 0` — добавляет
«· LLM: {llm_sec}с» и, если `d.tokens > 0` — «· {tokens} токенов».
### `site/app.py`
- `VERSION = "0.0.34"`.
## Проверка
- `py_compile` + `node --check` — OK.
- Накопление: 2 вызова → 240 prompt + 90 completion = 330 total, llm_sec = 6.0.
- Устойчивость: ответ без `usage` → токены 0, время 1.0 — без падения.
- SSE end-to-end: `complete` приходит с `{"total":1,"tokens":0,"llm_sec":0.0}`
(локально без ключа LLM — 0; в кластере с ключом — ненулевые).
+41
View File
@@ -0,0 +1,41 @@
# v0.0.37 — крупный блок статистики LLM в UI — 2026-08-19
**Дата:** 2026-08-19
**Версия:** 0.0.36 → 0.0.37
**Файл:** `site/templates/index.html`, `site/app.py` (версия)
---
## Суть
Раньше статистика LLM («... LLM: 5.3с · 1894 токенов») была МЕЛКИМ текстом
в статус-строке под кнопкой — юзер её не замечал. Теперь после обработки
показывается **крупный заметный блок** «📊 Итоги обработки»:
| Показатель | Значение |
|---|---|
| Общее время | крупное число, с |
| Время ИИ | крупное число, с |
| Токены ИИ | крупное число |
Оформление: зелёная плашка (фон-градиент, рамка 2px), три белые карточки,
значения шрифтом 24px жирным. Блок скрыт до завершения и при сбросе.
## Изменения (`index.html`)
- CSS: `.stats-block`, `.stats-title`, `.stats-row`, `.stat-item`,
`.stat-label`, `.stat-value`.
- HTML: блок `#statsBlock` с `#stTotalTime`, `#stLlmTime`, `#stLlmTokens`
(после кнопок скачивания).
- JS:
- `complete`-обработчик — заполняет и показывает блок (общее время всегда;
время ИИ и токены — если `llm_sec > 0`, иначе «—»);
- `resetAll` — скрывает блок.
## Проверка
- `node --check` — OK.
- Визуально через браузер: блок появился полноширинным, три карточки,
значения крупно — заметно.
- Версия `app.VERSION = 0.0.37`.
## Бэкенд
- Не менялся (метрики `tokens`/`llm_sec` в `event: complete` уже есть с v0.0.34).
@@ -0,0 +1,41 @@
# 2026-08-20 — LLM обрабатывает ВСЕ данные (чанкинг) — v0.0.57
## Задача
Пользователь: LLM должен обрабатывать ВСЕ данные, а не урезку 8000 символов.
Ветка сохранения пре-LLM конфигурации: `save-pre-llm-v0.0.56` (указывает на прежний master).
## Изменения (drhider/scanner.py)
`scan_llm_ner` переписан:
- Убраны глобальные урезки `t[:3000]` и `combined[:8000]`.
- Каждый файл обрабатывается ПОЛНОСТЬЮ: split_into_chunks(6000, overlap 500).
- Чанки файла — параллельно (ThreadPoolExecutor, concurrency 4), крупные файлы вперёд.
- Для каждого чанка — полный промпт + «Already captured by regex» (mapping).
- Дедуп найденного через normalize_entity (пробелы/регистр/кавычки/тире/ё).
- Верификация _verify_entity против ПОЛНОГО текста файла — отсекает галлюцинации LLM.
- regex-найденное не дублируется.
Новые хелперы (scanner.py):
- split_into_chunks(text, size=6000, overlap=500) — границы \n\n,\n,. ,? ,! ,; ,, ," "
- normalize_entity(s) — канонизация для дедупа
- _verify_entity(value, full_text) — точное/нормализованное вхождение
- _build_llm_prompt(text, already_found)
- _call_llm(text, llm_client) — вызов + разбор JSON (пусто при ошибке)
Параметры верхнего уровня (легко тюнить):
- _CHUNK_SIZE = 6000, _CHUNK_OVERLAP = 500, _LLM_CONCURRENCY = 4
## Ожидаемый эффект
- LLM покрывает все файлы целиком (в т.ч. хвосты и таблицы после 3000 символа).
- Время на ~300 файлов ≈ 3.5 мин при 4 потоках (~390 вызовов).
- Меньше галлюцинаций (верификация).
## Проверка
- py_compile OK.
- test_scanner 20/20.
- Юнит хелперов: split (12К→4 чанка ≤6100), verify (галлюцинация→False), normalize OK.
- Все 106 тестов OK.
- VERSION 0.0.57.
## Примечание
Реальная проверка LLM-покрытия и времени — на наборе T/ при прогоне на проде/локально
с ключом LLM (локально ключ отсутствует → LLM не выполняется, только regex).
@@ -0,0 +1,44 @@
# Запрос к Opus: валидация MSS Clamping плана
**Дата:** 2026-07-13
---
## Контекст
Кластер: bare-metal Kubernetes, 4 ноды, Cilium CNI (Geneve-туннель), kube-vip для VIP.
Штурвал управляет ingress через Helm-чарт (`shturval-ingress-controller-2.12.1`).
## Проблема
При `externalTrafficPolicy: Cluster` большие файлы (>5 MB) вызывают TCP stall на 51 секунду.
Диагностика: cross-node Geneve-туннель добавляет overhead (~50 байт) → пакет > MTU 1500 → ICMP Frag Needed блокируется → TCP RTO backoff.
Текущий костыль: `externalTrafficPolicy: Local` + `kubectl scale replicas=4` (Штурвал сбрасывает на 2 каждые ~час).
## Предлагаемое решение
Вместо костыля — выставить MTU в Cilium ConfigMap:
```yaml
# kubectl edit configmap -n kube-system cilium-config
mtu: 1450
```
После этого:
1. Вернуть `externalTrafficPolicy: Cluster`
2. Оставить ingress `replicaCount: 2` (стоковое значение Штурвала)
3. Никаких ручных scale
## Вопросы к Opus
**Читать только:** `docs/MSS-CLAMPING-PLAN.md`, `docs/KUBEVIP-EXTERNAL-TRAFFIC-POLICY.md`, `docs/INGRESS-SOLUTIONS.md`.
1. Корректно ли решение? Есть ли подводные камни при смене MTU через Cilium ConfigMap на работающем кластере?
2. Нужно ли что-то ещё кроме `mtu: 1450`? (например, перезапуск подов, пересоздание туннелей)
3. Как проверить что MSS clamping реально работает после изменения? (конкретные команды)
4. Есть ли риск для других сервисов (Keycloak, etc.) при смене MTU?
5. Если Cilium ConfigMap недоступен — iptables-вариант (`TCPMSS --clamp-mss-to-pmtu`) равноценен? На какую ноду вешать правило?
6. Итоговый вердикт: это правильный путь, или есть более грамотный вариант?
**Не лезть:** в код приложения, в TMP, в History (кроме указанных трёх файлов).
@@ -0,0 +1,35 @@
# Opus: проверка PROBLEM-AND-SOLUTION.md на ошибки
**Дата:** 2026-07-13
---
## Найденные ошибки
### 1. TCP backoff — неправильные числа
**Было:** `1с → 3с → 7с → 15с → 25с = 51с` — выдумано.
**Правильно:** начальный RTO = 200ms, удвоение:
```
0.2с → 0.4с → 0.8с → 1.6с → 3.2с → 6.4с → 12.8с → 25.6с = 51с
```
8 ретрансмитов от 200ms = ровно 51 секунда (Linux kernel default).
### 2. DSR — неверное объяснение причины stall
**Было:** «двойной инкапсуляции return-трафика (главная причина stall)».
**Правильно:** stall на **входящем** трафике (клиент → pod), а не на return. DSR оптимизирует обратный путь, но не решает проблему inbound MTU black-hole.
### 3. Ping измеряет pod MTU, не underlay
Утверждение что `ping -M do` доказывает underlay = 1450 — неточно. Ping из пода измеряет MTU пода (1450 = 1500 50 Cilium). Физический underlay = 1450 подтверждён эмпирически (фикс сработал), а не прямым замером.
## Что верно
- Geneve overhead 50 байт: outer IP(20) + UDP(8) + Geneve(8) + inner Eth(14) = 50 ✓
- `mtu: 1450` → pod MTU = 1400 ✓
- `rollout restart ds/cilium` обязателен ✓
- iptables может не работать с eBPF dataplane ✓
- Корень проблемы и решение верны ✓
@@ -0,0 +1,31 @@
# Opus: финальная проверка PROBLEM-AND-SOLUTION.md
**Дата:** 2026-07-13
---
## Вердикт
Документ технически добротный. Диагноз и корневое решение (`mtu: 1450` + `Cluster` + `replicaCount: 2`) — правильные.
## Исправлено
1. **Ethernet 14** — уточнено: «инкапсулируемый L2-кадр 14» (внутренний Ethernet, не внешний)
2. **51с = прикладной таймаут** — смягчено: «браузер/curl разрывает ожидание» вместо «соединение разрывается»
## Что подтверждено как верное
- PMTU blackhole как причина — классический сценарий
- Cilium: `mtu` в configmap = underlay девайса, pod MTU = mtu overhead
- Geneve overhead = 50 байт (внешний IP 20 + UDP 8 + Geneve 8 + L2 14)
- Решение: `mtu: 1450` → pod 1400 → Geneve-пакет 1450 → влазит в path 1450
- `rollout restart ds/cilium` обязателен
- `externalTrafficPolicy: Local``Cluster` после фикса MTU
- DSR решает только обратный путь, stall на inbound
- iptables может не работать с eBPF dataplane
## Мелкие замечания (не исправлены, допустимы)
- 200ms RTO — корректно для сценария (уже установленное TCP-соединение)
- Тип туннеля (Geneve vs VXLAN) — overhead одинаков (50), математика не меняется
- Замеры из таблицы результатов — эмпирические, из кода не подтвердить
@@ -0,0 +1,77 @@
# Opus: валидация MSS Clamping плана
**Дата:** 2026-07-13
---
## Ключевое исправление
В плане ошибка: `mtu: 1450` в cilium-config — это MTU **underlay-сети**, не туннеля. Cilium сам вычитает 50 байт на Geneve:
```
mtu: 1450 → tunnel MTU = 1450 50 = 1400
```
По умолчанию Cilium автодетектит underlay 1500 → туннель 1450. Если stall всё равно есть — underlay реально < 1500. **Число надо измерить, не гадать.**
---
## Ответы на вопросы
### 1. Корректно ли решение?
Да, направление верное. Но:
- ConfigMap сам не применяется — нужен `rollout restart ds/cilium`
- Число 1450 — гипотеза, нужен замер между нодами
- `mtu` глобален на весь кластер
- Существующие TCP-соединения не поменяют MSS — только новые
### 2. Что ещё нужно?
1. Измерить underlay MTU: `ping -M do -s 1472 <ip_другой_ноды>` (уменьшать пока не пройдёт)
2. `kubectl rollout restart ds/cilium -n kube-system`
3. Пересоздать поды приложения (ingress, drhider)
4. Вернуть `externalTrafficPolicy: Cluster`
### 3. Как проверить?
```bash
# Измерить до:
ping -M do -s 1472 <ip_другой_ноды>
tracepath <ip_другой_ноды>
# После изменения:
kubectl -n kube-system exec ds/cilium -- cilium-dbg status | grep -i mtu
ip link show | grep -E 'cilium|geneve'
tcpdump -ni any 'tcp[tcpflags] & tcp-syn != 0' -vv | grep mss
# Функционально: файл >5 MB без паузы
```
### 4. Риск для других сервисов?
Понижение MTU безопасно (меньше = консервативнее). Небольшой рост числа пакетов, микро-потеря throughput. Функционально ничего не ломает. Рестарт агентов — возможны короткие блипы, лучше в окно.
### 5. iptables равноценен?
**Нет.** Cilium с eBPF обходит netfilter FORWARD — правило может не видеть пакеты. Надёжнее `cilium-config`. Если iptables — то на всех 4 нодах (kube-vip раздаёт VIP на все).
### 6. Вердикт
Путь правильный. Уточнённый план:
1. Измерить path MTU между нодами
2. Выставить `mtu` = измеренное значение (вероятно < 1500)
3. Рестарт агентов + подов
4. Вернуть `Cluster` policy
При `Cluster` проблема реплик исчезает сама (2 хватает).
---
## Моё мнение
Opus прав. План нужно дополнить **замером**. Без замера `1450` — пальцем в небо. Обновлю `MSS-CLAMPING-PLAN.md` с учётом:
- Добавить шаг 0: измерение path MTU
- Исправить формулу: `mtu` = underlay, туннель = `mtu - 50`
- Добавить `rollout restart ds/cilium` + рестарт подов
- Убрать iptables как основной вариант (ненадёжен с Cilium)
+145
View File
@@ -0,0 +1,145 @@
# DrHider — Полный аудит и план рефакторинга
**Дата:** 2026-07-12
**Платформа:** Штурвал (Managed Flask) — запускает Flask сам, без Dockerfile/gunicorn
**URL:** https://drhider.pythonk8s.dev.nubes.ru/
**Репозиторий:** https://gitea.services.ngcloud.ru/Nail/drhider
---
## Текущее состояние (аудит каждого файла)
| Файл | Статус | Решение |
|---|---|---|
| `Dockerfile` | ❌ НЕ НУЖЕН | Штурвал сам запускает Flask. Удалить. |
| `drhider_server.py` | ❌ НЕ НУЖЕН | Standalone-сервер с ВМ (:8767). Flask app.py уже обрабатывает /api/drhider. Удалить. |
| `requirements.txt` | ⚠️ Нужны правки | Убрать `gunicorn` и `gevent`. Оставить `flask`, `python-docx`, `pdfplumber`, `httpx`. |
| `.gitignore` | ✅ OK | Без изменений. |
| `.gitea/workflows/deploy.yaml` | ✅ OK | CI для Штурвала. |
| `site/app.py` | ⚠️ Нужны правки | Удалить блок `if __name__ == "__main__"` (gunicorn). При рефакторинге — вынести LLMClient. |
| `site/templates/index.html` | ✅ OK | UI. |
| `site/static/favicon.svg` | ✅ OK | Иконка (из loadtest). |
| `drhider.py` | ⚠️ МОНОЛИТ | 560 строк. Разбить на ~20 модулей. |
| `History/` | ✅ OK | Документация. |
---
## План рефакторинга (Фаза 1: удаление лишнего)
### Шаг 1.1 — Удалить `Dockerfile`
### Шаг 1.2 — Удалить `drhider_server.py`
### Шаг 1.3 — Исправить `requirements.txt` (убрать gunicorn, gevent)
### Шаг 1.4 — Исправить `site/app.py` (убрать блок `if __name__ == "__main__"`)
### Шаг 1.5 — Проверить синтаксис → commit → push
---
## План рефакторинга (Фаза 2: модульная структура)
### Новая структура `drhider/` пакета (вместо `drhider.py` 560 строк):
```
drhider/ # Пакет ядра обфускации
├── __init__.py # re-export: obfuscate_files()
├── config.py # Константы: ENTITY_PATTERNS, COMPANY_PATTERN, PERSON_PATTERN,
│ # RU_SURNAMES, RU_NAMES, RU_PATRONYMICS,
│ # RU_CITIES, RU_STREETS, FAKE_DOMAINS
├── checksum.py # _checksum_inn10, _checksum_inn12, _checksum_ogrn
├── random_utils.py # _random_digits, _random_letters
├── generators/ # Генераторы фиктивных значений
│ ├── __init__.py # ENTITY_GENERATORS маппинг
│ ├── phone.py # generate_phone
│ ├── email.py # generate_email
│ ├── inn.py # generate_inn10, generate_inn12
│ ├── ogrn.py # generate_ogrn
│ ├── kpp.py # generate_kpp
│ ├── bik.py # generate_bik
│ ├── accounts.py # generate_rs, generate_ks
│ ├── passport.py # generate_passport
│ ├── company.py # generate_company
│ ├── person.py # generate_person
│ └── address.py # generate_address
├── extractor.py # _extract_text, _expand_zips, _convert_pdfs_to_docx
├── scanner.py # Проход 1: _scan_regex, _scan_llm_ner
├── replacer.py # Проход 2: _replace_in_docx, _replace_in_text, _apply_replacements
├── builder.py # Сборка: _build_zip, _build_mapping_csv
├── obfuscator.py # TwoPassObfuscator — оркестратор
└── llm_client.py # LLMClient (из app.py → сюда)
site/ # Flask-приложение
├── __init__.py
├── app.py # Только create_app() + регистрация blueprint'ов
├── routes/
│ ├── __init__.py # Регистрация всех blueprint'ов
│ ├── main_bp.py # GET /
│ ├── health_bp.py # GET /health
│ └── api_bp.py # POST /api/drhider
├── templates/index.html
└── static/favicon.svg
```
### Принципы:
- Каждый файл ≤ 50 строк (где возможно)
- Каждый файл — одна ответственность
- Максимум docstring и инлайн-комментариев
- Все импорты явные, никаких `import *`
---
## Результат рефакторинга (2026-07-12)
### Итоговая структура
```
drhider/ # Пакет ядра обфускации
├── __init__.py # re-export: obfuscate_files, LLMClient, TwoPassObfuscator
├── config.py # Константы: ENTITY_PATTERNS, словари имён/городов/улиц
├── checksum.py # Контрольные суммы: ИНН10, ИНН12, ОГРН
├── random_utils.py # Утилиты: random_digits, random_letters
├── llm_client.py # LLMClient — HTTP-клиент к LLM API
├── extractor.py # Извлечение текста + expand_zips + convert_pdfs_to_docx
├── scanner.py # Проход 1: scan_regex, scan_llm_ner
├── replacer.py # Проход 2: apply_replacements, replace_in_docx, replace_in_text
├── builder.py # Сборка: build_zip, build_mapping_csv
├── obfuscator.py # TwoPassObfuscator — оркестратор
└── generators/ # Генераторы фиктивных значений
├── __init__.py # ENTITY_GENERATORS маппинг
├── phone.py, email.py # Телефон, email
├── inn.py, ogrn.py, kpp.py # ИНН, ОГРН, КПП
├── bik.py, accounts.py # БИК, счета (р/с, к/с)
├── passport.py # Паспорт
├── company.py, person.py # Компания, ФИО
└── address.py # Адрес
site/ # Flask-приложение
├── app.py # create_app() + VERSION (20 строк)
├── routes/
│ ├── __init__.py # register_routes()
│ ├── main_bp.py # GET /
│ ├── health_bp.py # GET /health
│ └── api_bp.py # POST /api/drhider
├── templates/index.html
└── static/favicon.svg
```
### Что изменилось
| Было | Стало |
|---|---|
| `drhider.py` — 560 строк монолит | Пакет `drhider/` — 22 модуля по 20–100 строк |
| `site/app.py` — 100 строк (всё в одном файле) | `app.py` 33 строки + 3 blueprint'а по 2050 строк |
| `Dockerfile` | Удалён (Штурвал сам) |
| `drhider_server.py` | Удалён (Flask заменяет) |
### Коммиты
- `a2f754a` — Фаза 1: удалён Dockerfile, drhider_server.py, gunicorn
- `37581eb` — Фаза 2: config.py, checksum.py, random_utils.py, generators/
- `2cf4e08` — Фаза 2: llm_client.py, extractor.py, scanner.py, replacer.py, builder.py, obfuscator.py
- `51bc1a7` — Фаза 2: site/app.py → blueprint'ы, старый drhider.py удалён
| Переменная | Значение |
|---|---|
| `LLM_API_KEY` | (секрет — не хранить в git) |
| `LLM_URL` | `https://api.aillm.ru/v1/chat/completions` |
| `LLM_MODEL` | `gpt-oss-120b` |
+45
View File
@@ -0,0 +1,45 @@
# v3.0.0 — универсальная архитектура (2026-07-12)
Переход на Markdown-пайплайн + универсальное LLM-обнаружение.
## Что изменилось
### 1. `generators/fallback.py` — новый
Character-class preserving замена для неизвестных типов сущностей.
Цифры→цифры, буквы→буквы, спецсимволы→без изменений.
### 2. `scanner.py` — универсальный LLM-промпт
- Убран жёсткий список типов (person, company, address, passport)
- LLM САМА придумывает тип (snake_case) — person_name, contract_number, employee_id...
- Если тип не совпадает с известными — fallback-генератор
- Промпт на английском (LLM точнее)
### 3. `extractor.py` — Markdown-пайплайн
- `docx_to_markdown()`: DOCX → MD (Heading → #, Bold → **, таблицы → |...|)
- `pdf_to_markdown()`: PDF → MD (текст + таблицы, без форматирования)
- `extract_text()` возвращает только str (без docx-объектов)
- Удалён `convert_pdfs_to_docx()` (PDF → MD напрямую)
### 4. `replacer.py` — упрощён
- Удалён `replace_in_text()` (заменён на `apply_replacements()` + `.encode()`)
- Оставлен `replace_in_docx()` для будущего DOCX-выхода
### 5. `obfuscator.py` — новый пайплайн
- Pass 1: extract MD → scan_regex + scan_llm_ner
- Pass 2: apply_replacements к MD → .md файлы
- Нет docx-объектов, нет convert_pdfs_to_docx
## Коммиты
- `579f9c8` — generators: fallback.py
- `2ceeb05` — scanner.py: универсальный промпт
- `af2f2a9` — extractor.py: MD + obfuscator.py: новый пайплайн
- `f5427b0` — replacer.py: удалён replace_in_text
- `2a2cc14` — v3.0.0
## Плюсы
- LLM точнее (нет XML-шума, только Markdown)
- Новые типы сущностей — без правки кода
- Код проще: -120 строк, -1 функция, -1 модуль (convert_pdfs_to_docx)
- Единый внутренний формат (Markdown)
+51
View File
@@ -0,0 +1,51 @@
# План v3 — универсальная архитектура (2026-07-12)
## Цель
Убрать жёсткую привязку к фиксированному списку типов сущностей.
Перейти на Markdown как внутренний формат.
## Изменения по модулям
### 1. `scanner.py` — универсальный LLM-промпт
- Убрать список типов из промпта
- LLM сама называет типы (snake_case)
- `value` — verbatim из текста
### 2. `generators/` — fallback-генератор
- Новый `fallback.py`: character-class preserving замена
- `__init__.py`: добавить в ENTITY_GENERATORS
- `obfuscator.py`: использовать fallback для неизвестных типов
### 3. `extractor.py` — единый MD-пайплайн
- DOCX → MD (python-docx: стили, форматирование)
- PDF → MD (pdfplumber: текст + таблицы)
- TXT → как есть
- Убрать convert_pdfs_to_docx (не нужен)
- Убрать хранение docx-объектов
### 4. `replacer.py` — упростить
- `apply_replacements(text)` — уже есть
- `replace_in_docx(doc, mapping)` — сохранить как утилиту для DOCX-выхода
- Удалить `replace_in_text()` (заменяется на apply_replacements)
### 5. `obfuscator.py` — обновить пайплайн
- Pass 1: extract MD → scan_regex + scan_llm_ner
- Pass 2: apply_replacements к MD
- Опционально: replace_in_docx к оригинальному DOCX
### 6. `api_bp.py` — output_format
- Параметр `output_format: "md" | "docx"` (по умолчанию md)
### 7. `.doc` — потом
- Добавим convert_doc_to_docx через LibreOffice отдельно
## Порядок реализации
1. `generators/fallback.py` + обновить `__init__.py`
2. `scanner.py` — новый промпт
3. `extractor.py` — MD-конвертация
4. `replacer.py` — упростить
5. `obfuscator.py` — новый пайплайн
6. `api_bp.py` — output_format
7. Проверить всё → commit
+30
View File
@@ -0,0 +1,30 @@
# План v3.1 — пофайловая обработка + прогресс в таблице (2026-07-12)
## Проблема
Фронтенд v3.0 отправляет все файлы одним запросом → долгий LLM-вызов → таймаут.
Нужно: обрабатывать по одному, прогресс в таблице, все результаты → один ZIP.
## Решение
### Фронтенд (index.html)
1. **XHR** вместо fetch — надёжнее для blob, явный timeout
2. **Колонка «Статус»** в таблице файлов
3. **По одному файлу:** цикл, каждый файл → отдельный XHR → ZIP
4. **Прогресс в таблице:** ⏳ → ✓ 2.1 сек (или ✗ ошибка)
5. **JSZip** — склеивает все полученные ZIP'ы в один
6. **Скачивание** — один файл `drhider_output.zip` в конце
### Статусы в таблице
| Статус | Отображение |
|---|---|
| Ожидание | ⏳ |
| Успех | ✓ 2.1 сек |
| Ошибка | ✗ сообщение |
### Бэкенд
Без изменений. API уже принимает один файл.
### НЕ трогать
- gunicorn не нужен (файлы по одному, каждый запрос ≤10 сек)
- app.run() — остаётся
- drhider пакет — без изменений
@@ -0,0 +1,63 @@
# План для Flash: выбор целой папки рекурсивно (v0.0.68)
> Самодостаточный план. Файл: `site/templates/index.html` (весь фронт — ванильный JS, без сборки).
> Текущая версия 0.0.67 → поднять до **0.0.68** (в `site/app.py` `VERSION`).
## Задача
Кнопка «📁 Выбрать папку»: юзер выбирает папку → в таблицу рекурсивно добавляются ВСЕ файлы
(включая вложенные подпапки), как сейчас раскрываются ZIP. **Относительный путь сохраняется**
(без имени верхней выбранной папки): `Подпапка/Внутри/report.pdf`.
## Что уже есть (ориентиры в index.html)
- `<input type="file" id="fileInput" multiple accept=".doc,.docx,.pdf,.txt,.zip">` в `.file-input-wrap`.
- `const fi = document.getElementById('fileInput');` (в начале `<script>`).
- `fi.addEventListener('change', async () => {...})` — текущий обработчик: для `.zip``listZipFiles(f)` (раскрывает архив, возвращает `File[]`), иначе `addFileWithDedup(f)`; в конце пересобирает `DataTransfer`, `rr()`.
- `addFileWithDedup(file)` — дедуп по `file.name` + `file.size`, лимиты `MAX_FILE_BYTES`/`MAX_SESSION_BYTES`, `overNames`.
- `listZipFiles(file)` — рекурсивно раскрывает `.zip` (в т.ч. вложенные), фильтрует только документы
`['.pdf','.doc','.docx','.txt','.md']`, возвращает `File[]` (имена = пути внутри архива).
- `busy` — флаг «идёт загрузка/обработка»; есть `setBusy(b)`, guard'ы в `change` и `rm`.
- Footer: `<div class="footer-bar">` содержит `fileCount`, `cancelBtn`, `newSessionBtn`, `uploadBtn`.
## Реализация
### 1. HTML — кнопка и скрытый input
- В `.file-input-wrap` (рядом с кнопкой «Choose File», т.е. `#fileInput`) добавить кнопку:
`<button type="button" class="btn" id="folderBtn">📁 Выбрать папку</button>`
- Добавить скрытый input (вне/внутри формы без разницы):
`<input type="file" id="folderInput" webkitdirectory style="display:none">`
- `folderBtn.onclick = () => folderInput.click();`
### 2. JS — ссылки и обработчик
- `const folderInput = document.getElementById('folderInput');`
- `const DOC_EXTS = ['.pdf', '.doc', '.docx', '.txt', '.md'];` (можно переиспользовать из listZipFiles — там это локальная константа `allowedExt`; продублировать на верхнем уровне или вынести).
- `folderInput.addEventListener('change', async () => { ... })`:
1. `if (busy) return;`
2. `const files = Array.from(folderInput.files);`у каждого есть `file.webkitRelativePath` (вида `TopFolder/Подпапка/report.pdf`).
3. Для каждого файла:
- Определить `rel = file.webkitRelativePath.split('/')` и **отбросить первый сегмент** (верхняя папка): `rel = rel.slice(1).join('/')`.
- `const low = rel.toLowerCase();`
- Если `.zip``const nested = await listZipFiles(file);` затем для каждого `nf` создать
`new File([nf], rel_dir + '/' + nf.name, {lastModified: nf.lastModified})`, где
`rel_dir = rel.slice(0, rel.lastIndexOf('/'))` (папка, в которой лежал zip; если в корне — пусто),
и `addFileWithDedup(that)`. Обернуть в try/catch: при ошибке раскрытия добавить сам `.zip` как есть (по аналогии с текущим обработчиком).
- Иначе если `DOC_EXTS.some(e => low.endsWith(e))``addFileWithDedup(new File([file], rel, {lastModified: file.lastModified}))`.
- Иначе — пропустить (не документ).
4. В конце — показать индикатор «Разбираю папку…» на время обработки (как с ZIP: `document.body.style.cursor='wait'` + `st`), затем `rr()`.
- Показать индикатор до цикла, сбросить в `finally` (как текущий change-обработчик).
### 3. Важные детали
- **Дедуп/лимиты**: `addFileWithDedup` уже работает по `file.name`; так как имя = относительный путь,
файлы из разных подпапок не сливаются. Лимиты 50МБ/500МБ применяются автоматически.
- **Имя в таблице и в выходном ZIP** = относительный путь (с `/`). Это ожидаемо.
- **`webkitdirectory`**: поддерживается Chrome/Edge (полно), Firefox (частично), Safari — нет.
Best-effort, ничего не ломается в неподдерживаемых браузерах (кнопка просто не откроет выбор папки).
- **Guard `busy`**: во время загрузки/обработки выбор папки не должен работать (как и обычный input).
### 4. Версия и проверка
- `site/app.py`: `VERSION = "0.0.68"`.
- Проверки: `python3 -m py_compile site/app.py`; `node --check` на извлечённом `<script>` (как обычно).
- Тест вручную/браузером: папка с подпапками + вложенный zip + кириллические имена → все файлы в таблице
с относительными путями, дедуп/лимиты работают.
## Не трогать
- Логику обфускации, SSE, сессии, лимиты — только добавить кнопку/input/обработчик выбора папки.
@@ -0,0 +1,310 @@
# План реализации для Флаша — DrHider: TTL-фикс + трекинг файлов + прерывание + ETA + UI
> **Кому:** агент-кодер (Flash). Этот документ самодостаточен — код уже есть, нужно его доработать.
> **Проект:** DrHider — Flask-обфускатор документов. Локально `/home/naeel/nubes/drhider`.
> **Версия:** сейчас 0.0.62 → после реализации поднять до **0.0.63**.
> **Что НЕ делать:** не менять логику обфускации/LLM по сути; только добавить продление TTL, прогресс по файлам/чанкам, прерывание, оценку времени и UI.
---
## 1. Контекст и устройство кода
Точка входа: `site/app.py` (`create_app()`, `VERSION`). Роуты: `site/routes/api_bp.py`.
Сессии: `site/session.py`. Логика обфускации: пакет `drhider/`.
Ключевые файлы и функции (уже существующие):
| Файл | Что внутри |
|---|---|
| `site/session.py` | in-memory сессии, `TTL_SECONDS=30*60`, `MAX_FILE_BYTES`, `MAX_SESSION_BYTES`, `create_session/add_file/get_files/store_result/get_result/store_csv/get_csv/cleanup/file_count`, `_start_timer(sid)` |
| `site/routes/api_bp.py` | `upload`, `upload_refs`, `session_files`, `process_stream` (SSE), `process` (legacy), `download`, `csv_download` |
| `drhider/obfuscator.py` | `TwoPassObfuscator.obfuscate()` + обёртка `obfuscate_files()` |
| `drhider/scanner.py` | `scan_regex`, `split_into_chunks`, `scan_llm_ner`, `_call_llm`, `_LLM_CONCURRENCY=1` |
| `drhider/builder.py` | `build_zip(files, mapping_csv)`, `build_mapping_csv(mapping)` |
| `drhider/replacer.py` | `apply_replacements`, `_build_combined_re`, `replace_in_docx` |
| `site/templates/index.html` | весь фронт (ванильный JS, без фреймворков) |
### 1.1 Как сейчас устроен `obfuscate()` (важно)
`drhider/obfuscator.py`, метод `TwoPassObfuscator.obfuscate(files, progress_cb=None) -> (zip_bytes, csv_str)`:
1. `files = extractor.expand_zips(files)`; `files = _dedupe_file_names(files)`.
2. **Проход 1 (сбор):**
- Цикл по файлам: извлечь текст `extractor.extract_text(...)``all_texts[fname]=text`;
regex-скан `scanner.scan_regex(text, self._mapping, self._counters)`; `progress_cb("start", i, total, display_name, 0.0)` в начале каждого файла; `progress_cb("done", ...)` только для **пропущенных (битых)** файлов (`skipped.add(fname)`).
- После цикла: `scanner.scan_llm_ner(all_texts, self._mapping, self._llm_client, self._counters)`**один вызов на все файлы**, без прогресса.
- `self._sorted_keys = sorted(...)`, `self._compiled_re = replacer._build_combined_re(...)`.
3. **Проход 2 (замена):** цикл по файлам, для каждого `replacer.apply_replacements(...)`
`results.append((fname, obf_content))`; `progress_cb("done", i, total, display_name, round(file_times[i], 2))`.
4. **Сборка:** `csv_str = builder.build_mapping_csv(self._mapping)`; `zip_data = builder.build_zip(results, csv_str)`; return.
5. `finally:` очищает `self._mapping/_sorted_keys/_compiled_re/_counters`.
**Факт:** `mapping.csv` **НЕ кладётся в ZIP** (by design, в `build_zip`); CSV отдаётся отдельно через `/api/csv/<sid>`.
### 1.2 Как сейчас устроен SSE `process_stream`
`api_bp.py`, `process_stream(sid)`:
- `files = get_files(sid)`; `all_files = [(fname, content, "") for ...]`.
- `generate()`: `llm=LLMClient()`, `q=queue.Queue()`, `cancel=threading.Event()`, `progress(phase,idx,total,name,elapsed)` кладёт в `q`.
- `worker()`: `zip_data, csv_str = obfuscate_files(all_files, llm_client=llm, progress_cb=progress)`; `q.put(("result", zip, csv, stats))` / `q.put(("error", repr))`.
- Цикл: `q.get(timeout=1)`; при `Empty`**heartbeat** `event: llm` с `data: {active, elapsed, tokens}`; при `progress``event: <phase>`; при `result``store_result`+`store_csv`+`event: complete`; при `error``event: error`.
SSE-события сейчас: `start`, `done`, `llm` (heartbeat), `complete`, `error`.
### 1.3 Как сейчас устроен фронт (index.html)
Элементы: `fileList` (`fl`), `fileCount` (`fc`), `uploadBtn` (`ub`), `status` (`st`), `dlBtns` (`db`),
`liveBlock/liveTimer/liveFile/liveLlm/liveLlmTime`, `statsBlock/stTotalTime/stLlmTime/stLlmTokens`.
Функции: `fs(b)`, `rr()` (рендер таблицы), `ss(idx,html)` (обновить статус ячейки), `uploadFiles()`,
`downloadZip()`, `downloadCsv()`, `addFileWithDedup()`, `rm(i)`, `resetAll()`.
Обработчики SSE: `start`, `llm`, `done`, `complete`, `onerror`.
`sf` — массив File; `sendIdx[k]` — индекс в `sf` для k-го отправленного файла.
---
## 2. Цель (фича)
1. **TTL-фикс**: сессия не должна умирать во время долгой обработки (сейчас 30 мин → долгий прогон теряет результат).
2. **Трекинг файлов/чанков**: знать, какой файл сейчас обрабатывается, сколько прошло/осталось.
3. **Прерывание с сохранением**: кнопка «Прервать» → мягкая остановка → сохранить в ZIP+CSV **всё, что успело полностью обработаться**.
4. **Оценка времени**: глобальная (весь пакет) + per-file (текущий файл).
5. **UI**: таблица из 3 секций (готово / текущий / ожидают), кнопка «Прервать», диалог-подтверждение.
---
## 3. Блок A — TTL-фикс (`site/session.py`)
### Текущее
```python
TTL_SECONDS = 30 * 60
def _start_timer(sid):
def _clean():
with _lock:
_sessions.pop(sid, None)
timer = threading.Timer(TTL_SECONDS, _clean)
timer.daemon = True
timer.start()
return timer
```
Таймер хранится как `s["timer"]` в `create_session`.
### Изменения
1. Добавить `def touch(sid)`: отменить старый таймер (`s["timer"].cancel()`) и запустить новый (перезаписать `s["timer"]`). `touch` под `_lock`, безопасна при отсутствии сессии.
2. Добавить `def pause_ttl(sid)`: `s["timer"].cancel()` (без перезапуска) — «сессия живёт пока идёт обработка».
3. Добавить `def resume_ttl(sid)`: запустить `_start_timer(sid)` заново.
4. В `create_session` добавить в словарь `"cancel": threading.Event()`.
5. Добавить `def request_cancel(sid) -> bool`: если сессия есть — `s["cancel"].set()`, вернуть True; иначе False.
6. Добавить `def get_cancel_event(sid) -> Optional[threading.Event]`: вернуть `s["cancel"]` или None.
### Где использовать
- `process_stream`: в начале `pause_ttl(sid)`; в конце (complete/error/cancelled) `resume_ttl(sid)`.
- legacy `process()`: то же самое (тот же баг).
---
## 4. Блок B — трекинг файлов/чанков (`drhider/scanner.py`, `drhider/obfuscator.py`)
### 4.1 `scan_llm_ner` — новые параметры
```python
def scan_llm_ner(all_texts, mapping, llm_client, counters,
cancel_event=None, file_progress=None):
```
- `cancel_event`: `threading.Event` или None.
- `file_progress`: callable(event, fname, **fields) или None.
Цикл сейчас: `for fname, full_text in file_items:` (файлы отсортированы по убыванию длины).
Добавить:
- **В начале итерации файла**: `if cancel_event and cancel_event.is_set(): raise CancelRequested(...)` (между файлами — граница отмены).
- `file_progress("file_start", fname, chars=len(full_text), chunks=len(chunks))` (после разбиения на чанки).
- Внутри цикла по чанкам (последовательная ветка `for c in chunks:`): после каждого чанка
`file_progress("file_chunk", fname, chunks_done=k, chunks_total=len(chunks))`.
- После обработки файла: `file_progress("file_done", fname, elapsed=...)`.
**Важно:** отмену проверяем ТОЛЬКО между файлами (не между чанками) — файл добрается до конца,
граница всегда целая.
### 4.2 Новое исключение
В `drhider/obfuscator.py` (или отдельно) определить:
```python
class CancelRequested(Exception):
"""Обработка прервана пользователем."""
```
(данные частичного результата НЕ класть в исключение — см. Блок C: obfuscate сам собирает частичный результат и возвращает его с флагом.)
### 4.3 `obfuscate()` — новый контракт возврата
Поменять возврат с `(zip, csv)` на `(zip, csv, meta)`:
- `meta` — None при штатном завершении;
- при отмене — `{"cancelled": True, "processed": <int>, "total": <int>}`.
`obfuscate` получает новые параметры: `cancel_event=None`, `file_progress=None`.
Логика отмены внутри `obfuscate()`:
- Передать `cancel_event` и `file_progress` в `scan_llm_ner`.
- В проходе 2 (замена) перед каждым файлом: `if cancel_event.is_set(): break` (прервать замену).
- После LLM: если отменено ДО конца LLM — `scan_llm_ner` бросит `CancelRequested`. Поймать её,
вычислить список файлов, чей LLM завершён (см. 4.4), выполнить замену **только для них**,
собрать частичный `results` + `csv_str` и вернуть `(zip, csv, {"cancelled": True, "processed": X, "total": N})`.
- Если отменено во время прохода 2 (после полного LLM): `break` цикла замены → частичный `results` → тот же meta-контракт.
### 4.4 Как понять, какие файлы «полностью обработаны» при отмене в LLM
`scan_llm_ner` итерирует `file_items` (отсортированы по убыванию длины). Завести в `obfuscate`
множество `llm_done: set` — заполняется через `file_progress("file_done", fname)`.
При `CancelRequested` — файлы в `llm_done` считаются полностью проанализированными;
для них выполняется замена (их сущности уже в `self._mapping`). Файлы вне `llm_done` (и в `skipped`)
в частичный результат **не попадают**.
`total` в meta = число файлов после dedup/expand_zips (т.е. `len(files)` до прохода 2);
`processed` = число файлов, попавших в частичный `results`.
---
## 5. Блок C — прерывание в API (`site/routes/api_bp.py`)
### 5.1 Новый эндпоинт
```python
@api_bp.route("/cancel/<sid>", methods=["POST"])
def cancel(sid):
if request_cancel(sid):
return jsonify({"ok": True}), 200
return jsonify({"ok": False, "error": "Session not found"}), 404
```
### 5.2 `process_stream` — изменения
- `pause_ttl(sid)` в начале.
- `cancel_event = get_cancel_event(sid)`; передать в `obfuscate_files(..., cancel_event=cancel_event, file_progress=...)`.
- `file_progress` callback: класть события в `q` (новый тип `("file", event, fname, fields)`).
- `worker()`: принять `(zip, csv, meta)`; если `meta and meta.get("cancelled")`
`q.put(("cancelled", zip, csv, stats, meta))`, иначе как раньше `("result", ...)`.
- В цикле обработки `q`:
- `("file", event, fname, fields)``yield event: <event>` с `data: {"name": fname, **fields}`.
- `("cancelled", zip, csv, stats, meta)``store_result`+`store_csv``yield event: cancelled`
с `data: {"saved": meta["processed"], "total": meta["total"], "tokens": stats["tokens"], "llm_sec": stats["llm_sec"]}`.
- В конце любого исхода (complete/error/cancelled) — `resume_ttl(sid)`.
- **Heartbeat `llm`** — расширить: `data: {active, elapsed, tokens, eta_sec, done_chars, total_chars}`.
`total_chars` вычисляется после прохода 1 (нужно передать его из `obfuscate` в генератор —
например, через `file_progress` событие `extract_done` с `{total_chars}`), `done_chars`/`eta_sec` — из LLM-прогресса.
### 5.3 Схема новых SSE-событий (итоговый контракт)
| event | data (JSON) | когда |
|---|---|---|
| `start` | `{idx,name,total,elapsed}` | проход 1, каждый файл (уже есть) |
| `extract_done` | `{total_chars}` | после извлечения всех текстов |
| `file_start` | `{name, chars, chunks}` | начало LLM файла |
| `file_chunk` | `{name, chunks_done, chunks_total, eta_sec}` | после каждого чанка LLM |
| `file_done` | `{name, elapsed}` | LLM файла завершён |
| `llm` | `{active, elapsed, tokens, eta_sec, done_chars, total_chars}` | heartbeat (1 раз/сек) |
| `done` | `{idx,name,total,elapsed}` | проход 2, каждый файл (уже есть) |
| `complete` | `{total, tokens, llm_sec}` | успех (уже есть) |
| `cancelled` | `{saved, total, tokens, llm_sec}` | прерывание (новое) |
| `error` | `{error}` | ошибка (уже есть) |
---
## 6. Блок D — оценка времени (ETA)
### 6.1 Глобальная
- После `extract_done` известен `total_chars`.
- Во время LLM: `tokens` и `elapsed` уже есть в heartbeat. Скорость = `tokens/elapsed`.
- `est_total_tokens ≈ total_chars × K` (K — эмпирический коэффициент; взять ~7–10, подстроить по замерам).
- `eta_sec = max(0, (est_total_tokens tokens) / (tokens/elapsed))`.
- Вычислять в генераторе (у него есть `llm` и `total_chars`) и класть в heartbeat `eta_sec`.
### 6.2 Per-file (текущий файл)
- `file_start` даёт `chunks`. `file_chunk` даёт `chunks_done`.
- Скорость чанка (сек/чанк) = `elapsed_файла / chunks_done` (меряем по факту текущего файла).
- `file_eta = (chunks_total chunks_done) × (сек/чанк)`.
- На самом старте (chunks_done=0) — грубая оценка `chars × историч. сек/символ`.
- Класть `eta_sec` в `file_chunk` (вычисляет worker, у него точный elapsed).
---
## 7. Блок E — UI (`site/templates/index.html`)
### 7.1 Таблица из 3 секций (этап обработки)
Во время Фазы 2 перестроить таблицу. Завести `procState` — объект: `idx -> {st: 'pending'|'current'|'done'|'skipped', elapsed, eta}`.
Порядок строк сверху вниз:
1. **done** — уже обработанные; в столбце «Статус» фактическое время (`✓ 3.2с`).
2. **current** — подсвечен (класс `row-current`); в столбце «Статус» `прошло 0:42 / ~1:20` (тикает через `setInterval`).
3. **pending** — не обработанные; в столбце «Статус» оценка (`~12с`).
`rr()` модифицировать: если идёт обработка — рендерить 3 группы в этом порядке (по `procState`),
иначе — как сейчас (по `sf`).
Обновления:
- `start` (idx) → `procState[idx]={st:'pending'}` (или current — см. ниже).
- `file_start` (name→idx) → `procState[idx].st='current'`.
- `file_chunk` → обновить `eta` текущего.
- `file_done` → вернуть в pending (LLM-анализ не значит «готов в ZIP»; готовность — событие `done`).
- `done` (idx, elapsed) → `procState[idx]={st:'done', elapsed}`.
Нужен маппинг `name→idx` (или передавать `idx` в событиях `file_*` — проще: добавить `idx` в
`file_start/file_done` на бэке; см. замечание ниже).
### 7.2 Кнопка «Прервать»
- Новый элемент `<button id="cancelBtn" class="btn" style="display:none">⏹ Прервать</button>` в `footer-bar`.
- Показывать на Фазе 2, скрывать на Фазе 1 и после complete/cancelled.
- По клику — диалог-подтверждение (модал или `confirm`):
> «Остановить обработку? Будет сохранено: полностью обработанные файлы (сейчас готово X из N) и таблица замен. Остановить?»
- После подтверждения: `fetch('/api/cancel/' + currentSid, {method:'POST'})`.
- Обработчик SSE `cancelled`: статус «Сохранено X из N файлов + таблица замен», показать `dlBtns`.
### 7.3 Живой блок (liveBlock)
- В `llm` heartbeat — обновлять: `liveLlmTime` = elapsed, и добавить строку
«осталось ~X мин» (из `eta_sec`).
- Текущий файл — из `file_start`/`file_chunk`: «Файл 12/101: name.pdf — осталось ~1:20».
### 7.4 Прочее
- Скрыть кнопку «✕» (remove) на Фазе 2.
- Статическая оценка сразу после `upload_refs` (до SSE): «Загружено N файлов (X МБ). Обработка может занять ~M мин» — по объёму (коэффициент тот же K).
---
## 8. Замечания и ловушки (из код-ревью)
1. **`done`-фаза занята**: сейчас `progress_cb("done")` шлётся и для битых (в проходе 1), и для готовых
(в проходе 2). Не путать с готовностью файла в ZIP. Готовность = `done` в проходе 2.
Для UI-«готово» ориентироваться на события `done`, приходящие ПОСЛЕ всех `start` (т.е. в фазе замены).
2. **Частичные результаты не терять**: `results` — локальная переменная; НЕ полагаться на `finally`.
Возвращать `(zip, csv, meta)` из `obfuscate` (см. 4.3).
3. **`mapping.csv` не в ZIP**: частичный результат = частичный ZIP (только .md) + полный CSV отдельно
(через `/api/csv`). Полный CSV собирается из `self._mapping` (он полон на момент отмены в фазе замены).
4. **LLM-фаза сейчас без прогресса**: `scan_llm_ner` — один монолитный вызов. Именно поэтому добавляется
`file_progress`. Без него «текущий файл» в UI не определить.
5. **Имена в событиях `file_*`**: на бэке имена файлов — уже с `.md`/суффиксами после dedup; фронт
сопоставляет по `idx` (надёжнее, чем по имени). **Рекомендация**: передавать `idx` (0-based в `files`)
в события `file_start/file_done/file_chunk`.
6. **`obfuscate_files`-обёртка**: тоже меняет сигнатуру (`cancel_event`, `file_progress`) и возврат
`(zip, csv, meta)`. Обновить ОБА вызова: `process_stream` и legacy `process()`.
7. **Отключение SSE**: при закрытии вкладки `cancel` в генераторе уже ставится, но воркер его не видит.
Подключить тот же `cancel_event` (передать в `obfuscate_files`), чтобы закрытие вкладки реально
останавливало LLM (не жечь токены).
8. **`_LLM_CONCURRENCY = 1`** — последовательно. Не вводить параллельность (liberta/LLM не тянут).
---
## 9. Порядок реализации (коммитить по шагам)
1. **TTL-фикс** (`session.py` + `api_bp.py`): `touch/pause_ttl/resume_ttl`, `cancel` Event,
`request_cancel/get_cancel_event`, `POST /api/cancel/<sid>`.
2. **Трекинг** (`scanner.py`, `obfuscator.py`): `cancel_event` + `file_progress`, `CancelRequested`,
новый контракт возврата `(zip, csv, meta)`.
3. **Прерывание** (`obfuscator.py`, `api_bp.py`): частичный результат, событие `cancelled`.
4. **ETA** (`api_bp.py`, heartbeat `eta_sec`/`done_chars`/`total_chars`, `file_chunk` eta).
5. **UI** (`index.html`): 3-секционная таблица, кнопка «Прервать», диалог, live-обновления.
6. **Бамп версии** 0.0.62 → 0.0.63, py_compile + node --check, коммит.
---
## 10. Проверка
- `python3 -m py_compile` для всех изменённых `.py`.
- `node --check` для извлечённого `<script>` из `index.html`.
- Ручной тест: загрузить 100+ файлов → проверить 3 секции, ETA, прерывание → частичный ZIP+CSV.
- Регресс: короткий прогон без прерывания → `complete`, ZIP+CSV как раньше.
- Тест TTL: запустить долгий прогон (>30 мин) → скачать результат (не должен быть 404).
---
## 11. Что НЕ трогать
- Логику обфускации/LLM/NER (regex, chunking, dedup) — только прогресс/отмена поверх.
- `drhider/config.py`, `drhider/replacer.py`, `drhider/builder.py`, `drhider/extractor.py` — без изменений
(кроме возможного экспорта `CancelRequested` из `obfuscator.py`).
- Деплой/nginx/vmfiles — вне скоупа этой задачи.
@@ -0,0 +1,32 @@
# Запрос к Соннету — ERR_TIMED_OUT при открытии нового окна (2026-07-12)
## Симптом
DrHider на Managed Flask (Штурвал/pythonk8s). При открытии страницы в НОВОМ окне браузера — ERR_TIMED_OUT (20+ секунд). Старые вкладки работают нормально.
## Факты
- Изнутри кластера (kubectl exec → curl): HTTP 200 за 2.1 секунды
- Снаружи (браузер, curl с локальной машины): таймаут 10+ секунд
- v1 работало с gunicorn (CMD: `gunicorn --bind :5000 --worker-class gevent`)
- Сейчас: `app.run(host="0.0.0.0", port=5000)` — Flask dev server (Werkzeug)
- Страница: 12 KB HTML, 2 статических файла (logo.svg, favicon.svg — оба 2.8 KB)
- Ноль внешних зависимостей (никаких CDN, шрифтов, JS-библиотек)
- HTTP-заголовки: Cache-Control: no-cache, no-store, must-revalidate
- Штурвал запускает: cd site && python app.py
## Вопросы
1. Может ли Flask dev server (Werkzeug) быть причиной ERR_TIMED_OUT на новых соединениях, в то время как gunicorn работал нормально?
2. Если да — какой механизм? (keep-alive, thread pool, socket backlog?)
3. Если нет — что ещё может вызывать таймаут именно на НОВЫХ окнах при работающих старых?
4. Нужно ли вернуть gunicorn или есть другой способ заставить app.run() работать стабильно?
5. Может ли `Cache-Control: no-store` заставлять браузер делать лишние запросы при открытии нового окна?
## Контекст платформы
Штурвал — Managed Flask на Kubernetes:
- Запускает: `cd /var/www && pip install -r requirements.txt && cd site && python app.py`
- Readiness probe: `tcp-socket :5000`
- Нет Dockerfile, нет nginx перед приложением
- Ingress: внешний LoadBalancer → ClusterIP сервис → под
@@ -0,0 +1,83 @@
# Запрос к Соннету — анализ результатов DrHider v3 (2026-07-12)
## Контекст
DrHider v3 — обфускация документов через LLM + Markdown. Результаты уже есть.
Покажи Соннету реальный mapping.csv и фрагмент обфусцированного .md.
## Промпт для Соннета
Вот текущий LLM-промпт в scanner.py:
```
You are a PII detector. Find ALL sensitive or private information in the text below.
Return a JSON array. Each item must have:
- "type": short snake_case label describing the entity
(e.g. person_name, phone, email, address, inn, company, passport,
contract_number, employee_id, bank_account — WHATEVER you see)
- "value": the EXACT string from the text (copy verbatim)
Rules:
- Copy value VERBATIM — same spaces, punctuation, case as in text
- If same value appears multiple times — include only once
- Invent the type label yourself based on what you see
- Return ONLY the JSON array, no other text, no markdown wrapping
```
Результат mapping.csv (первые 10 строк):
```csv
тип_данных,оригинал,замена
text,"""****XXX****001****""","""****IPD****084****"""
text,"""НУБЕС""","""ФИОЩЦ"""
phone,+7 (495) 789-41-35,+7 (343) 986-75-93
email,info@nubes.ru,bxkuno@company.local
company,"ЗАО ""****XXX****001****""",ООО «Спектр»
company,"ЗАО ""XXX001""",ЗАО «Меридиан»
inn_ul,ИНН 9706005293,ИНН 9325731296
text,Исполнителем Заказчику Сторонами,Морозов М.П.
kpp,КПП 772401001,КПП 051543039
```
Фрагмент обфусцированного .md:
```markdown
**ООО «Спектр»** именуемое в дальнейшем "Заказчик", в лице Генерального директора ФИО,
действующего на основании Устава, и **ООО **"**НУБЕС**"**, именуемое в дальнейшем
"Исполнитель", в лице Генерального директора Степанов Н.М.
```
## Проблемы
1. **Контекстная роль Исполнителя** — сервис должен быть УНИВЕРСАЛЬНЫМ, без хардкода конкретных названий. LLM должна из контекста понимать: «вот эта компания — Исполнитель (сторона договора, оказывающая услуги), её НЕ заменяем». Как научить LLM этому без указания конкретных имён?
2. **Тип "text" для многих сущностей** — LLM не всегда правильно определяет тип (например, "Исполнителем Заказчику Сторонами" — это не ПДн, а просто текст договора)
3. **Regex находит структурированное (ИНН, телефон), LLM — остальное** — есть дублирование
## Вопросы
1. Как научить LLM определять Исполнителя из контекста договора (без хардкода имён) и НЕ заменять его данные?
2. Как заставить LLM точнее определять тип сущности (не "text" для всего подряд)?
3. Нужно ли добавить в промпт примеры того что НЕ является ПДн (юридические термины, роли сторон)?
4. Стоит ли передавать LLM уже найденное regex'ом чтобы избежать дублирования?
5. Правильно ли что промпт на английском, а документы на русском?
## Текущий промпт (полностью)
```
You are a PII (Personally Identifiable Information) detector.
Find ALL sensitive or private information in the text below.
Return a JSON array. Each item must have:
- "type": short snake_case label describing the entity
(e.g. person_name, phone, email, address, inn, company, passport,
contract_number, employee_id, bank_account — WHATEVER you see)
- "value": the EXACT string from the text (copy verbatim)
Rules:
- Copy value VERBATIM — same spaces, punctuation, case as in text
- If same value appears multiple times — include only once
- Invent the type label yourself based on what you see
- Return ONLY the JSON array, no other text, no markdown wrapping
Text:
{combined[:8000]}
```
@@ -0,0 +1,47 @@
# Запрос к Соннету — нумерация вместо генерации фейков (2026-07-12)
## Контекст
DrHider обфусцирует документы: ищет ПДн → заменяет на фиктивные значения.
Сейчас используется генерация "похожих" фейков (Иванов И.И., ООО «Ромашка», +7(495)...).
## Проблема
1. **Адреса → абракадабра.** Ул. Стекольная → иг. 8ц Лжчюсвхшбр. Потому что fallback-генератор меняет кириллицу на случайную кириллицу.
2. **Сложность.** 11 генераторов под каждый тип данных. Каждый надо поддерживать.
3. **Ложное ощущение реальности.** Фейковые ИНН с правильной контрольной суммой выглядят как настоящие.
## Идея
Заменить всю генерацию на **нумерацию по типам:**
| Оригинал | Замена |
|---|---|
| Иванов И.И. | `Лицо_0042` |
| ООО «НУБЕС» | `Компания_0003` |
| ул. Стекольная, д.7 | `Адрес_0015` |
| +7 (495) 789-41-35 | `Телефон_0007` |
| info@nubes.ru | `Email_0004` |
| ИНН 9706005293 | `ИНН_0002` |
| Р/с 40702... | `Счёт_0001` |
| договор-XXX001.docx | `Файл_0006` |
## Вопросы
1. **Плюсы/минусы** нумерации vs генерации фейков для задачи обфускации документов?
2. **Как нумеровать?** Глобальный счётчик на каждый тип? Или в рамках одного документа?
3. **Стоит ли сохранять формат?** Например `Телефон_0007` vs `+7 (000) 000-00-07`?
4. **mapping.csv** — при нумерации он становится главным документом аудита. Это ок?
5. **UUID вместо номеров?** `Лицо_a3f4b2` — лучше или хуже последовательных номеров?
## Текущий mapping.csv (для контекста)
```csv
тип_данных,оригинал,замена
phone,+7 (495) 789-41-35,+7 (495) 906-87-80
email,info@nubes.ru,ypiorej@company.local
company,"ЗАО ""XXX001""",ООО «Альянс»
inn_ul,ИНН 9706005293,ИНН 1846691249
text,Исполнителем Заказчику Сторонами,Кузнецов О.В.
text,"115404, г. Москва, ул. 1-я Стекольная, д.7с2","084810, б. Ввжзгд, гф. 6-в Фтпжыобнзщ, я.8с1"
```
@@ -0,0 +1,54 @@
# Запрос к Соннету — нумерация vs фейки для downstream LLM (2026-07-12)
## Контекст
Я делаю сервис **DrHider** — обфускация документов. Находит ПДн → заменяет.
После обфускации документы идут на дальнейшую LLM-обработку — **сверка спецификаций**
(другой сервис contracts, извлекает типы/номера/даты/контрагентов через LLM).
Сейчас генерация фейков: `Иванов И.И.``Петров А.С.`, `ООО «Ромашка»``ООО «Спектр»`.
**Проблема:** для неизвестных типов (адреса, нестандартные ID) — character-class замена
даёт абракадабру: `ул. Стекольная``иг. 8ц Лжчюсвхшбр`. Downstream LLM не может это обработать.
## Варианты замены
### А. Чистая нумерация
`Иванов И.И.``Лицо_0042`, `ООО «Ромашка»``Компания_0003`, `ул. Ленина``Адрес_0015`
Плюс: никакой абракадабры. Минус: LLM теряет контекст («Лицо_0042 заключил договор» — не понимает семантику).
### Б. Фейки (сейчас)
`Иванов И.И.``Петров А.С.`, `ООО «Ромашка»``ООО «Спектр»`
Плюс: LLM понимает роли. Минус: сложно (11 генераторов), ложное ощущение реальности,
для неизвестных типов — абракадабра.
### В. Гибрид: шаблон + номер
`Иванов И.И.``Иванов_5677`, `ООО «Ромашка»``ООО_Технология_0034`,
`ул. Стекольная, д.7``ул_Ленина_0015`, `+7 (495) 789-41-35``+7_495_000_0042`,
`info@nubes.ru``email_0007@fake`
Плюс: LLM видит что это человек/компания/адрес, номер гарантирует уникальность и обфускацию.
Минус: всё равно нужны шаблоны для каждого типа.
## Вопросы
1. Какой вариант лучше для downstream LLM-обработки (извлечение контрагентов, дат, номеров)?
2. Вариант В — достаточно ли LLM поймёт что `Иванов_5677` это персональные данные?
3. Для неизвестных типов — `[Данные_NNNN]` как fallback, ок?
4. Глобальная нумерация (сквозная через все документы пакета) vs локальная (в рамках одного файла)?
5. Стоит ли сохранять частичную структуру (например телефон `+7_495_000_0042` сохраняет формат номера)?
## Downstream задача (для контекста)
LLM в сервисе contracts получает текст документа и должна извлечь:
- Контрагент («с кем договор»)
- Номер и дату договора
- Список услуг с ценами (таблица)
Промпт для contracts LLM (фрагмент):
```
Извлеки из текста: тип документа, номер, дату, контрагента.
Верни JSON: {"type":"договор","number":"...","date":"...","counterparty":"..."}
```
+76
View File
@@ -0,0 +1,76 @@
# Запрос к Соннету — универсальная архитектура DrHider
## Контекст
Проект DrHider — обфускация документов (замена персональных данных на фиктивные). Сейчас:
- Python/Flask, без БД, Managed Flask на платформе Штурвал
- Двухпроходная архитектура: сбор сущностей → замена
- Проход 1: regex-паттерны (телефон, email, ИНН, ОГРН, КПП, БИК, счета, паспорт) + LLM NER (ФИО, компании, адреса, паспорта)
- Проход 2: замена по словарю mapping
## Проблема
Архитектура **в корне неверна** — жёстко привязана к фиксированному списку типов сущностей:
```python
# config.py — жёстко зашитые паттерны
ENTITY_PATTERNS = {
"phone": r'...',
"email": r'...',
"inn_fl": r'ИНН\s*\d{12}',
...
}
```
```python
# scanner.py — жёстко зашитый промпт
prompt = (
"Найди ВСЕ следующие сущности:\n"
"1. ФИО\n2. Компании\n3. Адреса\n4. Паспортные данные\n"
)
```
Если новый документ содержит СНИЛС, водительское удостоверение, номер договора, ИНН без префикса «ИНН» — система их НЕ обнаружит. Надо править config, generators, scanner, маппинги.
## Что нужно
**Универсальная архитектура**, где система САМА определяет что скрывать в любом документе:
1. Не привязана к списку типов
2. Не требует добавления паттернов под каждый новый вид данных
3. LLM сама решает что является персональными/конфиденциальными данными
4. Генерация фиктивных значений — тоже универсальная (не 11 отдельных функций)
## Текущая структура (полная)
```
drhider/
├── config.py # ENTITY_PATTERNS (10 regex), словари имён/городов
├── checksum.py # Контрольные суммы ИНН/ОГРН
├── random_utils.py # random_digits, random_letters
├── generators/ # 11 файлов: phone, email, inn, ogrn, kpp, bik, accounts, passport, company, person, address
├── llm_client.py # HTTP-клиент к LLM API
├── extractor.py # Извлечение текста + expand_zips + convert_pdfs_to_docx
├── scanner.py # scan_regex (regex) + scan_llm_ner (LLM NER с жёстким промптом)
├── replacer.py # apply_replacements, replace_in_docx, replace_in_text
├── builder.py # build_zip, build_mapping_csv
├── obfuscator.py # TwoPassObfuscator — оркестратор
├── site/app.py # Flask (3 blueprint'а)
└── site/templates/ # HTML-интерфейс
```
## Вопросы к Соннету
1. Как перестроить архитектуру чтобы обнаружение было универсальным (LLM сама решает что скрывать)?
2. Нужен ли regex вообще или достаточно одного LLM с правильным промптом?
3. Как сделать генерацию фиктивных значений универсальной? (Не 11 функций под каждый тип, а что-то общее)
4. Двухпроходная схема (сбор→замена) — сохранять или перейти на однопроходную (LLM сразу возвращает обфусцированный текст)?
5. Как должен выглядеть промпт чтобы LLM возвращала структурированный результат (что найдено + на что заменить)?
6. Стоит ли сохранять DOCX-форматирование (сейчас runs склеиваются-разделяются) или проще отдать LLM plain text?
## Ограничения
- Документы до 200 MB
- Форматы: .docx, .pdf, .txt, .zip
- LLM: OpenAI-совместимое API (aillm.ru, модель gpt-oss-120b, 8000 токенов)
- Без БД, всё в памяти
- Платформа: Managed Flask на Kubernetes
@@ -0,0 +1,19 @@
# Ответ Соннета — ERR_TIMED_OUT (2026-07-12)
## Причина
Werkzeug dev server: **один поток на TCP-соединение**. Браузер держит 6 keep-alive соединений на вкладку. 6 потоков заняты idle-соединениями → backlog (5) переполнен → новое окно получает SYN-дроп → таймаут.
gunicorn+gevent: async I/O, idle-соединения не занимают ресурсы.
## Решение
Три варианта:
| Вариант | Что | Изменения |
|---|---|---|
| A (waitress) | Pure Python WSGI | `requirements.txt` + 2 строки в `app.py` |
| B (gevent) | Async, как gunicorn | `requirements.txt` + 2 строки в `app.py` |
| C (gunicorn) | Точно как v1 | `requirements.txt` + смена CMD в Штурвале |
Соннет рекомендует: **A** если Штурвал не меняет CMD, **C** если меняет.
@@ -0,0 +1,47 @@
# Ответ Соннета v2 — улучшение LLM-промпта (2026-07-12)
## Q1. Как научить LLM определять Исполнителя из контекста?
**Двухфазная инструкция в промпте:**
- STEP 1: определи кто «Исполнитель» — его данные НЕ трогать
- STEP 2: найди всё остальное ПДн
Убирает хардкод `НУБЕС` из кода. Работает на любом договоре.
## Q2. Точная классификация типов
Вместо `"Invent the type label yourself"`**фиксированный список:**
`person_name, company, phone, email, address, inn, kpp, ogrn, bik, passport, contract_number, bank_account, other_id`
Если сомневаешься — НЕ включай.
## Q3. Примеры что НЕ является ПДн
Обязательно добавить блок:
```
NOT PII:
- Юридические роли: Исполнитель, Заказчик, Стороны
- Термины договора: Услуги, Приложение, Договор, НДС
- Должности: Генеральный директор
- Сама компания-Исполнитель
```
## Q4. Передавать LLM уже найденное regex'ом
`scan_llm_ner` получает `mapping.keys()` → промпт: "Already captured (skip these): ..."
Исключает дублирование, экономит токены.
## Q5. Язык промпта
Гибрид: структурные инструкции на английском, NOT PII и роли — на русском.
---
## План реализации (scanner.py)
1. Убрать хардкод `НУБЕС` из `scan_regex`
2. `scan_llm_ner` — передавать `mapping.keys()` как "Already captured"
3. Новый промпт:
- Фиксированный список типов
- Блок NOT PII (русский)
- Двухфазная инструкция (STEP 1: Исполнитель, STEP 2: остальное)
@@ -0,0 +1,46 @@
# Ответ Соннета v3 — нумерация вместо генерации фейков (2026-07-12)
## Q1. Нумерация vs фейки
- ✅ Адреса без абракадабры, код проще, аудит прозрачнее
- ❌ Документ нечитаем (`Лицо_0042 заключил договор с Компания_0003`)
- Вывод: для аудита/хранения — отлично, для показа людям — фейки лучше
## Q2. Глобальный счётчик на весь пакет
Одна сущность в разных документах → один номер. `TwoPassObfuscator` хранит counters.
## Q3. Сохранять формат?
Рекомендация: `Тип_NNNN` — проще всего, аудит максимально прозрачен.
## Q4. mapping.csv — главный документ аудита
Да, это улучшение. mapping.csv — секретный ключ деобфускации.
## Q5. Номера vs UUID
Номера (`Лицо_0042`) — лучше для читаемости и аудита.
---
## План изменений
### Удалить
- `drhider/generators/` — все 12 файлов
- `drhider/checksum.py` — больше не нужен (ИНН без контрольных сумм)
- `drhider/random_utils.py` — больше не нужен
### Изменить
- `scanner.py`: добавить `TYPE_PREFIXES` + `_next_token()`, убрать импорты генераторов
- `obfuscator.py`: `TwoPassObfuscator` хранит `counters` dict
- `config.py`: убрать словари имён/городов (RU_SURNAMES, ...)
### Новая логика замены
```python
TYPE_PREFIXES = {
"person_name": "Лицо", "company": "Компания", "phone": "Телефон",
"email": "Email", "address": "Адрес", "inn": "ИНН", ...
}
_FALLBACK = "Данные"
def _next_token(entity_type, counters):
prefix = TYPE_PREFIXES.get(entity_type, _FALLBACK)
counters[prefix] = counters.get(prefix, 0) + 1
return f"{prefix}_{counters[prefix]:04d}"
```
@@ -0,0 +1,19 @@
# Ответ Соннета v4 — гибридный формат замен (2026-07-12)
## Итог: Вариант В (шаблон + номер)
| Тип | Формат замены | Пример |
|---|---|---|
| ФИО | `Фамилия_NNNN` | `Иванов_5677` |
| Компания | `ООО_Слово_NNNN` | `ООО_Технология_0034` |
| Адрес | `ул_Шаблон_NNNN` | `ул_Ленина_0015` |
| Телефон | `+7_код_000_NNNN` | `+7_495_000_0042` |
| Email | `email_NNNN@fake` | `email_0007@fake` |
| ИНН | `ИНН_NNNNNNNNNN` | `ИНН_0000000042` |
| Неизвестный тип | `[Тип_NNNN]` | `[Адрес_0023]` |
## Ключевые принципы
1. **Читаемо человеком** — не ужиматься, пусть будет естественно
2. **Однозначно для LLM** — префикс указывает тип сущности
3. **Глобальная нумерация** — одна сущность = один номер во всех файлах пакета
4. **Цены/суммы** — не обфусцировать (нужны для downstream сверки)
@@ -0,0 +1,78 @@
# Ответ Соннета — универсальная архитектура DrHider (2026-07-12)
Кратко: 6 вопросов → 6 ответов → карта изменений.
---
## Q1. Как сделать обнаружение универсальным?
Промпт должен звучать не «найди вот эти типы», а «найди ВСЁ, что выглядит как приватная информация, и сам назови тип».
Затрагивает: `scanner.py` (промпт), `builder.py` (тип для mapping.csv).
---
## Q2. Regex или LLM?
**Гибрид — правильный выбор:**
- **Regex = pre-pass** для структурированных данных (телефон, email, ИНН, БИК). Экономит токены LLM.
- **LLM = post-pass** для неструктурированного (имена, адреса, нестандартные ID).
- Дублирования не страшны — mapping dict сам отсеет.
---
## Q3. Генерация фиктивных значений — универсальная?
Два уровня:
1. **Известные типы** — специализированные генераторы (оставить как есть).
2. **Неизвестные типы** — fallback-генератор: character-class preserving замена (цифры→цифры, буквы→буквы той же длины).
Дополнительно: в промпт добавить `"category": "person|org|contact|id|financial|other"` — 6 категорий вместо 10+ типов.
---
## Q4. Двухпроходная или однопроходная?
**Двухпроходная — обязательно.** Причина: «Иванов» в 5 документах должен заменяться одинаково. Однопроход не может этого гарантировать.
Текущий `TwoPassObfuscator` — правильный, не трогать.
---
## Q5. Новый промпт
```
You are a PII detector. Find ALL sensitive or private information.
Return JSON array: [{"type": "short_label", "value": "exact_string"}]
Rules:
- Copy "value" VERBATIM from text
- "type" is snake_case label you invent
- Same value → include once
- Return ONLY JSON
```
Ключевое: нет ограничения на типы, value verbatim.
---
## Q6. DOCX или Markdown?
- **Внутренняя обработка:** Markdown проще парсить, LLM понимает лучше.
- **На выходе:** опция `output_format: "docx" | "md"`. По умолчанию `"docx"`.
---
## Итоговая карта изменений
```
scanner.py — новый промпт (убрать список типов, LLM сама называет типы)
generators/ — добавить fallback-генератор для неизвестных типов
obfuscator.py — вызывать fallback если тип неизвестен
api_bp.py — принять параметр output_format
replacer.py — добавить replace_to_markdown()
builder.py — упаковывать .md если output_format=md
```
**Не трогать:** двухпроходная схема, checksum-генераторы, ZIP-безопасность.
@@ -0,0 +1,39 @@
# Запрос к Соннету — архитектура пофайловой обработки с одним ZIP (2026-07-13)
## Задача
DrHider (Flask + waitress). Ограничения:
- Фронтенд должен слать файлы **по одному** (сеть не пропускает большие multipart)
- Бэкенд должен обрабатывать **по одному** (без параллельных LLM-запросов)
- Результат: **один ZIP** со всеми обфусцированными .md + **один общий** mapping.csv
- **Никакой обработки данных в JS** — фронтенд только кнопки и скачивание
## Варианты
### А. Upload → Process
1. `POST /api/upload` — один файл → `{session:"abc"}`
2. Фронтенд циклом шлёт все, получает session
3. `POST /api/process/abc` — бэкенд обрабатывает всё → ZIP
4. Фронтенд скачивает ZIP
### Б. Один запрос, все файлы
1. Фронтенд: один POST, все файлы в FormData
2. Бэкенд: получил все → обработал по одному → ZIP
3. Waitress держит долгое соединение, таймаут 600с
### В. SSE с прогрессом
1. `POST /api/start``{session:"abc"}`
2. `POST /api/upload/abc` — по одному файлу
3. `GET /api/process/abc` — SSE стримит прогресс каждого файла и в конце ZIP
## Вопросы
1. Какой вариант правильный для Flask + waitress?
2. Вариант Б — пройдёт ли большой multipart через ingress (6 файлов × 30-60 KB)?
3. Если А — как чистить сессии? (пользователь закрыл вкладку, файлы остались в памяти)
4. Нужен ли SSE или достаточно простого XHR с таймером?
5. mapping.csv — генерировать на лету (по мере обработки) или после всех файлов?
## Сейчас работает (но сломано)
Сейчас: фронтенд шлёт по одному файлу, каждый получает отдельный ZIP. Пользователь хочет один ZIP в конце. JS обрабатывал ZIP'ы (распаковывал/перепаковывал) — это убрали, всё должно быть на бэкенде.
@@ -0,0 +1,114 @@
# Запрос к Sonnet: анализ обфускации
**Дата:** 2026-07-13
---
**Задача:** Найти причину расхождения между ожидаемыми кодами замен и реальными заменами.
## Ожидаемое поведение
`_next_token()` в `drhider/scanner.py` генерирует коды замен формата:
- `Технология_0001`, `Прогресс_0002` (для компаний)
- `+7_000_000_0001` (для телефонов)
- `email_0001` (для email)
- `ИНН_0001` (для ИНН)
- `Иванов_0001` (для ФИО)
## Реальный результат — `TMP/mapping.csv`
```
тип_данных,оригинал,замена
phone,+7 (495) 789-41-35,+7 (495) 906-87-80 ← НЕ код замены, реальный номер
email,info@nubes.ru,ypiorej@company.local ← НЕ код замены, реальный email
company,ЗАО ****XXX****001****,АО «Прогресс» ← НЕ код замены, реальное название
company,ЗАО XXX001,ООО «Альянс» ← НЕ код замены, реальное название
inn_ul,ИНН 9706005293,ИНН 1846691249 ← НЕ код замены, реальный ИНН
kpp,КПП 772401001,КПП 350443068 ← НЕ код замены, реальный КПП
ks,Кор/сч 30101810400000000225,Кор/сч 30101201490272090211 ← НЕ код замены
ogrn,ОГРН 1207700098759,ОГРН 1048672318430 ← НЕ код замены
company,ООО "НУБЕС",ООО «Спектр» ← НЕ код замены
text,Исполнителем Заказчику Сторонами,Кузнецов О.В.
text,"Обязанности Заказчика\nСвоевременно",Степанов А.В.
text,"Обязанности Исполнителя\nОказывать",Петров П.Е.
text,"Ответственность Заказчика\nЗаказчик",Новиков А.М.
text,"Ответственность Исполнителя\nИсполнитель",Попов О.М.
company,ПАО Сбербанк,АО «Формат»
```
**Ни одного кода замены формата `XXX_0000` в колонке «замена».**
## Реальный результат — `TMP/договор-XXX001-03700_obfuscated.md` (первые строки)
```markdown
**АО «Прогресс»** ... и **ООО **"**НУБЕС**" ... Новиков И.В. ...
```
- `Новиков И.В.` — НЕ заменён (должен быть `Иванов_0001`)
- `ООО "НУБЕС"` — НЕ заменён в тексте (есть в mapping но замена не применена?)
- `г. Москва, 1-я Стекольная 7с5` — адрес не заменён
## Файлы для анализа
**Обязательно прочитать:**
1. `drhider/scanner.py``_next_token()`, `scan_regex()`, `scan_llm_ner()`, TYPE_POOLS, промпт LLM (строки 109-151)
2. `drhider/obfuscator.py` — порядок вызова regex → LLM, как объединяется mapping (строки 63-95)
3. `drhider/config.py` — ENTITY_PATTERNS, COMPANY_PATTERN, PERSON_PATTERN
4. `drhider/llm_client.py``LLMClient.complete()`
**Прочитать для контекста:**
5. `TMP/mapping.csv` — полный результат
6. `TMP/договор-XXX001-03700_obfuscated.md` — обфусцированный файл
7. `TMP/примеры_договоров_для_ИИ/договор-XXX001-03700.docx` — оригинал (для сверки)
**Не лезть:** `builder.py`, `extractor.py`, `replacer.py`, `session.py`, `templates/`, docs/, History/.
## Ключевые точки для проверки
### 1. `scanner.py` — `_next_token()` (стр. 63-82)
```python
def _next_token(entity_type, counters):
pool, prefix_key = TYPE_POOLS.get(entity_type, _FALLBACK_POOL)
template = random.choice(pool)
key = f"{prefix_key}_{template}"
counters[key] = counters.get(key, 0) + 1
return f"{template}_{counters[key]:04d}"
```
Возвращает `Технология_0001`, `+7_000_000_0001`. **Никак не может вернуть** `АО «Прогресс»` или `+7 (495) 906-87-80`.
### 2. `scanner.py` — `scan_regex()` (стр. 89-108)
```python
mapping[original] = _next_token(entity_type, counters)
```
Всегда вызывает `_next_token`. Результат должен быть кодом замены.
### 3. `scanner.py` — `scan_llm_ner()` (стр. 109-179)
```python
mapping[val] = _next_token(ent_type, counters)
```
Тоже вызывает `_next_token`. Результат должен быть кодом замены.
### 4. `obfuscator.py` — порядок вызова (стр. 86-92)
```python
scanner.scan_regex(text, self._mapping, self._counters) # сначала
scanner.scan_llm_ner(all_texts, self._mapping, ...) # потом
```
### 5. `config.py` — regex-паттерны:
- `phone`: `(?:\+7|8)[\s\-]?\(?\d{3}\)?...` — должно матчить `+7 (495) 789-41-35`
- `email`: `[a-zA-Z0-9._%+-]+@...` — должно матчить `info@nubes.ru`
- `inn_ul`: `ИНН\s*\d{10}` — должно матчить `ИНН 9706005293`
- `COMPANY_PATTERN`: `(?:ООО|ЗАО|...)\s+(?:«[^»]+»|"[^"]+"|...)` — должно матчить `ООО "НУБЕС"` и `ЗАО "XXX001"`
- `PERSON_PATTERN`: `[А-Я][а-я]+\s+[А-Я]\.[А-Я]\.` — должно матчить `Новиков И.В.`
## Главный вопрос
Каждый путь (regex и LLM) вызывает `_next_token()`. `_next_token()` физически не может выдать `АО «Прогресс»` или `+7 (495) 906-87-80`. Откуда в `mapping.csv` берутся реалистичные замены? Кто и где их генерирует В ОБХОД `_next_token()`?
@@ -0,0 +1,17 @@
# Ответ Соннета — архитектура upload+process (2026-07-13)
## Рекомендация: Вариант А + блокирующий /process
```
POST /api/upload → {session_id} (фронт шлёт по одному, N раз)
POST /api/process/{sid} → {status:"done"} (блокирует до конца обработки)
GET /api/download/{sid} → ZIP
```
Без polling — `/process` просто блокирует XHR до готовности. Waitress держит.
## Детали
- Mapping.csv — на лету, аккумулируется после каждого файла
- Сессии — dict в памяти, TTL 30 минут через threading.Timer
- Фронт: цикл upload → process → download. Три простых XHR.
- Никакого JS-кода обработки данных.
@@ -0,0 +1,70 @@
# Sonnet: анализ обфускации — TMP-файлы vs текущий код
**Дата:** 2026-07-13
---
## Главный вывод
**TMP-файлы созданы СТАРОЙ версией кода.** Они не были сгенерированы текущим `_next_token()`-кодом. Это не баг в текущем коде — это артефакт предыдущей реализации.
---
## Доказательства
### 1. Имена файлов — старый формат
Текущий `obfuscator.py` генерирует имена вида `договор-НУБЕС001-03700.md` (меняет расширение). TMP-файлы: `договор-XXX001-03700_obfuscated.md` — суффикс `_obfuscated` и `XXX`-маскировка в имени. Текущий код так не делает.
### 2. Тип `text` — fallback из builder.py, не из сканера
`build_mapping_csv()` определяет тип постфактум по паттернам. Если ни один не совпал → `"text"`. Типа `text` нет в `TYPE_POOLS` scanner.py. Значит строки вида:
```
text,Исполнителем Заказчику Сторонами,Кузнецов О.В.
```
попали из старого кода, который не фильтровал роли (Исполнитель, Заказчик) — текущий LLM-промпт их явно исключает.
### 3. Одна сущность → разные замены
```
email,info@nubes.ru,ypiorej@company.local ← файл 1
email,info@nubes.ru,qifdckke@org.example.ru ← файл 2
```
Текущий `TwoPassObfuscator` гарантирует глобальную согласованность. Старый код обрабатывал файлы по одному — отсюда расхождения.
### 4. Реалистичные замены невозможны из `_next_token()`
```python
TYPE_POOLS = {
"phone": (["+7_000_000"], "phone"), # → "+7_000_000_0001"
"email": (["email"], "email"), # → "email_0001"
}
```
`+7 (495) 906-87-80` или `ypiorej@company.local` физически не могут выйти из этих пулов.
---
## Ответ на главный вопрос
> Кто и где генерирует замены в обход `_next_token()`?
**Никто в текущем коде.** Это артефакты старого кода (версии до v3). Текущий `_next_token()` написан позже.
---
## Что реально нужно проверить
| Вопрос | Как проверить |
|--------|---------------|
| Работает ли текущий код? | Прогнать оригиналы через v0.0.12, посмотреть mapping.csv |
| Regex ловит все сущности? | Проверить `config.py` паттерны на примерах из TMP |
| Адрес `115404, г. Москва...` — почему не заменён? | `ENTITY_PATTERNS` не содержит тип `address` |
| `ООО "НУБЕС"` в тексте не заменён? | Возможно кавычки — regex матчит `«»` но не `""` |
---
## Итог
**Ложное противоречие** — сравнивались TMP-файлы (старый код) с текущим scanner.py. Нужно прогнать текущий код на тех же документах и посмотреть результат.
@@ -0,0 +1,37 @@
# Sonnet: анализ багов обфускации — fix
**Дата:** 2026-07-13
---
## Баг 1: `ООО "НУБЕС"` не заменено
**Причина:** DOCX → Markdown: `**ООО **"**НУБЕС**"`. Кавычка в отдельном run → `**` разрывает строку. Replacer ищет точное совпадение `ООО "НУБЕС"` — не находит.
**Фикс (replacer.py):** добавить `_md_tolerant_pattern()` — паттерн, допускающий `**` между частями строки. При промахе точного совпадения — фоллбэк с этим паттерном.
```python
def _md_tolerant_pattern(original: str) -> str:
MD = r'(?:\*{1,2})?'
parts = re.findall(r'[А-ЯЁа-яёA-Za-z0-9]+|[^А-ЯЁа-яёA-Za-z0-9]', original)
return MD + MD.join(re.escape(p) for p in parts) + MD
```
## Баг 2: `Новиков И.В.` → `ФИО`
**Причина:** В оригинале буквально `ФИО` — незаполненный шаблон. Не баг паттерна.
**Превентивный фикс (config.py):** `\s+``[\s\xa0]+` в PERSON_PATTERN — на случай неразрывных пробелов в DOCX.
```python
PERSON_PATTERN = re.compile(
r'\b[А-Я][а-я]+[\s\xa0]+[А-Я]\.[А-Я]\.'
r'|\b[А-Я][а-я]+[\s\xa0]+[А-Я][а-я]+[\s\xa0]+[А-Я][а-я]+',
)
```
## Моё мнение
**Баг 1 — делать.** Логично, минимально, решает проблему Markdown-жирности.
**Баг 2 — `\xa0` делать** (превентивно). Сам `ФИО` — не баг, документ содержит незаполненный шаблон. Можно добавить `ФИО` в список игнорируемых (исключить из mapping), но не обязательно.
+105
View File
@@ -0,0 +1,105 @@
# 2026-08-20 — Вопрос-ответ по LLM-чанкингу (Q&A)
Контекст: сервис обфускации, двухступенчатый детект ПДн (regex + LLM). Пользователь
хочет, чтобы LLM обрабатывал ВСЕ данные, а не урезку 8000 символов.
Обсуждение с Sonnet: основной вопрос + 6 уточнений.
Предыдущие файлы:
- History/2026-08-20-sonnet-query-llm-pattern.md — рекомендуемая схема
- History/2026-08-20-sonnet-query-llm-followup.md — уточнения перед внедрением
---
## Исходная проблема
`scan_llm_ner` (drhider/scanner.py) обрезал:
- каждый файл до первых 3000 символов (`t[:3000]`);
- всё вместе до 8000 символов (`combined[:8000]`);
- один вызов LLM на весь набор.
Итог: при 299 файлах LLM «видел» только ~8000 символов (~0.10.3% данных).
Пользователь: «всё затевалось чтобы ЛЛМ ВСЁ обрабатывал». → убрать урезку.
## Рекомендуемая схема (Sonnet, основной ответ)
- Отказаться от глобального батчинга → «один файл = один или несколько вызовов LLM», параллельно.
- Чанкинг per-file: чанк 6000 символов, overlap 400–500 символов.
- Дедупликация по файлу + верификация (найденная строка реально в тексте → отсекает галлюцинации).
- Батчинг мелких файлов — опционально, второй шаг.
## Уточнения (У.1–У.6) и ответы Sonnet
### У.1 — Алгоритм разбивки на чанки
Приоритет границ: `\n\n``\n``. ``? ``! ``; ``, `` ` (пробел — крайний).
Ищем ближайшую границу с конца в диапазоне `[size//2 .. size]`. Жёсткий разрез — только если нет ни одного пробела.
Overlap: отступить назад на overlap, найти начало слова.
Псевдокод:
```python
BOUNDARIES = ['\n\n', '\n', '. ', '? ', '! ', '; ', ', ', ' ']
def split_into_chunks(text, size=6000, overlap=500):
chunks=[]; start=0
while start < len(text):
end=min(start+size, len(text))
if end==len(text):
chunks.append(text[start:]); break
cut=None
for b in BOUNDARIES:
pos=text.rfind(b, start+size//2, end)
if pos!=-1:
cut=pos+len(b); break
if cut is None: cut=end
chunks.append(text[start:cut])
ov=max(start, cut-overlap)
space=text.find(' ', ov)
start=(space+1) if (space!=-1 and space<cut) else ov
return chunks
```
### У.2 — Нормализация для дедупа
```python
def normalize_entity(s):
s=s.strip()
s=re.sub(r'\s+',' ',s)
s=s.lower()
s=re.sub(r'[«»“”‘’"\' ]','"',s) # кавычки → "
s=re.sub(r'[—–−-]','-',s) # тире → -
s=s.replace('\u00ad','') # мягкий перенос
s=s.replace('ё','е') # Ё→Е (OCR/PDF)
return s
```
Дедуп: `seen=set()` — добавлять, только если `normalize_entity(v) not in seen`.
### У.3 — Источник истины для верификации
Полный текст файла ДО обфускации (не чанк).
```python
def verify(entity, full_text):
if entity in full_text: return True
return normalize_entity(entity) in normalize_entity(full_text)
```
Если сущность из двух чанков с разным форматированием — нормализованный ключ одинаков → дедуп оставит один; хранить длиннее (или первый).
### У.4 — Батчинг мелких в первой итерации
НЕ нужен. 270 мелких: 1 файл=1 вызов ≈ 270 вызовов ≈ 101с при 4 потоках; батч по 5 → 54 вызова ≈ 20с. Разница ~80с несущественна. Батчинг — вторая итерация.
### У.5 — Промпт при чанкинге
Блок «Already captured by regex» ОСТАВИТЬ в каждом вызове (снижает дублирование с regex).
Блок правил — как есть в первую итерацию (если API с system/user — правила в system, текст чанка в user; иначе как сейчас).
Первый шаг: промпт не менять, только текстовый блок = чанк.
### У.6 — Общее время и rate-limit
Расчёт: 270 мелких (1 вызов) + 30 крупных (≈4 чанка) = 390 вызовов.
- 4 потока × 2с: 390/4×2 ≈ 195с ≈ 3.5 мин.
- 8 потоков: ~1.5–2 мин.
Rate-limit свой LLM: внешнего нет, ограничение GPU. Начать с 4, поднять до 8, если latency не растёт (>3× от базового — снижать).
---
## Итоговое решение (для внедрения, ждёт «делай»)
1. `scanner.py scan_llm_ner`: убрать `[:3000]`/`[:8000]`;
по каждому файлу из `all_texts` разбить на чанки (6000/500, split_into_chunks);
параллельные вызовы (ThreadPool, concurrency 4, крупные вперёд);
для каждого чанка — полный промпт + «regex-найденные»;
собрать сущности, normalize-дедуп, verify против полного текста файла, в mapping.
2. Добавить хелперы split_into_chunks / normalize_entity / verify в scanner.py.
3. Не дублировать regex-найденное; считать токены/время LLM корректно (llm_active/elapsed).
4. Прогнать тест на малом наборе (не прод): сколько сущностей добавит LLM, время.
Статус: НЕ внедрено (ждёт «делай»). Версия при внедрении — v0.0.57.
@@ -0,0 +1,42 @@
# Промпт для Sonnet — честный ответ: можно ли ускорить без риска устойчивости?
## Роль
Отвечай ЧЕСТНО и ПО ДЕЛУ. Без воды, без лирики. Если ускорение невозможно без
компромисса (устойчивость/таблицы/риск) — прямо скажи «нет» и объясни почему.
Не предлагай «смену библиотеки» — уже пробовали, PyMuPDF теряет таблицы (40 vs 94),
ТАБЛИЦЫ КРИТИЧНЫ для пользователя. Учитывай это ограничение железно.
## ФАКТЫ (результаты наших замеров, НЕ догадки)
1. `Spartan10Manual.pdf` (14.8 МБ, 619 страниц, скан/руководство):
- `extract_text()` суммарно = **64.4с** (104мс/стр) — УЗКОЕ МЕСТО
- `extract_tables()` суммарно = **0.2с** (0мс/стр) — ничтожно
- на 272 страницах без линий extract_tables = 0.0с
→ Твоё прошлое предположение «на сканах дорогой extract_tables» НЕ подтвердилось.
Узкое место — ИЗВЛЕЧЕНИЕ ТЕКСТА, а не таблиц. Не повторяй эту ошибку.
2. Большой PDF 19 МБ (0144-03-2023_отчет об оценке.pdf, 141 стр, 94 таблицы) — ~25-26с.
3. CPU пода = 2 ядра, Memory = 4Gi. Воркер один, последовательный.
4. Разброс времени одного файла между запусками: 70с → 470с (Spartan10). Причины
не установлены точно (подозрение: CPU throttling/нагрузка пода, декомпрессия битмапов).
5. `.doc` обрабатывается через HTTP-сервис liberta (IO-bound).
6. apply_replacements — один regex из всех ключей (уже оптимизирован).
7. Сессия хранит файлы в памяти до 500 МБ (session.py). ProcessPool с fork —
риск COW-копии памяти.
## Вопрос (ответь честно)
Можно ли ЗНАЧИТЕЛЬНО ускорить обработку (цель — сократить время на больших PDF/наборах),
НЕ рискуя:
- устойчивостью (битые файлы, SSE, память пода 4Gi),
- потерей таблиц (критично),
- сложностью поддержки (код должен остаться понятным)?
Требования к ответу:
- Дай КОНКРЕТНЫЕ пункты, каждый: что менять / где / ожидаемый эффект (с цифрами из фактов выше) / риски.
- Раздели на:
A) безопасно и просто (готов внедрить сейчас, низкий риск),
B) заметный выигрыш, но с рисками/сложностью (нужен осознанный выбор),
C) рискованно/не стоит (объясни почему).
- Если для БЕЗОПАСНОГО варианта реального выигрыша нет — так и скажи: «безопасно ускорить
значительно нельзя», и предложи что реально можно сделать без риска (даже если эффект мал).
- НЕ предлагай смену pdfplumber/PyMuPDF и не предлагай то, что ломает таблицы.
- Оцени РЕАЛЬНО: при CPU=2 параллельность текста по страницам даст хоть что-то?
(GIL/процессы, overhead fork/spawn, память). С цифрами.
@@ -0,0 +1,46 @@
# Промпт для Sonnet — уточнения по LLM-чанкингу (перед внедрением)
Ты дал рекомендуемую схему (чанкинг 6000 chars, overlap 500, параллельность 4–8,
дедуп + верификация). Перед внедрением уточни практические детали. Отвечай КРАТКО,
по пунктам, с конкретикой (алгоритм/числа). Без воды.
## У.1 — Разбивка на чанки: по чему резать
Ты сказал «разбивать по абзацам/предложениям, не по символам». Но в PDF-выгрузках
текст часто сливается в один-два гигантских «абзаца» (нет \n). Дай КОНКРЕТНЫЙ алгоритм
разбивки произвольного текста (в т.ч. без \n) на чанки ~6000 символов с overlap 500:
- по каким границам резать в приоритете (\n\n, \n, '. ', '? ', '; ', потом символы)?
- как применить overlap без разрыва слова?
- псевдокод функции split_into_chunks(text, size=6000, overlap=500).
## У.2 — Дедупликация при overlap и некорректном форматировании
LLM может вернуть один и тот же value из двух чанков с РАЗНЫМ форматированием
(лишние пробелы, регистр, тире/дефис). Проверки `strip().lower()` недостаточно.
Как нормализовать перед дедупом (например, сжать пробелы, унифицировать кавычки/тире)?
Дай конкретный набор нормализаций для русских деловых текстов.
## У.3 — Верификация против исходника
Проверку «найденного значения нет в исходном тексте → отбросить (галлюцинация)» делать
против ИСХОДНОГО извлечённого текста файла (до обфускации), верно? Или против чанка?
Уточни, что есть «источник истины» для верификации, и как быть, если value найден в
нескольких чанках, но слегка отличается (нормализованное сравнение).
## У.4 — Малые файлы: нужен ли батчинг в первую итерацию
Набор сотен мелких файлов (<1000 символов). Батчинг 5–8 в один вызов ускорит, но усложнит
(деградация разметки, больший ответ). Для ПЕРВОЙ итерации можно ли обойтись «1 файл = 1 вызов»
без батчинга, даже если файлов сотни? Оцени, насколько медленнее будет без батчинга.
## У.5 — Промпт при пофайловом чанкинге
Текущий промпт (в коде) содержит блоки «Already captured by regex» + большой список правил.
При пофайловом/почанковом вызове эти блоки повторяются в каждом запросе. Оставить промпт
как есть (меняя только текст), или его надо упростить под чанк (одна папка уже нашла — regex)?
Повлияет ли повтор правил на качество/токены?
## У.6 — Прогресс и общее время
300 файлов, многие >6000 символов → сотни LLM-вызовов × по 1–2с = минуты-десятки минут.
Пользователь готов ждать (LLM своя). Но оцени: с параллельностью 4 и ~5 вызовов/файл на
крупных — реальное общее время для набора ~300 файлов (в т.ч. 270 мелких + 30 крупных).
И: не упрёмся ли в rate-limit своего LLM-эндпоинта при 4-8 параллельных.
## Формат
- По каждому У.1–У.6: короткий ответ + конкретика (алгоритм/числа/решение).
- Если что-то некритично для первой итерации — скажи явно «можно первым шагом упростить так».
@@ -0,0 +1,30 @@
# Промпт для Sonnet — вопрос по использованию LLM (без ссылок на файлы)
## Контекст
У нас сервис обфускации документов (удаление персональных данных из русских деловых документов).
Двухступенчатый детект ПДн:
1. Regex-паттерны (телефоны, ИНН, email, типовые формы — быстро, локально, дёшево).
2. LLM (общая NER-подстраховка) — сейчас обрабатывает только урезанный фрагмент:
- от каждого файла берутся первые ~3000 символов,
- всё вместе обрезается до ~8000 символов,
- выполняется ОДИН вызов LLM на весь набор.
Пользователь хочет, чтобы LLM обрабатывал ВСЕ данные (ВСЕ файлы целиком), не только первые 8000 символов.
LLM — своя, стоимость не важна. Важно качество распознавания ПДн и разумная архитектура вызова.
## Вопрос
Как лучше организовать вызов LLM, чтобы он БЕЗ потерь покрывал все документы (набор из сотен файлов, каждый до нескольких десятков страниц), сохраняя качество распознавания русских деловых документов?
Ограничения/требования к ответу:
- Не предлагай менять модель/external API — LLM своя, она остаётся.
- Подумай про: пакетирование файлов (размер батча), максимальную длину контекста на запрос,
как избежать обрезки данных, как не потерять качество при больших объёмах,
параллельность запросов (если релевантна), дедупликацию найденного между пакетами.
- Дай конкретную рекомендуемую схему (числа: размер батча, лимит символов на файл/пакет, параллельность).
- Оцени риски: токены/время, качество, риск пропуска.
- Сухо, по делу, без воды.
## Формат
- Секция «Рекомендуемая схема» — конкретный план с числами.
- Секция «Риски» — что может пойти не так и как смягчить.
- Секция «Минимум для старта» — если хочется просто и быстро, что достаточно сделать первым шагом.
@@ -0,0 +1,53 @@
# Промпт для Sonnet — follow-up вопросы по ревью (скорость + сбои)
## Контекст
Ты дал ревью (файл: History/2026-08-20-sonnet-query-review-speed.md). Часть пунктов приняли,
часть — требуют уточнения. Отвечай ТОЛЬКО на вопросы ниже, сухо, конкретно, без воды.
Если что-то из твоих утверждений основано на допущении, а не на факте — скажи явно «допущение» и
что нужно проверить, чтобы подтвердить.
## Вопрос 1. Фильтр `page.lines or page.curves` перед `extract_tables()` (extractor.py)
Ты предложил: не вызывать extract_tables() на страницах без vector-линий, т.к. на сканах это пустой проход.
Вопросы:
1.1. Перечисли КОНКРЕТНО типы таблиц, которые `extract_tables()` находит, но которые НЕ дают
`page.lines`/`page.curves` (текстовые сетки, таблицы на заливке/fill, встроенные картинки, что ещё?).
1.2. Есть ли в нашем реальном наборе (TMP/спецификации, 0144-03-2023_отчет об оценке.pdf, документы из
DownLoads) риск, что фильтр отбросит реальную таблицу? Это надо проверить фактом — предложи
точный способ замера: как сравнить число таблиц с фильтром и без на конкретных файлах.
1.3. Насколько `page.lines`/`page.curves` дешевле `extract_tables()`? В pdfplumber они тоже делают
парсинг объектов страницы — дай оценку реального выигрыша на скан-PDF, не «в разы», а чем измерить.
1.4. Безопасная альтернатива: может, стоит вызывать extract_tables() только если `page.find_tables()`
вернул непусто? Или это то же самое по стоимости? Уточни, что реально дорого внутри extract_tables().
## Вопрос 2. `_decode_name`: попытка decode("utf-8") перед decode("cp866")
Ты предложил порядок: raw.decode("utf-8"), при ошибке — raw.decode("cp866").
2.1. Оцени риск ложного срабатывания: когда CP866-байты случайно образуют валидный UTF-8
(например, псевдографика 0xC0-0xDF + продолжения 0x80-0xBF). Насколько это реально для имён
файлов 1С? Есть ли способ отличить «настоящий UTF-8» от «случайного» (например, проверить
диапазон символов после декодирования)?
2.2. Подтверди, что для нашего реального случая (Info-ZIP UTF-8 без флага 0x800) этот порядок даёт
корректное имя, а для CP866 1С — не ломает.
## Вопрос 3. ProcessPoolExecutor по страницам / по файлам
Ты предложил распараллелить извлечение текста. Вопросы:
3.1. Риск памяти: в контейнере сессия уже держит файлы в памяти (до 500 МБ в session.py). При fork
воркеры получают COW-копию памяти родителя. Оцени реальный риск OOM в managed-поде (лимит CPU 2,
Memory 4Gi) при 4-8 процессах. Не будет ли хуже, чем текущее последовательное?
3.2. pdfplumber сам по себе уже использует один процесс. Дай конкретный план безопасного
распараллеливания: какие данные передавать в воркер (только bytes страницы или весь файл?),
как собирать результаты в порядке индексов, как не раздуть память.
3.3. Учитывая CPU 2 (2 ядра) — какой реальный выигрыш даст ProcessPool на 2 ядрах? Стоит ли это
сложности, или сначала дёшево (П.1 + П.2)?
## Вопрос 4. Что реально тормозит в Spartan10Manual.pdf (70470с на 14.8 МБ)
Ты утверждаешь: виноват пустой extract_tables() на скане. Но время скачет 70→470с — это подозрительно.
4.1. Как точно измерить, что дороже на ЭТОМ файле: extract_text() по страницам или extract_tables()?
Дай конкретный способ замера (тайминги по функциям, по страницам), чтобы не гадать.
4.2. Объясни разброс 70–470с: что в коде/данных может давать такой разброс между запусками
(кэши, LLM, сеть к liberta, нагрузка CPU пода)?
4.3. Есть ли в extract_text() внутри pdfplumber скрытый повторный парсинг (например, повторное чтение
объекта при каждом вызове), который можно убрать без смены библиотеки?
## Формат ответа
- По каждому вопросу: короткий ответ + «факт»/«допущение» + что проверить, если допущение.
- Без воды, без лирики.
@@ -0,0 +1,44 @@
# Промпт для Sonnet — код-ревью drhider (скорость + сбои)
## Роль
Ты — ревьюер кода. Отвечай ТОЛЬКО по делу: короткие технические тезисы. Без воды, без лирики, без «как было бы здорово», без общих фраз. Каждый тезис — конкретика: файл, функция, строка, суть.
## Ограничение: смотреть ТОЛЬКО эти файлы, нигде больше не рыться
- `drhider/obfuscator.py` — двухпроходный обфускатор (проход 1: извлечение текста + regex + LLM NER; проход 2: замена)
- `drhider/extractor.py` — pdf_to_markdown (pdfplumber), doc_to_markdown (сервис liberta), expand_zips (рекурсия zip, CP437→CP866), защита от zip-бомб
- `drhider/scanner.py` — scan_regex, scan_llm_ner
- `drhider/replacer.py` — apply_replacements (один regex из всех ключей)
- `drhider/llm_client.py` — OpenAI-совместимый клиент, счётчики токенов/времени
- `drhider/builder.py` — build_zip, build_mapping_csv
- `site/routes/api_bp.py` — upload, process_stream (SSE + воркер в потоке), download/csv
- `site/app.py` — create_app, лимиты, setup_logging (LOG_LEVEL/LOG_FILE)
- `site/session.py` — хранение сессий в памяти, TTL 30 мин, MAX_SESSION_BYTES
- `site/templates/index.html` — фронт: нативный unzip, таблица, SSE-прогресс, таймеры, лимиты 100МБ/1ГБ
Не читай README, docs, History, tests, Dockerfile, ничего про деплой.
## Контекст: сбои, которые были (объясни каждый, если увидишь первопричину в коде)
1. «SSE connection failed» при живом воркере. Выяснено: воркер падал на битом PDF — `PDFSyntaxError('No /Root object!')` в `extract_text` не был обёрнут по-файлово; после фикса (skip битого файла) SSE доходит до complete. Подтверди/опровергни по коду, есть ли ещё места, где одна ошибка файла роняет весь проход.
2. Кракозябры кириллицы в именах zip. Причина: Info-ZIP пишет UTF-8 без флага 0x800 → декодер `encode(cp437).decode(cp866)` ломает. Проверь `_decode_name` в extractor.py: покрывает ли оба реальных сценария (CP866 без флага и UTF-8 с флагом), есть ли дыры.
3. HTTP 413 при загрузке. Лимиты: MAX_CONTENT_LENGTH=200MB (запрос), MAX_SESSION_BYTES=500MB (сессия). Фронт пускает до 100МБ/файл и 1ГБ/сумма — рассинхрон фронт/бэк. Отметь.
## Главная задача: предложи БОЛЕЕ БЫСТРУЮ логику обработки
Известные факты производительности (факты, не догадки):
- pdfplumber на скане Spartan10Manual.pdf 14.8 МБ — 70–470 секунд на извлечение текста.
- Большой PDF 19 МБ — ~2526 с на extract_text.
- LLM NER — один общий вызов на все файлы, ~1.8с на маленьком наборе, зависит от числа токенов.
- apply_replacements оптимизирован (один regex), но проверить, нет ли лишней работы.
Что хочешь от тебя:
- Где реальные узкие места в коде (не «вообще», а конкретные функции/строки).
- Конкретные предложения ускорения: что менять, как, ожидаемый эффект. Без «использовать PyMuPDF» — уже пробовали, теряет таблицы (94 vs 40), ТАБЛИЦЫ КРИТИЧНЫ. Учитывай это ограничение.
- Можно ли ускорить без смены pdfplumber (параллельность страниц, кэши, ограничение повторных парсингов, предобработка)?
- Есть ли дублирующая работа между проходами 1 и 2 (например, повторный парсинг/чтение)?
- Безопасно ли распараллелить извлечение текста по файлам (потоки/GIL)? Если да — как.
## Формат ответа
- Секция «Сбои»: по каждому — подтверждение/опровержение по коду + где именно.
- Секция «Узкие места»: список файл:строка + суть + почему.
- Секция «Предложения ускорения»: пронумерованный список, каждый пункт: что/где/как/эффект/риски.
- Секция «Итог»: 3–5 самых важных действий по приоритету.
- Максимум — сухо, без воды.
@@ -0,0 +1,127 @@
# 2026-08-20 — Ревью Sonnet: вопросы и ответы (Q&A)
Серия: ревью скорости обработки + объяснение сбоев.
Предыдущие файлы:
- History/2026-08-20-sonnet-query-review-speed.md — исходный промпт ревью
- History/2026-08-20-sonnet-query-review-speed-followup.md — вопросы по спорным пунктам
- Настоящий файл — ответы Sonnet + решения
---
## В1. Фильтр `page.lines or page.curves` перед `extract_tables()` (extractor.py)
### В1.1 — Какие таблицы extract_tables() найдёт, но фильтр отбросит?
**Sonnet: допущение (не факт).**
`extract_tables()` со стратегией по умолчанию ищет только векторные линии (strategy="lines").
- Таблицы на цветном фоне (только `page.rects`, без линий) — фильтр `lines or curves` их пропустит → **дыра**.
Нужно `or page.rects`.
- Whitespace-таблицы (только пробелы/выравнивание) — не найдёт вообще без strategy="text".
- Растровые таблицы в embedded images — не найдёт вообще.
**Решение:** фильтр = `page.lines or page.curves or page.rects`. Обязательно проверить фактом (В1.2).
### В1.2 — Как проверить, что фильтр не потеряет таблицы?
**Sonnet:**
```python
with pdfplumber.open(fname) as pdf:
for i, page in enumerate(pdf.pages):
t_all = page.extract_tables()
has_lines = bool(page.lines or page.curves or page.rects)
t_filtered = page.extract_tables() if has_lines else []
if len(t_all) != len(t_filtered):
print(f"p{i}: потеряно {len(t_all)-len(t_filtered)} таблиц")
```
Прогнать на TMP-спецификациях и 0144-03-2023_отчет об оценке.pdf.
**Решение:** сделать замер до включения фильтра в код. ТАБЛИЦЫ КРИТИЧНЫ (прецедент PyMuPDF 40 vs 94).
### В1.3 — Насколько page.lines/curves дешевле extract_tables()?
**Sonnet: допущение.**
`page.lines``@cached_property` (уже разобран при первом обращении к странице). Стоимость ≈ 0.
Дорогой в `extract_tables()``TableFinder`: строит граф пересечений линий. На скан-PDF всё равно инициализируется.
Замер:
```python
t0 = time.perf_counter(); _ = page.lines; t1 = time.perf_counter()
t2 = time.perf_counter(); _ = page.extract_tables(); t3 = time.perf_counter()
print(f"lines={t1-t0:.4f}s extract_tables={t3-t2:.4f}s")
```
### В1.4 — find_tables() как guard?
**Sonnet:** `find_tables()` и `extract_tables()` — одна стоимость (`extract_tables()` вызывает `finder.find_tables()` внутри). Guard бесполезен.
---
## В2. `_decode_name`: попытка decode("utf-8") перед decode("cp866")
### В2.1 — Риск ложного срабатывания UTF-8 на CP866-байтах?
**Sonnet:**
CP866 кириллические заглавные = 0xC00xDF. В UTF-8 0xC0, 0xC1 — overlong (невалидны), 0xC2–0xDF — валидное начало двухбайта, требует продолжения 0x80–0xBF. CP866 строчные = 0xA0–0xBF — попадают в диапазон UTF-8 continuation bytes.
**Реальная коллизия:** «Т» (0xD2) + «г» (0xA3) = 0xD2 0xA3 = валидный UTF-8 (U+04A3 Ң). Риск для имён «Тг…», «Рп…» и т.п.
**Защита:** после `decode("utf-8")` проверить, что все символы — в U+0400–U+04FF (кириллица) или ASCII. Если есть символы вне диапазона — ложное срабатывание → фоллбэк cp866.
### В2.2 — Подтверждение порядка
**Sonnet: факт.**
- Info-ZIP UTF-8 без флага: zipfile(cp437) → `encode("cp437")` → исходные UTF-8 байты → `decode("utf-8")` → корректно.
- CP866 1С: zipfile(cp437) → `encode("cp437")` → CP866 байты → `decode("utf-8")` → почти всегда UnicodeDecodeError (0xC0/0xC1 overlong, 0xE00xFF без продолжения) → фоллбэк `decode("cp866")` → корректно.
**Решение:** фикс принимается, но с валидацией диапазона кириллицы/ASCII после utf-8.
---
## В3. ProcessPoolExecutor по страницам/файлам
### В3.1 — Риск памяти при fork в managed-поде
**Sonnet: допущение.**
fork → COW. pdfplumber в дочернем создаёт новые объекты (запись → COW). 500МБ session files в родителе — COW, читаются, не копируются. Пик: pdfplumber на 14.8МБ PDF ~150300МБ на воркер. 2 воркера + родитель ≈ 1–1.5ГБ пик. При 4Gi — умеренный риск.
`spawn` (не fork): дочерние стартуют чисто, память сессии не копируется, overhead ~0.5с/процесс.
### В3.2 — Безопасный план распараллеливания
**Sonnet:**
- Единица параллелизма: **файл** (не страница), воркер получает `(fname, content_bytes)` через pickle.
- content_bytes по pipe: 15МБ ≈ 0.1с overhead.
- Порядок: `{i: executor.submit(pdf_to_markdown, content)}`, collect `{i: future.result()}`.
- progress_cb из основного потока после future.result().
### В3.3 — Выигрыш на CPU=2
**Sonnet:** на 2 ядрах при ОДНОМ PDF — нет выигрыша (один процесс на одно ядро). Выигрыш только при нескольких PDF: 3 файла × 25с → ~30с вместо 75с.
**Рекомендация Sonnet:** сначала В1 (фильтр) + В2 (decode) — бесплатно и безопасно. ProcessPool — потом, если замер покажет, что узкое место реально там.
**Решение:** ОТЛОЖЕНО. Сначала В1+В2, замерить.
---
## В4. Разброс 70470с на Spartan10Manual.pdf (14.8 МБ)
### В4.1 — Как измерить, что дороже: extract_text или extract_tables
**Sonnet:**
```python
with pdfplumber.open(io.BytesIO(content)) as pdf:
for i, page in enumerate(pdf.pages):
t0 = time.perf_counter()
page.extract_text()
t1 = time.perf_counter()
page.extract_tables()
t2 = time.perf_counter()
print(f"p{i:3d}: text={t1-t0:.3f}s tables={t2-t1:.3f}s")
```
### В4.2 — Причина разброса
**Sonnet:**
- Факт: pdfplumber не кэширует между запросами — каждый вызов парсит заново.
- Допущение: CPU throttling при limit=2 в managed-поде (нагрузка → 470с, пусто → 70с). Проверить `kubectl top pod` во время обработки.
- Допущение: битмапы скана разного размера (цветной vs ч/б) → разное время декомпрессии (JPEG2000/JBIG2). Проверить замером по страницам.
### В4.3 — Скрытый повторный парсинг в extract_text?
**Sonnet: факт.** `extract_text()` читает `self.chars` (@cached_property), `extract_tables()``self.edges` (@cached_property). Оба используют уже разобранные структуры. Повторного чтения PDF в рамках одного `with pdfplumber.open()` нет.
---
## Итоговый план (по приоритету)
1. **В2**`_decode_name`: utf-8 + валидация диапазона (кириллица U+0400U+04FF/ASCII) → фоллбэк cp866. Фикс Info-ZIP-зипов (кракозябры).
2. **В1** — фильтр `page.lines or page.curves or page.rects` перед `extract_tables()` + замер числа таблиц до/после на реальных файлах.
3. Замер ускорения на Spartan10Manual.pdf.
4. ProcessPool — после фактов, если узкое место там (отложено).
5. Диагностика разброса 70–470с: `kubectl top pod` во время обработки + замер по страницам.
## Статус реализации
- Пока НЕ реализовано (ждёт «делай»). Код не менялся в рамках этого Q&A.
@@ -0,0 +1,49 @@
# Код-ревью DrHider — ответ Соннета (2026-08-24)
_sonnet. Промпт: `docs/code-review-sonnet.md` (12 файлов кода, 8 вопросов)._
_Ревью по v0.0.72. Ничего не исправлено — только задокументировано._
## 🔴 Критичные
| Баг | Файл | ~строка | Суть | Фикс |
|---|---|---|---|---|
| **SSRF** | `api_bp.py` `upload_refs` | ~93 | `url` из клиентского JSON без валидации → `httpx.stream("GET", url)` + `follow_redirects=True`. Доступ к metadata/kube-apiserver | валидировать URL-prefix == `VM_UPLOAD_URL` до запроса |
| **Path traversal / zip slip** | `api_bp.py` + `builder.py` | ~88, ~55 | `name = ref.get("name")` без санитизации → `../../evil.md` в выходном ZIP | `name = os.path.basename(ref.get("name",""))` |
## 🟠 Средние
| Баг | Файл | ~строка | Суть |
|---|---|---|---|
| Серверная ошибка не отображается | `api_bp.py` + `index.html` | ~388, ~840 | сервер шлёт `event: error` — это встроенное имя EventSource; клиент не слушает кастомное → «SSE connection failed» вместо реального msg. Переименовать в `proc_error` |
| idx-мисматч при `expand_zips` на бэке | `obfuscator.py` + `index.html` | ~90, sendIdx | фронт фильтрует zip по `.pdf/.doc/.docx/.txt/.md`, бэк расширяет ВСЕ файлы → число/порядок не совпадает → `sendIdx[idx]=undefined` |
| Слабый SID (48 бит) | `session.py` | 85 | `uuid4().hex[:12]`; убрать `[:12]` → 128 бит |
| ZIP-бомба: обход через поддельный `file_size` | `extractor.py` | ~240 | лимит проверяется по `info.file_size` (central dir, подделывается) ДО чтения; проверять ПОСЛЕ `zf.read()` |
| Неатомарность pull при ошибке | `api_bp.py` `upload_refs` | ~115 | при исчерпании ретраев 502 БЕЗ `session_id`; частичная сессия-сирота до TTL |
## 🟡 Мелкие
| Баг | Файл | ~строка | Суть |
|---|---|---|---|
| Отмена не работает в extract-фазе | `obfuscator.py` | ~130 | в extract-цикле нет `cancel_event.is_set()`; .doc (liberta 120с) → отмена ждёт все извлечения |
| TOCTOU: сессия между check и pause_ttl | `api_bp.py` | ~165-171 | `get_files` (lock снят) → `pause_ttl`; маловероятно |
| `fileTimers` не заполняется | `index.html` | ~792 | мёртвый код, fallback всегда 0.0с |
| Self-XSS: `f.name` в innerHTML | `index.html` | ~280, ~380 | имя файла без escaping → `textContent`/`escapeHtml()` |
| ZIP с non-doc расширениями → мусор | `extractor.py` | ~257 | фильтровать расширения в `expand_zips` |
| Сессия не чистится после `/download` | `api_bp.py` | ~430 | 500МБ висит 30 мин → OOM; `cleanup(sid)` |
| LLM-таймаут тихо обнуляет чанк | `llm_client.py` + `scanner.py` | ~70, ~220 | `except → return []`, PII не найден без предупреждения |
| Debug-эндпоинт `session_files` | `api_bp.py` | ~139 | список файлов любой сессии по SID без токена |
| LLM prompt injection | `scanner.py` | ~200 | текст документа дословно в prompt → подавление NER |
## Подтверждено корректным (Соннет)
- Различимость «сессия не найдена» vs «лимит» ✓
- Гонок на `cancel_event`/`queue` нет (thread-safe) ✓
- Утечек httpx/zipfile/EventSource/таймеров нет ✓
- Пустой txt, битый pdf, 0 файлов/все сверх лимита — обрабатываются ✓
## Что уже исправлено ранее (до этого ревью)
- Ретраи pull (v0.0.72) — Соннет отметил остаточную неатомарность.
- Разделение статусов «пропущен (лимит)» / «не извлечён» (v0.0.71).
- Счётчик дедупа (v0.0.72).
## TODO-связь
- Открытый баг кнопки «Обфусцировать» при 0 файлов — `docs/WhatTODO.md` (не покрыт ревью Соннета).
@@ -0,0 +1,23 @@
# Waitress + Connection: close — hop-by-hop заголовок
**Дата:** 2026-07-14
**Версия:** 0.0.21 → 0.0.22
---
## Ошибка
`Connection` — hop-by-hop заголовок. Waitress (WSGI) запрещает его использовать в приложении (PEP 3333).
```python
# ❌ НЕЛЬЗЯ
response.headers["Connection"] = "close"
# Ошибка:
AssertionError: Connection is a "hop-by-hop" header; it cannot be used by a WSGI application (see PEP 3333)
```
## Правило
**Никогда не добавлять `Connection` заголовок в Flask/Waitress-приложении.**
Нужно закрывать соединения — только через nginx (upstream keepalive) или uWSGI/gunicorn.
@@ -0,0 +1,48 @@
# SSE: отлов отключения клиента, v0.0.18
**Дата:** 2026-07-14
**Версия:** 0.0.17 → 0.0.18
---
## Проблема
После 2-3 F5 во время обработки — загрузка падает с «Ошибка загрузки: Сеть».
Причина: утечка потоков.
### Механика
1. `process_stream` создаёт SSE-генератор, занимающий поток Waitress (всего 8)
2. `obfuscate_files()` выполняется ~30 сек (LLM-запросы)
3. Пользователь делает F5 → TCP-соединение рвётся
4. Генератор **не знает** что клиент отключился — продолжает обрабатывать файлы
5. Поток занят, не освобождается
6. 2-3 F5 → все 8 потоков заняты → новые запросы не принимаются → «Сеть»
### Почему WSGI не отключает
`yield` в генераторе пишет в буфер WSGI, а не в сокет. Буфер никуда не уходит
(клиент отключён), но генератор об этом не узнаёт — `GeneratorExit` не кидается.
---
## Решение
Добавлен `try/except GeneratorExit` после каждого `yield` в `generate()`:
```python
try:
yield f"event: done..."
except GeneratorExit:
return # клиент отключился, освобождаем поток, НЕ обрабатываем остальные файлы
```
Waitress при отключении клиента кидает `GeneratorExit` при попытке записи в
закрытый сокет. Генератор ловит → выходит → поток освобождается.
---
## Изменённые файлы
- `site/routes/api_bp.py``try/except GeneratorExit` в `generate()`
- `site/app.py``VERSION = "0.0.18"`
@@ -0,0 +1,60 @@
# 2026-08-19 — Бенчмарк UI: 5 прогонов DownLoads.zip (v0.0.49)
Набор: `files/DownLoads.zip` (21.9 МБ) → распаковывается в 18 файлов
(4 docx + 14 pdf, включая `0144-03-2023_отчет об оценке.pdf` 19.0 МБ).
Условия: сервер waitress, порт 5059, threads=8, локально.
LLM-ключ отсутствует — этап ИИ не выполнялся (0с, в статистике «—»).
Итог каждого прогона: «✅ Обработано 18 файлов», кнопки ZIP/CSV, блок статистики.
## Итоговые метрики (5 прогонов)
| Прогон | Распаковка zip, мс | Стена UI, с | SSE «общее», с |
|--------|--------------------|-------------|----------------|
| 1 | 855 | 50.3 | 49.0 |
| 2 | 851 | 47.9 | 46.6 |
| 3 | 860 | 47.1 | 45.9 |
| 4 | 833 | 47.8 | 46.1 |
| 5 | 850 | 47.9 | 46.7 |
- Распаковка: 833–860 мс (Δ 27 мс) — стабильно, ~0.85 с.
- SSE «общее»: 45.949.0 с (Δ 3.1 с), среднее ≈ 46.9 с.
- Стена UI: 47.1–50.3 с (загрузка локально ≈ 1 с).
- SSE не рвётся: 5/5 прогонов до конца, по ~50 с каждый.
## Узкое место — 19 МБ PDF (0144-03-2023_отчет об оценке.pdf)
Время по прогонам: 25.8 / 24.5 / 26.0 / 25.0 / 25.8 с → стабильно ~25.4 с.
Это ~53–55% от общего времени. Причина — pdfplumber extract_text.
## Время по файлам, среднее по 5 прогонам (с)
| Файл | Размер | Среднее |
|------|--------|---------|
| 20_ФИЯР_Порядок распределения…docx | 13.1 KB | 0.04 |
| Proxy Voting Form 2026 EGM.docx | 16.1 KB | 0.1 |
| допник-1-XXX002-01200_3.docx | 11.9 KB | 0.1 |
| Экзаменационный материал…docx | 28.9 KB | 0.2 |
| 0144-03-2023_отчет об оценке.pdf | 19.0 MB | 25.4 |
| 20_ФИЯР_Регламент проведения ВИ МАГ_2026 (1).pdf | 167.2 KB | 1.1 |
| 959399eab8f6a91df785f469b5851574.pdf | 328.7 KB | 1.5 |
| EAC_979 419 6577.pdf | 314.1 KB | 0.3 |
| examrules_test.pdf | 486.3 KB | 2.3 |
| General Terms.pdf | 764.8 KB | 3.8 |
| Marshrutnaya_kvitanciya.pdf | 291.1 KB | 0.6 |
| P010028436 CERTIFICATE…pdf | 473.7 KB | 0.2 |
| P010028436 POLICY…pdf | 623.1 KB | 0.5 |
| PR00078013…pdf | 521.4 KB | 1.5 |
| Transaction_Confirmation_en…pdf | 70.3 KB | 0.1 |
| Лингвистика маг.pdf | 596.5 KB | 2.1 |
| Отчет арбитражного управляющего…pdf | 267.4 KB | 3.5 |
| ПРОГРАММА ВСТУПИТЕЛЬНЫХ ИСПЫТАНИЙ…pdf | 138.5 KB | 1.9 |
Сумма средних ≈ 45.3 с — сходится с SSE «общее».
## Стабильность
- Всё, кроме 19 МБ PDF, — стабильно: docx 0.00.2 с, PDF 0.14.3 с.
- Погрешность повторов мала; основной вклад — один большой PDF.
- Вывод: без LLM прогон 18 файлов (вкл. 19 МБ PDF) ≈ 4649 с.
С LLM добавится этап ИИ (в проде — ключ есть в кластере).
@@ -0,0 +1,45 @@
# drhider — тесты реальной обфускации и фикс .doc (2026-08-23, v0.0.600.0.61)
_Кластер `naeel-test-3`, v0.0.60 → v0.0.61. Корпус: `/mnt/y/T` (flat+many, 309 файлов pdf/doc/docx)._
## Проблема: .doc не конвертируется (v0.0.60)
Симптом: `.doc`-файлы в ZIP с заглушкой
`[DOC — conversion failed: libreoffice: failed to launch javaldx - java may not function correctly]`.
**Диагностика:**
- `.doc` конвертирует сервис **liberta** (`CONVERT_SERVICE_URL` дефолт
`https://liberta.containerk8s.dev.nubes.ru/convert`; в деплое не переопределён).
- liberta **жива** (200 на `/` и `/health`), последовательный `/convert` — 200 (~3с).
- **Под нагрузкой падает**: 5 параллельных `/convert` → 2-3 возвращают 500 `javaldx`
(JVM/libreoffice не тянет конкурентность).
- drhider конвертировал `.doc` **параллельно** (ThreadPoolExecutor max_workers=4 в `obfuscator.py`)
→ бил liberta потоком → часть конверсий падала.
## Фикс: v0.0.61 — параллельность убрана
- `obfuscator.py`: удалён ThreadPoolExecutor — `.doc` конвертируются **последовательно**.
- `scanner.py`: `_LLM_CONCURRENCY = 1` — LLM-чанки **последовательно**.
- Версия 0.0.60 → 0.0.61.
## Результаты после фикса (v0.0.61)
### Контрольный .doc (7 файлов)
Все конвертируются по 2.4–3.9с (реальное время), **javaldx-ошибок нет**.
Единственный FAIL — `mini_78b` (78 байт, битый файл) → «conversion produced no .docx» (не связано с параллельностью).
### Полный набор (30 файлов)
upload=30, zip=25 (5 мини-PDF пустые), **convfail=1** (только mini_78b), mapping=86,
tokens=61983, llm=79.9с, proc=118.9с. Сравнение с v0.0.60 (было 7 convfail) — регрессий нет.
### Долгий прогон (2×100 файлов)
| Сессия | upload | zip | convfail | mapping | tokens | llm | всего |
|---|---|---|---|---|---|---|---|
| SET1 | 100 | 99 | 0 | 430 | 700474 | 985с | 1142.5с |
| SET2 | 100 | 98 | 0 | 493 | 671170 | 841с | 972.1с |
| Итого | 200 | 197 | **0** | 923 | ~1.37 млн | ~1826с | ~35 мин |
## Выводы
1. `.doc`/liberta починено: **0 ошибок конверсии на 200 файлах**.
2. Обфускация на объёме стабильна (923 замены, без регрессий).
3. Цена «без параллельности» (по требованию): LLM-этап медленнее
(100 файлов ≈ 16–19 мин; с параллельностью было бы ~4× быстрее).
4. Файлы вне ZIP (2–3 шт) — битые/пустые PDF (нет текста), не ошибка.
@@ -0,0 +1,31 @@
# Тест выбора папки v0.0.69 на проде — полный цикл (2026-08-24)
_tests. Прод https://drhider.pythonk8s.dev.nubes.ru/, после редеплоя 18:31 (v0.0.69)._
## Что проверено (все ✓)
1. **Выбор папки рекурсивно с подпапками** — папка `Клиенты/` с `Подпапка/Акт_работ.txt`.
2. **Вложенный ZIP**`Клиенты/архивы/документы.zip` раскрыт, файлы добавлены с префиксом пути:
`архивы/Отчёт/Справка_Сидоров.txt`, `архивы/Счёт_на_оплату.txt`.
3. **Кириллические имена** — сохраняются корректно.
4. **Относительный путь** — в таблице и в результирующем ZIP.
5. **Дедуп** — повторный `Договор_Ромашка.txt` добавлен один раз.
6. **Полный цикл обфускации** (3 txt, 7.3с, ИИ 7с, токены 1858):
- CSV-таблица замен: `+7 916 123-45-67 → +7_000_000_0001`, `Петрова Анна Сергеевна → Николаев_0001`,
`Сидоров Пётр Николаевич → Павлов_0001`, `Козлова Мария Викторовна → Новиков_0001`, `Счёт 89 → Договор_0002`, `№45-А → Договор_0001`.
- ZIP: имена с путями (`Подпапка/Акт_работ.md`, `архивы/Отчёт/Справка_Сидоров.md`, `архивы/Счёт_на_оплату.md`);
содержимое обезличено («…Ответственный: Николаев_0001.»).
7. **Лимиты** — файл 51 МБ помечен «🔥 не учитывается», сводка «1 учитываются + 1 свыше лимита».
8. **Заморозка сессии** после обработки — выбор файлов/папок заблокирован до «Новая сессия».
## Найденный баг (косметика)
Счётчик «Добавлено из папки: N» завышается на число дублей: `added++` выполняется безусловно
после `addFileWithDedup`, даже когда файл не добавлен (дедуп). Пример: папка с одним дублем
→ «5 файлов» при 4 реально добавленных. На список файлов не влияет.
## Примечания
- Битый pdf-плейсхолдер (1 Б) не обработался (пропущен) — ожидаемо.
- В ZIP текстовые `.txt` выходят как `.md` — поведение бэкенда, не связано с выбором папки.
- Скачивание через событие `download` в Playwright не генерируется — данные получены прямым
`fetch /api/download/<sid>` и `/api/csv/<sid>` (sid из перехвата URL).
- Клик по «✕» в одном тике по двум кнопкам → `TypeError … 'name'` в `rm` — артефакт теста,
в реальном использовании не воспроизводится (rr() пере-рендерит DOM синхронно).
@@ -0,0 +1,30 @@
# Тесты v0.0.67: загрузка, лимиты, ZIP-кириллица, ETA, прерывание (2026-08-24)
_Фон: найден и разобран неконсистентный деплой (см. infra/2026-08-24-two-clusters-deploy.md).
Все тесты — на реальном внешнем бэкенде **iot-naeel (185.247.187.151), под v0.0.67**._
## 1. Загрузка: много + больших ✅
- 119 файлов / **480.5 МБ** (8×45МБ + 10×10МБ + 100×200КБ).
- PUT (P=8) 44.8с, `upload_refs` 51.9с, сессия: 119 файлов, 480.5МБ, 0 ошибок.
## 2. Лимит 50 МБ/файл ✅
- big60 (60МБ) + small1 (1МБ) → `upload_refs` `{count:1}`; в сессии только `small1.bin`; big60 удалён с ВМ.
- Раньше (старый под 0.0.61 в балансировке) big60 принимался — после чистки работает.
## 3. ZIP: кириллица без UTF-8-флага (v0.0.67) ✅
- ZIP с `TKM_6й_Семестр_ЭКЗАМЕН.docx`, `Договор_ООО_Ромашка.pdf` (UTF-8, флаг сброшен) → в браузере имена декодированы **верно** (было `╨╣…`-мусор). Оценки времени видны.
## 4. Обфускация 20 файлов: ETA и SSE-события ✅
- 21 файл (вкл. plan.txt): SSE 255с, `complete {total:21, tokens:104128, llm_sec:254.0}`.
- События: `start=21, extract_done=1, file_start=21, file_chunk=21, file_done=21, done=21, complete=1`.
- `llm`-heartbeat с `eta_sec`: 230 примеров, убывает 237→0 (адаптивная ETA работает).
- ZIP: 21 файл (29.4КБ), CSV: 295 строк.
## 5. Прерывание с частичным сохранением ✅
- 22 файла, через 25с `POST /api/cancel/<sid>``{"ok":true}`.
- SSE `cancelled: {saved:3, total:22, tokens:11640, llm_sec:30.2}`.
- **Частичный ZIP: 3 файла (4.4КБ), частичный CSV: 188 строк** — скачаны (HTTP 200).
## Вывод
На консистентном v0.0.67 все новые фичи работают: лимиты, декодирование имён ZIP,
ETA (global+per-file в SSE), прерывание с частичным результатом. Регрессий нет.
@@ -0,0 +1,52 @@
# drhider — тесты загрузки и обфускации (2026-08-24, v0.0.62)
_Кластер `naeel-test-3`, v0.0.62 (лимиты 50МБ/файл, 500МБ/сессия), под 1 CPU / 2 GiB
(requests=limits, подтверждено kubectl). ВМ-буфер: `https://contracts.kube5s.ru/drhider-upload/`._
## Тест 1 — загрузка (upload-only), много + больших файлов
Корпус: 118 бинарных файлов, 480 МБ (8×45МБ + 10×10МБ + 100×200КБ). Файлы >40МБ — 8 шт.
| Этап | Результат |
|---|---|
| PUT на ВМ (P=8, `--resolve` обход DNS) | **119/119 HTTP 201**, 43.4с |
| `POST /api/upload_refs` | HTTP 200, 50.8с, `{count:119, session}` |
| `GET /api/session_files` | 119 файлов, **480.5 МБ**, 8 больших (>40МБ) |
**Вывод:** загрузка через ВМ стабильна на объёме 480МБ/119 файлов под лимитом 500МБ, 0 ошибок.
## Тест 2 — обфускация, много мелких файлов
Корпус: 100 сгенерированных `.txt`-договоров с фейковыми PII (ФИО, телефоны, ИНН, ОГРН, БИК,
р/с, к/с, паспорта, адреса, email). Сессия `bba1a7272aed`.
| Показатель | Значение |
|---|---|
| start / done | **101 / 101** (0 ошибок) |
| complete | `{total: 101, tokens: 1 569 716, llm_sec: 1924.6}` |
| SSE-время | 1995.6с (~33.3 мин) |
| ZIP | **404 — НЕ скачался** |
| CSV | **404 — НЕ скачался** |
Обфускация отработала полностью (101 done, complete пришёл, `zip_len=147861` в логе worker),
но **результат потерян при скачивании** — см. баг ниже.
## ⚠️ Найденный баг: TTL сессии (30 мин) короче долгой обфускации
- `site/session.py`: `TTL_SECONDS = 30*60`, таймер запускается при создании сессии и
**не продлевается** во время обработки (`threading.Timer`, `_clean` просто `pop(sid)`).
- Обфускация заняла 1995с (> 1800с TTL) → таймер удалил сессию посреди обработки.
- `store_result(sid, zip)` при отсутствующей сессии молча возвращает `False` → результат не сохранён.
- `GET /api/download/<sid>` и `/api/csv/<sid>``get_result`=None → **404**.
- Подтверждено: `session_files/bba1a7272aed` → 404; лог: `worker: done ... in 1995.0s ... zip_len=147861`.
**Почему раньше не всплыло:** в тесте 23.08 каждая сессия завершалась за 19/16 мин < TTL.
**Предлагаемый фикс:** продлевать TTL при активности обработки (cancel+restart таймера на
каждое событие прогресса, либо отдельный длинный TTL для «в обработке», либо сохранять
результат даже при истёкшей сессии). **Не делалось — ждёт «делай».**
## Прочее
- Первый прогон теста 1 упал из-за бага тест-скрипта (отправил список вместо `{files:[...]}` в
`upload_refs`) — сервис тут ни при чём.
- DNS на Krupski медленный (~5с/резолв) — в тестах обходился `curl --resolve`.
@@ -0,0 +1,32 @@
# Массовое тестирование v0.0.72 на проде — 4 блока (2026-08-24)
_tests. Прод https://drhider.pythonk8s.dev.nubes.ru/, редеплой 19:25 (v0.0.72)._
## Блок 1 — выбор папки (всё ✓)
- Рекурсия + относительный путь: `Подпапка/Акт1.txt`.
- Вложенный ZIP с кириллицей: `архивы/док.zip``архивы/Отчёт/Справка_Сидорова.txt`, `архивы/Счёт_на_оплату.txt` (путь zip + путь внутри).
- Дедуп: дубль `Договор1.txt` отсеян → «Добавлено из папки: 4 файла» (было бы 5).
- Не-документ `.bin` (60 МБ) — пропущен.
## Блок 2 — полный цикл (всё ✓)
- 4 файла: «Обработано 4 файлов: 9.9с, ИИ 9.7с, токены 2508».
- CSV замен (152 Б): `+7 111. → +7_000_000_0001`, `Ивановым И.И. → Семёнов_0001`,
`Петрова Анна → Николаев_0001`, `Сидорова → Семёнов_0002`.
- ZIP (775 Б) с путями: `Договор1.md`, `Подпапка/Акт1.md`, `архивы/Отчёт/Справка_Сидорова.md`, `архивы/Счёт_на_оплату.md`.
- Содержимое обезличено: «Договор с Семёнов_0001, тел +7_000_000_0001» и т.д.
## Блок 3 — прерывание (всё ✓)
- 9 txt, прервано на 9-й секунде: «⏹ Остановлено. Сохранено 4 из 9 файлов + таблица замен (6.5с, токены 3183)».
- Частичный ZIP — только 4 готовых: `файл_3.md, файл_4.md, файл_5.md, файл_7.md`.
- CSV замен сохранён (1149 Б). Кнопки скачивания видны.
## Блок 4 — заморозка/UI (всё ✓)
- После прерывания: `fi.disabled`, `ub.disabled`, «Новая сессия» видна (заморозка).
- HELP открывает модалку (display:flex).
- «Новая сессия»: список очищен, статус пуст, dl-кнопки скрыты.
## Найденный мелкий баг
- После «Новой сессии» кнопка «Обфусцировать» активна при 0 файлов
(`resetAll()`: `rr()` ставит `ub.disabled=cntMain===0` (true), затем `ub.disabled=false` перекрывает).
Нажатие безопасно (`uploadFiles``if(sf.length===0) return`), но кнопка выглядит активной.
Фикс — 1 строка (не перекрывать после `rr()`). Открыто, ждёт решения.
@@ -0,0 +1,22 @@
# Тест v0.0.72 на проде — статусы, счётчик, полный цикл (2026-08-24)
_tests. Прод https://drhider.pythonk8s.dev.nubes.ru/, редеплой 19:25 (v0.0.72)._
## Сценарий
Папка: битый PDF (scan1.pdf) + 3 txt + файл 51 МБ (Big.txt).
## Результаты (все ✓)
1. **Счётчик дедупа (v0.0.72)** — папка с дублем [Договор.txt, Договор.txt, Акт.txt]
→ «✅ Добавлено из папки: 3 файла» (дубль отсеян, раньше было бы 4).
2. **Статус «не извлечён» (v0.0.71)** — во время обработки битый `scan1.pdf` в группе
«✓ Обработанные (1)» со статусом «не извлечён» (раньше было «пропущен»).
3. **Статус «пропущен (лимит)» (v0.0.71)**`Big.txt 51.0 MB` в отдельной группе
«⛔ Пропущены (сверх лимита)» (раньше попадал в «Ожидают обработки»).
4. **Полный цикл** — «✅ Обработано 3 файлов: 5.6с, ИИ 5.4с» (3 txt), битый/большой исключены.
5. **HELP + текст ограничений** (v0.0.70) — на месте.
6. **Ретраи pull (v0.0.72)** — загрузка прошла штатно (код на бэке, косвенная проверка).
## Замечания
- Статусы «не извлечён»/«пропущен (лимит)» видны ТОЛЬКО во время обработки
(3-секционная таблица); после завершения `rr()` рисует обычную таблицу.
- Страница приведена в чистое состояние («Новая сессия» → «Нет выбранных файлов»).
@@ -0,0 +1,20 @@
# Тест стабильности v0.0.75 на проде (2026-08-24)
_tests. Прод, редеплой 20:20 (v0.0.75). Комплексный сценарий после всех фиксов._
## Сценарий
Папка: битый PDF (scan.pdf) + txt + txt-дубль + Big.bin (51 МБ, не документ) + zip с документом (справка.txt).
## Результаты (все ✓)
1. **Состав**: «Добавлено из папки: 3 файла» — дубль отсеян, `.bin` пропущен,
zip раскрыт (справка.txt). Таблица: scan.pdf, Договор.txt, справка.txt.
2. **Полный цикл**: «Обработано 2 файлов: 4.9с, ИИ 4.7с» — битый PDF исключён.
3. **CSV**: глобально консистентные замены: `+7 916 111-22-33 → +7_000_000_0001`,
`+7 900 111-22-33 → +7_000_000_0002`, `Иванов Иван → Соколов_0001`, `Кузнецов Олег → Михайлов_0001`.
4. **ZIP**: `Договор.md`, `справка.md` (битый не попал).
5. **Кнопка**: disabled при 0 файлов → активна с файлом → disabled после «Новой сессии».
6. **Фильтр zip** (v0.0.75): не-документы не раскрываются, документ проходит.
## Итог
v0.0.75 стабильна: все фиксы (v0.0.70–0.0.75) работают совместно, регрессов нет.
Страница приведена в чистое состояние.
+59
View File
@@ -0,0 +1,59 @@
# Диагностика медленной/нестабильной загрузки + v0.0.43 — 2026-08-19
**Дата:** 2026-08-19
**Версия:** 0.0.42 → 0.0.43
---
## Проблема
Юзер: «Таймаут 30с» при загрузке 19 МБ PDF в drhider. Раньше (11с) грузилось.
## Диагностика (факты)
### Таймаут 30с
- Введён 14.07.2026 (коммит `b6dac6f`, «не висеть бесконечно»), НЕ мой код.
- 19 МБ / 30с = 0.63 МБ/с — при медленном/нестабильном канале не хватает.
### Замеры загрузки с ВМ (внешний путь)
- 1 МБ → 0.16с (быстро)
- 5 МБ → 51.6с (медленно)
- 10 МБ → 52.4с (медленно)
- Повтор 10 МБ ×4: **1.07с / 52.3с / 52.3с / 52.3с** — НЕСТАБИЛЬНО, чаще 52с.
- Повтор 1 МБ: то 51с, то 0.5с — задержка НЕ зависит от размера.
### Тайминг (10 МБ)
- `connect=0.046с`, `starttransfer=0.09с`, `total=52.5с`.
- Сервер отвечает быстро (starttransfer), но соединение «висит» ~52с после ответа.
- `Connection: close` — НЕ помогает (то же 52с).
### Сервер
- Из loadtest: внутри кластера 10 МБ = 0.2с — сервер/Flask быстрые.
- ClusterIP с ВМ недоступен (ВМ вне кластера) — проверить напрямую не вышло.
## Применённые аннотации ingress (НЕ помогли)
Добавлены на ingress `pythonk8s` (ns 20a75175...):
```yaml
nginx.ingress.kubernetes.io/proxy-request-buffering: "false"
nginx.ingress.kubernetes.io/client-body-buffer-size: "1024m"
```
- `kubectl patch` — применено, nginx reload (75с).
- В конфиге: `client_body_buffer_size 1024m` — применилось.
- `proxy_request_buffering on` — НЕ применилось (аннотация не подхватилась shturval-контроллером).
- Замер после: 5 МБ = 51.7с, 19 МБ = 53.8с — эффекта НЕТ.
## Вывод
- Задержка ~50с НЕ в коде drhider, НЕ в буферизации nginx, НЕ зависит от размера,
НЕСТАБИЛЬНА (то 0.5с, то 52с). Вероятно: внешний путь шлюз/балансировщик/сеть
(потеря пакетов / TCP RTO ~50с, или периодическая задержка шлюза).
- Совпадает с диагнозом loadtest: «на iot-naeel шлюз держит ~50с».
## Решение (v0.0.43) — защита от ошибки юзера
- `xhr.timeout`: 30000 → **300000 (300с)** — при нестабильной загрузке файл
догрузится (медленно, но без «Таймаут 30с»).
- Сообщение: «Таймаут 30с» → «Таймаут 300с».
## Осталось
- Аннотации на ingress оставлены (`client-body-buffer-size 1024m` не вредит;
`proxy-request-buffering` не работает на shturval — искать правильный способ).
- Корень задержки ~50с на внешнем пути — НЕ найден, требует сетевой диагностики
(tcpdump, проверка балансировщика/шлюза платформы).
@@ -0,0 +1,106 @@
# ПЛАН: тест-режим «только загрузка» (для Флэша)
_Дата: 2026-08-23. Цель: при нажатии «Обфусцировать» — только загрузить файлы
(браузер→ВМ→Flask), НЕ запускать обработку (SSE/LLM/обфускацию)._
## Суть
Добавить флаг тест-режима в фронтенд. Когда он включён, `uploadFiles()` проходит
Фазу 1 полностью (PUT на ВМ + `POST /api/upload_refs` — pull в сессию), а потом
**останавливается до Фазы 2** (не открывает EventSource, не запускает обработку).
Плюс опциональный отладочный endpoint на бэке — проверить, что файлы реально
легли в сессию с корректными размерами.
---
## Изменение 1 — флаг (site/templates/index.html)
В начало скрипта, рядом с `VM_UPLOAD_URL` (строка ~201), добавить:
```js
// Тест-режим: открыть страницу с ?upload-only=1 → только загрузка, без обработки
const TEST_UPLOAD_ONLY = new URLSearchParams(location.search).get('upload-only') === '1';
```
## Изменение 2 — ранний выход после загрузки (site/templates/index.html)
В `uploadFiles()`, сразу ПОСЛЕ блока «Шаг 1b» (try/catch `POST /api/upload_refs`)
и ПЕРЕД комментарием `// Фаза 2: обработка (SSE — прогресс по каждому файлу)`.
При этом в шаге 1b зафиксировать число загруженных файлов. Сейчас там:
```js
const data = await resp.json();
if (!data.ok) throw new Error(data.error || 'HTTP ' + resp.status);
currentSid = data.session;
```
Запомнить count:
```js
const data = await resp.json();
if (!data.ok) throw new Error(data.error || 'HTTP ' + resp.status);
currentSid = data.session;
uploadedCount = data.count || refs.length; // сколько реально легло в сессию
```
(объявить `let uploadedCount = 0;` рядом с `const refs = [];` в начале Фазы 1).
Сразу после `catch` шага 1b вставить:
```js
if (TEST_UPLOAD_ONLY) {
st.className = 'status done';
st.textContent = '✅ Загружено ' + uploadedCount + ' файлов (тест: обработка пропущена). Сессия: ' + currentSid;
ub.disabled = false;
return;
}
```
То есть: показываем итог, снова включаем кнопку, выходим из функции. Фаза 2 не выполняется.
## Изменение 3 (опционально, рекомендуется) — debug-endpoint (site/routes/api_bp.py)
Чтобы проверить размеры файлов в сессии (не повредились ли при pull), добавить:
```python
@api_bp.route("/session_files/<sid>", methods=["GET"])
def session_files(sid):
"""Отладка: список файлов сессии с размерами."""
files = get_files(sid)
if files is None:
return jsonify({"ok": False, "error": "Session not found"}), 404
return jsonify({
"ok": True, "count": len(files),
"files": [{"name": n, "size": len(b)} for n, b in files],
})
```
`get_files` уже импортирован в `api_bp.py`. Endpoint временный — убрать после теста.
---
## Как тестировать
1. Поднять сервис (локально `cd site && python app.py` или redeploy на Штурвале).
2. Открыть `https://drhider.pythonk8s.dev.nubes.ru/?upload-only=1` (или `http://127.0.0.1:5000/?upload-only=1`).
3. Выбрать файлы (в т.ч. большой >1МБ), нажать «Обфусцировать».
4. Ожидать: прогресс «Загрузка на ВМ…» по каждому файлу → «Передача ссылок…» →
«✅ Загружено N файлов (тест: обработка пропущена)». SSE НЕ открывается.
5. (Если сделан п.3) в консоли/curl: `curl /api/session_files/<sid>` → сверить `size` с оригиналом.
6. Проверить на ВМ: после pull файлы удалены (`ls /var/www/drhider-upload/` пусто).
## Как вернуть обратно (после теста)
- Флаг `TEST_UPLOAD_ONLY` без `?upload-only=1` даёт обычное поведение — **ничего выпиливать не надо**,
режим включается только query-параметром.
- Debug-endpoint (п.3) — убрать, когда тест завершён (или оставить под комментарием «отладка»).
## Замечания
- НЕ трогать бэк-логику pull (`/api/upload_refs`) — она уже работает (v0.0.58).
- НЕ менять Фазу 2 (SSE) — только добавить ранний `return` до неё.
- После правки: `python3 -c "import py_compile; py_compile.compile('site/routes/api_bp.py', doraise=True)"`,
JS проверить `node --check` (извлечь блок `<script>` из index.html).
- Версию `VERSION` в `site/app.py` НЕ поднимать, пока это тестовый режим — либо поднять, если деплоим.
@@ -0,0 +1,55 @@
# drhider — тесты загрузки и стресс (2026-08-23, v0.0.590.0.60)
_Платформа: Штурвал, кластер `naeel-test-3` (kubeconfig `~/.kube/config-naeel-test-3` на ВМ).
ВМ-буфер: `https://contracts.kube5s.ru/drhider-upload/` (nginx dav). Тест-режим: `?upload-only=1`._
## Изменения за день
- **v0.0.58** — ВМ-загрузка: PUT на ВМ + `/api/upload_refs` (egress pull) + тест-режим `?upload-only=1`.
- **v0.0.59** — убран лимит 100 МБ/файл (фронт; остался суммарный 1 ГБ).
- **v0.0.60** — дедуп по **имя+размер** (дату игнорируем), при совпадении остаётся **более свежий**.
## Edge-case тесты (v0.0.59, браузер/Playwright)
- Нулевой размер (`empty.txt` 0 B) — принимается, грузится (0 B/s), в сессии size=0. ✅
- Бинарный файл (`bin.dat`) — принимается, грузится. ✅
- Одинаковое имя, разный размер — суффикс `_2`. ✅
- Одинаковые имя+размер, разная дата — **дедуп** (было `_2`, с v0.0.60 — один, свежий). ✅
- Один файл в архиве и отдельно — **дедуп** (было `_2` из-за mtime, с v0.0.60 — один). ✅
## Стресс-тест загрузки (v0.0.60)
Под: `pythonk8s-8dbc866bb-d84p6`, requests/limits **cpu 2 / memory 4Gi**.
Путь: PUT на ВМ → `POST /api/upload_refs` (один JSON со всеми refs) → `GET /api/session_files`.
### Сессии (11 успешных + 1 OOM)
| Сес. | Файлов | Объём | PUT | refs |
|---|---|---|---|---|
| A | 120 | 364МБ | 34.4с | 37.8с |
| B | 120 | 316МБ | 29.9с | 33.3с |
| C | 1×250МБ | 250МБ | 23.2с | 23.9с |
| D | 120 | 378МБ | 40.0с | 41.6с |
| E | 120 | 301МБ | 26.7с | 31.5с |
| F | 1×200МБ | 200МБ | 18.9с | 33.8с |
| G | 120 | 360МБ | 34.8с | 36.4с |
| H | 120 | 309МБ | 29.0с | 38.4с |
| I | 1×250МБ | 250МБ | 23.2с | 24.1с |
| J | 120 | 357МБ | 33.4с | 37.9с |
| K | 120 | 366МБ | 34.3с | 40.1с |
| **L** | 120 | 369МБ | — | **FAIL (OOM)** |
| M | 120 | 347МБ | 32.5с | 36.0с (после рестарта) |
| N | 120 | 358МБ | 33.1с | 37.5с (после рестарта) |
### Память пода (рост линейный, ~1.2× объёма сессий)
55Mi (база) → 744Mi (3 сес.) → 1991Mi (6 сес.) → 3235Mi (9 сес., ~80% лимита) → **OOMKilled на L**.
### Итог OOM
- `restarts=1, lastReason=OOMKilled, exitCode=137` (лимит 4Gi).
- После авто-рестарта под работает, загрузка продолжается (M, N ok).
- **Все сессии A–L потеряны** (in-memory хранилище, TTL 30 мин, без персиста).
## Выводы / факты
1. Загрузка через ВМ стабильна на больших объёмах: до OOM — 11 сессий ~3.3ГБ, 0 ошибок, скорость не деградирует.
2. Потолок памяти = лимит пода 4Gi; OOM при ~3.5ГБ резидентных сессий.
3. Апп переживает OOM (рестарт + продолжение работы), но сессии теряются.
4. Код: `session.py MAX_SESSION_BYTES = 500МБ` (на сессию) — OOM реален только при ~8+ одновременных сессий.
5. `upload_refs` при переполнении сессии отвечает 404 «Session not found» (вводит в заблуждение — это лимит размера).
6. ГРАБЛЯ: на Krupski `dd` алиасится на `docker compose down`; прокси `172.17.192.1:10808` — тесты только `--noproxy '*'`/`NO_PROXY`.
7. Браузер-автоматизация (Playwright) видит Windows-ФС (`C:\tmp\...`) — тестовые файлы класть в `/mnt/c/tmp/` (WSL).
@@ -0,0 +1,36 @@
# 2026-08-24 — Лимиты: 50 МБ на файл, 500 МБ на сессию (v0.0.62)
## Контекст
- После переезда на naeel-test-3 ресурсы инстанса уменьшены до 1 CPU / 2048 MiB
(Штурвал, подтверждено kubectl: requests=limits 1CPU/2Gi).
- При 2Gi памяти лимит 1ГБ/сессия (фронт) + отсутствие лимита на файл стали опасны
(OOM-риск, сессии in-memory и теряются при рестарте).
- Пользователь: вернуть лимит — один файл 50 МБ, всего 500 МБ, и показывать лимиты во фронте.
## Изменения
Фронт (site/templates/index.html):
- `MAX_FILE_BYTES = 50МБ`, `MAX_SESSION_BYTES = 500МБ` (было 1ГБ, лимит на файл был убран).
- `addFileWithDedup`: файл сверх 50 МБ → `overNames` («не учитывается», красный, не обфусцируется);
суммарный лимит 500 МБ — как раньше.
- Шапка: «Ограничения: один файл не более 50 МБ, суммарно не более 500 МБ...».
Бэк:
- `site/session.py`: добавлен `MAX_FILE_BYTES = 50МБ` (рядом с `MAX_SESSION_BYTES = 500МБ`).
- `site/routes/api_bp.py` (защита):
- `POST /api/upload`: файл >50 МБ пропускается (log, continue).
- `POST /api/upload_refs`: ref с size>50МБ пропускается + DELETE с ВМ;
после pull проверяется фактический len(content)>50МБ → пропуск + DELETE.
- `site/app.py`: `MAX_CONTENT_LENGTH` 200МБ → 50МБ (защита памяти, согласовано с лимитом файла).
Версия: 0.0.61 → 0.0.62.
## Проверка
- py_compile app.py / session.py / api_bp.py — OK.
- node --check (извлечённый <script> index.html) — OK.
- get_errors — нет.
## Прочее (диагностика 5с)
- Медленная загрузка страницы ~5с — НЕ кластер/приложение: `time_namelookup=5.15с`
(медленный локальный резолвер: 8.8.8.8 первым в /etc/resolv.conf недоступен, фолбэк на 10.96.x.x).
Тот же паттерн на ВМ (5.4с). drhider отвечает за ~0.03с после соединения.
- Kubeconfig naeel-test-3 обновлён на ВМ (~/.kube/config-naeel-test-3), kubectl работает.
@@ -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,18 @@
# v0.0.68 — Кнопка «Выбрать папку» (webkitdirectory, рекурсивно) (2026-08-24)
_ux-frontend. По плану `plans/2026-08-24-folder-select-plan.md`. Код: `site/templates/index.html`._
## Что сделано
- Кнопка «📁 Выбрать папку» (`#folderBtn`) + скрытый `<input type="file" id="folderInput" webkitdirectory multiple>`.
- Обработчик `change` `#folderInput`:
- `if (busy) return` — во время загрузки/обработки выбор папки заблокирован.
- Берёт `webkitRelativePath` (`TopFolder/Подпапка/file.pdf`), **отбрасывает верхнюю папку**`rel`.
- `.zip` → раскрывает `listZipFiles`, префикс пути папки к именам; при ошибке — zip как есть.
- Документы (`.pdf .doc .docx .txt .md`) → `addFileWithDedup(new File([...], rel))`.
- Прочее — пропускается. Индикатор «Разбираю папку…», после — «✅ Добавлено N файлов».
- Дедуп/лимиты работают штатно (имя = относительный путь → подпапки не сливаются).
- Поддержка: Chrome/Edge — полно, Firefox — частично, Safari — нет (best-effort).
## Проверка
- `node --check` — OK; `py_compile` — OK.
- Версия 0.0.67 → 0.0.68.
@@ -0,0 +1,42 @@
# v0.0.69 — Архивы из папки: не терять, раскрывать документы (2026-08-24)
_ux-frontend. После ретеста v0.0.68 пользователем на проде._
## Ситуация
Пользователь выбрал папку, в которой лежали `iviconfig8257.zip` (17.1 KB) и
`Nail_Nevrolog_20250602.PDF`. Добавился только PDF («Добавлено из папки: 1 файлов»),
zip не раскрылся. Требование: «если в папке архив — его надо раскрывать!».
## Диагностика (Playwright на проде, v0.0.68)
- Сервер отдавал v0.0.68 с кнопкой — деплой прошёл; первая загрузка страницы
показывала старый кэш v0.0.67 (нужен reload).
- Собранный в браузере валидный zip с документами в подпапке — обработчик папки
РАСКРЫВАЕТ корректно: файлы добавляются с префиксом пути (`Подпапка/Договор…`),
статус «Добавлено из папки: 4 файлов».
- Вывод: `iviconfig8257.zip` не раскрылся, потому что ВНУТРИ нет документов
(конфиги/прочее) — `listZipFiles` вернул пусто. Оба обработчика (fileInput и
folderInput) молча ТЕРЯЛИ такой архив.
## Фикс (site/templates/index.html)
1. `fi`-обработчик: `nested.length` → добавлять файлы; иначе — архив как есть.
2. `folder`-обработчик: то же для zip из папки (если документов нет — архив как есть).
3. Склонение счётчика: «1 файл / 2 файла / 5 файлов».
Поведение после фикса:
- В папке zip с документами → раскрывается (файлы с путём в таблицу).
- В папке zip без документов / нераспакованный → добавляется как zip, не теряется.
## Проверка
- `node --check` — OK, `py_compile` — OK.
- Локальный запуск на 127.0.0.1:5010 (v0.0.69), сценарий папки
[zip-конфиг + PDF] → «✅ Добавлено из папки: 2 файла», в таблице оба файла.
- Версия 0.0.68 → 0.0.69.
## Проверка на проде (после редеплоя 18:31, v0.0.69)
- Сценарий A — папка [zip БЕЗ документов + PDF] → «✅ Добавлено из папки: 2 файла»,
в таблице `iviconfig8257.zip` (как есть) + `Nail_Nevrolog_20250602.PDF`. Архив не теряется.
- Сценарий B — папка [zip С документами в подпапке + PDF] → «✅ Добавлено из папки: 3 файла»,
в таблице `Подпапка/Договор_ООО_Ромашка.pdf` (раскрыт с путём), `Акт_выполненных_работ.docx`, `Отчёт.pdf`.
- Примечание: `curl` к проду отдаёт ОБРЕЗАННЫЙ HTML (рвётся соединение, ~15-18КБ из 49.6КБ
Content-Length) — это проблема шлюза для curl, НЕ деплоя. Браузер докачивает полностью.
@@ -0,0 +1,19 @@
# v0.0.70 — Заметная кнопка «?» + пояснение «пропущен» (2026-08-24)
_ux-frontend. По замечаниям пользователя после v0.0.69._
## Что сделано (site/templates/index.html)
1. **Кнопка «HELP» в правом верхнем углу** (вместо значка «?»):
- было: «?» 12px muted — почти не видно;
- стало: синяя кнопка-пилюля с надписью **HELP** (фон `#1d4ed8`, белый жирный текст 13px/700,
padding 5×14px, radius 6px, тень; hover темнее). Размер 62.6×23px.
- Клик открывает helpModal (проверено: display flex).
2. **Текст ограничений** — добавлено пояснение статуса «пропущен»:
«…помечаются красным и не участвуют в обфускации: при запуске обработки они
получают статус «пропущен» и не попадают в результат.»
## Проверка
- `node --check` — OK, `py_compile` — OK.
- Локально (127.0.0.1:5010, v0.0.70): кнопка HELP — синяя пилюля 62.6×23px, клик открывает справку;
текст ограничений обновлён.
- Версия 0.0.69 → 0.0.70.
@@ -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` + причина).
- Значение коэффициента оценки (уточнить по ещё паре замеров).
@@ -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. **Код не задеплоен** — нужен редеплой на кластере и прогон реальных тестов
(долгий прогон + прерывание).
@@ -0,0 +1,30 @@
# v0.0.640.0.65 — Тикеры времени и оценки обработки по файлам (2026-08-24)
_ux-frontend. Продолжение v0.0.63 (прерывание/ETA). По итогам замечаний: тикеры/оценки
должны быть видны СРАЗУ и на ВСЕХ этапах, а не только в LLM-фазе._
## Проблема (v0.0.63)
- Подсветка «▶ Текущий файл» и оценки появлялись только в LLM-фазе.
- На этапе извлечения (долгие PDF, .doc через liberta) таблица показывала всё
«○ Ожидают —», без текущего файла и оценок — юзер не видел никакого прогресса.
## v0.0.64 — тикер текущего файла на всех этапах + самокалибровка
- `start` (этап извлечения): файл, который обрабатывается сейчас, помечается `current`
(предыдущий демотируется), тикер `прошло/осталось` идёт через `procRefresh` (1с).
- Замер скорости извлечения: по предыдущему «текущему» файлу считается `procExtractRate`
(сек/МБ) — оценки «ожидающих» калибруются по факту, а не по константе.
- `extract_done`: снимает подсветку извлечения (LLM-фаза назначает текущий через `file_start`).
- Оценки у «ожидающих»: LLM (`chars × rate`) либо грубая по размеру (сек/МБ).
## v0.0.65 — мгновенные оценки (ещё до старта)
- При выборе файлов (простой режим) у каждого файла в «Статусе» — `~X` (по размеру,
`EST_MB_SEC = 12` с/МБ).
- В шапке таблицы — суммарная оценка: `N файлов · S МБ · ~T`.
- Подсказка: «⏱ Время обработки каждого файла — ориентировочное (обновляется по факту)».
## Проверка
- `node --check` — OK; `py_compile` — OK.
- Локально: имя из ZIP декодируется корректно (см. v0.0.67).
## Версии
- 0.0.63 → 0.0.64 (тикеры на всех этапах) → 0.0.65 (мгновенные оценки).
@@ -0,0 +1,34 @@
# v0.0.66 — Блокировки UI: предсказуемое поведение во всех состояниях (2026-08-24)
_ux-frontend. По замечанию: во время обработки «Выбрать файлы» оставалась активной —
юзер мог менять список и ломать состояние/индексы. Требование: ВСЁ предсказуемо,
учтены ВСЕ действия юзера._
## Состояния и доступные действия
| Состояние | Выбрать файлы | ✕ удалить | Обфусцировать | Прервать | Скачать | Новая сессия |
|---|---|---|---|---|---|---|
| Простой | ✅ | ✅ | ✅ | — | — | — |
| Загрузка (1/2) | 🔒 | 🔒 | 🔒 | — | — | — |
| Обработка (2/2) | 🔒 | 🔒 | 🔒 | ✅ | — | — |
| Результат готов | 🔒 | 🔒 | 🔒 | — | ✅ | ✅ |
| Ошибка | ✅ | ✅ | ✅ | — | — | — |
## Реализация
- Глобальный флаг `busy` + `setBusy(b)`: блокирует `fileInput` и скрывает кнопки «✕»
(CSS `body.busy .remove-btn { display:none }`).
- Двойная защита: обработчики `change` (выбор файлов) и `rm` (удаление) игнорируют
вызовы при `busy`.
- `uploadFiles()`: guard `if (busy) return` + `setBusy(true)`; ранние выходы (ошибки
фазы 1, test-режим) — `setBusy(false)`.
- **Заморозка сессии после результата**: `complete`/`cancelled``sessionDone=true`,
`fi`/`uploadBtn` выключены, показаны «Скачать» и новая кнопка **«🔄 Новая сессия»**
(`resetAll`). Ошибки — снимают блокировку (можно повторить).
- При закрытии вкладки (beforeunload) — `resetAll` сбрасывает всё.
## Найденный и исправленный баг при правке
- При добавлении заморозки случайно затёрлась строка `activeES.addEventListener('complete', ...)`
— JS-синтаксис сломался («Missing catch»). Восстановлено ДО пуша; `node --check` — OK.
## Проверка
- `node --check` (извлечённый <script>) — OK; `py_compile` — OK.

Some files were not shown because too many files have changed in this diff Show More