feat: селектор режима и стенда (v1.2.30)
Новое: - Селектор режима (🎭 Эмуляция / ☁️ Облако) над версией - Селектор стенда (DEV/TEST/PROD) в режиме эмуляции - В эмуляции — всё в полигон (/stand/api/v1/svc) - В эмуляции — токен дезактивирован - По умолчанию: эмуляция + TEST Изменено: - auth.py: get_mode(), get_polygon_stand(), get_client() с префиксом стенда - main.py: action=set_mode, единый get_client() вместо real_client/inst_client - index.html: селекторы, disable токена - service_list.py: обрезка polygon_ префикса - api_test.py: get_real_client → get_client Задокументировано в HISTORY/2026-08-03-session.md.
This commit is contained in:
+59
-41
@@ -5,11 +5,22 @@
|
|||||||
в main.py и api_test.py с идентичным или почти идентичным кодом.
|
в main.py и api_test.py с идентичным или почти идентичным кодом.
|
||||||
Теперь всё здесь — один источник правды для всех роутов.
|
Теперь всё здесь — один источник правды для всех роутов.
|
||||||
|
|
||||||
|
Режимы работы (2026-08-03):
|
||||||
|
- Эмуляция (mode=polygon): всё в полигон, стенд из селектора (dev/test/prod)
|
||||||
|
- Облако (mode=cloud): реальный API, стенд автоопределяется по токену
|
||||||
|
|
||||||
|
Cookie (устанавливаются main.py:action=set_mode):
|
||||||
|
- mode — "polygon" (по умолчанию) или "cloud"
|
||||||
|
- polygon_stand — "test" (по умолчанию), "dev", "prod"
|
||||||
|
|
||||||
Функции (все без аргументов — берут данные из Flask request/current_app):
|
Функции (все без аргументов — берут данные из Flask request/current_app):
|
||||||
get_token() — токен: cookie → env-переменная
|
get_mode() — "polygon" / "cloud"
|
||||||
get_client() — HttpClient с автоопределением стенда
|
get_polygon_stand() — "dev" / "test" / "prod"
|
||||||
|
get_token() — токен: cookie → env (в эмуляции только env)
|
||||||
|
get_client() — HttpClient с учётом режима и стенда
|
||||||
|
get_real_client() — алиас get_client() (историческая совместимость)
|
||||||
get_client_id() — ClientID из JWT (base64url, без проверки подписи)
|
get_client_id() — ClientID из JWT (base64url, без проверки подписи)
|
||||||
get_stand() — "dev" / "test" по токену
|
get_stand() — "polygon_test" / "dev" / "test"
|
||||||
get_token_info() — {email, company, client_id} из JWT для UI
|
get_token_info() — {email, company, client_id} из JWT для UI
|
||||||
get_token_masked() — маскированный токен (abc...xyz) для placeholder
|
get_token_masked() — маскированный токен (abc...xyz) для placeholder
|
||||||
"""
|
"""
|
||||||
@@ -21,57 +32,63 @@ from flask import request, current_app
|
|||||||
from api.http_client import HttpClient, detect_endpoint, stand_name, STANDS
|
from api.http_client import HttpClient, detect_endpoint, stand_name, STANDS
|
||||||
|
|
||||||
|
|
||||||
|
def get_mode():
|
||||||
|
"""Режим: 'polygon' (эмуляция) или 'cloud' (реальное облако).
|
||||||
|
|
||||||
|
Если POLYGON_ENDPOINT не задан в конфиге — всегда облако (селектор не нужен).
|
||||||
|
Иначе — из cookie, по умолчанию эмуляция."""
|
||||||
|
if not current_app.config.get("POLYGON_ENDPOINT", ""):
|
||||||
|
return "cloud"
|
||||||
|
return request.cookies.get("mode", "polygon")
|
||||||
|
|
||||||
|
|
||||||
|
def get_polygon_stand():
|
||||||
|
"""Стенд полигона: 'test' (по умолчанию), 'dev', 'prod'.
|
||||||
|
|
||||||
|
Используется ТОЛЬКО в режиме эмуляции — для построения URL и load_service_ids."""
|
||||||
|
return request.cookies.get("polygon_stand", "test")
|
||||||
|
|
||||||
|
|
||||||
def get_token():
|
def get_token():
|
||||||
"""Получить активный токен: сначала из cookie пользователя, потом из env.
|
"""Получить активный токен.
|
||||||
|
|
||||||
Приоритет:
|
В эмуляции — ТОЛЬКО env-токен (пользовательский ввод дезактивирован).
|
||||||
1. cookie "token" — пользователь ввёл свой токен в форме
|
В облаке — cookie → env (как раньше)."""
|
||||||
2. NUBES_API_TOKEN из env — сервисный токен (для автоматических тестов)
|
if get_mode() == "polygon":
|
||||||
|
return current_app.config["NUBES_API_TOKEN"]
|
||||||
Пользовательский токен приоритетнее — он переопределяет сервисный."""
|
|
||||||
return request.cookies.get("token") or current_app.config["NUBES_API_TOKEN"]
|
return request.cookies.get("token") or current_app.config["NUBES_API_TOKEN"]
|
||||||
|
|
||||||
|
|
||||||
def _make_client(*, use_polygon):
|
def get_client():
|
||||||
"""Фабрика HttpClient — ЕДИНСТВЕННОЕ место где выбирается endpoint.
|
"""HttpClient с учётом режима и стенда.
|
||||||
|
|
||||||
Args:
|
Эмуляция: polygon_url + "/" + stand → https://polygon.../{stand}/api/v1/svc
|
||||||
use_polygon: если True и POLYGON_ENDPOINT задан → polygon.
|
Облако: реальный API с автоопределением стенда (dev/test по токену)."""
|
||||||
Если False → всегда реальный API (для чтения сервисов/инстансов).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
HttpClient с правильным endpoint и токеном.
|
|
||||||
"""
|
|
||||||
token = get_token()
|
token = get_token()
|
||||||
|
mode = get_mode()
|
||||||
|
|
||||||
# Режим полигона — только если разрешено и переменная задана
|
if mode == "polygon":
|
||||||
if use_polygon:
|
|
||||||
polygon = current_app.config.get("POLYGON_ENDPOINT", "")
|
polygon = current_app.config.get("POLYGON_ENDPOINT", "")
|
||||||
if polygon:
|
stand = get_polygon_stand()
|
||||||
return HttpClient(polygon, token)
|
# POLYGON_ENDPOINT = "https://polygon.../api/v1/svc"
|
||||||
|
# → "https://polygon.../{stand}/api/v1/svc"
|
||||||
|
# rstrip на случай trailing slash в конфиге
|
||||||
|
return HttpClient(f"{polygon.rstrip('/')}/{stand}", token)
|
||||||
|
|
||||||
# Реальный API: автоопределение или NUBES_API_ENDPOINT
|
# Облако: NUBES_API_ENDPOINT с автоопределением
|
||||||
endpoint = current_app.config["NUBES_API_ENDPOINT"]
|
endpoint = current_app.config["NUBES_API_ENDPOINT"]
|
||||||
if endpoint in STANDS:
|
if endpoint in STANDS:
|
||||||
endpoint = detect_endpoint(token) or endpoint
|
endpoint = detect_endpoint(token) or endpoint
|
||||||
return HttpClient(endpoint, token)
|
return HttpClient(endpoint, token)
|
||||||
|
|
||||||
|
|
||||||
def get_client():
|
|
||||||
"""HttpClient для ОПЕРАЦИЙ (create/modify/delete/run/poll).
|
|
||||||
|
|
||||||
Если POLYGON_ENDPOINT задан → polygon.
|
|
||||||
Иначе → автоопределение реального стенда.
|
|
||||||
"""
|
|
||||||
return _make_client(use_polygon=True)
|
|
||||||
|
|
||||||
|
|
||||||
def get_real_client():
|
def get_real_client():
|
||||||
"""HttpClient для ЧТЕНИЯ (сервисы, список инстансов).
|
"""Алиас get_client() — историческая совместимость.
|
||||||
|
|
||||||
ВСЕГДА реальный API, даже при POLYGON_ENDPOINT.
|
Раньше get_real_client() всегда ходил в реальный API (для сервисов).
|
||||||
"""
|
Теперь в эмуляции сервисы тоже из полигона → обе функции идентичны.
|
||||||
return _make_client(use_polygon=False)
|
Оставлено чтобы не ломать все места вызова."""
|
||||||
|
return get_client()
|
||||||
|
|
||||||
|
|
||||||
def get_client_id():
|
def get_client_id():
|
||||||
@@ -100,12 +117,13 @@ def get_client_id():
|
|||||||
|
|
||||||
|
|
||||||
def get_stand():
|
def get_stand():
|
||||||
"""Определить стенд (dev/test/mock) по активному токену.
|
"""Определить стенд.
|
||||||
|
|
||||||
Если POLYGON_ENDPOINT задан → "polygon".
|
Эмуляция: "polygon_{dev|test|prod}" — для load_service_ids и трекера.
|
||||||
Иначе — автоопределение или stand_name."""
|
Облако: автоопределение или stand_name (dev/test/mock)."""
|
||||||
if current_app.config.get("POLYGON_ENDPOINT", ""):
|
mode = get_mode()
|
||||||
return "polygon"
|
if mode == "polygon":
|
||||||
|
return f"polygon_{get_polygon_stand()}"
|
||||||
|
|
||||||
token = get_token()
|
token = get_token()
|
||||||
endpoint = current_app.config["NUBES_API_ENDPOINT"]
|
endpoint = current_app.config["NUBES_API_ENDPOINT"]
|
||||||
|
|||||||
+1
-1
@@ -32,7 +32,7 @@ from routes.api_scenario_defs import bp_defs as api_scenario_defs_bp
|
|||||||
|
|
||||||
# Версия — показывается в топбаре UI. Меняется при КАЖДОМ изменении кода.
|
# Версия — показывается в топбаре UI. Меняется при КАЖДОМ изменении кода.
|
||||||
# Нужна для фильтрации истории (пользователь видит только записи своей версии).
|
# Нужна для фильтрации истории (пользователь видит только записи своей версии).
|
||||||
VERSION = "1.2.29"
|
VERSION = "1.2.30"
|
||||||
|
|
||||||
# Flask-приложение с Jinja2-шаблонами из папки templates/
|
# Flask-приложение с Jinja2-шаблонами из папки templates/
|
||||||
app = Flask(__name__, template_folder="templates", static_folder="static")
|
app = Flask(__name__, template_folder="templates", static_folder="static")
|
||||||
|
|||||||
@@ -33,10 +33,13 @@ def load_service_ids(stand="test"):
|
|||||||
"""Вернуть set разрешённых service_id для указанного стенда.
|
"""Вернуть set разрешённых service_id для указанного стенда.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
stand: str — "dev" или "test".
|
stand: str — "dev", "test", или "polygon_test" (с префиксом эмуляции).
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
set[int] — множество service_id. Пустой set если файла нет → показываем все."""
|
set[int] — множество service_id. Пустой set если файла нет → показываем все."""
|
||||||
|
# Обрезать префикс "polygon_" если есть → "polygon_test" → "test"
|
||||||
|
if stand.startswith("polygon_"):
|
||||||
|
stand = stand[len("polygon_"):]
|
||||||
path = os.path.join(_CONFIG_DIR, f"services_{stand}.txt")
|
path = os.path.join(_CONFIG_DIR, f"services_{stand}.txt")
|
||||||
ids = set()
|
ids = set()
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ def _unique_display_name(client, requested_name):
|
|||||||
@bp.route("/api/services")
|
@bp.route("/api/services")
|
||||||
def api_services():
|
def api_services():
|
||||||
try:
|
try:
|
||||||
raw = get_services(get_real_client())
|
raw = get_services(get_client())
|
||||||
svc_list = [{"svcId": s["svcId"], "svc": s["svc"], "svcExtendedName": s.get("svcExtendedName", "")} for s in raw]
|
svc_list = [{"svcId": s["svcId"], "svc": s["svc"], "svcExtendedName": s.get("svcExtendedName", "")} for s in raw]
|
||||||
svc_list.sort(key=lambda s: s["svcId"])
|
svc_list.sort(key=lambda s: s["svcId"])
|
||||||
return jsonify(svc_list)
|
return jsonify(svc_list)
|
||||||
|
|||||||
+41
-30
@@ -11,7 +11,7 @@ GET /api/operations/<svc_id> — операции и autotest-инста
|
|||||||
from flask import Blueprint, current_app, render_template, request, make_response, jsonify, redirect
|
from flask import Blueprint, current_app, render_template, request, make_response, jsonify, redirect
|
||||||
|
|
||||||
from api.http_client import HttpClient, detect_endpoint, create_client, stand_name
|
from api.http_client import HttpClient, detect_endpoint, create_client, stand_name
|
||||||
from api.auth import get_token, get_client_id, get_token_info, get_token_masked, get_client, get_real_client, get_stand
|
from api.auth import get_token, get_client_id, get_token_info, get_token_masked, get_client, get_real_client, get_stand, get_mode, get_polygon_stand
|
||||||
from operations.get_instances import get_organization, get_instances
|
from operations.get_instances import get_organization, get_instances
|
||||||
from operations.get_services import get_services, get_service_detail
|
from operations.get_services import get_services, get_service_detail
|
||||||
from operations.service_list import load_service_ids
|
from operations.service_list import load_service_ids
|
||||||
@@ -44,12 +44,17 @@ def index():
|
|||||||
"""Главная страница: организация, инфраструктура, сервисы, форма токена.
|
"""Главная страница: организация, инфраструктура, сервисы, форма токена.
|
||||||
|
|
||||||
GET — рендерит страницу с данными из API.
|
GET — рендерит страницу с данными из API.
|
||||||
POST — обрабатывает форму токена (action=save/clear)."""
|
POST — обрабатывает форму токена (action=save/clear) или смену режима (action=set_mode)."""
|
||||||
|
|
||||||
|
# Режим и стенд — из cookie (см. auth.py)
|
||||||
|
mode = get_mode()
|
||||||
|
polygon_stand = get_polygon_stand()
|
||||||
|
polygon_enabled = bool(current_app.config.get("POLYGON_ENDPOINT", ""))
|
||||||
|
|
||||||
# Токены: env — из переменной окружения, user — из cookie или формы
|
# Токены: env — из переменной окружения, user — из cookie или формы
|
||||||
env_token = current_app.config["NUBES_API_TOKEN"]
|
env_token = current_app.config["NUBES_API_TOKEN"]
|
||||||
user_token = request.cookies.get("token") or request.form.get("token") or ""
|
user_token = request.cookies.get("token") or request.form.get("token") or ""
|
||||||
active_token = user_token or env_token # пользовательский приоритетнее
|
active_token = user_token or env_token
|
||||||
|
|
||||||
org = None
|
org = None
|
||||||
error = None
|
error = None
|
||||||
@@ -76,6 +81,9 @@ def index():
|
|||||||
"services": services,
|
"services": services,
|
||||||
"instances": instances,
|
"instances": instances,
|
||||||
"instance_groups": instance_groups,
|
"instance_groups": instance_groups,
|
||||||
|
"mode": mode,
|
||||||
|
"polygon_stand": polygon_stand,
|
||||||
|
"polygon_enabled": polygon_enabled,
|
||||||
}
|
}
|
||||||
ctx.update(overrides)
|
ctx.update(overrides)
|
||||||
return render_template("index.html", **ctx)
|
return render_template("index.html", **ctx)
|
||||||
@@ -91,36 +99,43 @@ def index():
|
|||||||
user_token = request.form.get("token", "")
|
user_token = request.form.get("token", "")
|
||||||
active_token = user_token or env_token
|
active_token = user_token or env_token
|
||||||
resp = make_response()
|
resp = make_response()
|
||||||
resp.set_cookie("token", user_token, max_age=60*60*24*365, httponly=True, samesite="Strict", secure=True) # 1 год
|
resp.set_cookie("token", user_token, max_age=60*60*24*365, httponly=True, samesite="Strict", secure=True)
|
||||||
resp.headers["Location"] = "/" # редирект на GET (убирает POST из истории)
|
resp.headers["Location"] = "/"
|
||||||
resp.status_code = 302
|
resp.status_code = 302
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
# Загрузка данных из API (только если есть токен)
|
# Обработка action=set_mode: сохранить режим и стенд в cookie
|
||||||
|
if action == "set_mode":
|
||||||
|
new_mode = request.form.get("mode", "polygon")
|
||||||
|
new_stand = request.form.get("polygon_stand", "test")
|
||||||
|
resp = make_response(redirect("/"))
|
||||||
|
resp.set_cookie("mode", new_mode, max_age=60*60*24*365, httponly=True, samesite="Strict", secure=True)
|
||||||
|
resp.set_cookie("polygon_stand", new_stand, max_age=60*60*24*365, httponly=True, samesite="Strict", secure=True)
|
||||||
|
return resp
|
||||||
|
|
||||||
|
# Загрузка данных из API
|
||||||
|
# В эмуляции — без проверки токена (полигон не требует авторизации)
|
||||||
|
# В облаке — только если есть токен
|
||||||
services = []
|
services = []
|
||||||
instances = []
|
instances = []
|
||||||
instance_groups = {}
|
instance_groups = {}
|
||||||
config = {}
|
config = {}
|
||||||
config["VERSION"] = current_app.config["VERSION"] # всегда, даже без токена
|
config["VERSION"] = current_app.config["VERSION"]
|
||||||
config["service_ids"] = []
|
config["service_ids"] = []
|
||||||
stand = "?"
|
stand = "?"
|
||||||
if active_token:
|
if mode == "polygon" or active_token:
|
||||||
# Сервисы — ВСЕГДА из реального API (метаданные)
|
client = get_client()
|
||||||
real_client = get_real_client()
|
inst_client = client # единый клиент — и сервисы, и инстансы
|
||||||
# Инстансы — из polygon если POLYGON_ENDPOINT задан
|
|
||||||
inst_client = get_client()
|
|
||||||
endpoint = current_app.config["NUBES_API_ENDPOINT"]
|
|
||||||
stand = get_stand()
|
stand = get_stand()
|
||||||
if real_client:
|
|
||||||
try:
|
try:
|
||||||
# Организация — инстанс с serviceId=19 (из реального API)
|
# Организация — инстанс с serviceId=19
|
||||||
org = get_organization(real_client)
|
org = get_organization(client)
|
||||||
|
|
||||||
# Все сервисы — из реального API
|
# Все сервисы — из полигона (эмуляция) или реального API (облако)
|
||||||
raw_svc = get_services(real_client)
|
raw_svc = get_services(client)
|
||||||
services = sorted(raw_svc, key=lambda s: (s.get("svcId", 0), s.get("svc", "")))
|
services = sorted(raw_svc, key=lambda s: (s.get("svcId", 0), s.get("svc", "")))
|
||||||
|
|
||||||
# Инфраструктурные инстансы — из polygon или реального API
|
# Инфраструктурные инстансы
|
||||||
# 19=Org, 21=vDC, 22=NSX-T, 25=External IP, 26=vApp, 29=vDC Group
|
# 19=Org, 21=vDC, 22=NSX-T, 25=External IP, 26=vApp, 29=vDC Group
|
||||||
# 2=Template, 12=S3, 110=?, 150=K8s
|
# 2=Template, 12=S3, 110=?, 150=K8s
|
||||||
infra_ids = {2, 12, 21, 22, 25, 26, 29, 110, 150}
|
infra_ids = {2, 12, 21, 22, 25, 26, 29, 110, 150}
|
||||||
@@ -128,7 +143,6 @@ def index():
|
|||||||
instances = [i for i in raw_inst
|
instances = [i for i in raw_inst
|
||||||
if i.get("explainedStatus") not in ("deleted", "not created")
|
if i.get("explainedStatus") not in ("deleted", "not created")
|
||||||
and i.get("serviceId") in infra_ids]
|
and i.get("serviceId") in infra_ids]
|
||||||
# Сортировка: организация (svcId=19) первая, остальные по имени
|
|
||||||
instances.sort(key=lambda i: (0 if i.get("serviceId") == 19 else 1, i.get("displayName", "")))
|
instances.sort(key=lambda i: (0 if i.get("serviceId") == 19 else 1, i.get("displayName", "")))
|
||||||
|
|
||||||
# Группировка по типу сервиса для левой колонки UI
|
# Группировка по типу сервиса для левой колонки UI
|
||||||
@@ -147,7 +161,7 @@ def index():
|
|||||||
else:
|
else:
|
||||||
error = "Токен невалиден или просрочен"
|
error = "Токен невалиден или просрочен"
|
||||||
|
|
||||||
# После POST (save/clear) — редирект на GET, чтобы F5 не переотправлял форму
|
# После POST (save/clear/set_mode) — редирект на GET
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
return redirect("/")
|
return redirect("/")
|
||||||
return _tmpl()
|
return _tmpl()
|
||||||
@@ -170,22 +184,19 @@ def api_operations(svc_id):
|
|||||||
active_token = user_token or env_token
|
active_token = user_token or env_token
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Сервисы — ВСЕГДА из реального API (метаданные)
|
# Единый клиент — и сервисы, и инстансы (полигон или реальный API)
|
||||||
real_client = get_real_client()
|
client = get_client()
|
||||||
# Инстансы — из polygon если POLYGON_ENDPOINT задан
|
|
||||||
inst_client = get_client()
|
|
||||||
endpoint = current_app.config["NUBES_API_ENDPOINT"]
|
|
||||||
|
|
||||||
# Детали сервиса: список операций (modify, delete, suspend, ...)
|
# Детали сервиса: список операций (modify, delete, suspend, ...)
|
||||||
detail = get_service_detail(real_client, svc_id)
|
detail = get_service_detail(client, svc_id)
|
||||||
ops = detail.get("operations", [])
|
ops = detail.get("operations", [])
|
||||||
|
|
||||||
# Трекер: наши autotest-инстансы (изолирован по пользователю и стенду)
|
# Трекер: наши autotest-инстансы (изолирован по пользователю и стенду)
|
||||||
tracked = tracker_list(get_client_id(), stand_name(endpoint))
|
tracked = tracker_list(get_client_id(), get_stand())
|
||||||
tracked_by_uid = {t["instanceUid"]: t for t in tracked if t["svcId"] == svc_id}
|
tracked_by_uid = {t["instanceUid"]: t for t in tracked if t["svcId"] == svc_id}
|
||||||
|
|
||||||
# Все инстансы — из polygon или реального API
|
# Все инстансы — из полигона или реального API
|
||||||
instances = get_instances(inst_client)
|
instances = get_instances(client)
|
||||||
nubes_uids = {i["instanceUid"] for i in instances}
|
nubes_uids = {i["instanceUid"] for i in instances}
|
||||||
|
|
||||||
svc_instances = []
|
svc_instances = []
|
||||||
|
|||||||
@@ -59,11 +59,27 @@
|
|||||||
<button class="sidebar-item" data-view="logs-view" onclick="switchView('logs-view')">📜 Логи</button>
|
<button class="sidebar-item" data-view="logs-view" onclick="switchView('logs-view')">📜 Логи</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="sidebar-bottom">
|
<div class="sidebar-bottom">
|
||||||
<div style="padding:4px 12px;font-size:10px;color:var(--muted);">v{{ config.VERSION }} | {{ stand.upper() }}</div>
|
{% if polygon_enabled %}
|
||||||
|
<form method="post" style="padding:4px 12px 2px;">
|
||||||
|
<input type="hidden" name="action" value="set_mode" />
|
||||||
|
<select name="mode" onchange="this.form.submit()" style="width:100%;height:26px;font-size:11px;padding:0 4px;border:1px solid var(--brand-gray);border-radius:3px;background:#fff;margin-bottom:3px;">
|
||||||
|
<option value="polygon" {% if mode == 'polygon' %}selected{% endif %}>🎭 Эмуляция</option>
|
||||||
|
<option value="cloud" {% if mode == 'cloud' %}selected{% endif %}>☁️ Облако</option>
|
||||||
|
</select>
|
||||||
|
{% if mode == 'polygon' %}
|
||||||
|
<select name="polygon_stand" onchange="this.form.submit()" style="width:100%;height:26px;font-size:11px;padding:0 4px;border:1px solid var(--brand-gray);border-radius:3px;background:#fff;">
|
||||||
|
<option value="dev" {% if polygon_stand == 'dev' %}selected{% endif %}>DEV</option>
|
||||||
|
<option value="test" {% if polygon_stand == 'test' %}selected{% endif %}>TEST</option>
|
||||||
|
<option value="prod" {% if polygon_stand == 'prod' %}selected{% endif %}>PROD</option>
|
||||||
|
</select>
|
||||||
|
{% endif %}
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
<div style="padding:4px 12px;font-size:10px;color:var(--muted);">v{{ config.VERSION }} | {{ stand.upper().replace('_', ' ') }}</div>
|
||||||
{% if token_info.email %}<div style="padding:0 12px 4px;font-size:10px;color:var(--muted);">{{ token_info.email }}</div>{% endif %}
|
{% if token_info.email %}<div style="padding:0 12px 4px;font-size:10px;color:var(--muted);">{{ token_info.email }}</div>{% endif %}
|
||||||
<form method="post" class="token-row">
|
<form method="post" class="token-row">
|
||||||
<input type="password" name="token" placeholder="env: {{ env_token_masked }}" value="{{ '' if not has_user_token else '••••••••' }}" />
|
<input type="password" name="token" placeholder="env: {{ env_token_masked }}" value="{{ '' if not has_user_token else '••••••••' }}" {% if mode == 'polygon' %}disabled style="opacity:0.4;"{% endif %} />
|
||||||
<button type="submit" name="action" value="save" class="btn btn-sm">OK</button>
|
<button type="submit" name="action" value="save" class="btn btn-sm" {% if mode == 'polygon' %}disabled{% endif %}>OK</button>
|
||||||
{% if has_user_token %}<button type="submit" name="action" value="clear" class="btn btn-sm" style="color:var(--destructive);">✕</button>{% endif %}
|
{% if has_user_token %}<button type="submit" name="action" value="clear" class="btn btn-sm" style="color:var(--destructive);">✕</button>{% endif %}
|
||||||
</form>
|
</form>
|
||||||
{% if error %}<div style="padding:4px 12px;font-size:10px;color:var(--destructive);">{{ error }}</div>{% endif %}
|
{% if error %}<div style="padding:4px 12px;font-size:10px;color:var(--destructive);">{{ error }}</div>{% endif %}
|
||||||
|
|||||||
Reference in New Issue
Block a user