diff --git a/DOCS/ARCHITECTURE.legacy.md b/DOCS/ARCHITECTURE.legacy.md
new file mode 100644
index 0000000..254d5ae
--- /dev/null
+++ b/DOCS/ARCHITECTURE.legacy.md
@@ -0,0 +1,417 @@
+# ⚠️ LEGACY — НЕАКТУАЛЬНО. См. ARCHITECTURE.md для текущего состояния.
+
+# Архитектура app-autotest — полный документ (v1.0.76)
+
+---
+
+## 1. Обзор
+
+Flask-приложение для автотестов операций сервисов облачной платформы Nubes.
+Деплой: Nubes pythonk8s (gunicorn), тестовый стенд `atest.pythonk8s.dev.nubes.ru`.
+Репозиторий: `https://gitea.services.ngcloud.ru/forcloud/app-autotest.git`.
+
+Текущий срез потока операций:
+- CREATE и обычные операции разделены в UI и backend.
+- CREATE использует фиксированный префикс `autotest-`, non-create работает по выбранному инстансу.
+- Финальный статус показывает статус, операцию, длительность и имя инстанса.
+- Список autotest-инстансов строится cloud-first; tracker нужен только как короткий fallback после CREATE.
+- **Modify** (и другие операции с параметрами) показывает **текущие** значения параметров инстанса: создаётся pending-операция, читаются `cfsParams.paramValue`, форма заполняется реальными данными. При submit операция переиспользуется через `previewOpUid`.
+
+---
+
+## 2. Структура файлов
+
+```
+app-autotest/
+├── site/ ← НЕ пакет (без __init__.py, конфликт с stdlib)
+│ ├── app.py ← Flask(__name__), VERSION, blueprints, /health
+│ ├── api/
+│ │ └── http_client.py ← HttpClient + STANDS + detect_endpoint()
+│ ├── operations/
+│ │ ├── tracker.py ← Трекер: /tmp/instances.json + fcntl.flock
+│ │ ├── get_services.py ← get_services(), get_service_detail()
+│ │ └── get_instances.py ← get_instances(), get_organization()
+│ ├── routes/
+│ │ ├── main.py ← GET/POST / (главная), _tmpl()
+│ │ ├── api.py ← /api/run, /api/status, /api/config
+│ │ └── api_test.py ← /api/test, /api/test/status, _finish_op
+│ ├── templates/
+│ │ └── index.html ← UI: Jinja2 + JS (поллинг)
+│ └── static/
+│ └── style.css
+└── secrets/
+ ├── dev.token ← Токен для DEV стенда
+ ├── test.token ← Токен для TEST стенда
+ └── test.token.new
+```
+
+---
+
+## 3. Поток CREATE — полная трассировка (14 шагов)
+
+### Шаг 1: UI — пользователь начинает CREATE
+
+```javascript
+// index.html: startCreate()
+function startCreate(){
+ selectedInst=null; // нет выбранного инстанса
+ stopPoll(); // остановить предыдущий поллинг
+ document.querySelectorAll('[data-iuid]').forEach(e=>e.classList.remove('active'));
+ document.querySelectorAll('.inst-ops').forEach(e=>e.classList.remove('open'));
+ showParams(18,'create'); // 18 = svcOperationId для create у сервиса "Болванка"
+}
+```
+
+`selectedOp = {opId: 18, opName: "create", svcId: 1}`
+
+### Шаг 2: UI — загрузка параметров и показ формы
+
+```javascript
+// index.html: showParams(18, 'create')
+fetch('/api/params/18') // GET /api/params/18
+ .then(r=>r.json())
+ .then(params=>{
+ // Генерация формы: displayName + поля из params
+ const displayName = 'autotest-1-' + Date.now().toString(36);
+ form.innerHTML = ''
+ + params.map(p => '').join('');
+
+ btn.onclick = () => {
+ // Сбор params из формы
+ const pp = {};
+ document.querySelectorAll('#params-form [name^="p_"]').forEach(el => {
+ pp[el.name.replace('p_','')] = el.value;
+ });
+ executeOp(pp); // ← запуск
+ };
+ });
+```
+
+Бэкенд: `GET /instanceOperations/default/18` → возвращает список `cfsParams`.
+
+### Шаг 3: UI — executeOp (отправка)
+
+```javascript
+// index.html: executeOp(params)
+const displayName = document.getElementById('param-displayname')?.value || 'autotest-1';
+// Захват ДО очистки формы! (v1.0.48)
+
+document.getElementById('params-form').innerHTML = ''; // очистка
+
+fetch('/api/test', {
+ method:'POST',
+ body: JSON.stringify({
+ serviceId: 1, // SVC_ID
+ operation: "create",
+ svcOperationId: 18,
+ params: {...}, // собранные параметры
+ instanceUid: "", // пусто для create
+ displayName: displayName
+ })
+})
+```
+
+JSON: `{"serviceId":1, "operation":"create", "svcOperationId":18, "params":{...}, "instanceUid":"", "displayName":"autotest-1-lq5x3a"}`
+
+### Шаг 4: Бэкенд — api_test() разбор запроса
+
+```python
+# api_test.py: POST /api/test
+data = request.get_json()
+svc_id = data["serviceId"] # 1 (int)
+op_name = data["operation"] # "create" (str)
+svc_op_id = data["svcOperationId"] # 18 (int)
+params = data.get("params", {}) # {...} (dict)
+instance_uid = data.get("instanceUid") # "" (falsy)
+display_name = data.get("displayName", f"autotest-{svc_id}") # "autotest-1-lq5x3a"
+```
+
+### Шаг 5: Бэкенд — _client() с автоопределением стенда
+
+```python
+def _client():
+ token = request.cookies.get("token") or current_app.config["NUBES_API_TOKEN"]
+ endpoint = detect_endpoint(token) or current_app.config["NUBES_API_ENDPOINT"]
+ return HttpClient(endpoint, token)
+```
+
+`detect_endpoint(token)` пробует dev→test стенды, возвращает рабочий URL или None.
+
+### Шаг 6: Бэкенд — создание инстанса в Nubes
+
+```python
+payload = {"serviceId": 1, "displayName": "autotest-1-lq5x3a", "descr": ""}
+resp = client.post("/instances", payload)
+# Ответ: {"instanceUid": "93b0b65d-..."}
+instance_uid = resp.get("instanceUid") or _find_uid(resp) or _uid_from_location(...)
+# → "93b0b65d-e080-416d-bbb8-463f7adbda80"
+```
+
+### Шаг 7: Бэкенд — создание операции
+
+```python
+op_payload = {"instanceUid": "93b0b65d-...", "operation": "create"}
+op_resp = client.post("/instanceOperations", op_payload)
+# Ответ: {"instanceOperationUid": "0b0596d8-..."}
+op_uid = _find_uid(op_resp) or _uid_from_location(...)
+# _find_uid ищет: instanceOperationUid → instanceUid → uid → Uid
+# → "0b0596d8-b48d-4694-98c3-65f2f8eeb27d"
+```
+
+### Шаг 8: Бэкенд — tracker_add (v1.0.54: ДО params!)
+
+```python
+try:
+ tracker_add("93b0b65d-...", 1, "autotest-1-lq5x3a")
+except Exception as e:
+ print(f"[TRACKER ERROR] ...")
+```
+
+`tracker.py`: `_locked_read()` → `data["93b0b65d-..."] = {...}` → `_locked_write(data)`.
+Файл `/tmp/instances.json` обновлён под `fcntl.LOCK_EX | LOCK_NB`.
+
+### Шаг 9: Бэкенд — установка параметров и запуск
+
+```python
+for pid, pval in params.items():
+ client.post("/instanceOperationCfsParams", {
+ "instanceOperationUid": op_uid,
+ "svcOperationCfsParamId": int(pid),
+ "paramValue": str(pval)
+ })
+client.post(f"/instanceOperations/{op_uid}/run")
+```
+
+Если params пустые (например, повторный клик на «Готово») → `/run` → 422 "required CFS parameter ... is missing".
+
+### Шаг 10: Бэкенд — фоновый поток
+
+```python
+threading.Thread(
+ target=_finish_op,
+ args=(client, op_uid, instance_uid, svc_id, display_name, op_name, svc_op_id, True),
+ daemon=True
+).start()
+return jsonify({"status": "RUNNING", "opUid": op_uid, "instanceUid": instance_uid})
+```
+
+### Шаг 11: UI — поллинг статуса
+
+```javascript
+// index.html: executeOp() — после получения RUNNING
+pollTimer = setInterval(async () => {
+ const sr = await fetch('/api/test/status/' + opUid);
+ const sd = await sr.json();
+ showStages(sd.stages || []);
+ if (sd.status !== 'RUNNING') {
+ stopPoll();
+ // показать OK/FAIL
+ btn.textContent = 'Готово';
+ btn.disabled = false;
+ // ⚠️ БАГ: btn.onclick всё ещё активен!
+ if (sd.status === 'OK') {
+ await refreshInstances();
+ }
+ }
+}, 2000);
+```
+
+### Шаг 12: Бэкенд — _finish_op (фоновый)
+
+```python
+while time.time() < deadline: # 300 секунд
+ data = client.get(f"/instanceOperations/{op_uid}?fields=...")
+ op = data.get("instanceOperation", {})
+ dt_finish = op.get("dtFinish")
+
+ _op_results[op_uid] = {"status": "RUNNING", "stages": [...], "duration": ...}
+
+ if dt_finish and str(dt_finish).strip():
+ is_ok = op.get("isSuccessful")
+ # tracker_add уже вызван синхронно!
+ if is_ok and is_delete:
+ tracker_remove(instance_uid)
+ _op_results[op_uid] = {"status": "OK" if is_ok else "FAIL", ...}
+ return
+ time.sleep(5)
+```
+
+Весь цикл обёрнут в `try/except: print(traceback)` (v1.0.51).
+
+### Шаг 13: UI — refreshInstances
+
+```javascript
+async function refreshInstances(){
+ const r = await fetch('/api/operations/1');
+ const d = await r.json();
+ const newInsts = d.instances || [];
+ // Найти новые (отсутствуют в svcInstances)
+ const added = newInsts.filter(i => !oldUids.has(i.instanceUid));
+ // Обновить бейджи существующих
+ // Добавить новые в DOM перед кнопкой "+ Создать"
+}
+```
+
+### Шаг 14: Бэкенд — api_operations (GET /api/operations/1)
+
+```python
+tracked = tracker_list() # читает /tmp/instances.json
+tracked_by_uid = {uid: item for uid in tracked if svcId == 1}
+instances = get_instances(_client()) # запрос в Nubes API
+nubes_uids = {i["instanceUid"] for i in instances}
+
+# Инстансы из Nubes которые есть в трекере (кроме deleted)
+svc_instances = [i for i in instances
+ if i.get("instanceUid") in tracked_uids
+ and i.get("explainedStatus") not in ("deleted",)]
+
+# Инстансы из трекера которых Nubes ещё не отдаёт → status "creating"
+for uid, t in tracked_by_uid.items():
+ if uid not in nubes_uids:
+ svc_instances.append({
+ "instanceUid": uid,
+ "displayName": t["displayName"],
+ "explainedStatus": "creating",
+ ...
+ })
+```
+
+---
+
+## 4. Трекер инстансов
+
+### 4.1 Назначение
+
+Хранит UID инстансов, созданных приложением. Нужен чтобы показывать в UI только «наши» инстансы, а не все в организации.
+
+### 4.2 Реализация (v1.0.53+)
+
+Файл: `site/operations/tracker.py`
+
+- Хранилище: `/tmp/instances.json` (JSON-файл)
+- Блокировка: `fcntl.flock(fd, LOCK_EX | LOCK_NB)` с retry до 2 сек
+- Seed: `_INITIAL` (4 инстанса) при пустом/битом файле
+- Функции: `add(uid, svc_id, name)`, `remove(uid)`, `list_all()`
+
+### 4.3 Эволюция
+
+| Версия | Реализация | Проблема |
+|--------|-----------|----------|
+| v1.0.45 | `/tmp/instances.json` + `threading.Lock` | `except: pass` скрывал ошибки |
+| v1.0.49 | In-memory dict | Multi-worker gunicorn: у каждого свой dict |
+| v1.0.53 | `/tmp/instances.json` + `fcntl.flock(LOCK_EX)` | Без таймаута: потенциальный зависон |
+| v1.0.54 | `/tmp/instances.json` + `fcntl.flock(LOCK_EX\|LOCK_NB)` + retry 2s | ✅ Текущий |
+
+---
+
+## 5. Автоопределение стенда
+
+Файл: `site/api/http_client.py`
+
+```python
+STANDS = [
+ "https://lk-api-gateway-dev.ngcloud.ru/api/v1/svc",
+ "https://lk-api-gateway-test.ngcloud.ru/api/v1/svc",
+]
+
+def detect_endpoint(token):
+ for ep in STANDS:
+ try:
+ c = HttpClient(ep, token)
+ data = c.get("/instances", params={"pageSize": 1, "page": 1})
+ if data.get("results") is not None:
+ return ep
+ except Exception:
+ continue
+ return None
+```
+
+Используется в:
+- `main.py`: при загрузке главной страницы
+- `api_test.py: _client()`: при каждом API-запросе
+
+---
+
+## 6. Все известные баги (найдено/исправлено)
+
+### Исправленные
+
+| # | Версия | Баг | Причина | Исправление |
+|---|--------|-----|---------|-------------|
+| 1 | v1.0.46 | `_finish_op()` недостающие параметры | Не передавались `op_name`, `svc_op_id` | Добавлены в сигнатуру |
+| 2 | v1.0.48 | displayName терялся | `params-form.innerHTML=''` до чтения `param-displayname` | Захват до очистки |
+| 3 | v1.0.48 | tracker_add в daemon-потоке | Поток умирает под gunicorn | Перенос в синхронный код |
+| 4 | v1.0.50 | ❌ для этапов в процессе | `isSuccessful===false` для in-progress | Проверка `dtFinish` |
+| 5 | v1.0.50 | Инстанс не в списке | `explainedStatus:"not created"` фильтровался | Убран фильтр + tracked-сироты |
+| 6 | v1.0.50 | `selectService()` скрывал stages | Полный перерендер после OK | `refreshInstances()` с диффом |
+| 7 | v1.0.51 | `instance_groups` UnboundLocalError | Не иниц. до `if active_token:` | `instance_groups = {}` |
+| 8 | v1.0.51 | `_finish_op` молча умирал | `data.get()` вне try/except | Весь цикл в try/except |
+| 9 | v1.0.51 | Автостенд только в main.py | `_client()` в api_test.py не использовал | `detect_endpoint()` в http_client.py |
+| 10 | v1.0.52 | `token_info` UndefinedError | Не передан в шаблон при action=clear | `_tmpl()` helper со всеми переменными |
+| 11 | v1.0.53 | In-memory dict + multi-worker | У каждого воркера свой `_data` | `/tmp/instances.json` + `fcntl.flock` |
+| 12 | v1.0.54 | `_find_uid()` возвращал не тот UUID | Итерация по всем значениям dict | Поиск по ключам: instanceOperationUid→instanceUid |
+| 13 | v1.0.54 | Сироты при ошибке params | `tracker_add` ПОСЛЕ params loop | `tracker_add` ДО params loop |
+| 14 | v1.0.54 | `flock` без таймаута | `LOCK_EX` блокируется навсегда | `LOCK_EX \| LOCK_NB` + retry 2s |
+| 15 | v1.0.55 | F5 = предупреждение браузера | clear/stray POST → HTML без редиректа | `redirect("/")` для всех POST |
+| 16 | v1.0.56 | Кнопка «Готово» запускала повторный CREATE | `btn.onclick` оставался привязанным после завершения операции | `btn.onclick = null` после финального статуса |
+| 17 | v1.0.57 | Хрупкость финального состояния кнопки | Повторяемый код финализации статуса | `setFinishedState()` централизует финальный UI-состояние |
+| 18 | v1.0.59 | После F5 список терял tracked-инстанс | `api_operations()` не добирал tracked-сирот обратно из трекера | `tracked_by_uid` + добавление отсутствующих tracked-инстансов |
+| 19 | v1.0.60 | CREATE не появлялся сразу в списке после OK | UI делал diff и мог не перерисовать список целиком | Полная перерисовка `inst-list` в `refreshInstances()` |
+| 20 | v1.0.63 | Дубли и чужие инстансы | Принадлежность определялась не по namespace | `autotest-` prefix + уникализация `displayName` при CREATE |
+| 21 | v1.0.64 | Версия не показывалась в шапке | `load_config()` затирал VERSION | `config["VERSION"] = current_app.config["VERSION"]` |
+| 22 | v1.0.64 | displayName был editable value, а не placeholder | CREATE подставлял имя в value | Placeholder `autotest-...`, значение вводит пользователь |
+| 23 | v1.0.65 | `creating` из-за недочитанной страницы | `get_instances()` доверял `total` и мог не перейти на следующую страницу | Остановка по `len(batch) < pageSize` |
+
+### ✅ Закрыто в v1.0.56
+
+| Баг | Статус | Что поменяли |
+|-----|--------|--------------|
+| Кнопка «Готово» запускала повторный CREATE | закрыто | После финального статуса обработчик снимается через `btn.onclick = null` |
+| displayName "autotest-1" при повторном клике | закрыто | Повторный клик больше не вызывает `executeOp()` на очищенной форме |
+
+---
+
+## 7. HTTP-клиент
+
+Файл: `site/api/http_client.py`
+
+```python
+class HttpClient:
+ def __init__(self, endpoint, token):
+ # Bearer auth + User-Agent: Mozilla/5.0 (DDoS-Guard)
+
+ def get(self, path, timeout=10):
+ # GET → r.raise_for_status() → r.json()
+
+ def post(self, path, data, timeout=30):
+ # POST → проверка r.ok → парсинг JSON → Location header
+```
+
+---
+
+## 8. Шаблон Jinja2 — используемые переменные
+
+| Переменная | Где | Назначение |
+|-----------|-----|-----------|
+| `config.VERSION` | Строка 32 | Версия в топбаре |
+| `token_info.email` | Строка 33 | Email пользователя |
+| `token_info.company` | Строка 33 | Компания |
+| `env_token_masked` | Строка 36 | Маскированный токен в placeholder |
+| `has_user_token` | Строка 36 | Показать токен или маску |
+| `error` | Строка 40 | Ошибка |
+| `organization.displayName` | Строка 50 | Название организации |
+| `organization.explainedStatus` | Строка 51 | Статус организации |
+| `client_id` | Строка 50 | ID клиента |
+| `instance_groups` | Строки 55-62 | Группы инфраструктурных инстансов |
+
+---
+
+## 9. Ограничения платформы Nubes pythonk8s
+
+- `site/` — НЕ пакет (без `__init__.py`, конфликт с stdlib `site.py`)
+- Импорты: `from api.http_client import ...` (без префикса `site.`)
+- `app.run(host="0.0.0.0", port=5000)` — обязательно
+- Gunicorn: запускается платформой, количество воркеров неизвестно
+- Нет persistent volume — `/tmp/` теряется при редеплое
+- User-Agent: `Mozilla/5.0` обязателен (DDoS-Guard)
+- Деплой: `git push` → managed service редеплоит
diff --git a/DOCS/ARCHITECTURE.md b/DOCS/ARCHITECTURE.md
index 9410c28..e9119d8 100644
--- a/DOCS/ARCHITECTURE.md
+++ b/DOCS/ARCHITECTURE.md
@@ -1,21 +1,26 @@
-# Архитектура app-autotest — полный документ
+# Архитектура app-autotest — текущее состояние
-v1.0.76, 27.07.2026
+v1.0.87, 27.07.2026
---
## 1. Обзор
-Flask-приложение для автотестов операций сервисов облачной платформы Nubes.
-Деплой: Nubes pythonk8s (gunicorn), тестовый стенд `atest.pythonk8s.dev.nubes.ru`.
-Репозиторий: `https://gitea.services.ngcloud.ru/forcloud/app-autotest.git`.
+Flask-приложение (один HTML-файл, без SPA-фреймворка) для ручного тестирования операций
+сервисов облачной платформы Nubes. Работает через REST API Nubes, автоопределяет стенд
+(dev/test) по JWT-токену.
-Текущий срез потока операций:
-- CREATE и обычные операции разделены в UI и backend.
-- CREATE использует фиксированный префикс `autotest-`, non-create работает по выбранному инстансу.
-- Финальный статус показывает статус, операцию, длительность и имя инстанса.
-- Список autotest-инстансов строится cloud-first; tracker нужен только как короткий fallback после CREATE.
-- **Modify** (и другие операции с параметрами) показывает **текущие** значения параметров инстанса: создаётся pending-операция, читаются `cfsParams.paramValue`, форма заполняется реальными данными. При submit операция переиспользуется через `previewOpUid`.
+**Деплой**: Nubes pythonk8s (gunicorn, несколько воркеров).
+**URL**: `https://atest.pythonk8s.dev.nubes.ru`
+**Репозиторий**: `https://gitea.services.ngcloud.ru/forcloud/app-autotest.git`
+
+### Ключевые принципы
+
+1. **Источник правды — Nubes API**. Все данные (инстансы, параметры, статусы) получаются
+ только из API. Никаких локальных копий или кеша (кроме краткосрочного трекера).
+2. **Разделение CREATE и non-CREATE**. Это два принципиально разных потока.
+3. **Multi-worker safety**. Все общие ресурсы (/tmp файлы) защищены `fcntl.flock`.
+4. **Мелкие функции**. Каждое действие — отдельная функция.
---
@@ -23,395 +28,162 @@ Flask-приложение для автотестов операций серв
```
app-autotest/
-├── site/ ← НЕ пакет (без __init__.py, конфликт с stdlib)
-│ ├── app.py ← Flask(__name__), VERSION, blueprints, /health
+├── site/ ← НЕ пакет (без __init__.py)
+│ ├── app.py ← Flask(__name__), VERSION, blueprints, /health
+│ ├── runner.py ← [LEGACY] Раннер тестов
+│ ├── config.yaml ← [LEGACY] Конфиг раннера
│ ├── api/
-│ │ └── http_client.py ← HttpClient + STANDS + detect_endpoint()
+│ │ └── http_client.py ← HttpClient, автостенд
│ ├── operations/
-│ │ ├── tracker.py ← Трекер: /tmp/instances.json + fcntl.flock
-│ │ ├── get_services.py ← get_services(), get_service_detail()
-│ │ └── get_instances.py ← get_instances(), get_organization()
+│ │ ├── get_services.py ← get_services(), get_service_detail()
+│ │ ├── get_instances.py ← get_instances() (пагинация), get_organization()
+│ │ ├── get_params.py ← get_params_with_current_values() — слияние state.params + шаблон
+│ │ └── tracker.py ← /tmp/instances-{clientId}-{stand}.json + flock
│ ├── routes/
-│ │ ├── main.py ← GET/POST / (главная), _tmpl()
-│ │ ├── api.py ← /api/run, /api/status, /api/config
-│ │ └── api_test.py ← /api/test, /api/test/status, _finish_op
+│ │ ├── main.py ← GET/POST /, /api/operations/
+│ │ ├── api.py ← [LEGACY] /api/run, /api/status, /api/config
+│ │ └── api_test.py ← /api/test, /api/params, /api/log
│ ├── templates/
-│ │ └── index.html ← UI: Jinja2 + JS (поллинг)
+│ │ └── index.html ← Весь UI (Jinja2 + ванильный JS)
│ └── static/
-│ └── style.css
+│ ├── style.css
+│ ├── logo.svg
+│ └── favicon.svg
└── secrets/
- ├── dev.token ← Токен для DEV стенда
- ├── test.token ← Токен для TEST стенда
- └── test.token.new
+ ├── dev.token
+ └── test.token
```
---
-## 3. Поток CREATE — полная трассировка (14 шагов)
+## 3. Бэкенд: HTTP-клиент и автостенд
-### Шаг 1: UI — пользователь начинает CREATE
+### `http_client.py`
-```javascript
-// index.html: startCreate()
-function startCreate(){
- selectedInst=null; // нет выбранного инстанса
- stopPoll(); // остановить предыдущий поллинг
- document.querySelectorAll('[data-iuid]').forEach(e=>e.classList.remove('active'));
- document.querySelectorAll('.inst-ops').forEach(e=>e.classList.remove('open'));
- showParams(18,'create'); // 18 = svcOperationId для create у сервиса "Болванка"
-}
-```
+- `HttpClient(endpoint, token)` — обёртка над `requests.Session`:
+ - Заголовки: `Authorization: Bearer`, `User-Agent: Mozilla/5.0` (DDoS-Guard)
+ - `get(path)` → `raise_for_status()` → `.json()`
+ - `post(path, data)` → проверка `r.ok`, парсинг JSON, извлечение `Location`
+- `detect_endpoint(token)` — пробует dev→test стенды, возвращает рабочий URL
+- `stand_name(endpoint)` — "dev"/"test" по URL
+- `create_client(token, fallback)` — HttpClient + endpoint
-`selectedOp = {opId: 18, opName: "create", svcId: 1}`
+### `get_instances.py`
-### Шаг 2: UI — загрузка параметров и показ формы
+- `get_instances(client)` — `GET /instances` с пагинацией (pageSize=200). Остановка по `len(batch) < pageSize`.
+- `get_organization(client)` — первый инстанс с `serviceId == 19`
-```javascript
-// index.html: showParams(18, 'create')
-fetch('/api/params/18') // GET /api/params/18
- .then(r=>r.json())
- .then(params=>{
- // Генерация формы: displayName + поля из params
- const displayName = 'autotest-1-' + Date.now().toString(36);
- form.innerHTML = ''
- + params.map(p => '').join('');
+### `get_services.py`
- btn.onclick = () => {
- // Сбор params из формы
- const pp = {};
- document.querySelectorAll('#params-form [name^="p_"]').forEach(el => {
- pp[el.name.replace('p_','')] = el.value;
- });
- executeOp(pp); // ← запуск
- };
- });
-```
+- `get_services(client)` → `GET /services` → список
+- `get_service_detail(client, svc_id)` → `GET /services/{id}` → детали + операции
-Бэкенд: `GET /instanceOperations/default/18` → возвращает список `cfsParams`.
+### `get_params.py` — КЛЮЧЕВОЙ модуль
-### Шаг 3: UI — executeOp (отправка)
+`get_params_with_current_values(client, op_id, instance_uid)`:
+1. `GET /instances/{uid}` → `state.params` (текущие значения: `{"whereFail":"1",...}`)
+2. `GET /instanceOperations/default/{opId}` → шаблон (коды, типы, valueList, dataDescriptor)
+3. Слияние: `defaultValue = state.params["код"] ?? template.defaultValue`
+4. Приведение типов: `bool` → `"true"/"false"`, `dict` → `json.dumps()`
-```javascript
-// index.html: executeOp(params)
-const displayName = document.getElementById('param-displayname')?.value || 'autotest-1';
-// Захват ДО очистки формы! (v1.0.48)
+### `tracker.py`
-document.getElementById('params-form').innerHTML = ''; // очистка
-
-fetch('/api/test', {
- method:'POST',
- body: JSON.stringify({
- serviceId: 1, // SVC_ID
- operation: "create",
- svcOperationId: 18,
- params: {...}, // собранные параметры
- instanceUid: "", // пусто для create
- displayName: displayName
- })
-})
-```
-
-JSON: `{"serviceId":1, "operation":"create", "svcOperationId":18, "params":{...}, "instanceUid":"", "displayName":"autotest-1-lq5x3a"}`
-
-### Шаг 4: Бэкенд — api_test() разбор запроса
-
-```python
-# api_test.py: POST /api/test
-data = request.get_json()
-svc_id = data["serviceId"] # 1 (int)
-op_name = data["operation"] # "create" (str)
-svc_op_id = data["svcOperationId"] # 18 (int)
-params = data.get("params", {}) # {...} (dict)
-instance_uid = data.get("instanceUid") # "" (falsy)
-display_name = data.get("displayName", f"autotest-{svc_id}") # "autotest-1-lq5x3a"
-```
-
-### Шаг 5: Бэкенд — _client() с автоопределением стенда
-
-```python
-def _client():
- token = request.cookies.get("token") or current_app.config["NUBES_API_TOKEN"]
- endpoint = detect_endpoint(token) or current_app.config["NUBES_API_ENDPOINT"]
- return HttpClient(endpoint, token)
-```
-
-`detect_endpoint(token)` пробует dev→test стенды, возвращает рабочий URL или None.
-
-### Шаг 6: Бэкенд — создание инстанса в Nubes
-
-```python
-payload = {"serviceId": 1, "displayName": "autotest-1-lq5x3a", "descr": ""}
-resp = client.post("/instances", payload)
-# Ответ: {"instanceUid": "93b0b65d-..."}
-instance_uid = resp.get("instanceUid") or _find_uid(resp) or _uid_from_location(...)
-# → "93b0b65d-e080-416d-bbb8-463f7adbda80"
-```
-
-### Шаг 7: Бэкенд — создание операции
-
-```python
-op_payload = {"instanceUid": "93b0b65d-...", "operation": "create"}
-op_resp = client.post("/instanceOperations", op_payload)
-# Ответ: {"instanceOperationUid": "0b0596d8-..."}
-op_uid = _find_uid(op_resp) or _uid_from_location(...)
-# _find_uid ищет: instanceOperationUid → instanceUid → uid → Uid
-# → "0b0596d8-b48d-4694-98c3-65f2f8eeb27d"
-```
-
-### Шаг 8: Бэкенд — tracker_add (v1.0.54: ДО params!)
-
-```python
-try:
- tracker_add("93b0b65d-...", 1, "autotest-1-lq5x3a")
-except Exception as e:
- print(f"[TRACKER ERROR] ...")
-```
-
-`tracker.py`: `_locked_read()` → `data["93b0b65d-..."] = {...}` → `_locked_write(data)`.
-Файл `/tmp/instances.json` обновлён под `fcntl.LOCK_EX | LOCK_NB`.
-
-### Шаг 9: Бэкенд — установка параметров и запуск
-
-```python
-for pid, pval in params.items():
- client.post("/instanceOperationCfsParams", {
- "instanceOperationUid": op_uid,
- "svcOperationCfsParamId": int(pid),
- "paramValue": str(pval)
- })
-client.post(f"/instanceOperations/{op_uid}/run")
-```
-
-Если params пустые (например, повторный клик на «Готово») → `/run` → 422 "required CFS parameter ... is missing".
-
-### Шаг 10: Бэкенд — фоновый поток
-
-```python
-threading.Thread(
- target=_finish_op,
- args=(client, op_uid, instance_uid, svc_id, display_name, op_name, svc_op_id, True),
- daemon=True
-).start()
-return jsonify({"status": "RUNNING", "opUid": op_uid, "instanceUid": instance_uid})
-```
-
-### Шаг 11: UI — поллинг статуса
-
-```javascript
-// index.html: executeOp() — после получения RUNNING
-pollTimer = setInterval(async () => {
- const sr = await fetch('/api/test/status/' + opUid);
- const sd = await sr.json();
- showStages(sd.stages || []);
- if (sd.status !== 'RUNNING') {
- stopPoll();
- // показать OK/FAIL
- btn.textContent = 'Готово';
- btn.disabled = false;
- // ⚠️ БАГ: btn.onclick всё ещё активен!
- if (sd.status === 'OK') {
- await refreshInstances();
- }
- }
-}, 2000);
-```
-
-### Шаг 12: Бэкенд — _finish_op (фоновый)
-
-```python
-while time.time() < deadline: # 300 секунд
- data = client.get(f"/instanceOperations/{op_uid}?fields=...")
- op = data.get("instanceOperation", {})
- dt_finish = op.get("dtFinish")
-
- _op_results[op_uid] = {"status": "RUNNING", "stages": [...], "duration": ...}
-
- if dt_finish and str(dt_finish).strip():
- is_ok = op.get("isSuccessful")
- # tracker_add уже вызван синхронно!
- if is_ok and is_delete:
- tracker_remove(instance_uid)
- _op_results[op_uid] = {"status": "OK" if is_ok else "FAIL", ...}
- return
- time.sleep(5)
-```
-
-Весь цикл обёрнут в `try/except: print(traceback)` (v1.0.51).
-
-### Шаг 13: UI — refreshInstances
-
-```javascript
-async function refreshInstances(){
- const r = await fetch('/api/operations/1');
- const d = await r.json();
- const newInsts = d.instances || [];
- // Найти новые (отсутствуют в svcInstances)
- const added = newInsts.filter(i => !oldUids.has(i.instanceUid));
- // Обновить бейджи существующих
- // Добавить новые в DOM перед кнопкой "+ Создать"
-}
-```
-
-### Шаг 14: Бэкенд — api_operations (GET /api/operations/1)
-
-```python
-tracked = tracker_list() # читает /tmp/instances.json
-tracked_by_uid = {uid: item for uid in tracked if svcId == 1}
-instances = get_instances(_client()) # запрос в Nubes API
-nubes_uids = {i["instanceUid"] for i in instances}
-
-# Инстансы из Nubes которые есть в трекере (кроме deleted)
-svc_instances = [i for i in instances
- if i.get("instanceUid") in tracked_uids
- and i.get("explainedStatus") not in ("deleted",)]
-
-# Инстансы из трекера которых Nubes ещё не отдаёт → status "creating"
-for uid, t in tracked_by_uid.items():
- if uid not in nubes_uids:
- svc_instances.append({
- "instanceUid": uid,
- "displayName": t["displayName"],
- "explainedStatus": "creating",
- ...
- })
-```
+- Файл: `/tmp/instances-{clientId}-{stand}.json` (изолирован по пользователю и стенду)
+- Блокировка: `fcntl.flock(LOCK_EX | LOCK_NB)` + retry 2s
+- `add(client_id, stand, uid, svc_id, name)`, `remove(...)`, `list_all(...)`
+- Нужен ТОЛЬКО как fallback — когда инстанс только создан и ещё не в `/instances`
---
-## 4. Трекер инстансов
+## 4. Бэкенд: Роуты
-### 4.1 Назначение
+### `main.py`
-Хранит UID инстансов, созданных приложением. Нужен чтобы показывать в UI только «наши» инстансы, а не все в организации.
+- **`GET/POST /`** — главная: организация, инфраструктура (serviceId 2,12,19,21,22,25,26,29,110,150), сервисы
+ - `action=save` → сохранить токен в cookie
+ - `action=clear` → удалить cookie
+- **`GET /api/operations/`** — операции + autotest-инстансы:
+ - Cloud-first: фильтр по префиксу `autotest-`
+ - Tracker-fallback: статус "creating" для ещё невидимых
-### 4.2 Реализация (v1.0.53+)
+### `api_test.py` — основной модуль операций
-Файл: `site/operations/tracker.py`
+- **`GET /api/params/[?instanceUid=xxx]`**:
+ - Без `instanceUid` → шаблонные `defaultValue`
+ - С `instanceUid` → `get_params_with_current_values()`
+- **`POST /api/test`** — запуск операции:
+ - **CREATE**: `POST /instances` → `instanceUid` → `POST /instanceOperations` → `opUid` → `tracker_add` → params → `/run`
+ - **non-CREATE**: `POST /instanceOperations` → params → `/run` (redeploy: сразу `/run`)
+ - `displayName` = `_get_instance_display_name()` (из API, не UUID!)
+ - Фоновый поток: `_finish_op()` — поллинг до `dtFinish` (300s таймаут)
+- **`GET /api/test/status/`** — поллинг: in-memory `_op_results` → прямой API
+- **`GET /api/log`** — последние 200 строк из `/tmp/app-autotest.log`
-- Хранилище: `/tmp/instances.json` (JSON-файл)
-- Блокировка: `fcntl.flock(fd, LOCK_EX | LOCK_NB)` с retry до 2 сек
-- Seed: `_INITIAL` (4 инстанса) при пустом/битом файле
-- Функции: `add(uid, svc_id, name)`, `remove(uid)`, `list_all()`
+### Логирование
-### 4.3 Эволюция
-
-| Версия | Реализация | Проблема |
-|--------|-----------|----------|
-| v1.0.45 | `/tmp/instances.json` + `threading.Lock` | `except: pass` скрывал ошибки |
-| v1.0.49 | In-memory dict | Multi-worker gunicorn: у каждого свой dict |
-| v1.0.53 | `/tmp/instances.json` + `fcntl.flock(LOCK_EX)` | Без таймаута: потенциальный зависон |
-| v1.0.54 | `/tmp/instances.json` + `fcntl.flock(LOCK_EX\|LOCK_NB)` + retry 2s | ✅ Текущий |
+`_log(msg)` в `api_test.py`:
+- stdout (gunicorn) + `/tmp/app-autotest.log` (flock, общий для воркеров)
+- Ротация при 512 КБ
+- UI: скрытая панель, кнопка `log` (правый нижний угол)
---
-## 5. Автоопределение стенда
+## 5. Фронтенд
-Файл: `site/api/http_client.py`
+### Структура
-```python
-STANDS = [
- "https://lk-api-gateway-dev.ngcloud.ru/api/v1/svc",
- "https://lk-api-gateway-test.ngcloud.ru/api/v1/svc",
-]
+Один HTML-файл, три колонки:
+- Левая (280px): инфраструктура
+- Средняя (240px): сервисы
+- Правая (flex): инстансы + параметры + кнопка + этапы
-def detect_endpoint(token):
- for ep in STANDS:
- try:
- c = HttpClient(ep, token)
- data = c.get("/instances", params={"pageSize": 1, "page": 1})
- if data.get("results") is not None:
- return ep
- except Exception:
- continue
- return None
+### Глобальное состояние (JS)
+
+```
+svcInstances — кеш инстансов (из /api/operations)
+selectedInst — UID выбранного инстанса
+selectedOp — {opId, opName, svcId}
+pollTimer — таймер поллинга
+SVC_ID = 1 — фиксированный сервис (Болванка)
```
-Используется в:
-- `main.py`: при загрузке главной страницы
-- `api_test.py: _client()`: при каждом API-запросе
+### Потоки операций
+
+**CREATE**: `startCreate()` → `showParams(18,'create')` → форма → `executeOp(pp)` → POST /api/test → поллинг → refreshInstances
+
+**MODIFY**: `toggleInstance()` → кнопки → `runOp('modify',opId)` → `showParams(opId,'modify')` → GET /api/params?instanceUid= → форма с ТЕКУЩИМИ значениями → валидация map → `executeOp(pp)` → POST /api/test → поллинг
+
+**Без параметров** (suspend/delete/resume/redeploy): `runOp()` → confirm → `executeOp({})` → POST /api/test → поллинг
+
+### Валидация JSON
+
+- `_esc(s)` — HTML-экранирование (`"` → `"`, `&` → `&`, `<` → `<`)
+- `validateJson(el, quiet)` — `JSON.parse()` на `onblur` (красная рамка + текст)
+- Batch-проверка перед отправкой — ошибка → запрос не уходит
---
-## 6. Все известные баги (найдено/исправлено)
+## 6. Nubes API — используемые endpoint'ы
-### Исправленные
-
-| # | Версия | Баг | Причина | Исправление |
-|---|--------|-----|---------|-------------|
-| 1 | v1.0.46 | `_finish_op()` недостающие параметры | Не передавались `op_name`, `svc_op_id` | Добавлены в сигнатуру |
-| 2 | v1.0.48 | displayName терялся | `params-form.innerHTML=''` до чтения `param-displayname` | Захват до очистки |
-| 3 | v1.0.48 | tracker_add в daemon-потоке | Поток умирает под gunicorn | Перенос в синхронный код |
-| 4 | v1.0.50 | ❌ для этапов в процессе | `isSuccessful===false` для in-progress | Проверка `dtFinish` |
-| 5 | v1.0.50 | Инстанс не в списке | `explainedStatus:"not created"` фильтровался | Убран фильтр + tracked-сироты |
-| 6 | v1.0.50 | `selectService()` скрывал stages | Полный перерендер после OK | `refreshInstances()` с диффом |
-| 7 | v1.0.51 | `instance_groups` UnboundLocalError | Не иниц. до `if active_token:` | `instance_groups = {}` |
-| 8 | v1.0.51 | `_finish_op` молча умирал | `data.get()` вне try/except | Весь цикл в try/except |
-| 9 | v1.0.51 | Автостенд только в main.py | `_client()` в api_test.py не использовал | `detect_endpoint()` в http_client.py |
-| 10 | v1.0.52 | `token_info` UndefinedError | Не передан в шаблон при action=clear | `_tmpl()` helper со всеми переменными |
-| 11 | v1.0.53 | In-memory dict + multi-worker | У каждого воркера свой `_data` | `/tmp/instances.json` + `fcntl.flock` |
-| 12 | v1.0.54 | `_find_uid()` возвращал не тот UUID | Итерация по всем значениям dict | Поиск по ключам: instanceOperationUid→instanceUid |
-| 13 | v1.0.54 | Сироты при ошибке params | `tracker_add` ПОСЛЕ params loop | `tracker_add` ДО params loop |
-| 14 | v1.0.54 | `flock` без таймаута | `LOCK_EX` блокируется навсегда | `LOCK_EX \| LOCK_NB` + retry 2s |
-| 15 | v1.0.55 | F5 = предупреждение браузера | clear/stray POST → HTML без редиректа | `redirect("/")` для всех POST |
-| 16 | v1.0.56 | Кнопка «Готово» запускала повторный CREATE | `btn.onclick` оставался привязанным после завершения операции | `btn.onclick = null` после финального статуса |
-| 17 | v1.0.57 | Хрупкость финального состояния кнопки | Повторяемый код финализации статуса | `setFinishedState()` централизует финальный UI-состояние |
-| 18 | v1.0.59 | После F5 список терял tracked-инстанс | `api_operations()` не добирал tracked-сирот обратно из трекера | `tracked_by_uid` + добавление отсутствующих tracked-инстансов |
-| 19 | v1.0.60 | CREATE не появлялся сразу в списке после OK | UI делал diff и мог не перерисовать список целиком | Полная перерисовка `inst-list` в `refreshInstances()` |
-| 20 | v1.0.63 | Дубли и чужие инстансы | Принадлежность определялась не по namespace | `autotest-` prefix + уникализация `displayName` при CREATE |
-| 21 | v1.0.64 | Версия не показывалась в шапке | `load_config()` затирал VERSION | `config["VERSION"] = current_app.config["VERSION"]` |
-| 22 | v1.0.64 | displayName был editable value, а не placeholder | CREATE подставлял имя в value | Placeholder `autotest-...`, значение вводит пользователь |
-| 23 | v1.0.65 | `creating` из-за недочитанной страницы | `get_instances()` доверял `total` и мог не перейти на следующую страницу | Остановка по `len(batch) < pageSize` |
-
-### ✅ Закрыто в v1.0.56
-
-| Баг | Статус | Что поменяли |
-|-----|--------|--------------|
-| Кнопка «Готово» запускала повторный CREATE | закрыто | После финального статуса обработчик снимается через `btn.onclick = null` |
-| displayName "autotest-1" при повторном клике | закрыто | Повторный клик больше не вызывает `executeOp()` на очищенной форме |
+| Метод | Путь | Назначение |
+|-------|------|-----------|
+| GET | `/instances?pageSize=200&page=N` | Все инстансы (пагинация) |
+| GET | `/instances/{uid}` | Детали + state.params |
+| GET | `/services` | Список сервисов |
+| GET | `/services/{id}` | Детали + операции |
+| GET | `/instanceOperations/default/{opId}` | Шаблон параметров |
+| POST | `/instances` | Создать инстанс |
+| POST | `/instanceOperations` | Создать операцию |
+| POST | `/instanceOperationCfsParams` | Установить параметр |
+| POST | `/instanceOperations/{opUid}/run` | Запустить операцию |
+| GET | `/instanceOperations/{opUid}?fields=...` | Поллинг статуса |
---
-## 7. HTTP-клиент
+## 7. Ограничения платформы
-Файл: `site/api/http_client.py`
-
-```python
-class HttpClient:
- def __init__(self, endpoint, token):
- # Bearer auth + User-Agent: Mozilla/5.0 (DDoS-Guard)
-
- def get(self, path, timeout=10):
- # GET → r.raise_for_status() → r.json()
-
- def post(self, path, data, timeout=30):
- # POST → проверка r.ok → парсинг JSON → Location header
-```
-
----
-
-## 8. Шаблон Jinja2 — используемые переменные
-
-| Переменная | Где | Назначение |
-|-----------|-----|-----------|
-| `config.VERSION` | Строка 32 | Версия в топбаре |
-| `token_info.email` | Строка 33 | Email пользователя |
-| `token_info.company` | Строка 33 | Компания |
-| `env_token_masked` | Строка 36 | Маскированный токен в placeholder |
-| `has_user_token` | Строка 36 | Показать токен или маску |
-| `error` | Строка 40 | Ошибка |
-| `organization.displayName` | Строка 50 | Название организации |
-| `organization.explainedStatus` | Строка 51 | Статус организации |
-| `client_id` | Строка 50 | ID клиента |
-| `instance_groups` | Строки 55-62 | Группы инфраструктурных инстансов |
-
----
-
-## 9. Ограничения платформы Nubes pythonk8s
-
-- `site/` — НЕ пакет (без `__init__.py`, конфликт с stdlib `site.py`)
-- Импорты: `from api.http_client import ...` (без префикса `site.`)
-- `app.run(host="0.0.0.0", port=5000)` — обязательно
-- Gunicorn: запускается платформой, количество воркеров неизвестно
-- Нет persistent volume — `/tmp/` теряется при редеплое
+- `site/` — НЕ пакет (без `__init__.py`), импорты без префикса `site.`
+- `app.run(host="0.0.0.0", port=5000)`
+- Gunicorn multi-worker → общие данные через файлы + flock
+- `/tmp/` теряется при редеплое
- User-Agent: `Mozilla/5.0` обязателен (DDoS-Guard)
-- Деплой: `git push` → managed service редеплоит
diff --git a/DOCS/HISTORY.md b/DOCS/HISTORY.md
index 20537f3..822d7e5 100644
--- a/DOCS/HISTORY.md
+++ b/DOCS/HISTORY.md
@@ -1,28 +1,56 @@
# История разработки app-autotest
-## v1.0.76 (27.07.2026) — modify показывает актуальные значения параметров инстанса
+## v1.0.87 (27.07.2026) — документация: ARCHITECTURE.md переписан, legacy-доки помечены
+
+## v1.0.84 (27.07.2026) — лог-панель скрыта по умолчанию
### Что изменилось
-- В `site/routes/api_test.py` `GET /api/params/` теперь принимает `?instanceUid=xxx&opName=xxx`:
- - Создаёт pending-операцию через `POST /instanceOperations`, читает `GET /instanceOperations/{opUid}` и возвращает **текущие** `paramValue` из `cfsParams` вместо шаблонных `defaultValue`.
- - В ответ добавлен `previewOpUid` — UID созданной preview-операции.
-- В `site/routes/api_test.py` `POST /api/test` принимает опциональный `previewOpUid`:
- - Если передан — переиспользует уже созданную операцию, пропускает `POST /instanceOperations`.
-- В `site/templates/index.html` `showParams()` для не-create операций передаёт `?instanceUid=&opName=` в запрос параметров и сохраняет `previewOpUid`.
-- В `site/templates/index.html` `executeOp()` передаёт `previewOpUid` в теле запроса.
-- Версия поднята до `1.0.76`.
+- Лог-панель теперь `display:none`, показывается кнопкой `log` (правый нижний угол).
+- При скрытой панели `/api/log` не поллится.
+
+## v1.0.83 (27.07.2026) — HTML-escape + JSON-валидация map-полей
+
+### Что изменилось
+- `_esc(s)` — HTML-экранирование значений (`"` → `"`, `&` → `&`, `<` → `<`).
+- `validateJson(el, quiet)` — проверка `JSON.parse()` при `onblur` (красная рамка + текст ошибки).
+- Batch-проверка всех map-полей перед отправкой — ошибка → запрос не уходит.
### Что это чинит
-- Форма modify теперь показывает **реальные текущие значения** параметров инстанса (а не шаблонные `0`, `false`, `""`).
-- Раньше: `GET /instanceOperations/default/{opId}` → `defaultValue` из шаблона.
-- Теперь: `POST /instanceOperations` → `GET /instanceOperations/{opUid}` → `paramValue` из инстанса.
+- Поломку HTML при значениях с кавычками (напр. `{"f":1}`).
+- Отправку битого JSON в API.
-### Как это работает
-1. Пользователь нажимает modify → `showParams(opId, 'modify')`
-2. Фронт вызывает `GET /api/params/{opId}?instanceUid={iuid}&opName=modify`
-3. Бэкенд создаёт pending-операцию и читает её `cfsParams` с актуальными `paramValue`
-4. Форма заполняется реальными значениями, `previewOpUid` сохраняется
-5. При submit → `POST /api/test` с `previewOpUid` → переиспользуется та же операция, выставляются новые params → run
+## 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
diff --git a/DOCS/sonnet-full-code-review.md b/DOCS/sonnet-full-code-review.md
index ee3912f..539aa0d 100644
--- a/DOCS/sonnet-full-code-review.md
+++ b/DOCS/sonnet-full-code-review.md
@@ -1,3 +1,5 @@
+# ⚠️ LEGACY — НЕАКТУАЛЬНО. Исторический документ.
+
# Полный обзор кода app-autotest — запрос к Sonnet
Отправлено: 27.07.2026
diff --git a/DOCS/sonnet-missed-bugs.md b/DOCS/sonnet-missed-bugs.md
index 387b353..7becfd6 100644
--- a/DOCS/sonnet-missed-bugs.md
+++ b/DOCS/sonnet-missed-bugs.md
@@ -1,3 +1,5 @@
+# ⚠️ LEGACY — НЕАКТУАЛЬНО. Исторический документ.
+
# Sonnet — почему ты пропустил эти баги?
Отправлено: 27.07.2026, после v1.0.49 (твой анализ выполнен, НЕ ПОМОГЛО)
diff --git a/DOCS/sonnet-question-tracker.md b/DOCS/sonnet-question-tracker.md
index 4f348e9..eb591d4 100644
--- a/DOCS/sonnet-question-tracker.md
+++ b/DOCS/sonnet-question-tracker.md
@@ -1,3 +1,5 @@
+# ⚠️ LEGACY — НЕАКТУАЛЬНО. Исторический документ.
+
# Вопрос к Sonnet — архитектура трекера инстансов + UI кнопок
Отправлено: 27.07.2026
diff --git a/DOCS/sonnet-response-round2.md b/DOCS/sonnet-response-round2.md
index 0dbbe9e..fadd1e6 100644
--- a/DOCS/sonnet-response-round2.md
+++ b/DOCS/sonnet-response-round2.md
@@ -1,3 +1,5 @@
+# ⚠️ LEGACY — НЕАКТУАЛЬНО. Исторический документ.
+
# Sonnet — ответ 27.07.2026 (v1.0.49, раунд 2)
## Найденные баги
diff --git a/DOCS/step-by-step-audit-v1.0.50.md b/DOCS/step-by-step-audit-v1.0.50.md
index f8bde83..2466b33 100644
--- a/DOCS/step-by-step-audit-v1.0.50.md
+++ b/DOCS/step-by-step-audit-v1.0.50.md
@@ -1,3 +1,5 @@
+# ⚠️ LEGACY — НЕАКТУАЛЬНО. Исторический документ.
+
# Пошаговый аудит кода — 27.07.2026 (v1.0.50)
## Методология
diff --git a/site/app.py b/site/app.py
index 141f4c1..0e2c2d5 100644
--- a/site/app.py
+++ b/site/app.py
@@ -6,7 +6,7 @@ from routes.main import bp as main_bp
from routes.api import bp as api_bp
from routes.api_test import bp as api_test_bp
-VERSION = "1.0.86"
+VERSION = "1.0.88"
app = Flask(__name__, template_folder="templates", static_folder="static")
app.config["NUBES_API_ENDPOINT"] = os.getenv("NUBES_API_ENDPOINT", "https://lk-api-gateway-dev.ngcloud.ru/api/v1/svc")