diff --git a/site/app.py b/site/app.py index 25ade0f..6faff28 100644 --- a/site/app.py +++ b/site/app.py @@ -18,7 +18,7 @@ from routes.api import bp as api_bp from routes.api_test import bp as api_test_bp # Версия — меняется при КАЖДОМ изменении кода. Показывается в топбаре UI. -VERSION = "1.0.93" +VERSION = "1.0.95" 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") diff --git a/site/db/init_db.py b/site/db/init_db.py index 3be6be8..6d8e32e 100644 --- a/site/db/init_db.py +++ b/site/db/init_db.py @@ -8,10 +8,12 @@ Таблица runs — история запусков операций: - id, created_at - client_id, stand (изоляция по пользователю и стенду) + - user_email (кто запустил) - svc_id, svc_name, op_name, svc_op_id - - instance_uid, display_name + - instance_uid, display_name, op_uid - status (OK/FAIL/TIMEOUT), duration_sec, error_log - params (JSONB), stages (JSONB) + - app_version (версия приложения) """ import os @@ -23,17 +25,20 @@ CREATE TABLE IF NOT EXISTS runs ( created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), client_id VARCHAR(64) NOT NULL, stand VARCHAR(16) NOT NULL, + user_email VARCHAR(128), svc_id INTEGER NOT NULL, svc_name VARCHAR(128), op_name VARCHAR(32) NOT NULL, svc_op_id INTEGER, + op_uid VARCHAR(64), instance_uid VARCHAR(64), display_name VARCHAR(255), status VARCHAR(16) NOT NULL, duration_sec DOUBLE PRECISION, error_log TEXT, params JSONB, - stages JSONB + stages JSONB, + app_version VARCHAR(16) ); CREATE INDEX IF NOT EXISTS idx_runs_client_stand @@ -46,9 +51,16 @@ CREATE INDEX IF NOT EXISTS idx_runs_created ON runs (created_at); """ +# Миграция для существующих таблиц (добавляем колонки если их нет) +MIGRATION_SQL = """ +ALTER TABLE runs ADD COLUMN IF NOT EXISTS op_uid VARCHAR(64); +ALTER TABLE runs ADD COLUMN IF NOT EXISTS user_email VARCHAR(128); +ALTER TABLE runs ADD COLUMN IF NOT EXISTS app_version VARCHAR(16); +""" + def init_db(): - """Применить схему — идемпотентно, безопасно для конкурентного вызова.""" + """Применить схему + миграции — идемпотентно, безопасно для конкурентного вызова.""" if not os.getenv("DB_USER"): print("[DB] DB_USER not set — skipping init", flush=True) return @@ -61,6 +73,7 @@ def init_db(): try: cur = conn.cursor() cur.execute(SCHEMA_SQL) + cur.execute(MIGRATION_SQL) conn.commit() cur.close() print("[DB] Schema initialized", flush=True) diff --git a/site/db/save_run.py b/site/db/save_run.py index a7f2d5d..02c3eb7 100644 --- a/site/db/save_run.py +++ b/site/db/save_run.py @@ -6,9 +6,9 @@ import json from db.pool import get_conn, put_conn -def save_run(client_id, stand, svc_id, svc_name, op_name, svc_op_id, - instance_uid, display_name, status, duration_sec, - error_log, params, stages): +def save_run(client_id, stand, user_email, svc_id, svc_name, op_name, svc_op_id, + op_uid, instance_uid, display_name, status, duration_sec, + error_log, params, stages, app_version): """Сохранить один запуск в таблицу runs.""" conn = get_conn() if not conn: @@ -17,16 +17,17 @@ def save_run(client_id, stand, svc_id, svc_name, op_name, svc_op_id, try: cur = conn.cursor() cur.execute(""" - INSERT INTO runs (client_id, stand, svc_id, svc_name, op_name, svc_op_id, - instance_uid, display_name, status, duration_sec, - error_log, params, stages) - VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) + INSERT INTO runs (client_id, stand, user_email, svc_id, svc_name, + op_name, svc_op_id, op_uid, instance_uid, display_name, + status, duration_sec, error_log, params, stages, app_version) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) """, ( - client_id, stand, svc_id, svc_name, op_name, svc_op_id, - instance_uid, display_name, status, duration_sec, + client_id, stand, user_email, svc_id, svc_name, op_name, svc_op_id, + op_uid, instance_uid, display_name, status, duration_sec, error_log, json.dumps(params) if params else None, json.dumps(stages) if stages else None, + app_version, )) conn.commit() cur.close() diff --git a/site/routes/api_test.py b/site/routes/api_test.py index 138d543..fac1cdd 100644 --- a/site/routes/api_test.py +++ b/site/routes/api_test.py @@ -234,7 +234,7 @@ def api_test(): client.post(f"/instanceOperations/{op_uid}/run") # фоном ждать завершения - threading.Thread(target=_finish_op, args=(client, op_uid, instance_uid, svc_id, display_name, op_name, svc_op_id, True, get_client_id(), get_stand()), daemon=True).start() + threading.Thread(target=_finish_op, args=(client, op_uid, instance_uid, svc_id, display_name, op_name, svc_op_id, True, get_client_id(), get_stand(), params), daemon=True).start() return jsonify({"status": "RUNNING", "opUid": op_uid, "instanceUid": instance_uid, "displayName": display_name}) else: @@ -264,7 +264,7 @@ def api_test(): # Получить реальное имя инстанса из API (не UUID!) if not display_name: display_name = _get_instance_display_name(client, instance_uid) - threading.Thread(target=_finish_op, args=(client, op_uid, instance_uid, svc_id, display_name, op_name, svc_op_id, False, get_client_id(), get_stand(), is_delete), daemon=True).start() + threading.Thread(target=_finish_op, args=(client, op_uid, instance_uid, svc_id, display_name, op_name, svc_op_id, False, get_client_id(), get_stand(), is_delete, params), daemon=True).start() return jsonify({"status": "RUNNING", "opUid": op_uid, "instanceUid": instance_uid, "displayName": display_name}) except Exception as e: @@ -303,7 +303,7 @@ def _get_instance_display_name(client, instance_uid): return instance_uid -def _finish_op(client, op_uid, instance_uid, svc_id, display_name, op_name, svc_op_id, is_create, client_id, stand, is_delete=False): +def _finish_op(client, op_uid, instance_uid, svc_id, display_name, op_name, svc_op_id, is_create, client_id, stand, is_delete=False, params=None): """Фоном ждать dtFinish и сохранить результат.""" import time _cleanup_op_results() # очистить старые записи перед добавлением новой @@ -344,13 +344,18 @@ def _finish_op(client, op_uid, instance_uid, svc_id, display_name, op_name, svc_ # Сохранить в БД историю try: from db.save_run import save_run - save_run(client_id, stand, svc_id, op.get("svc", ""), op_name, svc_op_id, - instance_uid, display_name, + from api.auth import get_token_info + user = get_token_info() + save_run(client_id, stand, + user.get("email", ""), + svc_id, op.get("svc", ""), op_name, svc_op_id, + op_uid, instance_uid, display_name, "OK" if is_ok else "FAIL", round(time.time() - t0, 1), str(err) if err else "", - {}, # params недоступны в _finish_op — будут при следующем улучшении - op.get("stages", [])) + params or {}, + op.get("stages", []), + current_app.config.get("VERSION", "")) except Exception: pass # не ронять фоновый поток из-за БД return @@ -413,8 +418,10 @@ def api_history(): return jsonify([]) cur = conn.cursor() cur.execute(""" - SELECT id, created_at, client_id, stand, svc_id, svc_name, - op_name, instance_uid, display_name, status, duration_sec, error_log + SELECT id, created_at, client_id, stand, user_email, + svc_id, svc_name, op_name, svc_op_id, op_uid, + instance_uid, display_name, + status, duration_sec, error_log, app_version FROM runs WHERE client_id = %s AND stand = %s ORDER BY created_at DESC