v1.0.78: log panel in UI + /api/log endpoint + _log() buffer

This commit is contained in:
2026-07-27 13:50:57 +04:00
parent 6c8d852f6c
commit 07aa2115d4
3 changed files with 46 additions and 10 deletions
+1 -1
View File
@@ -6,7 +6,7 @@ from routes.main import bp as main_bp
from routes.api import bp as api_bp 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
VERSION = "1.0.77" VERSION = "1.0.78"
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-dev.ngcloud.ru/api/v1/svc") app.config["NUBES_API_ENDPOINT"] = os.getenv("NUBES_API_ENDPOINT", "https://lk-api-gateway-dev.ngcloud.ru/api/v1/svc")
+25 -9
View File
@@ -1,5 +1,6 @@
from flask import Blueprint, current_app, jsonify, request from flask import Blueprint, current_app, jsonify, request
import uuid import uuid
import collections
from api.http_client import HttpClient, detect_endpoint from api.http_client import HttpClient, detect_endpoint
from operations.get_services import get_services, get_service_detail from operations.get_services import get_services, get_service_detail
@@ -11,6 +12,15 @@ bp = Blueprint("api_test", __name__)
AUTOTEST_PREFIX = "autotest-" AUTOTEST_PREFIX = "autotest-"
# Кольцевой буфер логов (показываются в UI)
_log_lines = collections.deque(maxlen=200)
def _log(msg):
"""Пишет и в stdout (для gunicorn), и в буфер (для UI)."""
print(msg, flush=True)
_log_lines.append(msg)
def _with_prefix(name): def _with_prefix(name):
name = (name or "").strip() or "instance" name = (name or "").strip() or "instance"
@@ -102,28 +112,28 @@ def api_params(op_id):
try: try:
instance_uid = request.args.get("instanceUid") instance_uid = request.args.get("instanceUid")
op_name = request.args.get("opName") op_name = request.args.get("opName")
print(f"[api_params] op_id={op_id} instanceUid={instance_uid!r} opName={op_name!r}", flush=True) _log(f"[api_params] op_id={op_id} instanceUid={instance_uid!r} opName={op_name!r}")
if instance_uid and op_name: if instance_uid and op_name:
print(f"[api_params] PREVIEW branch: creating operation for instance={instance_uid}", flush=True) _log(f"[api_params] PREVIEW branch: creating operation for instance={instance_uid}")
# Создать pending-операцию чтобы прочитать ТЕКУЩИЕ значения параметров инстанса # Создать pending-операцию чтобы прочитать ТЕКУЩИЕ значения параметров инстанса
op_payload = {"instanceUid": instance_uid, "svcOperationId": op_id, "operation": op_name} op_payload = {"instanceUid": instance_uid, "svcOperationId": op_id, "operation": op_name}
op_resp = _client().post("/instanceOperations", op_payload) op_resp = _client().post("/instanceOperations", op_payload)
preview_op_uid = _find_uid(op_resp) or _uid_from_location(op_resp.get("_location", "")) preview_op_uid = _find_uid(op_resp) or _uid_from_location(op_resp.get("_location", ""))
print(f"[api_params] preview_op_uid={preview_op_uid!r}", flush=True) _log(f"[api_params] preview_op_uid={preview_op_uid!r}")
if not preview_op_uid: if not preview_op_uid:
return jsonify({"error": "Не удалось создать preview-операцию"}), 500 return jsonify({"error": "Не удалось создать preview-операцию"}), 500
op_data = _client().get(f"/instanceOperations/{preview_op_uid}") op_data = _client().get(f"/instanceOperations/{preview_op_uid}")
cfs_params = op_data.get("instanceOperation", {}).get("cfsParams", []) cfs_params = op_data.get("instanceOperation", {}).get("cfsParams", [])
print(f"[api_params] cfsParams count={len(cfs_params)}", flush=True) _log(f"[api_params] cfsParams count={len(cfs_params)}")
result = [] result = []
for p in cfs_params: for p in cfs_params:
dd = p.get("dataDescriptor") dd = p.get("dataDescriptor")
pv = p.get("paramValue") pv = p.get("paramValue")
dv = p.get("defaultValue") dv = p.get("defaultValue")
print(f"[api_params] {p.get('svcOperationCfsParam')}: paramValue={pv!r} defaultValue={dv!r}", flush=True) _log(f"[api_params] {p.get('svcOperationCfsParam')}: paramValue={pv!r} defaultValue={dv!r}")
result.append({ result.append({
"svcOperationCfsParamId": p["svcOperationCfsParamId"], "svcOperationCfsParamId": p["svcOperationCfsParamId"],
"name": p.get("svcOperationCfsParam", ""), "name": p.get("svcOperationCfsParam", ""),
@@ -133,11 +143,11 @@ def api_params(op_id):
"valueList": p.get("valueList"), "valueList": p.get("valueList"),
"dataDescriptor": {k: {"dataType": v.get("dataType",""), "valueList": v.get("valueList",""), "isRequired": v.get("isRequired", False)} for k, v in dd.items()} if isinstance(dd, dict) else None, "dataDescriptor": {k: {"dataType": v.get("dataType",""), "valueList": v.get("valueList",""), "isRequired": v.get("isRequired", False)} for k, v in dd.items()} if isinstance(dd, dict) else None,
}) })
print(f"[api_params] PREVIEW returning {len(result)} params", flush=True) _log(f"[api_params] PREVIEW returning {len(result)} params")
return jsonify({"params": result, "previewOpUid": preview_op_uid}) return jsonify({"params": result, "previewOpUid": preview_op_uid})
# Без instanceUid — шаблонные значения по умолчанию # Без instanceUid — шаблонные значения по умолчанию
print(f"[api_params] DEFAULT branch: template defaults", flush=True) _log(f"[api_params] DEFAULT branch: template defaults")
data = _client().get(f"/instanceOperations/default/{op_id}") data = _client().get(f"/instanceOperations/default/{op_id}")
params = data["svcOperation"]["cfsParams"] params = data["svcOperation"]["cfsParams"]
result = [] result = []
@@ -152,11 +162,11 @@ def api_params(op_id):
"valueList": p.get("valueList"), "valueList": p.get("valueList"),
"dataDescriptor": {k: {"dataType": v.get("dataType",""), "valueList": v.get("valueList",""), "isRequired": v.get("isRequired", False)} for k, v in dd.items()} if isinstance(dd, dict) else None, "dataDescriptor": {k: {"dataType": v.get("dataType",""), "valueList": v.get("valueList",""), "isRequired": v.get("isRequired", False)} for k, v in dd.items()} if isinstance(dd, dict) else None,
}) })
print(f"[api_params] DEFAULT returning {len(result)} params", flush=True) _log(f"[api_params] DEFAULT returning {len(result)} params")
return jsonify({"params": result}) return jsonify({"params": result})
except Exception as e: except Exception as e:
import traceback import traceback
print(f"[api_params] EXCEPTION: {e}", flush=True) _log(f"[api_params] EXCEPTION: {e}")
traceback.print_exc() traceback.print_exc()
return jsonify({"error": str(e)}), 500 return jsonify({"error": str(e)}), 500
@@ -326,6 +336,12 @@ def api_test_status(op_uid):
return jsonify({"error": str(e)}), 500 return jsonify({"error": str(e)}), 500
@bp.route("/api/log")
def api_log():
"""Отдать последние строки лога для UI-панели."""
return jsonify(list(_log_lines))
def _find_uid(resp): def _find_uid(resp):
"""Извлечь instanceUid или instanceOperationUid из ответа API.""" """Извлечь instanceUid или instanceOperationUid из ответа API."""
if isinstance(resp, dict): if isinstance(resp, dict):
+20
View File
@@ -349,6 +349,26 @@ async function refreshInstances(){
</div>`; </div>`;
document.getElementById('inst-list').innerHTML=html; document.getElementById('inst-list').innerHTML=html;
} }
// === Панель логов ===
let logPollTimer=null;
function startLogPoll(){
if(logPollTimer) return;
logPollTimer=setInterval(async()=>{
try{
const r=await fetch('/api/log');
const lines=await r.json();
const el=document.getElementById('log-panel');
if(!el) return;
el.innerHTML=lines.map(l=>`<div>${l}</div>`).join('');
el.scrollTop=el.scrollHeight;
}catch(e){}
},2000);
}
startLogPoll();
</script> </script>
<div id="log-panel" style="position:fixed;bottom:0;left:0;right:0;height:180px;background:#1e1e1e;color:#d4d4d4;font:11px monospace;overflow-y:auto;padding:6px 10px;border-top:2px solid #555;z-index:9999;"></div>
</body> </body>
</html> </html>