# История разработки app-autotest ## Инфраструктура - **Кластер**: `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_SSLMODE - `db/init_db.py`: проверка `DB_USER` вместо `DATABASE_URL` - `secrets/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=1` - `initServices()` — загрузка из `/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 часа - `_ts` timestamp в каждой записи ## v1.0.99 (28.07.2026) — фикс: Flask-прокси в фоновом потоке ### Найденные ошибки 1. `get_token_info()` → `request.cookies` → в потоке нет request-контекста → RuntimeError 2. `current_app.config.get("VERSION")` → Flask-proxy → та же проблема 3. `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_blueprint - `CREATE 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 шагов) 1. `api/auth.py` — вынести get_token/get_client/get_client_id/get_stand 2. Заменить дублирующиеся функции в main.py и api_test.py 3. Cookie `httponly=True, samesite='Strict'` 4. `_op_results` TTL/очистка 5. JS вынести в `/static/app.js` + `window.APP` 6. Динамические сервисы из `/api/services` 7. `db/pool.py` + `db/init_db.py` (PostgreSQL) 8. Запись истории в runs при завершении операции 9. `/api/history` эндпоинт + UI ## v1.0.89 (27.07.2026) — Sonnet Round 1: архитектурный аудит - Cloud-first подход с tracker-fallback - HTML-экранирование в JS **🔴 Критические находки:** 1. `_op_results` dict растёт бесконечно — утечка памяти, нужен TTL/очистка 2. Cookie токена без `httponly` и `samesite` 3. `/api/log` без проверки авторизации **🟡 Дублирование:** 1. `_client_id()` — идентичный код в main.py и api_test.py 2. `_client()` — разное поведение в main.py и api_test.py 3. Инлайн `