31 KiB
31 KiB
История разработки app-autotest
v1.1.2 (28.07.2026) — refSvcId: выпадающие списки для cross-service параметров
get_params.py+api_test.py: параметры включаютrefSvcIdиз API- Фронт:
refSvcId→fetch('/api/instances/list')→ фильтр поserviceId→<select>сdisplayName - Универсально: S3, K8s-кластер, любой сервис — data-driven
Инфраструктура
- Кластер:
iot-naeel(resourceRealm K8s) - URL:
https://atest.pythonk8s.dev.nubes.ru - Деплой: git push → managed service pythonk8s → gunicorn
- PostgreSQL:
foriot(509145c3-...), PG 17,iot-naeel
v1.0.93 (28.07.2026) — отдельные DB_* переменные
Что изменилось
db/pool.py:_dsn()собирает DSN из отдельных переменных: DB_HOST, DB_PORT, DB_NAME, DB_USER, DB_PASSWORD, DB_SSLMODEdb/init_db.py: проверкаDB_USERвместоDATABASE_URLsecrets/pg-foriot.md: полный jsonEnv для pythonk8s (6 DB-переменных + 2 существующие)app.py: NUBES_API_ENDPOINT по умолчанию TEST
v1.0.92 (27.07.2026) — Steps 5-9: динамические сервисы + PostgreSQL история
Шаг 5: JS → /static/app.js
- JS вынесен из index.html в отдельный файл
window.APP = {version, stand, hasUserToken}— передача Jinja2-переменных
Шаг 6: Динамические сервисы
currentSvcIdвместо хардкодаSVC_ID=1initServices()— загрузка из/api/servicesпри старте
Шаг 7-8: PostgreSQL
db/pool.py— ThreadedConnectionPool (lazy-init, 1-5 соединений)db/init_db.py— таблицаrunsс индексами (идемпотентно)db/save_run.py— запись результатов в БДrequirements.txt— добавленpsycopg2-binary
Шаг 9: /api/history
- GET
/api/history— последние 50 записей (фильтр по client_id + stand)
v1.0.90 (27.07.2026) — Steps 1-4: auth.py + TTL + cookie
Шаг 1-2: api/auth.py
- Новый модуль:
get_token(),get_client(),get_client_id(),get_stand(),get_token_info(),get_token_masked() - Убраны дубликаты из main.py и api_test.py
Шаг 3: Cookie httponly + samesite
set_cookie(..., httponly=True, samesite="Strict")
Шаг 4: _op_results TTL
- Максимум 500 записей, удаление старше 1 часа
_tstimestamp в каждой записи
v1.0.99 (28.07.2026) — фикс: Flask-прокси в фоновом потоке
Найденные ошибки
get_token_info()→request.cookies→ в потоке нет request-контекста → RuntimeErrorcurrent_app.config.get("VERSION")→ Flask-proxy → та же проблемаexcept: passглушил ошибки → записи молча не сохранялись
Исправление
user_emailиapp_versionполучаются вapi_test()(где есть request) и передаются параметрами в_finish_op→save_run- Добавлено логирование ошибок в
save_run(больше не глухоеpass)
Урок
py_compile проверяет только синтаксис. Flask-прокси (request, current_app, g, session)
не работают в фоновых потоках. Все нужные значения — до threading.Thread параметрами.
v1.0.89 (27.07.2026) — Sonnet Rounds 1-3 + документирование
1. PostgreSQL connection string
os.getenv("DATABASE_URL")— никакого service discovery- Инжектится через
jsonEnvпараметры pythonk8s-сервиса (как NUBES_API_TOKEN) - Порядок: создать PostgreSQL-инстанс в Nubes → взять host/port/user/pass из state_out → собрать DSN → jsonEnv → деплой
2. Миграции при деплое
init_db()на уровне модуля в app.py, до register_blueprintCREATE TABLE/INDEX IF NOT EXISTS— идемпотентно, каждый воркер выполнит независимо- PostgreSQL обрабатывает конкурентный DDL корректно
- Никаких ручных шагов при деплое
DROP COLUMN— не идемпотентно, осторожно
3. Multi-service UI (map-fixed)
dataDescriptorуже в API-ответе — инфраструктура готоваmap(свободный): textarea + JSON-валидация (как сейчас)map-fixed: таблица подполей с отдельными input/select из dataDescriptor- Бэкенд не меняется — paramValue всегда строка
- Изменения только во фронте: showParams() + сбор подполей в JSON перед отправкой
4. Тесты
- pytest для чистых функций без моков:
_find_uid,_uid_from_location,_with_prefix, слияние params, tracker - Структура:
tests/conftest.py,test_utils.py,test_tracker.py,test_get_params.py - Роуты с Flask test client + mock HttpClient — отложить (высокая стоимость мокирования)
v1.0.89 (27.07.2026) — Sonnet Round 2: PostgreSQL, auth.py, детальный план
Результаты анализа
Sonnet дал конкретные ответы на все 7 вопросов Round 2:
1. PostgreSQL схема:
- Таблица
runsс JSONB-полямиparamsиstages - 3 индекса:
(client_id, stand, created_at DESC),(instance_uid),(created_at) - Stages в JSONB (не отдельная таблица), instances не нужны (API — source of truth)
- Чувствительные params маскировать при сохранении
2. Connection pool:
psycopg2.pool.ThreadedConnectionPool— минимум зависимостей- Lazy-init в
db/pool.py— безопасно для fork-модели gunicorn flask.g+@app.teardown_appcontextдля возврата соединений
3. Миграции:
db/init_db.pyсIF NOT EXISTS— идемпотентно, авто-применение при старте- Alembic — когда 3+ таблицы или нужен rollback (сейчас не нужно)
4. Трекер vs PostgreSQL:
- НЕ заменять трекер на БД — это разные слои
- Трекер = кеш (секунды), PostgreSQL = история (недели)
- Сетевое обращение к БД на критическом пути CREATE недопустимо
5. auth.py:
- Явные функции вместо
before_request— проще, нет скрытых зависимостей - Одна реализация
get_client_id()вместо двух копий - Кеширование
token → endpoint(опционально, TTL 5 мин)
6. Динамические сервисы:
/api/servicesиз API → фильтр поenabled: trueиз config.yaml- UI: заменить хардкод
SVC_ID=1на динамическую загрузку вselectService()
7. JS в файл:
- Классический скрипт, один файл
app.js(300 строк — не нужно ES modules) window.APP = {...}черезtojsonфильтр Jinja2 для передачи переменных
Итоговый порядок реализации (9 шагов)
api/auth.py— вынести get_token/get_client/get_client_id/get_stand- Заменить дублирующиеся функции в main.py и api_test.py
- Cookie
httponly=True, samesite='Strict' _op_resultsTTL/очистка- JS вынести в
/static/app.js+window.APP - Динамические сервисы из
/api/services db/pool.py+db/init_db.py(PostgreSQL)- Запись истории в runs при завершении операции
/api/historyэндпоинт + UI
v1.0.89 (27.07.2026) — Sonnet Round 1: архитектурный аудит
- Cloud-first подход с tracker-fallback
- HTML-экранирование в JS
🔴 Критические находки:
_op_resultsdict растёт бесконечно — утечка памяти, нужен TTL/очистка- Cookie токена без
httponlyиsamesite /api/logбез проверки авторизации
🟡 Дублирование:
_client_id()— идентичный код в main.py и api_test.py_client()— разное поведение в main.py и api_test.py- Инлайн
<style>дублируетstyle.css
🟡 Захардкодено:
SVC_ID=1и список сервисов в HTML — хотя/api/servicesсуществуетcreate_client()в main.py vs_client()в api_test.py — разные подходы
Приоритетный план исправлений:
- 🔴
_op_results— добавить очистку/TTL - 🔴 Cookie
httponly=True, samesite='Strict' - 🟡 Вынести
_client_id()/get_token()в общий модульapi/auth.py - 🟡 Загрузка сервисов из
/api/servicesвместо хардкода - 🟡 Вынести JS в
/static/app.js - 🟢 SQLite история запусков
- 🟢
/api/logпроверка токена
Создан запрос Round 2
Файл: DOCS/sonnet-architecture-review-v1.0.89-r2.md — уточняющие вопросы по реализации.
v1.0.89 (27.07.2026) — документация и комментарии кода
v1.0.87 (27.07.2026) — документация: ARCHITECTURE.md переписан, legacy-доки помечены
v1.0.84 (27.07.2026) — лог-панель скрыта по умолчанию
Что изменилось
- Лог-панель теперь
display:none, показывается кнопкойlog(правый нижний угол). - При скрытой панели
/api/logне поллится.
v1.0.83 (27.07.2026) — HTML-escape + JSON-валидация map-полей
Что изменилось
_esc(s)— HTML-экранирование значений ("→",&→&,<→<).validateJson(el, quiet)— проверкаJSON.parse()приonblur(красная рамка + текст ошибки).- Batch-проверка всех map-полей перед отправкой — ошибка → запрос не уходит.
Что это чинит
- Поломку HTML при значениях с кавычками (напр.
{"f":1}). - Отправку битого JSON в API.
v1.0.82 (27.07.2026) — displayName из API для non-create операций
Что изменилось
_get_instance_display_name(client, uid)—GET /instances/{uid}→displayName.- Используется в не-create ветке
POST /api/testвместоinstance_uidкак fallback.
Что это чинит
- UUID вместо имени инстанса в финальном статусе.
v1.0.81 (27.07.2026) — get_params.py: текущие значения из state.params
Что изменилось
- Новый файл
site/operations/get_params.py— независимый модуль. get_params_with_current_values(client, op_id, instance_uid):GET /instances/{uid}→state.params(текущие значения)GET /instanceOperations/default/{opId}→ шаблон- Слияние:
defaultValue = state.params["код"] ?? template.defaultValue
- Убран
previewOpUidполностью (и из бэкенда, и из фронтенда).
v1.0.80 (27.07.2026) — попытка fix через svcOperationId (НЕ СРАБОТАЛО)
v1.0.79 (27.07.2026) — файловый лог для multi-worker
Что изменилось
_log()пишет в/tmp/app-autotest.logподfcntl.flock(вместо in-memory deque)./api/logчитает из файла последние 200 строк.- Ротация при 512 КБ.
- UI-панель логов (180px, автоскролл, поллинг 2s).
v1.0.77 (27.07.2026) — debug-логирование в api_params
v1.0.76 (27.07.2026) — modify: preview-операция для получения paramValue (НЕ СРАБОТАЛО)
v1.0.74 (27.07.2026) — version bump for push
Что изменилось
- В
site/templates/index.htmlcreate-операция отделена от обычных операций: CREATE использует фиксированный префиксautotest-, а non-create берёт имя выбранного инстанса. - Из non-create ветки убран случайный fallback
tutи общий create-styledisplayName. - В
site/routes/api_test.pybackend больше не подставляет create-имя по умолчанию для обычных операций и возвращаетdisplayNameдля финального статуса. - В
site/routes/main.pyсписок autotest-инстансов теперь строится cloud-first без старой склейки через map по имени; tracker остался только как временный fallback, если cloud ещё не вернул новый инстанс.
Что это чинит
- Путаницу между CREATE и обычными операциями.
- Ситуацию, когда в UI появлялся лишний
tut. - Дубли
running/creatingна одном autotest-инстансе. - Неясный финальный статус, где было видно операцию, но не было понятно, над каким инстансом она выполнялась.
v1.0.72 (27.07.2026) — service list keeps one autotest row per displayName
Что изменилось
- В
site/routes/main.pyсервисный список теперь cloud-first и не склеивает разные строки в одну map. - Tracker остаётся только как fallback, если cloud ещё не вернул конкретный autotest-инстанс.
Что это чинит
- Дубли
running/creatingна одном и том же autotest-инстансе.
v1.0.71 (27.07.2026) — finished status shows instance name
Что изменилось
- В
site/templates/index.htmlфинальная строка послеOK/FAILтеперь показывает имя инстанса. - Имя операции и длительность остаются рядом, отдельно от имени инстанса.
Что это чинит
- Ситуацию, когда после завершения delete/modify было видно только название операции, но не ясно, над каким инстансом она выполнялась.
v1.0.70 (27.07.2026) — dedupe autotest instances by displayName
Что изменилось
- В
site/routes/main.pyсервисный список теперь склеивает записи поdisplayName. - Если cloud уже отдал
running, он выигрывает у tracker-fallbackcreating.
Что это чинит
- Дубли одной и той же autotest-записи после CREATE, когда в списке появлялись и
running, иcreating.
v1.0.69 (27.07.2026) — autotest prefix fixed in CREATE field
Что изменилось
- В
site/templates/index.htmlпрефиксautotest-отображается отдельным фиксированным текстом. - Рядом с ним остаётся только редактируемый суффикс имени.
- При отправке CREATE имя собирается как
autotest-+ введённый хвост.
Что это чинит
- Ситуацию, когда весь
displayNameпоказывался в одном редактируемом поле. - Риск случайно стереть обязательный префикс
autotest-.
v1.0.68 (27.07.2026) — operation name shown after OK + muted operation buttons
Что изменилось
- В
site/templates/index.htmlфинальный статус теперь показывает имя операции рядом со временем. - Кнопки операций получили спокойные неброские оттенки по типу операции.
Что это чинит
- Неясность после завершения операции, когда было видно только
OKи время без указания, что именно выполнялось. - Слишком кислотный вид кнопок операций.
v1.0.67 (27.07.2026) — autotest instances are shown by cloud prefix
Что изменилось
- В
site/routes/main.pyсписок инстансов сервиса больше не отфильтровывается через/tmp/instances.json. - Теперь в таблицу попадают все облачные инстансы с префиксом
autotest-. - Трекер оставлен только как fallback, если новый autotest-инстанс ещё не успел появиться в ответе облака.
Что это чинит
- Ситуацию, когда autotest-инстанс уже есть в облаке, но не показывается в таблице из-за отсутствия записи в json.
v1.0.66 (27.07.2026) — backend status resolver for operations
Что изменилось
- В
site/routes/main.pyдобавлен канонический резолвер статуса инстанса для/api/operations/<svc_id>. - Backend теперь отдаёт
statusдля инстансов, а UI читает именно его вместо угадывания поexplainedStatus. - В
site/templates/index.htmlубрано раннее отображение «Готово» во время выполнения операции.
Что это чинит
- Путаницу между
creating,runningи фактическим статусом в UI. - Ситуацию, когда фронтенд сам интерпретировал статусы и показывал не то состояние.
v1.0.65 (27.07.2026) — cloud list pagination fix
Что изменилось
- В
site/operations/get_instances.pyпагинация списка инстансов теперь останавливается по размеру страницы, а не поtotal. - Это убирает ситуацию, когда инстанс уже создан в облаке, но приложение не дочитало следующую страницу и показывает
creating. - В
DOCS/ARCHITECTURE.mdиsite/app.pyсинхронизирована версия1.0.65.
Что это чинит
- Ложный
creatingдля уже существующего инстанса, если он попал не на первую страницу/instances.
v1.0.64 (27.07.2026) — header version + editable displayName placeholder
Что изменилось
- В
site/routes/main.pyверсия теперь гарантированно попадает вconfig.VERSIONдаже послеload_config(). - В
site/templates/index.htmlполеdisplayNameостаётся редактируемым, но автотест-имя показывается как placeholder. - При CREATE пользователь может ввести своё имя, а backend всё равно добавит
autotest-и проверит уникальность. - В
DOCS/ARCHITECTURE.mdзафиксированы оба изменения.
Что это чинит
- Пропадающую версию в шапке.
- Неправильное поведение поля
displayName, когда оно выглядело как готовое значение вместо подсказки.
v1.0.63 (27.07.2026) — autotest namespace for instances
Что изменилось
- В
site/templates/index.htmlCREATE теперь генерируетdisplayNameс префиксомautotest-и проверяет его на уникальность среди текущих инстансов. - В
site/routes/api_test.pybackend тоже нормализуетdisplayNameс этим префиксом и, если нужно, добавляет суффикс для уникальности перед созданием в облаке. - В
site/routes/main.pyсписок инстансов фильтруется по namespaceautotest-, чтобы в UI попадали только тестовые инстансы приложения. - В
DOCS/ARCHITECTURE.mdзафиксирован переход на namespace-based фильтрацию.
Что это чинит
- Дубли
displayNameпри CREATE. - Чужие инстансы в списке.
- Зависимость от локального трекера как от основного признака принадлежности.
v1.0.62 (27.07.2026) — immediate visual refresh after CREATE
Что изменилось
- В
site/templates/index.htmlпосле завершения CREATE список инстансов перерисовывается полностью. refreshInstances()больше не зависит от DOM-diff и сразу ставит новый инстанс в список.DOCS/ARCHITECTURE.mdиsite/app.pyсинхронизированы на1.0.62.
Что это чинит
- После появления кнопки «Готово» созданный инстанс должен сразу быть виден в списке.
v1.0.61 (27.07.2026) — immediate list rebuild after CREATE
Что изменилось
- В
site/templates/index.htmlrefreshInstances()теперь всегда полностью перерисовывает список инстансов после завершения операции. - После
OKновый инстанс должен появляться в списке сразу, без зависимости от DOM-diff и без перезагрузки страницы. - В
DOCS/ARCHITECTURE.mdиsite/app.pyсинхронизирована версия1.0.61.
Что это чинит
- Убрана ситуация, когда после CREATE статус уже
OK, но в UI инстанс ещё не виден.
v1.0.60 (27.07.2026) — full list rerender after CREATE
Что изменилось
- В
site/templates/index.htmlrefreshInstances()теперь полностью перерисовывает список инстансов, а не пытается вставить только diff. - После завершения CREATE новый инстанс должен появляться в списке сразу после
OK. - В
DOCS/ARCHITECTURE.mdдобавлен отдельный шаг про полную перерисовку списка.
Что это чинит
- Снята зависимость от частичного DOM-diff-а, из-за которого созданный инстанс мог не появляться сразу.
v1.0.59 (27.07.2026) — restore tracked instance after reload
Что изменилось
- В
site/routes/main.pytracked-инстансы, которых Nubes уже не отдает в ответе после reload, снова добавляются в список из/tmp/instances.json. - В
site/templates/index.htmlстатус CREATE теперь показывает имя инстанса, а не толькоcreate. - В
DOCS/ARCHITECTURE.mdзафиксирован новый шаг восстановления списка после F5.
Что это чинит
- После F5 инстанс не пропадает из UI, если он есть в локальном трекере.
- В статусе выполнения видно, по какому displayName идет CREATE.
v1.0.58 (27.07.2026) — version bump
- Поднята версия проекта до
1.0.58. - Синхронизированы заголовки в
site/app.pyиDOCS/ARCHITECTURE.md.
v1.0.57 (27.07.2026) — UI hardening + history sync
Что изменилось
- В
site/templates/index.htmlфинальное состояние кнопки вынесено вsetFinishedState(), чтобы не держать логику завершения операции в двух местах. - После завершения операции кнопка «Готово» теперь гарантированно сбрасывает обработчик через
btn.onclick = null. - Повторный CREATE через старый обработчик больше не воспроизводится.
displayNameбольше не теряется в нормальном сценарии завершения CREATE.- В
site/app.pyверсия поднята до1.0.57. - В
DOCS/ARCHITECTURE.mdотмечено закрытие бага с повторным CREATE и синхронизирован статус поdisplayName.
Зачем это было сделано
- Убрать хрупкость вокруг состояния кнопки после завершения операции.
- Привести документацию и версию к текущему состоянию кода.
v1.0.54 (27.07.2026) — Аудит #3: 4 бага исправлено
Баг #1: _client() не автоопределял стенд (КРИТИЧЕСКИЙ)
- Где:
api_test.py:_client()— использовалcurrent_app.config["NUBES_API_ENDPOINT"](raw env var) - Симптом: main.py автоопределял стенд для главной, но POST /api/test падал с 401 если токен от другого стенда
- Исправление:
detect_endpoint()вынесен вapi/http_client.py, используется в_client() - Как пропустили: анализировал main.py и api_test.py в изоляции, не сравнил
Баг #2: _find_uid() возвращал не тот UUID (КРИТИЧЕСКИЙ)
- Где:
api_test.py:_find_uid()— итерировал по ВСЕМ значениям dict, искал 36-символьную строку - Симптом:
/instanceOperationsвозвращает обаinstanceUid+instanceOperationUid, функция брала первый попавшийся →runшёл на неправильный URL - Исправление: ищет по конкретным ключам:
instanceOperationUid→instanceUid→uid→Uid - Как пропустили: не проверил реальный формат ответа API
Баг #3: Сироты при ошибке params (СРЕДНИЙ)
- Где:
api_test.py: порядок —POST /instances→POST /instanceOperations→ params loop →tracker_add - Симптом: если params падает — инстанс и операция созданы в Nubes, трекер пуст → сироты
- Исправление:
tracker_addвызывается ДО params loop, сразу после полученияinstanceUid - Как пропустили: не трассировал линейно порядок вызовов
Баг #4: flock без таймаута (НИЗКИЙ)
- Где:
tracker.py:_locked_read/_write—fcntl.flock(fd, LOCK_EX)без LOCK_NB - Симптом: при зависшем процессе с локом все воркеры блокируются навсегда
- Исправление:
_acquire_lock()сLOCK_EX | LOCK_NB+ retry до 2 секунд - Как пропустили: новый код flock не перечитал с нуля после добавления
Архитектурное: detect_endpoint вынесен в http_client.py
- Раньше был в main.py → недоступен для api_test.py
- Теперь в
api/http_client.py→ оба модуля используют
v1.0.53 (27.07.2026) — Файловый трекер с fcntl.flock
- In-memory dict заменён на
/tmp/instances.jsonсfcntl.LOCK_EX - Multi-worker gunicorn: все воркеры читают/пишут один файл под локом
v1.0.52 (27.07.2026) — _tmpl() helper
- Все переменные шаблона передаются во всех трёх return-путях index()
- Исправлен UndefinedError при «Выйти»
v1.0.51 (27.07.2026) — Автоопределение стенда + UnboundLocalError
detect_endpoint()в main.py: пробует токен против dev и test APIinstance_groups = {}инициализирован до if_finish_opобёрнут в полный try/except
v1.0.50 (27.07.2026) — showStages + api_operations fix
showStages(): dtFinish вместо isSuccessful для ⏳/✅/❌api_operations(): не фильтровать "not created", добавлять tracked-сирот
v1.0.49 (27.07.2026) — In-memory tracker + refreshInstances
- Трекер: in-memory dict вместо файла
/tmp/instances.json refreshInstances()добавляет новые инстансы в DOM (не только бейджи)selectService()заменён наrefreshInstances()после OK
v1.0.48 (27.07.2026) — tracker_add синхронно
tracker_addвapi_test()доthreading.Thread- Лог
is_okв_finish_op - Flexbox-кнопки горизонтально
v1.0.47 (27.07.2026) — displayName fix
- displayName захватывается до очистки формы
- tracker_add до _op_results[OK]
- autotest-1 в _INITIAL
v1.0.46 (27.07.2026) — _finish_op signature
- Добавлены op_name, svc_op_id в _finish_op()
v1.0.45 и ранее
- Базовый CREATE/MODIFY/SUSPEND/DELETE/RESUME/REDEPLOY
- UI с этапами, поллинг, params форма
- Множественные баги CREATE flow (документированы в DOCS/api-create-flow.md)