From 3ddd3bfbd065007cb3f549d0cf2fa4d0c7a1f898 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CNaeel=E2=80=9D?= Date: Tue, 14 Jul 2026 22:09:34 +0400 Subject: [PATCH] docs: document contracts-flask VM/Flask architecture as of 2026-07-14 --- History/flask-vm-architecture-2026-07-14.md | 236 ++++++++++++++++++++ 1 file changed, 236 insertions(+) create mode 100644 History/flask-vm-architecture-2026-07-14.md 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 + +
...
+ + +
+ +
+ +
+ + +
+ + + + + + + + + +
+ + +``` + +## Flask (site/app.py) + +```python +# Тонкий прокси, ~90 строк +@app.route("/") → render_template("index.html") +@app.route("/chat") → прокси на ВМ (LLM API) +@app.route("/api/prompts") → прокси на ВМ +@app.route("/api/prompts/list") → прокси на ВМ +@app.route("/api/prompts/save") → прокси на ВМ +@app.route("/api/prompts/activate") → прокси на ВМ +@app.route("/architect") → render_template("architect.html") + +VM_API = "https://contracts.kube5s.ru" +LLM_URL = "https://api.aillm.ru/v1/chat/completions" +``` + +## ВМ (deploy/convert_server.py) + +Python HTTP-сервер (не Flask!) на `http.server` + `ThreadingMixIn`. Все эндпоинты перечислены в общей схеме выше. + +**Критичные модули ВМ**: +- `deploy/db/` — PostgreSQL (connection.py, documents.py, supplements.py, spec_current.py, prompts.py) +- `deploy/compare/` — upload.py, unzip.py, process.py (SSE) +- `deploy/convert_doc.py` — libreoffice .doc → .docx (stdin → stdout) +- `deploy/classify_worker.py` — асинхронная классификация +- `deploy/llm_prompt.py` — построение промптов + +## Pipeline (степпер) + +``` +○ Загрузка ──→ ⏳ Загрузка ──→ ✓ Загрузка + └─→ ⏳ Классификация ──→ ✓ Классификация + └─→ ⏳ Группировка ──→ ✓ Группировка + └─→ ⏳ Сравнение ──→ ✓ Сравнение +``` + +Сброс: при добавлении новых файлов → `resetStepper('stepUpload')` сбрасывает всё начиная с загрузки. + +## Что нужно перенести с ВМ на Flask + +Все API-эндпоинты, которые сейчас на `deploy/convert_server.py`, должны стать Flask-роутами в `site/app.py`: + +| Эндпоинт | Метод | Назначение | +|----------|-------|-----------| +| `/upload` | POST | Загрузка файла → парсинг → БД | +| `/convert-doc` | POST | .doc → .docx (libreoffice) | +| `/unzip-upload` | POST | Распаковка ZIP | +| `/process-v2` | GET | SSE сравнения | +| `/api/classify-batch` | POST | Классификация батча | +| `/api/batch-progress` | GET | Прогресс классификации | +| `/api/groups` | GET | Группы по batch_id | +| `/api/documents/:id` | GET | Инфо о документе | +| `/api/supplements` | GET | Список supplement'ов | +| `/api/sync` | POST | Синхронизация БД | +| `/api/apply-groups` | POST | Применить группы | +| `/api/spec-current` | GET | Текущая спецификация | +| `/api/cleanup` | POST | Очистка старых записей | + +Плюс перенос БД-логики из `deploy/db/` во Flask-приложение и конвертера `convert_doc.py`.