14 Commits
Author SHA1 Message Date
naeel a1f7617d7f doc: LEGACY.* -> LEGACY/ подпапка 2026-08-04 08:29:13 +04:00
naeel 4e899b8d36 doc: 54 устаревших файла -> LEGACY.*, README обновлён
Актуальные (4): ARCHITECTURE.md, howto-flask-nubes.md, polygon-plan.md, sonnet-app-autotest-review-v1.2.39.md
Всё остальное — исторические артефакты v1.0.x/v1.1.x, старые ревью, промпты.
2026-08-04 08:28:06 +04:00
naeel 8d111a05e1 doc: ARCHITECTURE.md — полное переописание (v1.2.43, 13 разделов) 2026-08-04 08:23:35 +04:00
naeel 547b0f9e21 doc: ARCHITECTURE — разделы БД, режимы, стенды (v1.2.43) 2026-08-04 08:18:29 +04:00
naeel b78bae571e doc: HISTORY — код-ревью Соннета 4.6 (10 находок) 2026-08-03 10:49:03 +04:00
naeel 366a6754fe fix: перенос ревью app-autotest из polygon-docs в DOCS/ 2026-08-03 10:45:01 +04:00
naeel 9ca9f8ba54 doc: code review Соннета — app-autotest v1.2.39 (10 находок) 2026-08-03 10:44:37 +04:00
naeel f67facae20 doc: типовой сценарий — 8 шагов с jq, поиск сервиса/операции/параметров 2026-08-03 09:35:44 +04:00
naeel cd06040d62 doc: HISTORY — фаза 2: селектор режима и стенда 2026-08-03 08:50:21 +04:00
naeel af2cede717 doc: POLYGON-FULL.md — полное описание полигона для AI-агентов 2026-08-03 07:59:28 +04:00
naeel c64812b039 doc: промпт Соннету — полный аудит Polygon + Swagger + тесты 2026-08-02 09:47:46 +04:00
naeel 18d08ccccd doc: ответы Соннету раунд 3 — defaultValue/valueList сравнивать, cfsParamsByOp не надо, stdout достаточно 2026-08-02 07:38:24 +04:00
naeel cfc341db44 doc: ответы Соннету раунд 2 — три категории, HTML нормализация, lifecycle нет 2026-08-02 07:36:45 +04:00
naeel 333a3d5e63 doc: ответы Соннету — dataType:None не баг, YAML вручную, лаг непредсказуем 2026-08-02 07:34:47 +04:00
60 changed files with 1235 additions and 255 deletions
+320 -160
View File
@@ -1,27 +1,36 @@
# Архитектура app-autotest — текущее состояние # Архитектура app-autotest — полное описание
v1.0.93, 28.07.2026 v1.2.43, 04.08.2026
--- ---
## 1. Обзор ## 1. Обзор
Flask-приложение (один HTML-файл, без SPA-фреймворка) для ручного тестирования операций Flask 3.1 + vanilla JS + PostgreSQL (psycopg2). Веб-приложение для автоматического
сервисов облачной платформы Nubes. Работает через REST API Nubes, автоопределяет стенд тестирования сервисов облачной платформы Nubes через REST API.
(dev/test) по JWT-токену.
**Деплой**: Nubes pythonk8s (gunicorn, несколько воркеров). **Два режима работы:**
**Кластер**: `iot-naeel` (K8s resourceRealm, DEV-стенд) - Эмуляция — всё в Polygon (мок-сервер, без реальной инфраструктуры)
**URL**: `https://atest.pythonk8s.dev.nubes.ru` - Облако — реальный Nubes API (dev/test, автоопределение по JWT)
**Репозиторий**: `https://gitea.services.ngcloud.ru/forcloud/app-autotest.git`
**Ручной режим:** сервис -> операция (create/modify/delete/suspend/resume/redeploy) ->
параметры -> запуск -> поллинг.
**Сценарный режим:** последовательность шагов с передачей контекста, редактор через
модальный UI, хранение в БД.
**Деплой:** Nubes pythonk8s managed service, gunicorn --workers 2.
**URL:** https://atest.pythonk8s.dev.nubes.ru
**Полигон:** https://polygon.pythonk8s.dev.nubes.ru
**Репозиторий:** https://gitea.services.ngcloud.ru/forcloud/app-autotest.git (master)
### Ключевые принципы ### Ключевые принципы
1. **Источник правды — Nubes API**. Все данные (инстансы, параметры, статусы) получаются 1. Единый HttpClient — auth.py:get_client(), одна точка для обоих режимов.
только из API. Никаких локальных копий или кеша (кроме краткосрочного трекера). 2. Изоляция по (client_id, stand) — все таблицы БД, трекер, сценарии.
2. **Разделение CREATE и non-CREATE**. Это два принципиально разных потока. 3. Cloud-first + tracker-fallback — инстансы сначала из API, трекер для только что созданных.
3. **Multi-worker safety**. Все общие ресурсы (/tmp файлы) защищены `fcntl.flock`. 4. Multi-worker safety/tmp файлы защищены fcntl.flock, _op_results под threading.Lock.
4. **Мелкие функции**. Каждое действие — отдельная функция. 5. Защита от XSS — _esc() на всех данных из API перед innerHTML.
--- ---
@@ -29,27 +38,59 @@ Flask-приложение (один HTML-файл, без SPA-фреймвор
``` ```
app-autotest/ app-autotest/
├── site/ ← НЕ пакет (без __init__.py) ├── requirements.txt
│ ├── app.py ← Flask(__name__), VERSION, blueprints, /health ├── site/
│ ├── runner.py ← [LEGACY] Раннер тестов │ ├── app.py # Точка входа: VERSION, blueprints, /health
│ ├── config.yaml ← [LEGACY] Конфиг раннера
│ ├── api/ │ ├── api/
│ │ ── http_client.py HttpClient, автостенд │ │ ── http_client.py # HttpClient, detect_endpoint, stand_name
│ │ ├── auth.py # get_client(), get_mode(), get_stand(), токены, cookie
│ │ └── utils.py # find_uid(), uid_from_location()
│ ├── operations/ │ ├── operations/
│ │ ├── get_services.py ← get_services(), get_service_detail() │ │ ├── executor.py # ЕДИНЫЙ запуск: create->params->run
│ │ ├── get_instances.py ← get_instances() (пагинация), get_organization() │ │ ├── poll.py # poll_until_done()
│ │ ├── get_params.py ← get_params_with_current_values() — слияние state.params + шаблон │ │ ├── scenario.py # run_scenario() — шаги с резолвингом
│ │ ── tracker.py ← /tmp/instances-{clientId}-{stand}.json + flock │ │ ── terraform.py # send_params_terraform() — нормализация
│ │ ├── tracker.py # JSON-файловый кеш /tmp/instances-*.json (flock)
│ │ ├── get_instances.py # GET /instances с пагинацией
│ │ ├── get_params.py # Параметры с ТЕКУЩИМИ значениями из state.params
│ │ ├── get_services.py # GET /services
│ │ └── service_list.py # services_{stand}.txt фильтр
│ ├── db/
│ │ ├── pool.py # ThreadedConnectionPool(1,5)
│ │ ├── init_db.py # CREATE TABLE + миграции + idx_one_running
│ │ ├── save_run.py # Ручной UPSERT (UPDATE -> INSERT)
│ │ └── scenario_defs.py # CRUD + lock_check
│ ├── routes/ │ ├── routes/
│ │ ├── main.py GET/POST /, /api/operations/<svc_id> │ │ ├── main.py # GET/POST /, set_mode, api_operations
│ │ ├── api.py ← [LEGACY] /api/run, /api/status, /api/config │ │ ├── api_test.py # /api/test, /api/params, /api/log, _finish_op
│ │ ── api_test.py ← /api/test, /api/params, /api/log │ │ ── api_scenario_run.py # /api/scenario/run, /api/scenario/status
│ ├── templates/ │ ├── api_scenario_defs.py# CRUD /api/scenario/definitions
│ │ ── index.html ← Весь UI (Jinja2 + ванильный JS) │ │ ── api_scenario.py # [ДУБЛИКАТ] Не зарегистрирован
│ └── static/ │ └── api.py # [LEGACY] /api/run
├── style.css │ ├── static/
├── logo.svg ├── app.js # Инициализация: startLogPoll(), selectService()
── favicon.svg ── style.css # Дизайн-система Nubes
│ │ └── js/
│ │ ├── utils.js # _esc(), validateJson(), logPoll, relativeTime()
│ │ ├── views.js # switchView()
│ │ ├── instances.js # selectService(), toggleInstance()
│ │ ├── operations.js # startCreate(), runOp(), showParams()
│ │ ├── params-render.js# renderParamRow(), collectParams()
│ │ ├── history.js # toggleHistory(), loadHistory()
│ │ ├── scenario-list.js# toggleScenario(), runScenario(), поллинг
│ │ ├── scenario-form.js# showScenarioEditor()
│ │ ├── scenario-create.js, scenario-edit.js, scenario-delete.js
│ │ ├── icons.js # SVG-иконки
│ │ └── snackbar.js # showSnackbar()
│ └── templates/
│ └── index.html # Весь UI: сайдбар, виды, лог-панель
├── tests/
│ ├── conftest.py # app_client, polygon_server фикстуры
│ ├── test_api_scenario_run.py# 3 теста: 503, 409, unique violation
│ ├── test_db_scenario_defs.py# 2 теста: lock_check None
│ ├── test_polygon_integration.py# 15 интеграционных тестов
│ ├── test_static_regressions.py# 2 статических теста
│ └── README.md
└── secrets/ └── secrets/
├── dev.token ├── dev.token
└── test.token └── test.token
@@ -57,174 +98,293 @@ app-autotest/
--- ---
## 3. Бэкенд: HTTP-клиент и автостенд ## 3. Система режимов — auth.py
### `http_client.py` Центральный модуль. Все роуты получают HttpClient ТОЛЬКО через функции этого модуля.
- `HttpClient(endpoint, token)` — обёртка над `requests.Session`: ### Переменные окружения
- Заголовки: `Authorization: Bearer`, `User-Agent: Mozilla/5.0` (DDoS-Guard)
- `get(path)``raise_for_status()``.json()`
- `post(path, data)` → проверка `r.ok`, парсинг JSON, извлечение `Location`
- `detect_endpoint(token)` — пробует dev→test стенды, возвращает рабочий URL
- `stand_name(endpoint)` — "dev"/"test" по URL
- `create_client(token, fallback)` — HttpClient + endpoint
### `get_instances.py` | Переменная | По умолчанию | Назначение |
|-----------|-------------|-----------|
| NUBES_API_ENDPOINT | lk-api-gateway-test.../api/v1/svc | Реальный API |
| NUBES_API_TOKEN | (пусто) | Сервисный JWT-токен |
| POLYGON_ENDPOINT | (пусто) | URL полигона. Если задан -> селектор режима |
- `get_instances(client)``GET /instances` с пагинацией (pageSize=200). Остановка по `len(batch) < pageSize`. ### Cookie
- `get_organization(client)` — первый инстанс с `serviceId == 19`
### `get_services.py` | Cookie | По умолчанию | Допустимые значения |
|--------|-------------|-------------------|
| mode | "polygon" | "polygon", "cloud" |
| polygon_stand | "test" | "dev", "test", "prod" |
| token | (пусто) | JWT пользователя |
- `get_services(client)``GET /services` → список Все cookie: httponly=True, samesite=Strict, secure=True, max_age=1 год.
- `get_service_detail(client, svc_id)``GET /services/{id}` → детали + операции
### `get_params.py` — КЛЮЧЕВОЙ модуль ### Функции
`get_params_with_current_values(client, op_id, instance_uid)`: get_mode() -> "polygon" | "cloud"
1. `GET /instances/{uid}``state.params` (текущие значения: `{"whereFail":"1",...}`) Без POLYGON_ENDPOINT -> всегда "cloud"
2. `GET /instanceOperations/default/{opId}` → шаблон (коды, типы, valueList, dataDescriptor) Из cookie, whitelist-валидация
3. Слияние: `defaultValue = state.params["код"] ?? template.defaultValue`
4. Приведение типов: `bool``"true"/"false"`, `dict``json.dumps()`
### `tracker.py` get_polygon_stand() -> "dev" | "test" | "prod"
Из cookie, whitelist-валидация
- Файл: `/tmp/instances-{clientId}-{stand}.json` (изолирован по пользователю и стенду) get_token() -> JWT-строка
- Блокировка: `fcntl.flock(LOCK_EX | LOCK_NB)` + retry 2s Эмуляция: только NUBES_API_TOKEN (env)
- `add(client_id, stand, uid, svc_id, name)`, `remove(...)`, `list_all(...)` Облако: cookie "token" -> NUBES_API_TOKEN
- Нужен ТОЛЬКО как fallback — когда инстанс только создан и ещё не в `/instances`
_cached_detect(token) -> URL эндпоинта (кеш в flask.g)
Вызывает detect_endpoint() только при первом обращении за запрос
Устраняет двойной HTTP-запрос (get_client + get_stand)
get_client() -> HttpClient
Эмуляция: polygon_url.replace("/api/v1/svc", "/{stand}/api/v1/svc")
Облако: _cached_detect(token)
get_stand() -> "polygon_test" | "dev" | "test" | "mock"
Эмуляция: "polygon_" + get_polygon_stand()
Облако: stand_name(_cached_detect(token))
### URL полигона
POLYGON_ENDPOINT = https://polygon.pythonk8s.dev.nubes.ru/api/v1/svc
Стенд ВСТАВЛЯЕТСЯ перед /api/v1/svc:
DEV: https://polygon.../dev/api/v1/svc
TEST: https://polygon.../test/api/v1/svc
PROD: https://polygon.../prod/api/v1/svc
--- ---
## 4. Бэкенд: Роуты ## 4. HTTP-клиент — http_client.py
### `main.py` HttpClient — обёртка над requests.Session:
- Заголовки: Authorization: Bearer, User-Agent: Mozilla/5.0 (DDoS-Guard)
- get(path) -> raise_for_status() -> .json(), таймаут 10с
- post(path, data) -> r.ok, JSON, Location -> UUID, таймаут 30с
- raw_delete(url) -> DELETE без авторизации (CMDB)
- **`GET/POST /`** — главная: организация, инфраструктура (serviceId 2,12,19,21,22,25,26,29,110,150), сервисы STANDS = [dev, test URL]
- `action=save` → сохранить токен в cookie detect_endpoint(token) — пробует dev->test, returns URL where results != None
- `action=clear` → удалить cookie stand_name(endpoint) — ТОЧНОЕ сравнение URL -> "dev"/"test"/"?"
- **`GET /api/operations/<svc_id>`** — операции + autotest-инстансы:
- Cloud-first: фильтр по префиксу `autotest-`
- Tracker-fallback: статус "creating" для ещё невидимых
### `api_test.py` — основной модуль операций
- **`GET /api/params/<op_id>[?instanceUid=xxx]`**:
- Без `instanceUid` → шаблонные `defaultValue`
- С `instanceUid``get_params_with_current_values()`
- **`POST /api/test`** — запуск операции:
- **CREATE**: `POST /instances``instanceUid``POST /instanceOperations``opUid``tracker_add` → params → `/run`
- **non-CREATE**: `POST /instanceOperations` → params → `/run` (redeploy: сразу `/run`)
- `displayName` = `_get_instance_display_name()` (из API, не UUID!)
- Фоновый поток: `_finish_op()` — поллинг до `dtFinish` (300s таймаут)
- **`GET /api/test/status/<op_uid>`** — поллинг: in-memory `_op_results` → прямой API
- **`GET /api/log`** — последние 200 строк из `/tmp/app-autotest.log`
### Логирование
`_log(msg)` в `api_test.py`:
- stdout (gunicorn) + `/tmp/app-autotest.log` (flock, общий для воркеров)
- Ротация при 512 КБ
- UI: скрытая панель, кнопка `log` (правый нижний угол)
--- ---
## 5. Фронтенд ## 5. Режимы работы и разделы БД
### Структура | Режим | Сервисы | Инстансы | Стенд в БД |
|--------|---------|----------|-----------|
| Эмуляция | Полигон | Полигон | polygon_{dev|test|prod} |
| Облако | Реальный API | Реальный API | dev / test |
Один HTML-файл, три колонки: Облако (2 стенда): dev, test
- Левая (280px): инфраструктура Эмуляция (3 стенда): polygon_dev, polygon_test, polygon_prod
- Средняя (240px): сервисы PROD в облаке недоступен — только через эмуляцию.
- Правая (flex): инстансы + параметры + кнопка + этапы
### Глобальное состояние (JS) ### Сервис-листы
``` load_service_ids(stand) обрезает префикс polygon_:
svcInstances — кеш инстансов (из /api/operations) polygon_test -> test -> services_test.txt
selectedInst — UID выбранного инстанса polygon_dev -> dev -> services_dev.txt
selectedOp — {opId, opName, svcId} polygon_prod -> prod -> services_prod.txt (не existe -> пустой set -> все сервисы)
pollTimer — таймер поллинга
SVC_ID = 1 — фиксированный сервис (Болванка)
```
### Потоки операций В шаблоне: если список пуст — показываются все сервисы.
**CREATE**: `startCreate()``showParams(18,'create')` → форма → `executeOp(pp)` → POST /api/test → поллинг → refreshInstances
**MODIFY**: `toggleInstance()` → кнопки → `runOp('modify',opId)``showParams(opId,'modify')` → GET /api/params?instanceUid= → форма с ТЕКУЩИМИ значениями → валидация map → `executeOp(pp)` → POST /api/test → поллинг
**Без параметров** (suspend/delete/resume/redeploy): `runOp()` → confirm → `executeOp({})` → POST /api/test → поллинг
### Валидация JSON
- `_esc(s)` — HTML-экранирование (`"``&quot;`, `&``&amp;`, `<``&lt;`)
- `validateJson(el, quiet)``JSON.parse()` на `onblur` (красная рамка + текст)
- Batch-проверка перед отправкой — ошибка → запрос не уходит
--- ---
## 6. Nubes API — используемые endpoint'ы ## 6. Роуты
| Метод | Путь | Назначение | ### main.py
|-------|------|-----------|
| GET | `/instances?pageSize=200&page=N` | Все инстансы (пагинация) | GET/POST / — главная страница
| GET | `/instances/{uid}` | Детали + state.params | action=save — сохранить токен в cookie
| GET | `/services` | Список сервисов | action=clear — удалить cookie
| GET | `/services/{id}` | Детали + операции | action=set_mode — сохранить mode + polygon_stand в cookie
| GET | `/instanceOperations/default/{opId}` | Шаблон параметров | polygon_stand: из формы, иначе из cookie (Облако->Эмуляция)
| POST | `/instances` | Создать инстанс | Валидация: mode in {polygon,cloud}, stand in {dev,test,prod}
| POST | `/instanceOperations` | Создать операцию | Загрузка: mode=="polygon" or active_token
| POST | `/instanceOperationCfsParams` | Установить параметр | Единый client = get_client()
| POST | `/instanceOperations/{opUid}/run` | Запустить операцию |
| GET | `/instanceOperations/{opUid}?fields=...` | Поллинг статуса | GET /api/operations/<svc_id> — операции + autotest-инстансы
Cloud-first + tracker-fallback, дедупликация
### api_test.py — ручной режим
GET /api/services — список сервисов
GET /api/instances/list — все инстансы
GET /api/params/<op_id>[?instanceUid=xxx] — параметры (шаблон или текущие)
POST /api/test — запуск операции, возвращает opUid
Валидация: serviceId(int>0), operation, svcOperationId(int>0), UUID(36), params(dict)
CREATE: _unique_display_name() + execute_operation()
non-CREATE: execute_operation()
delete: CMDB -> fallback API delete
Фон: _finish_op() — поллинг + save_run + tracker_remove
GET /api/test/status/<op_uid> — статус (_op_results -> API)
GET /api/log — последние 200 строк из /tmp/app-autotest.log
GET /api/history — последние 50 записей из runs (client_id + stand)
### api_scenario_run.py — сценарии
GET /api/scenarios — список определений
POST /api/scenario/run — запуск
lock_check -> None(503), False(409)
INSERT scenario_runs status=RUNNING (partial unique index)
UniqueViolation(pgcode 23505) -> 409
Фон: run_scenario(client, steps, cid, stand, ...)
GET /api/scenario/run/<run_id> — статус
GET /api/scenario/status — последние 10 запусков
### api_scenario_defs.py — CRUD
GET/POST /api/scenario/definitions — список/создать
GET/PUT/DELETE /api/scenario/definitions/<id> — один/обновить/удалить
PUT: оптимистичная блокировка (version -> 409)
DELETE: мягкое (is_active=FALSE)
--- ---
## 7. Ограничения платформы ## 7. Операции
- `site/` — НЕ пакет (без `__init__.py`), импорты без префикса `site.` ### executor.py
- `app.run(host="0.0.0.0", port=5000)`
- Gunicorn multi-worker → общие данные через файлы + flock execute_operation() — единый запуск:
- `/tmp/` теряется при редеплое 1. CREATE: POST /instances -> instanceUid -> tracker_add -> POST /instanceOperations -> opUid
- User-Agent: `Mozilla/5.0` обязателен (DDoS-Guard) 2. non-CREATE: POST /instanceOperations -> opUid
3. send_params_terraform() — нормализация, refSvcId, validate-cfs
4. POST /run -> запуск
### scenario.py
run_scenario() — для каждого шага:
1. get_service_detail() -> svcOperationId
2. GET /instanceOperations/default/{id} -> резолв параметров
3. Резолвинг instance_uid: явный uid > instance_ref > service_id
4. execute_operation()
5. save_run(RUNNING)
6. poll_until_done()
7. save_run(final) — UPDATE существующей строки
8. tracker_remove() после успешного delete
9. _save_scenario_run() — прогресс
### tracker.py
JSON-файловый кеш: /tmp/instances-{clientId}-{stand}.json
fcntl.flock(LOCK_EX|LOCK_NB) + retry 2s
add/remove/list_all — атомарные операции
--- ---
## 8. Безопасность и конкуренция (аудит GPT-5.3-Codex, 2026-07-31) ## 8. База данных
Полный аудит 29 файлов (~6000 строк Python + vanilla JS). Исправлено в v1.2.19-v1.2.20. ### pool.py
ThreadedConnectionPool(1,5), ленивый init в каждом воркере
_ensure_schema() -> init_db() при первом обращении
### Защита от XSS (Frontend) ### Таблицы
- **`_esc(s)` в utils.js** — HTML-escape: `&``&amp;`, `"``&quot;`, `<``&lt;` runs — история операций:
Применяется ко ВСЕМ данным из API перед `innerHTML`. id, created_at, client_id, stand, user_email, svc_id, svc_name,
- **params в scenario-list.js** — `_esc(k)+'='+_esc(v)` (было `k+'='+v` без экранирования). op_name, svc_op_id, op_uid, instance_uid, display_name, status,
- **JS injection в onclick** — имена сценариев с `'` теперь `replace(/'/g, "\\'")` перед duration_sec, error_log, params(JSONB), stages(JSONB), app_version,
вставкой в JS-строку внутри HTML-атрибута. scenario_run_id, step_number, instance_meta(JSONB)
Индексы: client_stand, instance, created, op_uid(UNIQUE WHERE NOT NULL)
### Защита от гонок (Backend) scenario_runs — запуски сценариев:
id, client_id, stand, scenario_name, status, current_step, total_steps,
instance_bindings(JSONB), duration_sec, error_log
Partial UNIQUE INDEX: (client_id, stand) WHERE status='RUNNING'
- **`_op_results`** — `threading.Lock()` вокруг всех операций чтения/записи/cleanup. scenario_definitions — определения:
`pop(k, None)` вместо `del dict[k]` — безопасно при конкурентном доступе. id, client_id, stand, name, steps(JSONB), version, is_active
- **Partial unique index для сценариев** — `CREATE UNIQUE INDEX idx_one_running UNIQUE: (client_id, stand, LOWER(name))
ON scenario_runs (client_id, stand) WHERE status = 'RUNNING'`. Seed: client_id="" AND stand="" — видны всем
Делает `INSERT INTO scenario_runs ... status='RUNNING'` атомарной проверкой:
вторая параллельная вставка получает unique violation → 409.
Это заменило сломанную реализацию на `pg_try_advisory_lock` (v1.2.20),
где lock брался на одном соединении, а unlock — на другом (из пула).
- **Tracker** — `_atomic_update()`: read→mutate→write под одним `fcntl.flock`.
Исключает lost-update между `add()` и `remove()` из разных воркеров gunicorn.
### Защита от зависания (Frontend polling) ### save_run.py
- **Счётчик ошибок** в `scenarioPollTimer` — после 5 последовательных ошибок: Ручной UPSERT:
`stopScenarioPoll()` + `busy=false` + сообщение об ошибке. 1. UPDATE runs SET ... WHERE op_uid = %s
- **Generation token** в `scenario-form.js` — `_renderGen` предотвращает перезапись 2. Если rowcount == 0 -> INSERT
нового DOM старыми данными от async `loadStepParams()`.
### Известные ограничения ---
- **`_op_results` in-memory на воркер** — не shared между gunicorn-воркерами. ## 9. Фронтенд
При отсутствии stickiness статус может читаться из API fallback вместо кеша.
Решение (отложено): Redis или общая таблица в БД для статусов операций. Сайдбар (192px, flex column):
sidebar-nav (flex:0) — кнопки видов
Селектор режима (радиокнопки, только при POLYGON_ENDPOINT)
sidebar-bottom — версия, email, токен (disabled в эмуляции)
Виды: workbench, overview, history-view, scenarios-view, logs-view
JS модули (12 файлов):
utils.js — _esc(), startLogPoll(), relativeTime()
views.js — switchView()
instances.js — selectService(), toggleInstance()
operations.js — startCreate(), runOp(), executeOp()
history.js — toggleHistory(), loadHistory()
scenario-list.js — runScenario(), поллинг (5 ошибок -> стоп)
scenario-form.js — редактор сценариев
Защита от XSS:
_esc(s) -> &amp;, &quot;, &lt;
o.operation: JS-escape + HTML-escape
escName: \\, \', &quot;
---
## 10. Nubes API — эндпоинты
GET /services — список
GET /services/{id} — детали + операции
GET /instances — все (пагинация)
GET /instances/{uid} — детали + state.params
GET /instanceOperations/default/{id} — шаблон cfsParams
POST /instances — создать -> 201 + Location
POST /instanceOperations — создать операцию -> 201 + Location
POST /instanceOperationCfsParams — установить параметр
GET /.../validate-cfs — валидация
POST /.../run — запустить
GET /.../{uid}?fields=... — поллинг
---
## 11. Безопасность
Гонки:
_op_results — threading.Lock(), pop(k,None)
idx_one_running — partial unique index (атомарный lock)
Tracker — fcntl.flock, атомарное read->mutate->write
save_run — ручной UPSERT без индекса
Cookie: httponly, samesite=Strict, secure, 1 год
Валидация:
serviceId: int>0, instanceUid: UUID(36), svcOperationId: int>0
params: dict, mode: {polygon,cloud}, stand: {dev,test,prod}
---
## 12. Тесты
22 теста: python3 -m pytest tests/ -v
test_api_scenario_run.py — 3 теста (503, 409, unique violation)
test_db_scenario_defs.py — 2 теста (lock_check None)
test_polygon_integration.py — 15 тестов (полный цикл с полигоном)
test_static_regressions.py — 2 теста (DDL, escName)
---
## 13. История версий (v1.2.28 -> v1.2.43)
1.2.28 — исходная (03.08)
1.2.29 — фикс сервисов при POLYGON_ENDPOINT
1.2.30 — селектор режима/стенда
1.2.31 — фикс URL полигона
1.2.35 — sidebar-nav flex:0 + форма по центру
1.2.36 — tracker_remove в сценариях + логи
1.2.39 — ручной UPSERT
1.2.40 — код-ревью #1: _esc() для o.operation
1.2.41 — код-ревью #2: кеш detect_endpoint
1.2.42 — код-ревью #3: whitelist mode/stand
1.2.43 — код-ревью #5,#6,#8,#9,#10: finally, stand_name, индекс, flush
+20
View File
@@ -0,0 +1,20 @@
Сергей Мищук, [22.07.2026 17:20]
у меня просьба подумать, как сделать автотесты на выбранные операции выбранных сервисов. Может быть, конфигурить через файл. Может быть, сделать веб интерфейс со списком сервисов-операций и статусами тестирования... Может сделать там же конфигурирование тестов. Запускать наверно захочется прямо на платформе
Владимир Крупский, [22.07.2026 17:36]
операции - create modify redeploy и тд ?
Владимир Крупский, [22.07.2026 17:37]
можно ... по всем сервисам есть yaml с полным перечнем всего что в сервисах есть.
Владимир Крупский, [22.07.2026 17:38]
сделаю во фласке или нодеjs
Владимир Крупский, [22.07.2026 17:39]
пока со своим токеном ... токенами по соотв. стендам
Сергей Мищук, [22.07.2026 18:07]
только не по всем можно все, поэтому нужна настройка. Это будет гоняться на проде. Например, организацию создавать нельзя, объекты надо делать и удалять в специальной тестовой организации. Кроме того, для многих сервисов действует удаление с передержкой. Поэтому для них можно 1 раз сделать создание и не делать удаление
Сергей Мищук, [22.07.2026 18:07]
это все надо конфигурить, а не кодировать
+9 -95
View File
@@ -1,103 +1,17 @@
# DOCS — Архив документации # DOCS — Архив документации
## Архитектура ## Актуальное
| Файл | Описание | | Файл | Описание |
|------|----------| |------|----------|
| `ARCHITECTURE.md` | Актуальная архитектура: эндпоинты, схема БД, безопасность (§8 с фиксами аудита) | | `ARCHITECTURE.md` | **Главный документ.** Полная архитектура v1.2.43: режимы, БД, роуты, безопасность, тесты |
| `ARCHITECTURE-FULL.md` | Развёрнутая архитектура: полный список файлов, все роуты, зависимости | | `howto-flask-nubes.md` | Как писать Flask-приложение под Nubes: структура `site/`, порт, `app.run()` |
| `ARCHITECTURE.legacy.md` | Архитектура v1.0.x (до рефакторинга) | | `polygon-plan.md` | План мок-полигона: 3 фазы, 12 шагов, YAML-спецификация |
| `architecture-final.md` | Финальная архитектура после всех рефакторингов и аудитов | | `sonnet-app-autotest-review-v1.2.39.md` | Код-ревью Соннета 4.6 (2026-08-03): 10 находок |
| `architecture-next.md` | Черновик архитектуры vNext — идеи на будущее |
| `architecture-questions-for-sonnet.md` | Вопросы к Sonnet по архитектуре (перед ревью) |
| `architecture-review-sonnet.md` | Ревью архитектуры от Sonnet |
| `architecture-round2.md` | Второй раунд архитектурных решений |
## Как делать (How-to) ## LEGACY/ — исторические
| Файл | Описание | 53 файла в папке `LEGACY/` — история разработки: старые архитектуры, ревью
|------|----------| v1.0.x/v1.1.x, промпты для Sonnet/Opus/Sol, аудиты, планы, обсуждения.
| `howto-flask-nubes.md` | **ВАЖНО.** Как писать Flask-приложение под Nubes: структура `site/`, порт 5000, `app.run()`, запреты |
## API Nubes Актуальная архитектура — только в `ARCHITECTURE.md` (v1.2.43).
| Файл | Описание |
|------|----------|
| `api-access.md` | Как получить доступ к Nubes API: токены, clientId, стенды |
| `api-create-flow.md` | Полный flow создания инстанса: instances → instanceOperations → params → run → poll |
| `api-operation-stages.md` | Стадии выполнения операций (plan, apply, etc.) |
| `terraform-operations-full-logic.md` | Логика Terraform-операций: validate-cfs, refSvc, нормализация параметров |
## Планы
| Файл | Описание |
|------|----------|
| `polygon-plan.md` | **ВАЖНО.** План мок-полигона: 3 фазы, 12 шагов, YAML-спецификация |
| `opus-plan-2026-07-31.md` | План Опуса: унификация executor, гибкие ссылки, 4 фазы 13 шагов |
| `test-results-history-plan.md` | План истории результатов тестов |
| `gpt56-sol-plan.md` | План Sol по GPT-5.6 |
## Промпты для агентов
| Файл | Описание |
|------|----------|
| `agent-analysis-request.md` | Запрос на анализ проекта (архитектура, риски, рекомендации) |
| `opus-architecture-prompt.md` | Промпт Опусу: полное описание проекта + вопросы по архитектуре |
| `opus-frontend-prompt.md` | Промпт Опусу: вопросы по фронтенду (instance dropdown) |
| `gpt56-sol-prompt.md` | Промпт GPT-5.6: описание проекта для Sol |
| `sonnet-review-prompt.md` | Промпт Sonnet для code review |
## Ревью и аудиты — Sonnet
| Файл | Описание |
|------|----------|
| `sonnet-full-code-review.md` | Полный code review всех файлов |
| `sonnet-full-response.md` | Полный ответ Sonnet на review |
| `sonnet-full-review.md` | Сводка review |
| `sonnet-architecture-review-v1.0.89.md` | Архитектурное ревью v1.0.89 |
| `sonnet-architecture-review-v1.0.89-r2.md` | Второй раунд архитектурного ревью |
| `sonnet-review-v1.1.11.md` | Ревью v1.1.11 |
| `sonnet-review-answers.md` | Ответы на замечания Sonnet |
| `sonnet-review-round2.md` | Второй раунд ревью |
| `sonnet-review-round3.md` | Третий раунд ревью |
| `sonnet-final-audit.md` | Финальный аудит |
| `sonnet-final-review.md` | Финальное ревью |
| `sonnet-missed-bugs.md` | Пропущенные баги (что Sonnet не нашёл) |
| `sonnet-params-and-audit.md` | Параметры + аудит |
| `sonnet-response-params-audit.md` | Ответ: параметры и аудит |
| `sonnet-response-final.md` | Финальный ответ |
| `sonnet-response-round2.md` | Ответ второго раунда |
| `sonnet-response-v1.1.11.md` | Ответ на ревью v1.1.11 |
| `sonnet-response-serviceInstanceUid.md` | Ответ про serviceInstanceUid |
| `sonnet-response-state-out.md` | Ответ про state.out |
| `sonnet-state-out-valuelist.md` | stateOut + valueList |
| `sonnet-terraform-diff.md` | Разбор Terraform diff |
| `sonnet-new-chat.md` | Новый чат с Sonnet |
| `sonnet-question-tracker.md` | Трекер вопросов к Sonnet |
## Ревью — Опус
| Файл | Описание |
|------|----------|
| `opus-questions-2026-07-31.md` | 19 уточняющих вопросов Опуса по архитектуре |
| `opus-instance-dropdown-questions.md` | Вопросы Опуса про instance dropdown |
| `opus-review-v1.2.0.md` | Ревью v1.2.0 от Опуса |
## Вопросы-ответы
| Файл | Описание |
|------|----------|
| `questions-to-sol.md` | Вопросы к Sol |
| `sol-answers.md` | Ответы Sol |
| `sol-scenario-editor.md` | Sol про редактор сценариев |
## Прочее
| Файл | Описание |
|------|----------|
| `HISTORY.md` | Краткая хронология версий (v1.0.x → v1.2.x) |
| `dialogues.md` | Диалоги/дискуссии в процессе разработки |
| `service-categories.md` | Категории сервисов Nubes (37 сервисов, их типы) |
| `review-comparison-sonnet-opus.md` | Сравнение ревью Sonnet vs Opus |
| `step-by-step-audit-v1.0.50.md` | Пошаговый аудит v1.0.50 |
| `vm-213.md` | Заметки о VM-213 (тестовый стенд) |
+132
View File
@@ -0,0 +1,132 @@
# Code Review: app-autotest v1.2.39 — Соннет
Дата: 2026-08-03
---
## 🔴 Критические
**1. XSS в instances.js — `o.operation` в innerHTML без `_esc()`**
instances.js (функция `toggleInstance`):
```js
opsEl.innerHTML = ops.map(o =>
`<button ... onclick="runOp('${o.operation}',${o.svcOperationId})">${o.operation}</button>`
).join('');
```
`o.operation` вставляется **три раза без `_esc()`**: в onclick-атрибут (`'${...}'`), в текст кнопки (`>${...}<`). Если API вернёт `operation = "'; alert(1)//"` — onclick-атрибут ломается. На практике операции — это "modify"/"delete" из Nubes API, но принцип нарушен. `_esc()` определён специально для этого.
---
## 🟡 Важные
**2. Двойной вызов `detect_endpoint()` при каждом HTTP-запросе**
auth.py — `get_client()` вызывает `detect_endpoint(token)`, и `get_stand()` вызывает `detect_endpoint(token)` независимо. В api_test.py при `POST /api/test`:
```python
client = get_client() # detect_endpoint() #1
...
get_client_id(), get_stand() # detect_endpoint() #2
```
Итого **2 лишних HTTP-запроса к Nubes API** (dev + test) при каждом запросе в облачном режиме. Оба блока `if endpoint in STANDS` срабатывают при стандартной конфигурации.
**3. `mode` и `polygon_stand` не валидируются при `set_mode`**
main.py:
```python
new_mode = request.form.get("mode", "polygon")
new_stand = request.form.get("polygon_stand") or request.cookies.get("polygon_stand", "test")
```
Любое значение пишется в cookie. Если `polygon_stand = "evil/../../../etc"`, то в `get_client()` формируется URL:
```
polygon_url.replace("/api/v1/svc", "/evil/../../../etc/api/v1/svc")
```
Путь нормализуется HTTP-клиентом/сервером. Допустимые значения известны: `mode` ∈ {"polygon","cloud"}, `polygon_stand` ∈ {"dev","test","prod"} — надо добавить whitelist.
**4. В polygon-режиме у всех пользователей одинаковый `client_id`**
auth.py:
```python
def get_token():
if get_mode() == "polygon":
return current_app.config["NUBES_API_TOKEN"] # env-токен
```
`get_client_id()` тоже парсит env-токен → у всех пользователей в эмуляции одинаковый ClientID. Вся изоляция данных в БД (`runs`, `scenario_runs`, `scenario_definitions`) — по `(client_id, stand)`. Если два пользователя переключатся в polygon-режим — они видят историю и сценарии **друг друга**. Если приложение использует только один человек — не проблема. Если несколько — критично.
**5. `api_history` — соединение не в `finally`**
api_test.py:
```python
try:
conn = get_conn()
cur = conn.cursor()
cur.execute(...)
...
cur.close()
put_conn(conn) # явный return соединения
return jsonify(result)
except Exception as e:
try: cur.close() # NameError если исключение до cur = ...
except: pass
try: put_conn(conn) # NameError если исключение до conn = ...
except: pass
return jsonify({"error": str(e)}), 500
```
Все остальные функции (`api_scenario_run_status`, `api_scenario_status`) используют `finally: put_conn(conn)` — корректный паттерн. Здесь — нет. При ошибке до `cur.close()` соединение возвращается, но `cur` не закрывается. Не утечка (psycopg2 закроет при возврате conn в пул), но несоответствие стилю.
**6. `stand_name()` — подстрочный поиск**
http_client.py:
```python
for name in ("dev", "test"):
if name in (endpoint or ""):
return name
```
Если URL содержит "dev" как часть другого слова (например, "development", "devnull"), вернёт неверное значение. URL-то сейчас конкретные, но хрупко.
---
## 🟢 Рекомендации
**7. Двойной отступ в scenario.py**
scenario.py:
```python
for i, step in enumerate(steps):
step_num = i + 1 # 8 пробелов вместо 4
```
Весь цикл имеет нестандартный отступ (8 пробелов). Работает корректно (Python), но выглядит как след удалённого `try:` или `with:` блока, который был внутри for.
**8. `get_real_client()` — мёртвый алиас**
auth.py: функция идентична `get_client()`, есть только для обратной совместимости. В main.py и других файлах есть вызовы `get_real_client()`. Стоит постепенно заменить на `get_client()` и убрать алиас.
**9. Нет `UNIQUE` на `runs.op_uid`**
init_db.py: ручной UPSERT в save_run.py (UPDATE → INSERT) предполагает уникальность `op_uid`. Индекса нет. При маловероятной гонке двух параллельных `save_run` с одним `op_uid` — оба сделают UPDATE (rowcount=0) → оба сделают INSERT → дубль. Добавить `CREATE UNIQUE INDEX IF NOT EXISTS idx_runs_op_uid ON runs (op_uid) WHERE op_uid IS NOT NULL`.
**10. Ротация лога: `flock(LOCK_UN)` перед flush буфера**
api_test.py:
```python
f.write(rest)
fcntl.flock(f, fcntl.LOCK_UN) # лок снят, но буфер ещё не сброшен
```
Python-буфер сбрасывается при закрытии файла (`with`-блок), но лок уже снят. Другой воркер может прочитать неполные данные. Добавить `f.flush()` перед `LOCK_UN`.
---
## Итого по приоритетам
| # | Файл | Проблема | Серьёзность |
|---|------|----------|-------------|
| 1 | `static/js/instances.js` | `o.operation` в innerHTML без `_esc()` | 🔴 |
| 2 | `api/auth.py` | Двойной `detect_endpoint()` на запрос | 🟡 |
| 3 | `routes/main.py` | `mode`/`polygon_stand` без whitelist-валидации | 🟡 |
| 4 | `api/auth.py` | Shared `client_id` в polygon-режиме | 🟡 |
| 5 | `routes/api_test.py` | `api_history` без `finally` | 🟡 |
| 6 | `api/http_client.py` | `stand_name()` substring match | 🟡 |
| 7 | `operations/scenario.py` | Двойной отступ в for-цикле | 🟢 |
| 8 | `api/auth.py` | `get_real_client()` мёртвый алиас | 🟢 |
| 9 | `db/init_db.py` | Нет UNIQUE индекса на `runs.op_uid` | 🟢 |
| 10 | `routes/api_test.py` | `flock(LOCK_UN)` перед flush в ротации | 🟢 |
+124
View File
@@ -0,0 +1,124 @@
# 2026-08-03-session — Полигон: интеграция, баг с сервисами
## Контекст
Сессия началась с чтения двух файлов AI-агентом:
- `AGENT_BRIEFING.md` — контекст app-autotest (v1.2.28)
- `polygon-docs/POLYGON-FULL.md` — полное описание полигона (v0.5.9)
jsonEnv автотеста на проде:
```json
{
"POLYGON_ENDPOINT": "https://polygon.pythonk8s.dev.nubes.ru/api/v1/svc",
"NUBES_API_ENDPOINT": "https://lk-api-gateway-test.ngcloud.ru/api/v1/svc"
}
```
## Архитектурное решение: разделение сервисы/инстансы
**Зафиксировано (2026-08-03):**
| Режим | Сервисы (метаданные) | Инстансы |
|--------|---------------------|-----------|
| Без полигона | Реальный API | Реальное облако |
| С полигоном | Реальный API | Виртуальные (полигон) |
`POLYGON_ENDPOINT` подменяет ТОЛЬКО инстансы/операции. Сервисы — это «справочник», всегда из реального API.
### Где это в коде
`auth.py:_make_client(use_polygon)`:
- `use_polygon=True``get_client()`: если `POLYGON_ENDPOINT` задан → полигон, иначе → `NUBES_API_ENDPOINT`
- `use_polygon=False``get_real_client()`: **всегда** `NUBES_API_ENDPOINT`
## Баг: «нет сервисов» при POLYGON_ENDPOINT
### Причина
Цепочка:
1. `get_stand()``"polygon"` (auth.py:141, POLYGON_ENDPOINT задан)
2. `load_service_ids("polygon")``services_polygon.txt` не существует → пустой `set`
3. `sorted(пустой_set)``[]`
4. Шаблон `index.html:81`: `{% if svc.svcId in config.service_ids %}``svc.svcId in []`**всегда False** → ни один сервис не показывается
Хотя `get_real_client()` корректно получает сервисы из реального API, фильтр в шаблоне убивает весь список.
### Исправление (v1.2.29)
Шаблон `index.html:81`: добавить `or not config.service_ids`:
```jinja2
{# Было #}
{% if svc.svcId in config.service_ids %}
{# Стало #}
{% if not config.service_ids or svc.svcId in config.service_ids %}
```
Это соответствует изначальному замыслу `load_service_ids`: «пустой set → показываем все».
---
## Фаза 2: селектор режима и стенда (v1.2.30)
### Требования
1. **Селектор режима** над версией: 🎭 Эмуляция / ☁️ Облако (только если POLYGON_ENDPOINT задан)
2. **Селектор стенда** (DEV/TEST/PROD) — только в режиме эмуляции
3. **По умолчанию:** эмуляция + TEST
4. **В эмуляции:** токен дезактивирован
5. **В облаке:** стенд автоопределяется по токену (как раньше)
6. При смене режима/стенда → сервисы обновляются
### Новая архитектура
| Режим | Сервисы | Инстансы | Стенд |
|--------|---------|----------|-------|
| Эмуляция | Полигон (/{stand}/api/v1/svc) | Полигон | Выбор (dev/test/prod) |
| Облако | Реальный API | Реальный API | Авто (токен→dev/test) |
В эмуляции **всё** идёт в полигон — и сервисы, и инстансы. Полигон имеет полный набор эндпоинтов (/services, /instances, /instanceOperations).
`get_real_client()` упраздняется — становится алиасом `get_client()`.
### Cookie
- `mode``"polygon"` (по умолчанию) или `"cloud"`
- `polygon_stand``"test"` (по умолчанию), `"dev"`, `"prod"`
### Изменённые файлы
1. **`auth.py`** — новые функции `get_mode()`, `get_polygon_stand()`; `get_token()` в режиме полигона только env-токен; `get_client()` строит URL с префиксом стенда; `get_real_client()` → алиас
2. **`main.py`** — `action=set_mode` (cookie), single `get_client()` вместо real_client/inst_client, переменные `mode`/`polygon_stand`/`polygon_enabled`
3. **`index.html`** — селекторы над версией, disable токена в эмуляции
4. **`service_list.py`** — обрезать `polygon_` префикс
5. **`api_test.py`** — `get_real_client()``get_client()`
---
## Фаза 3: Код-ревью Соннета 4.6 (v1.2.39)
### Промпт
```
Ты — senior backend-разработчик, делаешь код-ревью веб-приложения app-autotest.
Flask 3.1 + vanilla JS + PostgreSQL. Два режима: эмуляция (Polygon) / облако.
Проверь внимательно: безопасность, URL, гонки, целостность данных, ошибки, архитектуру, JS.
Для каждой находки: 🔴 критическое / 🟡 важное / 🟢 рекомендация.
Если неясно — спроси, устроим диалог.
```
### Результаты
| # | Файл | Проблема | Серьёзность |
|---|------|----------|-------------|
| 1 | `static/js/instances.js` | `o.operation` в innerHTML без `_esc()` | 🔴 |
| 2 | `api/auth.py` | Двойной `detect_endpoint()` на запрос | 🟡 |
| 3 | `routes/main.py` | `mode`/`polygon_stand` без whitelist-валидации | 🟡 |
| 4 | `api/auth.py` | Общий `client_id` в polygon-режиме (env-токен) | 🟡 |
| 5 | `routes/api_test.py` | `api_history` без `finally` для conn | 🟡 |
| 6 | `api/http_client.py` | `stand_name()` substring match | 🟡 |
| 7 | `operations/scenario.py` | Двойной отступ в for-цикле | 🟢 |
| 8 | `api/auth.py` | `get_real_client()` мёртвый алиас | 🟢 |
| 9 | `db/init_db.py` | Нет UNIQUE индекса на `runs.op_uid` | 🟢 |
| 10 | `routes/api_test.py` | `flock(LOCK_UN)` перед flush в ротации лога | 🟢 |
+458
View File
@@ -0,0 +1,458 @@
# Polygon — эмулятор Nubes API
> Версия: v0.5.9 | Деплой: `polygon.pythonk8s.dev.nubes.ru` | Формат: Nubes managed Flask
---
## 1. Что такое Polygon
Polygon — **эмулятор REST API облачной платформы Nubes** для интеграционных тестов.
Он полностью повторяет контракты реального API (сервисы, инстансы, операции, параметры, валидацию),
но работает **без реальной инфраструктуры** — в памяти, с мгновенным откликом.
### Зачем нужен
- Тестировать создание/изменение/удаление инстансов без реальных облачных ресурсов
- Отлаживать UI автодеплоя (`app-autotest`) на мок-данных
- Писать интеграционные тесты с детерминированным состоянием
- Проверять краевые случаи (ошибки валидации, сбои операций)
- Демонстрировать API заказчикам через Swagger UI
### Что НЕ делает
- Не управляет реальной инфраструктурой
- Не хранит данные между перезапусками (всё в памяти)
- Не авторизует пользователей (кроме `_mock/*` служебных эндпоинтов)
- Не повторяет ВСЕ эндпоинты реального API — только те что нужны для тестов
---
## 2. Архитектура
### Технологический стек
| Компонент | Технология | Зачем |
|-----------|-----------|-------|
| Веб-фреймворк | Flask 3.0 | Маршрутизация, шаблоны, JSON-ответы |
| WSGI-сервер | gunicorn | Запуск на проде (встроен в Nubes managed Flask) |
| Конфигурация | YAML (PyYAML) | Описание сервисов, операций, параметров |
| Состояние | In-memory dict | Инстансы, операции, параметры |
| Шаблоны | Jinja2 | HTML-страницы (главная, Swagger) |
| OpenAPI | OpenAPI 3.1.0 | Спека API (генерится динамически) |
| Swagger UI | Swagger UI 5 (CDN) | Интерактивная документация |
### Ключевое ограничение: 1 воркер
```python
# app.py
os.environ.setdefault("WEB_CONCURRENCY", "1")
```
**Почему:** состояние (инстансы, операции) хранится в памяти Python-процесса.
Два воркера = два набора инстансов = хаос. Поэтому жёстко 1 gunicorn-воркер.
**Следствия:**
- Только 1 запрос обрабатывается одновременно
- Большие ответы (>20KB) могут обрываться из-за таймаута сети
- Swagger-спека встроена прямо в HTML чтобы избежать второго запроса
- Нельзя горизонтально масштабировать
### Модель данных в памяти
```
MockState (синглтон на стенд)
├── instances: {instanceUid → {instanceUid, serviceId, displayName, status, state, ...}}
├── operations: {opUid → {instanceOperationUid, instanceUid, operation, dtStart, dtFinish, ...}}
├── op_params: {opUid → {paramId(int) → paramValue(str)}}
└── fail_next: bool (one-shot флаг для симуляции ошибок)
```
### Жизненный цикл запроса
```
HTTP-запрос → Nubes ingress → gunicorn (1 воркер) → Flask
→ StandMiddleware (извлекает stand_id из URL, перезаписывает PATH_INFO)
→ before_request (копирует stand_id в flask.g)
→ Blueprint-роут (через LocalProxy резолвит state/SERVICES под текущий стенд)
→ JSON-ответ
```
---
## 3. Стенды (dev / test / prod)
Polygon эмулирует **три изолированных стенда** облака:
| Стенд | URL-префикс | Сервисов | YAML из |
|-------|------------|----------|---------|
| dev | `/dev/api/v1/svc/...` | 37 | `~/tf_provider/generated/dev/` |
| test | `/test/api/v1/svc/...` | 37 | `~/tf_provider/generated/test/` |
| prod | `/prod/api/v1/svc/...` | 35 | `~/tf_provider/generated/prod/` |
**Изоляция:** у каждого стенда **свои** инстансы, операции, параметры.
`fail-next` на dev не влияет на test. Состояние каждого стенда независимо.
**Переключение в Swagger:** выпадающий список серверов (Server selector).
**Переключение в URL:** просто добавь префикс — `/dev/`, `/test/`, `/prod/`.
Без префикса — стенд по умолчанию (dev).
**Настройка:** переменная окружения `POLYGON_STANDS`:
```
POLYGON_STANDS=dev:services/dev,test:services/test,prod:services/prod
```
Формат: `имя_стенда:путь_к_YAML,имя_стенда:путь_к_YAML,...`
---
## 4. YAML-пайплайн (откуда берутся сервисы)
Polygon не знает о сервисах сам — он читает их из YAML-конфигов,
которые генерируются из терраформ-репы.
### Цепочка
```
~/tf_provider/generated/{dev,test,prod}/resources_yaml/*.yaml ← источник (терраформ)
│ from_stands.py — конвертация формата (ВРУЧНУЮ)
│ • дедупликация параметров (один param в create/modify/delete → одна запись)
│ • построение cfsParamsByOp (какие параметры к какой операции)
│ • сбор stateParams из create-операции
│ • генерация stateOut из subresource-операций
│ • раскодирование HTML-entities (&quot; → ")
polygon/site/services/{dev,test,prod}/*.yaml ← скоммичены в git
│ config/loader.py — загрузка ВСЕХ YAML в память при старте
│ • _load_one() для каждой папки стенда
│ • STANDS = {dev: {SERVICES, OPS_INDEX}, test: {...}, prod: {...}}
│ • ~0.7 MB на 3 стенда
Память Flask-процесса → API отдаёт данные из памяти (без диска)
```
### Когда обновлять YAML
Когда терраформ-репа обновилась (добавили сервис, изменили параметры):
```bash
cd polygon/site
python from_stands.py ~/tf_provider/generated/dev/resources_yaml services/dev
python from_stands.py ~/tf_provider/generated/test/resources_yaml services/test
python from_stands.py ~/tf_provider/generated/prod/resources_yaml services/prod
cd .. && git add services/ && git commit -m "regenerate YAML" && git push
# → редеплоить polygon через Nubes UI
```
**Это ручная операция.** Нет автоматического триггера.
---
## 5. Структура кода
```
polygon/
├── requirements.txt # Flask>=3.0, gunicorn>=21.2, PyYAML>=6.0
├── Procfile # web: gunicorn site.app:app --bind 0.0.0.0:8000
├── tests/
│ ├── test_api.py # 39 smoke-тестов против деплоя
│ ├── fuzz_test.py # 147 фаззинг-тестов (злонамеренные входные данные)
│ ├── compare_test.py # Сравнение полигона с реальным Nubes API
│ ├── test_converter.py # Юнит-тесты from_stands.py
│ └── test_state_machine.py # Юнит-тесты apply_effect()
└── site/
├── app.py # Flask-приложение: StandMiddleware, blueprint'ы
├── mock_state.py # MockState — хранилище инстансов/операций
├── state_machine.py # apply_effect() — мутация состояния инстанса
├── from_stands.py # Конвертер terraform YAML → polygon YAML
├── config/
│ └── loader.py # Загрузка YAML → SERVICES, OPS_INDEX, STANDS
├── routes/
│ ├── root.py # /health, /, /swagger
│ ├── services_routes.py # /api/v1/svc/services
│ ├── instances_routes.py # /api/v1/svc/instances
│ ├── operations_routes.py # /api/v1/svc/instanceOperations/*
│ ├── run.py # /api/v1/svc/instanceOperations/<uid>/run
│ ├── mock_routes.py # /api/v1/svc/_mock/*
│ └── openapi.py # /api/v1/svc/openapi.json (OpenAPI 3.1.0 спека)
├── services/
│ ├── dev/ # 37 YAML-конфигов (стенд dev)
│ ├── test/ # 37 YAML-конфигов (стенд test)
│ └── prod/ # 35 YAML-конфигов (стенд prod)
├── static/
│ ├── style.css # Дизайн-система Nubes
│ ├── logo.svg # Логотип
│ └── favicon.svg # Иконка
├── templates/
│ ├── index.html # Главная страница (инфо + ссылки на стенды)
│ └── swagger.html # Swagger UI (спека встроена в HTML)
└── utils/
├── now.py # now() — UTC ISO с 'Z'
└── pluralize.py # pluralize() — плюрализация
```
### Ключевые архитектурные решения
**LocalProxy** — все роуты используют `werkzeug.local.LocalProxy` для доступа к `state` и `SERVICES`:
```python
# В каждом роуте:
from flask import g
from werkzeug.local import LocalProxy
import mock_state, config.loader as _cfg
state = LocalProxy(lambda: mock_state.get_state(g.stand_id))
SERVICES = LocalProxy(lambda: _cfg.get_services(g.stand_id))
```
Это позволяет одному и тому же коду роута работать с разными стендами — `g.stand_id` определяет какой стенд используется.
**StandMiddleware** (app.py) — WSGI-middleware который извлекает `stand_id` из URL ДО Flask-роутинга:
```python
class StandMiddleware:
def __call__(self, environ, start_response):
path = environ["PATH_INFO"]
parts = path.split("/")
if parts[1] in STANDS:
environ["polygon.stand_id"] = parts[1]
environ["PATH_INFO"] = "/" + "/".join(parts[2:]) # /dev/api/... → /api/...
else:
environ["polygon.stand_id"] = DEFAULT_STAND
return self.wsgi_app(environ, start_response)
```
**before_request** (app.py) — копирует stand_id в `flask.g`:
```python
@app.before_request
def _set_stand():
g.stand_id = request.environ.get("polygon.stand_id", DEFAULT_STAND)
```
---
## 6. API — 17 эндпоинтов
| Метод | Путь | Назначение | Ответ |
|-------|------|------------|-------|
| GET | `/health` | Healthcheck для Nubes | `OK` (text) |
| GET | `/` | HTML с инфо + ссылки на стенды | HTML |
| GET | `/swagger` | Swagger UI (спека в HTML) | HTML |
| GET | `/api/v1/svc/openapi.json` | OpenAPI 3.1.0 спека | JSON |
| GET | `/api/v1/svc/services` | Список сервисов | `{results: [...]}` |
| GET | `/api/v1/svc/services/{id}` | Детали сервиса (операции) | `{svc: {operations: [...]}}` |
| GET | `/api/v1/svc/instances` | Список инстансов (пагинация) | `{results, pageSize, page, total}` |
| GET | `/api/v1/svc/instances/{uid}` | Полные данные инстанса | `{instance: {...}}` |
| POST | `/api/v1/svc/instances` | Создать инстанс | `201 + Location + {instanceUid}` |
| GET | `/api/v1/svc/instanceOperations/default/{id}` | Шаблон операции (cfsParams) | `{svcOperation: {cfsParams: [...]}}` |
| POST | `/api/v1/svc/instanceOperations` | Создать операцию | `201 + Location + {instanceOperationUid}` |
| GET | `/api/v1/svc/instanceOperations/{uid}` | Статус операции | `{instanceOperation: {...}}` |
| POST | `/api/v1/svc/instanceOperationCfsParams` | Установить параметр | `{}` |
| GET | `/api/v1/svc/instanceOperations/{uid}/validate-cfs` | Валидация параметров | `""` (пустое тело) |
| POST | `/api/v1/svc/instanceOperations/{uid}/run` | Выполнить операцию | `{ok: true/false, error?}` |
| POST | `/api/v1/svc/_mock/reset` | Сброс состояния | `{reset: "ok"}` |
| GET | `/api/v1/svc/_mock/state` | Дамп состояния (отладка) | `{instances, operations}` |
| GET | `/api/v1/svc/_mock/services` | Список загруженных сервисов (отладка) | `{count, services}` |
| POST | `/api/v1/svc/_mock/delay/{s}` | Задать задержку операций (max 5s) | `{delay: N}` |
| POST | `/api/v1/svc/_mock/fail-next` | Следующая операция упадёт (one-shot) | `{fail_next: true}` |
### Типовой сценарий: создать инстанс и запустить операцию
Это основной флоу, который повторяет логику реального облачного API.
Все примеры — с jq для наглядности, но работают и с `python3 -m json.tool`.
**Шаг 1. Узнать ID сервиса по имени:**
```bash
curl -s "$URL/services" | jq '.results[] | select(.svc == "НазваниеСервиса") | .svcId'
```
**Шаг 2. Узнать ID операции у этого сервиса:**
```bash
curl -s "$URL/services/$SVC_ID" | jq '.svc.operations[] | select(.operation == "create") | .svcOperationId'
```
**Шаг 3. Получить список входных параметров (cfsParams):**
```bash
curl -s "$URL/instanceOperations/default/$OP_ID" | jq '.svcOperation.cfsParams[] | {svcOperationCfsParamId, svcOperationCfsParam, dataType, valueList, isRequired}'
```
Ответ показывает для каждого параметра: числовой ID, код, тип данных, список допустимых значений (если есть), обязательность.
**Шаг 4. Создать инстанс:**
```bash
curl -s -X POST "$URL/instances" \
-H "Content-Type: application/json" \
-d '{"serviceId": '$SVC_ID', "displayName": "мой тестовый инстанс"}' \
| jq '.instanceUid'
```
**Шаг 5. Создать операцию:**
```bash
curl -s -X POST "$URL/instanceOperations" \
-H "Content-Type: application/json" \
-d '{"instanceUid": "'$INST_UID'", "operation": "create", "svcOperationId": '$OP_ID'}' \
| jq '.instanceOperationUid'
```
**Шаг 6. Установить параметры (повторить для каждого):**
```bash
curl -s -X POST "$URL/instanceOperationCfsParams" \
-H "Content-Type: application/json" \
-d '{"instanceOperationUid": "'$OP_UID'", "svcOperationCfsParamId": '$PARAM_ID', "paramValue": "значение"}'
```
**Шаг 7. Проверить валидацию и запустить:**
```bash
# Проверить что все параметры валидны (200 = OK)
curl -s -o /dev/null -w "%{http_code}" "$URL/instanceOperations/$OP_UID/validate-cfs"
# Запустить операцию (будет ждать DELAY секунд)
curl -s -X POST "$URL/instanceOperations/$OP_UID/run" | jq '{ok, error}'
```
**Шаг 8. Проверить результат:**
```bash
curl -s "$URL/instanceOperations/$OP_UID?fields=dtFinish,isSuccessful" | jq '.instanceOperation | {dtFinish, isSuccessful}'
```
Полигон полностью повторяет эту последовательность: те же эндпоинты, те же поля, те же статус-коды (201 при создании, 409 при повторном run, 404 при несуществующем ресурсе).
### Валидация входных данных
- `serviceId` — только integer > 0 (строка/float/отрицательное → 400)
- Тело запроса — должно быть JSON-объектом (массив → 400)
- `svcOperationCfsParamId` — только integer (строка → 400)
- Повторный `run` — 409 (already completed)
### Аутентификация
- Основные эндпоинты — **без авторизации**
- `_mock/*` — заголовок `X-Mock-Auth` (если `MOCK_AUTH_TOKEN` задан в env)
- По умолчанию `MOCK_AUTH_TOKEN` не задан → `_mock/*` открыты
---
## 7. Тесты
### test_api.py — 39 smoke-тестов
Запуск: `python3 tests/test_api.py`
Проверяет деплоенный полигон по всем 17 эндпоинтам:
- Health, services, instances (CRUD + pagination + edge cases)
- Operations (create → params → validate → run → double-run 409)
- Fail-next (one-shot error simulation)
- Auth (без токена, неверный токен, верный токен)
- Mock state (reset, state dump, delay bounds)
- Multi-stand (изоляция dev/test/prod, fail-next изоляция)
### fuzz_test.py — 147 фаззинг-тестов
Запуск: `cd site && PYTHONPATH=. python3 ../tests/fuzz_test.py` (локально, Flask test client)
Злонамеренные и экстремальные сценарии:
- SQL injection, XSS, Unicode-emoji, null-байты, 10000-символьные строки
- Неверные типы (строка вместо int, float, отрицательные)
- Отсутствующие поля, null-поля
- Path traversal, двойные слеши
- Быстрые повторы (10 инстансов подряд)
- Невалидные UUID
- Неправильные HTTP-методы (PUT на GET, DELETE на POST)
### compare_test.py — сравнение с реальным API
Запуск: `python3 tests/compare_test.py`
Сравнивает read-only эндпоинты полигона с реальным Nubes API (dev/test/prod):
- `GET /services` — одинаковый ли список сервисов
- `GET /services/{id}` — одинаковые ли операции (svcOperationId, operation, kind, action)
- `GET /instanceOperations/default/{id}` — одинаковые ли cfsParams
Классификация расхождений:
- 🔴 BUG — полигон неправ (лишний/неверный параметр)
- 🟡 LAG — реальный API обогнал (новый параметр, YAML устарел)
- 🟢 BETTER — полигон правильнее реального API (null→"string", HTML entities)
### Юнит-тесты
```bash
pytest tests/test_converter.py -v # 10 тестов from_stands.py
pytest tests/test_state_machine.py -v # 9 тестов apply_effect()
```
---
## 8. Переменные окружения
| Переменная | По умолчанию | Описание |
|-----------|-------------|----------|
| `POLYGON_STANDS` | (пусто) | Стенды: `dev:services/dev,test:services/test,prod:services/prod` |
| `MOCK_AUTH_TOKEN` | (пусто) | Токен для `_mock/*`. Если пусто — auth отключена |
| `MOCK_OP_DELAY` | `0.1` | Задержка операции в секундах |
| `POLYGON_ENDPOINT` | `https://polygon.pythonk8s.dev.nubes.ru` | URL для OpenAPI-спеке |
| `WEB_CONCURRENCY` | `1` | ⛔ НЕ менять — сломает изоляцию состояния |
---
## 9. Ограничения и известные проблемы
| Ограничение | Причина | Обход |
|------------|---------|-------|
| 1 gunicorn-воркер | Состояние в памяти | Не менять WEB_CONCURRENCY |
| Большие ответы (>20KB) обрываются | 1 воркер + сетевой таймаут | Swagger-спека встроена в HTML |
| Нет персистентности | Всё в памяти | Использовать `_mock/reset` в тестах |
| Auth не включена на проде | `MOCK_AUTH_TOKEN` не задан | Добавить в env деплоя |
| YAML обновляется вручную | Нет автотриггера | Запускать `from_stands.py` при изменении терраформа |
| Нет HTTPS-валидации сертификатов | Dev-инструмент | — |
| Swagger загружается с CDN (jsdelivr) | Нет офлайн-версии | — |
---
## 10. Деплой
Polygon — managed Flask на Nubes pythonk8s.
**Procfile:**
```
web: gunicorn site.app:app --bind 0.0.0.0:8000
```
**Процесс редеплоя:**
1. Закоммитить и запушить изменения в `master`
2. В Nubes UI нажать «Redeploy»
3. Nubes: стягивает репу, билдит, запускает gunicorn на порту 8000
4. Healthcheck: `GET /health` → должен вернуть `OK`
**Важно:** YAML-конфиги (`services/`) скоммичены в git.
При редеплое Nubes клонирует репу → YAML уже на месте.
Не нужно запускать `from_stands.py` при деплое.
---
## 11. Чеклист для нового AI-агента
При старте нового чата прочитай:
1. Этот документ
2. `polygon/site/app.py` — точка входа
3. `polygon/site/config/loader.py` — загрузка конфигов
4. `polygon/site/routes/openapi.py` — OpenAPI-спека
5. `polygon/tests/test_api.py` — основной тест-сьют
Ключевые инварианты (НЕ нарушать):
- WEB_CONCURRENCY = 1 всегда
- `state` и `SERVICES` — через LocalProxy, не напрямую
- `_mock/*` требует X-Mock-Auth (если задан MOCK_AUTH_TOKEN)
- `serviceId` валидируется как int > 0
- YAML обновляется через `from_stands.py`, коммитится, редеплоится
@@ -90,3 +90,95 @@
Я хочу чтобы в итоге получился **конкретный план действий**: Я хочу чтобы в итоге получился **конкретный план действий**:
что тестировать, как часто, что считать ошибкой, что — допустимым что тестировать, как часто, что считать ошибкой, что — допустимым
расхождением. расхождением.
---
## Ответ Соннета (2026-08-02)
### 4. dataType: None — первопричина найдена
`dict.get(key, default)` возвращает `default` **только если ключ отсутствует**.
Если в YAML написано `data_type: null` — ключ *есть*, значение — `None`,
дефолт `"string"` не срабатывает.
### Мои ответы
**Q4.1 — data_type: null в YAML?** Проверил — в терраформ-YAML нет
`data_type: null`. Реальный API возвращает `dataType: null` для параметра
`nestedRefExample` (param 396). Полигон возвращает `"string"` — он ПРАВИЛЬНО
применяет дефолт там, где реальный API отдаёт null. Это не баг полигона,
а улучшение.
**Q4.2 — _convert_sub_params?** Та же уязвимость потенциально есть, но не
проявляется — sub_params всегда имеют data_type.
**Q1.1 — частота регенерации YAML?** ВРУЧНУЮ. `from_stands.py` запускается
человеком когда он вспомнит. Никакого cron/webhook.
**Q1.2 — лаг от реального API до YAML?** Непредсказуемо. От часов до недель.
Зависит от того когда кто-то запустит `from_stands.py`.
**Q1.3 — потребитель результатов?** Разработчик. Ему нужно знать «полигон
устарел, перегенери YAML», а не «полигон сломан».
**Q2.1 — дополнительные эндпоинты в реальном API?** Не проверял. Надо
сравнить полный список эндпоинтов.
**Q2.2 — lifecycle поля?** Не сравниваются в текущем compare_test.py. Надо
добавить.
---
## Ответ Соннета — раунд 2
### Три категории расхождений — 👍 принимаю
| Категория | Значение | Реакция |
|---|---|---|
| 🔴 REAL BUG | полигон ≠ реальный API, полигон неправ | FAIL |
| 🟡 LAG | новый параметр в реальном API, нет в полигоне | WARN |
| 🟢 POLYGON BETTER | реальный API отдаёт null/entities, полигон — правильно | INFO |
Для prod 🟡 LAG тоже должен быть заметен.
### Мои ответы — раунд 2
**Q5.1 — сервис есть в полигоне, пропал из реального API?**
Теоретически да — если сервис удалили из реального API, а terraform ещё
не обновили. Это 🔴 REAL BUG и должно быть FAIL. Полигон не должен
эмулировать несуществующие сервисы.
**Q5.2 — HTML-entities?**
Нормализовать при сравнении: `html.unescape()` для real API перед сравнением.
Считать 🟢 POLYGON BETTER, не ошибка.
**Q5.3 — lifecycle поля?**
Проверил — ни реальный API, ни полигон НЕ возвращают `lifecycle` в
`GET /services/{id}`. Сравнивать нечего, вопрос снят.
---
## Ответ Соннета — раунд 3 (финальный)
### Q6.1 — defaultValue, valueList и др.
Реальный API возвращает **29 полей** на каждый cfsParam: `defaultValue`,
`valueList`, `isModifiable`, `isRequired`, `isHidden`, `descr`, `man`,
`regex`, `maxlength`, `minvalue` и т.д. Полигон возвращает подмножество
из ~6-8 полей.
Сравнивать нужно только те поля, которые `from_stands.py` реально генерирует:
`defaultValue`, `valueList`, `isModifiable`, `isRequired`. Остальные либо
отсутствуют в терраформ-YAML, либо не имеют смысла для мока.
### Q6.2 — cfsParamsByOp
Это **внутренний индекс** полигона, не API-эндпоинт. Связь «какие параметры
к какой операции» уже проверяется через `GET /instanceOperations/default/{id}`
— если в ответе правильный набор параметров, значит cfsParamsByOp правильный.
Отдельно сравнивать не нужно.
### Q6.3 — формат вывода
stdout + exit code — достаточно. Разработчик запускает вручную, смотрит
глазами. Файл отчёта переусложнит. Если понадобится история — можно потом.
+80
View File
@@ -0,0 +1,80 @@
# Соннет: полный аудит Polygon v0.5.5 + Swagger + тесты
> Адресат: Claude Sonnet 4.6 (новый чат)
> ⛔ Только анализ и советы в чат. Не редактировать файлы.
---
## Что такое Polygon
Эмулятор REST API облачной платформы Nubes для интеграционных тестов.
Задеплоен на `polygon.pythonk8s.dev.nubes.ru`. Flask 3.0 + gunicorn, 1 воркер.
**3 изолированных стенда:** dev (37 сервисов), test (37), prod (35).
Состояние в памяти, URL: `/dev/api/v1/svc/...`, `/test/...`, `/prod/...`.
В Swagger — выпадайка выбора стенда.
YAML-конфиги сервисов генерируются из терраформ-репы
(`~/tf_provider/generated/{dev,test,prod}/resources_yaml/`) через `from_stands.py`.
**Файлы для анализа:**
- `polygon/site/routes/openapi.py` — OpenAPI 3.1.0 спека (~500 строк)
- `polygon/site/templates/swagger.html` — Swagger UI 5
- `polygon/site/templates/index.html` — главная страница
- `polygon/site/static/style.css` — дизайн-система
- `polygon/site/routes/` — все роуты (7 blueprint'ов)
- `polygon/tests/test_api.py` — 35 smoke-тестов
- `polygon/tests/fuzz_test.py` — 147 фаззинг-тестов
- `polygon/tests/compare_test.py` — сравнение с реальным API
## Что уже сделано
- 17 эндпоинтов, полный CRUD инстансов и операций
- Аутентификация `X-Mock-Auth` для `_mock/*`
- Валидация serviceId (int > 0, не массив, не null)
- 35 smoke + 147 fuzz тестов — 0 реальных багов
- Сравнение с реальным API: YAML ↔ API — 0 расхождений
## Что нужно от тебя
### 1. Swagger/OpenAPI
Открой `polygon/site/routes/openapi.py` и `polygon/site/templates/swagger.html`.
Проанализируй:
- Полнота схем — все ли поля ответов описаны?
- Правильные ли status codes (200/201/400/404/409)?
- Удобство Try it out — example'ы, enum'ы, default'ы
- Группировка тегов — логично ли?
- Авторизация в Swagger UI — правильно ли работает?
- Нет ли лишнего или недостающего?
- Русские описания — понятны ли, не слишком ли длинные?
### 2. Сравнительное тестирование
У нас есть 3 read-only эндпоинта для сравнения:
- `GET /services`
- `GET /services/{id}`
- `GET /instanceOperations/default/{id}`
Какие ещё эндпоинты можно безопасно сравнивать с реальным API?
Что ещё можно проверить не делая мутирующих запросов?
### 3. Дополнительные тесты
Что мы упустили? Какие сценарии, краевые случаи, негативные тесты
стоит добавить? В том числе:
- Тесты через Swagger UI (браузерные)
- Нагрузочные/параллельные
- Специфичные для отдельных сервисов
- Тесты на совместимость с app-autotest
### 4. Замечания по коду/архитектуре
Что можно улучшить не переписывая всё? Любые баги, уязвимости,
потенциальные проблемы которые ты видишь.
---
Формат: свободный. Главное — **конкретные советы** с указанием что и где
менять, а не общие рассуждения.