# Sprint 25 — autonomous continuous quality monitoring Цель: чтобы существующие фичи постоянно проверялись на работоспособность, и при поломке — автоматическая попытка починить через инцидент-задачу в очередь Server-Claude'а. Старт: 2026-06-08. Исполнитель: Claude Opus 4.7. ## Чек-лист - [x] **1. Hourly smoke watchdog** — `~/quality-watchdog.sh` запускается cron'ом каждый час в минуту 5. 8 проверок (~60 сек): /health/ready, signup→login→/api/me, GET /api/catalog/products, Playwright UI flow (3.1 product create-list-get), /metrics, /hubs/notifications/negotiate, multi-tenant изоляция, performance p95. Падение → Telegram + лог в `~/.fm-watchdog/quality.log`. - [x] **2. Auto-fix loop при двукратном падении** — при consecutive_fail≥2 watchdog создаёт `~/.fm-watchdog/incident-{ts}-{step}.txt` + копирует в очередь Server-Claude'а как `queue/0000-incident-*` (префикс `0000-` гарантирует сортировку в начало). После завершения spirinta Server-Claude возвращается к текущему. - [x] **3. Quality dashboard** — `docs/quality-status.md` рендерится hourly через `scripts/quality-dashboard.py`: текущий статус-эмодзи, список 8 шагов с last-green/last-red, perf baseline (median p95 по последним 10 измерениям), история падений за 7 дней, sparkline 24 последних прогонов. - [x] **4. Multi-tenant smoke 24/7** — создаёт TWO org с уникальными именами `quality-{epoch}-A/B`, заводит товар в A, проверяет что B не видит его ни через GET-by-id (404/403), ни через search (total=0). Leak в multi-tenant изоляции — самый дорогой баг, поэтому ежечасный контроль. - [x] **5. Performance regression hourly** — p95 за 10 повторов для `/api/me`, `/api/catalog/products`, `/api/sales/retail/stats`. Baseline = median последних 10 измерений (robust к шуму). Регрессия = текущий p95 >50% от baseline → alert + (при consecutive≥2) incident. Первые 5 запусков всегда green (собираем стартовую выборку). - [x] **6. Hangfire job: cleanup quality test orgs** — `HousekeepingJobs.PruneQualityTestOrgsAsync` (recurring-id `prune-quality-test-orgs`, default cron `30 2 * * *` UTC). Удаляет org'и `quality-%` старше 24h: динамически по `information_schema` находит все tenant-таблицы с `OrganizationId`, итеративно DELETE'ит с обработкой FK-violation (до 10 проходов), затем чистит `AspNetUserRoles/Tokens/Claims/Logins`, `OpenIddictTokens/Authorizations` по email-шаблону `quality-%@test-fm.local`, и finally `users` + `organizations`. Threshold переопределяется через `Cleanup:QualityTestOrgHours`. - [x] **7. Status badge в README** — `` маркер с эмодзи 🟢/🟡/🔴 + ссылкой на `docs/quality-status.md`. Обновляется через `scripts/quality-dashboard.py`. 🟢 = последний run all green; 🟡 = красные шаги, но <2 consecutive (transient); 🔴 = 2+ consecutive (incident-уровень). - [x] **8. Cron-валидация** — временно поменял на `*/3 * * * *`, поймал 3 cron-запуска подряд (12:42 / 12:45 / 12:48 +05), все 8/8 green. Вернул на hourly (`5 * * * *`). См. `~/.fm-watchdog/quality-cron.log`. ## Журнал ### 2026-06-08 старт Sprint 24 закрыт (8/8). Поехали по quality watchdog. ### 2026-06-08 quality-watchdog.sh (#1) Главный скрипт ~16 КБ: - shell-helpers `state_get`/`state_set`/`mark_red`/`mark_green`/`create_incident`. - Все шаги логируют в `~/.fm-watchdog/quality.log` + state в `~/.fm-watchdog/quality-state.json` (per-step `consecutive_fail`, `last_green`, `last_red`, `last_detail`). - Telegram-нотификация на каждый red-шаг. Если token/chat не настроены (default на dev-vm) — просто лог без падения. - Лог ротируется при превышении 1 МБ. Подобранные через debug refs: - `/api/catalog/units-of-measure` (не `/api/refs/units`) - `/api/catalog/product-groups` (обязательное поле ProductGroupId) - POST product требует `barcodes: [{code, type, isPrimary}]` + price `[{priceTypeId, amount, currencyId}]` где priceTypeId = isRequired-row из `/api/catalog/price-types`. ### 2026-06-08 auto-fix loop (#2) `create_incident` пишет 2 файла: один в `~/.fm-watchdog/incident-{ts}-{step}.txt` (для архива/обзора инженером), другой в `~/.fm-watchdog/queue/0000-incident-{ts}-{step}.txt`. Префикс `0000-` критичен — `fm-watchdog.sh` ротирует очередь через `ls | sort | head -1`, так что инцидент-задача рулит раньше любой плановой работы. Проверил через симуляцию (STAGE_URL=https://nonexistent.example.invalid): 1-я итерация — yellow, 2-я — RED + 7 incident-файлов в очереди (по одному на каждый упавший шаг, кроме ui_flow — там node_modules + playwright всё равно работают). ### 2026-06-08 dashboard + history (#3) `scripts/quality-dashboard.py` (Python 3.11+, no deps) рендерит markdown из `quality-history.jsonl` (1 JSONL-строка на прогон, append-only, trimmed до 400 последних = ~2 недели hourly). Содержит: - Текущий статус-эмодзи. - Таблицу 8 шагов: `статус | last-change | consecutive_fail`. - Performance baseline (median p95 по 10 последним измерениям). - История за 7 дней + green-ratio %. - Sparkline последних 24 прогонов (🟢/🔴). `updateReadmeBadge` инжектит ` ... ` маркер в README между h1 и existing badges. ### 2026-06-08 multi-tenant scenario (#4) 2 signup'a с unique `quality-{epoch}-A/B`, токены через `/connect/token`, POST product в A → GET в B должен вернуть 404/403 + search в B должен дать total=0. Хитрость: B видит "Все товары" (root group) **своей** организации (создан bootstrap'ом при signup), не A's; это правильно. Leak бы означал, что product создан вне tenant'a A. ### 2026-06-08 performance baseline (#5) Первая версия использовала `min(old, new)` как baseline — после первого быстрого замера (cold connection) baseline=200ms, и любой warm-replay 295ms давал +47% и срабатывал alert. Переделал на median последних 10 измерений + первые 5 запусков всегда green (warm-up). Это сглаживает шум и держит alert на действительно sustained регрессиях. Файл `~/.fm-watchdog/quality-perf-baseline.json` теперь содержит: ```json { "median": { "_api_me": 245, ... }, "samples": { "_api_me": [216, 245, 297, ...], ... } } ``` ### 2026-06-08 PruneQualityTestOrgs job (#6) Добавил метод в `HousekeepingJobs` + регистрация в `HangfireJobsConfigurator` под id `prune-quality-test-orgs`, cron `30 2 * * *` UTC. Реализация: 1. `_db.Organizations.IgnoreQueryFilters().Where(Name LIKE 'quality-%' AND CreatedAt < threshold)` → candidate ids. 2. `DO $do$ ... END $do$` блок с `org_ids uuid[] := ARRAY[...]`. 3. Итеративный FOR pass IN 1..10 LOOP по `information_schema.columns WHERE column_name = 'OrganizationId'` — каждая итерация пытается DELETE из каждой tenant-таблицы; при `foreign_key_violation` пропускает (следующий проход уберёт parent запись). Реально хватает 2 проходов (нашёл employees ↔ employee_roles). 4. AspNetUserRoles/Tokens/Claims/Logins/OpenIddictTokens/Authorizations через email-pattern `quality-%@test-fm.local`. 5. `users` + `organizations` отдельным шагом в конце. Dry-run на stage (24 кандидата, ROLLBACK): pass 1 remaining=2, pass 2 remaining=0, итог 0 orgs в `quality-%` — работает. ### 2026-06-08 README badge (#7) `updateReadmeBadge` инжектит маркированный блок: ```html 🟢 **Quality:** [`docs/quality-status.md`](docs/quality-status.md) ``` Каждый запуск watchdog'a обновляет эмодзи по результату последнего прогона. ### 2026-06-08 cron-валидация (#8) Crontab: `5 * * * * /home/nns/quality-watchdog.sh >> ~/.fm-watchdog/quality-cron.log 2>&1`. Для быстрого подтверждения временно поменял на `*/3 * * * *`, поймал 3 запуска подряд (12:42 / 12:45 / 12:48 +05), все 8/8 green. Восстановил hourly. ## Архитектура ``` ~/quality-watchdog.sh (cron 5 * * * *) ↓ запускает 8 проверок ├── HTTP curl (steps 1, 2, 3, 5, 6, 7, 8) └── Playwright @smoke (step 4, использует tests/regression/flows/03) ↓ итог ├── ~/.fm-watchdog/quality.log (append-only) ├── ~/.fm-watchdog/quality-state.json (per-step consecutive_fail) ├── ~/.fm-watchdog/quality-history.jsonl (1 line/run, 400 lines max) ├── ~/.fm-watchdog/quality-perf-baseline.json (median + samples) └── docs/quality-status.md + README badge (через quality-dashboard.py) ↓ если consecutive_fail ≥ 2 ├── ~/.fm-watchdog/incident-{ts}-{step}.txt └── ~/.fm-watchdog/queue/0000-incident-{ts}-{step}.txt (для fm-watchdog.sh ротации) Hangfire job `prune-quality-test-orgs` (cron 30 2 * * * UTC): - находит orgs `quality-%` старше 24h - итеративно DELETE'ит из всех tenant-таблиц - чистит Identity+OpenIddict по email-pattern - DELETE users + organizations ``` ## Что найдено и зафиксировано - **Stage perf шумит на старте**: первые 1-2 запроса к /api/sales/retail/stats доходят до 400ms (cold DB cache), затем 200-250ms steady. Поэтому baseline стал median-based, не min-based. - **POST product требует non-trivial payload**: barcodes (массив объектов с code/type/isPrimary), prices (массив с priceTypeId/amount/currencyId), productGroupId — все обязательные. Зафиксил в watchdog: вытягивает refs из дефолтных значений orga через `/api/catalog/{units,product-groups,price-types,currencies}`. - **FK dependency между employees и employee_roles**: первый DELETE на `employee_roles` валится на employees.RoleId FK. Решил итеративным ретраем (10 проходов) — гибкая стратегия, не привязанная к ручному порядку, новые tenant-таблицы автоматом подцепятся. ## Метрики | | До Sprint 25 | После | Δ | |---|---|---|---| | **Watchdog scripts** | `fm-watchdog.sh` + `nightly-verify.sh` + `nightly-perf-check.sh` | + `quality-watchdog.sh` + `quality-dashboard.py` | +2 | | **Cron entries** | 2 | 3 | +1 | | **Hangfire recurring jobs** | 11 | 12 | +1 (prune-quality-test-orgs) | | **Hourly smoke шаги** | (нет hourly smoke) | 8 шагов | new | | **Docs (.md в `docs/`)** | 60 | 62 | +2 (quality-status.md, sprint25-progress.md) | | **README badges** | 5 | 6 | +1 (quality 🟢/🟡/🔴) | | **Auto-fix integration** | (нет) | incident → fm-watchdog queue | new | ## Итог 8/8 ✓. 3 cron-trigger'd runs все green. `~/.fm-watchdog/DONE` создан после ratiosit's финального прогона.