docs: receive all docs from app-autotest

This commit is contained in:
2026-07-30 13:38:34 +04:00
parent cca20c070f
commit de560cb066
29 changed files with 3213 additions and 0 deletions
+478
View File
@@ -0,0 +1,478 @@
# История разработки 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)