diff --git a/History/flask-vm-architecture-2026-07-14.md b/History/flask-vm-architecture-2026-07-14.md new file mode 100644 index 0000000..4c65bfc --- /dev/null +++ b/History/flask-vm-architecture-2026-07-14.md @@ -0,0 +1,236 @@ +# Архитектура contracts-flask — срез 2026-07-14 + +## Общая схема + +``` +Браузер (index.html) + │ + │ JS-модули (6 файлов, грузятся как статика Flask) + │ XHR POST /upload, /convert-doc, /unzip-upload + │ EventSource GET /process-v2 (SSE) + │ fetch /api/* (классификация, группы, документы, промпты) + ▼ +Flask (site/app.py) — ~90 строк, ТОЛЬКО ПРОКСИ + │ GET / → render_template("index.html") + │ POST /chat → прокси на ВМ + │ GET /api/prompts → прокси на ВМ + │ Весь HTML/JS/CSS → статика /static/* + ▼ +ВМ (deploy/convert_server.py) — ВСЯ бизнес-логика + │ POST /upload → compare/upload.py + │ POST /convert-doc → convert_doc.py (libreoffice) + │ POST /unzip-upload → compare/unzip.py + │ GET /process-v2 → compare/process.py (SSE) + │ POST /api/classify-batch → classify_worker.py + │ GET /api/groups → db/groups + │ GET /api/documents/:id → db/documents + │ GET /api/supplements → db/supplements + │ POST /api/sync → db/sync + │ POST /api/apply-groups → db/supplements + │ GET /api/spec-current → db/spec_current + │ GET /api/batch-progress → db/classify-progress + │ GET /api/prompts → db/prompts + │ GET /api/prompts/list → db/prompts + │ POST /api/prompts/save → db/prompts + │ POST /api/prompts/activate → db/prompts + │ POST /api/cleanup → db/cleanup + ▼ +PostgreSQL (таблицы: documents, supplements, spec_current, prompts, contracts) +``` + +## JS-фронтенд — 6 модулей + +Все лежат в `contracts-flask/deploy/`, грузятся в HTML в этом порядке: + +```html + + + + + + +``` + +### 1. `state.js` — Центральное состояние + +```javascript +var state = { + files: [], // [{ name, size, lastModified, doc_id, uploaded, parsed, parseInfo, supp_id, supp_type, status, zip_source }] + contractId: null, // ID контракта (приходит с бэкенда после первого upload) + batchId: crypto.randomUUID(), // Уникальный ID сессии + groups: null, // [{ contract_number, counterparty, documents[], compare }] + _activeCompare: { es: null, timer: null }, // Одно активное SSE-сравнение + ui: { + steps: { upload: '○', classify: '○', groups: '○', compare: '○' } + } +}; +``` + +**ПАТТЕРН**: `action → mutate state → render(state)`. DOM — проекция state, никогда не читаем из DOM. + +### 2. `app_utils.js` — Утилиты + +| Функция | Назначение | +|---------|-----------| +| `formatSize(bytes)` | Байты → "1.5 MB" | +| `formatDate(ts)` | Timestamp → российская дата+время | +| `escHtml(s)` | Экранирование HTML | +| `removeFile(i)` | Удалить файл из state.files → render → syncDB | +| `moveUp(i)` / `moveDown(i)` | Переместить файл (не используется) | +| `openAbout()` / `closeAbout()` | Модальное окно "О сервисе" | + +### 3. `files.js` — Загрузка и таблица файлов + +| Функция | Назначение | +|---------|-----------| +| `statusToHTML(st)` | Чистая: структура → HTML-статус | +| `renderFiles(state)` | Рендер таблицы файлов (группировка по zip_source) | +| `syncDB()` | Синхронизация БД с текущим составом | +| `toggleClassifyDetail(i)` | Раскрыть результат классификации | +| `uploadFile(file, onProgress, zipSource)` | **XHR-загрузка** одного файла | +| `refreshSupps()` | Обновить supp_id/supp_type | +| `reconcileSelection(files, newFiles)` | Чистая: согласование состава | +| `applyParseResult(entry, parsed, elapsed)` | Чистая: применить результат парсинга | +| `addZipFile(file)` | Обработка ZIP (unzip → for each → addRegularFile) | +| `addRegularFile(file, zipSource)` | Обработка обычного файла (upload → parse → status) | +| `finalizeUpload()` | Завершение цикла загрузки | +| `onFilesSelected(newFiles)` | **Оркестратор** загрузки | + +**Критичные детали `uploadFile`**: +- `.doc` → конвертация в `.docx` через `CONVERT_URL` перед загрузкой +- XHR с `timeout = 180000` (3 минуты для больших PDF) +- `onProgress(pct)` — callback с процентом +- Дедупликация по ключу `(zip_source, name)` +- При совпадении имён: `confirm()` — перезаписать/пропустить + +### 4. `groups.js` — Карточки групп + +| Функция | Назначение | +|---------|-----------| +| `loadGroupsAction()` | fetch `/api/groups` → state.groups → renderGroups | +| `renderGroups(state)` | Пересобрать ВСЕ карточки в #diffBody | +| `renderGroupCard(g, gi)` | HTML карточки необработанной группы | +| `renderGroupCardDone(g, gi)` | HTML карточки обработанной группы | +| `renderUnresolvedCard(g)` | HTML карточки нераспознанных | + +### 5. `compare.js` — SSE-сравнение + +| Функция | Назначение | +|---------|-----------| +| `applyCompareEvent(sections, event)` | Чистая: SSE-событие → мутация sections | +| `renderCompareSectionHeader(sec)` | Чистая: заголовок секции | +| `renderCompareOpsTable(ops)` | Чистая: таблица ADD/UPDATE/DELETE | +| `renderCompareSectionBody(sec)` | Чистая: тело секции | +| `startCompareSSE(url, container, statusEl, callbacks)` | **Унифицированный** жизненный цикл SSE | + +**Критичные детали SSE**: +- `EventSource` → события: `extract_start`, `llm_done`, `applied`, `extract_error`, `apply_error` +- Только ОДНО сравнение одновременно (`state._activeCompare`) +- Все кнопки блокируются на время сравнения +- Таймер обновляет statusEl каждые 200мс + +### 6. `app.js` — Оркестратор + +| Функция | Назначение | +|---------|-----------| +| `render(state)` | Главная функция рендеринга → `renderFiles` + `renderStepper` | +| `renderStepper(state)` | Рендер степпера из `state.ui.steps` | +| `stepDone(id)` / `stepActive(id)` | Мутация шагов | +| `resetStepper(fromId)` | Сброс шагов | +| `showClassifyBtn()` | Создать/показать кнопку классификации | +| `runClassify()` | Классификация (sync/async + poll каждые 2с) | +| `runCompareForGroup(gi, btn)` | Сравнение группы (apply-groups → SSE) | +| `llmBtn` handler | Общее сравнение через SSE | + +## HTML-структура (site/templates/index.html) + +```html + +
+ + +