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

217 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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**`<!-- quality-badge -->` маркер с
эмодзи 🟢/🟡/🔴 + ссылкой на `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` инжектит `<!-- 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` теперь содержит:
```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-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 финального прогона.