# Финальный план рефакторинга — store + render Согласовано: DeepSeek + Opus. Все открытые вопросы закрыты. ## Архитектурное решение **Патерн:** единый `state` объект + одна `render(state)` + actions мутируют state. ``` action → мутация state → render(state) ``` - Асинхронные операции (upload, classify, compare-SSE) — actions, пишут в state - Синхронные трансформации (group, applyParseResult) — чистые функции - DOM — проекция state, никто не читает из DOM - `console.log(state)` показывает всё ## Структура state ```javascript state = { batchId, contractId, files: [{ id, name, size, status: { kind, pct, text, count }, parse, classify, supp_id, detailOpen }], groups: [{ contractNumber, counterparty, documents, unresolved, compare: { status, sections: { [suppId]: { header, body, ops } }, totalTime, collapsed } }], compareAll: { status, sections, totalTime }, ui: { steps: { upload, classify, groups, compare } }, _activeCompare: { es, timer } } ``` ## Фазы ### Фаза 0 — Каркас - Ввести `state`, `render(state)` (пока вызывает существующие рендеры) - Соглашение: мутировал → `render()` ### Фаза 1 — Файлы - `fileQueue` → `state.files` - `renderTable` → `renderFiles(state)` - `fileInput change` разбит: - `onFilesSelected(files)` — оркестратор (10-15 строк) - `reconcileSelection(newFiles)` — чистая мутация массива - `addZipFiles(file)` — async-action - `addRegularFile(file)` — async-action - `applyParseResult(entry, parsed)` — чистая функция (убирает дубль zip/обычной ветки) - `finalizeUpload()` — refreshSupps + stepDone + syncDB + кнопки - `status` → структура `{ kind, pct, text, count }`, HTML через `statusToHTML(status)` - Убрать `renderTable()` из обработчиков — только `render()` в конце action ### Фаза 2 — Группы - `loadGroups` → `loadGroupsAction()` (fetch → state.groups) + `renderGroups(state)` - `buildGroupCard` → `renderGroupCard(group)` — чистая HTML-функция - **`markGroupDone` удалить** — готовность = `state.groups[gi].compare.status === 'done'` - Убрать `window._groupsData` - `renderGroups` пересобирает ВСЕ карточки целиком ### Фаза 3 — Сравнение/SSE - Унификация через общие функции: - `applyCompareEvent(target, event)` — чистая мутация SSE-события в target - `startCompareSSE(url, target, { onDone })` — жизненный цикл SSE - `renderCompareSections(sections)` — общий рендер таблиц ops - `runCompareForGroup` → тонкая обёртка над `startCompareSSE` - `llmBtn` → тонкая обёртка над `startCompareSSE` - ~160 строк дублирования → ~60 строк общих + 2 вызова ### Фаза 4 — Чистка - `contractId`, `batchId` → `state` - Степпер → `renderStepper(state)` - Промпты, чат, showText — вне scope (отдельная итерация) ## Верификация 1. После каждой правки: `node -c` на app.js + app_utils.js 2. После Фазы 2: node-тесты на чистые функции (`applyParseResult`, `statusToHTML`, `renderGroupCard`) 3. Ручной прогон upload→classify→group→compare после git push 4. Регресс: «Готово» не перезапускается, параллельные сравнения заблокированы, группы не затираются ## Релевантные файлы - `contractor/deploy/app.js` — все фазы (~1040 строк) - `contractor/deploy/app_utils.js` — removeFile/moveUp/moveDown (Фаза 1) - `contractor/index.cfm` — DOM-скелет, версия v1.0.177, `?v=` для cache bust