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 — это эмулятор 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.
+
+
+
+
+
+
+
+
+
+
+
+ Метод
+ Путь
+ Описание
+
+
+
+ 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Следующая операция упадёт
+
+
+
+
+
+
+
+
+
+
+
+
+ ID
+ Название
+ Операций
+ CfsParams
+ StateParams
+ StateOut
+
+
+
+ {% for svc in svc_list %}
+
+ {{ 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 %}
+
+ {% endfor %}
+
+
+
+
+
+
+
+
+