diff --git a/site/config/loader.py b/site/config/loader.py index d8a795f..fcfae69 100644 --- a/site/config/loader.py +++ b/site/config/loader.py @@ -71,4 +71,4 @@ DELAY = float(os.getenv("MOCK_OP_DELAY", "0.1")) # Версия полигона — единый источник правды для app.py и routes/root.py. # Меняется при КАЖДОМ изменении кода. -VERSION = "0.3.1" +VERSION = "0.3.2" diff --git a/site/routes/root.py b/site/routes/root.py index 9385875..b495f74 100644 --- a/site/routes/root.py +++ b/site/routes/root.py @@ -5,6 +5,7 @@ Blueprint "root" регистрируется в app.py БЕЗ url_prefix. Отвечает за healthcheck (для Nubes) и HTML-страницу с информацией о сервисе. """ +import os from flask import Blueprint, render_template import mock_state @@ -32,15 +33,34 @@ def index(): """Корневая HTML-страница с информацией о сервисе. Показывает: версию, количество загруженных сервисов, количество инстансов, - текущую задержку операций. Использует Jinja2-шаблон templates/index.html - и внешний CSS из static/style.css. + текущую задержку операций, список сервисов с операциями. + Использует Jinja2-шаблон templates/index.html и внешний CSS из static/style.css. """ svc_count = len(_cfg.SERVICES) inst_count = len(mock_state.state.instances) + + # Список сервисов для таблицы: name, id, кол-во операций + svc_list = [] + for sid, svc in sorted(_cfg.SERVICES.items(), key=lambda x: x[1].get("name", x[0])): + svc_list.append({ + "id": sid, + "name": svc.get("name", sid), + "display_name": svc.get("service_display_name", svc.get("name", "")), + "op_count": len(svc.get("operations", [])), + "has_cfs": bool(svc.get("cfsParams")), + "has_state": bool(svc.get("stateParams")), + "has_out": bool(svc.get("stateOut")), + }) + + # URL для примеров — из переменной окружения или автоопределение + endpoint = os.getenv("POLYGON_ENDPOINT", "https://polygon.pythonk8s.dev.nubes.ru") + return render_template( "index.html", version=_cfg.VERSION, svc_count=svc_count, inst_count=inst_count, delay=_cfg.DELAY, + svc_list=svc_list, + endpoint=endpoint, ) diff --git a/site/static/favicon.svg b/site/static/favicon.svg new file mode 100644 index 0000000..11a9ece --- /dev/null +++ b/site/static/favicon.svg @@ -0,0 +1,55 @@ + + diff --git a/site/static/logo.svg b/site/static/logo.svg new file mode 100755 index 0000000..d76eef6 --- /dev/null +++ b/site/static/logo.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + diff --git a/site/static/style.css b/site/static/style.css index 97e8baf..3f803dc 100644 --- a/site/static/style.css +++ b/site/static/style.css @@ -1,31 +1,310 @@ /* - * polygon — тёмная тема для корневой страницы (/). - * Минимальный CSS, без фреймворков. + * polygon — дизайн-система Nubes. + * Токены из nubes-design-system.md. */ +/* ── Reset & Base ── */ +*, *::before, *::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +:root { + --brand-primary: #2563eb; + --brand-primary-dark: #1d4ed8; + --brand-gray: #d1d5db; + --brand-grey-light: #f3f4f6; + --bg: #ffffff; + --text: #1a1a1a; + --text-muted: #6b7280; + --border: #e5e7eb; + --destructive: #ef4444; + --green: #22c55e; + --radius: 12px; + --radius-sm: 6px; +} + body { - font-family: system-ui, -apple-system, sans-serif; - max-width: 600px; - margin: 40px auto; - padding: 0 20px; - background: #111; - color: #eee; + font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; + font-size: 14px; + line-height: 1.5; + color: var(--text); + background: #f9fafb; + min-height: 100vh; } -h1 { - color: #4af; +/* ── Header ── */ +.header { + background: var(--bg); + border-bottom: 4px solid var(--brand-primary); + padding: 16px 24px 12px; + margin-bottom: 24px; } -.ok { - color: #4a4; +.header-top { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 8px; } -.ver { - color: #888; +.logo { + display: block; + height: 33px; + width: auto; } +.header-status { + display: flex; + align-items: center; + gap: 8px; + color: var(--text-muted); + font-size: 13px; + margin-left: 4px; +} + +.header-status strong { + color: var(--text); +} + +.sep { + color: var(--brand-gray); +} + +/* ── Layout ── */ +main { + max-width: 960px; + margin: 0 auto; + padding: 0 16px 40px; + display: flex; + flex-direction: column; + gap: 20px; +} + +/* ── Badge ── */ +.badge { + display: inline-block; + padding: 2px 10px; + border-radius: 100px; + font-size: 12px; + font-weight: 600; +} + +.badge-blue { + background: var(--brand-primary); + color: #fff; +} + +/* ── Dots ── */ +.dot { + display: inline-block; + width: 8px; + height: 8px; + border-radius: 50%; + flex-shrink: 0; +} + +.dot-green { background: var(--green); } +.dot-gray { background: var(--brand-gray); } + +.status-text { + font-weight: 600; + color: var(--green); +} + +/* ── Cards (ui-kit:bg-card ui-kit:rounded-xl ui-kit:border ui-kit:shadow-sm) ── */ +.card { + background: var(--bg); + border: 1px solid var(--border); + border-radius: var(--radius); + box-shadow: 0 1px 2px rgba(0,0,0,0.04); + overflow: hidden; +} + +.card-header { + background: var(--brand-grey-light); + padding: 12px 16px; + font-weight: 600; + font-size: 16px; +} + +.card-header h2 { + font-size: 16px; + font-weight: 600; +} + +.card-body { + padding: 16px; +} + +.card-body p { + margin-bottom: 12px; +} + +.card-body p:last-child { + margin-bottom: 0; +} + +.card-body ul { + padding-left: 20px; + margin-bottom: 0; +} + +.card-body li { + margin-bottom: 4px; +} + +/* ── 2-column grid ── */ +.grid-2col { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 24px; + margin-top: 8px; +} + +@media (max-width: 640px) { + .grid-2col { + grid-template-columns: 1fr; + } +} + +/* ── Headings ── */ +.h4 { + font-size: 14px; + font-weight: 600; + margin-bottom: 6px; + margin-top: 16px; + color: var(--text); +} + +.h4:first-child { + margin-top: 0; +} + +/* ── Code blocks ── */ +.code-block { + background: #1e1e2e; + color: #cdd6f4; + padding: 12px 16px; + border-radius: var(--radius-sm); + font-family: "SF Mono", "Fira Code", "Cascadia Code", monospace; + font-size: 13px; + line-height: 1.6; + overflow-x: auto; + margin-bottom: 12px; + white-space: pre-wrap; + word-break: break-all; +} + +.code-block code { + background: none; + padding: 0; + font-size: inherit; +} + +/* ── Note / Callout ── */ +.note { + background: #eff6ff; + border: 1px solid #bfdbfe; + border-radius: var(--radius-sm); + padding: 10px 14px; + margin-top: 16px; + font-size: 13px; + color: #1e40af; +} + +.note code { + background: #dbeafe; + padding: 1px 5px; + border-radius: 3px; + font-size: 12px; +} + +/* ── Table ── */ +.table-wrap { + padding: 0; + overflow-x: auto; +} + +.table { + width: 100%; + border-collapse: collapse; + font-size: 13px; +} + +.table th { + background: var(--brand-grey-light); + text-transform: uppercase; + font-size: 11px; + font-weight: 600; + letter-spacing: 0.05em; + color: var(--text-muted); + padding: 8px 12px; + text-align: left; + border-right: 1px solid var(--border); + border-bottom: 1px solid var(--border); + white-space: nowrap; +} + +.table th:last-child { + border-right: none; +} + +.table td { + padding: 7px 12px; + border-right: 1px solid var(--border); + border-bottom: 1px solid var(--border); + vertical-align: middle; +} + +.table td:last-child { + border-right: none; +} + +.table tbody tr:hover { + background: rgba(243, 244, 246, 0.5); +} + +.table tbody tr:last-child td { + border-bottom: none; +} + +/* ── HTTP Method badges ── */ +.method { + display: inline-block; + padding: 1px 7px; + border-radius: 3px; + font-size: 11px; + font-weight: 700; + font-family: monospace; + text-transform: uppercase; +} + +.method-get { background: #dbeafe; color: #1d4ed8; } +.method-post { background: #dcfce7; color: #16a34a; } + +/* ── Muted text ── */ +.muted { + color: var(--text-muted); + font-weight: 400; +} + +/* ── Inline code ── */ code { - background: #333; - padding: 2px 6px; - border-radius: 4px; + background: var(--brand-grey-light); + padding: 1px 5px; + border-radius: 3px; + font-family: "SF Mono", "Fira Code", monospace; + font-size: 12px; +} + +/* ── Footer ── */ +.footer { + text-align: center; + padding: 20px 16px; + color: var(--text-muted); + font-size: 12px; + max-width: 960px; + margin: 0 auto; } diff --git a/site/templates/index.html b/site/templates/index.html index 96367ee..8d5956c 100644 --- a/site/templates/index.html +++ b/site/templates/index.html @@ -2,13 +2,173 @@ - polygon v{{ version }} + + Polygon — Mock Nubes API + -

