diff --git a/DOCS/ARCHITECTURE.md b/DOCS/ARCHITECTURE.md
index 67de0dc..ff24fe1 100644
--- a/DOCS/ARCHITECTURE.md
+++ b/DOCS/ARCHITECTURE.md
@@ -1,6 +1,6 @@
# Архитектура app-autotest — полный документ
-v1.0.69, 27.07.2026
+v1.0.71, 27.07.2026
---
diff --git a/DOCS/HISTORY.md b/DOCS/HISTORY.md
index 99cc710..f6d84a6 100644
--- a/DOCS/HISTORY.md
+++ b/DOCS/HISTORY.md
@@ -1,6 +1,24 @@
# История разработки app-autotest
-# v1.0.69 (27.07.2026) — autotest prefix fixed in CREATE field
+# v1.0.71 (27.07.2026) — finished status shows instance name
+
+### Что изменилось
+- В `site/templates/index.html` финальная строка после `OK/FAIL` теперь показывает имя инстанса.
+- Имя операции и длительность остаются рядом, отдельно от имени инстанса.
+
+### Что это чинит
+- Ситуацию, когда после завершения delete/modify было видно только название операции, но не ясно, над каким инстансом она выполнялась.
+
+# v1.0.70 (27.07.2026) — dedupe autotest instances by displayName
+
+### Что изменилось
+- В `site/routes/main.py` сервисный список теперь склеивает записи по `displayName`.
+- Если cloud уже отдал `running`, он выигрывает у tracker-fallback `creating`.
+
+### Что это чинит
+- Дубли одной и той же autotest-записи после CREATE, когда в списке появлялись и `running`, и `creating`.
+
+## v1.0.69 (27.07.2026) — autotest prefix fixed in CREATE field
### Что изменилось
- В `site/templates/index.html` префикс `autotest-` отображается отдельным фиксированным текстом.
diff --git a/site/app.py b/site/app.py
index 5092d78..cee0b82 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.69"
+VERSION = "1.0.71"
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/main.py b/site/routes/main.py
index e1941e5..c3fb6d8 100644
--- a/site/routes/main.py
+++ b/site/routes/main.py
@@ -156,7 +156,19 @@ 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 = []
+ 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
+
for i in instances:
if not str(i.get("displayName", "")).startswith(AUTOTEST_PREFIX):
continue
@@ -164,11 +176,11 @@ def api_operations(svc_id):
continue
item = dict(i)
item["status"] = _resolve_instance_status(item, tracked_by_uid.get(i.get("instanceUid")))
- svc_instances.append(item)
+ _store_instance(item)
# Если Nubes ещё не отдаёт только что созданный autotest-инстанс, добавляем его из трекера.
for uid, t in tracked_by_uid.items():
if uid not in nubes_uids:
- svc_instances.append({
+ _store_instance({
"instanceUid": uid,
"displayName": t["displayName"],
"serviceId": svc_id,
@@ -176,6 +188,7 @@ def api_operations(svc_id):
"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 975c927..bac3aa4 100644
--- a/site/templates/index.html
+++ b/site/templates/index.html
@@ -242,7 +242,7 @@ function showParams(opId,opName){
});
}
-function setFinishedState(statusText, statusClass, statusError, statusDuration){
+function setFinishedState(statusText, statusClass, statusError, statusDuration, instanceName){
const btn=document.getElementById('btn-test');
btn.disabled=false;
btn.textContent='Готово';
@@ -250,7 +250,8 @@ function setFinishedState(statusText, statusClass, statusError, statusDuration){
const opName=selectedOp?.opName||'операция';
const durationText=statusDuration!==undefined&&statusDuration!==null?`${statusDuration}s`:'0s';
const extra=statusError||'';
- document.getElementById('test-status').innerHTML=` ${statusText} ${opName} ${durationText} ${extra}`;
+ const instName=instanceName||findInstName()||selectedInst||'';
+ document.getElementById('test-status').innerHTML=` ${statusText} ${opName} ${durationText} ${instName} ${extra}`;
}
function setRunningState(opName, displayName){
@@ -289,13 +290,14 @@ async function executeOp(params){
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;
pollTimer=setInterval(async()=>{
const sr=await fetch('/api/test/status/'+opUid);
const sd=await sr.json();
showStages(sd.stages||[]);
if(sd.status!=='RUNNING'){
stopPoll();
- setFinishedState(sd.status, sd.status==='OK'?'badge-success':'badge', sd.error, sd.duration);
+ setFinishedState(sd.status, sd.status==='OK'?'badge-success':'badge', sd.error, sd.duration, instanceName);
if(sd.status==='OK'){
await refreshInstances();
}