diff --git a/DOCS/ARCHITECTURE.md b/DOCS/ARCHITECTURE.md index ff24fe1..30b4696 100644 --- a/DOCS/ARCHITECTURE.md +++ b/DOCS/ARCHITECTURE.md @@ -1,6 +1,6 @@ # Архитектура app-autotest — полный документ -v1.0.71, 27.07.2026 +v1.0.74, 27.07.2026 --- @@ -10,6 +10,12 @@ Flask-приложение для автотестов операций серв Деплой: 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. + --- ## 2. Структура файлов diff --git a/DOCS/HISTORY.md b/DOCS/HISTORY.md index f6d84a6..5c291e9 100644 --- a/DOCS/HISTORY.md +++ b/DOCS/HISTORY.md @@ -1,5 +1,28 @@ # История разработки app-autotest +# 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 ### Что изменилось diff --git a/site/app.py b/site/app.py index cee0b82..ef3ac33 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.71" +VERSION = "1.0.74" 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") diff --git a/site/routes/api_test.py b/site/routes/api_test.py index 5a9599c..a6575c2 100644 --- a/site/routes/api_test.py +++ b/site/routes/api_test.py @@ -129,12 +129,14 @@ def api_test(): svc_op_id = data["svcOperationId"] params = data.get("params", {}) instance_uid = data.get("instanceUid") - display_name = data.get("displayName", f"autotest-{svc_id}") + display_name = data.get("displayName") or "" client = _client() try: if op_name == "create": + if not display_name: + display_name = f"autotest-{svc_id}" display_name = _unique_display_name(client, display_name) payload = {"serviceId": svc_id, "displayName": display_name, "descr": ""} resp = client.post("/instances", payload) @@ -190,8 +192,9 @@ def api_test(): # фоном ждать завершения is_delete = (op_name == "delete") + display_name = display_name or instance_uid or f"instance-{svc_id}" threading.Thread(target=_finish_op, args=(client, op_uid, instance_uid, svc_id, display_name, op_name, svc_op_id, False, is_delete), daemon=True).start() - return jsonify({"status": "RUNNING", "opUid": op_uid, "instanceUid": instance_uid}) + return jsonify({"status": "RUNNING", "opUid": op_uid, "instanceUid": instance_uid, "displayName": display_name}) except Exception as e: import traceback diff --git a/site/routes/main.py b/site/routes/main.py index c3fb6d8..2112aba 100644 --- a/site/routes/main.py +++ b/site/routes/main.py @@ -156,39 +156,35 @@ def api_operations(svc_id): tracked_by_uid = {t["instanceUid"]: t for t in tracked if t["svcId"] == svc_id} instances = get_instances(client) nubes_uids = {i["instanceUid"] for i in instances} - svc_instances_by_name = {} - def _store_instance(item): - display_name = str(item.get("displayName", "") or "") - if not display_name.startswith(AUTOTEST_PREFIX): - return - existing = svc_instances_by_name.get(display_name) - if existing is None: - svc_instances_by_name[display_name] = item - return - if existing.get("status") == "creating" and item.get("status") != "creating": - svc_instances_by_name[display_name] = item + svc_instances = [] + cloud_names = set() for i in instances: - if not str(i.get("displayName", "")).startswith(AUTOTEST_PREFIX): + display_name = str(i.get("displayName", "") or "") + if not display_name.startswith(AUTOTEST_PREFIX): continue if i.get("explainedStatus") in ("deleted",): continue item = dict(i) item["status"] = _resolve_instance_status(item, tracked_by_uid.get(i.get("instanceUid"))) - _store_instance(item) - # Если Nubes ещё не отдаёт только что созданный autotest-инстанс, добавляем его из трекера. + svc_instances.append(item) + cloud_names.add(display_name) + + # tracker — только временный fallback, если cloud ещё не отдал конкретный autotest-инстанс for uid, t in tracked_by_uid.items(): if uid not in nubes_uids: - _store_instance({ + display_name = str(t.get("displayName", "") or "") + if display_name in cloud_names: + continue + svc_instances.append({ "instanceUid": uid, - "displayName": t["displayName"], + "displayName": display_name, "serviceId": svc_id, "svc": detail.get("svc", ""), "explainedStatus": "", "status": "creating", }) - svc_instances = list(svc_instances_by_name.values()) return jsonify({"svc": detail.get("svc", ""), "operations": ops, "instances": svc_instances}) except Exception as e: return jsonify({"error": str(e)}), 500 diff --git a/site/templates/index.html b/site/templates/index.html index bac3aa4..5cff956 100644 --- a/site/templates/index.html +++ b/site/templates/index.html @@ -263,10 +263,10 @@ function setRunningState(opName, displayName){ async function executeOp(params){ stopPoll(); - // Захватить displayName ДО очистки формы + const isCreate=selectedOp?.opName==='create'; const displayNameInput=document.getElementById('param-displayname'); - const displayNameSuffix=(displayNameInput?.value||'').trim() || 'tut'; - const displayName=`${AUTOTEST_PREFIX}${displayNameSuffix}`; + const displayNameSuffix=(displayNameInput?.value||'').trim(); + const displayName=isCreate ? `${AUTOTEST_PREFIX}${displayNameSuffix || makeCreateDisplayName()}` : findInstName(); document.getElementById('params-card').style.display='block'; document.getElementById('params-form').innerHTML=''; const btn=document.getElementById('btn-test'); @@ -284,13 +284,13 @@ async function executeOp(params){ svcOperationId:selectedOp.opId, params, instanceUid:selectedInst||'', - displayName + displayName:isCreate ? displayName : '' })}); const d=await r.json(); if(d.status==='FAIL'){document.getElementById('test-status').innerHTML=` FAIL ${d.error||''}`;btn.disabled=false;return;} // start polling const opUid=d.opUid; - const instanceName=d.displayName||displayName; + const instanceName=d.displayName||displayName||findInstName(); pollTimer=setInterval(async()=>{ const sr=await fetch('/api/test/status/'+opUid); const sd=await sr.json();