From 7180587538c69faf5e266b0a18786b9ed0f89eee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CNaeel=E2=80=9D?= Date: Mon, 27 Jul 2026 21:56:35 +0400 Subject: [PATCH] v1.0.88: new ARCHITECTURE.md (current state) + HISTORY updated + legacy docs marked --- DOCS/ARCHITECTURE.legacy.md | 417 +++++++++++++++++++++++++ DOCS/ARCHITECTURE.md | 486 ++++++++--------------------- DOCS/HISTORY.md | 64 ++-- DOCS/sonnet-full-code-review.md | 2 + DOCS/sonnet-missed-bugs.md | 2 + DOCS/sonnet-question-tracker.md | 2 + DOCS/sonnet-response-round2.md | 2 + DOCS/step-by-step-audit-v1.0.50.md | 2 + site/app.py | 2 +- 9 files changed, 603 insertions(+), 376 deletions(-) create mode 100644 DOCS/ARCHITECTURE.legacy.md 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")