63 Commits
Author SHA1 Message Date
Repinoid 52da9a3923 Update upload interface and ignore temporary E2E artifacts
Deploy drhider / validate (push) Canceled after 0s
2026-09-16 07:42:31 +03:00
Repinoid 49bb4d70d8 feat: integrate upload-platform v0.2.2 into drhider (v0.0.78)
Deploy drhider / validate (push) Canceled after 0s
- Update upload/ module to v0.2.2 with modular Layer 1 (FilePicker) and Layer 2 (Streaming transit upload)
- Replace legacy manual file table in site/templates/index.html with FilePicker.initFilePicker
- Wire uploadViaVM with per-file status updates and abort signal support
- Add dist bundles to dist/ and site/static/dist/ with routes in site/routes/main_bp.py
- Add test_hardening.py and test_safe_name.py from upload-platform
- Bump version to 0.0.78 in site/app.py
- Document integration plan and report in History/upload-integration/
2026-09-15 12:23:31 +03:00
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
159 changed files with 10325 additions and 952 deletions
+1 -1
View File
@@ -3,7 +3,7 @@
- НЕЛЬЗЯ менять ЧТО-ЛИБО при вопросе (?, почему, как, где, что) — только ответ словами - НЕЛЬЗЯ менять ЧТО-ЛИБО при вопросе (?, почему, как, где, что) — только ответ словами
- НЕЛЬЗЯ спешить — обдумать, проверить, потом отвечать - НЕЛЬЗЯ спешить — обдумать, проверить, потом отвечать
- НЕЛЬЗЯ лезть в /home/naeel/nubes/contracts/ (кроме loadtest) - НЕЛЬЗЯ лезть в /home/naeel/nubes/contracts/ (кроме loadtest)
- НЕЛЬЗЯ предполагать — сомневаешься = спроси - НИКОГДА не руководствоваться догадками. Если что-либо неизвестно точно или недоступно — немедленно остановиться и сообщить об этом; не продолжать действие и не выдавать предположение за факт.
- НЕЛЬЗЯ грузить внешние CDN/библиотеки на фронтенд — всё в коде - НЕЛЬЗЯ грузить внешние CDN/библиотеки на фронтенд — всё в коде
# ✅ ПОРЯДОК ПРАВКИ # ✅ ПОРЯДОК ПРАВКИ
+25
View File
@@ -1,9 +1,34 @@
__pycache__/ __pycache__/
*.pyc *.pyc
.env .env
.env.*
!.env.example
venv/
.venv/
env/
ENV/
instance/
.flaskenv
.pytest_cache/
.mypy_cache/
.ruff_cache/
.coverage
coverage.xml
htmlcov/
*.log
build/
dist/
!dist/
!dist/file-picker.esm.js
!dist/file-picker.iife.js
*.egg-info/
# TMP/ # TMP/
# about1500.md # about1500.md
*.har *.har
chrome-net-export-log.json chrome-net-export-log.json
History/tests/2026-09-15-*.md
tests/e2e_mass_processing.py
TSTFILES/ TSTFILES/
files/ files/
upload-platform/
.agent-tmp/
-1
View File
@@ -1 +0,0 @@
python3
-1
View File
@@ -1 +0,0 @@
/usr/bin/python3
-1
View File
@@ -1 +0,0 @@
python3
-1
View File
@@ -1 +0,0 @@
lib
-5
View File
@@ -1,5 +0,0 @@
home = /usr/bin
include-system-site-packages = false
version = 3.12.3
executable = /usr/bin/python3.12
command = /usr/bin/python3 -m venv /home/naeel/nubes/drhider/.venv
+158
View File
@@ -0,0 +1,158 @@
# 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/) | Загрузка файлов: диагностика, лимиты, стресс-тесты |
| [upload-integration/](upload-integration/) | Интеграция переиспользуемого модуля upload-platform |
| [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.64–0.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.59–0.0.60, кластер naeel-test-3).
- **2026-08-24-upload-limits.md** — v0.0.62: лимиты 50 МБ на файл, 500 МБ на сессию.
## upload-integration/ — интеграция платформы загрузки
- [README.md](upload-integration/README.md) — стандарт и журнал процесса интеграции `upload-platform`.
- [2026-09-15-upload-platform-integration-plan.md](upload-integration/2026-09-15-upload-platform-integration-plan.md) — детальный архитектурный план и этапы интеграции версии v0.2.2 в drhider.
## 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.60–0.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` (иногда с версией). В начале каждого файла — дата и версия/статус.
При сортировке файл относится в папку по главной теме (имя файла/содержание), не по дате.
@@ -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,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.
@@ -140,6 +140,6 @@ site/ # Flask-приложение
| Переменная | Значение | | Переменная | Значение |
|---|---| |---|---|
| `LLM_API_KEY` | `sk-ucI5YvOticoOQ9Kuj5K9mQ` | | `LLM_API_KEY` | (секрет — не хранить в git) |
| `LLM_URL` | `https://api.aillm.ru/v1/chat/completions` | | `LLM_URL` | `https://api.aillm.ru/v1/chat/completions` |
| `LLM_MODEL` | `gpt-oss-120b` | | `LLM_MODEL` | `gpt-oss-120b` |
@@ -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,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,45 @@
# drhider — тесты реальной обфускации и фикс .doc (2026-08-23, v0.0.60–0.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) работают совместно, регрессов нет.
Страница приведена в чистое состояние.
@@ -0,0 +1,102 @@
# План интеграции upload-platform (v0.2.2) в drhider
**Дата:** 2026-09-15
**Статус:** План составлен, ожидает команды на выполнение
**Целевой сервис:** drhider (Managed Flask на платформе Штурвал)
**Источник модуля:** `upload-platform/` (релиз v0.2.2, commit `df0576a`)
---
## 1. Контекст и цели
В `drhider` (v0.0.77) модуль загрузки `upload/` зафиксирован в промежуточном состоянии:
- Фронтенд Слоя 1 использует устаревшую табличную вёрстку с ручным парсингом ZIP без поддержки древовидного сворачивания.
- Слой 2 работает по смешанной схеме, в то время как в `upload-platform` v0.2.2 реализован и протестирован чистый пофайловый транзит (Streaming transit: PUT $\to$ POST `/api/upload_refs` $\to$ исходящий pull в RAM $\to$ немедленный DELETE $\to$ repeat).
**Цель интеграции:**
Заменить устаревшие наработки в `upload/` и `site/templates/index.html` на проверенную автономную версию `upload-platform` v0.2.2 без нарушения бизнес-логики обфускации (Слой 3: LLM, SSE-стриминг, генерация ZIP).
---
## 2. Границы ответственности слоёв
```
┌────────────────────────────────────────────────────────────────────────┐
│ БРАУЗЕР │
│ [ Слой 1: FilePicker ] ──► [ Слой 2: uploadViaVM ] │
└─────────────────────────────────────┬──────────────────────────────────┘
│ 1. PUT файл
▼
┌─────────────────────────────┐
│ ВМ-буфер (RAM WebDAV) │
└──────────────┬──────────────┘
│ 3. Исходящий GET (pull в RAM)
│ 4. DELETE с ВМ
▼
┌────────────────────────────────────────────────────────────────────────┐
│ FLASK BACKEND │
│ [ Слой 2: Blueprint upload_refs ] ──► [ RAM-сессия: add_file ] │
│ │ │
│ [ Слой 3: drhider engine ] ◄─────────────────────┘ get_files(sid) │
│ (SSE-прогресс, обфускация, mapping, download) │
└────────────────────────────────────────────────────────────────────────┘
```
- **Слой 1 (Фронтенд)**: инициализация через `FilePicker.initFilePicker()`. Полностью изолирован от транспорта. Возвращает список объектов `File` через `picker.getFiles()`.
- **Слой 2 (Фронтенд + Бэкенд)**: `uploadViaVM` передаёт файлы по одному через буфер на ВМ в RAM сессии Flask.
- **Слой 3 (drhider)**: логика обфускации забирает готовые файлы из `upload.backend.session.get_files(sid)` и запускает существующий пайплайн (`/api/drhider/<sid>`).
---
## 3. Этапы интеграции
### Этап 1. Синхронизация модуля ядра (`upload/`)
1. Заменить содержимое `upload/` в корне `drhider` на актуальную версию из `upload-platform/upload/`:
- `upload/backend/`: актуальные `upload_refs/` (поддержка `httpxTransport`, безопасный pull, санитизация `safe_name`) и `session/` (хранилище в RAM, TTL, блокировки).
- `upload/frontend/`: чистые ES-модули (`index.js`, `table/`, `upload/`, `zip/` на базе `fflate`).
2. Скопировать готовые бандлы из `upload-platform/dist/` в `site/static/dist/` (или настроить прямую раздачу из `upload/frontend` через `main_bp.py`).
### Этап 2. Проверка и настройка бэкенда (`site/routes/`)
1. Проверить регистрацию Blueprint в [site/routes/__init__.py](site/routes/__init__.py):
```python
app.register_blueprint(create_upload_refs_blueprint({
"apiPrefix": "/api",
"vmUploadPrefix": "https://contracts.kube5s.ru/drhider-upload/",
"maxFileBytes": 50 * 1024 * 1024,
"maxSessionBytes": 500 * 1024 * 1024,
"ttlSeconds": 30 * 60,
"pullRetries": 3,
"pullRetryDelay": 2,
}))
```
2. Убедиться, что [site/routes/api_bp.py](site/routes/api_bp.py) бесшовно использует сессии из обновлённого `upload.backend.session`.
### Этап 3. Обновление пользовательского интерфейса (`site/templates/index.html`)
1. Заменить устаревшую ручную вёрстку таблицы и инпутов на контейнер для монтирования Слой 1:
```html
<div id="file-picker-container"></div>
```
2. Подключить модуль `initFilePicker` и сконфигурировать фильтрацию по `.pdf, .doc, .docx, .txt, .md`.
3. Модифицировать обработчик кнопки «🛡️ Обфусцировать»:
- Вызов `uploadViaVM(files, { vmUploadUrl, backendUploadUrl, onFileStatus, onProgress })`.
- По завершении загрузки всех файлов — автоматический переход к Стек/Фазе 2: запуск SSE-стрима `/api/drhider/<sid>`.
4. Сохранить существующий трёхсекционный рендер обработки (Готово / Обрабатывается / Ожидают) и индикацию ETA/статистики LLM.
### Этап 4. Тестирование и валидация
1. **Автономные тесты модуля `upload`**:
- Запуск pytest для `upload/` внутри `drhider`.
2. **Интеграционные тесты `drhider`**:
- Запуск существующих тестов `tests/test_builder.py`, `tests/test_extractor.py`, `tests/test_replacer.py`, `tests/test_scanner.py`.
3. **Ручная/Smoke проверка сквозного цикла**:
- Выбор одиночного документа, папки и архива ZIP.
- Проверка пофайловой загрузки через буфер и немедленного удаления с ВМ.
- Проверка успешного прохождения двухпроходной обфускации и скачивания результирующего архива.
---
## 4. Контрольные точки (Definition of Done)
- [ ] Все тесты `pytest tests/` завершаются успешно (PASS).
- [ ] Отсутствуют временные файлы на диске (всё в RAM).
- [ ] Не нарушена логика отмены (`cancel`) и работы со стримами SSE.
- [ ] Сборка и синтаксис проверены через `py_compile`.
- [ ] Документ с отчётом о завершении зафиксирован в `History/upload-integration/`.
@@ -0,0 +1,46 @@
# 2026-09-15: Интеграция upload-platform v0.2.2 в drhider
## 1. Цель и контекст
Интеграция протестированного модульного релиза `upload-platform` v0.2.2 в сервис `drhider`.
Версия сервиса повышена: `v0.0.77` $\rightarrow$ `v0.0.78`.
## 2. Выполненные изменения
### Ядро модуля `upload/`
- **Слой 1 (Frontend)**: переход на модульный `initFilePicker` с распаковкой ZIP через `fflate`, древовидным представлением папок и архивов, встроенным сворачиванием/разворачиванием групп и дедупликацией. Устаревшие ручные парсеры удалены.
- **Слой 2 (Transit Upload)**:
- **Frontend**: пофайловый стриминговый транзит `uploadViaVM` (PUT на ВМ $\rightarrow$ POST `/api/upload_refs` $\rightarrow$ исходящий pull $\rightarrow$ DELETE из буфера $\rightarrow$ повтор для следующего файла).
- **Backend**: обновлён Blueprint `create_upload_refs_blueprint`: добавлена поддержка локальных путей/mock-буфера (`is_path_only_prefix`), кастомного `httpxTransport` для безопасного тестирования, возвращение параметра `added`.
- **Session**: in-memory хранилище сессий с потокобезопасным TTL и проверкой лимитов.
### Приложение Flask и маршруты
- В [site/routes/main_bp.py](site/routes/main_bp.py) добавлены маршруты раздачи бандлов: `/file-picker/<path:filename>` и `/dist/<path:filename>`.
- Бандлы `file-picker.iife.js` и `file-picker.esm.js` размещены в `dist/` и `site/static/dist/`.
### Пользовательский интерфейс ([site/templates/index.html](site/templates/index.html))
- Вёрстка выбора файлов заменена контейнером `#filePicker`.
- Инициализирован `FilePicker.initFilePicker` с разрешёнными расширениями (`.pdf`, `.doc`, `.docx`, `.txt`, `.md`).
- Логика кнопки «🛡️ Обфусцировать» переведена на `FilePicker.uploadViaVM` с поддержкой отмены (`AbortController`) и пофайловым отображением статусов.
- Сохранён и адаптирован этап 2 обработки (SSE `/api/process_stream/<sid>`, тикеры времени, статистика LLM, выгрузка ZIP и CSV).
### Тесты
- Скопированы и адаптированы тесты:
- [tests/test_hardening.py](tests/test_hardening.py) (проверка устойчивости, malformed requests, SSRF, traversal).
- [tests/test_safe_name.py](tests/test_safe_name.py) (санитизация путей).
- Обновлён [tests/test_upload.py](tests/test_upload.py) (длина сессии `uuid4().hex` = 32 символа).
## 3. Проверки и верификация
- `pytest tests/`: **39 passed** в 4 тестовых файлах.
- `python3 tests/test_builder.py`: **20/20 PASS**.
- `python3 tests/test_extractor.py`: **10/10 PASS**.
- `python3 tests/test_replacer.py`: **10/10 PASS**.
- `python3 tests/test_scanner.py`: **20/20 PASS**.
- `python3 tests/test_zip.py`: **28/28 PASS**.
- Проверка синтаксиса `py_compile` по всем `.py` файлам проекта: **ALL PY FILES COMPILED OK**.
- Проверка импортов ядра: `from drhider import obfuscate_files, LLMClient` $\rightarrow$ **OK**.
- Проверка HTTP:
- `GET /` $\rightarrow$ **HTTP 200**, HTML отрендерен (28571 байт).
- `GET /health` $\rightarrow$ **HTTP 200**, `{"ok":true,"version":"0.0.78"}`.
- `GET /file-picker/file-picker.iife.js` $\rightarrow$ **HTTP 200** (44465 байт).
+21
View File
@@ -0,0 +1,21 @@
# History / upload-integration — Журнал интеграции upload-platform
В этом каталоге фиксируется полный жизненный цикл интеграции автономного модуля
`upload-platform` (Слой 1: File Picker + Слой 2: пофайловый транзит через ВМ-буфер)
в сервис `drhider`.
## Стандарт ведения документации по интеграции
Каждый этап или значимый результат документируется отдельным файлом по формату:
`YYYY-MM-DD-<краткое-действие>.md`
Обязательные разделы каждого документа:
1. **Цель и контекст**: что делалось, версии компонентов (`drhider`, `upload-platform`).
2. **Выполненные изменения**: затронутые файлы и границы слоёв (Слой 1, Слой 2, Слой 3).
3. **Проверки и тесты**: команды запуска тестов и результаты (PASS/FAIL).
4. **Обнаруженные нюансы/отклонения**: проблемы обратной совместимости, сетевые или платформенные особенности.
## Документы раздела
- [2026-09-15-upload-platform-integration-plan.md](2026-09-15-upload-platform-integration-plan.md) — детальный архитектурный план и этапы интеграции версии v0.2.2 в drhider.
- [2026-09-15-upload-platform-v022-integration-report.md](2026-09-15-upload-platform-v022-integration-report.md) — отчёт об успешной интеграции и верификации v0.2.2 (версия drhider v0.0.78).
@@ -0,0 +1,55 @@
# drhider — тесты загрузки и стресс (2026-08-23, v0.0.59–0.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,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.

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