Compare commits

...
4 Commits
10 changed files with 286 additions and 1 deletions
+7
View File
@@ -35,6 +35,7 @@
- **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 (вопросы и ответы)
@@ -89,6 +90,7 @@
- **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/ — фичи фронтенда
@@ -101,6 +103,9 @@
- **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-ui-locks-session-freeze.md** — v0.0.66: блокировки UI (выбор/удаление) на время работы, заморозка сессии + кнопка «Новая сессия».
## upload/ — загрузка файлов
@@ -119,12 +124,14 @@
- **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, прерывание с частичным сохранением.
---
@@ -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,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,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,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,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,30 @@
# v0.0.64–0.0.65 — Тикеры времени и оценки обработки по файлам (2026-08-24)
_ux-frontend. Продолжение v0.0.63 (прерывание/ETA). По итогам замечаний: тикеры/оценки
должны быть видны СРАЗУ и на ВСЕХ этапах, а не только в LLM-фазе._
## Проблема (v0.0.63)
- Подсветка «▶ Текущий файл» и оценки появлялись только в LLM-фазе.
- На этапе извлечения (долгие PDF, .doc через liberta) таблица показывала всё
«○ Ожидают —», без текущего файла и оценок — юзер не видел никакого прогресса.
## v0.0.64 — тикер текущего файла на всех этапах + самокалибровка
- `start` (этап извлечения): файл, который обрабатывается сейчас, помечается `current`
(предыдущий демотируется), тикер `прошло/осталось` идёт через `procRefresh` (1с).
- Замер скорости извлечения: по предыдущему «текущему» файлу считается `procExtractRate`
(сек/МБ) — оценки «ожидающих» калибруются по факту, а не по константе.
- `extract_done`: снимает подсветку извлечения (LLM-фаза назначает текущий через `file_start`).
- Оценки у «ожидающих»: LLM (`chars × rate`) либо грубая по размеру (сек/МБ).
## v0.0.65 — мгновенные оценки (ещё до старта)
- При выборе файлов (простой режим) у каждого файла в «Статусе» — `~X` (по размеру,
`EST_MB_SEC = 12` с/МБ).
- В шапке таблицы — суммарная оценка: `N файлов · S МБ · ~T`.
- Подсказка: «⏱ Время обработки каждого файла — ориентировочное (обновляется по факту)».
## Проверка
- `node --check` — OK; `py_compile` — OK.
- Локально: имя из ZIP декодируется корректно (см. v0.0.67).
## Версии
- 0.0.63 → 0.0.64 (тикеры на всех этапах) → 0.0.65 (мгновенные оценки).
@@ -0,0 +1,34 @@
# v0.0.66 — Блокировки UI: предсказуемое поведение во всех состояниях (2026-08-24)
_ux-frontend. По замечанию: во время обработки «Выбрать файлы» оставалась активной —
юзер мог менять список и ломать состояние/индексы. Требование: ВСЁ предсказуемо,
учтены ВСЕ действия юзера._
## Состояния и доступные действия
| Состояние | Выбрать файлы | ✕ удалить | Обфусцировать | Прервать | Скачать | Новая сессия |
|---|---|---|---|---|---|---|
| Простой | ✅ | ✅ | ✅ | — | — | — |
| Загрузка (1/2) | 🔒 | 🔒 | 🔒 | — | — | — |
| Обработка (2/2) | 🔒 | 🔒 | 🔒 | ✅ | — | — |
| Результат готов | 🔒 | 🔒 | 🔒 | — | ✅ | ✅ |
| Ошибка | ✅ | ✅ | ✅ | — | — | — |
## Реализация
- Глобальный флаг `busy` + `setBusy(b)`: блокирует `fileInput` и скрывает кнопки «✕»
(CSS `body.busy .remove-btn { display:none }`).
- Двойная защита: обработчики `change` (выбор файлов) и `rm` (удаление) игнорируют
вызовы при `busy`.
- `uploadFiles()`: guard `if (busy) return` + `setBusy(true)`; ранние выходы (ошибки
фазы 1, test-режим) — `setBusy(false)`.
- **Заморозка сессии после результата**: `complete`/`cancelled` → `sessionDone=true`,
`fi`/`uploadBtn` выключены, показаны «Скачать» и новая кнопка **«🔄 Новая сессия»**
(`resetAll`). Ошибки — снимают блокировку (можно повторить).
- При закрытии вкладки (beforeunload) — `resetAll` сбрасывает всё.
## Найденный и исправленный баг при правке
- При добавлении заморозки случайно затёрлась строка `activeES.addEventListener('complete', ...)`
— JS-синтаксис сломался («Missing catch»). Восстановлено ДО пуша; `node --check` — OK.
## Проверка
- `node --check` (извлечённый <script>) — OK; `py_compile` — OK.
+1 -1
View File
@@ -21,7 +21,7 @@ if _sys_path_root not in sys.path:
sys.path.insert(0, _sys_path_root)
# Версия приложения (меняется при изменениях)
VERSION = "0.0.67"
VERSION = "0.0.68"
def setup_logging():
+47
View File
@@ -144,6 +144,8 @@
</p>
<div class="file-input-wrap">
<input type="file" id="fileInput" multiple accept=".doc,.docx,.pdf,.txt,.zip">
<button type="button" class="btn" id="folderBtn" onclick="document.getElementById('folderInput').click()">📁 Выбрать папку</button>
<input type="file" id="folderInput" webkitdirectory multiple style="display:none">
</div>
<div class="table-wrap">
<table>
@@ -200,6 +202,8 @@
</div>
<script>
const fi = document.getElementById('fileInput');
const folderInput = document.getElementById('folderInput');
const DOC_EXTS = ['.pdf', '.doc', '.docx', '.txt', '.md']; // документы (из папки/архивов)
const fl = document.getElementById('fileList');
const fc = document.getElementById('fileCount');
const ub = document.getElementById('uploadBtn');
@@ -572,6 +576,49 @@ fi.addEventListener('change', async () => {
rr();
});
// ═══ Выбор целой папки (webkitdirectory): рекурсивно, относительный путь сохраняется ═══
folderInput.addEventListener('change', async () => {
if (busy) return; // во время загрузки/обработки менять список нельзя
const incoming = Array.from(folderInput.files);
if (!incoming.length) return;
document.body.style.cursor = 'wait';
st.className = 'status progress';
st.textContent = 'Разбираю папку…';
let added = 0;
try {
for (const f of incoming) {
// webkitRelativePath: "TopFolder/Подпапка/file.pdf" — отбрасываем верхнюю папку
const parts = (f.webkitRelativePath || f.name).split('/');
const rel = parts.slice(1).join('/') || f.name;
const low = rel.toLowerCase();
const slashIdx = rel.lastIndexOf('/');
const relDir = slashIdx >= 0 ? rel.slice(0, slashIdx) : '';
if (low.endsWith('.zip')) {
try {
const nested = await listZipFiles(f);
for (const nf of nested) {
const nm = relDir ? relDir + '/' + nf.name : nf.name;
addFileWithDedup(new File([nf], nm, { lastModified: nf.lastModified }));
added++;
}
} catch (err) {
addFileWithDedup(new File([f], rel, { lastModified: f.lastModified })); // zip как есть
added++;
}
} else if (DOC_EXTS.some(e => low.endsWith(e))) {
addFileWithDedup(new File([f], rel, { lastModified: f.lastModified }));
added++;
}
// иначе — не документ, пропускаем
}
} finally {
document.body.style.cursor = '';
}
folderInput.value = ''; // чтобы повторный выбор той же папки сработал
rr();
if (added) { st.className = 'status done'; st.textContent = '✅ Добавлено из папки: ' + added + ' файлов'; }
});
function ss(idx, h) { const e = document.getElementById('st-' + idx); if (e) e.innerHTML = h; }
async function uploadFiles() {