Compare commits
67
Commits
2a735e34f8
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
52da9a3923 | ||
|
|
49bb4d70d8 | ||
|
|
3fd6f1cffc | ||
|
|
24bd89b511 | ||
|
|
f90d97b276 | ||
|
|
1ff305f38e | ||
|
|
741edafc4a | ||
|
|
04affca363 | ||
|
|
d0128392e3 | ||
|
|
11716259a1 | ||
|
|
4b33593a82 | ||
|
|
e7d771e00f | ||
|
|
ce234132dd | ||
|
|
3abcc6c1fc | ||
|
|
0ad9e0047f | ||
|
|
52c3eb53f1 | ||
|
|
17e6dbd065 | ||
|
|
53751a1f7b | ||
|
|
a33dff2f47 | ||
|
|
af172115b0 | ||
|
|
5711e97fe5 | ||
|
|
5f5ed000b8 | ||
|
|
a18877415d | ||
|
|
7a4f642452 | ||
|
|
090f5e8993 | ||
|
|
d8dee114a3 | ||
|
|
f2141ec731 | ||
|
|
eaa064a721 | ||
|
|
6c51df96d9 | ||
|
|
ba4546ecc9 | ||
|
|
dd1e285c8c | ||
|
|
ff4895b63b | ||
|
|
4b73657e4c | ||
|
|
82acc84311 | ||
|
|
933ee79e3c | ||
|
|
b0c055f926 | ||
|
|
44e12db886 | ||
|
|
08e4788a35 | ||
|
|
e31476ebab | ||
|
|
0479afb44a | ||
|
|
cee18046c2 | ||
|
|
ec660ddd1f | ||
|
|
a901c4f023 | ||
|
|
89bb8a3442 | ||
|
|
2a49155b05 | ||
|
|
f45ecb603d | ||
|
|
cfe6b9e2bf | ||
|
|
d5b3ed3f68 | ||
|
|
45c6f3b7f0 | ||
|
|
023dba3daa | ||
|
|
c346be5add | ||
|
|
dd9785747d | ||
|
|
efa18795db | ||
|
|
a645493c32 | ||
|
|
db7cdbdd84 | ||
|
|
25e4b46e76 | ||
|
|
0e5f3ec9a1 | ||
|
|
7a860d645d | ||
|
|
6542d51375 | ||
|
|
0dc5d8673d | ||
|
|
2fd4afa907 | ||
|
|
1509eeb989 | ||
|
|
c17fca792c | ||
|
|
4087f10345 | ||
|
|
6dcbd2bade | ||
|
|
5ddee6b4bd | ||
|
|
82b597e1c2 |
@@ -3,7 +3,7 @@
|
|||||||
- НЕЛЬЗЯ менять ЧТО-ЛИБО при вопросе (?, почему, как, где, что) — только ответ словами
|
- НЕЛЬЗЯ менять ЧТО-ЛИБО при вопросе (?, почему, как, где, что) — только ответ словами
|
||||||
- НЕЛЬЗЯ спешить — обдумать, проверить, потом отвечать
|
- НЕЛЬЗЯ спешить — обдумать, проверить, потом отвечать
|
||||||
- НЕЛЬЗЯ лезть в /home/naeel/nubes/contracts/ (кроме loadtest)
|
- НЕЛЬЗЯ лезть в /home/naeel/nubes/contracts/ (кроме loadtest)
|
||||||
- НЕЛЬЗЯ предполагать — сомневаешься = спроси
|
- НИКОГДА не руководствоваться догадками. Если что-либо неизвестно точно или недоступно — немедленно остановиться и сообщить об этом; не продолжать действие и не выдавать предположение за факт.
|
||||||
- НЕЛЬЗЯ грузить внешние CDN/библиотеки на фронтенд — всё в коде
|
- НЕЛЬЗЯ грузить внешние CDN/библиотеки на фронтенд — всё в коде
|
||||||
|
|
||||||
# ✅ ПОРЯДОК ПРАВКИ
|
# ✅ ПОРЯДОК ПРАВКИ
|
||||||
|
|||||||
+25
@@ -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 +0,0 @@
|
|||||||
python3
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
/usr/bin/python3
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
python3
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
lib
|
|
||||||
@@ -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
|
|
||||||
@@ -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 байт).
|
||||||
@@ -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
Reference in New Issue
Block a user