food-market/docs/sprint25-progress.md
nns 019c57ae3b
Some checks are pending
Auto-tag / Create date-tag (push) Waiting to run
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
Docker API / Build + push API (push) Waiting to run
Docker API / Deploy API on stage (push) Blocked by required conditions
feat(s25): autonomous continuous quality monitoring (8/8)
Hourly smoke watchdog + auto-fix loop + dashboard + multi-tenant guard
+ perf regression + cleanup job + README badge.

1. ~/quality-watchdog.sh (cron 5 * * * *) — 8 checks (~60s):
   /health/ready, signup→login→/api/me, GET products, Playwright UI
   smoke (3.1 product CRUD), /metrics format, /hubs/notifications
   negotiate with token, multi-tenant isolation, perf p95.
2. Auto-fix loop: 2× consecutive red → ~/.fm-watchdog/incident-*.txt
   + queue/0000-incident-* to bump it ahead of Server-Claude's
   sprint queue. fm-watchdog.sh sees prefix 0000- as next.
3. scripts/quality-dashboard.py — renders docs/quality-status.md
   (current emoji, 8-step table, perf baseline, 7-day history,
   24-run sparkline) + injects README badge 🟢/🟡/🔴.
4. Multi-tenant smoke: signup 2 orgs `quality-{epoch}-A/B`, create
   product in A, verify B sees 404/403 + total=0.
5. Perf regression: p95 over 10 reqs for /api/me, products,
   sales/retail/stats. Baseline = median of last 10 samples
   (robust to noise). >50% from baseline → alert. First 5 runs
   always green (warm-up).
6. HousekeepingJobs.PruneQualityTestOrgsAsync (cron 30 2 * * * UTC):
   finds orgs `quality-%` older than 24h, dynamically scans
   information_schema for tables with OrganizationId, iteratively
   DELETEs with FK-violation retry (up to 10 passes), then cleans
   AspNetUser*/OpenIddict* by email pattern `quality-%@test-fm.local`,
   finally users + organizations.
7. README badge: <!-- quality-badge --> marker updated each run.

Validated: stage deploy ✓, Hangfire job registered ✓, dry-run SQL on
24 stage candidates → 0 remaining ✓, 3 cron-triggered runs all 8/8
green (12:42/12:45/12:48 +05) ✓.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-08 12:50:35 +05:00

13 KiB
Raw Blame History

Sprint 25 — autonomous continuous quality monitoring

Цель: чтобы существующие фичи постоянно проверялись на работоспособность, и при поломке — автоматическая попытка починить через инцидент-задачу в очередь Server-Claude'а.

Старт: 2026-06-08. Исполнитель: Claude Opus 4.7.

Чек-лист

  • 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.

  • 2. Auto-fix loop при двукратном падении — при consecutive_fail≥2 watchdog создаёт ~/.fm-watchdog/incident-{ts}-{step}.txt + копирует в очередь Server-Claude'а как queue/0000-incident-* (префикс 0000- гарантирует сортировку в начало). После завершения spirinta Server-Claude возвращается к текущему.

  • 3. Quality dashboarddocs/quality-status.md рендерится hourly через scripts/quality-dashboard.py: текущий статус-эмодзи, список 8 шагов с last-green/last-red, perf baseline (median p95 по последним 10 измерениям), история падений за 7 дней, sparkline 24 последних прогонов.

  • 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 изоляции — самый дорогой баг, поэтому ежечасный контроль.

  • 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 (собираем стартовую выборку).

  • 6. Hangfire job: cleanup quality test orgsHousekeepingJobs.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.

  • 7. Status badge в README<!-- quality-badge --> маркер с эмодзи 🟢/🟡/🔴 + ссылкой на docs/quality-status.md. Обновляется через scripts/quality-dashboard.py. 🟢 = последний run all green; 🟡 = красные шаги, но <2 consecutive (transient); 🔴 = 2+ consecutive (incident-уровень).

  • 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 инжектит <!-- quality-badge --> ... <!-- /quality-badge --> маркер в 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 теперь содержит:

{
  "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 инжектит маркированный блок:

<!-- quality-badge --> 🟢 **Quality:** [`docs/quality-status.md`](docs/quality-status.md) <!-- /quality-badge -->

Каждый запуск 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 финального прогона.