# 2026-07-31 — Сессия (v1.1.57 → v1.2.1) ## Контекст Обсуждение архитектуры: унификация ручного и сценарного режимов, гибкие ссылки на инстансы, новый UI редактора сценариев. ## Agent consultations ### Промпт для Opus Составлен `DOCS/opus-architecture-prompt.md` — полное описание проекта, дублирование CREATE-флоу, ограничение instance_map, вопросы по архитектуре. ### Ответ Opus — 19 уточняющих вопросов **A. Единый executor (operations/executor.py)** 1. Граница ответственности: executor делает всё до /run, поллинг — снаружи? 2. Обработка ошибок: исключения с типом или dict {ok, error, failed_step}? 3. Трекер: вызывать tracker_add внутри executor для create? Нужны ли сценарные инстансы в трекере? 4. _finish_op: остаётся в api_test.py или выносим в общий модуль поллинга? **B. Гибкие ссылки на инстансы** 5. Приоритет резолвинга: instance_uid > instance_ref > (для create — новый) 6. Где хранить output→uid: в памяти (instance_map) или в scenario_runs.instance_bindings? 7. Валидация _validate_steps: проверять ссылки на output, уникальность, обязательность 8. Обратная совместимость: поддерживать старый формат (service_id) или мигрировать? **C. Схема БД** 9. Формат в steps JSONB без изменений схемы? (подтверждено) 10. Использовать существующую колонку scenario_runs.instance_bindings? **D. UI редактора** 11. Модал vs inline — рекомендация: модал на весь экран 12. Переиспользовать renderParamRow из operations.js в общем модуле? 13. Pre-fill параметров: авто или по кнопке? 14. Формат параметров: символические имена в БД (как сейчас) или иное? **E. Оптимизация** 15. Вынести _find_uid/_uid_from_location в общий utils 16. Механизм восстановления зависших scenario_runs? 17. Ограничение lock_check: только один RUNNING сценарий — осознанное? 18. _op_results в памяти → мигрировать в БД/Redis? 19. Приоритет: бэкенд или UI редактора в первую очередь? ## Ключевые решения (отвечено DeepSeek) **A. Executor:** - A1. Граница: до /run включительно, без поллинга. Возвращает {instance_uid, op_uid, display_name} - A2. Ошибки: dict {ok, error, failed_step}, не исключения - A3. Трекер: да, tracker_add внутри executor для всех create - A4. _finish_op: оставить в api_test.py, цикл поллинга → operations/poll.py **B. Ссылки:** - B5. Приоритет: instance_uid > instance_ref > новый create - B6. Хранение: память + scenario_runs.instance_bindings - B7. Валидация: проверять ссылки, уникальность, обязательность - B8. Совместимость: оба формата, без миграции, fallback на service_id **C. БД:** - C9. Без изменений схемы - C10. Использовать instance_bindings **D. UI:** - D11. Модал на весь экран - D12. Общий модуль params-render.js - D13. Авто pre-fill, все параметры с defaults - D14. Символические имена в БД **E. Оптимизация:** - E15. _find_uid → api/utils.py - E16. Startup check: TIMEOUT для зависших >1ч - E17. lock_check оставить - E18. _op_results не в scope - E19. Порядок: executor → формат → api_test/scenario → UI ### Финальный план (Опус, утверждён) Сохранён в `DOCS/opus-plan-2026-07-31.md`. Ветка: `opus-architecture-2026-07-31`. **4 фазы, 13 шагов:** **Фаза 1 — Общие модули:** - `api/utils.py` (NEW) — find_uid(), uid_from_location() - `operations/poll.py` (NEW) — poll_until_done() - `operations/executor.py` (NEW) — execute_operation() до /run, без поллинга **Фаза 2 — Формат шагов:** - `routes/api_scenario_defs.py` — _validate_steps с output/instance_ref/instance_uid - `operations/scenario.py` — резолвинг instance_uid > instance_ref > service_id **Фаза 3 — Миграция вызывающих:** - `routes/api_test.py` — CMDB delete early return, остальное через executor - `operations/scenario.py` — через executor + poll_until_done - `db/init_db.py` — startup cleanup зависших scenario_runs **Фаза 4 — UI редактора:** - `static/js/params-render.js` (NEW) — общий рендер параметров - `static/js/operations.js` — использовать params-render.js - `static/js/scenario-form.js` — модальный редактор - `templates/index.html` — разметка модала ### CMDB delete Жёсткое удаление через `DELETE cmdb-api.deck.nubes.ru/instances/{uid}` (без авторизации). Нужно для недосозданных инстансов (not created). Остаётся в api_test.py, не в executor. Добавлено после переписки с Георгием Родионовым 29.07.2026. ## Реализация (v1.2.0 — v1.2.1) ### v1.2.0 — Unified executor + flexible refs **Новые файлы:** api/utils.py, operations/poll.py, operations/executor.py, static/js/params-render.js **Изменено:** api_test.py (через executor + poll), scenario.py (output/ref/bindings), api_scenario_defs.py (_validate_steps), init_db.py (cleanup), operations.js (→ params-render), index.html (load order) - Дублирование CREATE-флоу устранено: ручной и сценарный → один executor - instance_uid > instance_ref > service_id (гибкие ссылки) - Общий поллинг poll_until_done() - +254 / −277 строк (меньше кода) ### v1.2.1 — Opus review fixes **Критическое:** tracker_add внутрь executor (защита от сирот, A3) **Исправлено:** labelCls в renderMapFixedRow, descr с контекстом, порядок скриптов **5 файлов:** executor.py, api_test.py, scenario.py, params-render.js, app.py ### Проверка в поде (v1.2.1) - Все 4 новых файла на месте - 11 JS → 200, порядок правильный - Schema OK, seed OK, API отвечает ## Что дальше **Фаза 4 — модальный редактор сценариев (✅ v1.2.2-v1.2.4):** - ✅ Модал с дропдаунами сервисов/операций - ✅ output/instance_ref с облачными инстансами - ✅ Кнопки CRUD крупнее, справка «📖 Как заполнять» ### v1.2.16 — instance_meta JSONB После каждого прогона сохраняется полная информация об инстансе (GET /instances/{uid}). Все поля кроме instanceUid/displayName/svc/serviceId/explainedStatus. ## Идеи на будущее (НЕ ДЕЛАТЬ, обдумать) **Context snapshot:** сохранять снапшот ВСЕХ инстансов пользователя на момент запуска (instanceUid, displayName, serviceId, svc, explainedStatus, specification). При анализе FAIL — видеть контекст: «было 3 running Болванки, возможно конфликт ресурсов». Хранить в `runs.context_snapshot JSONB` и `scenario_runs.context_snapshot JSONB`. Данные обезличенные, не гигабайты. Отложено до реальной необходимости. --- ## Аудит безопасности GPT-5.3-Codex (2026-07-31) Проведён полный code review 29 файлов (~6000 строк). Найдено 11 проблем. Результаты зафиксированы в DOCS/ARCHITECTURE.md (раздел 8). ### КРИТИЧЕСКИЕ (исправлены) 1. **XSS через params в scenario-list.js:119** — k/v параметров в innerHTML без `_esc`. Stored XSS через БД сценариев. → v1.2.19 2. **JS injection в onclick** (scenario-list.js:126-128) — `def.name` в `'...'` без JS-escape. `_esc` не экранирует `'` → разрыв строки. → v1.2.19 3. **Гонка `_op_results`** (api_test.py:264-280) — dict без lock, читается/пишется/чистится из нескольких потоков. → v1.2.20: `threading.Lock()` + `pop(k, None)` 4. **Неатомарный lock сценариев** (scenario_defs.py + api_scenario_run.py) — `lock_check` (SELECT) и `INSERT RUNNING` разделены. → v1.2.20: `pg_try_advisory_lock` ### СРЕДНИЕ (исправлены) 5. **Lost update трекера** (tracker.py) — `_locked_read` + `_locked_write` в разных lock. → v1.2.19: `_atomic_update()` под одним lock 6. **Зависание UI поллинга** (scenario-list.js:211) — пустой catch, `busy` не сбрасывается. → v1.2.19: счётчик ошибок + `stopScenarioPoll` + `busy=false` 7. **`has_target` не проверяется** (api_scenario_defs.py:58) — вычисляется и игнорируется. → v1.2.19: явная проверка 8. **`_ensure_schema` silent** (pool.py:64) — `except Exception: pass`. → v1.2.20: `traceback.print_exc()` ### ПОТЕНЦИАЛЬНЫЕ (исправлены) 9. **Stale async в редакторе** (scenario-form.js) — `loadStepParams` после `renderEditor` может перезаписать новый DOM. → v1.2.20: `_renderGen` generation token 10. **validate-cfs хрупкий** (terraform.py:250-254) — фильтрация по тексту исключения. → v1.2.20: явный `except json.JSONDecodeError` ### НЕ ИСПРАВЛЕНО (архитектурное ограничение) 11. **In-memory `_op_results` на воркер** — не shared между gunicorn-воркерами. Статус иногда читается из API fallback. Решение: Redis/БД для статусов. Отложено — низкая вероятность проблемы на практике (2 воркера, stickiness). --- ## Повторный аудит Codex (2026-07-31, вторая итерация) Codex проверил исправления и нашёл **критические ошибки в моих же фиксах**: ### ОШИБКА AI #1: Advisory lock сломан (v1.2.20) **Что я сделал:** `lock_check()` брал `pg_try_advisory_lock` на соединении `conn1`, возвращал `True`, и `conn1` уходил обратно в пул. `unlock_scenario()` вызывал `get_conn()` → получал `conn2` (другое соединение!) → unlock на `conn2` не снимал lock с `conn1`. Плюс ранние `return` в `api_scenario_run.py` после успешного `lock_check` вообще не вызывали unlock. **Почему ошибся:** не учёл что PostgreSQL advisory lock привязан к сессии (соединению), а соединения возвращаются в пул. Передача соединения между `lock_check` и потоком сценария требовала бы сложной оркестрации. **Как исправлено (v1.2.21):** заменён на **partial unique index** на уровне БД: ```sql CREATE UNIQUE INDEX idx_one_running ON scenario_runs (client_id, stand) WHERE status = 'RUNNING'; ``` Теперь `INSERT INTO scenario_runs ... status='RUNNING'` сам становится атомарной проверкой — вторая вставка получает unique violation → 409 без гонок. `lock_check` возвращён к простому SELECT (быстрая предпроверка для красивого 409). `unlock_scenario` удалён полностью. `try/finally` из `run_scenario` убран. ### ОШИБКА AI #2: escName не экранирует `"` (v1.2.19) **Что я сделал:** `def.name.replace(/\\/g,'\\\\').replace(/'/g,"\\'")` — экранировал `\` и `'` для JS-строки, но забыл `"` для HTML-атрибута `onclick="..."`. **Почему ошибся:** фокусировался только на JS-контексте (строка в `'...'`), не учёл что она внутри HTML-атрибута в `"..."`. **Как исправлено (v1.2.21):** добавлено `.replace(/"/g,'"')` в `escName`. ### НОВАЯ находка Codex: instances.js:78 `instanceUid` в `onclick="toggleInstance('${i.instanceUid}')"` — теоретически уязвим, но на практике UUID всегда `[a-f0-9-]+` → безопасен. Отмечен как низкий риск, исправление не требуется. ### ИТОГ | # | Слой | Статус | |---|------|--------| | Advisory lock | Python | ❌ СЛОМАН → ✅ partial unique index | | escName `"` | JS | ❌ Неполный → ✅ добавлен `"` | | instances.js onclick | JS | ⚠️ Низкий риск, UUID безопасен | | `lock_check` fallback | Python | ✅ `True`→`False` при ошибке БД |