polygon v{{ version }}

-

● running

-

Сервисов: {{ svc_count }}  |  Инстансов: {{ inst_count }}  |  delay: {{ delay }}s

-

Эндпоинты: /api/v1/svc/*

+ + +
+
+ + polygon v{{ version }} +
+
+ + Работает + · + Сервисов: {{ svc_count }} + · + Инстансов: {{ inst_count }} + · + Задержка: {{ delay }}s +
+
+ +
+ + +
+
+

Что такое Polygon?

+
+
+

+ Polygon — это эмулятор REST API облачной платформы + Nubes для интеграционных тестов. + Он полностью повторяет контракты реального API: сервисы, инстансы, + операции, параметры, валидацию — но работает без реальной + инфраструктуры, в памяти, с мгновенным откликом. +

+
+
+

Зачем нужен

+
    +
  • Тестировать создание/изменение/удаление инстансов без реальных ресурсов
  • +
  • Отлаживать UI автодеплоя (app-autotest) на мок-данных
  • +
  • Писать интеграционные тесты с детерминированным состоянием
  • +
  • Проверять краевые случаи (ошибки валидации, сбои операций)
  • +
+
+
+

Как устроен

+
    +
  • Flask 3.0 + gunicorn, 1 воркер (состояние в памяти)
  • +
  • Конфигурация сервисов — из YAML (генерируется из STANDS)
  • +
  • Все операции — синхронные, с настраиваемой задержкой
  • +
  • Управляемый сброс состояния через /_mock/reset
  • +
+
+
+
+
+ + +
+
+

Как использовать

+
+
+

1. Получить список сервисов

+
curl -s {{ endpoint }}/api/v1/svc/services | python3 -m json.tool
+ +

2. Создать инстанс

+
curl -s -X POST {{ endpoint }}/api/v1/svc/instances \
+  -H "Content-Type: application/json" \
+  -d '{"serviceId": "s3", "displayName": "my-bucket"}' | python3 -m json.tool
+ +

3. Заполнить параметры и запустить операцию

+
# Установить параметры операции
+curl -s -X POST {{ endpoint }}/api/v1/svc/instanceOperationCfsParams/<opUid>
+
+# Запустить операцию (create/modify/delete)
+curl -s -X POST {{ endpoint }}/api/v1/svc/instanceOperations/<opUid>/run
+ +

4. Сбросить состояние

+
curl -s -X POST {{ endpoint }}/api/v1/svc/_mock/reset
+ +
+ Подсказка: используйте переменную окружения + POLYGON_ENDPOINT={{ endpoint }} + в app-autotest — и все операции пойдут через полигон + вместо реального API. +
+
+
+ + +
+
+

API эндпоинты

+
+
+ + + + + + + + + + + + + + + + + + + + + + + +
МетодПутьОписание
GET/healthHealthcheck
GET/api/v1/svc/servicesСписок сервисов
GET/api/v1/svc/services/<id>Детали сервиса
GET/api/v1/svc/instancesСписок инстансов
GET/api/v1/svc/instances/<uid>Детали инстанса
POST/api/v1/svc/instancesСоздать инстанс
GET/api/v1/svc/instanceOperationsСписок операций
POST/api/v1/svc/instanceOperations/<uid>/runЗапустить операцию
GET/api/v1/svc/instanceOperations/<uid>/statusСтатус операции
POST/api/v1/svc/_mock/resetСброс состояния
GET/api/v1/svc/_mock/stateДамп состояния
POST/api/v1/svc/_mock/delay/<s>Задать задержку
POST/api/v1/svc/_mock/fail-nextСледующая операция упадёт
+
+
+ + +
+
+

Загруженные сервисы ({{ svc_count }})

+
+
+ + + + + + + + + + + + + {% for svc in svc_list %} + + + + + + + + + {% endfor %} + +
IDНазваниеОперацийCfsParamsStateParamsStateOut
{{ svc.id }}{{ svc.display_name }}{{ svc.op_count }}{% if svc.has_cfs %}{% else %}{% endif %}{% if svc.has_state %}{% else %}{% endif %}{% if svc.has_out %}{% else %}{% endif %}
+
+
+ +
+ + +