From bcacb1fb3318032056e92a0312f4355ef3354da0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CNaeel=E2=80=9D?= Date: Fri, 31 Jul 2026 08:39:19 +0400 Subject: [PATCH] DOCS+HISTORY: Opus architecture prompt, Q&A, implementation plan, session logs --- DOCS/opus-architecture-prompt.md | 213 ++++++++++++++++++++++++++++++ DOCS/opus-plan-2026-07-31.md | 138 +++++++++++++++++++ DOCS/opus-questions-2026-07-31.md | 61 +++++++++ HISTORY/2026-07-30-session.md | 103 +++++++++++++++ HISTORY/2026-07-31-session.md | 71 ++++++++++ 5 files changed, 586 insertions(+) create mode 100644 DOCS/opus-architecture-prompt.md create mode 100644 DOCS/opus-plan-2026-07-31.md create mode 100644 DOCS/opus-questions-2026-07-31.md create mode 100644 HISTORY/2026-07-30-session.md create mode 100644 HISTORY/2026-07-31-session.md diff --git a/DOCS/opus-architecture-prompt.md b/DOCS/opus-architecture-prompt.md new file mode 100644 index 0000000..ba83819 --- /dev/null +++ b/DOCS/opus-architecture-prompt.md @@ -0,0 +1,213 @@ +# Prompt for Opus — Architecture: Unified Scenario System + +**Ты можешь задавать уточняющие вопросы.** Если чего-то не хватает для принятия решения — спроси. Я (DeepSeek V4 Pro, ассистент Naael) отвечу. + +## ПОРЯДОК ЧТЕНИЯ (обязательно прочитай в этом порядке) + +**Шаг 1 — понять что такое Nubes и как работает API:** +`/home/naeel/nubes/autotest/DOCS/terraform-operations-full-logic.md` + +**Шаг 2 — увидеть дублирование своими глазами (самое важное):** +Смотри раздел «КЛЮЧЕВОЙ КОД» ниже — там оба CREATE-флоу (ручной и сценарный) бок о бок. + +**Шаг 3 — понять текущую архитектуру кода:** +- `/home/naeel/nubes/autotest/app-autotest/site/routes/api_test.py` (строки 175-270 — ручной CREATE) +- `/home/naeel/nubes/autotest/app-autotest/site/operations/scenario.py` (строки 85-210 — сценарный CREATE) +- `/home/naeel/nubes/autotest/app-autotest/site/operations/terraform.py` (общая send_params_terraform) +- `/home/naeel/nubes/autotest/app-autotest/site/routes/api_scenario_defs.py` (CRUD определений) +- `/home/naeel/nubes/autotest/app-autotest/site/db/scenario_defs.py` (SQL-функции) +- `/home/naeel/nubes/autotest/app-autotest/site/db/init_db.py` (схема БД — найди scenario_definitions) + +**Шаг 4 — понять фронтенд:** +- `/home/naeel/nubes/autotest/app-autotest/site/templates/index.html` (вся страница, CSS, Jinja2) +- `/home/naeel/nubes/autotest/app-autotest/site/static/js/operations.js` (showParams, executeOp — как запускается ручная операция) +- `/home/naeel/nubes/autotest/app-autotest/site/static/js/scenario-form.js` (renderEditor, saveScenario — текущий редактор) + +**НЕ читай — это легаси/устарело:** +- `DOCS/ARCHITECTURE.legacy.md`, `DOCS/ARCHITECTURE.md` +- `DOCS/sonnet-*.md` — переписки с другим AI +- `DOCS/gpt56-*`, `DOCS/questions-to-sol.md`, `DOCS/sol-*` +- `HISTORY/` — история сессий +- `development/` — HAR-файлы + +--- + +## Что это за приложение + +**Nubes** — облачная платформа (IaaS/PaaS). ~30 типов сервисов: VM, K8s, PostgreSQL, Redis, S3, Kafka, ClickHouse и др. Каждый сервис имеет операции: create, modify, delete, suspend, resume, redeploy. + +**Autotest** — Flask-приложение, которое делает то же самое что личный кабинет Nubes, но через REST API и автоматически. Два режима: + +1. **Ручной** — пользователь выбирает сервис → видит список autotest-инстансов → кликает → выбирает операцию (modify/delete/suspend/...) → заполняет параметры → запускает. Или жмёт «+ Создать» для CREATE. + +2. **Сценарии** — пользователь создаёт последовательность шагов (create → modify → delete), сохраняет в БД, запускает одним кликом. Каждый шаг = атомарная операция над инстансом. + +### Как работает API Nubes (CREATE) + +``` +1. POST /instances body={serviceId, displayName, descr} → 201, Location: ./UUID +2. POST /instanceOperations body={instanceUid, operation:"create"} → opUid +3. GET /instanceOperations/{opUid}?fields=cfsParams → параметры с defaults +4. POST /instanceOperationCfsParams (×N — все параметры) +5. GET /instanceOperations/{opUid}/validate-cfs → валидация +6. POST /instanceOperations/{opUid}/run → запуск +7. Поллинг GET /instanceOperations/{opUid}?fields=dtFinish,isSuccessful,... до dtFinish +``` + +MODIFY/DELETE/SUSPEND/RESUME: шаг 1 пропускается, шаг 2 с `{instanceUid, svcOperationId, operation}` (с svcOperationId!). + +### БД (PostgreSQL, одна на все gunicorn-воркеры) + +```sql +scenario_definitions (id, client_id, stand, name, steps JSONB, version, is_active, ...) +scenario_runs (id, scenario_name, status, current_step, total_steps, error_log, app_version, ...) +runs (id, svc_id, op_name, instance_uid, status, duration_sec, params JSONB, stages JSONB, ...) +``` + +--- + +## КЛЮЧЕВОЙ КОД — ДУБЛИРОВАНИЕ CREATE-ФЛОУ + +### Ручной режим (api_test.py, функция api_test) + +```python +if op_name == "create": + display_name = _unique_display_name(client, display_name) + descr = f"created by autotest v{current_app.config.get('VERSION', '')}" + payload = {"serviceId": svc_id, "displayName": display_name, "descr": descr} + resp = client.post("/instances", payload) + instance_uid = resp.get("instanceUid") or _find_uid(resp) or _uid_from_location(resp.get("_location", "")) + if not instance_uid: + return jsonify({"status": "FAIL", "error": "Не удалось получить instanceUid"}), 500 + # ---------------------------------------------------------- + op_payload = {"instanceUid": instance_uid, "operation": "create"} + op_resp = client.post("/instanceOperations", op_payload) + op_uid = _find_uid(op_resp) or _uid_from_location(op_resp.get("_location", "")) + if not op_uid: + return jsonify({"status": "FAIL", "error": "Не удалось получить opUid"}), 500 + # ---------------------------------------------------------- + send_params_terraform(client, op_uid, params) # ← ОБЩАЯ ФУНКЦИЯ + client.post(f"/instanceOperations/{op_uid}/run") + # ---------------------------------------------------------- + threading.Thread(target=_finish_op, args=(client, op_uid, instance_uid, ...), daemon=True).start() + return jsonify({"status": "RUNNING", "opUid": op_uid, "instanceUid": instance_uid, ...}) +``` + +### Сценарий (scenario.py, функция run_scenario) + +```python +if op_name == "create": + display_name = f"{AUTOTEST_PREFIX}{scenario_name}-{uuid.uuid4().hex[:6]}" + descr = f"scenario {scenario_name} step {step_num}" + payload = {"serviceId": svc_id, "displayName": display_name, "descr": descr} + resp = client.post("/instances", payload) + instance_uid = resp.get("instanceUid") or _find_uid(resp) or _uid_from_location(resp.get("_location", "")) + if not instance_uid: + raise RuntimeError(f"CREATE: no instanceUid in response: ...") + instance_map[svc_id] = instance_uid + # ---------------------------------------------------------- + op_payload = {"instanceUid": instance_uid, "operation": op_name} # без svcOperationId + op_resp = client.post("/instanceOperations", op_payload) + op_uid = op_resp.get("instanceOperationUid") or _find_uid(op_resp) or _uid_from_location(...) + if not op_uid: + raise RuntimeError(f"No opUid in response: ...") + # ---------------------------------------------------------- + from operations.terraform import send_params_terraform + send_params_terraform(client, op_uid, resolved_params) # ← ТА ЖЕ ФУНКЦИЯ + client.post(f"/instanceOperations/{op_uid}/run") + # ---------------------------------------------------------- + # ДАЛЕЕ: синхронный while-поллинг (1800s таймаут), save_run(), _save_scenario_run() +``` + +**Эти два блока делают ОДНО И ТО ЖЕ.** Различаются только: +- displayName (autotest-xxx vs autotest-scenario-xxx) +- Поллинг (async thread vs sync while) +- Сохранение (runs через _finish_op vs runs + scenario_runs) + +--- + +## КЛЮЧЕВОЙ КОД — ОГРАНИЧЕНИЕ instance_map + +```python +# scenario.py, строка 91 +instance_map = {} # service_id → instanceUid + +# Шаг CREATE: +instance_map[svc_id] = instance_uid + +# Шаг НЕ-CREATE: +instance_uid = instance_map.get(svc_id) +if not instance_uid: + raise RuntimeError(f"No instance for service_id {svc_id} — need CREATE first") +``` + +**Проблема:** привязано к `service_id`. Нельзя: +- Два инстанса одного сервиса в сценарии (второй CREATE перезапишет первый) +- Сослаться на инстанс из другого сценария +- Использовать существующий инстанс по UUID + +--- + +## Что нужно спроектировать + +### 1. Единый executor (operations/executor.py) + +```python +def execute_operation(client, service_id, operation, instance_uid_or_none, params, display_name=None) -> dict: + """ + Единая точка входа для ручного и сценарного запуска. + Возвращает {"instance_uid": ..., "op_uid": ..., "display_name": ...} + """ +``` + +`api_test.py` и `scenario.py` вызывают эту функцию. Поллинг и save_run — снаружи (у каждого свой). + +### 2. Гибкие ссылки на инстансы + +Новый формат шага в `scenario_definitions.steps`: + +```json +[ + {"service_id": 1, "operation": "create", "params": {...}, "output": "d1"}, + {"service_id": 1, "operation": "modify", "params": {...}, "instance_ref": "d1"}, + {"service_id": 90, "operation": "create", "params": {...}, "output": "pg"}, + {"service_id": 1, "operation": "delete", "params": {}, "instance_uid": "UUID-явно"} +] +``` + +Резолвинг на бэкенде: `output` → сохраняем в словарь `{name: instance_uid}`. `instance_ref` → берём из словаря. `instance_uid` → используем как есть. + +### 3. UI редактора сценариев + +**Текущее:** inline-форма в `scenario-body`, сервис = numeric input, операция = text input (БАГ), параметры = key:value строки. + +**Нужно:** полноценный редактор с: +- Выпадающий список сервисов (`GET /api/services`) +- Выпадающий список операций (`GET /api/operations/{svcId}`) +- Параметры с автоподгрузкой из `/api/params/{svcOpId}`: name, type, default, valueList, dataDescriptor +- Поле `output` для create-шагов (имя для ссылок) +- Дропдаун `instance_ref` для не-create шагов (output-имена предыдущих шагов) +- `[↑][↓]` для перестановки шагов + +**Вопросы:** +1. Модальное окно или раскрытие внутри `scenario-body`? Аргументируй. +2. Как показывать параметры: таблица (name|type|default|value) или упрощённо (key=value)? +3. pre-fill параметров при смене операции — авто или по кнопке? +4. Куда скроллится страница при открытии редактора? + +### 4. Документация для чтения (кроме кода) + +- `/home/naeel/nubes/autotest/DOCS/terraform-operations-full-logic.md` ← обязательно +- `/home/naeel/nubes/autotest/DOCS/ARCHITECTURE-FULL.md` ← общая архитектура +- `/home/naeel/nubes/autotest/DOCS/architecture-final.md` ← финальная версия + +--- + +## Вопросы + +1. **Unified executor:** сигнатура, возврат, обработка ошибок на каждом шаге CREATE-флоу +2. **Формат шагов:** как парсить `output`/`instance_ref`/`instance_uid`, валидация, резолвинг на бэкенде +3. **Схема БД:** нужны ли изменения в `scenario_definitions.steps`? Новая колонка для output-блоков? +4. **UI редактора:** модал vs inline, компоновка блоков, автоподгрузка параметров, скролл +5. **Миграция:** что делать с существующим dummy_test при смене формата шагов +6. **Порядок:** в какой последовательности реализовывать diff --git a/DOCS/opus-plan-2026-07-31.md b/DOCS/opus-plan-2026-07-31.md new file mode 100644 index 0000000..cd7f11c --- /dev/null +++ b/DOCS/opus-plan-2026-07-31.md @@ -0,0 +1,138 @@ +# Opus Implementation Plan — Унификация сценариев autotest (2026-07-31) + +> Основано на DOCS/opus-questions-2026-07-31.md (19 Q+A) и ревью пользователя. + +## Цель +Устранить дублирование CREATE-флоу (ручной api_test.py vs сценарный scenario.py), +ввести единый execute_operation, гибкие ссылки на инстансы (output/instance_ref/instance_uid), +модальный редактор сценариев с богатым рендером параметров. + +## Порядок реализации (E19): бэкенд → формат → миграция вызовов → UI + +--- + +## Фаза 1 — Общие модули (фундамент) + +### 1. api/utils.py (NEW) +- `find_uid(resp)` — поиск UUID по ключам: instanceOperationUid → instanceUid → uid. +- `uid_from_location(loc)` — UUID из Location-заголовка. +- Заменяет 2 дубля (в api_test.py и scenario.py — сейчас разные реализации). + +### 2. operations/poll.py (NEW) +- `poll_until_done(client, op_uid, timeout=1800)` → dict {status, is_successful, + error_log, stages, duration, svc}. +- Критерий завершения: dtFinish != "" (НЕ isInProgress). +- Общий цикл для async (_finish_op) и sync (run_scenario). Убирает хардкод 1800s в 3 местах. + +### 3. operations/executor.py (NEW) +- `execute_operation(client, service_id, operation, instance_uid, params, + svc_op_id=None, display_name=None)` → dict {ok, error, failed_step, + instance_uid, op_uid, display_name}. +- Делает всё ДО /run включительно: POST /instances (только create) → + POST /instanceOperations → send_params_terraform → POST /run. НЕ поллит. +- Ветвится по operation=="create" ВНУТРИ: + - create: POST /instances → op_payload {instanceUid, operation}; svc_op_id игнорируется. + - non-create: op_payload {instanceUid, svcOperationId, operation}. +- failed_step ∈ {instances, instanceOperations, params, run}. +- tracker_add вызывается ВНУТРИ executor для всех create (E3) — сразу после + получения instanceUid, до params/run (защита от сирот). +- Переиспользует существующую send_params_terraform (operations/terraform.py) как есть. + +--- + +## Фаза 2 — Формат шагов и резолвинг (depends Фаза 1) + +### 4. routes/api_scenario_defs.py — _validate_steps +Новые опциональные ключи шага: output, instance_ref, instance_uid. +Валидация: +- output уникален в пределах сценария. +- instance_ref ссылается на output из ПРЕДЫДУЩИХ шагов. +- для не-create шага обязателен instance_ref | instance_uid | (fallback старый формат). +- Старый формат (только service_id) продолжает проходить валидацию. + +### 5. operations/scenario.py — резолвинг инстанса +Приоритет: instance_uid > instance_ref > instance_map[service_id] (fallback). +После create: bindings[output] = instance_uid — в память И в +scenario_runs.instance_bindings (колонка JSONB уже есть, DEFAULT '{}'). +Резолвинг во время выполнения — ТОЛЬКО из памяти (БД для наблюдаемости). + +--- + +## Фаза 3 — Миграция вызывающих (depends Фаза 1-2) + +### 6. routes/api_test.py +- CMDB delete ОСТАЁТСЯ как предпроверка ПЕРЕД executor: при cmdb_ok → early return + (opUid="cmdb-...", без /run, без поллинга). Иначе → execute_operation. +- Остальные операции: заменить inline флоу на execute_operation. +- _finish_op использует poll_until_done (save_run, _op_results, tracker_remove(delete) + остаются здесь). +- Импорт find_uid/uid_from_location из api/utils.py; удалить локальные копии. + +### 7. operations/scenario.py +- Заменить inline флоу на execute_operation. +- sync while-поллинг → poll_until_done. +- Удалить локальные _find_uid/_uid_from_location. + +### 8. db/init_db.py — startup cleanup (E16) +В init_db() добавить: +UPDATE scenario_runs SET status='TIMEOUT', error_log='worker restart' +WHERE status='RUNNING' AND created_at < NOW() - INTERVAL '1 hour'; +(init_db вызывается из pool._ensure_schema() раз на воркер, идемпотентно). + +--- + +## Фаза 4 — UI редактора (depends Фаза 2) + +### 9. static/js/params-render.js (NEW) +Вынести из operations.js: +- renderParamRow(p, allInst) — уже чистая, без глобалов. +- renderMapFixedRow(p, dfl) — чистая. +- collectParams(containerSelector='#params-form') — параметризовать контейнер + (единственная правка сигнатуры; сейчас хардкодит #params-form). +Глобалы AUTOTEST_PREFIX/currentSvcId/makeCreateDisplayName остаются в operations.js. +_esc (utils.js) и validateJson — общие глобалы. + +### 10. static/js/operations.js +Использовать общий params-render.js (удалить дубли рендера). + +### 11. static/js/scenario-form.js — модальный редактор +- Модал на весь экран (не inline scenario-body — параметры map-fixed слишком тесны). +- Дропдаун сервисов (GET /api/services). +- Дропдаун операций (GET /api/operations/{svcId}). +- Авто pre-fill параметров при смене операции (GET /api/params/{svcOpId}), + показать ВСЕ параметры с defaults + кнопка «Сбросить на defaults». +- Поле output для create-шагов. +- Дропдаун instance_ref для не-create (output'ы предыдущих шагов). +- Кнопки [↑][↓] перестановки шагов. +- В БД — символические имена параметров ({"durationMs":"5000"}), резолв в numeric ID в runtime. + +### 12. templates/index.html +Разметка модала + подключить params-render.js. + +### 13. app.py — bump VERSION. + +--- + +## Решения (зафиксировано) +- Схема БД без изменений (C9), только новые ключи в steps JSONB. +- Обе версии формата параллельно, без миграции данных (B8), fallback на service_id. +- _op_results в памяти — вне scope (E18). +- lock_check (один RUNNING сценарий) сохраняется (E17). +- instance_bindings (JSONB) — используется (C10). + +## Verification +1. py_compile для .py, node -c для .js. +2. Ручной CREATE через UI → инстанс создан, в трекере, запись в runs. +3. Ручной DELETE → CMDB-путь работает (early return). +4. Сценарий старый формат (seed dummy_test) → работает без изменений. +5. Сценарий новый формат: create output:d1 → modify instance_ref:d1 → + delete instance_ref:d1 → все OK, instance_bindings заполнен. +6. Два create одного сервиса с разными output → два разных инстанса. +7. Валидация: instance_ref на несуществующий output → ошибка при сохранении. +8. Startup-cleanup: зависший RUNNING >1ч → TIMEOUT после рестарта. + +## Файлы +NEW: operations/executor.py, operations/poll.py, api/utils.py, static/js/params-render.js +MOD: routes/api_test.py, operations/scenario.py, routes/api_scenario_defs.py, + db/init_db.py, static/js/scenario-form.js, static/js/operations.js, + templates/index.html, app.py (VERSION) diff --git a/DOCS/opus-questions-2026-07-31.md b/DOCS/opus-questions-2026-07-31.md new file mode 100644 index 0000000..d03846e --- /dev/null +++ b/DOCS/opus-questions-2026-07-31.md @@ -0,0 +1,61 @@ +# Opus Questions + Answers — 2026-07-31 + +## A. Единый executor (operations/executor.py) + +**A1. Граница executor: до /run включительно, без поллинга.** +Executor делает все шаги: POST /instances → POST /instanceOperations → send_params_terraform → /run. Возвращает `{instance_uid, op_uid, display_name}`. Поллинг — забота вызывающего (api_test.py: async thread, scenario.py: sync while). + +**A2. Ошибки: dict `{ok, error, failed_step}`.** +Не исключения. `failed_step` = "instances" | "instanceOperations" | "params" | "run". Оба вызывающих конвертируют в свой формат (jsonify / RuntimeError). + +**A3. Трекер: да, вызывать tracker_add внутри executor для всех create.** +Сценарные инстансы — такие же реальные инстансы в облаке, должны быть в трекере. Единообразие. + +**A4. _finish_op: оставить в api_test.py.** +Но вынести цикл поллинга в общий хелпер `poll_until_done(client, op_uid, timeout=1800)` в `operations/poll.py`, который используют и _finish_op (async), и run_scenario (sync). + +## B. Гибкие ссылки на инстансы + +**B5. Приоритет: да. instance_uid > instance_ref > новый create.** + +**B6. Хранение output→uid: и в памяти, и в БД.** +В памяти — словарь для быстрого резолвинга во время выполнения. В БД — `scenario_runs.instance_bindings` обновляется после каждого create-шага. При падении воркера видно что создалось. Но runner не зависит от БД для резолвинга (только память). + +**B7. Валидация: да на всё.** +`_validate_steps()` проверяет: (a) instance_ref ссылается на output из предыдущих шагов, (b) output уникален в пределах сценария, (c) для не-create шага обязателен один из instance_ref/instance_uid. + +**B8. Обратная совместимость: поддерживать оба формата, без миграции.** +Если шаг без output/instance_ref/instance_uid → fallback на старый instance_map[service_id]. Seed dummy_test работает как есть. Новые сценарии используют новый формат. + +## C. Схема БД + +**C9. Да, без изменений схемы.** Только новые опциональные ключи в JSONB-объектах шагов. + +**C10. Да, использовать instance_bindings.** После create: `bindings[output_name] = instance_uid`. Сохранять в БД после каждого шага. + +## D. UI редактора сценариев + +**D11. Модальное окно на весь экран.** Параметры с dataDescriptor (map-fixed) — много вложенных полей, inline в scenario-body слишком тесно. + +**D12. Да, вынести рендер параметров в общий модуль.** +`static/js/params-render.js`: `renderParamRow()`, `renderMapFixedRow()`, `collectParams()`. Используется и в operations.js, и в scenario-form.js. + +**D13. Pre-fill: авто при смене операции.** Показывать ВСЕ параметры с defaults, пользователь удаляет лишние или меняет значения. Кнопка «Сбросить на defaults». + +**D14. Символические имена в БД.** `{"durationMs": "5000"}` — человекочитаемо, стабильно. Резолвинг в numeric ID — в runtime. + +## E. Оптимизация + +**E15. Да, вынести в `api/utils.py`.** Обе реализации (_find_uid, _uid_from_location) унифицировать. + +**E16. Да, startup check.** При старте: `UPDATE scenario_runs SET status='TIMEOUT', error_log='worker restart' WHERE status='RUNNING' AND created_at < NOW() - INTERVAL '1 hour'`. Просто, без доп. инфраструктуры. + +**E17. Оставить lock_check.** Один сценарий за раз — безопасно. Можно ослабить позже. + +**E18. Не в scope.** fallback на API работает, потеря stages — minor. Отдельная задача. + +**E19. Порядок: бэкенд → UI.** +1. `operations/executor.py` + `operations/poll.py` + `api/utils.py` +2. Формат шагов (output/instance_ref) + валидация + резолвинг +3. Минимальные правки api_test.py и scenario.py (вызов executor) +4. UI редактор (модал + общий рендер параметров) diff --git a/HISTORY/2026-07-30-session.md b/HISTORY/2026-07-30-session.md new file mode 100644 index 0000000..444416a --- /dev/null +++ b/HISTORY/2026-07-30-session.md @@ -0,0 +1,103 @@ +# 2026-07-30 — Сессия (v1.1.51 → v1.1.55) + +## Контекст +Сценарий `dummy_test` падает на шаге CREATE: `no instanceUid in response`. Ручное создание Болванки через UI работает. + +## Найденные и исправленные баги + +### Баг #1 — no instanceUid in response (ЛОЖНАЯ ТРЕВОГА) +**Где:** `scenario.py:122` — `resp.get("instanceUid")` возвращал None. + +**Причина:** сценарий НЕ перезапускался после деплоя v1.1.52. UI показывал старый результат из БД от v1.1.51 (баг #3). + +**Проверка curl:** API отвечает корректно — 201, `Location: ./UUID`, тело `""`. `http_client.py` правильно парсит. + +**Добавлено в v1.1.52:** `_status` (HTTP-код) в результат `http_client.post()` для отладки. + +### Баг #2 — лишний svcOperationId для create (ИСПРАВЛЕН в v1.1.52) +**Где:** `scenario.py:127` + +Для `create` операция `/instanceOperations` не должна содержать `svcOperationId` (по спецификации Terraform). Было одинаково для всех операций, стало: +```python +if op_name == "create": + op_payload = {"instanceUid": instance_uid, "operation": op_name} +else: + op_payload = {"instanceUid": instance_uid, "svcOperationId": svc_op_id, "operation": op_name} +``` + +### Баг #3 — старые результаты сценариев после редеплоя (ИСПРАВЛЕН в v1.1.53) +**Где:** `api_scenario.py` + `app.js` + +**Симптом:** после редеплоя UI показывал результат сценария от СТАРОЙ версии (из БД), пользователь думал что новый код не работает. + +**Причина:** `/api/scenario/status` не возвращал `app_version`, фронтенд не фильтровал по версии. + +**Исправление:** +- `api_scenario.py`: добавлен `app_version` в SELECT обоих эндпоинтов +- `app.js`: `loadScenarios()` фильтрует историю по `window.APP.version` — показывает только запуски текущей версии + +### Баг #4 — нельзя удалять без suspend (ОБНАРУЖЕН) +DELETE после CREATE падает: «Невозможно выполнить операцию удаления услуги. Услуга не остановлена». +Нужно сначала suspend + 15 мин ожидания. **Вывод:** не использовать delete в сценариях. + +## Хронология + +### v1.1.52 — fix scenario create + http_client _status debug +- `scenario.py`: убран `svcOperationId` из create payload +- `http_client.py`: добавлен `{"_status": r.status_code}` в результат для отладки + +### v1.1.53 — filter scenario history by app_version +- `api_scenario.py`: `app_version` в SELECT +- `app.js`: фильтр `verHistory = srHistory.filter(r => r.app_version === curVer)` + +### v1.1.55 — split large files + CRUD scenario editor + +**Backend (5→7 файлов):** +- `api_test.py`: 622→479 строк, terraform-функции → `operations/terraform.py` (142) +- `api_scenario.py` → `api_scenario_run.py` (153) + `api_scenario_defs.py` (108) +- `db/scenario_defs.py`: + `client_id`, `stand`, `is_seed` в `list_definitions` +- `api_scenario_defs.py`: `_validate_steps()` — валидация шагов перед create/update +- `scenario.py`: импорт `send_params_terraform` из `operations/terraform.py` (вместо кросс-импорта из routes) + +**Frontend (1→10 файлов):** +- `app.js`: 554→16 (загрузчик) +- `utils.js` (45), `instances.js` (89), `operations.js` (249) +- `history.js` (35), `scenario-list.js` (90) +- CRUD: `scenario-form.js` (128), `scenario-create.js` (25, +clone), `scenario-edit.js` (12), `scenario-delete.js` (13) +- Кнопки [▶][✏][⎘][🗑], seed-сценарии только [▶][⎘] +- Inline-редактор: имя, шаги (service_id, operation, params), 409 conflict + +**Максимальный размер файла:** JS 249 строк, Python 479 строк. + +## Agent consultations + +### Вопрос к Sonnet: план CRUD-редактирования сценариев + +**Суть:** сейчас сценарии правятся только через `scenario_seed.yaml` → редеплой. Нужно редактирование через UI без редеплоя. + +**Ответ Sonnet (ключевые выводы):** + +1. **CRUD-эндпоинты УЖЕ реализованы** в `api_scenario.py` (я ошибался, они есть): + - `GET/POST /api/scenario/definitions` + - `GET/PUT/DELETE /api/scenario/definitions/` + - DB-функции в `scenario_defs.py` полностью готовы + +2. **Нужны только 3 мелкие правки бэкенда:** + - `list_definitions`: добавить `client_id`, `stand`, `is_seed` в SELECT + - `api_scenario.py`: `_validate_steps()` с проверкой service_id, operation, params + - `index.html`: `window.APP.services` (список сервисов для фронтенда) + +3. **Фронтенд — ~200 строк JS:** + - Кнопки [▶][✏][⎘][🗑] у каждого сценария + - Inline-редактор: имя, шаги (сервис▾, операция▾, параметры) + - Автоподгрузка операций при смене сервиса (`GET /api/operations/{svcId}`) + - Автозаполнение параметров при смене операции (`GET /api/params/{svcOpId}`) + - Оптимистичная блокировка (version → 409) + +4. **Безопасность:** изоляция по client_id+stand, seed-сценарии только [▶][⎘] + +## Отладка через kubectl +- SSH: `naeel@5.172.178.213` (ключ `secrets/id_ed25519.txt`) +- Неймспейс autotest: `01a2d5b2-4df4-4cfe-b98e-8dd3534a3bb5` +- Код в поде: `/var/www/site/` +- Проверка версии: `kubectl exec -n $NS deploy/pythonk8s -c app -- grep VERSION /var/www/site/app.py` diff --git a/HISTORY/2026-07-31-session.md b/HISTORY/2026-07-31-session.md new file mode 100644 index 0000000..b1e5213 --- /dev/null +++ b/HISTORY/2026-07-31-session.md @@ -0,0 +1,71 @@ +# 2026-07-31 — Сессия (v1.1.57 → ...) + +## Контекст +Обсуждение архитектуры: унификация ручного и сценарного режимов, гибкие ссылки на инстансы, новый 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