v1.1.14: serviceId filter, svcShort displayName, descr, remove duplicate endpoint
This commit is contained in:
@@ -1,41 +1,39 @@
|
|||||||
# ⚠️ LEGACY — НЕАКТУАЛЬНО. Исторический документ.
|
# Sonnet 4.6 — Round 2 Response
|
||||||
|
|
||||||
# Sonnet — ответ 27.07.2026 (v1.0.49, раунд 2)
|
Дата: 2026-07-29
|
||||||
|
|
||||||
## Найденные баги
|
## Ответы
|
||||||
|
|
||||||
### Баг #1: showStages() — ❌ для этапов в процессе
|
### 1. Фильтр serviceId ✅
|
||||||
Файл: `site/templates/index.html`
|
Добавить `if i.get("serviceId") != svc_id: continue` — безопасно.
|
||||||
Nubes возвращает `isSuccessful: false` для этапов «в процессе». Код:
|
Tracker-fallback уже отфильтрован по svc_id, не сломается.
|
||||||
```javascript
|
Даже улучшит дедупликацию cloud_names.
|
||||||
const icon=ok===true?'✅':ok===false?'❌':'⏳';
|
|
||||||
```
|
|
||||||
`false === false` → ❌. Должно быть ⏳.
|
|
||||||
|
|
||||||
Исправление: проверять `s.dtFinish`. Если нет → ⏳, если есть и `true` → ✅, если есть и `false` → ❌.
|
### 2. Генерация 3 символов
|
||||||
|
Надёжнее: `(Math.random() * 46656 | 0).toString(36).padStart(3, '0')`
|
||||||
|
Ровно 3 символа, равномерно по 46 656 комбинациям.
|
||||||
|
|
||||||
### Баг #2: api_operations() — фильтр теряет новые инстансы
|
### 3. descr
|
||||||
Файлы: `site/routes/api_test.py` + `site/routes/main.py`
|
Не нужно тащить через JS! `app_version` уже есть в той же функции.
|
||||||
После `tracker_add` (мгновенно in-memory) инстанс есть в трекере, но Nubes API ещё не обновил `explainedStatus` — инстанс имеет статус `"not created"`. Фильтр:
|
Одна строка: `"descr": f"created by autotest v{app_version}"`
|
||||||
```python
|
|
||||||
and i.get("explainedStatus") not in ("deleted", "not created")
|
|
||||||
```
|
|
||||||
отбрасывает его. Инстанс есть в трекере, есть в ответе API — но статус "not created" → фильтр удаляет.
|
|
||||||
|
|
||||||
Исправление: для tracked UID не применять фильтр `"not created"`. Добавлять отсутствующие из трекера напрямую.
|
### 4. Пропущенный баг: stand-ключ трекера
|
||||||
|
main.py → `stand_name(endpoint)`
|
||||||
|
api_test.py → `get_stand()`
|
||||||
|
Могут разойтись → tracker_add и tracker_list под разными ключами → creating инстансы не видны.
|
||||||
|
|
||||||
### Баг #3: Дублирующийся route
|
### 5. Дубликат эндпоинта
|
||||||
`main_bp` зарегистрирован ПЕРВЫМ, `api_test_bp` — ВТОРЫМ. Оба определяют `GET /api/operations/<int:svc_id>`. Flask использует первое совпадение → `main.api_operations` вызывается, `api_test.api_operations` — никогда.
|
Удалить `/api/operations/{svc_id}` из api_test.py (строки 108-148).
|
||||||
|
Недостижим, старый код, техдолг.
|
||||||
|
|
||||||
### Потенциальная проблема #4: Multi-worker gunicorn
|
## Итоговый план исправлений
|
||||||
In-memory dict НЕ работает при >1 gunicorn worker. Один worker добавляет в свой `_data`, другой читает из своего. Если pythonk8s запускает >1 worker — ни одно решение не сработает.
|
|
||||||
|
|
||||||
Нужно проверить логи пода на `Booting worker with pid`.
|
| # | Файл | Что |
|
||||||
|
|---|------|-----|
|
||||||
---
|
| 1 | main.py:194 | `if i.get("serviceId") != svc_id: continue` |
|
||||||
|
| 2 | main.py:227 | `"svcShort": detail.get("svcShort", "")` |
|
||||||
## Исправления (v1.0.50)
|
| 3 | app.js:53 | `currentSvcShort = d.svcShort\|\|''` |
|
||||||
|
| 4 | app.js:113-116 | Переписать `makeCreateDisplayName()` |
|
||||||
1. `showStages()`: использовать `dtFinish` этапа
|
| 5 | api_test.py:212 | `f"created by autotest v{app_version}"` |
|
||||||
2. `api_operations()`: не фильтровать "not created" для tracked UID, добавлять сирот из трекера
|
| 6 | api_test.py:108-148 | Удалить дубликат эндпоинта |
|
||||||
3. Убрать дублирующийся route
|
| 7 | main.py + api_test.py | Унифицировать stand |
|
||||||
|
|||||||
+1
-1
@@ -18,7 +18,7 @@ from routes.api import bp as api_bp
|
|||||||
from routes.api_test import bp as api_test_bp
|
from routes.api_test import bp as api_test_bp
|
||||||
|
|
||||||
# Версия — меняется при КАЖДОМ изменении кода. Показывается в топбаре UI.
|
# Версия — меняется при КАЖДОМ изменении кода. Показывается в топбаре UI.
|
||||||
VERSION = "1.1.13"
|
VERSION = "1.1.14"
|
||||||
|
|
||||||
app = Flask(__name__, template_folder="templates", static_folder="static")
|
app = Flask(__name__, template_folder="templates", static_folder="static")
|
||||||
app.config["NUBES_API_ENDPOINT"] = os.getenv("NUBES_API_ENDPOINT", "https://lk-api-gateway-test.ngcloud.ru/api/v1/svc")
|
app.config["NUBES_API_ENDPOINT"] = os.getenv("NUBES_API_ENDPOINT", "https://lk-api-gateway-test.ngcloud.ru/api/v1/svc")
|
||||||
|
|||||||
+2
-34
@@ -115,39 +115,6 @@ def api_instances_list():
|
|||||||
return jsonify({"error": str(e)}), 500
|
return jsonify({"error": str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/api/operations/<int:svc_id>")
|
|
||||||
def api_operations(svc_id):
|
|
||||||
try:
|
|
||||||
detail = get_service_detail(get_client(), svc_id)
|
|
||||||
ops = detail.get("operations", [])
|
|
||||||
# только отслеживаемые инстансы этого сервиса
|
|
||||||
tracked = tracker_list(get_client_id(), get_stand())
|
|
||||||
tracked_by_uid = {t["instanceUid"]: t for t in tracked if t["svcId"] == svc_id}
|
|
||||||
tracked_uids = set(tracked_by_uid.keys())
|
|
||||||
instances = get_instances(get_client())
|
|
||||||
nubes_uids = {i["instanceUid"] for i in instances}
|
|
||||||
svc_instances = [i for i in instances
|
|
||||||
if i.get("instanceUid") in tracked_uids
|
|
||||||
and i.get("explainedStatus") not in ("deleted",)]
|
|
||||||
# Добавить tracked инстансы, которых Nubes ещё не отдаёт (creating)
|
|
||||||
for uid, t in tracked_by_uid.items():
|
|
||||||
if uid not in nubes_uids:
|
|
||||||
svc_instances.append({
|
|
||||||
"instanceUid": uid,
|
|
||||||
"displayName": t["displayName"],
|
|
||||||
"serviceId": svc_id,
|
|
||||||
"svc": detail.get("svc", ""),
|
|
||||||
"explainedStatus": "creating",
|
|
||||||
})
|
|
||||||
return jsonify({
|
|
||||||
"svc": detail.get("svc", ""),
|
|
||||||
"operations": [{"svcOperationId": o["svcOperationId"], "operation": o["operation"]} for o in ops],
|
|
||||||
"instances": svc_instances,
|
|
||||||
})
|
|
||||||
except Exception as e:
|
|
||||||
return jsonify({"error": str(e)}), 500
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/api/params/<int:op_id>")
|
@bp.route("/api/params/<int:op_id>")
|
||||||
def api_params(op_id):
|
def api_params(op_id):
|
||||||
"""Параметры операции. Если передан instanceUid — с текущими значениями из API."""
|
"""Параметры операции. Если передан instanceUid — с текущими значениями из API."""
|
||||||
@@ -209,7 +176,8 @@ def api_test():
|
|||||||
if not display_name:
|
if not display_name:
|
||||||
display_name = f"autotest-{svc_id}"
|
display_name = f"autotest-{svc_id}"
|
||||||
display_name = _unique_display_name(client, display_name)
|
display_name = _unique_display_name(client, display_name)
|
||||||
payload = {"serviceId": svc_id, "displayName": display_name, "descr": ""}
|
descr = f"created by autotest v{current_app.config.get('VERSION', '')}"
|
||||||
|
payload = {"serviceId": svc_id, "displayName": display_name, "descr": descr}
|
||||||
resp = client.post("/instances", payload)
|
resp = client.post("/instances", payload)
|
||||||
instance_uid = resp.get("instanceUid") or _find_uid(resp) or _uid_from_location(resp.get("_location", ""))
|
instance_uid = resp.get("instanceUid") or _find_uid(resp) or _uid_from_location(resp.get("_location", ""))
|
||||||
if not instance_uid:
|
if not instance_uid:
|
||||||
|
|||||||
@@ -194,6 +194,8 @@ def api_operations(svc_id):
|
|||||||
display_name = str(i.get("displayName", "") or "")
|
display_name = str(i.get("displayName", "") or "")
|
||||||
if not display_name.startswith(AUTOTEST_PREFIX):
|
if not display_name.startswith(AUTOTEST_PREFIX):
|
||||||
continue # не наш инстанс
|
continue # не наш инстанс
|
||||||
|
if i.get("serviceId") != svc_id:
|
||||||
|
continue # не нашего сервиса
|
||||||
if i.get("explainedStatus") in ("deleted",):
|
if i.get("explainedStatus") in ("deleted",):
|
||||||
continue # удалённые не показываем
|
continue # удалённые не показываем
|
||||||
item = dict(i)
|
item = dict(i)
|
||||||
@@ -220,6 +222,7 @@ def api_operations(svc_id):
|
|||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"svc": detail.get("svc", ""),
|
"svc": detail.get("svc", ""),
|
||||||
|
"svcShort": detail.get("svcShort", ""),
|
||||||
"operations": ops, # [{svcOperationId, operation}, ...]
|
"operations": ops, # [{svcOperationId, operation}, ...]
|
||||||
"instances": svc_instances, # [{instanceUid, displayName, status, ...}, ...]
|
"instances": svc_instances, # [{instanceUid, displayName, status, ...}, ...]
|
||||||
})
|
})
|
||||||
|
|||||||
+3
-2
@@ -47,6 +47,7 @@ async function selectService(svcId){
|
|||||||
const d=await r.json();
|
const d=await r.json();
|
||||||
svcInstances=d.instances||[];
|
svcInstances=d.instances||[];
|
||||||
currentSvcName=d.svc||'';
|
currentSvcName=d.svc||'';
|
||||||
|
currentSvcShort=d.svcShort||'';
|
||||||
// Обновить текст кнопки создания
|
// Обновить текст кнопки создания
|
||||||
const btnSpan=document.querySelector('#create-btn-area span');
|
const btnSpan=document.querySelector('#create-btn-area span');
|
||||||
if(btnSpan) btnSpan.textContent='+ Создать новый инстанс '+(currentSvcName||'сервиса');
|
if(btnSpan) btnSpan.textContent='+ Создать новый инстанс '+(currentSvcName||'сервиса');
|
||||||
@@ -110,8 +111,8 @@ async function startCreate(){
|
|||||||
}
|
}
|
||||||
|
|
||||||
function makeCreateDisplayName(){
|
function makeCreateDisplayName(){
|
||||||
const suffix = Date.now().toString(36);
|
const rand = (Math.random() * 46656 | 0).toString(36).padStart(3, '0');
|
||||||
return suffix;
|
return currentSvcShort ? currentSvcShort+'-'+rand : rand;
|
||||||
}
|
}
|
||||||
|
|
||||||
function runOp(opName,opId){
|
function runOp(opName,opId){
|
||||||
|
|||||||
Reference in New Issue
Block a user