Files

479 lines
31 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# История разработки app-autotest
## v1.1.2 (28.07.2026) — refSvcId: выпадающие списки для cross-service параметров
- `get_params.py` + `api_test.py`: параметры включают `refSvcId` из API
- Фронт: `refSvcId``fetch('/api/instances/list')` → фильтр по `serviceId``<select>` с `displayName`
- Универсально: S3, K8s-кластер, любой сервис — data-driven
## Инфраструктура
- **Кластер**: `iot-naeel` (resourceRealm K8s)
- **URL**: `https://atest.pythonk8s.dev.nubes.ru`
- **Деплой**: git push → managed service pythonk8s → gunicorn
- **PostgreSQL**: `foriot` (509145c3-...), PG 17, `iot-naeel`
## v1.0.93 (28.07.2026) — отдельные DB_* переменные
### Что изменилось
- `db/pool.py`: `_dsn()` собирает DSN из отдельных переменных: DB_HOST, DB_PORT, DB_NAME, DB_USER, DB_PASSWORD, DB_SSLMODE
- `db/init_db.py`: проверка `DB_USER` вместо `DATABASE_URL`
- `secrets/pg-foriot.md`: полный jsonEnv для pythonk8s (6 DB-переменных + 2 существующие)
- `app.py`: NUBES_API_ENDPOINT по умолчанию TEST
## v1.0.92 (27.07.2026) — Steps 5-9: динамические сервисы + PostgreSQL история
### Шаг 5: JS → /static/app.js
- JS вынесен из index.html в отдельный файл
- `window.APP = {version, stand, hasUserToken}` — передача Jinja2-переменных
### Шаг 6: Динамические сервисы
- `currentSvcId` вместо хардкода `SVC_ID=1`
- `initServices()` — загрузка из `/api/services` при старте
### Шаг 7-8: PostgreSQL
- `db/pool.py` — ThreadedConnectionPool (lazy-init, 1-5 соединений)
- `db/init_db.py` — таблица `runs` с индексами (идемпотентно)
- `db/save_run.py` — запись результатов в БД
- `requirements.txt` — добавлен `psycopg2-binary`
### Шаг 9: /api/history
- GET `/api/history` — последние 50 записей (фильтр по client_id + stand)
## v1.0.90 (27.07.2026) — Steps 1-4: auth.py + TTL + cookie
### Шаг 1-2: api/auth.py
- Новый модуль: `get_token()`, `get_client()`, `get_client_id()`, `get_stand()`, `get_token_info()`, `get_token_masked()`
- Убраны дубликаты из main.py и api_test.py
### Шаг 3: Cookie httponly + samesite
- `set_cookie(..., httponly=True, samesite="Strict")`
### Шаг 4: _op_results TTL
- Максимум 500 записей, удаление старше 1 часа
- `_ts` timestamp в каждой записи
## v1.0.99 (28.07.2026) — фикс: Flask-прокси в фоновом потоке
### Найденные ошибки
1. `get_token_info()``request.cookies` → в потоке нет request-контекста → RuntimeError
2. `current_app.config.get("VERSION")` → Flask-proxy → та же проблема
3. `except: pass` глушил ошибки → записи молча не сохранялись
### Исправление
- `user_email` и `app_version` получаются в `api_test()` (где есть request)
и передаются параметрами в `_finish_op``save_run`
- Добавлено логирование ошибок в `save_run` (больше не глухое `pass`)
### Урок
`py_compile` проверяет только синтаксис. Flask-прокси (`request`, `current_app`, `g`, `session`)
**не работают** в фоновых потоках. Все нужные значения — до `threading.Thread` параметрами.
## v1.0.89 (27.07.2026) — Sonnet Rounds 1-3 + документирование
### 1. PostgreSQL connection string
- `os.getenv("DATABASE_URL")` — никакого service discovery
- Инжектится через `jsonEnv` параметры pythonk8s-сервиса (как NUBES_API_TOKEN)
- Порядок: создать PostgreSQL-инстанс в Nubes → взять host/port/user/pass из state_out → собрать DSN → jsonEnv → деплой
### 2. Миграции при деплое
- `init_db()` на уровне модуля в app.py, до register_blueprint
- `CREATE TABLE/INDEX IF NOT EXISTS` — идемпотентно, каждый воркер выполнит независимо
- PostgreSQL обрабатывает конкурентный DDL корректно
- Никаких ручных шагов при деплое
- `DROP COLUMN` — не идемпотентно, осторожно
### 3. Multi-service UI (map-fixed)
- `dataDescriptor` уже в API-ответе — инфраструктура готова
- `map` (свободный): textarea + JSON-валидация (как сейчас)
- `map-fixed`: таблица подполей с отдельными input/select из dataDescriptor
- Бэкенд не меняется — paramValue всегда строка
- Изменения только во фронте: showParams() + сбор подполей в JSON перед отправкой
### 4. Тесты
- pytest для чистых функций без моков: `_find_uid`, `_uid_from_location`, `_with_prefix`, слияние params, tracker
- Структура: `tests/conftest.py`, `test_utils.py`, `test_tracker.py`, `test_get_params.py`
- Роуты с Flask test client + mock HttpClient — отложить (высокая стоимость мокирования)
## v1.0.89 (27.07.2026) — Sonnet Round 2: PostgreSQL, auth.py, детальный план
### Результаты анализа
Sonnet дал конкретные ответы на все 7 вопросов Round 2:
**1. PostgreSQL схема:**
- Таблица `runs` с JSONB-полями `params` и `stages`
- 3 индекса: `(client_id, stand, created_at DESC)`, `(instance_uid)`, `(created_at)`
- Stages в JSONB (не отдельная таблица), instances не нужны (API — source of truth)
- Чувствительные params маскировать при сохранении
**2. Connection pool:**
- `psycopg2.pool.ThreadedConnectionPool` — минимум зависимостей
- Lazy-init в `db/pool.py` — безопасно для fork-модели gunicorn
- `flask.g` + `@app.teardown_appcontext` для возврата соединений
**3. Миграции:**
- `db/init_db.py` с `IF NOT EXISTS` — идемпотентно, авто-применение при старте
- Alembic — когда 3+ таблицы или нужен rollback (сейчас не нужно)
**4. Трекер vs PostgreSQL:**
- **НЕ заменять** трекер на БД — это разные слои
- Трекер = кеш (секунды), PostgreSQL = история (недели)
- Сетевое обращение к БД на критическом пути CREATE недопустимо
**5. auth.py:**
- Явные функции вместо `before_request` — проще, нет скрытых зависимостей
- Одна реализация `get_client_id()` вместо двух копий
- Кеширование `token → endpoint` (опционально, TTL 5 мин)
**6. Динамические сервисы:**
- `/api/services` из API → фильтр по `enabled: true` из config.yaml
- UI: заменить хардкод `SVC_ID=1` на динамическую загрузку в `selectService()`
**7. JS в файл:**
- Классический скрипт, один файл `app.js` (300 строк — не нужно ES modules)
- `window.APP = {...}` через `tojson` фильтр Jinja2 для передачи переменных
### Итоговый порядок реализации (9 шагов)
1. `api/auth.py` — вынести get_token/get_client/get_client_id/get_stand
2. Заменить дублирующиеся функции в main.py и api_test.py
3. Cookie `httponly=True, samesite='Strict'`
4. `_op_results` TTL/очистка
5. JS вынести в `/static/app.js` + `window.APP`
6. Динамические сервисы из `/api/services`
7. `db/pool.py` + `db/init_db.py` (PostgreSQL)
8. Запись истории в runs при завершении операции
9. `/api/history` эндпоинт + UI
## v1.0.89 (27.07.2026) — Sonnet Round 1: архитектурный аудит
- Cloud-first подход с tracker-fallback
- HTML-экранирование в JS
**🔴 Критические находки:**
1. `_op_results` dict растёт бесконечно — утечка памяти, нужен TTL/очистка
2. Cookie токена без `httponly` и `samesite`
3. `/api/log` без проверки авторизации
**🟡 Дублирование:**
1. `_client_id()` — идентичный код в main.py и api_test.py
2. `_client()` — разное поведение в main.py и api_test.py
3. Инлайн `<style>` дублирует `style.css`
**🟡 Захардкодено:**
1. `SVC_ID=1` и список сервисов в HTML — хотя `/api/services` существует
2. `create_client()` в main.py vs `_client()` в api_test.py — разные подходы
**Приоритетный план исправлений:**
1. 🔴 `_op_results` — добавить очистку/TTL
2. 🔴 Cookie `httponly=True, samesite='Strict'`
3. 🟡 Вынести `_client_id()` / `get_token()` в общий модуль `api/auth.py`
4. 🟡 Загрузка сервисов из `/api/services` вместо хардкода
5. 🟡 Вынести JS в `/static/app.js`
6. 🟢 SQLite история запусков
7. 🟢 `/api/log` проверка токена
### Создан запрос Round 2
Файл: `DOCS/sonnet-architecture-review-v1.0.89-r2.md` — уточняющие вопросы по реализации.
## v1.0.89 (27.07.2026) — документация и комментарии кода
## v1.0.87 (27.07.2026) — документация: ARCHITECTURE.md переписан, legacy-доки помечены
## v1.0.84 (27.07.2026) — лог-панель скрыта по умолчанию
### Что изменилось
- Лог-панель теперь `display:none`, показывается кнопкой `log` (правый нижний угол).
- При скрытой панели `/api/log` не поллится.
## v1.0.83 (27.07.2026) — HTML-escape + JSON-валидация map-полей
### Что изменилось
- `_esc(s)` — HTML-экранирование значений (`"``&quot;`, `&``&amp;`, `<``&lt;`).
- `validateJson(el, quiet)` — проверка `JSON.parse()` при `onblur` (красная рамка + текст ошибки).
- Batch-проверка всех map-полей перед отправкой — ошибка → запрос не уходит.
### Что это чинит
- Поломку HTML при значениях с кавычками (напр. `{"f":1}`).
- Отправку битого JSON в API.
## v1.0.82 (27.07.2026) — displayName из API для non-create операций
### Что изменилось
- `_get_instance_display_name(client, uid)``GET /instances/{uid}``displayName`.
- Используется в не-create ветке `POST /api/test` вместо `instance_uid` как fallback.
### Что это чинит
- UUID вместо имени инстанса в финальном статусе.
## v1.0.81 (27.07.2026) — get_params.py: текущие значения из state.params
### Что изменилось
- Новый файл `site/operations/get_params.py` — независимый модуль.
- `get_params_with_current_values(client, op_id, instance_uid)`:
- `GET /instances/{uid}``state.params` (текущие значения)
- `GET /instanceOperations/default/{opId}` → шаблон
- Слияние: `defaultValue = state.params["код"] ?? template.defaultValue`
- Убран `previewOpUid` полностью (и из бэкенда, и из фронтенда).
## v1.0.80 (27.07.2026) — попытка fix через svcOperationId (НЕ СРАБОТАЛО)
## v1.0.79 (27.07.2026) — файловый лог для multi-worker
### Что изменилось
- `_log()` пишет в `/tmp/app-autotest.log` под `fcntl.flock` (вместо in-memory deque).
- `/api/log` читает из файла последние 200 строк.
- Ротация при 512 КБ.
- UI-панель логов (180px, автоскролл, поллинг 2s).
## v1.0.77 (27.07.2026) — debug-логирование в api_params
## v1.0.76 (27.07.2026) — modify: preview-операция для получения paramValue (НЕ СРАБОТАЛО)
# v1.0.74 (27.07.2026) — version bump for push
### Что изменилось
- В `site/templates/index.html` create-операция отделена от обычных операций: CREATE использует фиксированный префикс `autotest-`, а non-create берёт имя выбранного инстанса.
- Из non-create ветки убран случайный fallback `tut` и общий create-style `displayName`.
- В `site/routes/api_test.py` backend больше не подставляет create-имя по умолчанию для обычных операций и возвращает `displayName` для финального статуса.
- В `site/routes/main.py` список autotest-инстансов теперь строится cloud-first без старой склейки через map по имени; tracker остался только как временный fallback, если cloud ещё не вернул новый инстанс.
### Что это чинит
- Путаницу между CREATE и обычными операциями.
- Ситуацию, когда в UI появлялся лишний `tut`.
- Дубли `running/creating` на одном autotest-инстансе.
- Неясный финальный статус, где было видно операцию, но не было понятно, над каким инстансом она выполнялась.
# v1.0.72 (27.07.2026) — service list keeps one autotest row per displayName
### Что изменилось
- В `site/routes/main.py` сервисный список теперь cloud-first и не склеивает разные строки в одну map.
- Tracker остаётся только как fallback, если cloud ещё не вернул конкретный autotest-инстанс.
### Что это чинит
- Дубли `running/creating` на одном и том же autotest-инстансе.
# v1.0.71 (27.07.2026) — finished status shows instance name
### Что изменилось
- В `site/templates/index.html` финальная строка после `OK/FAIL` теперь показывает имя инстанса.
- Имя операции и длительность остаются рядом, отдельно от имени инстанса.
### Что это чинит
- Ситуацию, когда после завершения delete/modify было видно только название операции, но не ясно, над каким инстансом она выполнялась.
# v1.0.70 (27.07.2026) — dedupe autotest instances by displayName
### Что изменилось
- В `site/routes/main.py` сервисный список теперь склеивает записи по `displayName`.
- Если cloud уже отдал `running`, он выигрывает у tracker-fallback `creating`.
### Что это чинит
- Дубли одной и той же autotest-записи после CREATE, когда в списке появлялись и `running`, и `creating`.
## v1.0.69 (27.07.2026) — autotest prefix fixed in CREATE field
### Что изменилось
- В `site/templates/index.html` префикс `autotest-` отображается отдельным фиксированным текстом.
- Рядом с ним остаётся только редактируемый суффикс имени.
- При отправке CREATE имя собирается как `autotest-` + введённый хвост.
### Что это чинит
- Ситуацию, когда весь `displayName` показывался в одном редактируемом поле.
- Риск случайно стереть обязательный префикс `autotest-`.
## v1.0.68 (27.07.2026) — operation name shown after OK + muted operation buttons
### Что изменилось
- В `site/templates/index.html` финальный статус теперь показывает имя операции рядом со временем.
- Кнопки операций получили спокойные неброские оттенки по типу операции.
### Что это чинит
- Неясность после завершения операции, когда было видно только `OK` и время без указания, что именно выполнялось.
- Слишком кислотный вид кнопок операций.
## v1.0.67 (27.07.2026) — autotest instances are shown by cloud prefix
### Что изменилось
- В `site/routes/main.py` список инстансов сервиса больше не отфильтровывается через `/tmp/instances.json`.
- Теперь в таблицу попадают все облачные инстансы с префиксом `autotest-`.
- Трекер оставлен только как fallback, если новый autotest-инстанс ещё не успел появиться в ответе облака.
### Что это чинит
- Ситуацию, когда autotest-инстанс уже есть в облаке, но не показывается в таблице из-за отсутствия записи в json.
## v1.0.66 (27.07.2026) — backend status resolver for operations
### Что изменилось
- В `site/routes/main.py` добавлен канонический резолвер статуса инстанса для `/api/operations/<svc_id>`.
- Backend теперь отдаёт `status` для инстансов, а UI читает именно его вместо угадывания по `explainedStatus`.
- В `site/templates/index.html` убрано раннее отображение «Готово» во время выполнения операции.
### Что это чинит
- Путаницу между `creating`, `running` и фактическим статусом в UI.
- Ситуацию, когда фронтенд сам интерпретировал статусы и показывал не то состояние.
## v1.0.65 (27.07.2026) — cloud list pagination fix
### Что изменилось
- В `site/operations/get_instances.py` пагинация списка инстансов теперь останавливается по размеру страницы, а не по `total`.
- Это убирает ситуацию, когда инстанс уже создан в облаке, но приложение не дочитало следующую страницу и показывает `creating`.
- В `DOCS/ARCHITECTURE.md` и `site/app.py` синхронизирована версия `1.0.65`.
### Что это чинит
- Ложный `creating` для уже существующего инстанса, если он попал не на первую страницу `/instances`.
## v1.0.64 (27.07.2026) — header version + editable displayName placeholder
### Что изменилось
- В `site/routes/main.py` версия теперь гарантированно попадает в `config.VERSION` даже после `load_config()`.
- В `site/templates/index.html` поле `displayName` остаётся редактируемым, но автотест-имя показывается как placeholder.
- При CREATE пользователь может ввести своё имя, а backend всё равно добавит `autotest-` и проверит уникальность.
- В `DOCS/ARCHITECTURE.md` зафиксированы оба изменения.
### Что это чинит
- Пропадающую версию в шапке.
- Неправильное поведение поля `displayName`, когда оно выглядело как готовое значение вместо подсказки.
## v1.0.63 (27.07.2026) — autotest namespace for instances
### Что изменилось
- В `site/templates/index.html` CREATE теперь генерирует `displayName` с префиксом `autotest-` и проверяет его на уникальность среди текущих инстансов.
- В `site/routes/api_test.py` backend тоже нормализует `displayName` с этим префиксом и, если нужно, добавляет суффикс для уникальности перед созданием в облаке.
- В `site/routes/main.py` список инстансов фильтруется по namespace `autotest-`, чтобы в UI попадали только тестовые инстансы приложения.
- В `DOCS/ARCHITECTURE.md` зафиксирован переход на namespace-based фильтрацию.
### Что это чинит
- Дубли `displayName` при CREATE.
- Чужие инстансы в списке.
- Зависимость от локального трекера как от основного признака принадлежности.
## v1.0.62 (27.07.2026) — immediate visual refresh after CREATE
### Что изменилось
- В `site/templates/index.html` после завершения CREATE список инстансов перерисовывается полностью.
- `refreshInstances()` больше не зависит от DOM-diff и сразу ставит новый инстанс в список.
- `DOCS/ARCHITECTURE.md` и `site/app.py` синхронизированы на `1.0.62`.
### Что это чинит
- После появления кнопки «Готово» созданный инстанс должен сразу быть виден в списке.
## v1.0.61 (27.07.2026) — immediate list rebuild after CREATE
### Что изменилось
- В `site/templates/index.html` `refreshInstances()` теперь всегда полностью перерисовывает список инстансов после завершения операции.
- После `OK` новый инстанс должен появляться в списке сразу, без зависимости от DOM-diff и без перезагрузки страницы.
- В `DOCS/ARCHITECTURE.md` и `site/app.py` синхронизирована версия `1.0.61`.
### Что это чинит
- Убрана ситуация, когда после CREATE статус уже `OK`, но в UI инстанс ещё не виден.
## v1.0.60 (27.07.2026) — full list rerender after CREATE
### Что изменилось
- В `site/templates/index.html` `refreshInstances()` теперь полностью перерисовывает список инстансов, а не пытается вставить только diff.
- После завершения CREATE новый инстанс должен появляться в списке сразу после `OK`.
- В `DOCS/ARCHITECTURE.md` добавлен отдельный шаг про полную перерисовку списка.
### Что это чинит
- Снята зависимость от частичного DOM-diff-а, из-за которого созданный инстанс мог не появляться сразу.
## v1.0.59 (27.07.2026) — restore tracked instance after reload
### Что изменилось
- В `site/routes/main.py` tracked-инстансы, которых Nubes уже не отдает в ответе после reload, снова добавляются в список из `/tmp/instances.json`.
- В `site/templates/index.html` статус CREATE теперь показывает имя инстанса, а не только `create`.
- В `DOCS/ARCHITECTURE.md` зафиксирован новый шаг восстановления списка после F5.
### Что это чинит
- После F5 инстанс не пропадает из UI, если он есть в локальном трекере.
- В статусе выполнения видно, по какому displayName идет CREATE.
## v1.0.58 (27.07.2026) — version bump
- Поднята версия проекта до `1.0.58`.
- Синхронизированы заголовки в `site/app.py` и `DOCS/ARCHITECTURE.md`.
## v1.0.57 (27.07.2026) — UI hardening + history sync
### Что изменилось
- В `site/templates/index.html` финальное состояние кнопки вынесено в `setFinishedState()`, чтобы не держать логику завершения операции в двух местах.
- После завершения операции кнопка «Готово» теперь гарантированно сбрасывает обработчик через `btn.onclick = null`.
- Повторный CREATE через старый обработчик больше не воспроизводится.
- `displayName` больше не теряется в нормальном сценарии завершения CREATE.
- В `site/app.py` версия поднята до `1.0.57`.
- В `DOCS/ARCHITECTURE.md` отмечено закрытие бага с повторным CREATE и синхронизирован статус по `displayName`.
### Зачем это было сделано
- Убрать хрупкость вокруг состояния кнопки после завершения операции.
- Привести документацию и версию к текущему состоянию кода.
## v1.0.54 (27.07.2026) — Аудит #3: 4 бага исправлено
### Баг #1: `_client()` не автоопределял стенд (КРИТИЧЕСКИЙ)
- **Где:** `api_test.py:_client()` — использовал `current_app.config["NUBES_API_ENDPOINT"]` (raw env var)
- **Симптом:** main.py автоопределял стенд для главной, но POST /api/test падал с 401 если токен от другого стенда
- **Исправление:** `detect_endpoint()` вынесен в `api/http_client.py`, используется в `_client()`
- **Как пропустили:** анализировал main.py и api_test.py в изоляции, не сравнил
### Баг #2: `_find_uid()` возвращал не тот UUID (КРИТИЧЕСКИЙ)
- **Где:** `api_test.py:_find_uid()` — итерировал по ВСЕМ значениям dict, искал 36-символьную строку
- **Симптом:** `/instanceOperations` возвращает оба `instanceUid` + `instanceOperationUid`, функция брала первый попавшийся → `run` шёл на неправильный URL
- **Исправление:** ищет по конкретным ключам: `instanceOperationUid``instanceUid``uid``Uid`
- **Как пропустили:** не проверил реальный формат ответа API
### Баг #3: Сироты при ошибке params (СРЕДНИЙ)
- **Где:** `api_test.py`: порядок — `POST /instances``POST /instanceOperations` → params loop → `tracker_add`
- **Симптом:** если params падает — инстанс и операция созданы в Nubes, трекер пуст → сироты
- **Исправление:** `tracker_add` вызывается ДО params loop, сразу после получения `instanceUid`
- **Как пропустили:** не трассировал линейно порядок вызовов
### Баг #4: `flock` без таймаута (НИЗКИЙ)
- **Где:** `tracker.py:_locked_read/_write``fcntl.flock(fd, LOCK_EX)` без LOCK_NB
- **Симптом:** при зависшем процессе с локом все воркеры блокируются навсегда
- **Исправление:** `_acquire_lock()` с `LOCK_EX | LOCK_NB` + retry до 2 секунд
- **Как пропустили:** новый код flock не перечитал с нуля после добавления
### Архитектурное: `detect_endpoint` вынесен в http_client.py
- Раньше был в main.py → недоступен для api_test.py
- Теперь в `api/http_client.py` → оба модуля используют
---
## v1.0.53 (27.07.2026) — Файловый трекер с fcntl.flock
- In-memory dict заменён на `/tmp/instances.json` с `fcntl.LOCK_EX`
- Multi-worker gunicorn: все воркеры читают/пишут один файл под локом
## v1.0.52 (27.07.2026) — `_tmpl()` helper
- Все переменные шаблона передаются во всех трёх return-путях index()
- Исправлен UndefinedError при «Выйти»
## v1.0.51 (27.07.2026) — Автоопределение стенда + UnboundLocalError
- `detect_endpoint()` в main.py: пробует токен против dev и test API
- `instance_groups = {}` инициализирован до if
- `_finish_op` обёрнут в полный try/except
## v1.0.50 (27.07.2026) — showStages + api_operations fix
- `showStages()`: dtFinish вместо isSuccessful для ⏳/✅/❌
- `api_operations()`: не фильтровать "not created", добавлять tracked-сирот
## v1.0.49 (27.07.2026) — In-memory tracker + refreshInstances
- Трекер: in-memory dict вместо файла `/tmp/instances.json`
- `refreshInstances()` добавляет новые инстансы в DOM (не только бейджи)
- `selectService()` заменён на `refreshInstances()` после OK
## v1.0.48 (27.07.2026) — tracker_add синхронно
- `tracker_add` в `api_test()` до `threading.Thread`
- Лог `is_ok` в `_finish_op`
- Flexbox-кнопки горизонтально
## v1.0.47 (27.07.2026) — displayName fix
- displayName захватывается до очистки формы
- tracker_add до _op_results[OK]
- autotest-1 в _INITIAL
## v1.0.46 (27.07.2026) — _finish_op signature
- Добавлены op_name, svc_op_id в _finish_op()
## v1.0.45 и ранее
- Базовый CREATE/MODIFY/SUSPEND/DELETE/RESUME/REDEPLOY
- UI с этапами, поллинг, params форма
- Множественные баги CREATE flow (документированы в DOCS/api-create-flow.md)